> MCP 2026-07-28 drops sessions and the initialize handshake. For server authors: _meta, server/discover, MRTR, auth and a migration checklist.
>
> Web page: https://balazscsorba.com/blog/mcp-2026-07-28-stateless-migration-guide · Language: English · Also available in: [Deutsch](https://balazscsorba.com/de/blog/mcp-2026-07-28-stateless-migration-guide.md) · [Magyar](https://balazscsorba.com/hu/blog/mcp-2026-07-28-stateless-migration-guide.md)
> Author: Balázs Csorba · Published: 2026-07-29 · Updated: 2026-09-01 · Keywords: MCP 2026-07-28, stateless MCP, MCP migration guide, MCP sessions removed, Mcp-Session-Id removed, MCP server/discover, MCP multi round-trip requests, how to migrate an MCP server to the new spec, MCP CIMD client ID metadata document, MCP roots sampling logging deprecated

[Blog](https://balazscsorba.com/blog)/[AI agents](https://balazscsorba.com/blog/category/agents)

# MCP 2026-07-28 migration guide: what changes for stateless MCP servers

MCP 2026-07-28 drops sessions and the initialize handshake. For server authors: \_meta, server/discover, MRTR, auth and a migration checklist.

[Balázs Csorba](https://balazscsorba.com/about)·July 29, 2026·updated September 1, 2026·9 min read

-   [MCP](https://balazscsorba.com/blog/tag/mcp)
-   Protocol migration
-   Stateless APIs
-   OAuth
-   Agents

![MCP 2026-07-28 migration: a robot uses one of several identical vending machines while an old machine is moved away.](https://balazscsorba.com/images/blog/mcp-2026-07-28-stateless-migration-guide/cover.webp?v=e9fbdaee62)

## 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.

On this page

1.  [Why did MCP drop sessions?](https://balazscsorba.com/#why-sessions-were-dropped)
2.  [How does a stateless MCP request work?](https://balazscsorba.com/#how-stateless-mcp-works)
3.  [What replaces server-initiated requests? Multi Round-Trip Requests](https://balazscsorba.com/#multi-round-trip-requests)
4.  [Where does state go now? Handles, Tasks and subscriptions](https://balazscsorba.com/#state-tasks-subscriptions)
5.  [What changed in MCP authorization?](https://balazscsorba.com/#authorization-changes)
6.  [What is deprecated, and when should you not rush?](https://balazscsorba.com/#deprecations-and-trade-offs)
7.  [MCP 2026-07-28 migration checklist and test plan](https://balazscsorba.com/#migration-checklist)
8.  [Sources](https://balazscsorba.com/#sources)

Listen to this article

0:00

**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](https://modelcontextprotocol.io/specification/2026-07-28/changelog) and the [release post](https://blog.modelcontextprotocol.io/posts/2026-07-28/); where the spec text is the source of truth, I link to it. For the code side, read the [official reference servers](https://balazscsorba.com/blog/mcp-reference-servers) next 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](https://blog.modelcontextprotocol.io/posts/2026-mcp-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](https://modelcontextprotocol.io/specification/2026-07-28/basic/versioning) 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](https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http)). 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 flow versus stateless MCP 2026-07-28 flow, with session IDs and the server/discover call](https://balazscsorba.com/images/blog/mcp-2026-07-28-stateless-migration-guide/diagram-10.webp?v=7b1863980a)

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):

HTTP

```
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](https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/mrtr) 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.

![Sequence diagram of an MCP multi round-trip request: input_required result, user elicitation, then retry with state](https://balazscsorba.com/images/blog/mcp-2026-07-28-stateless-migration-guide/diagram-16.webp?v=0cd6e2fb26)

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:

JSON

```
{"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](https://modelcontextprotocol.io/specification/2026-07-28/server/tools) 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](https://balazscsorba.com/blog/llm-cost-latency-prompt-caching-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](https://www.rfc-editor.org/rfc/rfc9207), 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](https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization/client-registration), 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](https://balazscsorba.com/blog/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](https://modelcontextprotocol.io/specification/2026-07-28/deprecated) 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](https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http). 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:

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. If you're planning a migration like this for your own servers, that's part of what I do as an [AI engineer](https://balazscsorba.com/expertise/ai-engineer).

## Sources

1.  [The 2026-07-28 Specification](https://blog.modelcontextprotocol.io/posts/2026-07-28/) – MCP blog, 28 July 2026
2.  [MCP 2026-07-28 Key Changes (changelog)](https://modelcontextprotocol.io/specification/2026-07-28/changelog)
3.  [MCP 2026-07-28: Versioning and Compatibility](https://modelcontextprotocol.io/specification/2026-07-28/basic/versioning)
4.  [MCP 2026-07-28: Streamable HTTP transport](https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http)
5.  [MCP 2026-07-28: Multi Round-Trip Requests](https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/mrtr)
6.  [MCP 2026-07-28: Tools (state handles)](https://modelcontextprotocol.io/specification/2026-07-28/server/tools)
7.  [MCP 2026-07-28: Client registration (CIMD)](https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization/client-registration)
8.  [MCP 2026-07-28: Deprecated features registry](https://modelcontextprotocol.io/specification/2026-07-28/deprecated)
9.  [The 2026 MCP Roadmap](https://blog.modelcontextprotocol.io/posts/2026-mcp-roadmap/) – MCP blog, 9 March 2026
10.  [RFC 9207: OAuth 2.0 Authorization Server Issuer Identification](https://www.rfc-editor.org/rfc/rfc9207)

## 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.

Written by Balázs Csorba

Senior fullstack & AI engineer in Styria, Austria – 10+ years of Vue, Nuxt, Node.js and PHP, now building tooling for AI agents.

[AI engineering & MCP servers →](https://balazscsorba.com/expertise/ai-engineer)[About me →](https://balazscsorba.com/about)

## More articles

-   [Designing tools for AI agents: names, descriptions, schemas and error messages](https://balazscsorba.com/blog/agent-tool-design-descriptions-errors)
-   [Browser agents for E2E testing: let agents write Playwright tests, not run them](https://balazscsorba.com/blog/browser-agents-e2e-testing-playwright)
-   [The A2A protocol explained: agent cards, tasks, and where it fits next to MCP](https://balazscsorba.com/blog/a2a-protocol-agent-interoperability)
-   [OpenCode review: the open-source coding agent for any model](https://balazscsorba.com/blog/opencode)

## Sounds like what you need?

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

[Get in touch](mailto:contact@balazscsorba.com) [Connect on LinkedIn](https://www.linkedin.com/in/balazs-csorba)
