MCP 2026-07-28 is out, and sessions are gone. The whole thing in plain English.

Before: the balancer is sticky, so every request goes to Instance A and dies with it. After: the next free instance takes it.
There is a restaurant in your city with one very good waiter. He remembers everything. Your table number, your no-onions rule, the fact that your friend wants the bill split in two. You never repeat yourself.
Then the waiter gets sick.
The new waiter knows nothing. Not your order, not the onions, not the split bill. You start again from zero. The whole restaurant, it turns out, was running on one person's memory.
Until this week, the Model Context Protocol worked exactly like that restaurant.
First: what is MCP?
If you have never heard of MCP, the docs' own one-line pitch is the fastest start: a USB-C port for AI applications. It is an open standard. It lets AI applications (Claude, IDEs, chatbots, agents) plug into tools and data the same way, no matter who built them.

Without a shared protocol you build one integration per pair. With one, you build one per side.
In our restaurant: your AI app is the host, and it sends one client to each kitchen it wants to order from. You, at the counter, are that client. The kitchen is the server, a program that offers tools, resources (files and data), and prompts. The dishes on the menu are the tools. And MCP itself is the ordering system: how you ask, how the kitchen answers, what happens when something goes wrong.
On July 28, the MCP team released the 2026-07-28 revision (their word for a version). When the release candidate went out in May they called it the largest revision of the protocol since launch. Having now read the whole diff, I would not argue: it changes the ordering system from top to bottom.
The waiter is gone: MCP is now stateless
In the old protocol, a client and a server first did a handshake called initialize, a "hello, who are you?" step. After that, the server kept a session for you. On HTTP there was even a special header, Mcp-Session-Id, that locked you to one specific server instance (one running copy of the server).
That session was the waiter's memory, and it had the waiter's weakness. If the instance holding it crashed, your session died with it. So if you ran three copies of the server, the load balancer in front of them had to be sticky: it had to send you back to the same copy every time, the way a host sends you back to the same waiter. Serverless platforms broke this outright. Services like AWS Lambda start and stop instances whenever a request arrives, so there is no same copy to go back to.
The new version removes sessions from the protocol completely. initialize is gone. Instead, every request carries everything the server needs, inside a small _meta block: which protocol version you speak and what your client can do. Just the technical facts, not your conversation; anything else the server needs arrives as normal request parameters.
So the restaurant reopened as a counter-service place: you order at the counter, receipt in hand. Your whole order is printed on the receipt, and any cashier can pick it up and serve it. If one cashier goes home, the next one reads the same receipt and continues. Nobody has to remember you.
Stateless does not mean your server has to forget everything. It means the protocol no longer remembers for you. If a server needs state across calls, it mints its own handle and returns it from a tool like any other output. The model passes that handle back as an ordinary argument on the next call, so the state is visible in the request instead of hidden in the transport. It is the ticket number on your receipt: the counter does not know who you are, but it knows what number 47 ordered.

Old: one waiter's memory. New: a receipt any cashier can read.
The smallest possible before and after:
// Before (2025-era): a handshake first, then a session to protect
{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2025-06-18",
"capabilities": {},
"clientInfo": { "name": "my-app", "version": "1.0.0" }
}
}
// After (2026-07-28): no handshake, the receipt carries everything
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/list",
"params": {
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientCapabilities": {}
}
}
}
The animation at the top of this article is the same story in fifteen seconds. On the left the host is sticky: every request goes to Instance A, with a session tag pinned to it, and when A dies the client dies with it. On the right the host sends each new arrival to whichever counter is free, taking turns between A, B and C. When A dies, the queue keeps moving.
This is the payoff. You can now deploy an MCP server on serverless or edge platforms (small servers close to the user), or scale it behind any ordinary load balancer.
The card at the door: server/discover
Without a handshake, how do you learn what a server offers? One new call: server/discover. It returns the server's capabilities (what it can do) and the protocol versions it supports, in a single response, and every 2026-07-28 server must implement it. Clients do not have to call it: you can send tools/list straight away and handle an unsupported-version error if one comes back. server/discover is there for when you want the whole picture up front, and it is how you probe an older server over stdio.
There is now a card at the door listing what this branch can do. You still ask for the dishes separately, with tools/list. The card even says how long it stays fresh.

Capabilities at the door, dishes on the menu. Two different calls.
The buzzer: subscriptions/listen
Sometimes the kitchen needs to tell you something: a resource changed, the tool list was updated. The old protocol did this with a stream over HTTP GET (a stream is a connection that stays open so the server can keep sending; over HTTP these are SSE, Server-Sent Events), plus separate resources/subscribe and resources/unsubscribe calls. All of that is replaced by one method: subscriptions/listen.
Think of the buzzer they hand you at the counter: it sits on your table and rings only when your order changes. You tell the server what you care about (a resource changing, or the tool or prompt list changing), and the server opens a stream tagged with your subscription ID. When the server wants to close the stream on purpose, it should now say a proper goodbye: a small "complete" result carrying that same ID. That way your client knows the difference between a normal close and a broken connection.

