Skip to content
Chiplab logo
Chiplab logo
Back to Now

MCP went stateless. Some of us are holding a running chip.

I read the 2026-07-28 changelog the way everyone reads a changelog, scanning for the line that breaks me. It was the first bullet. In that protocol revision, sessions are gone. Not deprecated, not optional, removed.

Two weeks later I think the decision is correct, which is an annoying thing to conclude when you maintain a server whose whole job is keeping something alive between calls. So this is not a complaint. The protocol did not drop state. It moved responsibility for state across a line, was deliberate about the easy side of that line, and left the hard side to whoever is standing on it.

What changed when MCP went stateless on 28 July

From the changelog, verbatim:

"1. Remove protocol-level sessions and the Mcp-Session-Id header from the Streamable HTTP transport. List endpoints (tools/list, resources/list, prompts/list) no longer vary per-connection. Servers that need cross-call state use explicit, server-minted handles passed as ordinary tool arguments (SEP-2567)."

"2. Make MCP stateless: remove the initialize/notifications/initialized handshake. Every request now carries its protocol version and client capabilities in _meta (io.modelcontextprotocol/protocolVersion, io.modelcontextprotocol/clientCapabilities)... Version mismatches return UnsupportedProtocolVersionError (SEP-2575)."1

The base specification states the rule normatively: MCP "is a stateless protocol: all the information needed to process a request is contained in the request itself. A server processes each request independently; no state should be inferred from previous requests, even those on the same connection or stream." Servers "MUST NOT rely on prior requests over the same connection to establish context (e.g., capabilities, protocol version, client identity)."2

Diagram comparing MCP session-scoped state before 2026-07-28 with stateless requests carrying explicit handles afterwards

The scaling argument is right, and I'll grant it before I argue

Most takes on this revision skip to the grievance. I'd rather concede the case first, because it is a strong one and it was made in public. SEP-2575's motivation, verbatim:

"1. Impediment to Scalability: The most critical issue is the difficulty of load balancing stateful MCP. A simple stateless load balancer (e.g., L4/L7 round-robin) cannot be used, as it would route a client's requests to different backend servers, none of which would have the correct session state. Operators are forced to implement complex and fragile solutions like sticky sessions..."

"2. Poor Resilience and Fault Tolerance: In a stateful model, if the specific server instance handling a client session fails, that session state is lost..."

"3. Increased Implementation Complexity: ... Developers must implement logic to create, manage, and eventually garbage-collect per-client session state. This is a common source of bugs and memory leaks."3

All three are true. I have written the fragile sticky-session config and I have watched state evaporate because an instance rolled during a deploy. The third stings most: per-client lifecycle code is where the leaks live, and no amount of care makes it fun.

I'd have made the same call. A protocol that supports both shapes supports neither well.

The replacement is a name you carry yourself

There is exactly one normative sentence about cross-call state, in the base specification: state spanning multiple requests "MUST be referenced by an explicit identifier the client passes on each request."2 That is a MUST, and it constrains the wire shape.

Everything else about handles is guidance, and the specification says so itself in its own first line on the subject. From "Stateful Tools":

"This section is non-normative guidance for tool design. The protocol has no concept of a state handle; from the wire's perspective a handle is an ordinary string in a tool result and an ordinary argument to subsequent tool calls."

"Servers that need to maintain state across calls — a shopping cart, an open browser context, a database transaction — should do so by returning an explicit handle from a creation tool and accepting that handle as an argument on subsequent calls."4

The launch post puts the same idea in the maintainers' own words. This is blog commentary, not specification: "If your server needs to carry state across calls, mint an explicit handle from a tool and have the model pass it back as an argument. We found this works better than session state hidden in the transport - the model can see the handle and thread it between tools."5

Three kinds of text, three weights:

Kind of sourceExampleWhat it binds
Normative specificationcross-call state "MUST be referenced by an explicit identifier"Every compliant server
The spec's own non-normative guidancethe "Stateful Tools" handle pattern, labeled non-normative in its first sentenceNobody; design advice from a good source
Maintainer blog commentary"the model can see the handle and thread it between tools"Nobody; an opinion, and one I mostly share

Anyone telling you the specification requires the handle pattern has collapsed row one into row two. The handle pattern is the recommended way to satisfy the MUST, not the rule.

The gradient runs past where the examples stop

Look again at the three examples the guidance picks: a shopping cart, an open browser context, a database transaction. They are not arbitrary. They sit on a gradient, and the gradient is the story.

Behind the handleCost to rehydrate on another instanceSurvives a restart?
Shopping cartOne row readYes, trivially
Open browser contextRe-navigate, replay cookies and storage; lose the JS heap, scroll, timersPartially, with real effort
Database transactionLocks and snapshot are bound to one connection in one processNo
Firmware running on a virtual MCURegisters, RAM, flash contents, peripheral timers, a half-full UART FIFO, an interrupt about to fireOnly if something explicitly snapshots it

Row one is what the pattern was designed against, and for it the pattern is perfect. Statelessness buys real horizontal scale and costs you a SELECT.

Row four is where I live. If one tool call boots firmware on a virtual STM32 and the next reads what came out of the UART, the thing behind that handle isn't data, it's an execution. The protocol's contract for row four is identical to its contract for row one: give it a name, pass the name back.

The sandbox-versus-target distinction is one I've written about before, in E2B gives agents a computer. A fresh container per call is a legitimate design. A fresh chip per call means the firmware you flashed is gone.

Diagram of a gradient from a shopping cart to a running MCU core showing increasing cost to rehydrate state behind an MCP handle

What the spec constrains, and what it leaves to you

The sloppy version of my argument is that the specification ignores stateful servers. It doesn't. There is a MUST, there is a documented pattern, and the SEP is unusually honest about the limits of both. SEP-2567 says outright that the handle pattern is not a protocol feature:

