Stateless Is a Property of the Protocol, Not Your Architecture

The 2026-07-28 revision of the Model Context Protocol removed protocol-level sessions. I covered what the specification says in August and I won’t re-derive it here. The short version: the initialize handshake and the Mcp-Session-Id header are gone, every request carries its own version and capabilities in _meta, and any instance behind a round-robin load balancer can answer any request.

What I want to look at is the bill. The announcement led with what got deleted, and the deletions are real. AWS’s own architecture guidance opens with the correction that matters more than any of it: “stateless describes the protocol, not your application.” That sentence is the whole argument, and three months on the open question is not whether the protocol is stateless but where the work went instead.

Session meant three different things

An independent migration guide by Hidekazu Konishi opens with a table worth keeping. “Session” in MCP meant at least three different things. There was the protocol layer, meaning Mcp-Session-Id and the version agreement that came with the handshake. There was the application layer, meaning carts, browser instances, and database transactions. There was the platform layer, meaning whatever identifier your host uses to pin a route.

The protocol layer is removed. The application layer is untouched, with a different attachment point. The platform layer was never in the specification, and the guide’s line about it is the sharpest in the piece: “removing Mcp-Session-Id from the specification does not make the header disappear from your traffic.”

That conflation is what the guide’s opening section is written to prevent. Deleting the protocol session did not make stateful tools illegal, and a migration plan built on that misreading rebuilds working tools for no reason.

The protocol does not implement handles

