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.

··9 min read

  • MCP
  • Protocol migration
  • Stateless APIs
  • OAuth
  • Agents
Pipeline of five migration steps for MCP 2026-07-28: upgrade the SDK, remove sessions, add server/discover, rewrite prompts as MRTR, test across instances

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/protocolVersion and io.modelcontextprotocol/clientCapabilities are required in every request's _meta. A request without them is malformed and gets -32602 (Invalid params), with HTTP 400.
  • io.modelcontextprotocol/clientInfo should be on every request, and servers should return io.modelcontextprotocol/serverInfo in 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 a supported list, and the client retries with a version from that list.
  • Servers must implement the new server/discover RPC, 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-Method and, for tools/call, resources/read and prompts/get, Mcp-Name (SEP-2243). If a header disagrees with the body, the server answers 400 with a HeaderMismatch error (-32020). Gateways and WAFs can now route and rate-limit on headers without parsing JSON.
Stateful MCP 2025-11-25 versus stateless MCP 2026-07-28 Left, the 2025-11-25 flow: the client sends initialize, the server instance returns a result with an Mcp-Session-Id, the client sends notifications/initialized, then tools/call with the session ID, so every call must reach the same instance. Right, the 2026-07-28 flow: an optional server/discover call returns versions and capabilities, then tools/call carries the protocol version and client capabilities in _meta and returns a result with resultType complete, so any instance can answer. 2025-11-25 · statefulclientinstance Ainitializeresult + Mcp-Session-Idnotifications/initializedtools/call + session IDresultsticky routing: the sessionlives on instance A2026-07-28 · statelessclientany instanceserver/discover (optional)versions, capabilitiestools/call + _metaversion · capabilitiesresult (complete)round-robin is fine: eachrequest describes itself
Before and after. In 2025-11-25 the handshake creates a session that pins the client to one instance. In 2026-07-28 each request carries its protocol version and client capabilities in _meta, server/discover is optional for the client, and any instance can answer.

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.

Multi Round-Trip Request sequence Three lifelines: user, client and server. The client sends tools/call with id 1. The server answers with an input_required result that contains inputRequests and a requestState, which ends request 1. The client asks the user through an elicitation form and gets an answer. The client then sends tools/call with id 2, carrying inputResponses and the echoed requestState. The server verifies the state and returns a result with resultType complete. userclientservertools/call (id 1)input_requiredinputRequests · requestStaterequest 1 is finishedelicitation formanswertools/call (id 2)inputResponses · requestStateresult (complete)
MRTR: the server never calls the client. It returns input_required with the questions and an opaque requestState, the client asks the user, and a second, independent tools/call carries the answers back.

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:

  • requestState is 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/get and resources/read may return InputRequiredResult, 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 iss parameter in authorization responses per RFC 9207, and clients must validate a present iss against 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_type during registration (SEP-837). This is why some desktop and CLI clients saw redirect_uri errors 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_name and redirect_uris. Authorization servers advertise support with client_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 mechanism2026-07-28What to change in your server
initialize handshakeRemoved; version and capabilities in every _metaRead them per request; return -32022 with supported versions
Nothingserver/discover (servers must implement)Advertise versions, capabilities and identity
Mcp-Session-IdRemovedMove state into explicit, authorized handles
Server-initiated elicitation, sampling, rootsMRTR: input_required + retryReturn InputRequiredResult; sign requestState
GET stream, resources/subscribesubscriptions/listenAnswer GET and DELETE with 405
Last-Event-ID resumabilityRemovedMake tool calls safe to re-issue
Experimental tasks, tasks/resultTasks extension, tasks/get pollingPoll; drop tasks/list
JSON body onlyMcp-Method / Mcp-Name headersReject header/body mismatches (-32020)
Resource not found -32002-32602Update 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:

  1. Upgrade to a Tier 1 SDK release that speaks 2026-07-28 and read its migration notes first; the SDKs absorb most transport changes.
  2. Search your code for session IDs and per-connection caches. Replace each one with an explicit handle or with data in the request.
  3. Implement server/discover and return serverInfo in result _meta.
  4. Rewrite every server-initiated request as MRTR, with an integrity-protected requestState bound to user, expiry and request.
  5. Add resultType, ttlMs and cacheScope, and sort tools/list deterministically.
  6. Validate the Mcp-Method and Mcp-Name headers against the body and update error codes (-32602, -32020 to -32022).
  7. Replace Roots, Sampling and Logging with tool parameters, direct provider calls and OpenTelemetry. Trace context now has documented _meta keys (traceparent, SEP-414).
  8. 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

  1. The 2026-07-28 Specification – MCP blog, 28 July 2026
  2. MCP 2026-07-28 Key Changes (changelog)
  3. MCP 2026-07-28: Versioning and Compatibility
  4. MCP 2026-07-28: Streamable HTTP transport
  5. MCP 2026-07-28: Multi Round-Trip Requests
  6. MCP 2026-07-28: Tools (state handles)
  7. MCP 2026-07-28: Client registration (CIMD)
  8. MCP 2026-07-28: Deprecated features registry
  9. The 2026 MCP Roadmap – MCP blog, 9 March 2026
  10. 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.

Sounds like what you need?

Tell me about your project or role – I’d love to hear from you.