Skip to content

Stateless MCP: A Hands-On Guide to the 2026 Spec

Stateless MCP just dropped and the community is buzzing. Here's what actually changed in the 2026-07-28 spec - plus the migration traps nobody's warning you about.

8 min readBeginner

The #1 mistake people are making with stateless MCP right now: reading the announcement, seeing the word “stateless,” and assuming their server no longer needs to keep any state at all. That’s wrong, and it’ll leave you with a broken tool the first time a user tries to do anything multi-step.

Stateless MCP means the protocol is stateless. Your application state – a shopping cart, a running total, a partially-built query – still exists. It just doesn’t ride on a session anymore. It rides on the payload. Get that mental model right and everything else clicks.

What just dropped (the 30-second context)

The 2026-07-28 Model Context Protocol specification shipped July 28, 2026, bringing a stateless protocol core, Multi Round-Trip Requests, header-based routing, cacheable list results, authorization hardening, and updated Tier 1 SDKs. Simon Willison called it “Stateless MCP day” – the biggest shift to the MCP spec since it first launched (his blog, July 31, 2026). The scale of adoption makes it real: both the TypeScript and Python SDKs have crossed 1 billion total downloads, with Tier 1 SDKs pulling close to half a billion downloads per month (MCP Blog, 2026-07-28).

Two SEPs do the actual heavy lifting. SEP-2575 removes initialize/initialized in favour of per-request _meta fields; SEP-2567 removes Mcp-Session-Id from Streamable HTTP. That’s the wire-level change. Everything else – routing headers, MRTR, cacheable lists – falls out of those two decisions. You can read the SEP-2575 text directly for the primary source.

The correct mental model, reverse-engineered

Old MCP: client says hello, server hands out a session ID, both sides remember each other, requests are little messages between two acquainted parties.

New MCP: every request is a stranger walking up to any available server with a folder in their hand containing everything the server needs to serve them. The server does the work, hands back the answer plus (if needed) an updated folder, and forgets them. The next request may hit a different instance. It doesn’t matter – the folder came with them.

That folder is the explicit state handle. When your tool needs to remember something across calls, you serialize it (usually signed or encrypted), return it to the client, and the client echoes it back on the next call. State travels in the payload, not the connection.

Building a stateless MCP tool: the tally example

Forget hello-world. Let’s build something that needs state – a running total – and see how the new pattern handles it. The whole point is that instances share nothing, so the total has to live in the request itself.

// Pseudo-TypeScript, MCP SDK style
server.tool("tally", {
 input: { amount: "number", handle: "string?" },
 handler: async ({ amount, handle }) => {
 // 1. Decode incoming state (or start fresh)
 const prev = handle ? decodeSigned(handle) : { total: 0 };

 // 2. Do the work
 const total = prev.total + amount;

 // 3. Return result + updated state handle
 return {
 content: [{ type: "text", text: `Total: ${total}` }],
 _meta: { handle: encodeSigned({ total }) }
 };
 }
});

Three calls, three different server instances, still correct. The explicit state-handle pattern is exactly what the stateless spec is designed to enable – and what makes a plain round-robin load balancer viable without any protocol-level affinity.

Pro tip: If you’re on TypeScript, pass sessionIdGenerator: undefined when constructing the Streamable HTTP transport. In C#, set Stateless = true. Making this choice explicit in code – rather than inheriting a default – is the single cheapest piece of future-proofing you can do right now.

What the SDK settings actually do

Turns out the C# SDK isn’t stateless by default. As of August 2026, Stateless = false is still the default (sessions enabled), and the team expects that to flip once MRTR brings server-to-client interactions fully into stateless mode – but it hasn’t happened yet (C# SDK stateless docs). So if you generate a fresh project and deploy it, you may still be on the old model without realizing. Set it explicitly. Every time.

Interactive tools: MRTR replaces the callback

The tricky bit isn’t state. It’s interactivity.

Old MCP let a server ask the client questions mid-call – approve this destructive action, sample from your LLM, list your workspace roots. That required a held-open stream, which stateless kills. The replacement is Multi Round-Trip Requests: instead of pushing a request down an open stream, the server returns resultType: "input_required" along with the requests it needs answered. The client retries the original call with the answers attached in inputResponses. Nothing has to stay connected between turns (SEP-2322).

Concretely, a delete tool returns something like:

