Stateless MCP: The Protocol Deleted Its Own Handshake

On July 28, 2026, the Model Context Protocol shipped its biggest change since launch, and the headline was a subtraction. The spec removed the initialize handshake and the Mcp-Session-Id header from the wire. In the maintainers' own words, MCP is "transforming from a bidirectional stateful protocol into a request/response stateless protocol." A protocol deleting its own sessions sounds like regression. It is the opposite, and the reason is a lesson the web learned in 1991 and MCP relearned the hard way.


Two things called "stateless MCP"

Before anything else, a distinction that trips up every discussion. There have been two eras of "stateless MCP," and they are not the same thing.

In the old era, "stateless" was an opt-in flag. The Streamable HTTP transport kept server-side sessions by default, and you could turn them off with something like FastMCP("srv", stateless_http=True) in the Python SDK. The sessions still existed in the protocol; you were just declining to use them. In the new era, as of the 2026-07-28 spec, statelessness is the protocol core. There is no session to decline. The initialize and notifications/initialized exchange is gone, and the Mcp-Session-Id header is retired from the transport. A server on the new revision that receives that header must ignore it. This essay is about the second thing.

What a request looks like now

The old flow had a ceremony. The client POSTed initialize, the server minted an Mcp-Session-Id and handed it back, and every subsequent request carried that id so the server could look up who it was talking to. A separate GET opened a long-lived server-sent-events stream for anything the server wanted to push. State lived in the transport.

The new flow has no ceremony. Every POST is self-describing. The protocol version rides in a header and again in the request body's _meta, along with the client's identity and capabilities, so the server needs nothing it did not receive in this exact request.

# one self-contained request, no prior handshake
POST /mcp HTTP/1.1
Content-Type: application/json
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: get_weather

{"jsonrpc":"2.0","id":1,"method":"tools/call",
 "params":{"name":"get_weather","arguments":{"location":"Seattle"},
   "_meta":{
     "io.modelcontextprotocol/protocolVersion":"2026-07-28",
     "io.modelcontextprotocol/clientInfo":{"name":"ExampleClient","version":"1.0.0"},
     "io.modelcontextprotocol/clientCapabilities":{}}}}

The Mcp-Method and Mcp-Name headers are new and mandatory: they duplicate the routing-relevant parts of the body at the HTTP layer, so a gateway or load balancer can route on headers alone without parsing JSON. The version in the header must match the version in _meta, or the server returns 400 with a HeaderMismatch error, code -32020. Capability discovery still exists as an optional server/discover call, but a client is free to skip it and just send the request, handling the error if the version is not supported.

Play both flows and watch the one thing that actually changed: the session the server has to hold, drawn as the bar down its lifeline, is simply gone.

Fig. 1 · the message flow, before and after

Why deleting sessions is a scaling feature

Here is the failure the old design produced, quoted almost verbatim from the FastMCP horizontal-scaling docs. A client connects to instance A, and a session is created in instance A's memory. The next request routes to instance B. The session does not exist there. The request fails. The usual patch is sticky sessions at the load balancer, pinning each client to one instance, except that most MCP clients, Cursor and Claude Code included, call fetch() internally and do not forward the cookies that stickiness relies on. So the patch does not even work.

A stateless protocol dissolves the whole problem. If every request carries everything the server needs, then any request can land on any replica, behind a plain round-robin load balancer, with no shared storage and no affinity. Replicas become interchangeable and disposable. You can scale to zero, scale to a hundred, or lose one mid-traffic, and nothing breaks. Flip the mode below, then kill a replica and watch the difference.

Fig. 2 · kill a replica, two waysstateless

Three clients, a round-robin load balancer, a pool of replicas. In session mode each client is pinned to one replica; stateless mode routes each request to any live one. Send traffic, then kill a replica.

load balancerround-robin

Pick a mode, send traffic, then kill a replica.

Four ways this shows up

1. A tools server that is just a fetch handler. A stateless MCP handler takes a request and returns a response with nothing held in between, which is the exact shape of an edge function. Cloudflare's Agents SDK now serves "tools, prompts, resources, and elicitation without an MCP transport session or Durable Object." The same handler drops onto Vercel, a Lambda, or a plain Node box unchanged, because there is no shared state to coordinate. AWS put it plainly: developers can now "deploy MCP servers on standard, scalable infrastructure without managing sessions or persistent connections."

2. A pool behind a dumb load balancer. This is Fig. 2. No sticky sessions, no session store in Redis, no affinity rules. The load balancer routes on the Mcp-Method and Mcp-Name headers if it wants to, or round-robins blindly if it does not. Replicas autoscale on CPU like any other stateless web tier.

3. Confirmation without a held-open stream. The obvious objection is that some tools need to ask the user something mid-call, and that used to require a server-initiated message down the SSE stream, which a stateless server has no way to hold open. The spec's answer is Multi Round-Trip Requests. The server returns an input_required result listing what it needs; the client gathers the answer and re-sends the original request with the responses attached. Supabase, whose MCP server runs statelessly, said this is exactly what unlocks a tool confirming "the cost of a new project before it's created, or a query that would delete data." The interaction survives; it just becomes two plain requests instead of one request plus a back-channel.

4. State you actually need, made explicit. Stateless protocol does not mean stateless application. If your work genuinely spans calls, the spec's guidance is to stop hiding state in the transport and make it visible: "mint an explicit handle from a tool and have the model pass it back as an argument." A create_deployment tool returns a deployment_id; the model threads that id into the next tool call. The state lives in your database keyed by that id, the model can see it, and any replica can look it up. This is better than session state precisely because it is not hidden.

What it costs, honestly

The change is wire-incompatible in both directions, which the maintainers do not hide. A 2025-era client and a 2026-07-28 server cannot speak without a shim, so gateways and SDKs carry the interop during the migration. On Hacker News the reactions split cleanly: the gateway operators were relieved, one noting that a large share of their bugs came from "the need to persist server state," while skeptics argued the sessions should never have been in a transport that sits on top of stateless HTTP in the first place. Both are right. What you give up is real but narrow: SSE resumability is gone, so a broken stream means re-issuing the request; server-push notifications now come through an explicit subscriptions/listen stream you opt into; and truly long-running work moves to the Tasks extension, which you poll. Nothing forces you to migrate, though. Existing servers keep working on the version they already negotiated.

Three features are formally deprecated on the way out, on a minimum twelve-month clock: Roots, Sampling, and Logging (SEP-2577). Roots become plain tool parameters or resource URIs, Sampling becomes a direct call to your LLM provider, and Logging moves to stderr or OpenTelemetry. I wrote the Rust SDK's implementation of that deprecation, which is a small, clarifying change: the three features that most needed a held-open connection are the three the stateless core is happiest to let go.

The deeper point is the one I keep coming back to on this site: the new vocabulary is usually an old fundamental in fresh packaging. Stateless request-response with everything in the message, no server memory between calls, horizontal scale behind a dumb balancer: that is HTTP, described in 1991, and it is why HTTP scaled to the entire web. MCP spent two years building sessions on top of it, discovered the sessions were the source of its reliability problems, and deleted them. The protocol got smaller and got better. That is the good kind of change, and it is rarer than it should be.

rg
Rohit Ghumare

CNCF Ambassador and Google Developer Expert. I build agent infrastructure and write about the fundamentals underneath the AI stack. Spec facts here are from the 2026-07-28 specification and its changelog.

Related: new names, old fundamentals · More posts · X