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.
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.
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".
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
stderrfor 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:
- Delete the
initializehandler and any session store keyed byMcp-Session-Id. Re-model cross-call state as an explicit handle passed as a tool argument. - Implement
server/discover, and assume it may never be called. - Read
_metafor protocol version, client capabilities, and client info on every request; returnserverInfoin every result. - Set
resultTypeon every result. Convert any elicitation to anInputRequiredResultwithinputRequests, and put whatever you need to resume intorequestState. - Stop calling
sampling/createMessageandroots/list; call a provider API and take scope as a parameter instead. - Emit
ttlMsandcacheScopeon the five list/read results, and ordertools/listdeterministically. - Move long-running work to the tasks extension and make repeatable calls idempotent.
- Check your auth stack for
issvalidation 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.