{
 "resultType": "input_required",
 "inputRequests": {
 "confirm": {
 "type": "elicitation",
 "message": "Delete 3 files?",
 "schema": { "type": "boolean" }
 }
 },
 "requestState": "eyJzdGVwIjoxLCJmaWxlcyI6WyJhIiwiYiIsImMiXX0="
}

The client collects the requested input and re-issues the call with inputResponses plus the echoed requestState. Because requestState carries everything needed to resume, the retry can land on a completely different server instance and still pick up where it left off.

Common pitfalls to avoid

Reading tutorials is one thing. Reading migration bug reports is more useful. Here’s what actually breaks:

  • Forgetting the _meta mirror. 400 Bad Request – that’s what you get if the MCP-Protocol-Version HTTP header value doesn’t match the value in the request payload’s _meta field (SEP-2575 requires this). Sending only the header is a very common early mistake.
  • Assuming MRTR covers everything. It doesn’t. Docs say Sampling and Roots are deprecated (MCP9005); runtime says they throw in stateless mode. SampleAsync and RequestRootsAsync remain callable in stateful mode but will throw in stateless. Only elicitation has a clean stateless path today. If your tool leaned on sampling, you’ll need to rethink the flow, not just port it.
  • Infra-level stickiness. Even after you fix the protocol, your platform can still pin clients. On Azure App Service, clientAffinityEnabled: false is still required – ARR affinity cookies can pin a client to one instance at the infra layer, undermining the stateless setup entirely. Audit any platform with session-affinity toggles.
  • Trusting the client’s handle. If you don’t sign or encrypt the state handle you return, a client can rewrite it. Treat it exactly like a JWT you emit.

What the performance story actually looks like

Any instance serves any request, for real. No affinity tuning, no protocol session fighting your load balancer. Deploy behind a plain round-robin, scale to N replicas, done.

The interactive story is fuzzier. MRTR trades one round-trip-with-open-stream for two-or-more request/response cycles. Whether that’s faster or slower per user-visible interaction depends on network latency and how chatty your tool is. As of early August 2026, no public benchmark exists comparing MRTR interactive latency to the old SSE-based flow – the community consensus is “probably slightly slower per interactive turn, but hugely simpler to run,” and this may change once numbers land.

When NOT to go stateless

Not every server should migrate. Some real reasons to stay stateful for now:

  • You rely on sampling. If your tool asks the client’s LLM to generate something mid-call, the stateless path currently doesn’t support that. Wait for it to mature or restructure the flow so the client does the sampling and passes results in.
  • You rely on Roots. Same story – deprecated but still usable in stateful mode.
  • You’re on a single-instance deployment with no scaling plans. Publishing a new spec does not switch anything off. A server still running 2025-11-25 keeps working, and deprecated features carry a minimum 12-month window under the new feature-lifecycle policy (digitalapplied, 2026). Don’t refactor for its own sake.
  • You need push notifications from server to client without a client-initiated request. Stateless has no channel for that. Protocol constraint, not a bug.

The official announcement is worth reading before you commit to a migration path.

FAQ

Do I have to migrate my existing MCP server right now?

No. Old servers keep working. The minimum deprecation window is 12 months, and stateful mode remains fully supported.

What’s the difference between stateless MCP and just building a REST API?

Architecturally? Less than you’d think – every request is self-contained, no session. The difference is what you get on top: a standardized tool/resource contract, the server/discover mechanism, and a client-side ecosystem that already knows how to call MCP tools from LLM agents. Say you’re building a tool that needs to plug into Claude, GPT-4o, and a custom agent framework simultaneously. With MCP, you write the server once and all three clients connect with no extra glue. Without it, you’re writing adapters. That’s what MCP actually saves you – not a fundamentally different transport model, but the shared vocabulary and plug-and-play client story.

Can a stateless server still enforce authentication and authorization?

Yes – arguably better than before. Method and tool names now travel in the Mcp-Method and Mcp-Name HTTP headers (SEP-2243), so gateways can route and authorize without parsing JSON-RPC bodies at all.

Next step: pick one small tool in your existing MCP server, set Stateless = true (or sessionIdGenerator: undefined), and rewrite it to pass its state via the payload. Deploy two replicas, put a round-robin in front, and hit it. If the tool still works when consecutive calls land on different instances, you’ve got the pattern. Everything else is just repetition.