MCP 2026-07-28: The Server Can No Longer Speak First
Sessions, ping, server-initiated requests, resumable streams, and the GET endpoint all went in one release. Roots, Sampling, and Logging are deprecated on top. Every removal moves initiative from the server to the client, and that is the migration.
The 2026-07-28 MCP specification shipped on 28 July 2026. It is usually described as the release that made MCP stateless, which is true and undersells it.
Read the removals as a list and one pattern falls out. The initialize handshake and Mcp-Session-Id are gone. ping is gone. logging/setLevel is gone. notifications/roots/list_changed is gone. The HTTP GET endpoint and resources/subscribe are gone. SSE stream resumability is gone. Server-initiated requests, which is to say roots/list, sampling/createMessage, and elicitation/create, are gone as a mechanism. And Roots, Sampling, and Logging are deprecated outright.
Every one of those was a way for the server to start something. After this release there are none. The client asks, the server answers, and if the server needs more it has to say so in an answer and wait to be asked again.
The mechanic that replaces server-initiated requests
Multi Round-Trip Requests is the piece worth understanding before anything else, because it is how a server now gets information it did not receive.
Instead of the server sending its own request back down the connection, every result carries a required resultType field. Ordinary answers are "complete". When the server needs more, it answers the original call with resultType: "input_required" and an inputRequests field describing what it wants. The client then retries the same request, carrying inputResponses.
resultType as "complete", which is what keeps old servers working against new clients.The consequence for server code is that a request handler is now a function that may be called more than once for the same logical operation, with more information each time. Anything you were holding between the outbound request and its reply has to be encoded into what you send back. The spec’s own answer for correlating across retries is requestState, which the server encodes itself, because the identifier that used to do that job for elicitation was removed along with the completion notification.
Where state lives now
The changelog is unusually direct about this: servers that need cross-call state “use explicit, server-minted handles passed as ordinary tool arguments.”
That is the whole design. There is no session to hang things on, so a handle becomes a value in the conversation, which means the client carries it and any instance of your server can pick it up. List endpoints no longer vary per connection, so tools/list returns the same thing to everyone and can be cached by intermediaries.
Connection metadata moved to _meta on every message under reverse-DNS keys: io.modelcontextprotocol/protocolVersion and io.modelcontextprotocol/clientCapabilities, with clients identifying themselves via io.modelcontextprotocol/clientInfo and servers doing the same in each result’s _meta. A version mismatch is an UnsupportedProtocolVersionError rather than a failed handshake.
Servers must implement server/discover, which advertises supported protocol versions, capabilities, and identity. Clients may call it before anything else, or use it as a compatibility probe on stdio.
Log level went the same way. There is no logging/setLevel; a client sets io.modelcontextprotocol/logLevel per request in _meta, and servers must not emit notifications/message for requests that did not include it. That is a real behavioral rule, not a style note: an unconditional log emission is now a spec violation.
The two removals that will surprise operators
Two changes are infrastructure problems rather than code problems.
Streamable HTTP dropped SSE stream resumability. The Last-Event-ID header and SSE event IDs are gone, and the spec says 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. If your server does expensive non-idempotent work inside a long response, a dropped connection now means the client will legitimately do it again. Idempotency keys were previously a nicety here. They are not any more.
And the HTTP GET endpoint went away, along with resources/subscribe and resources/unsubscribe. Change notifications now come through subscriptions/listen, a single long-lived POST response stream that the client opts into by naming the types it wants: toolsListChanged, promptsListChanged, resourcesListChanged, resourceSubscriptions. The server acknowledges and tags notifications with io.modelcontextprotocol/subscriptionId. Request-scoped notifications such as notifications/progress keep flowing on the response stream of the request they belong to, not on this one.
Meanwhile every list result now carries required ttlMs and cacheScope fields, the latter being "public" or "private" to say whether shared intermediaries may cache it. Combined with the deterministic ordering the spec now asks for in tools/list, the direction is clear: your tool list is meant to be cached, by clients and by things in between.
What is deprecated, not removed
Roots, Sampling, and Logging are all deprecated as features. They still work through the deprecation window, and the spec names the migrations: pass directories or files as tool parameters or resource URIs instead of Roots, call an LLM provider’s API directly instead of Sampling, and write to stderr or use OpenTelemetry instead of Logging.
Sampling being deprecated is the one worth sitting with. It was the mechanism by which a server could ask the host to run an inference on its behalf, and it was the most architecturally distinctive thing in the protocol. The replacement is “talk to a model yourself,” which is simpler and also means the server now needs its own credentials and its own bill.
The old HTTP+SSE transport, deprecated since 2025-03-26, is now formally Deprecated under the lifecycle policy. And OAuth 2.0 Dynamic Client Registration is deprecated in favor of Client ID Metadata Documents, which is a separate migration for anyone doing remote server auth.
Extensions, and one method that did not survive
Rather than growing the core, optional capabilities now live as independently versioned extensions negotiated through extensions maps on client and server capabilities. Three ship in the repository: apps, auth, and tasks.
Tasks moved out of the experimental core into io.modelcontextprotocol/tasks and was redesigned on the way. The blocking tasks/result is replaced by polling with tasks/get, tasks/update carries client-to-server input, and servers may return task handles unsolicited without a per-request opt-in.
tasks/list did not survive, and the reason is instructive. You cannot scope “list all tasks” safely once there is no session to scope it to. Whose tasks, on which instance? Without a session boundary the question has no clean answer, so it was cut. If your design assumed you could enumerate outstanding work, track handles on the client side or key them to something durable you control.
Migrating, cheapest first
Grep for Mcp-Session-Id, for your initialize handler, and for any map keyed by session or connection. That inventory is the actual scope; the rest is mechanical.
Move connection metadata reads from the handshake to _meta, and implement server/discover. Because the metadata now arrives on every message, the “we have not initialized yet” ordering case disappears, which quietly deletes a class of bugs.
Convert anything you kept per-connection into a server-minted handle passed as a tool argument, or into a task, or into external storage keyed by something the client sends every time.
Rewrite server-initiated requests as MRTR. This is the largest code change and the one most likely to need a redesign rather than a refactor, because a handler that used to block on a reply now has to return, be forgotten, and be re-entered.
Fix the gateway. If it routed on session ID, that header does not exist; route on the now-required Mcp-Method and Mcp-Name headers instead, or drop affinity entirely, which is the point of the whole release. This is usually the biggest operational win available and it is infrastructure work, so it needs scheduling rather than a ticket.
Nothing forces you to move on a date. Old spec versions keep working and clients that omit resultType are treated as "complete". What changes is which clients can talk to you, and the spec has now published the error codes for when they cannot, renumbered into a reserved range: HeaderMismatch at -32020, MissingRequiredClientCapability at -32021, UnsupportedProtocolVersion at -32022.
Spec contents, SEP numbers, and deprecations were read from the 2026-07-28 changelog in the modelcontextprotocol repository on 23 August 2026, where the release tag is dated 28 July 2026.