MCP Protocol Deep Dive
MCP Overview covered the architecture (client/server, tools/resources/prompts) at a conceptual level. This page is the engineering detail underneath: how a client and server actually talk, what a real MCP server implementation deals with, and how it gets deployed safely in an organization. The details below follow the official spec (2025-06-18 revision) directly, not a paraphrase of it — where a rule matters (a MUST, a specific header name, an exact message shape), it's reproduced as written.
Transport
MCP encodes every message as JSON-RPC 2.0, UTF-8 encoded. The transport is just how those bytes move between client and server — request/response/notification semantics are identical either way. Two standard transports are defined:
stdio
The client launches the MCP server as a local subprocess and talks to it over its own stdin/stdout:
- The server reads JSON-RPC messages from
stdinand writes them tostdout. - Messages are newline-delimited and must not contain embedded newlines — one message per line, full stop.
- The server must not write anything to
stdoutthat isn't a valid MCP message —stdoutis the message channel, not a place for strayprint()debugging output.stderris free for logging; the client may capture it, forward it, or ignore it entirely. - Clients should support stdio whenever possible — it's the simplest transport and the spec explicitly recommends it as the default.
Streamable HTTP
For remote servers — a hosted MCP server for a SaaS product that many different clients connect to over the network. This replaces the older HTTP+SSE transport from the 2024-11-05 spec revision:
- The server exposes a single endpoint (e.g.
https://example.com/mcp) that supports bothPOSTandGET. - Every JSON-RPC message the client sends is a new
POSTto that endpoint, with anAcceptheader listing bothapplication/jsonandtext/event-stream. - The server responds either with
Content-Type: application/json(one JSON object) orContent-Type: text/event-stream(an SSE stream) — useful for a long-running tool call where the server wants to send progress notifications before the final result. - A client can also issue a
GETto open a standing SSE stream, letting the server push requests/notifications to the client without the client speaking first. - Security requirement: servers must validate the
Originheader on every incoming connection (to prevent DNS rebinding attacks), and should bind tolocalhostonly when running purely locally.
Sessions and Lifecycle
An MCP connection goes through three explicit phases, and the initialization phase must be the first interaction — no other request is valid before it completes.
-
Initialization — the client sends an
initializerequest:The client sends the latest protocol version it supports. The server responds with the same version if it supports it, or a different version it does support otherwise — if the client can't accept the server's answer, it should disconnect. The response also carries the server's own capabilities (which of
tools/resources/prompts/logging/completionsit offers) andserverInfo. -
initializednotification — after a successful response, the client must send a one-waynotifications/initializedmessage (noid, no response expected) signaling it's ready for real traffic. Neither side should send ordinary requests before this point — only pings are allowed earlier. -
Operation — normal
tools/call,resources/read, etc., using only the capabilities that were actually negotiated in step 1. Using a capability neither side declared is a protocol violation, not just bad practice. -
Shutdown — MCP defines no dedicated shutdown message; termination happens at the transport layer. For stdio, the client closes the server's
stdin, waits, then escalates toSIGTERMand finallySIGKILLif the process doesn't exit. For HTTP, closing the connection is the signal.
Version and capability negotiation together determine what's actually usable for the rest of the session — a client requesting sampling support means nothing if the server never checks for it, and a server offering resources.subscribe is wasted if the client doesn't understand subscriptions.
For Streamable HTTP specifically, this lifecycle maps onto an explicit session: the server may assign a session ID at initialization time, returned via an Mcp-Session-Id HTTP response header. If issued, the client must echo it on every subsequent request; a server that requires sessions should reject a non-init request missing it with 400. The server may terminate a session at any point, after which it must respond 404 to further requests carrying that ID — and on seeing that 404, the client must start over with a fresh initialize request carrying no session ID at all.
Tool Discovery and Schemas
Discovery — a client sends a tools/list request (paginated via an optional cursor); the server responds with each tool's name, description, and inputSchema:
This is the mechanism that lets a client (and the LLM behind it) learn what a server can do without any hardcoded, tool-specific integration code — the entire point of standardizing the protocol. A tool can optionally declare an outputSchema too, describing the shape of its structuredContent — when present, the server must return conforming structured results and the client should validate against it.
Invocation — a tools/call request carries the tool's name and its arguments, matched against the declared inputSchema:
The result's content is an array that can mix content types in one response — text, image (base64 + mimeType), audio, a resource_link (a URI the client can fetch or subscribe to later), or a fully embedded resource. A well-specified schema (clear types, required vs. optional fields, descriptive field names) directly determines how reliably the LLM calls the tool correctly — a vague or missing schema is a common, avoidable cause of malformed tool calls.
Resource and prompt discovery follow the identical pattern: resources/list / prompts/list for discovery, resources/read / prompts/get for use — the same discovery-before-use shape as tools, just for two different capability kinds.
Errors
MCP tools actually have two separate error-reporting paths, and conflating them is a common implementation mistake:
-
Protocol errors — a standard JSON-RPC error object, used when the request itself is invalid (an unknown tool name, malformed arguments, an internal server fault):
-
Tool execution errors — the request was valid and the tool ran, but failed at the business-logic level (an API rate limit, a downstream 500, invalid input data it couldn't process). This is a normal JSON-RPC success response, with
isError: trueset inside the result:
The distinction matters for how a caller reacts: a protocol error usually means "this call was malformed, fix the call"; a tool execution error means "the call was fine, but here's what happened" — exactly the kind of structured failure an agentic loop can feed back to the LLM so it can self-correct (see Agent Architectures — Durable Execution for the retry patterns built on top of this) rather than crashing the whole session.
Authentication and Authorization
Authorization in MCP is optional, applies only to HTTP-based transports, and is built on a specific, restricted subset of OAuth 2.1 — a stdio server instead just reads credentials from its environment.
- Roles: the MCP server itself acts as an OAuth 2.1 resource server; the MCP client acts as the OAuth client; a separate (or co-hosted) authorization server issues tokens after handling the actual user consent.
- Discovery — the server must implement OAuth 2.0 Protected Resource Metadata (RFC 9728), and on an unauthenticated request must return
401with aWWW-Authenticateheader pointing to its metadata URL. The client fetches that metadata to learn which authorization server(s) it trusts, then fetches that server's own OAuth 2.0 Authorization Server Metadata (RFC 8414) to get its actual endpoints. - The
resourceparameter — the client must implement RFC 8707 Resource Indicators, including aresourceparameter (the MCP server's canonical URI, e.g.https://mcp.example.com/mcp) in both the authorization request and the token request. This binds the issued token to this specific server, not just to whatever the authorization server happened to hand out. - PKCE is mandatory — the client must implement PKCE (OAuth 2.1 §7.5.2) to prevent authorization-code interception.
- Using the token — every request carries
Authorization: Bearer <access-token>as a header; tokens must never appear in a URL query string.
Authorization (once authenticated) is a distinct question from who is connecting: which tools an identity can call, which resources it can read. A server exposing sensitive tools (executing code, touching a production database) needs authorization scoped per-identity, not an all-or-nothing "connected or not" model — the same least-privilege principle as AI Security — Excessive Agency.
Security
MCP's power — letting an LLM discover and call arbitrary tools dynamically — is also its core security surface, and the spec calls out specific attack patterns by name:
- Prompt injection via tool results: a malicious or compromised data source returned by a
resources/reador atools/callresult can contain text designed to manipulate the LLM's subsequent behavior (see AI Security — Prompt Injection) — treat all MCP-returned content as untrusted data, not as trusted instructions, exactly as with RAG-retrieved content. - Tool permission scoping: a client should not blindly grant every connected server's every tool unrestricted access. The spec is explicit that there should always be a human in the loop able to deny a tool invocation — clients should show which tools are exposed, indicate visually when one is invoked, and confirm sensitive operations before running them.
- The confused deputy problem: if an MCP server acts as an intermediary to a third-party API, forwarding the client's token unmodified to that upstream API lets the upstream incorrectly trust it as validated by the server. The spec is explicit here: an MCP server must not pass through the token it received from its own client — if it needs to call an upstream API, it acts as its own separate OAuth client and obtains a different token for that call.
token A
token B (new)
- Token audience validation: a server must validate that any token presented to it was actually issued for it (its audience claim), and reject tokens issued for other services — accepting a token meant for a different resource is exactly what enables the confused-deputy attack above.
- Server trust: connecting to an MCP server means running code (for local stdio servers) or granting network access to a service (for remote servers) — the same supply-chain trust question as installing any other third-party package or dependency, and worth treating with the same scrutiny.
Enterprise Deployment
Running MCP servers inside an organization, rather than a single developer's local tool, adds operational concerns familiar from any internal service:
- Centralized server registries — teams discover approved, sanctioned MCP servers rather than connecting to arbitrary ones found online.
- Centralized auth/identity integration — tying MCP authentication into existing SSO rather than per-server credentials, using the OAuth discovery flow above against an organization's own authorization server.
- Audit logging of tool calls — especially for tools that take real-world actions (writing files, calling external APIs, touching production data); this is the direct application of Legal, Licensing & Governance — Audit Trails to the MCP layer specifically.
- Network-level restrictions — which servers are reachable from which environments, so a compromised or misconfigured MCP server can't reach further than it needs to.
This is the MCP-specific instance of the general AI governance problem of knowing what an AI system can access and proving it after the fact.
Building an MCP Server
A minimal MCP server needs to: handle the initialization handshake and declare its capabilities, implement tools/list (and/or resources/list, prompts/list), implement the tools/call handler (validate arguments against the declared schema, execute, return a structured result or error), and speak the chosen transport (stdio is the simplest starting point). In practice, the official SDKs handle nearly all of this — a real example, from the official Python quickstart, using the FastMCP decorator pattern:
The inputSchema from earlier is derived automatically from the function's type hints and docstring — nothing hand-written. The plain string return value is wrapped into the content array's text block automatically. mcp.run(transport="stdio") handles the JSON-RPC framing and the entire initialize/initialized handshake underneath — the actual server code is almost entirely just the tool logic itself.
This is a large part of why MCP adoption spread quickly across AI coding tools and agent frameworks: the protocol overhead of "become MCP-compatible" is deliberately small, and the SDKs absorb nearly all of the wire-format detail covered on this page.
Next: A2A (Agent-to-Agent) — the complementary protocol for agents talking to other agents, rather than to tools.