IAMUVIN

AI & Machine Learning

MCP Went Stateless: Migrating Your Server

Uvin Vindula·August 12, 2026·15 min read
Share

TL;DR

The Model Context Protocol revision dated 28 July 2026 removes protocol-level sessions. There is no initialize / initialized handshake, no Mcp-Session-Id header, and the legacy HTTP+SSE transport is deprecated. Every request is self-contained: protocol version and client identity travel with the request itself, and capability discovery moves to an optional server/discover call. Server-initiated requests over an open stream are replaced by Multi Round-Trip Requests — the server answers with resultType: "input_required" and the client retries with the answers attached. Two new HTTP headers, Mcp-Method and Mcp-Name, let a gateway route and meter without parsing the JSON body. List and read results can carry ttlMs and cacheScope so clients cache them. Roots, Sampling, Logging and HTTP+SSE are deprecated with a minimum 12-month support window. Auth hardened in three places: RFC 9207 issuer validation against authorization-server mix-up, credential binding to issuers, and Dynamic Client Registration deprecated in favour of Client ID Metadata Documents. Tasks moved out of experimental core into the io.modelcontextprotocol/tasks extension. Tier 1 SDKs — TypeScript, Python, Go and C# — are updated; Rust is in beta.


What the 28 July 2026 MCP revision removed

Two things every MCP tutorial teaches are gone, and a third is on a clock.

The handshake. The session ID. And the legacy HTTP+SSE transport, deprecated with a year-long offramp.

That is not a refactor. It is a different protocol wearing the same name, and the revision announcement dated 28 July 2026 is explicit about it: the core is now stateless request/response, with each request carrying everything the server needs to serve it.

Here is the full mapping, old to new.

Removed or deprecatedWhat replaces itStatus
initialize / initialized handshakeProtocol version and client identity carried on every requestRemoved from core
Mcp-Session-Id headerNothing at the protocol layer — state is your problem nowRemoved from core
Capability negotiation inside the handshakeOptional server/discoverReplaced
Server-initiated requests over an open streamMulti Round-Trip Requests, resultType: "input_required"Replaced
Legacy HTTP+SSE transportStateless request/response over HTTPDeprecated, 12-month minimum window
Roots (roots/list)Multi Round-Trip RequestsDeprecated, 12-month minimum window
Sampling (sampling/createMessage)Multi Round-Trip RequestsDeprecated, 12-month minimum window
LoggingDeprecated, 12-month minimum window
Dynamic Client RegistrationClient ID Metadata DocumentsDeprecated
Tasks in experimental coreio.modelcontextprotocol/tasks extensionMoved out of core

Two rows in that table are the whole migration. Losing the handshake changes how a server learns who it is talking to. Losing the session ID changes where state lives. Everything else is smaller than it looks.

The scale matters when you decide how fast to move. The same announcement puts Tier 1 SDK downloads at close to half a billion a month, with TypeScript and Python each past a billion cumulative. A protocol at that volume does not get to break clients quietly, which is why the deprecation windows exist and why your server will be talking to old clients for a year.

Every request is now self-contained

Under the old model, a client opened a connection, sent initialize, got back the server's capabilities, sent initialized, and then started working. Everything after that leaned on the handshake having happened. Protocol version, negotiated capabilities, client identity — all established once, then assumed.

The 2026-07-28 core removes that assumption. Each request carries the protocol version and the client identity itself. The server reads them per request and answers.

What this buys you is worth saying plainly, because it is the reason the change happened at all: a stateless server scales horizontally with no sticky routing. Any instance can serve any request. You can put it behind a plain load balancer. You can run it on a serverless platform without the cold-start dance of rebuilding session state. You can kill a pod mid-flight and lose nothing.

What it costs you is that every request pays for its own context. If your server did expensive setup once per session — loading a schema, opening a database handle, resolving a tenant — that work now either happens per request or moves into a cache you own.

A sketch of the shape, in TypeScript:

typescript
// Illustrative, not a quotation of the schema. Read the spec before
// you write your types — field names beyond the ones the revision
// announcement names are my own.
type McpRequest = {
  method: string;
  params: unknown;
  _meta: {
    // Namespaced, per the announcement's own example:
    "io.modelcontextprotocol/clientInfo": { name: string; version: string };
  };
  // Protocol version rides the MCP-Protocol-Version HTTP header,
  // not _meta, in the announcement's wire example.
};

I am flagging that deliberately. The revision announcement names the behaviour and the new headers and result fields; it does not hand you a schema in prose. Take the wire format from the spec and the Tier 1 SDK for your language, not from any blog post, mine included.

Where session state goes when Mcp-Session-Id disappears

