MCP went stateless and the session just moved

MCPCloudflareArchitectureDeveloper ToolsAI CodingSecurity

August 23, 2026

Dark editorial inspect panel with a Worker request card, a dashed line to a sealed capsule, and the words the session just moved on the capsule

Stateless MCP deleted the protocol session. Continuity packed itself into the blob the client sends back on retry. Cloudflare's August post called the rewrite a fully stateless protocol that a Worker can speak with no Durable Object. That sentence is true for the handshake. It is the wrong reading of elicitation, carts, and tokens.

Stateless was the headline they sold#

Split HUD with a dim WORKER card and empty SESSION slot on the left, a glowing STATELESS stamp on the right
The stamp is true for the handshake, and the empty slot is still drawn underneath.

The docs invited the naive model. The 2026-07-28 changelog removes initialize, Mcp-Session-Id, and protocol sessions from Streamable HTTP. The MCP project's release post said the core moved from a bidirectional stateful protocol to request/response. A round-robin load balancer can take any POST.

That is the reading a lot of people wanted in 2025, when a well-behaved remote server meant sticky sessions, held streams, and message replay. Cloudflare's post is blunt that McpAgent is no longer required to speak the protocol. A Worker with createMcpHandler is enough for tools, prompts, and resources.

Believe that headline for the transport. Then look at what still has to come back on the next call. The session did not vanish. It changed pockets.

punkpeye said on the spec's HN thread that a huge slice of Glama's gateway bugs came from persisting server state. That pain was real. The rewrite answers it for the protocol layer, which is why the headline landed so clean. The leftover work is deciding which state was protocol theater and which state is still a cart.

The handshake still happens, one POST later#

A POST glass card carrying three teal chips lettered VERSION, CLIENT, and CAPS
Identity did not leave. It rides every call now.

The cheap version of stateless is that the handshake's payload left. It did not. Every request now carries io.modelcontextprotocol/protocolVersion and io.modelcontextprotocol/clientCapabilities in _meta. Client identity is a SHOULD on each call. Server identity stamps the result.

server/discover exists for clients that want capabilities up front. It is optional. The spec's statelessness clause is the part people skip. A server MUST NOT infer version, identity, or capabilities from an earlier request on the same connection.

A leftover Mcp-Session-Id is not a compatibility gift. Streamable HTTP says ignore it, and do not mint or echo one. GET and DELETE on the MCP endpoint answer 405. The old session header is litter.

This is not the byte-tax argument from Every Stateless MCP Server Rebuilds State Somewhere. That post measured _meta against the handshake it replaced. The point here is narrower. Identity never left the wire. It just stopped living in a clerk's drawer.

The open stream became a retry#

Left a broken dim stream labeled DEAD STREAM, right a sealed teal capsule lettered REQUEST STATE on a RETRY path
The open stream died. The client carries the suitcase back.

This is where the session actually moved. Old elicitation needed a held SSE stream so the server could fire elicitation/create mid-call. That stream was the sticky part people built Durable Objects to keep warm. Timeouts, drained instances, and replay tables were the tax.

Multi Round-Trip Requests kill that channel. The server returns resultType: "input_required" with inputRequests. The client collects the answer and retries the original tool, prompt, or resource. The JSON-RPC id MUST change. The two POSTs are independent requests.

Independent except for the suitcase. The spec lets the server encode needed context into requestState, an opaque string the client MUST echo byte-for-byte. Official TypeScript docs are blunter than the spec. createMcpHandler builds a fresh server per request. inputResponses hold only the latest round. requestState is the only cross-round memory.

You'll hit this the first time a deploy tool asks for confirmation, then a second question about the environment. Round two does not still have round one's form sitting in a map on the Worker. If you needed that confirmation later, it had to be sealed into requestState on the way out.

The official SDK says to mint only what earlier rounds already proved. Treat the blob as attacker-controlled. HMAC it, bind principal, method, and a short TTL. The codec they ship is signed, not encrypted, so keep secrets out of it.

POST /mcp HTTP/1.1
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: deploy

