Every Stateless MCP Server Rebuilds State Somewhere
July 28, 2026

The 2026-07-28 specification deleted the initialize handshake and the Mcp-Session-Id header. It did not delete one byte of what they carried. A stateless MCP server still holds that state somewhere, and the only decision left is which of three places absorbs it.
The handshake moved onto every request#
Most people read the removal of the MCP session handshake as a deletion. Read the request schema instead. Protocol version, client identity and client capabilities were negotiated once at initialize, and they are now mandatory fields inside every single request's _meta.
protocolVersion, negotiated once at startup before, now restated on every single callclientInfo, the name and version string, repeated foreverclientCapabilities, the field the server is explicitly forbidden from caching
{
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientInfo": { "name": "claude-code", "version": "2.1.0" },
"io.modelcontextprotocol/clientCapabilities": {
"roots": { "listChanged": true }, "sampling": {}, "elicitation": {}
}
}
}That block rides along forever, because the spec forbids the server from remembering it. SEP-2575 is blunt about the consequence. "servers MUST NOT infer capabilities from prior requests."
So the state did not evaporate. It became payload, and payload has a price you can measure.
A minimal tools/call built to the SEP-2575 schema, with its required HTTP header, weighs 393 bytes. The same call under 2025-06-18 weighed 163. That is 230 extra bytes on every request, a 141% increase, to carry what one handshake used to carry once.
Cumulative request bytes, handshake versus per-request metadata
The handshake it replaced cost 219 bytes, paid a single time. Break-even lands at 0.95 calls. The stateless protocol is already the more expensive one before the first response comes back.
That exact percentage will not be yours. It scales with how fat your clientInfo and clientCapabilities objects are, so measure your own payload rather than quoting this one.
const bytes = (o) => Buffer.byteLength(JSON.stringify(o), "utf8");
console.log(bytes(yourCallWithMeta) - bytes(yourCallWithoutMeta));Subscriptions became the client's bookkeeping#

MCP resource subscriptions are gone, and the spec names the reason without flinching. "Resource subscriptions are inherently stateful", it says, because the server "must remember which resources each client has subscribed to".
Read that again. The feature was not removed because nobody wanted it. It was removed because it required somebody to remember something, and the server was no longer allowed to be that somebody.
The remembering still happens. A client now declares every resource URI it cares about in the params of a subscriptions/listen request, which means the client holds the list. On STDIO the spec hands over recovery too, requiring the client to re-send the whole subscription request when a server crashes and restarts.
Your server got simpler. The system did not.
Tasks replaced the state store it removed#
Resumable SSE streams died in the same release, for the same reason. Resuming a dropped stream would need the server to retain per-request state across the failure, which is exactly what stateless-by-default forbids.
Removing resumability leaves a hole, and durable work still has to survive a dropped connection. The spec fills the hole by pointing at a different stateful thing. Workloads needing durability or resumability must use the tasks primitive instead, which fetches results after a drop.
Tasks graduated from experimental to an official extension in this release, with a poll-based tasks/get and a new tasks/update. A poll-based task store is a state store that happens to have a spec number.
The renaming goes further. SEP-2567 proposes removing sessions entirely and replacing them with explicit state handles, and the official release post tells implementers to "mint an explicit handle from a tool and have the model pass it back as an argument". The word session retired. The concept got a new job title.
The session-free server still pays#
The strongest objection to all of this is that a genuinely session-free server now exists in production, and it does. Cloudflare's Agents SDK v0.20.0 serves "tools, prompts, resources, and elicitation without an MCP transport session or Durable Object". Its previous stateful primitive, McpAgent, is now deprecated and feature-frozen.
No session. No storage object. That is the real counterexample, and it deserves better than a hand-wave.
It relocates state twice anyway. Every request carries the full _meta block from the first section, which is the wire absorbing it. And elicitation runs through multi-round-trip requests, where the SDK collects the input, retries the original operation, then resolves the original callTool promise.
Look at what that retry implies. A half-finished operation existed across two round trips, and the server was forbidden from holding it, so the client held it. The server did not become stateless. It became the part of the system that forgot.
Here is the honest limit of the argument. A pure tools/call server with no auth, no pagination and no elicitation genuinely holds nothing between requests, and it pays for that with 230 bytes of re-sent identity per call. Even the simplest case buys statelessness rather than getting it free.
Pick where the state lands#

Three destinations, and the spec is neutral about which you choose.
- The wire, cheapest to build, and it bills you per request forever. Fine for low call volume, bad for chatty agents.
- The client, free for you, and it moves the failure into somebody else's reconnect logic. Fine when you control the client.
- A store such as tasks, a handle, Redis or a Durable Object. Costs real infrastructure and is the only one that survives a crash.
The migration guides going around this week are checklists for the header changes. They are correct and they are not the decision. If you are porting a server, the parts nobody explains matter less than answering which of those three rows your state moved into, because you already picked one by accident.
What would change this position is a real deployment, running any session-scoped feature, where no component anywhere holds more than it did before. Not a benchmark. An architecture. Nobody has shown one yet, and the round-trip math suggests why that is hard.
Stateless is a statement about the protocol. It was never a statement about your system.