This is the question every team I have talked to hits first, and the answer is short: nowhere in the protocol. The protocol stopped having an opinion.

That leaves three real options, and the right one depends on what the state is for.

Put it in the request. If the state is small and the client owns it — a cursor, a selected workspace, a filter — pass it as a parameter. This is the boring answer and it is right more often than teams expect. It also survives the client reconnecting, restarting, or being replaced by a different client entirely.

Put it behind the auth token. If the state is per-user or per-tenant, derive it from the credential on the request. The tenant is not session state. It is identity, and identity was never supposed to live in a session ID.

Put it in your own store, keyed by something durable. If the state is genuinely a conversation — a long job, an accumulating draft, a multi-step workflow — give it your own identifier, return it in the result, and let the client send it back. That is a session, but it is your session, with your TTL and your eviction policy, not a protocol-level one the spec has to support forever.

The failure mode to avoid is reimplementing Mcp-Session-Id under a different name in a header and calling it done. If you do that you keep the sticky routing, keep the cold-start problem, and gain nothing but a migration diff.

For anything long-running, the pattern worth stealing is the one Anthropic describes in its Managed Agents architecture, published 8 April 2026: treat the session as a durable, queryable log that lives outside the context window, and call the execution environment through a stateless execute(name, input) -> string interface. Anthropic reports that decoupling the reasoning layer from the execution layer cut p50 time-to-first-token by roughly 60% and p95 by more than 90%, because containers are provisioned only when they are needed. The same shape works for an MCP server: stateless call surface, durable log behind it.

Multi Round-Trip Requests replace server-initiated calls

The old protocol let a server initiate a request back to the client over an open stream. That is how elicitation worked, and it is a large part of why the transport had to be stateful in the first place — you cannot push to a client you have no connection to.

The replacement inverts it. The server returns a result with resultType: "input_required", describing what it needs. The client then retries the original call with the answers attached.

Three consequences for server authors.

Your handler has to be resumable from its inputs. If a tool needs a confirmation halfway through, you cannot pause the function and wait. You return early, say what you need, and handle the retry as a fresh call that happens to carry more parameters. In practice that means side effects before the input request are a bug: either do the whole thing on the retry, or make the partial work idempotent.

Your client has to be prepared to loop. One tool call can now be two or three HTTP round trips. That is a latency budget question, not a correctness one, but it will show up in your traces.

And this is where the old server-initiated Sampling feature sits awkwardly. The announcement does two things at once: it deprecates Sampling (SEP-2577), and it says server-to-client sampling is being redesigned onto MRTR. So the near-term path is MRTR and the long-term one is unsettled. Either way, if your server relies on asking the client's model for a completion mid-tool, treat that pattern as on a clock: move the model call to your own side, or restructure the tool so the agent makes the decision, not the server.

Mcp-Method and Mcp-Name: routing without parsing the body

Two new HTTP headers ship with the revision: Mcp-Method and Mcp-Name. Streamable HTTP requests must now include both (SEP-2243). They exist so a gateway can route and meter a request without parsing the JSON body.

http
POST /mcp HTTP/1.1
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: search_invoices
Content-Type: application/json

This is the change I expect to earn the most operationally, and it is getting the least attention. Before it, any proxy that wanted per-tool rate limits, per-tool authorisation, per-tool billing or per-tool metrics had to deserialise the body to find out what was being called. That is a JSON parse on the hot path of every request, at the edge, on untrusted input.

Now your gateway can do all of it on headers. Rate-limit tools/call differently from resources/read. Block one tool name for one client class. Emit a metric per tool without instrumenting the server. Charge per call.

Set them correctly on the client side and read them defensively on the server side. A header is a claim, not a fact. If a gateway authorises on Mcp-Name and the server dispatches on the body, an attacker sends one name in the header and a different method in the body. Validate that the two agree before you do anything, and fail the request if they do not.

That mismatch class is the same family of bug I cover in the API security failures I keep finding in Next.js codebases: two layers reading two different copies of the same claim.

Cacheable list and read results: ttlMs and cacheScope

List and read results can now carry ttlMs and cacheScope, which tells a client how long a result stays good and how widely it may be shared.

For a tools list this is close to free money. Most servers return an identical tool list to every client on every connection, and under the old stateful model that list was fetched on every handshake. Now you can mark it cacheable and stop serving it.

Two things to get right.

cacheScope is a correctness field, not a performance field. If a result depends on the caller's identity — a resource list filtered by tenant, a tool list gated by plan — it must not be cached at a scope wider than that identity. Getting this wrong is a cross-tenant data leak with good latency.

ttlMs interacts with deployment. If your tool list changes on deploy and you set a one-hour TTL, clients run the old list for up to an hour after you ship. Either keep the TTL under your deploy cadence or make tool additions backward-compatible by construction.

