MCP — Model Context Protocol
The standard for plugging tools into agents. Write your integration once; every MCP-compatible client can use it.
What you'll learn
- What MCP is and why it exists
- The architecture — servers, clients, and the three primitives
- How a minimal MCP server looks in Python
- Why this matters for the agent ecosystem
Before you start
A platform team ships a “look up an order” tool for their support agent. Then sales wants it in their internal chat assistant. Then an engineer wants it in Cursor. Then someone files a ticket asking why the Claude Desktop integration disagrees with the production version. Four copies of the same forty-line function, all subtly different. By mid-2025 this was every enterprise team’s reality.
For the first two years of the agent boom, every team built tool integrations from scratch. Want your agent to read GitHub? Write a GitHub tool. Want it in Cursor too? Write the same tool again, in Cursor’s plugin format. Slack? Same code, third format. Each new LLM client meant rebuilding the same connectors.
Model Context Protocol (MCP) is the fix. It’s an open standard, originally proposed by Anthropic and now governed as a vendor-neutral open specification at modelcontextprotocol.io, that defines how an LLM client talks to a tool provider. MCP is not Anthropic-only — any LLM host or agent framework can implement it, and most major ones already do. The bet is simple: write your integration once as an MCP server, and every MCP-compatible client can use it.
MCP-compatible clients include Claude Desktop and Claude Code, Cursor, Cline, Continue, VS Code (via the Copilot Chat MCP adapter), Windsurf, Zed, ChatGPT (announced support), and the major agent frameworks (LangChain, LangGraph, MS Agent Framework, Google ADK). The ecosystem has settled on MCP the way the web settled on HTTP.
Step through a real MCP handshake — message by message
Click any message chip to expand the actual JSON-RPC payload. The lane shows direction: client is the MCP host (Cursor, Claude Desktop, your agent); server is your orders service. The full handshake is just eight messages.
The inspector walks a connection the way clients opened one before the
2026-07-28 revision, starting with an initialize handshake. The message shapes
are still what you will meet in most servers running today; what changed is that
the handshake and the session it created are gone, and each request now carries
its own version and capabilities. The stateless flow is spelled out under
“Transport and security” below.
Why it exists
Before MCP, the integration matrix looked like this:
N tools × M LLM clients = N×M custom integrations
Every new client meant porting every tool. Every new tool meant shipping in every client’s format. Teams ended up maintaining 4-5 copies of the same connector.
MCP collapses it:
N tools + M LLM clients = N + M (each speaks MCP)
The shape is the same idea as USB or LSP (Language Server Protocol). Define the protocol once, and the N×M problem becomes N+M.
The architecture
The current MCP spec separates three roles:
- MCP host — the AI application (Claude Desktop, Claude Code, Cursor, an agent framework). It coordinates one or more MCP clients.
- MCP client — a component inside the host that maintains a dedicated connection to one MCP server. One client per server.
- MCP server — exposes capabilities (your tools, your data). Runs as a process the client launches (local) or as an HTTP service it connects to (remote).
A single host typically runs several clients in parallel — one for filesystem access, one for GitHub, one for your internal database, one for Slack. The host stitches them together; the servers don’t know about each other.
The MCP host coordinates a client per server. Each client speaks JSON-RPC over stdio (local) or streamable HTTP (remote).
The three primitives
An MCP server exposes one or more of three things, each with list,
get, and (for tools) call JSON-RPC methods:
- Tools — executable functions the model can call. Same idea as
plain tool-calling, but discoverable via MCP instead of hard-coded in
the client.
query_database,create_issue,send_message. Model-initiated. - Resources — read-only data sources the host can pull into context. Files, database rows, API responses. The host decides when to read them. Application-initiated.
- Prompts — reusable prompt templates the server publishes for the user to invoke (think slash-commands). The server author packages domain expertise — “here’s the right way to ask my server to do X”. User-initiated.
Tools are the workhorse. Most MCP servers are 90% tools, with a few resources for things like “list current open PRs” and prompts for common workflows.
The spec also defines client-exposed primitives that servers can reach back
into. Elicitation — the server asking the user for a missing value or a
confirmation mid-call — is the one that survived the 2026-07-28 revision, and it
now travels as a multi round-trip request: the server answers with
resultType: "input_required", and the client re-sends the same call with the
answers in inputResponses. Sampling (a server borrowing the host’s LLM),
roots (the client declaring which paths a server may touch) and logging
were all deprecated in that revision. They still work, and the spec keeps them
for at least twelve months, but don’t build new servers on them — see
advanced MCP primitives for what to use
instead.
A minimal MCP server in Python
The official Python SDK is the mcp package. FastMCP is the
high-level builder; the shape is striking in how plain it looks — a
decorator on a function, and you’re done. (There are official SDKs for
TypeScript, Go, C#, Java, and Kotlin too. The four Tier-1 SDKs — TypeScript,
Python, Go, and C# — spoke the 2026-07-28 revision on the day it landed; the
Rust SDK is in beta.)
# A minimal MCP server with FastMCP.
# In production: pip install mcp, then run with: python server.py
# The real import is: from mcp.server.fastmcp import FastMCP
# Here we mock FastMCP so the shape — decorator on a function — is visible.
class FastMCP:
def __init__(self, name):
self.name = name
self._tools = {}
self._resources = {}
def tool(self):
def deco(fn):
self._tools[fn.__name__] = fn
return fn
return deco
def resource(self, uri):
def deco(fn):
self._resources[uri] = fn
return fn
return deco
def run(self):
print(f"server '{self.name}' would now listen on stdio")
print(f" tools: {list(self._tools.keys())}")
print(f" resources: {list(self._resources.keys())}")
server = FastMCP("orders-server")
# 1. A tool — pydoc becomes the description the LLM reads.
@server.tool()
def search_orders(email: str, limit: int = 10) -> dict:
"""Search a customer's recent orders by email address.
Use this when the user asks about past orders or order status.
Returns up to 'limit' orders, newest first.
"""
# In real code: query your DB
return {"orders": [{"id": "A-101", "total": 42.00, "status": "shipped"}]}
# 2. Another tool
@server.tool()
def refund_order(order_id: str, reason: str) -> dict:
"""Issue a refund for an order. Requires order_id and a reason string.
This is a destructive action. The client should confirm with the user
before invoking.
"""
return {"order_id": order_id, "refunded": True, "reason": reason}
# 3. A resource — read-only data the client can pull into context
@server.resource("orders://recent")
def recent_orders() -> str:
"""The 10 most recent orders across all customers."""
return "order A-101 ($42.00), order A-102 ($17.50), ..."
# In a real server you'd call server.run() to start the stdio loop.
server.run()
# Try the tool directly:
print()
print(server._tools["search_orders"]("alex@example.com"))
server 'orders-server' would now listen on stdio
tools: ['search_orders', 'refund_order']
resources: ['orders://recent']
{'orders': [{'id': 'A-101', 'total': 42.0, 'status': 'shipped'}]}
Notice what the decorators bought you: search_orders and refund_order are
just normal Python functions, yet the server now knows their names, and the
real SDK would turn each docstring and type hint into the JSON schema the LLM
reads. Calling search_orders directly returns the same dict an MCP client
would get back over the wire. The framework handles serialization, schema
generation, and the wire protocol — you write a function.
To actually run this in production, you’d:
pip install mcp
python server.py
Then in Claude Desktop’s config (or Cursor’s, or wherever), you point
at this server. Suddenly every agent in those clients can use your
search_orders and refund_order tools.
Transport and security
MCP defines two production transports (the wire protocol the client and server use to exchange JSON-RPC messages):
- stdio — the client launches the server as a child process and talks to it over stdin/stdout. This is what Claude Desktop, Claude Code, and Cursor use for local servers. Simple, no network exposure.
- Streamable HTTP — the current spec for remote servers. HTTP POST for client-to-server messages with optional Server-Sent Events for streaming back. The older HTTP+SSE-only transport was deprecated outright in the 2026-07-28 revision; it keeps working through the twelve-month deprecation window, but nothing new should use it.
Stateless by default
Until mid-2026, a remote MCP connection was a session. The client sent
initialize, the server answered with its capabilities, and every later request
carried an Mcp-Session-Id header back to the same server instance. The
2026-07-28 revision removed both. In their place:
- Every request carries its own protocol version, client identity, and client
capabilities under
_meta— the keys areio.modelcontextprotocol/protocolVersion,/clientInfo, and/clientCapabilities. - A server must implement
server/discover, which advertises the protocol versions, capabilities, and identity it supports. Discovery is now something you call when you need it, not a handshake you must finish first. - Streamable HTTP requests carry
Mcp-MethodandMcp-Nameheaders, so a gateway can route and rate-limit on the operation without parsing the body. tools/list,prompts/list,resources/list,resources/templates/list, andresources/readreturnttlMsandcacheScope, so clients and proxies know how long a listing stays fresh and whether it may be shared.
The payoff is operational: any request can land on any server instance behind a plain round-robin load balancer — no sticky routing, no shared session store. If your server needs state across calls, it mints an explicit handle (a cart ID, a workflow ID) and takes it back as an ordinary tool argument.
For remote servers, the spec recommends OAuth 2.0 for obtaining bearer tokens, and also allows API keys and custom headers. For stdio servers, auth is process-level — whoever launched the server inherits its capabilities, so any secrets are typically loaded from the host environment or a config file.
Treat any MCP server you write the way you’d treat any API endpoint: scope permissions, rotate tokens, log access.
Why this matters
Three things shifted once MCP took hold:
- Tool integrations got reusable. A GitHub MCP server written by GitHub itself is now used by Claude Desktop, Cursor, ChatGPT, Windsurf, Zed, and a long tail of agent frameworks. The integration exists once.
- Internal tools are MCP-ified. Enterprise teams ship MCP servers for their data warehouse, their CRM, their ticket system. Every internal agent — and every developer’s IDE — gets the same access with the same auth story.
- Agent frameworks adopted it. LangChain, LangGraph, MS Agent Framework, and Google ADK all ship MCP client adapters. You can mix: use LangGraph to orchestrate, point it at three MCP servers, and skip writing connector code entirely.
When you’d write an MCP server vs. just register a tool
It’s still fine — and often correct — to just hand-register tools in your agent code, especially for one-off internal agents. The decision point is reuse:
- Tool is used in one agent → just register it inline. No MCP needed.
- Tool is used in multiple agents or clients (IDE, chat, internal bot) → MCP server. Write once, share.
- Tool wraps a system you don’t own (third-party API) → check if there’s already an MCP server for it. Most popular services have one.
In one breath
- MCP standardizes how an LLM client connects to a tool provider — write the integration once as a server, and every MCP client can use it.
- It turns the N×M connector problem (every tool rebuilt for every client) into N+M — the same move USB and LSP made.
- Architecture: a host runs one client per server; each server exposes capabilities over JSON-RPC via stdio (local) or Streamable HTTP (remote).
- Since the 2026-07-28 revision the protocol is stateless — no
initializehandshake, noMcp-Session-Id; each request carries its version and capabilities in_meta, andserver/discoveradvertises the rest. - Three primitives: tools (model-initiated actions), resources (app-initiated read-only data), prompts (user-initiated templates) — most servers are mostly tools.
- Reach for a server when a tool is reused across clients; and treat any server you install as untrusted code — it runs with your privileges.
Quick check
Quick check
Wrap-up of the series
You’ve now seen Anthropic’s five workflow patterns plus the agent shape, the evaluator-optimizer loop, the production tool-calling contract, and the MCP standard that ties the ecosystem together. From here:
- Pick a framework (LangChain, LangGraph, MAF, or ADK) and build a tool-using agent against your own data.
- Wrap your most-used tools as an MCP server so every agent — and every IDE assistant on your team — can use them.
- Build an eval harness before you ship. The patterns are the easy part; knowing whether your agent is actually working is the hard part.
Practice this in an interview
All questionsMCP is an open protocol from Anthropic that standardizes how LLM applications discover and connect to external tools, data sources, and prompts through a common client-server interface. It replaces bespoke per-integration glue with a single protocol, so any MCP-compatible host can use any MCP server, and has been adopted across the broader ecosystem.
Tool poisoning embeds malicious influence in a tool description or result. Cross-server shadowing lets one low-trust server steer use of another server's capability. A rug pull changes a previously reviewed schema, description, implementation, dependency, or destination later. Defend with isolated trust contexts, capability snapshots, change review, sandboxing and runtime authorization.
Forwarding a client token intended for a downstream API erases the MCP server's own audience boundary and can create a confused deputy. The server should accept tokens intended for itself, authorize the client, and obtain a distinct downstream credential with the target audience and minimum scope.
Function calling lets an LLM output a structured request to invoke an external function with arguments, which the runtime executes and feeds back, enabling agents to act in the world. Good tool design uses clear names and descriptions, minimal well-typed parameters, narrow single-purpose scope, least privilege, and informative error messages so the model can choose and call them reliably.