One buzzer replaces the old GET stream and both subscribe calls.
Something else quietly disappeared: you can no longer resume a broken stream. The old spec let you pick up where you left off with a Last-Event-ID header, which meant the server had to remember what it had already sent you. State again, hiding in the transport. Now, if a stream breaks, you send the request again with a new ID. If your buzzer breaks, nobody repairs it mid-ring; you take a new buzzer from the counter.
The question written on your receipt: MRTR
Nobody can come to your table anymore; the waiter who would have carried the kitchen's question is gone. So the cashier writes on your receipt: "Which sauce?" You come back to the counter with the same receipt and the answer written on it. Any cashier can complete the order from there. That is the pattern the spec settled on, and it has a name: Multi Round-Trip Requests (MRTR).
In the old protocol, a server could send requests back to the client in the middle of an operation. For example: "please ask the user a question" or "please run this through your model". That needs a two-way, stateful channel (stateful means the server must remember you between messages), which is exactly the thing the new version threw out.
The replacement: when a server needs something from you, it does not call you. It finishes the current request with a result marked resultType: "input_required", and lists what it needs in inputRequests. Your client then retries the same request with the answers attached. If the server sent a state token, the client sends that back too, unread; that is the note scribbled on your receipt. The server reads it and picks up where it stopped.

The kitchen writes its question on your receipt. You come back with the answer.
This is also why every result must now declare a resultType, either "complete" or "input_required". If an older server sends nothing, clients treat it as "complete".
The menu photo has an expiry time: caching hints
There is a photo of the menu taped up, with a note on it: valid for 10 minutes. That is what the new caching hints are. Being stateless has a cost, because clients ask "what tools do you have?" far more often, so the spec standardises the answer's shelf life.
Five results (tools/list, prompts/list, resources/list, resources/read, resources/templates/list) plus server/discover now carry two required fields: ttlMs and cacheScope. ttlMs is how long the result stays valid, in milliseconds. cacheScope decides who else may reuse it: "private" means only the same logged-in user (the same auth token), "public" means anyone. The photo either goes on the public board next to the card at the door, or stays on your table where only you can use it. Only final "complete" results carry these hints; an "input_required" half-answer can never be saved for reuse.
The complaint codes on the wall
Every branch of the restaurant now prints the same complaint codes on the wall. Error codes were renumbered and, for the first time, given a real rule about who owns which numbers: header mismatch is -32020, missing required client capability is -32021, unsupported protocol version is -32022. The range -32020 to -32099 now belongs to the spec itself, and servers and clients must not invent their own codes inside it. One older number moved too: resource not found is now -32602 instead of -32002, so clients should keep accepting the old code from older servers. Your client can react to a number instead of reading an error message and guessing.
The old side door is closing
A release this big also says goodbye to things:
-
Roots, Sampling and Logging are deprecated: still working today, marked for removal. Roots let a server see your folders; Sampling let it borrow your model. Logging control moved into
_meta, so you set alogLevelper request instead of callinglogging/setLevel. -
ping,logging/setLevel, and the roots list-changed notification are removed. - The original HTTP+SSE transport from 2024 (a transport is the way messages travel) is now formally deprecated under the new lifecycle policy.
- Tasks moved out of the core into an extension, so the core spec stays smaller.
- OAuth Dynamic Client Registration is deprecated and replaced by Client ID Metadata Documents. Skip this bullet if you do not touch OAuth.
The old side door still opens for a while, because the spec defines careful rules for talking to older servers. But the sign on it now says: please use the main entrance.
Why I am the one telling you this
I am not neutral about this release. For the last few weeks I have been moving the Dart team's MCP SDK (package:dart_mcp) to it, with five merged pull requests so far in dart-lang/ai. The work is tracked publicly in dart-lang/ai#162. When the final spec came out, I compared it line by line with the release candidate (the near-final draft) and posted the report there. So everything above comes from the schema and the docs themselves, not from a summary of a summary.
What this means for you
If you build apps that use MCP servers: wait for your SDK to update. After that there is less to get wrong: no handshake, no session to lose. Version and capabilities travel with every request.
If you write MCP servers: tape this one to the kitchen wall. server/discover is mandatory now, and every result needs a resultType. Your */list and resources/read results need ttlMs and cacheScope on top of that. The painful part is anywhere you used to call back into the client: that has to be redesigned around input_required, and it changes your control flow, not just your types. What you get back is a server that runs anywhere.
If you are just curious: the practical news is freedom in where servers can run. MCP servers used to need a machine that stays up and remembers things. Now they can run as short-lived functions close to users, and scale like any normal web service.
Also changed, in one breath
A few smaller items, so you know they exist:
- The OpenTelemetry keys
traceparent,tracestateandbaggagegot reserved slots in_meta, so one request can be traced across services. -
tools/listshould now return tools in a stable order. - A tool parameter can now be mirrored into an HTTP header with the new
x-mcp-headerannotation, under strict naming rules. -
clientInfois no longer required on requests, and servers should now sendserverInfoin every result's_meta. Both are recommended, not mandatory. - JSON Schema
$refresolution over the network (fetching schema pieces from other servers) is now banned by default.
None of it changes the story above. It is the boring kind of change, which in a protocol is the good kind.
Your receipt, please
A restaurant that runs on one waiter's memory is a lovely place, right up until the day he is not there. Counter service is duller. It also never loses your order, and it can open a hundred branches without breaking.
MCP just made that trade-off, and I think it chose well. If you want to see the change happen in real code, the Dart migration in dart-lang/ai#162 is open to read, one pull request at a time.
Top comments (0)