There is a second-order effect worth naming: a cached tools list on the client side also means a stable prompt prefix, which is what prompt caching on the model side needs. Anthropic's prompt caching documentation puts the invalidation cascade as tools, then system, then messages — a change to the tools array invalidates everything downstream of it. A tool list that churns per request is not just an HTTP cost, it is a cache miss on the model call that follows. I go through the full cost picture in the tokens-per-task cost model.

The auth changes are the ones that will break integrations quietly

Transport changes break loudly. Auth changes break at 3am for one customer on one identity provider.

Three landed together in this revision.

RFC 9207 issuer validation. The client now validates which authorization server actually issued a response, as a defence against authorization-server mix-up attacks. The obligation sits on the client, not on your resource server: the authorization server returns iss and the client must validate it before it redeems the authorization code (SEP-2468). If you ship an MCP client, or your server acts as a client to a downstream authorization server, that validation is yours to add.

Credential binding to issuers. A credential is bound to the issuer it came from. Same class of problem, enforced at the credential rather than at the response.

Dynamic Client Registration is formally deprecated in favour of Client ID Metadata Documents. DCR let a client register itself at runtime and receive a client ID. CIMD replaces that with a document the client publishes and the server reads. If your onboarding flow depends on DCR, that dependency is now on a clock.

The revision also requires clients to set application_type during Dynamic Client Registration (SEP-837), so authorization servers stop rejecting http://localhost redirects for desktop and CLI clients — historically the messiest corner of the whole auth story. Note where that fix lives: inside DCR, the mechanism the same revision deprecates.

If you run an MCP server that touches customer data, do the auth work before the transport work. A server still speaking HTTP+SSE is merely legacy. A server with a mix-up vulnerability is an incident. I write about why boundaries beat model-level defences in containment engineering for agents with tool access, and the same argument applies here: the tool handler is where authorisation belongs, not the layer above it.

Deprecations and what a 12-month window actually buys you

Roots, Sampling, Logging and the legacy HTTP+SSE transport are deprecated with a minimum 12-month support window from the revision.

Read "minimum" carefully. It is a floor on how long they stay supported, not a promise about when they go. It also says nothing about when clients stop sending them, which is the number that actually governs your work.

Here is how I read the window for a server already in production.

A year is enough time to do this properly and not enough time to ignore it for eight months. The trap is that nothing breaks for a while, so it never reaches the top of a backlog, and then the SDK you depend on drops the code path in a major version and you are doing an urgent migration during a release freeze.

Tasks also moved — out of experimental core and into the io.modelcontextprotocol/tasks extension. That is not a deprecation, but it does mean the import path and the capability check change, and that a server or client without the extension will not see it.

A migration order for a server already in production

This is the order I would do it in, and the reasoning is that each step is independently shippable and none of them require a client to move first.

  1. Instrument before you change anything. Log protocol version, client name and client version per request. You cannot plan a deprecation you cannot measure. If you learn that 94% of your traffic is one client at one version, the migration is a different project than if it is forty clients.
  2. Make handlers stateless without removing the session. Move every piece of per-session state to either a request parameter, the auth token, or your own keyed store. Keep accepting Mcp-Session-Id while you do it. At the end of this step the header is decorative, and that is the point.
  3. Do the auth work. Credential binding to issuers and a path off Dynamic Client Registration to Client ID Metadata Documents on the server side; RFC 9207 iss validation before code redemption on whatever client code you own. Independent of transport, and the highest-severity item on the list.
  4. Add the stateless transport alongside the old one, with `Mcp-Method` and `Mcp-Name` on every request. Serve both. Old clients keep working. New clients get self-contained requests. The headers are required on Streamable HTTP, so they ship with the transport, not after it — validate them against the body before you dispatch, then move gateway policy onto them.
  5. Add `server/discover`. Capability discovery is optional now, so this is additive.
  6. Add `ttlMs` and `cacheScope` to list and read results. Start conservative. A short TTL you lengthen later is cheaper than a long TTL you have to invalidate.
  7. Convert server-initiated flows to Multi Round-Trip Requests. This is the largest code change, which is why it is late — by now everything else is stable and you can measure the regression cleanly.
  8. Turn off HTTP+SSE when your own telemetry says it is safe, not when the window closes.

Tier 1 SDKs — TypeScript, Python, Go and C# — are updated for the revision. Rust is in beta. If you are on Rust, plan around beta-grade support rather than assuming parity.

The client side: your tool definitions cost more than your transport

Everything above is server work. The expensive half of an MCP deployment is on the client, and it is not the transport.

