MCP 2026-07-28: delete the session, make the protocol stateless again

The new MCP revision removes the initialize handshake and the Mcp-Session-Id header, and turns cross-call state into explicit server-minted handles. What changed, why, and what it means for an existing server implementation.

MCP’s new revision (2026-07-28) is a subtraction. The previous one was 2025-11-25, and the headline difference fits in one sentence:

There is no longer a session at the protocol layer.

What was removed:

  • Protocol-level sessions and the Mcp-Session-Id header from the Streamable HTTP transport
  • The initialize / notifications/initialized handshake
  • ping, logging/setLevel, notifications/roots/list_changed
  • The old HTTP GET endpoint, plus resources/subscribe / resources/unsubscribe

Why remove it

Sessions are state, and a stateful protocol over HTTP has an old problem: the moment a request lands on a different machine, the context is gone. Sticky sessions or shared storage — both expensive, and both turn “scale horizontally” into a slogan.

The old approach issued an Mcp-Session-Id per connection and carried it on every subsequent request. That is completely fine on a developer laptop and miserable in production.

The new revision demotes state to data:

  • Every request carries its protocol version and client capabilities in _meta (io.modelcontextprotocol/protocolVersion, io.modelcontextprotocol/clientCapabilities)
  • Clients SHOULD identify themselves on each request (io.modelcontextprotocol/clientInfo), and servers SHOULD identify themselves in every result’s _meta (io.modelcontextprotocol/serverInfo)
  • Anything that needs cross-call state uses an explicit, server-minted handle passed as an ordinary tool argument (SEP-2567)

That last point is the heart of the change: state was not eliminated, it was moved from “the protocol remembers it for you” to “a string you can see, store, expire, and revoke”.

Version negotiation moved too

The old flow negotiated once during the handshake. There is no handshake to negotiate in, so:

  • A version mismatch returns UnsupportedProtocolVersionError (SEP-2575)
  • A new server/discover RPC exists, and servers MUST implement it, advertising their supported protocol versions, capabilities, and identity
  • Clients may call it before anything else for up-front version selection, or use it as a backward-compatibility probe on STDIO

Subscriptions became one stream

Previously, server-to-client change notifications went through the HTTP GET endpoint (a long-lived connection) plus resources/subscribe. Now there is a single subscriptions/listen: one long-lived POST-response stream. Clients opt in to specific types (toolsListChanged, promptsListChanged, resourcesListChanged, resourceSubscriptions); the server acknowledges and tags each notification with io.modelcontextprotocol/subscriptionId.

One detail worth noticing: request-scoped notifications such as notifications/progress and notifications/message still travel on the response stream of the request they belong to, not on subscriptions/listen. Mixing the two would make “whose progress is this” unanswerable.

Logging follows the same logic. The level is now per-request (io.modelcontextprotocol/logLevel in _meta), and servers MUST NOT emit notifications/message for requests that did not include that field. That single constraint kills “the quiet client gets drowned in logs” at the specification level.

Also: tasks left the core

Experimental tasks moved into an official extension, io.modelcontextprotocol/tasks. This is healthy — a core protocol should not host experimental features indefinitely; extensions can evolve and be retired on their own schedule.

What it means if you already wrote a server

If you implemented against 2025-11-25:

  1. Delete your session store. Biggest win, and the main motivation for the revision.
  2. Implement server/discover. It is a MUST; without it, new clients cannot do version selection.
  3. Turn cross-call state into handles. Anything you used to stash in a session — user identity, temporary context, pagination cursors — becomes an explicit tool argument.
  4. Revisit logging. logging/setLevel used to be a global switch. It is per-request now, and omitting it means “do not speak”.

Where I land

This revision trades a design that looks convenient for one that looks verbose, and what it buys is concrete: any machine can serve any request. No sticky sessions, no shared session store.

The price is a few dozen extra bytes of _meta per request. On HTTP that trade is almost always worth it — statelessness is why HTTP is still here, and MCP had been standing on its shoulders while bolting an extra wheel onto its side.

← Back to all posts

Comments

…