The changelog’s actual instruction is blunt: “Servers that need cross-call state use explicit, server-minted handles passed as ordinary tool arguments.” That reads like a feature, but the protocol does not implement it. SEP-2567 says so directly: “There is no handles/* method, no handle type in the schema, no wire-level concept of a handle at all. From the protocol’s perspective a handle is a string in a tool result and a string in a tool argument, indistinguishable from any other tool data.”

Everything a session used to decide for you is now a design decision you make: the lifetime, the expiry, the permission check, and the question nobody writes into the design doc, which is what happens when a handle outlives the row behind it. Konishi’s closing sentence is better than anything I would write. What the specification stopped providing is “the container. It is not the state itself.”

There is a real gain buried in this and I don’t want to skip it. AWS points out that the identifier “sits in the model’s context rather than hidden in a header,” which means the model can reason about it and thread it across tools. Per-resource sharing scope is also an improvement on the old one-session-one-cardinality model. But expressiveness is paid for in design work, and that work lands per tool, not once in the stack.

The savings are worth pricing before you budget the work. The session infrastructure AWS tells you to delete is real and it is small: the post quantifies a two-node cache.t4g.micro ElastiCache session store at about $23 a month. Deleting it is correct and it will not fund the migration.

Reliability got reassigned, not solved

The release removed SSE stream resumability and Last-Event-ID. A broken stream now loses the request in flight, and the client has to re-issue it with a new request ID. AWS’s before/after table is honest about the consequence. The old cell said “Rely on stream resumption (Last-Event-ID) for broken responses.” The new one says “Make tools idempotent. Clients re-issue broken calls.”

Idempotency across every mutating tool is not a small ask. It is also the kind of work that never appears in a migration plan, because no plan has a step called “make your tools safe to retry.” Under the old transport a dropped stream was the transport’s problem. Now it is the tool author’s, which moves the work to a different team and a different budget.

The same reassignment happens to interactive tools. MRTR replaced server-initiated requests, and requestState is where the server’s memory goes. Because it comes back through the client, the spec tells servers to treat it as attacker-controlled input, to integrity-protect it with HMAC or AEAD when it touches authorization or business logic, and to bind it to the principal and a short expiry. Konishi adds the part people skip: none of that makes the state single-use. A one-time redemption needs a server-side check that you write.

A tool that asks for confirmation now owns a signing scheme, an expiry policy, and a replay story. That is a security surface the old held-open stream did not have.

The dual lane is where the money goes

AWS’s table shows a real deletion. ALB stickiness becomes plain round-robin. The DynamoDB or ElastiCache session store goes away. The warning sits in the row immediately after that table: “Your ALB stickiness rules and session store (DynamoDB/ElastiCache) must remain in place until you stop serving pre-2026-07-28 clients.” The recommended action is to instrument the gateway to log protocol version per request and decommission only after traffic on the old version reaches zero.

Anyone following that advice is running the old glue and the new contract at the same time. That is not a state you pass through in a sprint. It ends when the last client on a legacy revision stops calling, which the gateway log can tell you and the server changelog cannot. Legacy covers more than the one upgrade in front of you: the spec defines it as 2025-11-25 and earlier, so a client on an earlier revision keeps the lane open the same as one on 2025-11-25.

The compatibility matrix explains why you can’t just cut over. A legacy client against a modern-only server fails, and the spec says that legacy clients “have no fall-forward mechanism.” Konishi’s advice is to get one side to dual-era first so both revisions run concurrently and you can pick your direction. Before any of that, the server has to advertise both revisions, and the ordering constraints are the kind that produce an outage rather than a failed test: don’t move clients before the server advertises both, and don’t drop the old revision before every client has moved.

The stage-by-stage sequence in that guide is the most useful artifact in this set, and it also shows how differently the same upgrade behaves in each language.

In the Go SDK, StreamableHTTPOptions.Stateless has to be set to true. Without it, the handler keeps sessions and answers 2026-07-28 requests with -32022. ImportStatic sent a modern tools/call at a default handler and got exactly that, with the observation that nothing breaks for SDK clients because they fall back to the handshake. You stay on the old protocol and everything looks healthy. The C# SDK inverted the default, so upgrading the package alone changes behavior. Python serves both revisions on one endpoint with no configuration. TypeScript’s stable release notes were inconclusive enough that the guide tells you to confirm against the version you pin.

If your fleet spans languages, “we upgraded the SDKs” means three different things, only one of which is a migration.

The dual lane has a trap of its own. A stateless Go handler will still answer a legacy client’s initialize, so plain tools keep working. Tools that need input from that client do not, because the stateless handler uses a temporary session with default initialization parameters per request, so the capabilities the client declared are gone by the time the tool runs. The result is a support ticket waiting for the first customer who uses a confirmation flow.

The gateway inherited an enforcement job

The header change is the part of the release that does what the marketing said. Mcp-Method and Mcp-Name let a gateway, WAF, or rate limiter route and meter on headers “instead of parsing JSON bodies.” If you operate the gateway, that is a real simplification.

It also creates a control that did not exist. Headers and body can disagree, and the server is required to reject the mismatch with -32020. The reason makes this a security requirement rather than housekeeping. Different components on the network cannot be allowed to trust different sources of truth, or a proxy authorizes one tool while the server runs another.

Per-server enforcement of a shared control is the pattern that ages badly, and the timing supports the complaint. CIS published its MCP server benchmark on September 16, seven weeks after the release, and I covered it separately so I won’t relitigate the contents here. The point for this post is narrower: the protocol moved routing into headers, and the guidance on how to configure that routing arrived afterwards, from somewhere else.

What I would tell someone starting now

Start with the inventory, not the SDK. Find what your server keeps per session, what your gateway does with Mcp-Session-Id, and where you parse -32002. Then move state onto handles while you are still on the old revision, because that step doesn’t require the switch and it is the longest one.

Decide whether the legacy lane is a requirement or a comfort blanket. If no client is on a legacy revision, a modern-only server is simpler and the compatibility matrix stops mattering. If clients do need it, price the dual period honestly, including the elicitation gap.

Then give idempotency and requestState integrity to an owner. They are tool work, no SDK does them for you, and nobody will notice they are missing until a retry duplicates a payment.

One caution about the AWS post, since it is the best document in this set. It recommends migrating and, in its opening paragraphs, notes that AgentCore Gateway handles “protocol management and backward compatibility” for you. The technical analysis is sound. The recommendation is also a sales argument from a company selling the compatibility layer, so the before/after table is the part worth keeping and the migrate recommendation is the part to discount.

My feeling about the release is mixed. The stateless core is a better protocol and I am glad it shipped. AWS is right that the session design fought horizontal scaling, and Solo.io’s description of the old failure mode, where “restarting that instance takes down every conversation it was holding,” is a fair summary of why this had to change. What I object to is the word simplification being applied to the whole operation, because the protocol got simpler while the systems that speak it collected a pile of new responsibilities, and the bill for those arrives in the same quarter as everything else already in the backlog.

Sources

  1. The 2026-07-28 Specification — MCP blog
  2. The New MCP Roadmap — MCP blog
  3. MCP 2026-07-28 Key Changes — modelcontextprotocol.io
  4. Versioning and Compatibility — modelcontextprotocol.io
  5. Streamable HTTP transport — modelcontextprotocol.io
  6. Deprecated Features registry — modelcontextprotocol.io
  7. MCP went stateless: Is your AWS MCP server deployment well-architected? — AWS Architecture Blog
  8. Migrating to the Stateless Model Context Protocol — Hidekazu Konishi
  9. MCP Stateless Spec 2026-07-28: What to Change — ImportStatic
  10. MCP Stateless Spec Changes: The Engineering Details — Solo.io
  11. CIS Releases the CIS MCP Server Benchmark v1.0.0 — Center for Internet Security
  12. SEP-2567: Sessionless MCP via Explicit State Handles — modelcontextprotocol.io