Anthropic's tool search documentation gives the number: a typical multiserver setup — GitHub, Slack, Sentry, Grafana and Splunk — can consume roughly 55,000 tokens in tool definitions before the model does any work. Tool search typically cuts that by over 85%, loading only the three to five tools the model needs.

There is a second number in the same page that matters more, because it is about quality rather than cost: the model's ability to pick the right tool degrades once you exceed 30 to 50 available tools. Adding a sixth MCP server to an agent that already has forty tools does not give it more ability. It gives it more ways to pick wrong.

The mechanism is worth understanding before you switch it on. Setting defer_loading: true keeps a definition out of the system-prompt prefix, but you still send it on every request. Discovered tools come back as tool_reference blocks expanded inline, which is what leaves the cached prefix untouched. The documented limits are 10,000 deferred tools, five search results by default, 200-character regex queries and 500-character BM25 queries.

So the client-side rule for an aggregated MCP fleet is: defer everything, search for what you need, and stop treating "number of connected servers" as a capability metric. I go through the broader version of this argument — that never admitting the tokens beats compressing them afterwards — in the context engineering guide, and the decomposition question in multi-agent systems as a context decision. If you are still deciding whether to build a server at all, the fundamentals are in MCP servers as a standard tool integration.

What the 2026 roadmap says lands next, and what I could not verify

The 2026 MCP roadmap published 9 March 2026 moved governance from releases to working groups, with four priority areas: transport evolution and scalability, including .well-known server metadata and horizontal scaling; agent communication, including Tasks retry semantics and result expiry; governance maturation, with a contributor ladder from community participant to maintainer plus a delegation model that lets trusted working groups accept SEPs in their own domain without a full core review; and enterprise readiness — audit trails, SSO, gateway behaviour and config portability, delivered as extensions rather than core changes.

The practical read: proposals outside those four areas get slower review. If you are waiting on a feature that does not sit in one of them, plan around it not arriving.

Two things I will not state as fact.

I could not find a reliable production-adoption figure for MCP. The numbers in circulation trace back to small vendor-run surveys of self-selected senior technical leaders, published by companies with a commercial interest in MCP tooling. I could not verify a sample size or a methodology for any of them. Treat any "X% of teams run MCP in production" claim, including the ones you will see quoted confidently, as directional at best. A widely repeated "80% of the Fortune 500" claim comes from a marketing blog and should not be repeated at all.

And the spec details above came from the revision announcement, not from a line-by-line read of the schema. The behaviour, the header names, the resultType: "input_required" shape, the ttlMs and cacheScope fields and the deprecation list are all stated there. Exact JSON structures are not, which is why the code in this article is labelled as a sketch. Read the spec.

Key Takeaways

  • The 28 July 2026 revision removes protocol-level sessions. No initialize / initialized handshake, no Mcp-Session-Id, and every request carries its own protocol version and client identity.
  • Session state becomes your problem, and that is usually an improvement. Push it into request parameters, derive it from the auth token, or key it in your own store — do not rebuild Mcp-Session-Id under a new name.
  • Multi Round-Trip Requests replace server-initiated calls. The server returns resultType: "input_required" and the client retries with answers attached, so handlers must be resumable from their inputs and side effects before the retry must be idempotent.
  • `Mcp-Method` and `Mcp-Name` move gateway policy off the JSON body. Validate that the headers agree with the body before dispatching, or you have built an authorisation bypass.
  • Do the auth work first. RFC 9207 issuer validation, credential binding to issuers, and the move from Dynamic Client Registration to Client ID Metadata Documents are higher severity than the transport.
  • Roots, Sampling, Logging and HTTP+SSE have a minimum 12-month window. That is a floor on support, not a schedule, and it says nothing about when clients stop sending them.
  • The client side is where the money is. A five-server setup can burn around 55,000 tokens in tool definitions before any work happens, and tool-selection accuracy degrades past 30 to 50 tools.

About the Author

I'm Uvin Vindula — a Web3 and AI engineer based between Sri Lanka and the UK. I build and ship MCP servers, which is why this migration is on my own backlog and not a theoretical exercise. You can see my work at iamuvin.com or reach out about a project at hello@iamuvin.com.

If you are running an MCP server in production and need the stateless migration done without breaking the clients you already have, let's talk about your project.

Working on a Web3 or AI project?

Share

More in AI & Machine Learning

All AI & Machine Learning articles
Uvin Vindula

Uvin Vindula

Web3 and AI engineer based in Sri Lanka and the UK. Author of The Rise of Bitcoin. Founder of ASI Research Labs. Director of Blockchain and Software Solutions at Terra Labz. Founder of uvin.lk — Sri Lanka's Bitcoin education platform with 10,000+ learners.