MCP (Model Context Protocol) is an open specification from Anthropic that standardizes how LLM applications discover, invoke, and manage external tools, resources, and prompts. It defines a JSON-RPC 2.0 based protocol over stdio or HTTP/SSE transports, letting clients and servers negotiate capabilities at runtime without hard-coded integrations. Think of it as LSP for LLM tooling: a common language so any compliant client can talk to any compliant server.
How the protocol works
MCP separates concerns into three roles: hosts (the LLM application), clients (the protocol implementation inside the host), and servers (the tool/resource providers). A host like Claude Desktop or a custom agent runtime embeds an MCP client. That client connects to one or more MCP servers — each exposing a discrete capability such as filesystem access, GitHub API, or a database query engine.
The handshake follows a fixed sequence:
- Initialize — Client sends
initializewith protocol version and client capabilities. Server responds with its capabilities:tools,resources,prompts, andloggingsupport. - Capability negotiation — Both sides advertise what they support. A server might offer tools but not resources; a client might support sampling but not roots.
- Runtime operation — After initialization, the client invokes
tools/call, readsresources/read, or rendersprompts/getas the model requests them.
All messages are JSON-RPC 2.0 envelopes. A minimal initialize request looks like:
{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2024-11-05",
"capabilities": {
"tools": {},
"resources": {},
"prompts": {}
},
"clientInfo": {
"name": "my-agent",
"version": "1.0.0"
}
}
}
The server replies with its own capabilities and server info. After that, the client can list available tools:
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/list",
"params": {}
}
Response:
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"tools": [
{
"name": "read_file",
"description": "Read a file from the workspace",
"inputSchema": {
"type": "object",
"properties": {
"path": { "type": "string" }
},
"required": ["path"]
}
}
]
}
}
When the model decides to call a tool, the client sends tools/call:
{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "read_file",
"arguments": { "path": "src/main.py" }
}
}
The server executes and returns structured content — text, images, or embedded resources — that the client feeds back into the model context.
Transport layer
MCP defines two transports. Stdio is the default for local servers: the client spawns the server as a child process and communicates over stdin/stdout. This works well for CLI tools, language servers, and anything that runs on the same machine. HTTP/SSE enables remote servers: the client POSTs JSON-RPC requests to an endpoint and receives Server-Sent Events for responses and notifications. The spec requires servers to support both transports; clients choose based on deployment model.
A stdio server in Python using the official SDK:
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import Tool, TextContent
app = Server("filesystem")
@app.list_tools()
async def list_tools() -> list[Tool]:
return [
Tool(
name="read_file",
description="Read a file",
inputSchema={
"type": "object",
"properties": {"path": {"type": "string"}},
"required": ["path"]
}
)
]
@app.call_tool()
async def call_tool(name: str, arguments: dict) -> list[TextContent]:
if name == "read_file":
path = arguments["path"]
with open(path, "r") as f:
return [TextContent(type="text", text=f.read())]
raise ValueError(f"Unknown tool: {name}")
if __name__ == "__main__":
import asyncio
asyncio.run(stdio_server(app))
An HTTP server uses the same app but wraps it with sse_server or a FastAPI/Starlette integration. The protocol is transport-agnostic; the SDK handles framing.
Why it matters for agent architecture
Before MCP, every agent framework invented its own tool calling convention. LangChain had BaseTool, LlamaIndex had FunctionTool, AutoGPT had its own registry, and each LLM provider exposed different function calling formats. If you wanted to use a GitHub tool in LangChain and a filesystem tool from a community repo, you wrote glue code for each framework.
MCP solves this by moving the integration point to the protocol layer. A tool server written once works with any MCP-compatible host: Claude Desktop, Cursor, Continue, custom LangGraph agents, or a bare-bones REPL you build yourself. The ecosystem effect is real — there are already 1,000+ community servers for databases, APIs, browsers, and niche tooling.
For engineers, this means:
- Portability — Write a server once; run it against any host. No framework lock-in.
- Composability — Connect multiple servers to one host. The model sees a unified tool namespace.
- Security boundaries — Each server runs in its own process (stdio) or network boundary (HTTP). You can sandbox filesystem servers, rate-limit API servers, and audit tool calls independently.
- Observability — The JSON-RPC layer is inspectable. Log every request/response, trace latency, inject middleware for auth or quotas.
The protocol also supports resources (read-only data sources like files, docs, or database rows) and prompts (reusable prompt templates the server defines). Resources use URI schemes (file://, github://, postgres://) and support pagination and subscriptions. Prompts let servers ship opinionated workflows — “code review this PR” — that the host can render with context filled in.
Concrete example: building a code review agent
Let’s walk through a realistic setup. You want an agent that can review pull requests by reading changed files, querying your internal style guide, and posting comments. You’ll use three MCP servers:
- github-server — Official Anthropic server. Exposes tools:
list_pr_files,get_file_content,create_review_comment. - filesystem-server — Local file access for the repo clone. Tools:
read_file,glob,grep. - style-guide-server — Your custom server wrapping a vector DB of internal conventions. Tool:
query_style_guide.
The host (say, a LangGraph agent) connects to all three at startup. The agent’s system prompt instructs the model: “When reviewing a PR, first list changed files, then read each file, then query the style guide for relevant rules, then post comments.”
A simplified LangGraph node that uses the MCP client:
from mcp.client.session import ClientSession
from mcp.client.stdio import stdio_client
from langgraph.graph import StateGraph
async def review_pr(state: dict) -> dict:
# Assume sessions initialized at startup: github, fs, style
github = state["github_session"]
fs = state["fs_session"]
style = state["style_session"]
pr_number = state["pr_number"]
files = await github.call_tool("list_pr_files", {"pr_number": pr_number})
comments = []
for f in files["result"]["files"]:
content = await fs.call_tool("read_file", {"path": f["path"]})
guidance = await style.call_tool("query_style_guide", {"code": content["result"]})
if guidance["result"]["violations"]:
comments.append({
"path": f["path"],
"line": guidance["result"]["line"],
"body": guidance["result"]["message"]
})
if comments:
await github.call_tool("create_review_comment", {
"pr_number": pr_number,
"comments": comments
})
return {"status": "done", "comments_posted": len(comments)}
Notice the agent code doesn’t know how the GitHub server authenticates, where the style guide lives, or which filesystem the fs server mounts. Those are server concerns. The agent only knows tool names and schemas — discovered at runtime via tools/list.
This separation lets you swap the GitHub server for a GitLab server, or run the style guide server on a GPU box for embedding inference, without touching the agent logic.
Common misconceptions
“MCP is just function calling with extra steps”
Function calling is a model-level feature: the model outputs a structured tool_call and the host executes it. MCP is a protocol for discovering and invoking tools across process and network boundaries. The model still uses function calling (or tool use) to express intent, but MCP standardizes the registry, invocation, and result encoding so the host doesn’t need per-tool adapters. You can run MCP servers with models that don’t support function calling — the host can parse free-text tool invocations and map them to tools/call — though in practice you’ll want a model that supports structured output.
“MCP replaces RAG”
MCP resources can serve document chunks, but they’re not a retrieval system. A resource is a source — a file, a database row, an API response. Retrieval (embedding, ranking, filtering) happens inside a server or as a separate tool. You might build an mcp-server-rag that wraps a vector store and exposes search as a tool and document:// URIs as resources. MCP gives you the plumbing; you bring the retrieval logic.
“MCP requires Anthropic models”
The protocol is model-agnostic. The spec, SDKs, and reference servers are open source (MIT/Apache-2). Any host that speaks JSON-RPC 2.0 and implements the client side can use MCP servers. OpenAI models, local Llama models, or mixtures work fine — the host just needs to map the model’s tool calling format to MCP tools/call. Several community hosts already support this.
“MCP servers must be local”
Stdio is the simplest transport, but HTTP/SSE is first-class. You can deploy MCP servers as containerized services behind a load balancer, authenticate with OAuth or mTLS, and scale them independently. The protocol supports notifications/message for server-initiated updates (e.g., a file watcher pushing changes), which works over SSE. For high-throughput or multi-tenant deployments, HTTP is the right choice.
“MCP is only for tools”
Tools get the attention, but resources and prompts are equally important for production systems. Resources let the model read context without tool overhead — a 50MB log file streamed via resources/read with pagination is cleaner than a tool that returns the whole thing. Prompts let servers encode domain expertise: a Kubernetes server might ship a debug_pod prompt that expands to a multi-step investigation workflow. Hosts can present these as slash commands or auto-invoke them based on heuristics.
What’s still rough
The spec is young (v1.0 finalized late 2024). Expect churn in:
- Authentication — The spec defines an
Authorizationheader for HTTP but leaves token formats and scopes to implementations. No standard OAuth flow for stdio. - Streaming tool results — Large outputs (e.g.,
grepover a codebase) currently return as one payload. Chunked streaming is discussed but not standardized. - Client-side tool filtering — A host connecting to 20 servers gets 200+ tools. The model’s context window fills fast. No standard way to say “only show me tools tagged
kubernetes” yet — each host implements its own filtering. - Versioning —
protocolVersionininitializehandles major breaks, but minor capability additions (new tool annotations, resource metadata fields) lack a formal extension mechanism.
Where to start
If you’re evaluating MCP for a project:
- Install Claude Desktop and enable a few community servers (filesystem, github, sqlite) in
claude_desktop_config.json. See the protocol in action without writing code. - Read the spec at
modelcontextprotocol.io/specification. It’s short — ~2000 lines of markdown — and precise. - Build a trivial server using the Python or TypeScript SDK. Expose one tool that does something useful for your team (query an internal API, run a linter, fetch a secret).
- Connect it to your agent framework. Most frameworks have MCP client integrations now: LangGraph, LlamaIndex, Semantic Kernel, PydanticAI. If yours doesn’t, the client SDK is ~200 lines — you can wrap it in an afternoon.
MCP won’t solve prompt engineering, eval, or model selection. But it eliminates a whole class of integration boilerplate that every agent team rewrites. For that alone, it’s worth learning.