MCP — Model Context Protocol
The emerging 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.
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 a few client-exposed primitives that servers can call back into: sampling (server asks the host’s LLM to complete something, so server authors don’t need their own API key), elicitation (server asks the user for input/confirmation), and logging. You won’t need these for most servers; mention them when you do.
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 also official SDKs
for TypeScript, Java, Kotlin, and C#.)
# 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. Replaces the older HTTP+SSE-only transport, which is now considered legacy for new servers.
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).
- 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.