Deep dive · Theo Vance (The Builder) · 2026-08-01 · The new MCP spec drops server-side sessions — here’s what changes in your remote server on Monday.

Here is a bug you have probably hit, or will.

You wrote a remote MCP server. Locally it was perfect. You deployed two replicas behind a load balancer, and it started failing on the second call. The client sent initialize, got back an Mcp-Session-Id, then sent tools/call — and the balancer routed that second request to the other pod, which had never heard of the session. HTTP 404. The client dutifully re-initialized, landed on a third pod, and the loop began.

The fix everyone reached for was sticky sessions: pin a client to a pod by cookie or IP hash. Which means you just turned a stateless HTTP service back into a stateful one, gave up clean autoscaling, and made a pod recycle a user-visible event. You did that because the protocol told you to.

The 2026-07-28 MCP spec stops telling you to. It deletes the session. That is the whole story, and it is a bigger deal for the way you deploy than it sounds.

What actually shipped

The old model — the one in the 2025-06-18 spec, still live in production everywhere — let a server “establish stateful sessions.” On initialize, the server may mint an Mcp-Session-Id header; after that, the client “MUST include it on all subsequent HTTP requests,” and a server that loses the session answers 404, at which point the client “MUST start a new session.” A session is server-side memory keyed by a header. That memory is exactly what a load balancer cannot see.

You could already turn it off. The TypeScript SDK has long accepted sessionIdGenerator: undefined to run stateless — and doing so dropped resumability and half the server-to-client feature set with it. Stateless was the mode you used when you gave up on features. The new spec inverts that: stateless is the default, and the features were redesigned to survive it.

Three changes carry the weight.

One: every request stands alone. Per the announcement, each request now “travels on its own, carrying its protocol version, client identity, and client capabilities in _meta.” There is no init handshake the server has to remember. In the SDK, the server stops being a long-lived object bound to a transport and becomes a handler that builds a fresh instance per request:

// 2025: one server, one transport, session state in memory
new StreamableHTTPServerTransport({ sessionIdGenerator: undefined })

// 2026-07-28: a request handler, fresh instance each call
const handler = createMcpHandler(() => {
  const server = new McpServer({ name: 'my-server', version: '1.0.0' },
    { capabilities: { tools: {} } });
  // register tools/resources/prompts once
  return server;
});

That handler exports cleanly as a Vercel or Lambda function. No shared process, nothing to keep warm, nothing to pin. Server identity itself moved out of a stateful serverInfo handshake and into _meta['io.modelcontextprotocol/serverInfo'] on the response. Scale is now someone else’s problem — the platform’s.

Two: the server stops needing to talk first. This is the part that made sessions load-bearing in the old design. When a tool needed a value from the user mid-call — elicitation — or wanted the client’s model to sample a completion, the server had to send a server-to-client request. Over HTTP that meant holding open an SSE stream so the server had a channel to push down. A held-open stream is a stateful connection by another name, and it dies when the pod does.

The new spec replaces push with a return. The pattern is called Multi Round-Trip Requests. A tool that needs input doesn’t call out; it returns input_required and waits to be called again with the answer:

// 2025: server pushes a request down a live stream
const r = await ctx.mcpReq.elicitInput({ inputRequests: { count: schema } });
return handleResponse(r.count);

// 2026-07-28: server returns "I need input", client re-enters with it
const state = ctx.mcpReq.requestState<MyState>();
if (!state) {
  return inputRequired({
    inputRequests: { count: inputRequired.elicit({ /* schema */ }) },
    requestState: await codec.mint({ step: 'awaiting-count' }),
  });
}
const accepted = acceptedContent(ctx.mcpReq.inputResponses, 'count', SCHEMA);
return finalResult(accepted);

Sampling works the same way now — inputRequired.sampling(...) instead of a pushed request. Roots and per-session logging as separate server-to-client calls are simply gone; log level rides along as a _meta.logLevel key on the request that needs it. The through-line: nothing the server used to initiate needs a live back-channel anymore. It answers, it forgets, it gets called again.

Three: the state didn’t vanish — it moved into the client’s pocket. A multi-step tool call still has a middle. Where does the “we’re on step two” live if the server keeps nothing? In that requestState above. The server mints it, the client carries it, the client hands it back on re-entry. It is “HMAC-signed, not encrypted,” minted with a secret key and a TTL. If that smells like a signed cookie or a stateless JWT, it should — this is the oldest trick in web scaling, and MCP just adopted it wholesale. Sign the state, give it to the caller, verify it when it comes home, store nothing.

