MCP didn’t remove sessions. It handed them to the model

Read more at:

A few years ago, I helped move an application off sticky sessions so we could scale behind an ordinary round-robin load-balancer. On paper it was an infrastructure change. Every service was supposed to be stateless, so removing session affinity should have changed nothing.

Nothing crashed. CPU looked normal. Every health check stayed green. But users started reporting workflows that would randomly jump backwards. One request would pick up exactly where the previous one left off. The next would behave as if it belonged to an entirely different conversation.

The bug wasn’t in our business logic. It was in an assumption we’d inherited about where correlation lived. We’d quietly relied on the platform to remember which requests belonged together. Once that responsibility disappeared, our application didn’t fail loudly. It just became inconsistent in ways that were very hard to reproduce.

We fixed it the obvious way. We stopped relying on the platform and started passing an explicit identifier on every request. It worked, and it kept working, because the thing carrying that identifier did exactly what we told it to do.

That’s the incident I kept thinking about while I read the new Model Context Protocol specification.

When MCP shipped its stateless core on July 28, I did what everyone else did. I opened the changelog, found the SDK migration notes and scoped the work. The code diff was smaller than I expected. I took that as good news for about four hours.

I’d been reading it as a transport change. Sessions gone, requests self-contained, no sticky routing. All true, and all covered elsewhere. What I missed was what the maintainers had done with correlation.

They didn’t delete it.

They handed it to the model.

What the spec actually moved

Buried in the release post is guidance for servers that still need state across calls: mint an explicit handle from a tool, such as a basket ID or a browser ID, then have the model pass it back as an ordinary argument on later calls. The maintainers are clear about why. The model can see the handle and thread it between tools, instead of state hiding in a connection you can’t inspect.

From an engineering standpoint, I think they’re right. It scales, it survives a load-balancer and it matches how HTTP APIs have worked for decades.

But read it again as an operator. The identifier your server uses to decide which basket, which document or which tenant is now carried by a language model, held in its context window and re-emitted on a later turn.

We just made an LLM part of the request path. That’s not a sentence I expected to write about a transport specification.

It means your server’s correctness now depends, in part, on the model faithfully carrying an identifier from one turn to the next.

There’s an objection I hear every time. MCP was always stateless in the way that matters, people say. It never held your shopping cart. It never stored your pull request. Connect a calendar and the model won’t know about the meeting you added ten minutes ago until something fetches it. All correct, and all true since launch.

But MCP always had two kinds of state. The first is your application’s data, which lives in your application, in GitHub or in Outlook. MCP never owned that and still doesn’t.

The second is protocol session state: the handshake, the negotiated capabilities and the session identifier that told the server two requests belonged to the same conversation. MCP did own that one, and that’s the one that’s gone.

The confusion runs in a convenient direction. If you believe MCP was always stateless, this release looks like a scaling optimization with no architectural consequences.

It isn’t.

The protocol didn’t remove your application’s state. It relocated correlation, out of a protocol-managed session and into an argument the model carried from one turn to the next.

We have measurement on this

What turns this from an observation into something teams get hurt by is that threading an identifier across turns isn’t novel. It has a name in the research literature. Cross-turn information fusion is among the most studied failure modes in multi-turn model behavior.

LLMs Get Lost in Multi-Turn Conversation,” an outstanding paper at ICLR 2026, ran more than 200,000 simulated conversations across fifteen leading open and closed-weight models. It found a 39% average performance drop in multi-turn settings against single-turn settings with identical information. Function calling was one of the six tasks tested.

First, how the degradation decomposes. Aptitude falls about 16%. Unreliability rises 112%. The models stay capable and become dramatically less consistent. If you’ve shipped anything agentic you know what that feels like from the outside: a system that works in every demo, passes every test you wrote, then produces one inexplicable result a week that nobody can reproduce.

Second, not everything degraded. Translation, which can be done sentence by sentence with no dependency between turns, showed no degradation at all. What fell apart were the tasks that required information to be carried across turns and fused.

That’s exactly what a handle is.

So, the protocol relocated session state into the one place we have the strongest evidence models struggle: carrying information reliably across turns. It did so for good reasons.

This is a failure mode I’ve hit repeatedly on agentic systems, and it’s why I’ve become insufferable about observability. When a model passes the wrong handle, your server doesn’t get an error. It gets a well-formed request with a valid-looking argument. If the handle points at a real object, the call succeeds.

No log line says the model lost the thread. No metric turns red. A session that quietly split into two half-populated baskets looks exactly like a user who changed their mind.

Red Hat published an incident writeup recently that shows the shape of it, with no MCP involved. An agent needed to charge Account A. The identifier was right there in its prompt. The model selected Account B. The billing API accepted the call, and four thousand dollars went to the wrong customer. It had passed every test in staging.

In ordinary software, a misrouted call is contained by two things: credential scoping limits what a workload can reach, and the downstream API enforces which parameters are valid for that caller. Nobody would ship a web application where one set of credentials could write to every customer’s billing record. We ship agents like that all the time.

Now take that failure and make the identifier something the model carries across turns instead of something handed to it in a single prompt.

There’s a second-order consequence I haven’t seen raised. Follow the chain. Your server’s behavior depends on the handle. The handle is carried by the model. The model is chosen by the client, not by you.

So, the reliability of your MCP server now depends, in part, on whichever model the client happens to be using. That varies across vendors, across model upgrades and across the same client over time. The ICLR paper found meaningful spread across the fifteen models tested. Your integration suite runs against one of them.

What I’m doing about it

Nothing exotic. The controls are ordinary. Only the reason is new.

I authorize every handle against the authenticated caller on every request, with no exemption for internal tools. I give them real entropy and short lifetimes. I use idempotency keys so a retry after a lost handle doesn’t quietly create a second object. And I test at turn depth thirty rather than turn one, because turn one is where all these systems look fine.

The adversarial reading of this isn’t mine. Akamai’s threat research team got there first, and SecurityWeek covered it ahead of the final specification: once protocol sessions are gone, the handles that replace them become the attack surface, opening paths to workflow hijacking, reading another agent’s data and cross-tenant actions.

I’d add one wrinkle.

A session identifier lived in a protocol header the model never saw. A handle lives in the context window, right next to whatever the last tool call returned. If any of your tools ingest untrusted content, whether a web page, a ticket body or a customer email, your state identifier and an attacker’s text now sit in the same context, interpreted by the same model.

I’d rather have explicit handles I can audit than session state hidden in a connection. That’s the better architecture and I’d have voted for it.

But there’s a difference between state you can see and state you can trust.

This release solved the first problem by introducing the second.

Which leaves the question I can’t stop thinking about.

If the identifier is now carried by the model, what exactly is the boundary that decides whether it should have been trusted in the first place?

That’s no longer a transport question.

It’s an identity question, and the answer isn’t in this specification.

Source link

spot_img
Multi-Function Air Blower: Blowing, suction, extraction, and even inflation
spot_img

Leave a reply

Please enter your comment!
Please enter your name here