"That third point is not a protocol change. There is no handles/* method, no handle type in the schema, no wire-level concept of a handle at all... 'Explicit state handles' is a tool-design pattern that the spec documents and recommends — in the same way it might document pagination or error-message conventions — not something it implements."6

In future work it concedes the blind spot: "nothing marks basket_id as a state handle to the client or model — the relationship... is inferred from naming and tool descriptions, not declared."6

So the gap is narrow, and I want it stated narrowly. The specification mandates how cross-call state is referenced. It says nothing about where the resource behind that reference lives, how it survives a restart, or how it stays addressable under the horizontal scale-out that removing sessions was meant to enable.

For a cart those three questions share one answer, and it's your existing database. For something that cannot be cheaply serialized and rehydrated, those three questions are the entire product. The work didn't evaporate when the transport stopped carrying it. When I went through every hardware MCP server I could find, nearly all were this shape: one long-lived thing, fronted by tool calls.

The authors knew exactly where the sharp edge was

Multi round-trip requests replaced server-initiated requests. Per the spec they exist to handle them "without requiring a shared storage layer across server instances or requiring stateful load balancing."7

Which makes the exception instructive. MRTR does define a state carrier: requestState, "an opaque string meaningful only to the server", which clients "MUST NOT inspect, parse, modify, or make any assumptions about its contents". And it has teeth. If requestState influences authorization, resource access or business logic, servers "MUST protect its integrity (e.g. HMAC or AEAD) and MUST reject state that fails verification."7

So the authors were willing to specify a state-carrying field when its scope was one round trip, and they specified it with mandatory integrity protection because they knew what happens to state crossing a trust boundary.

Tasks are about a call that hasn't finished

The immediate objection is that Tasks solve this. They don't, and the distinction fits in a line: Tasks model execution progress, not a standing resource.

Tasks moved out of core into the io.modelcontextprotocol/tasks extension.8 Per the release candidate post — commentary, not spec — "Task creation is server-directed: the client advertises the extension and the server decides when a call should run as a task. tasks/list is removed because it can't be scoped safely without sessions."9 Its lifecycle terminates: completed, failed, cancelled.

That's a call you're waiting on. What a stateful server owns is different: a thing deliberately still there after the call that created it returned successfully, expecting five more unrelated calls to find it again. A task that never terminates is a bug. A handle whose resource never terminates on its own is the entire point.

Twelve months for the soft features, nothing for the hard one

Put two policies side by side. Roots, Sampling and Logging are deprecated in 2026-07-28 with a runway of at least twelve months: earliest removal is the first revision on or after 2027-07-28.10

The session removal got no deprecation window. SEP-2567: "Rollout is a clean break: sessions are removed in the next spec version, with no deprecation window. Servers that currently rely on session-scoped state stay on the current protocol version until they have migrated to explicit handles."6

That does not make old servers noncompliant or unreachable. Protocol negotiation lets a client speak the older revision to an unmigrated server. But it does put the migration cost in one place: the stateful server keeps working on the old revision while its owner builds the explicit state layer required to adopt the new one.

Diagram showing MCP specifying how state is referenced while leaving storage, restart survival and addressability to the implementer

What we do about it

Every handle we mint points at row four of that gradient, because we run firmware on virtual instances of real microcontrollers. Chiplab's answer is the boring one the spec implies and does not describe: a run is a first-class object with an identity of its own, it lives outside the process that served the request, and any instance can address it. Moving to 2026-07-28 means the transport no longer participates in that at all, which, irritatingly, makes the design cleaner.

The wider point is the one I keep circling. As hardware becomes an API, the servers fronting it inherit every problem a stateless protocol pushes downward, and no shared database makes a half-executed interrupt rehydrate on another box. The protocol got this right; it should not know what a target is. But an agent that wants to hold a chip still needs someone to hold the chip, and as of 28 July that someone is unambiguously us.

Sources

Footnotes

  1. Model Context Protocol, "Specification changelog, 2026-07-28," modelcontextprotocol.io, July 28, 2026. https://modelcontextprotocol.io/specification/2026-07-28/changelog

  2. Model Context Protocol, "Base Protocol — Statelessness," MCP Specification 2026-07-28, July 28, 2026. https://modelcontextprotocol.io/specification/2026-07-28/basic#statelessness 2

  3. Model Context Protocol, "SEP-2575: Make MCP stateless," modelcontextprotocol GitHub repository, 2026. https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2575

  4. Model Context Protocol, "Server — Tools, Stateful Tools," MCP Specification 2026-07-28, July 28, 2026. https://modelcontextprotocol.io/specification/2026-07-28/server/tools#stateful-tools

  5. Model Context Protocol, "MCP 2026-07-28 release," MCP Blog, July 28, 2026. https://blog.modelcontextprotocol.io/posts/2026-07-28/

  6. Model Context Protocol, "SEP-2567: Remove protocol-level sessions," modelcontextprotocol GitHub repository, 2026. https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2567 2 3

  7. Model Context Protocol, "Base Protocol — Patterns, Multi Round-Trip Requests," MCP Specification 2026-07-28, July 28, 2026. https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/mrtr 2

  8. Model Context Protocol, "SEP-2663: Tasks extension," modelcontextprotocol GitHub repository, 2026. https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2663

  9. Model Context Protocol, "2026-07-28 release candidate," MCP Blog, July 28, 2026. https://blog.modelcontextprotocol.io/posts/2026-07-28-release-candidate/

  10. Model Context Protocol, "SEP-2596: Deprecation policy," modelcontextprotocol GitHub repository, 2026. https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2596

Daniel Frassinelli
Published Aug 13, 2026