Two things follow from “signed, not encrypted,” and you have to internalize both. The flow state is visible to the client, so never put a secret in it. And it is tamper-evident with an expiry, so a stale or forged requestState must be rejected, not trusted. The SDK guide is blunt about the re-entry rule: per-round inputResponses “replace on re-entry; never accumulate.” You are building a small state machine whose memory lives on the wire. That is more discipline than a session hash-map, and it is the discipline that makes the thing deploy.

Who this is really for

Read the other changes and a pattern shows up. They are not for the person writing a tool. They are for the box in front of it.

Header-based routing (SEP-2243) lets a tool mark an input as x-mcp-header: true, and it arrives as an Mcp-Param-* HTTP header instead of a field buried in the JSON-RPC body. The announcement is explicit about why: “gateways can route and authorize on headers directly” without parsing the body. Tool and resource lists gained ttlMs and cacheScope so a cache in the middle knows how long to hold them. On the auth side, the 2025 spec already made the MCP server an OAuth 2.1 resource server that “MUST validate that access tokens were issued specifically for them as the intended audience” (RFC 8707) and forbade token passthrough — and it already required the Authorization header “in every HTTP request from client to server, even if they are part of the same logical session.” The new spec finishes the job: RFC 9207 issuer validation, and Dynamic Client Registration deprecated in favor of Client ID Metadata Documents.

Per-request auth plus stateless transport plus header routing equals one thing: an MCP server that is fungible behind a gateway. Any pod can serve any request; the middlebox authorizes and routes without opening the envelope. That is the protocol being shaped for infrastructure — the same layer where the platforms sit, the one that ate LLMOps (the wrapper is a feature, the gateway is a company), and the same tool list that already bloats your context window before you type (the context tax). MCP hit close to half a billion SDK downloads a month. At that size a protocol stops being a spec and starts being a deployment target, and deployment targets get optimized for the operators, not the authors.

The honest cost

Stateless is not free, and anyone selling it as pure upgrade is skipping the bill.

You gave up cheap resumability. A dropped connection mid-stream used to replay from a Last-Event-ID cursor the server held; now a genuinely long-running job belongs in the new io.modelcontextprotocol/tasks extension, which is poll-based, or in your own store. The protocol stopped pretending the connection was your database — which is correct, but it means you now own the durability you were getting by accident. Every MRTR round also re-ships the _meta, the capabilities, and the signed state, so a chatty multi-step tool pays in round-trips and bytes what it used to pay in server RAM. For a plain tools/call — already one POST, one response — you pay nothing new. For a five-prompt wizard, you feel it.

The counter to the counter: stickiness was never free either. It was a hidden coupling you stopped seeing because the SDK hid it, and it billed you the day a pod recycled or the balancer rehashed. Moving the state onto a signed token makes the cost legible and puts it where you can pay it deliberately. That is the trade, stated plainly: you swap invisible server memory for visible, signed, expiring wire state. It is the same trade the web made twenty years ago when it moved from server sessions to signed cookies, and it scaled the web.

Do, watch, ignore

Do. If you run a remote MCP server, move to the stateless handler now and kill your session affinity — the legacy: 'stateless' default serves both 2025 and 2026 clients while you migrate, so this is not a flag day. Externalize any per-call flow state into a sealed requestState (short TTL, no secrets in it), or into a real store for anything long-running. And if you are not already validating token audience with the RFC 8707 resource parameter, do that regardless of this spec — it is the fix for the confused-deputy hole, not a nice-to-have.

Watch. Header-based routing and CIMD-over-DCR are the tells for where this goes: managed MCP gateways, per-tool authorization at the edge, caching middleboxes. If your org runs more than a handful of internal servers, that gateway is coming, and the servers that deploy behind it cleanly are the stateless ones. Watch Tasks-extension adoption as the real home for long-running agent work.

Ignore. The “stateless is slower” reflex — for ordinary tool calls it is a wash, and the round-trip cost only bites deep wizards you can redesign. And ignore this entirely if your servers are local stdio processes: that transport is unchanged, no sessions, no gateways, nothing to migrate. This is a remote-server story. If you ship one, the session era ended on July 28, and your next deploy is lighter for it.