Skip to content
datarekha
Agents August 15, 2026

MCP went stateless — what server authors have to change

The 2026-07-28 revision deleted the initialize handshake and the session header, replaced server-initiated requests with a retry pattern, and put Roots, Sampling and Logging on a twelve-month clock.

12 min read · by datarekha · mcpagentsprotocolsstatelessmigration

If you wrote an MCP server before this summer, the first function in your file is now dead code.

The initialize handler — the one that received the client’s protocol version, answered with your capabilities, and established the session that everything afterwards depended on — was removed outright in the 2026-07-28 revision of the Model Context Protocol. So was the Mcp-Session-Id header that carried that session, along with ping, logging/setLevel, the stream-resumability machinery in Streamable HTTP, and the entire concept of a server sending a request down to a client.

This is the largest break in MCP’s history, and it is not a cosmetic one. The protocol stopped being a conversation and became a series of independent requests. Everything below follows from that single decision.

The session is gone, and so is the handshake

Two SEPs did the demolition. SEP-2567 removed protocol-level sessions and the Mcp-Session-Id header from the Streamable HTTP transport, and specified that servers needing state across calls must mint an explicit handle and take it back as an ordinary tool argument. SEP-2575 removed the initialize / notifications/initialized exchange entirely.

The consequence that matters operationally: list endpoints such as tools/list, resources/list and prompts/list no longer vary per connection. There is no connection for them to vary by. Every request is self-describing, so any request can land on any instance behind a plain round-robin load balancer — no sticky routing, no shared session store, no draining problem when an instance is replaced mid-conversation.

If your server keeps a cart, a workflow, or a partially-built query in session-scoped memory today, that state is now your problem to model explicitly. Mint an opaque cart_id, return it, accept it back as a parameter, and store it somewhere every instance can reach.

Before — session-scopedclientinitializeinstance AMcp–Session–Idinstance Ainstance Binstance Csticky routing requiredB and C cannot serve this clientAfter — statelessclient_meta carriesversion · capabilitiesclientInfoinstance Ainstance Binstance Cplain round–robin, any instancecross–call state travels as an explicit,server–minted handle in the arguments
Removing the session removes sticky routing. That is the whole payoff, and everything else in the revision is a consequence of paying for it.

What a request carries now, and the RPC you must implement

With no handshake, the information the handshake used to negotiate has to travel with every request, and it does — under _meta, namespaced:

  • io.modelcontextprotocol/protocolVersion — the revision the client speaks.
  • io.modelcontextprotocol/clientCapabilities — what the client can do.
  • io.modelcontextprotocol/clientInfo — who the client is. Clients should send this on every request.

Servers reciprocate: each result’s _meta should carry io.modelcontextprotocol/serverInfo. A version the server cannot speak comes back as UnsupportedProtocolVersionError rather than failing at a handshake that no longer exists.

The one genuinely new obligation is server/discover. Every server must implement it; it advertises the protocol versions, capabilities, and identity the server supports. Clients may call it before anything else to pick a version up front, or use it as a backwards-compatibility probe on stdio. Note the asymmetry — mandatory to implement, optional to call. A client is entitled to fire tools/call as its very first message, so your server cannot assume discovery happened.

Log level moved into the same place. logging/setLevel is gone; a client sets io.modelcontextprotocol/logLevel per request in _meta, and a server must not emit notifications/message for a request that did not include it. Distributed tracing gets conventions in the same field, with traceparent, tracestate and baggage keys documented for OpenTelemetry propagation.

Asking by returning

The deepest change is that servers can no longer initiate anything. A session was the channel that made roots/list, sampling/createMessage and elicitation/create possible; with the session gone, there is nowhere to push a request.

SEP-2322 replaces all of it with Multi Round-Trip Requests. When a server needs more information to finish a call, it does not ask — it returns an InputRequiredResult, carrying resultType: "input_required" and an inputRequests field describing what it needs. The client gathers the answers and retries the original request, supplying them in inputResponses.

Because the exchange is two ordinary requests rather than one held-open conversation, the retry can land on a different instance than the first call. Any correlation the server needs must travel in the payload: the notifications/elicitation/complete notification and the elicitationId field introduced in the previous revision were both removed, and servers that need to match an elicitation across retries now encode their own identifier in requestState.

One detail with a wide blast radius: every result now carries a required resultType field, either "complete" or "input_required". Clients must treat a result from an older server that omits the field as "complete".

A multi round-trip request crosses instancesclientasks the userfor the answer1. tools/call →instance A← 2. resultType: “input_required”inputRequests · requestState3. same call + inputResponses →instance B← 4. resultType: “complete”no sharedsession statebetween them
The server asks by returning. Everything it needs to resume rides in the payload, which is exactly why a different instance can finish the job.

