AI Engineering Playbook
MCP

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.

FlowSequenceWhen it appliesWhat breaks it
Initialize / negotiationinitialize → version + caps + serverInfonotifications/initializedEvery connection before tool traffic (pre-2026-07-28)Incompatible versions; undeclared capabilities later; missing HTTP version/session headers
Tool discoverytools/list → schemas → host flattens into model tool defsAfter init (or server/discover); again after list_changedMounting every server's full catalog; schema drift without re-list
Tool invocationModel function call → host routes → tools/callcontent / structuredContent / isErrorModel-controlled actionsTool failures as JSON-RPC errors; unbounded payloads; no progress/cancel
Change notificationsDeclared listChangednotifications/tools/list_changed → re-tools/listHot-reload without reconnectNotifications without the capability flag
ShutdownNo teardown RPC — stdio: close stdin, SIGTERM→SIGKILL; HTTP: close / drop sessionProcess exit, host unloadStray 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).

StepWire / headerApprox. cost
POST initializeprotocolVersion: "2025-11-25"; caps { tools: { listChanged: true } }, MCP-Session-Id: sess_9f3a…~80–150 ms
Notificationnotifications/initialized → 202 Accepted~20–40 ms
Discoverytools/listsearch_tickets (~400–600 tokens once in the model prompt)~50–100 ms
Model turnModel emits search_tickets({site:"LON-WH", severity:"Sev1", status:"open"})model latency
Invocationtools/call id=7; optional SSE progress; ~15 tickets ≈ 400–800 tokens in content~100–300 ms
Next model turnHost returns tool result; model answers in proseanother 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:

FailureWire shapeWho sees it
Typo method tool/callJSON-RPC error -32601Client / host only
Valid call, limit over maxisError: true, "limit must be ≤ 100"Model (can retry)
Handler throwsSame in-band isError: true pathModel

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

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.
Production

On this page