{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"deploy","arguments":{"env":"prod"},"inputResponses":{"confirm":{"action":"accept","content":{"ok":true}}},"requestState":"AEAD-protected blob","_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{"elicitation":{"form":{}}}}}}

That second POST can land on a different isolate. Nothing in the protocol requires the first Worker to still be alive. The session is the blob. Lose it, and the approval is gone. Forge it, and you have a confused deputy unless the verify hook rejects the token.

Mcp-Method and Mcp-Name ride the same POST, which is why MCP is now traffic a firewall can name exists. A gateway can sort tools/call without parsing JSON. This post does not retell that detector. The header surface is how a session-free POST became ordinary HTTP.

Handles replaced the session map#

Application state was never the protocol's job. Sessions just made it look like it was. SEP-2567 is the complementary removal. Instead of a cart keyed on Mcp-Session-Id, a tool returns basket_id and later calls take that id as an argument. Linear issues, GitHub PRs, Stripe customers already worked that way.

The SEP's 1000-repo sample is the receipt that this is not a mass migration. What it does not show is a fleet of sticky routers. What it does show is that tools/list could still vary across session boundaries, so clients could not cache it.

  • 90 percent never referenced the session id at application level
  • 2.5 percent keyed app state on it
  • 0.7 percent were sticky gateways

whitew1994 asked on the spec's HN thread whether optional stateful tool calling would come back, because making the model carry the id wastes tokens. Fair complaint. The id also survives in the transcript, which is how a chat restart finds the cart without a private session table. Token cost versus a Durable Object you could not drain. Pick the cost you can see.

Durable Objects wait for actual state#

A lit WORKER card beside a dim Durable Object cube lettered CART, TOKEN, and BROWSER
Speaking MCP needs no object. A cart, a token, or a browser still does.

createMcpHandler graduated into the official TypeScript SDK. Cloudflare still wraps it at agents/mcp/server with Origin checks and notify for subscriptions/listen. The v0.20.0 changelog is the line that matters for Workers. Tools, prompts, resources, and elicitation can run with no transport session and no Durable Object.

McpAgent is deprecated and feature-frozen. No removal version is announced. The migration guide keeps a legacy lane for protocol sessions, event replay, standalone GET streams, and pushed server-to-client requests. isLegacyRequest() is the drain, not the destination.

Do not yank the Durable Object binding because the protocol got religion. Cloudflare's own post says Durable Objects remain the right primitive when the application needs coordinated state. Store a cart in a Durable Object, D1, KV, or R2, and address it with an authenticated handle. URL-mode elicitation is the sharp edge. Third-party OAuth tokens must not transit the MCP client. The elicitation spec says that token store makes the MCP server stateful.

You'll ship createMcpHandler, delete the object, then watch a billing tool's URL mode bounce because the callback has nowhere to bind the token. Protocol-stateless and app-stateless are different jobs. Mix them up and the missing store shows up as a random 400 on retry.

Delete the sticky route, keep the store#

This week, take three things out of the old stack.

  • Sticky routing on Mcp-Session-Id
  • The standalone GET stream
  • McpAgent as the default server

Keep three things that actually hold continuity.

  • Integrity-protected requestState
  • Explicit handles for anything that outlives a POST
  • A real store when the app has a cart, a browser, or a third-party token

Auth is a side door, not the plot. Pre-register when you already know the client. Prefer Client ID Metadata Documents when you do not.

Dynamic Client Registration is deprecated. The deprecated registry sets earliest removal at the first revision on or after 2027-07-28. Cloudflare's "after summer 2027" line is that date in marketing clothes. It is eligibility, not a cliff.

Dual-era will linger. Official clients still default to the 2025 handshake unless you opt into versionNegotiation. Punkpeye said on the spec's HN thread that the rewrite is wire-incompatible in both directions, and that gateways will eat the mess. Fine. Eat it on a reject-or-shim lane. Do not rebuild sticky sessions in user land and call it compatibility.

The protocol went stateless. The session just moved into the retry. Pack it like a suitcase you do not trust, and put the valuables in a store you own.

Where the session went

Does going stateless delete stateful tool calling?

No. Protocol sessions are gone. Application state is not. SEP-2567's replacement is an explicit handle, a basket_id the model passes back as an ordinary argument. The HN objection that the model now burns tokens carrying that id is real. The id is also visible in the transcript, which is the point.

asked on news.ycombinator.com
Do existing MCP clients keep working after the rewrite?

On the wire, 2026-07-28 is a clean break. Cloudflare's createMcpHandler still answers ordinary 2025 tools, prompts, and resources per request with no session id. Sessionful GET streams, pushed elicitation, and replay stay on a temporary legacy lane you drain. Punkpeye's warning that the protocol is wire-incompatible in both directions is the reason that lane exists.

asked on news.ycombinator.com
Does a remote MCP server still need to persist session state?

Not to speak the protocol. Cloudflare's Worker path serves tools, prompts, resources, and elicitation with no transport session and no Durable Object. Persist the cart, the browser, or a third-party OAuth token behind a handle. Glama's gateway bugs were the session store. That store is no longer the protocol's problem.

asked on news.ycombinator.com
Where do you stash credentials when the upstream API is basic auth?

Not in form-mode elicitation. The elicitation spec says servers MUST NOT use form mode for passwords, API keys, access tokens, or payment credentials, and MUST use URL mode for those. URL mode sends the user to a page you control. The token store after that is application state, which is the one place that page says the MCP server must be stateful. dan-kwiat's HN reply pointed at URL elicitation for exactly this case.

asked on news.ycombinator.com
Share

Newsletter

New posts land in your inbox when they publish. No spam, unsubscribe anytime.

Prefer RSS