
MCP Just Deleted Sessions. Here's What Breaks in My Production Server — and Why I'm Glad.
When I wrote last week that MCP wasn't dead, I didn't expect the strongest evidence to arrive as a changelog. The release candidate for the 2026-07-28 specification is the biggest revision the protocol has had since launch — locked on May 21, final on July 28, with a ten-week validation window in between. People are already calling it MCP 2.0, and for once the version-number inflation is earned: sessions are gone from the protocol core, three original features just entered deprecation, and the extension system grew up overnight.
I run a production MCP server — TypeScript, wired into a Next.js app, the one I wrote about in the honest version a couple of weeks back. So I didn't read this release candidate as a spectator. I read it with my own codebase open in the next pane, grepping for every identifier the changelog touches. This post is what came out of that afternoon: what actually changed, what breaks for server authors, and the migration I'm planning for the validation window.
Verdict up front, as usual: this is the best revision MCP has shipped, it deletes the single most annoying part of running a server on serverless infrastructure, and it will still cost you a real week of work. All three things are true at once.
Sessions are gone. Good.
Until now, every MCP connection started with a handshake: the client sends initialize, the server responds with its capabilities and hands back an Mcp-Session-Id, and every subsequent request carries that header so the server can find the conversation again. On paper that's tidy. On real infrastructure it's a tax. If your server runs as serverless functions — which is exactly what a Next.js route handler is — there is no "the server" to remember the session. You either pin clients to instances with sticky routing, or you push session state into shared storage and pay the latency on every single request. I did the second one, and it was the part of my server I liked least.
The 2026-07-28 RC deletes all of it. The initialize/initialized handshake is gone. The session header is gone. Every request is now self-contained: protocol version, client info, and capabilities travel in _meta on each call, and a new server/discover method lets clients fetch capabilities up front when they want them. The Streamable HTTP transport gets a matching upgrade:
POST /api/mcp HTTP/1.1
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: searchPostsThat last detail is sneaky-important: a load balancer can now route MCP traffic on headers alone, without inspecting the JSON-RPC body. Any server instance can answer any request. This is the protocol choosing boring HTTP infrastructure over protocol cleverness, and it is the correct trade.
State didn't disappear — it moved where the model can see it
"But my tools are stateful" was my first objection too. The answer is the explicit-handle pattern: instead of the protocol hiding state inside a session, your tool returns an identifier and the model passes it back on the next call. The spec puts it plainly: "The explicit-handle pattern simply makes the state visible to the model rather than hidden away."
// Before: state lives in the session, invisible to the model
{ "content": [{ "type": "text", "text": "Added. 3 items in your basket." }] }
// After: state is a handle the model can reason about and pass along
{ "structuredContent": { "basket_id": "bkt_123", "item_count": 3 } }Having migrated an agent system away from hidden state before, I think this is the right call for a second reason: agents are better at state they can see. A model that holds bkt_123 in its context can recover from a failed call, hand the handle to a sub-task, or show it to the user. State buried in a session header can do none of those things.
Server-initiated requests got the same treatment. A server that needs more input mid-call no longer holds a session open waiting; it returns an InputRequiredResult carrying the elicitation prompts plus a base64 requestState payload, and the client re-issues the original call with the answers and the state echoed back. Any instance can process the retry, because the request carries everything it needs.
Extensions grow up: MCP Apps and Tasks
The extension system stops being a hand-wave. Extensions now have reverse-DNS identifiers (io.modelcontextprotocol/extensionName), independent versioning, their own ext-* repositories with delegated maintainers, and a formal negotiation step through capability maps. There's even an Extensions Track in the SEP process, so a feature can prove itself as an extension before anyone argues about putting it in core.
MCP Apps standardizes the embedded-UI experiments everyone has been doing ad hoc: server-rendered HTML in sandboxed iframes, with UI templates declared ahead of time so clients can prefetch and security-review them, and every UI action routed back through JSON-RPC with the same consent and audit path as a direct tool call.
Tasks is the redesign of the experimental long-running-work API, rebuilt for the stateless world: the server answers tools/call with a task handle, and the client drives the lifecycle through tasks/get, tasks/update, and tasks/cancel — the server decides which calls become tasks. Note the omission: tasks/list is gone, because without sessions there is no safe way to scope "whose tasks?". If you built on the 2025-11-25 experimental Tasks API, this migration is not optional.
Three deprecations, zero tears (mostly)
Roots, Sampling, and Logging — three features from the original spec — are now deprecated. They keep working until at least 2027-07-28, and actually removing them requires a separate SEP under the new lifecycle policy. That's the kind of grown-up deprecation discipline you do not see from a dying protocol.
The replacements tell you why each one lost. Roots gives way to plain tool parameters and resource URIs. Sampling — the elegant idea that a server could borrow the client's model — gives way to calling your LLM provider directly; elegant or not, client support was always patchy, and I never shipped anything on it. Logging gives way to stderr for stdio servers and OpenTelemetry for everything else, and the spec now documents W3C Trace Context (traceparent, tracestate, baggage) propagating through _meta. Distributed traces that survive the hop from client to gateway to server — that one I'll use the first day the SDK supports it.
The small print that will actually bite you
A few changes are small enough to miss and sharp enough to cut. The error code for a missing resource changes from -32002 to the JSON-RPC standard -32602 — if any client code matches on that number for retry logic, grep for it now. Tool inputSchema and outputSchema upgrade to full JSON Schema 2020-12, which finally means oneOf, anyOf, conditionals, and $ref/$defs in tool inputs — but implementations must not auto-dereference external $ref URIs, and you're expected to bound schema depth and validation time. List and read results can now carry ttlMs and cacheScope, so clients finally have a sanctioned way to cache. And authorization tightens: clients must validate the iss parameter per RFC 9207, declare an OpenID Connect application_type during dynamic client registration, and treat credentials as bound to the issuing authorization server.
My ten-week plan
Here's what the validation window looks like for my server, in order.
One: audit every place I assumed a session. For me that's the session-backed storage I built to make Next.js route handlers stateful, plus anything that quietly assumed two requests from the same client land on the same warm instance. The goal of this pass is a list, not a fix.
Two: convert stateful tools to explicit handles. Most of my tools are already request-shaped; the two that aren't become the basket-id pattern above.
Three: grep for the old error code. Every client and every test that matches on -32002 gets updated.
Four: rewrite my ugliest tool input schema with oneOf. I've wanted discriminated unions in tool inputs since the day I shipped this server. Flattened, everything-optional schemas are where tool-call hallucinations breed.
Five: track the TypeScript SDK, not the spec. Tier 1 SDKs are expected to ship support within the ten-week window. I'll pin the RC build in staging when it lands, and nothing touches production until the spec goes final on July 28.
The verdict
Last week I wrote that the MCP obituaries had mistaken a hype correction for a death. This release candidate is the protocol making my argument for me: you don't delete your session layer, formalize a deprecation lifecycle, and stand up an extensions governance track for a technology you're winding down.
More than that, every change points the same direction: away from protocol cleverness, toward boring, scalable HTTP — stateless requests, header-based routing, standard error codes, standard schema, standard tracing. Each of those choices makes an MCP server cheaper to run and easier to put behind infrastructure you already have. It costs me a migration week and it deletes my least favorite subsystem. I'll take that trade every time. Read the RC now, migrate staging during the window, and ship when it's final on July 28.
More writing