Roots, Sampling and Logging are on a twelve-month clock

SEP-2577 deprecates all three. They remain fully functional during the window, but new implementations should not adopt them, and the revision’s new feature lifecycle policy — Active, Deprecated, Removed, with a minimum twelve-month deprecation window — means they will eventually go.

The spec’s suggested migrations are direct:

  • Roots → pass directories or files via tool parameters, resource URIs, or server configuration.
  • Sampling → integrate directly with an LLM provider’s API from your server, rather than borrowing the host’s model.
  • Logging → write to stderr for stdio servers, or use OpenTelemetry for anything structured.

Sampling is the loss people will feel; it was how a server could be intelligent without shipping its own model or key. It was also the protocol’s largest prompt-injection surface, and few clients ever implemented it properly, which is a fair summary of why it went.

The HTTP+SSE transport, deprecated in practice since the 2025-03-26 version, is now formally Deprecated under the same lifecycle policy. Streamable HTTP is the transport.

What the transport demands now

Four smaller changes will each break something if you miss them.

Standard headers are required. Streamable HTTP POSTs must carry Mcp-Method and Mcp-Name. This is the change gateway operators wanted: a proxy, WAF, or rate limiter can now route and authorise on the operation without parsing a JSON body.

List and read results must be cacheable. tools/list, prompts/list, resources/list, resources/read and resources/templates/list now return ttlMs — a freshness hint in milliseconds — and cacheScope, either "public" or "private", controlling whether shared intermediaries may cache the response. Get cacheScope wrong on a per-user tool listing and you have built a data-leak between tenants.

Tool ordering should be deterministic. Servers should return tools from tools/list in a stable order, so clients can cache and so the prompt built from that listing hits the model provider’s cache.

Streams are no longer resumable. SSE resumability and message redelivery are gone: no Last-Event-ID, no SSE event IDs. A broken response stream loses the in-flight request, and the client must re-issue it as a new request with a new request ID. Long-running work belongs in the io.modelcontextprotocol/tasks extension, which now polls tasks/get instead of blocking on tasks/result.

Authorization: validate the issuer, retire DCR

Three authorization changes landed alongside the transport work.

Authorization servers should include the iss parameter in authorization responses per RFC 9207, and MCP clients must validate a present iss against the recorded issuer before redeeming the authorization code — a mixed-up-authorization-server defence.

Client credentials are now explicitly bound to the authorization server that issued them. Clients must key persisted credentials by issuer identifier, must not reuse them with a different authorization server, and must re-register when the authorization server changes.

And Dynamic Client Registration (RFC 7591) is deprecated as a registration mechanism in favour of Client ID Metadata Documents — a URL that is the client ID and resolves to the client’s metadata, removing the per-server registration round trip. DCR remains available for backwards compatibility with authorization servers that do not yet support CIMD, and where it is still used, clients must specify an appropriate application_type to avoid OpenID Connect redirect-URI conflicts.

The migration checklist

For an existing server, in order:

  1. Delete the initialize handler and any session store keyed by Mcp-Session-Id. Re-model cross-call state as an explicit handle passed as a tool argument.
  2. Implement server/discover, and assume it may never be called.
  3. Read _meta for protocol version, client capabilities, and client info on every request; return serverInfo in every result.
  4. Set resultType on every result. Convert any elicitation to an InputRequiredResult with inputRequests, and put whatever you need to resume into requestState.
  5. Stop calling sampling/createMessage and roots/list; call a provider API and take scope as a parameter instead.
  6. Emit ttlMs and cacheScope on the five list/read results, and order tools/list deterministically.
  7. Move long-running work to the tasks extension and make repeatable calls idempotent.
  8. Check your auth stack for iss validation and issuer-keyed credentials.

All four Tier-1 SDKs — TypeScript, Python, Go and C# — supported the revision on the day it landed, with the Rust SDK in beta, so most of steps 2 through 6 are library upgrades rather than hand-written protocol code. Steps 1 and 7 are the ones that are genuinely yours.

The protocol got less clever and much easier to operate. That is usually the right trade, and it is nearly always the one a standard makes on its way to being boring infrastructure.

Learn it as a system

Start with MCP — Model Context Protocol for the architecture, the three primitives, and the stateless transport section that covers _meta and server/discover in detail. Then read Advanced MCP primitives for the full before-and-after of the server-initiated layer, including the MRTR payload shape and the tasks and MCP Apps extensions. Finish with MCP security, because an input_required result is still a server asking your user a question.

Sources