Rayfin | Go from prompt to production backend
June 24, 2026Customer spotlight: Capgemini is turning comms and conversations into AI-ready knowledge
June 24, 2026A couple of months ago I wrote about scaling MCP servers behind App Service’s built-in load balancer. The trick back then was to lean on stateless HTTP transport so any instance could serve any request — and to make sure you turned off ARR affinity so the load balancer was actually free to spread traffic around.
That post still works. But the MCP spec just caught up to it in a big way.
The 2026-07-28 release candidate is the largest revision of the Model Context Protocol since it launched, and the headline change is exactly the thing we were working around: MCP is now stateless at the protocol layer. The handshake is gone, the session header is gone, and the sticky-routing-and-shared-session-store dance that horizontal deployments used to need is no longer part of the protocol at all.
If you’re hosting an MCP server on App Service, this is good news — and it means a few of the steps from my last post are now things the protocol does for you. Here’s what changed, and what (if anything) you need to do about it.
Here’s the before and after, straight from the spec. In
2025-11-25, the clientPOSTs aninitializecall to/mcpfirst and gets a session ID back:
{“jsonrpc”:”2.0″,”id”:1,”method”:”initialize”,
“params”:{“protocolVersion”:”2025-11-25″,”capabilities”:{},
“clientInfo”:{“name”:”my-app”,”version”:”1.0″}}}
Heads up on timing:
2026-07-28is a release candidate as I write this; the final spec ships July 28, 2026. It contains breaking changes, so treat this as “get ready” guidance rather than “rip everything out today.”
Quick recap: how we scaled MCP before
In the original post, the recipe looked like this:
- Run the MCP server in stateless HTTP mode (the
2025-11-25transport). - Scale App Service out to N instances (the sample used three).
- Set
clientAffinityEnabled: falseso there’s no ARR affinity cookie pinning a client to one instance. - If you genuinely needed cross-request state, externalize it — typically into Azure Cache for Redis — so every instance saw the same data.
- Watch traffic spread across instances in Application Insights via
cloud_RoleInstance.
The catch: even in “stateless HTTP” mode, the 2025-11-25 protocol still started every connection with an initialize handshake and handed back an Mcp-Session-Id that the client had to send on every follow-up request. That session ID pinned a client to whichever instance issued it — so to scale cleanly you either kept affinity on (and gave up even load balancing) or did real work to share session state across instances.
That’s the part the 2026 spec deletes.
What the 2026 spec actually changes
The handshake and the session are gone
Two proposals do the heavy lifting:
- SEP-2575 removes the
initialize/initializedhandshake. The protocol version, client info, and client capabilities that used to be exchanged once at connect time now ride along in_metaon every request. A newserver/discovermethod lets a client ask for server capabilities when it actually wants them. - SEP-2567 removes the
Mcp-Session-Idheader and the protocol-level session that came with it.
With both gone, any MCP request can land on any instance. The sticky routing and shared session stores that horizontal deployments needed before just aren’t required at the protocol layer anymore.
Here’s the before and after, straight from the spec. In 2025-11-25, the client POSTs an initialize call to /mcp first and gets a session ID back:
{“jsonrpc”:”2.0″,”id”:1,”method”:”initialize”,
“params”:{“protocolVersion”:”2025-11-25″,”capabilities”:{},
“clientInfo”:{“name”:”my-app”,”version”:”1.0″}}}
…then every later call has to carry the Mcp-Session-Id header the server handed back, which pins it to that instance:
{“jsonrpc”:”2.0″,”id”:2,”method”:”tools/call”,
“params”:{“name”:”search”,”arguments”:{“q”:”otters”}}}
In 2026-07-28, the same tool call is one self-contained request that any instance can answer. The routing info rides in headers — MCP-Protocol-Version, Mcp-Method, and Mcp-Name — and the body carries everything else:
{“jsonrpc”:”2.0″,”id”:1,”method”:”tools/call”,
“params”:{“name”:”search”,”arguments”:{“q”:”otters”},
“_meta”:{“io.modelcontextprotocol/clientInfo”:{“name”:”my-app”,”version”:”1.0″}}}}
No handshake, no session ID, nothing to pin.
Traffic you can route and cache at the edge
A few smaller changes make this traffic much friendlier to the infrastructure App Service already gives you:
- Routable headers (SEP-2243): Streamable HTTP now requires
Mcp-MethodandMcp-Nameheaders, so load balancers, gateways, and rate-limiters can route or throttle on the operation without cracking open the request body. (Servers reject requests where the headers and body disagree.) - Cacheable lists (SEP-2549):
tools/listand resource-read results now carryttlMsandcacheScope, modeled on HTTPCache-Control. Clients know exactly how long a tool list is fresh and whether it’s safe to share across users — no more holding an SSE stream open just to learn the list changed. - Traceable calls (SEP-414): W3C Trace Context (
traceparent,tracestate,baggage) propagation in_metais now documented with fixed key names. A trace that starts in the host app can follow a tool call through the client SDK, your MCP server, and whatever it calls downstream — and show up as one span tree in any OpenTelemetry backend, including Application Insights.
That last one pairs really nicely with the App Insights setup from the original sample, which already tags spans with cloud_RoleInstance.
Why this is easier on App Service now
App Service’s built-in load balancer has always wanted to round-robin your requests. The thing stopping it from doing that cleanly with MCP was the protocol’s own session affinity. Now that the protocol is stateless:
- No affinity tuning to reason about. You still want
clientAffinityEnabled: false, but there’s no longer a protocol session fighting it. - Any instance serves any request, for real. Scale from 3 to 10 instances and the load balancer just spreads the work — no shared session store required for protocol state.
- Less Redis glue. In the old model, Redis was often there to share protocol session state. That reason is gone (see the next section for what Redis is still great for).
“Stateless protocol” doesn’t mean “stateless app”
This is the part I want to be really clear about, because it’s easy to over-read the headline.
Removing the protocol session does not mean your application can’t have state. It means the protocol stops carrying state for you. If your server needs to remember something across calls, you do what HTTP APIs have always done: mint an explicit handle and let the model pass it back as an argument.
The spec calls this the explicit-handle pattern. A tool returns a basket_id (or browser_id, or whatever), and later calls include that ID as a normal parameter:
// 1) create returns a handle
{“name”: “create_basket”, “arguments”: {}}
// -> { “basket_id”: “b_12345” }
// 2) later calls pass it back as an ordinary argument
{“name”: “add_item”, “arguments”: {“basket_id”: “b_12345”, “sku”: “ABC”}}
The nice side effect: the model can see the handle, compose it across tools, and hand it off between steps — in ways that session state hidden in transport metadata never really allowed.
So where does Redis fit now? Exactly where it always belonged — your application’s data, not the protocol’s plumbing:
- Backing store for those explicit handles (what’s actually in basket
b_12345). - Caching expensive lookups or model responses across instances.
- App-level conversation memory or rate-limit counters.
Stateless protocol, stateful application. You externalize state because your app needs it shared, not because the transport forces you to.
Migrating an existing MCP server on App Service
If you deployed the original sample (or something like it), here’s the punch list to get to the 2026 model. The good news: the App Service / infra side barely changes — most of the work is in the protocol layer your SDK handles for you.
App Service config — mostly already done:
- Keep
clientAffinityEnabled: false. (Still the right call.) - Keep scaling out to N instances. Nothing here changes.
- Keep Application Insights + OpenTelemetry — and lean into the new Trace Context key names for cleaner end-to-end traces.
Protocol layer — the real work:
- Update to an SDK build that speaks
2026-07-28. The handshake and session handling go away; your server reads protocol version and client info from_metaper request instead of from aninitializeexchange. - Emit
ttlMs/cacheScopeontools/listand resource reads so clients (and your gateway) can cache them. - Make sure your server honors / validates the
Mcp-MethodandMcp-Nameheaders. - If you were storing anything keyed off
Mcp-Session-Id, move it to the explicit-handle pattern (handle in, handle out, state in Redis/Cosmos/etc.). - Audit for the breaking bits:
tasks/listis removed, Roots/Sampling/Logging are deprecated, and the “resource not found” error code moves from-32002to the standard-32602.
I built a standalone companion sample for exactly this — the
2026-07-28version of the original, with the handshake gone, everything read from_meta,server/discoverimplemented, and the explicit-handle pattern shown in a real tool. Link below.
Try it yourself
I built a companion sample for this post: a FastAPI MCP server that speaks 2026-07-28 natively — no handshake, no session — running on three App Service instances behind the built-in load balancer, with a staging slot, App Insights, a spec-compliant client, and a k6 load test:
👉 seligj95/app-service-mcp-stateless-scale-2026-python
azd auth login
azd up
That provisions a Premium v3 plan with capacity: 3, the web app with clientAffinityEnabled: false, a staging slot, and Log Analytics + Application Insights. No initialize, no Mcp-Session-Id anywhere — discovery is a single server/discover call, and every request carries its own protocol version and client info in _meta.
The part I like best is the tally tool. It keeps a running total across calls using an explicit, signed handle instead of a session — so you can watch the total stay correct even as the load balancer routes each call to a different instance:
+10 -> total=10 served_by=2103650c…
+5 -> total=15 served_by=08fc7022… (different instance, total still right)
+100 -> total=115 served_by=08fc7022…
That’s the stateless handle pattern from earlier, made concrete: state travels with the request, not the connection. Then watch the load spread in Application Insights:
requests
| where timestamp > ago(15m)
| where name contains “/mcp”
| summarize count() by cloud_RoleInstance
Want the 2025-11-25 version for comparison? That’s the original Part 1 sample: seligj95/app-service-mcp-stateless-scale-python. Diff the two main.py files and you can see the handshake and session handling simply disappear.
The takeaway
When I wrote the first post, “make MCP stateless so App Service can load-balance it” was a pattern you had to apply. With the 2026 spec, it’s just how MCP works. The protocol deleted the exact friction we were routing around — which means hosting a horizontally scaled MCP server on App Service is now closer to “deploy a normal web app and scale it out” than ever.
If you’re already running MCP on App Service: you did the hard part early. The spec just made it official.
Got an MCP server running on App Service? I’d love to hear how the migration goes — drop a comment.