Blog/AI agents
MCP 2026-07-28 migration guide: what changes for stateless MCP servers
MCP 2026-07-28 removes sessions and the initialize handshake. What changes for server authors: _meta, server/discover, MRTR, auth and a migration checklist.
Balázs Csorba··9 min read
- MCP
- Protocol migration
- Stateless APIs
- OAuth
- Agents

Key takeaways
- MCP 2026-07-28, released 28 July 2026, removes the initialize handshake and the Mcp-Session-Id header, so every request carries its own version and capabilities.
- Servers must implement server/discover; clients may call it first, or send any request and handle UnsupportedProtocolVersionError.
- Multi Round-Trip Requests replace server-initiated elicitation, sampling and roots: the server returns input_required and the client retries with inputResponses.
- Cross-call state moves into explicit handles passed as tool arguments, and requestState must be integrity-protected because it is attacker-controlled.
- Roots, Sampling, Logging and Dynamic Client Registration are deprecated, with the earliest removal in the first revision on or after 28 July 2027.
MCP 2026-07-28 is the revision of the Model Context Protocol released on 28 July 2026, and it turns MCP from a stateful, session-based protocol into a stateless request/response protocol. The initialize handshake and the Mcp-Session-Id header are gone, every request describes itself, and servers can no longer send requests to the client in the middle of a call. For anyone who runs an MCP server, this is the largest breaking change since remote MCP arrived.
This guide explains what actually changes for a server author, as of September 2026: how a stateless request is built, what replaces server-initiated requests, where cross-call state goes, the authorization hardening, the deprecation timeline, and a migration checklist with a test plan. Everything below comes from the official changelog and the release post; where the spec text is the source of truth, I link to it.
Why did MCP drop sessions?
MCP dropped protocol-level sessions because they made servers hard to scale: a session lived on one server instance, so every later request had to reach that same instance. Removing them lets any request land on any instance behind an ordinary load balancer.
The 2026 roadmap (9 March 2026) put "Transport Evolution and Scalability" first of four priorities and named the problem directly: stateful sessions fight with load balancers, and horizontal scaling needed workarounds. In practice that meant sticky routing, a shared session store, or both. Serverless platforms had it worse, because there is no long-lived process to hold a session at all.
The release post states the goal in one line: any request can now land on any server instance behind a plain round-robin load balancer, without shared storage. The changelog lists 9 major and 12 minor changes against the previous revision, 2025-11-25. Most of the major ones are consequences of that single decision.
How does a stateless MCP request work?
A stateless MCP request carries everything the server needs in the request itself: the protocol version and the client's capabilities travel in _meta on every call, and HTTP requests repeat the method and tool name in headers. There is no handshake to remember.
Under 2025-11-25, a client sent initialize, got back capabilities and a session ID, confirmed with notifications/initialized, and then attached the session ID to every call. Under 2026-07-28 the versioning rules are per request:
io.modelcontextprotocol/protocolVersionandio.modelcontextprotocol/clientCapabilitiesare required in every request's_meta. A request without them is malformed and gets-32602(Invalid params), with HTTP 400.io.modelcontextprotocol/clientInfoshould be on every request, and servers should returnio.modelcontextprotocol/serverInfoin each result's_meta. Both are self-reported and must not drive security decisions.- If the server does not support the requested version, it returns
UnsupportedProtocolVersionError(-32022) with asupportedlist, and the client retries with a version from that list. - Servers must implement the new
server/discoverRPC, which advertises versions, capabilities and identity. Clients may call it first but don't have to. - On Streamable HTTP, POST requests must carry
MCP-Protocol-Version,Mcp-Methodand, fortools/call,resources/readandprompts/get,Mcp-Name(SEP-2243). If a header disagrees with the body, the server answers 400 with aHeaderMismatcherror (-32020). Gateways and WAFs can now route and rate-limit on headers without parsing JSON.
A tools/call request under the new revision looks like this (illustrative, following the spec's field names):
POST /mcp HTTP/1.1
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: search_issues
{"jsonrpc": "2.0", "id": 7, "method": "tools/call",
"params": {"name": "search_issues", "arguments": {"query": "status = Open"},
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientCapabilities": {"elicitation": {}},
"io.modelcontextprotocol/clientInfo": {"name": "my-agent", "version": "1.4.0"}
}}}What replaces server-initiated requests? Multi Round-Trip Requests
Multi Round-Trip Requests (MRTR, SEP-2322) replace the server-to-client requests elicitation/create, sampling/createMessage and roots/list. Instead of calling the client mid-call over a held-open stream, the server ends the request with an "input required" result, and the client retries the original request with the answers attached.
The MRTR spec page defines the flow. The server returns an InputRequiredResult with resultType: "input_required", an inputRequests map (keys are server-chosen IDs, values are elicitation, sampling or roots requests), and an optional opaque requestState. The client gathers the answers, then re-sends the original call with inputResponses under the same keys, echoes requestState, and uses a new JSON-RPC id. The first request is finished at that point; the retry is an independent request that any instance can handle.
A trimmed interim result for a tool that needs a confirmation looks like this:
{"jsonrpc": "2.0", "id": 1, "result": {
"resultType": "input_required",
"inputRequests": {
"confirm_transition": {
"method": "elicitation/create",
"params": {"mode": "form", "message": "Move PROJ-42 to Done?",
"requestedSchema": {"type": "object",
"properties": {"confirm": {"type": "boolean"}}, "required": ["confirm"]}}
}
},
"requestState": "<HMAC-protected blob>"
}}Three rules from the spec change how you write this code:
requestStateis attacker-controlled input. If it influences authorization, resource access or business logic, you must protect its integrity (HMAC or AEAD) and reject state that fails verification. The spec recommends binding the authenticated principal, a short expiry and a digest of the original request inside it. If a state must be used at most once, enforce that server-side.- Only
tools/call,prompts/getandresources/readmay returnInputRequiredResult, and only with request types the client declared in its capabilities. - Every result now needs
resultType:"complete"for normal results. Clients treat a missing field from older servers as complete, but your new server should always send it.
MRTR also changes timing. The client may never retry, so a tool must not leave half-done work behind while it waits for an answer. Do the side effect after the confirmation arrives, not before.
Where does state go now? Handles, Tasks and subscriptions
Cross-call state moves out of the transport and into the tools: a tool mints an explicit handle, returns it, and the model passes it back as an ordinary argument. Long-running work uses the Tasks extension, and change notifications use a new subscriptions/listen stream.
Explicit handles
The tools spec uses a shopping basket as its example: create_basket returns bsk_a1b2c3, and add_item takes basket_id as a parameter. The release post argues this works better than hidden session state because the model can see the handle and thread it between tools. The spec's design notes are worth copying into your review checklist: for authenticated servers a handle is a name, not a capability, so check the caller's authorization on every call; keep handles opaque; state the retention policy in the creating tool's description; and return a tool execution error for an expired handle so the model can recover.
Tasks and subscriptions
Tasks moved out of the experimental core into the official extension io.modelcontextprotocol/tasks (SEP-2663). The blocking tasks/result is replaced by polling with tasks/get, a new tasks/update carries client-to-server input, and tasks/list is gone. Extensions are negotiated through a new extensions field in client and server capabilities.
The old HTTP GET endpoint and resources/subscribe are replaced by subscriptions/listen, a single long-lived POST response stream where the client opts in to notification types such as toolsListChanged. Stream resumability is also gone: a broken response stream loses the in-flight request, and the client must re-issue it with a new ID. That makes idempotency your problem. If a tool call can be sent twice, it should be safe to run twice, or carry a key that lets you detect the duplicate.
List results got cheaper to cache. tools/list, prompts/list, resources/list, resources/read and resources/templates/list must include ttlMs and cacheScope ("public" or "private"), and servers should return tools in a deterministic order. A stable tool list keeps the client's prompt cache warm, which matters for cost (see prompt caching and routing).
What changed in MCP authorization?
The 2026-07-28 authorization changes close an authorization-server mix-up hole, bind client credentials to the server that issued them, and formally deprecate Dynamic Client Registration in favor of Client ID Metadata Documents (CIMD).
- Issuer validation (SEP-2468). Authorization servers should include the
issparameter in authorization responses per RFC 9207, and clients must validate a presentissagainst the recorded issuer before redeeming the code. - Credentials keyed by issuer (SEP-2352). 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.
application_typeduring registration (SEP-837). This is why some desktop and CLI clients sawredirect_urierrors for localhost callbacks.- CIMD over DCR. With CIMD, the client ID is an HTTPS URL pointing to a JSON document with at least
client_id,client_nameandredirect_uris. Authorization servers advertise support withclient_id_metadata_document_supported. DCR still works for backward compatibility.
If your server only validates tokens, most of this lands in the client and the authorization server. Your server's job is unchanged and still strict: accept only tokens issued for it, and never pass them through to upstream APIs. The MCP server security checklist covers that side.
What is deprecated, and when should you not rush?
Roots, Sampling and Logging are deprecated (SEP-2577), as are Dynamic Client Registration and the old HTTP+SSE transport. Nothing was removed yet, so the real trade-off is not "migrate or break" but how long you run a dual-era server.
The deprecated features registry gives the earliest removal for Roots, Sampling, Logging and DCR as the first revision released on or after 28 July 2027. The suggested migrations are concrete: pass directories via tool parameters or configuration instead of Roots, call the LLM provider directly instead of Sampling, and log to stderr or OpenTelemetry instead of Logging. ping and logging/setLevel are already removed from the protocol; the log level now travels per request as io.modelcontextprotocol/logLevel.
Clients in the field won't all move at once, so the spec defines a backward-compatibility path. A modern client tries a modern request first and falls back to initialize only when a 400 response body is not a modern JSON-RPC error. A server that speaks only the new revision should answer HTTP GET or DELETE with 405, ignore Mcp-Session-Id without minting one, and ignore Last-Event-ID.
My opinion: if your clients are all SDK-based and you control them, migrate in one step. If you serve third-party hosts you don't control, run dual-era for a while, and watch MCP-Protocol-Version in your access logs to decide when to drop the legacy path. The four Tier 1 SDKs (TypeScript, Python, Go and C#) support the revision, with Rust in beta, so the migration is mostly an SDK upgrade plus the design changes above.
| 2025-11-25 mechanism | 2026-07-28 | What to change in your server |
|---|---|---|
initialize handshake | Removed; version and capabilities in every _meta | Read them per request; return -32022 with supported versions |
| Nothing | server/discover (servers must implement) | Advertise versions, capabilities and identity |
Mcp-Session-Id | Removed | Move state into explicit, authorized handles |
| Server-initiated elicitation, sampling, roots | MRTR: input_required + retry | Return InputRequiredResult; sign requestState |
GET stream, resources/subscribe | subscriptions/listen | Answer GET and DELETE with 405 |
Last-Event-ID resumability | Removed | Make tool calls safe to re-issue |
Experimental tasks, tasks/result | Tasks extension, tasks/get polling | Poll; drop tasks/list |
| JSON body only | Mcp-Method / Mcp-Name headers | Reject header/body mismatches (-32020) |
Resource not found -32002 | -32602 | Update error mapping and tests |
MCP 2026-07-28 migration checklist and test plan
Migrating an MCP server to 2026-07-28 comes down to upgrading the SDK, removing every assumption of a session, and proving that two consecutive requests can hit two different instances. This is the order I'd work in:
- Upgrade to a Tier 1 SDK release that speaks 2026-07-28 and read its migration notes first; the SDKs absorb most transport changes.
- Search your code for session IDs and per-connection caches. Replace each one with an explicit handle or with data in the request.
- Implement
server/discoverand returnserverInfoin result_meta. - Rewrite every server-initiated request as MRTR, with an integrity-protected
requestStatebound to user, expiry and request. - Add
resultType,ttlMsandcacheScope, and sorttools/listdeterministically. - Validate the
Mcp-MethodandMcp-Nameheaders against the body and update error codes (-32602,-32020to-32022). - Replace Roots, Sampling and Logging with tool parameters, direct provider calls and OpenTelemetry. Trace context now has documented
_metakeys (traceparent, SEP-414). - Decide your dual-era window and log the protocol version of every request.
Test plan
- Run two instances behind round-robin and send each step of a multi-call workflow to a different one.
- Cut a response stream mid-call and check that a re-issued call does no double work.
- Tamper with, replay and expire a
requestState; each must be rejected. - Present one user's handle with another user's token; it must fail.
- Point a legacy 2025-11-25 client at the server and confirm the fallback you chose.
Stateless transport also changes how you should think about tool design, because handles and confirmations now live in your tool schemas. I wrote about that in designing MCP tools agents pick correctly. If you're planning a migration like this for your own servers, that's part of what I do as an AI engineer.
Sources
- The 2026-07-28 Specification – MCP blog, 28 July 2026
- MCP 2026-07-28 Key Changes (changelog)
- MCP 2026-07-28: Versioning and Compatibility
- MCP 2026-07-28: Streamable HTTP transport
- MCP 2026-07-28: Multi Round-Trip Requests
- MCP 2026-07-28: Tools (state handles)
- MCP 2026-07-28: Client registration (CIMD)
- MCP 2026-07-28: Deprecated features registry
- The 2026 MCP Roadmap – MCP blog, 9 March 2026
- RFC 9207: OAuth 2.0 Authorization Server Issuer Identification
Frequently asked questions
Does MCP 2026-07-28 still support the initialize handshake?
No. The 2026-07-28 revision removes initialize and notifications/initialized. Every request carries its protocol version and client capabilities in _meta instead. A server can still serve older clients by also implementing the 2025-11-25 behavior, and modern clients fall back to initialize when a 400 response body is not a recognized modern JSON-RPC error.
How do I keep state between MCP tool calls without sessions?
Mint an explicit handle in a tool, such as a basket or workflow ID, return it in the result, and accept it as an ordinary argument on later calls. Store the state server-side under that key, check the caller's authorization against the handle on every call, keep handles opaque, and return a clear error when a handle has expired.
What is requestState in MCP Multi Round-Trip Requests?
requestState is an opaque string the server returns with an input_required result and the client echoes back on the retry. It lets a stateless server resume its work. The spec treats it as attacker-controlled input: if it affects authorization or business logic, protect it with an HMAC or AEAD, bind it to the user, a short expiry and the original request, and reject anything that fails verification.
When will Roots, Sampling and Logging be removed from MCP?
They are deprecated in 2026-07-28 but still fully functional. The deprecated features registry lists the earliest removal as the first spec revision released on or after 28 July 2027, and the actual removal is a maintainer decision. The suggested replacements are tool parameters or configuration for Roots, direct LLM provider calls for Sampling, and stderr or OpenTelemetry for Logging.