The MCP Protocol
Client/host/server architecture, transports, tools vs resources vs prompts, lifecycle.
Prerequisites
- Tool Calling — MCP is a distribution and lifecycle layer around tool calling, not a replacement for how the model emits structured calls.
- Agent Foundations — the agent loop is what mounts MCP tools and consumes their results as observations.
- Agent Reliability — human approval, tool hallucination, and error compounding all apply once MCP tools enter the loop.
The intuition
Think of MCP the way USB-C works for peripherals. Before a common connector, every laptop brand needed a custom cable for every accessory — N hosts × M accessories meant a drawer of adapters. USB-C made each side implement one plug. MCP does the same for LLM applications and external capabilities: the host (Claude Desktop, an IDE, your agent product) is the laptop; each MCP server is a peripheral that exposes tools, documents, or prompt templates; the protocol is the shared plug and handshake.
The plug is not the model's native function-calling API. Function calling is how the model asks for a tool inside one vendor's chat API. MCP is how the application discovers, connects to, and talks to tool providers that may live in other processes or on other machines. Once the host has a tool list from MCP, it usually flattens those definitions into ordinary function-calling schemas. The model never speaks JSON-RPC.
Key insight
MCP standardizes the boundary between app and tool provider — discovery, transport, lifecycle, capability negotiation. It does not replace model-side function calling; it feeds it. N×M integrations collapse to N+M only when both sides speak the same protocol.
Why it exists
Without a shared protocol, every AI product reinvents connectors. Five agent apps each needing Jira, GitHub, Postgres, and Slack is twenty adapters, twenty auth stories, and twenty places to fix the same bug. Hard-coded function lists also cannot pick up a server shipped by another team, or tools that appear mid-session. And in-process tool functions share memory and credentials with the host; a separate server process is where consent, sandboxing, and isolation can actually live.
Transports differ by deployment. A filesystem server on a laptop wants stdin/stdout with no network surface. A multi-tenant ticket service wants HTTP and OAuth. One protocol must cover both, and optional capabilities plus date-based version negotiation must let features ship without a flag-day upgrade.
Plain function calling wins inside one app you fully own. Custom REST or gRPC bridges lose on interoperability — every host reimplements discovery, lifecycle, and error semantics. MCP is the USB-C / Language Server Protocol move for this domain: write the server once; any MCP host can use it.
The core idea
The Model Context Protocol (MCP) is an open standard, released by Anthropic in November 2024 and now community-maintained, that standardizes how LLM applications connect to external tools and data. Each app implements MCP once; each tool exposes MCP once — N+M instead of N×M.
Three roles matter, and the host/client split is the one people get wrong. The host is the LLM application: conversation, model, consent UI, security policy. The host spawns one client per server connection — a protocol adapter inside the host that maintains a 1:1 relationship with one server. The server is a separate process (local or remote) that exposes capabilities. Three integrations means three clients and three servers. Isolation is deliberate: servers never see the whole conversation and cannot see into each other; the host aggregates context and enforces boundaries.
Servers expose three primitives by who controls invocation. Tools are model-controlled — executable actions with JSON Schema inputs. Resources are application-controlled — documents or data the host injects as context, addressed by URI. Prompts are user-controlled — templates the user picks explicitly, like slash commands. Clients can expose capabilities back: sampling (server asks the host's LLM for a completion), roots (filesystem scope), and elicitation (server requests structured user input mid-operation). In the 2026-07-28 revision, roots and sampling are deprecated; the three server-side primitives remain the conceptual core.
Messages are JSON-RPC 2.0 over stdio (local subprocess) or Streamable HTTP (remote). How a
session starts depends on protocol version: through 2025-11-25 an initialize handshake negotiates
version and capabilities; 2026-07-28 makes the core stateless and drops that handshake.
How it actually works
Wire format. JSON-RPC 2.0, UTF-8: requests (have id, expect a response), responses
(result or error with matching id), and notifications (no id, no reply). Methods are
namespaced — initialize, tools/list, tools/call, resources/read, prompts/get,
notifications/tools/list_changed. A tool call:
{"jsonrpc": "2.0", "id": 2, "method": "tools/call",
"params": {"name": "get_weather", "arguments": {"city": "Hyderabad"}}}The result carries a content array (text/image/resource blocks), optional structuredContent
matching a declared outputSchema, and isError. Tool execution failures are in-band results so
the model can self-correct. Protocol failures (unknown method, invalid params) are JSON-RPC errors
(-32601, -32602) that stop at the client/host unless the host deliberately translates them.
Lifecycle (through 2025-11-25). (1) Initialization: client sends initialize with
protocolVersion (a date string such as "2025-11-25"), capabilities, and clientInfo; server
replies with negotiated version, its capabilities, serverInfo, and optional instructions;
client fires notifications/initialized. The client offers its latest version; the server echoes
it or proposes its own latest; the client disconnects if incompatible. (2) Operation: both sides
use only negotiated capabilities — no list_changed notifications without declaring
tools.listChanged, no sampling requests unless the client advertised sampling. (3) Shutdown: no
teardown RPC — stdio closes stdin then SIGTERM→SIGKILL; HTTP closes connections.
Key insight
After initialize, capability flags are a contract, not a wishlist. Emitting undeclared
notifications or calling undeclared client features is a protocol violation — silent
half-features that only fail in mixed-version fleets.
stdio. Client launches the server as a subprocess. Newline-delimited JSON-RPC on stdin/stdout —
messages must not contain embedded newlines, and the server must write nothing non-protocol to
stdout. A stray print() corrupts the stream; that is the classic first bug. Logging goes to
stderr.
Streamable HTTP. One endpoint (for example /mcp) for POST and GET. Every client→server
message is a POST; the server answers with application/json or upgrades to text/event-stream
SSE for progress, server→client requests, and the final response. A client may also GET a standing
SSE stream for unsolicited messages. Through 2025-11-25, sessions are optional: the server may set
MCP-Session-Id at initialize; the client echoes it and sends MCP-Protocol-Version on later
requests. SSE event IDs act as cursors; reconnect with Last-Event-ID to resume.
Common misconception
Session IDs are not authentication. The spec forbids treating Mcp-Session-Id as proof of
identity. They bind streams and optional server state; credentials (or stdio's lack of network
surface) are a separate concern. Predictable session IDs enable hijacking via resumable streams.
Version history that changes decisions. 2024-11-05 shipped HTTP+SSE (two endpoints, mandatory
long-lived SSE, no resumability) — painful behind load balancers. 2025-03-26 replaced it with
Streamable HTTP. Later revisions added OAuth resource-server semantics, structured tool output,
elicitation, and OpenID Connect discovery. HTTP+SSE remains detectable for backwards compatibility
(POST initialize; on 4xx, fall back to GET expecting the old endpoint event).
2026-07-28 (current latest) is the largest revision since launch: a stateless protocol core.
initialize / initialized and Mcp-Session-Id are gone. Each request carries version, client
identity, and capabilities in _meta; optional server/discover replaces the handshake when
needed. Any instance behind round-robin can serve any request. Streamable HTTP requires
Mcp-Method and Mcp-Name headers so gateways can route without parsing bodies.
Server-initiated sampling and elicitation move to Multi Round-Trip Requests (MRTR): the server
returns resultType: "input_required", and the client retries with answers. Roots, sampling, and
logging are deprecated (twelve-month minimum window); tasks become an official extension. Expect
both protocol styles in the wild for a long time.
On the governed enterprise platform, agent mode mounts several MCP servers under a host that owns consent UI and model access through the gateway. The protocol keeps those servers isolated while the host presents a unified tool surface to the model.
The flows
Operational sequences you can trace on the wire. The table is the 2025-11-25 shape most deployed servers still use; under 2026-07-28, skip initialize and treat each request as self-describing.
| Flow | Sequence | When it applies | What breaks it |
|---|---|---|---|
| Initialize / negotiation | initialize → version + caps + serverInfo → notifications/initialized | Every connection before tool traffic (pre-2026-07-28) | Incompatible versions; undeclared capabilities later; missing HTTP version/session headers |
| Tool discovery | tools/list → schemas → host flattens into model tool defs | After init (or server/discover); again after list_changed | Mounting every server's full catalog; schema drift without re-list |
| Tool invocation | Model function call → host routes → tools/call → content / structuredContent / isError | Model-controlled actions | Tool failures as JSON-RPC errors; unbounded payloads; no progress/cancel |
| Change notifications | Declared listChanged → notifications/tools/list_changed → re-tools/list | Hot-reload without reconnect | Notifications without the capability flag |
| Shutdown | No teardown RPC — stdio: close stdin, SIGTERM→SIGKILL; HTTP: close / drop session | Process exit, host unload | Stray stdout after partial close |
Happy path (stdio): launch subprocess → initialize → tools/list → host injects schemas →
model emits a tool call → tools/call → in-band result → loop continues → close stdin on unload.
Failure branches: version mismatch (disconnect with a clear host error); unknown tool name
(JSON-RPC error the model never sees unless the host translates); handler exception
(isError: true with guidance); network drop mid-SSE (Last-Event-ID resume); stateful HTTP
without sticky routing (next replica does not know the session).
A worked example
The governed enterprise platform mounts a remote ticket search MCP server over Streamable HTTP:
"Show open Sev1 tickets for the London warehouse."
1. Connection lifecycle (illustrative timings, 2025-11-25-style session).
| Step | Wire / header | Approx. cost |
|---|---|---|
POST initialize | protocolVersion: "2025-11-25"; caps { tools: { listChanged: true } }, MCP-Session-Id: sess_9f3a… | ~80–150 ms |
| Notification | notifications/initialized → 202 Accepted | ~20–40 ms |
| Discovery | tools/list → search_tickets (~400–600 tokens once in the model prompt) | ~50–100 ms |
| Model turn | Model emits search_tickets({site:"LON-WH", severity:"Sev1", status:"open"}) | model latency |
| Invocation | tools/call id=7; optional SSE progress; ~15 tickets ≈ 400–800 tokens in content | ~100–300 ms |
| Next model turn | Host returns tool result; model answers in prose | another model turn |
2. Request shape (abbreviated):
{"jsonrpc": "2.0", "id": 7, "method": "tools/call",
"params": {"name": "search_tickets",
"arguments": {"site": "LON-WH", "severity": "Sev1", "status": "open"}}}3. Two error layers:
| Failure | Wire shape | Who sees it |
|---|---|---|
Typo method tool/call | JSON-RPC error -32601 | Client / host only |
Valid call, limit over max | isError: true, "limit must be ≤ 100" | Model (can retry) |
| Handler throws | Same in-band isError: true path | Model |
4. Mid-session surface change: ops deploys search_tickets_v2; server emits
notifications/tools/list_changed; client re-lists; host refreshes model tool defs (~one extra
RTT). Under 2026-07-28, list responses also carry cache hints (ttlMs, cacheScope).
What omitted stages look like in production
- Skip capability discipline — clients wait for notifications that never come.
- Skip discovery refresh — model keeps calling a renamed tool; user sees a "broken agent."
- Collapse tool errors into protocol errors — model never reads
"limit must be ≤ 100". - Mount every server's full catalog — token bloat and worse tool selection.
- Treat session ID as auth — stream hijacking risk; real auth is OAuth (Building MCP Servers).
Production concerns
Sessions and scale. Through 2025-11-25, Mcp-Session-Id pins state to the instance that
handled initialize. Without sticky routing or a shared session store, multi-replica deployments
flake with "unknown session" — the pain the 2026-07-28 stateless core removes. Until peers
upgrade, keep remote servers as stateless as possible. Timeouts and cancellation:
Reliability.
Security on the wire. Session IDs must be cryptographically random and are not auth. HTTP
servers must validate Origin (403 on mismatch) and bind local servers to 127.0.0.1, not 0.0.0.0
— otherwise a malicious page can drive a local MCP server via DNS rebinding. Remote auth:
Building MCP Servers and Security.
Context cost and latency. Every connected server's tool definitions enter the model prompt.
Tens of servers × dozens of tools bloats tokens and degrades tool selection; production hosts
filter, namespace, or dynamically load toolsets. 2026-07-28 cacheable list results help keep prompt
caches warm. stdio is effectively free; remote HTTP adds a round trip per tool call on top of
model latency. Stream progress for long ops, but enforce a hard timeout and cancel via
notifications/cancelled. See Cost and
Latency.
Version skew and trust. Fleets span 2024-11-05 through 2026-07-28. Handle peers that negotiate
down or reject an unsupported version header; HTTP servers with no version header often assume
2025-03-26. Every mounted server is code with real permissions — tool descriptions enter the
prompt, so hosts need consent UI, allowlists, and sandboxed local servers.
Common drill-downs
What does MCP add beyond function calling? Function calling is how a model requests a call inside one API. MCP adds discovery, process isolation, and multi-host reuse — one server works in Claude Desktop, an IDE, and your agent without a custom adapter per host.
Why three primitives? Different who-decides semantics. Tools are model-controlled; resources are application-controlled reads; prompts are user-picked. Collapsing them forces either constant approval prompts or auto-approving a broad tool surface.
Sampling — and where it is going?
sampling/createMessage lets a server use the host's LLM without its own API key. In 2026-07-28
sampling is deprecated; mid-call interaction moves to Multi Round-Trip Requests
(input_required / inputResponses) without a held-open stream.
How do errors reach the model?
Protocol errors stop at the host. Execution failures return isError: true with text in
content so the model can retry. Confusing the two is the most common host-layering bug.
Test yourself
A host mounts three MCP servers. After initialize, the model calls a tool that only exists on server B — but the host routes the call to server A. What went wrong, and what should the host have done at discovery time?
You see JSON-RPC error `-32602` (invalid params) on a `tools/call` in host logs, but the model argues the ticket search 'failed mysteriously.' Diagnose the layering bug.
A remote MCP deployment works with one replica but flakes under load with sticky sessions disabled. Initialize succeeds; later `tools/call` returns unknown session. What broke, and what are the remediation paths?
Why is collapsing resources into tools a consent-model regression, even if the model could 'just call read_document'?
A local stdio server logs with print() and 'randomly' fails to parse on the client. Why is this structural, not flaky?
Go deeper
- MCP Specification (2025-11-25) — still widely deployed; start with Architecture, Lifecycle, and Transports
- MCP Specification (2026-07-28) — current latest; changelog for the full delta
- The 2026-07-28 Specification — official release post: stateless core, MRTR, header routing, deprecations
- Anthropic's MCP announcement — original framing of the N×M problem (Nov 2024)
- MCP Python SDK — official Tier 1 SDK; transport code is the fastest way to internalize the wire protocol
- Building Agents with MCP — Mahesh Murag (AI Engineer) — deep walkthrough from the MCP co-creator team
Where this connects
- Building MCP Servers — implementing tools, choosing stdio vs HTTP, and getting remote OAuth right.
- Tool Calling — how the host presents MCP schemas to the model and handles tool-result errors inside the agent loop.
- Security — prompt injection via tool descriptions and results; untrusted servers are code inside your decision loop.
- Design: LLM Gateway — governed model access pairs with governed tool access; same quotas, audit, and policy instincts.