Coordinates clients. Owns the user session. Decides which tools appear and whether a call needs approval.
One protocol,
multiple tools.
The Model Context Protocol replaces a pile of bespoke plugin systems with a single JSON-RPC contract between an AI application and the tools it is allowed to reach. This page covers the theory, the wire format, and the commands.
Three components, one direction
MCP is a client–server protocol. The host is the AI application you talk to. It spawns one client per server. Each client owns exactly one connection. That 1:1 mapping is the reason permission, logging, and trust stay tractable.
MCP Host
The AI application. Manages the conversation, decides which clients exist, and surfaces tools to the user.Client 01
One connection. Tools list, calls, roots.Client 02
Another connection, another server, isolated.Server · local
Subprocess, stdio transport, inherits your OS identity.Server · remote
HTTP endpoint, Streamable HTTP transport, OAuth boundary.A connection object, not a separate product. It exists so a host can hold N independent sessions without them sharing state.
Exposes tools, resources, and prompts. "Server" describes the role, not the location — local and remote are both servers.
Discovery, capability negotiation, primitives, notifications. Identical on every transport. Encoded as JSON-RPC 2.0, UTF-8.
How bytes are framed and delivered, where request metadata rides, and how a pending request is abandoned. A transport is only a binding.
Define your requirements
A transport defines framing, metadata, and cancellation. It does not define meaning. Pick stdio for anything local, Streamable HTTP for anything shared.
| Property | stdio | Streamable HTTP |
|---|---|---|
| Framing | Newline-delimited JSON on a client-launched subprocess | One HTTP POST per message to a single MCP endpoint |
| Reply shape | Same stdout stream | JSON object, or a request-scoped SSE stream |
| Typical scope | One server, one client, one machine | Many clients, many machines |
| Version header | Not applicable | MCP-Protocol-Version |
| Cancellation | notifications/cancelled |
Close the response stream |
| Auth | Process boundary. Inherits your OS identity. | OAuth 2.1, resource metadata per RFC 9728 |
{"jsonrpc":"2.0","id":1,"method":"tools/list"} {"jsonrpc":"2.0","id":1,"result":{"tools":[{"name":"add"}]}} // one JSON object per line, stdin → stdout, UTF-8. stdout is protocol. // Anything you print to stdout for humans corrupts the stream — log to stderr.
POST /mcp HTTP/1.1 Accept: application/json, text/event-stream Content-Type: application/json MCP-Protocol-Version: 2026-07-28 {"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"add","arguments":{"a":1,"b":2}}}
Discovery, then traffic
In revision 2026-07-28 the initialize handshake is gone. Discovery is a single
mandatory RPC, and the protocol version is declared per request rather than pinned to a session.
// client → server. Mandatory RPC. Optional to call: any request may be sent directly. { "jsonrpc": "2.0", "id": 1, "method": "server/discover" } // server → client. Illustrative shape, trimmed to the fields that matter. { "protocolVersions": ["2026-07-28"], "capabilities": { "tools": { "listChanged": true } }, "serverInfo": { "name": "demo", "version": "1.0.0" } }
{ "jsonrpc": "2.0", "id": 2, "method": "tools/call", "meta": { "io.modelcontextprotocol/protocolVersion": "2026-07-28" }, "params": { "name": "add", "arguments": { "a": 1, "b": 2 } } } // The server accepts or rejects each request independently. // Mismatch → UnsupportedProtocolVersionError listing supported versions.
| Message | Direction | Has id | Expects |
|---|---|---|---|
| Request | client → server | yes | Exactly one result or error |
| Response | server → client | echoed | Nothing — it closes the request |
| Notification | either | no | Nothing. Fire and forget. |
| Code | Meaning |
|---|---|
-32700 |
Parse error — invalid JSON |
-32600 |
Invalid request |
-32601 |
Method not found |
-32602 |
Invalid params — schema violation |
-32603 |
Internal error |
Revisions 2025-11-25 and earlier used an initialize handshake that opened a
connection-scoped session and let servers send requests. Modern peers detect the counterpart's era
and fall back. Implement both if you must serve old hosts.
notifications/cancelled. Over Streamable HTTP it closes the response
stream. The protocol rules afterwards are identical everywhere.
Who gets to decide?
Three server primitives, split by who controls them. That split is the design: a model may only reach for what is model-controlled, and the user controls prompts.
| Primitive | Controlled by | Methods | Reads / writes |
|---|---|---|---|
| Tools | model | tools/list tools/call | Writes. Databases, APIs, files. |
| Resources | application | resources/list resources/read resources/templates/list | Reads. Docs, schemas, calendars. |
| Prompts | user | prompts/list prompts/get | Templates the user picks explicitly. |
{ "method": "tools/call", "params": { "name": "searchFlights", "arguments": { "origin": "NYC", "destination": "BCN", "date": "2026-08-14" } } } // A tool is a JSON Schema plus a description. The description is the // only thing that tells the model when to reach for it. { "name": "searchFlights", "description": "Search for available flights", "inputSchema": { "type": "object", "properties": { "origin": { "type": "string", "description": "Departure city" }, "destination": { "type": "string", "description": "Arrival city" }, "date": { "type": "string", "format": "date" } }, "required": ["origin","destination","date"] } }
// Direct resource — a fixed URI with a declared MIME type. calendar://events/2026 // Resource template — discovered via resources/templates/list. travel://activities/{city}/{category} travel://activities/barcelona/museums
Client features: asking the user
A server can pause mid-call and ask the user for input it could not know.
Form mode collects structured data; URL mode sends the user out of band, so the
data never touches the client. Uses the MRTR pattern — the server returns
InputRequiredResult, the client retries with the collected answers.
Servers could request an LLM completion through the client, keeping permissions
and user control where they belong. Deprecated in 2026-07-28. It stays in the
spec for at least twelve months while you migrate.
Clients used to declare which directories a server should focus on. Also deprecated
in 2026-07-28. Do not build new servers on it; use elicitation or explicit
arguments.
Running and Inspection
Build with the SDK, debug with the Inspector, register with a config file. Python needs 3.10+.
- Install the SDK
# uv (recommended) uv add "mcp[cli]" # or plain pip pip install "mcp[cli]" # one-off, no project uv run --with "mcp[cli]" mcp dev server.py # typescript npm install @modelcontextprotocol/sdk
- Write a server — no schema, no request parsing, no protocol boilerplate
from mcp.server import MCPServer mcp = MCPServer("Demo") @mcp.tool() def add(a: int, b: int) -> int: """Add two numbers.""" return a + b @mcp.resource("greeting://{name}") def greeting(name: str) -> str: """Greet someone by name.""" return f"Hello, {name}!"
- Debug with the Inspector
# launches the MCP Inspector against a local script uv run mcp dev server.py # or attach the standalone inspector to any command npx -y @modelcontextprotocol/inspector uvx mcp-server-fetch # call add with a=1 b=2 → you get 3 back
- Serve over HTTP, then drive it from a client
# serve; default port 8000, endpoint /mcp uv run mcp run server.py --transport streamable-http
import asyncio from mcp import Client async def main() -> None: async with Client("http://localhost:8000/mcp") as client: result = await client.call_tool("add", {"a": 1, "b": 2}) print(result.structured_content) asyncio.run(main())
- Register with a host — pick the file your host actually reads
| Host | File | Top-level key |
|---|---|---|
| VS Code — user profile | %APPDATA%\Code\User\mcp.json |
servers |
| VS Code — workspace | .vscode/mcp.json deprecated |
servers |
| VS Code — portable | .mcp.json |
mcpServers |
| Copilot CLI / Agent Host | ~/.copilot/mcp-config.json |
mcpServers |
| Claude Desktop (Windows) | %APPDATA%\Claude\claude_desktop_config.json |
mcpServers |
| Cursor | ~/.cursor/mcp.json |
mcpServers |
| Codex CLI | ~/.codex/config.toml |
[mcp_servers.NAME] |
{
"servers": {
"playwright": {
"command": "npx",
"args": ["-y", "@microsoft/mcp-server-playwright"]
},
"sentry": {
"type": "http",
"url": "https://mcp.sentry.dev/mcp"
}
}
}
code --add-mcp "{\"name\":\"my-server\",\"command\":\"uvx\",\"args\":[\"my-mcp-server\"]}"
${input:apiKey} input variables or an environment file, and keep the local
stdio command reviewable — anyone who reads the file can run it.
| Symptom | Cause | Fix |
|---|---|---|
Spawn ENOENT |
Executable not found on PATH | Absolute path, or a full package name after -y |
| Server starts, no tools | Protocol chatter printed to stdout | Move logging to stderr |
| Method not found | Client speaks an older revision | Check server/discover output; support both eras |
| HTTP hangs | Wrong port, or POST without an SSE-capable Accept | Hit /mcp directly; send the MCP-Protocol-Version header |