Transports, statelessness, and what replaced sessions
Lesson 3 of 5 in MCP Fundamentals: One Protocol Instead of M × N Integrations.
The docs split MCP into two layers: a data layer — the JSON-RPC 2.0 protocol itself, covering discovery, server features, client features, and utilities — and a transport layer, covering connection establishment, message framing, and authorization. The same JSON-RPC message format rides every transport, which is why a server can move from a subprocess to a hosted URL without changing its tool code.
The spec defines exactly two standard transports, and permits custom ones.
| stdio | Streamable HTTP | |
|---|---|---|
Where the server runs | Local subprocess, launched by the client itself. | Anywhere reachable over HTTP — same machine, your VPC, or a vendor’s service. |
Who it serves | Typically one client — the process that spawned it. | Typically many clients, like any web service. |
Framing | Newline-delimited JSON-RPC on stdin/stdout. Keep stdout clean — stray prints corrupt the stream. | One HTTP POST per message to a single endpoint; response is JSON or a request-scoped SSE stream. |
Credentials | From the environment. The authorization spec says stdio implementations SHOULD NOT follow the OAuth flow. | The OAuth 2.1-based authorization flow applies here — and remains OPTIONAL for implementations. |
Trust posture | You are running someone’s program on your machine with your user’s privileges. Full local blast radius. | Network boundary and audience-bound tokens — but now a third party sees your requests. |
Named examples from the docs | A filesystem server launched by Claude Desktop. | The Sentry MCP server. |
Now the part that trips up anyone who learned MCP in 2025. Revision 2026-07-28 made the base protocol stateless: requests are self-contained and capability negotiation happens per request. Three familiar mechanisms went away with it.
The initialize/notifications/initialized handshake was removed (SEP-2575). Every request now carries its protocol version and client capabilities in _meta, and servers MUST implement a new server/discover RPC that advertises supported protocol versions, capabilities, and identity; clients MAY call it before anything else. ping, logging/setLevel, and notifications/roots/list_changed were removed too.
Protocol-level sessions are gone — so where does state live?
Revision 2026-07-28 removed protocol-level sessions and the Mcp-Session-Id header (SEP-2567). A server that genuinely needs state across calls — a shopping cart, a browser context, an open transaction — now returns an explicit handle from a creation tool and accepts it as an ordinary argument on later calls. The model is responsible for carrying the handle forward.
The spec’s non-normative guidance for handles: authorize them per call, make them opaque and high-entropy (especially for unauthenticated servers), and document their lifetime. Read that list again as a threat model — a guessable long-lived handle on an unauthenticated server is a cross-user data leak, and “the model carries it forward” means a prompt-injected model can carry someone else’s forward too if the server does not re-check authorization.
Servers no longer initiate requests — the MRTR pattern
Under 2026-07-28, servers never initiate JSON-RPC requests and clients never send JSON-RPC responses. The old server-initiated calls (roots/list, sampling/createMessage, elicitation/create) were replaced by Multi Round-Trip Requests (MRTR, SEP-2322): the server returns an InputRequiredResult with resultType: "input_required" carrying inputRequests, and the client retries the original request with inputResponses.
All results now carry a required resultType field — and for backwards compatibility, clients MUST treat results from earlier-protocol servers that lack resultType as complete.
Change notifications became opt-in
resources/subscribe and the standalone HTTP GET SSE stream were replaced by a single long-lived subscriptions/listen stream. The client names the notification types it wants — toolsListChanged, promptsListChanged, resourcesListChanged, resourceSubscriptions — and the server first replies with notifications/subscriptions/acknowledged reflecting the subset it accepted.
Delivery is best-effort: every notification carries its subscription ID in _meta, and clients should still poll, because delivery is not guaranteed across reconnects. Resumable SSE via Last-Event-ID is not supported in this revision.
Required headers on every POST
On Streamable HTTP under 2026-07-28, every POST to the MCP endpoint MUST include an MCP-Protocol-Version header — for example MCP-Protocol-Version: 2026-07-28 — matching the _meta.io.modelcontextprotocol/protocolVersion field in the body. A mismatch is rejected with HTTP 400 and JSON-RPC error -32020 (HeaderMismatch). The revision also requires Mcp-Method and Mcp-Name mirror headers.
The MCP-Protocol-Version header itself is older — it arrived in revision 2025-06-18. Mirror headers exist so infrastructure (proxies, gateways, WAFs) can route and police MCP traffic without parsing the JSON body.
What about interoperating with older peers?
The old mechanisms have not evaporated from the world. Sessions and the standalone GET stream still apply when interoperating with peers speaking 2025-03-26 through 2025-11-25, and version identifiers are dates precisely so you can reason about this: a revision id is the last date backwards-incompatible changes were made, and it is not incremented for backwards-compatible ones.
Practical consequence: “which MCP version?” is a real deployment question, not trivia. Client and server can disagree, and the negotiation now happens per request.
Which transport for this server?
Interactive decision tree — outcomes:
- stdio
The default for local servers: the client launches the subprocess, credentials come from the environment, and the authorization spec explicitly says stdio implementations SHOULD NOT follow the OAuth flow. Keep stdout free of anything that is not a JSON-RPC message, and remember the trade: you are executing someone’s code with your user’s privileges.
- Streamable HTTP bound to localhost
Several local clients can share one server over HTTP — but now you own the local-network attack surface. Bind to 127.0.0.1, validate the Origin header on every connection, and return 403 for anything unexpected. This is the DNS-rebinding case the spec calls out by name.
- Streamable HTTP with authorization
A multi-user remote server needs the OAuth 2.1-based flow: the server acts as an OAuth resource server, MUST implement Protected Resource Metadata (RFC 9728) for discovery, and MUST validate that each access token was issued for it as the intended audience. Clients MUST implement Resource Indicators (RFC 8707) and MUST NOT put tokens in the query string.
- Streamable HTTP, authorization optional
Authorization is OPTIONAL in MCP, so a genuinely public read-only server can skip it. Two cautions: “read-only” must be true of the tools, not just the annotations; and if you mint handles for stateful tools, make them opaque and high-entropy, because unauthenticated plus guessable equals cross-user leakage.
Tool: Protocol Picker — MCP, A2A, plain function calling, or a REST API? Work through real integration briefs in the Protocol Picker and see where each protocol stops being the right answer.
Interactive checkpoint quiz (2 questions) — open this page in a browser to take it.