vvek

MCP goes stateless: what the 2026-07-28 revision changes for server authors

The newest Model Context Protocol revision drops sessions and the initialize handshake. Here is what that means if you run or build an MCP server.

If you have written or run a Model Context Protocol (MCP) server, the 2026-07-28 revision of the specification is worth an hour of your time. It is the first revision in a while that changes how a server is shaped, not just what it can say. This post walks through the parts that matter most in practice. It is a reading of the official changelog, so check the spec itself before you change any code.

The big idea: no more session

Until now, an MCP connection started with a handshake. The client sent initialize, the server answered, the client confirmed, and only then did real work begin. Both sides kept that agreed state in memory for the life of the connection.

The new revision removes that handshake. As the changelog puts it, the protocol is now stateless: every request carries its protocol version and client capabilities in _meta, under keys such as io.modelcontextprotocol/protocolVersion and io.modelcontextprotocol/clientCapabilities. Clients are also asked to identify themselves on each request, and servers to identify themselves in each result.

The Mcp-Session-Id header is gone from the Streamable HTTP transport too. List endpoints such as tools/list no longer vary per connection.

Why that is good news for operators

A stateful protocol and a load balancer are an awkward pair. If request two must reach the same process that handled request one, you need sticky routing or a shared session store. Both are extra moving parts that fail in interesting ways during deploys and scale-down.

With every request self-contained, any replica can answer any call. You can run three copies of a server behind a plain round-robin balancer, roll them one at a time, and nothing breaks because nobody held a session.

If you have ever kept a design principle like "pods hold no state, the database does", this will feel familiar. The same thinking sits behind how we run the pieces of vvek, and we wrote about the isolation side of it in why every agent gets its own virtual machine.

What if my server needs state?

Some tools genuinely need continuity: a multi-step import, a browsing context, a draft being built up. The spec's answer is explicit handles. A server that needs state across calls mints an identifier and hands it back, and the client passes it as an ordinary tool argument next time.

That is a small shift in mindset, and a healthy one. The state lives where you can see it, in your own storage, keyed by something you control. It also survives a restart, which an in-memory session never did.

A sketch of what a tool result might look like in that style:

{
  "resultType": "complete",
  "content": [{ "type": "text", "text": "Draft created" }],
  "structuredContent": { "draft_id": "drf_01J9Z..." }
}

The next call, say publish_draft, takes draft_id as an argument. The identifier in the example is made up; the point is the pattern.

Discovery replaces the handshake

Without initialize, a client still needs a way to learn what a server supports. That is the job of the new server/discover call. Servers must implement it to advertise supported protocol versions, capabilities and identity. A client may call it before anything else to choose a version, or use it as a probe against older servers.

If a request names a version the server does not speak, the server answers with UnsupportedProtocolVersionError. Handle that case on purpose rather than by accident.

Changes to notifications and long operations

Three changes affect anything that pushes updates or runs for a long time.

  • subscriptions/listen replaces the old HTTP GET endpoint and the subscribe and unsubscribe calls. It is a single long-lived response stream where a client opts in to the notification types it wants, such as tools or resources list changes.
  • Resumability is removed. The spec drops SSE stream resumption (Last-Event-ID). If a response stream breaks, the in-flight request is lost and the client must send it again as a new request with a new ID. That is a good reason to make your tools safe to retry.
  • Tasks become an extension. Experimental tasks move out of the core into an official extension, with polling through tasks/get instead of a blocking result call.

There is also a new pattern for when a server needs more input mid-request. Instead of the server sending its own request back to the client, it returns an interim result with resultType: "input_required". The client retries the original request with the answers attached. It is a bit like a form that comes back to you with a missing field highlighted.

Small changes that are easy to miss

  • Results from list calls must now carry ttlMs and cacheScope, so clients can cache them and poll less.
  • Servers should return tools in a deterministic order. That helps client-side caching and also helps model providers reuse cached prompts.
  • Trace context keys (traceparent, tracestate, baggage) are documented for _meta, so a trace can follow a tool call across services.
  • Authorization gets tighter: clients must validate the iss parameter when one is present, and credentials are bound to the authorization server that issued them.

What is deprecated

Roots, Sampling and Logging are deprecated. They keep working during a deprecation window, but new code should not rely on them. The spec suggests passing paths as tool parameters, calling model providers directly, and using stderr or OpenTelemetry for logs. The old HTTP+SSE transport is formally marked deprecated as well, and Dynamic Client Registration gives way to Client ID Metadata Documents. The new lifecycle policy sets a minimum twelve-month window before anything is removed, so there is time.

A practical checklist

  1. Read the changelog end to end once. It is short.
  2. Find anywhere your server stores per-connection state, and decide which handle it should become.
  3. Make tool calls idempotent, since a lost stream means a re-sent request. Our post on idempotency keys covers the idea in general terms.
  4. Implement server/discover and return a clear error for versions you do not support.
  5. Add ttlMs and cacheScope to your list results and sort tool lists deterministically.
  6. Plan to move off Sampling, Roots and Logging over the coming year.

None of this needs to happen in a day. The old revision is not going anywhere soon, and the SDKs will absorb much of the change. But if you are designing a new server today, designing it stateless from the start will save you a migration later.

Sources

Keep reading

A short email when we publish. Confirm once, unsubscribe any time.