MCP just had its biggest release since launch.
On July 28, the maintainers shipped the 2026-07-28 spec, and it changes how MCP servers work at a pretty fundamental level.
The handshake is gone.
Sessions are gone.
Three long-standing features are deprecated.
The maintainers themselves called it the most substantial change since authorization was added.
Their words, not mine.
Sounds scary.
But it actually makes MCP servers much easier to deploy.
And what am I here for?
I'm here to help you build and deploy one.
Your MCP server is now just a regular stateless HTTP service.
Round-robin load balancing, autoscaling, and caching all work.
No sticky sessions or shared session state.
In this guide, we'll build a small MCP server on the new spec, connect a client to it, see every headline feature actually running, and then deploy it to Cloudflare Workers.
For free. ℹ️ All the code here uses the new TypeScript SDK v2, released alongside the spec.
If you're on the old package, that's v1 now.
What's Covered What actually changed in the 2026-07-28 spec in short Building an MCP server with the new SDK v2 Stateless core in action MRTR: how a tool requests user confirmation without holding a stream open A graceful fallback for clients that don't speak MRTR yet (there are many) Cacheable tool lists with Testing it with a client and raw curl Deploying it to Cloudflare Workers on the free plan Changes in the new MCP Spec (2026-07-28) Quick rundown of what's new.
If you want the full changelog, it's on the official spec site.
The handshake is gone The / exchange and the Mcp-Session-Id header are officially retired.
Every request is now self-describing.
It carries its own protocol version, client identity, and capabilities in .
Any request can land on any server instance behind a plain load balancer.
Such a relief!!
There's an optional RPC if a client wants capabilities up front.
But it's optional.
One bare POST is a complete conversation now.
Multi Round-Trip Requests (MRTR) This one is my favorite.
Before, if a tool needed something from the user mid-call, such as confirmation or a missing parameter, the server had to push an request back over a held-open stream.
That meant you needed a held-open stream, which was bad for stateless deployments.
MRTR flips it.
The server returns with the questions it needs answered, and closes the connection.
The client collects the answers and retries the original call with them attached, plus an opaque token so the server knows where it left off.
No open streams.
No sessions.
Interactive tools on fully stateless infra.
Header-based routing Requests now carry and HTTP headers.
Your gateway, rate limiter, or WAF can route and meter on headers without parsing JSON bodies.
Cacheable list results , , , and responses now carry and fields, modeled on HTTP's Cache-Control.
Clients cache your tool catalog instead of re-fetching it every time they connect.
Extensions framework + deprecations Tasks moved out of the experimental core into an official extension ().
MCP Apps and Enterprise Managed Authorization live there too.
You can build your own extensions as well.
And the deprecations: Roots, Sampling, and Logging are deprecated.
They keep working for at least 12 months, but new implementations shouldn't use them.
The legacy HTTP+SSE transport is deprecated with a year-long offramp.
Dynamic Client Registration is deprecated in favor of Client ID Metadata Documents (CIMD).
There's also a formal deprecation policy now: a 12-month minimum window for anything marked deprecated.
So you get to plan upgrades, which is noicee!
The SDK Split One more thing before we build: the TypeScript SDK is no longer one package. v2 splits it into , , and thin framework adapters (, , , ).
Building an MCP Server Finally, we're onto the build.
We will build a quick tiny deploy bot over MCP.
It has three tools: asks the user for confirmation before deploying (MRTR in action). reads back the deployment history proves a fresh server instance handled every request Here's the trick that pays off at deploy time: all the MCP logic lives in one platform-neutral file (), and each platform gets a tiny entry file.
Node gets .
Cloudflare gets .
Both are about ten lines.
An MCP server on the new spec is just a fetch handler; the platform is a serving shim.
You'll understand everything along the way.
Step 1: Install the SDK v2 Run the following command: ℹ️ On TypeScript 6+, add to your tsconfig after installing .
TS 6 no longer auto-includes , and you'll get errors without it.
Ask me how I know. 😴 Step 2: The server logic Create .
This is the whole MCP server, with zero platform code in it: A few things worth explaining here: runs on every single request.
Not once at startup.
Every request gets a brand-new instance.
If that surprises you, I get it.
It surprised me too.
But this is literally the canonical pattern from the SDK's own examples, and it's the whole point of the release.
Construction is just object creation and a handler map, microseconds of work.
There's no protocol state to preserve anymore, so there's nothing to keep alive.
Per-request server construction, per-process resources.
App state (our deployments array, the state codec, your DB pool in real life) lives at module level.
The server instance is disposable.
The tool never blocks.
When it needs confirmation, it returns and the request is over.
Done.
Connection closed.
The token is the only thing that survives between rounds, and it round-trips through the client.
This means the client could tamper with it.
That's why we seal it with , so tampered or expired state gets rejected with a wire-level error before our handler even runs.
Notice the codec is lazily created on first use instead of at module level.
That looks like a pointless indirection on Node.
It's not.
Cloudflare Workers forbids generating random values in global scope, and this exact line is what lets the same file run on both platforms.
Same story with the guards: Workers has no global by default.
So the tool reads the client's declared capabilities from the per-request envelope (that's the lookup, with a legacy-connection fallback) and if: Client supports elicitation then the full MRTR confirmation flow Client doesn't then the tool accepts an optional argument, and without it, it returns a plain instruction: "Ask the user, then call deploy again with confirm: true" Step 3: The Node entry Create .
This is everything Node-specific: That's it. gives you a standard fetch-style handler, and Hono is just routing. validates Host/Origin headers (DNS rebinding protection) and only allows localhost out of the box, so the env var is there for when this runs behind a real domain.
Everything is env-driven (, , , ) because that's what a VM or a PaaS like Railway wants.
We won't use this file for the Cloudflare deploy, but it's your path if you'd rather run this on Node anywhere.
Step 4: The client Create : ⚠️ Don't miss .
Without it, the client negotiates the legacy 2025-11-25 protocol and the MRTR flow fails.
This took me half an hour to debug.
Notice the elicitation handler is a completely normal handler, the same one you'd write for the old flow.
The SDK's auto-fulfillment engine routes the embedded MRTR request through it and retries the tool call for you.
Your code doesn't even see the round trip.
Step 5: Run it In two terminals (better with tmux), run the following: In the first terminal: And in the other: This is the kinda output you'd get: Every line here demonstrates a spec feature, and I designed it that way: : we're on the new protocol, not the legacy fallback + : the second never touched the network The elicitation line, then the deploy: that was two POSTs.
First one returned and closed.
Second had the answer plus the sealed .
No stream was ever held open. : six requests, six fresh server instances, one process.
Under a load balancer, those six could've hit six different machines.
How cool is that?
The final : app state survived even though protocol s
