kulikowski.me
← Writing

One trip, two MCP clients: 2026-07-28 MCP revision

8 min read#agentic-web #mcpview as markdown

MCP just shipped its 2026-07-28 revision, and the headline change is a big one: the protocol session is gone. No more initialize / notifications/initialized handshake, no more Mcp-Session-Id. I read through the release post and the changelog, and built a demo against it. Those two links cover the full list of changes better than I would, so let me walk you through the demo instead and point out the nuances along the way 🤔.

The highlight of this release is a stateless protocol core - MCP is transforming from a bidirectional stateful protocol into a request/response stateless protocol.

Demo - MCP Travel

Starting something on your laptop and finishing it on your phone is normal everywhere else on the web. I wanted to see what that looks like when the two ends are separate MCP clients that never talk to each other 😀.

So I started experimenting: Kulikowski/mcp-2026-07-28-playground, a small trip-booking demo built on the v2 TypeScript SDKs with the protocol pinned to 2026-07-28.

The demo running in three panels. Left: the MCP Travel website at step 3 of 4, reviewing the itinerary - a completed 'Add destination' MCP App card, a chat message 'The route is ready. Show me the itinerary.', and a 'View itinerary' App with a Load my itinerary button. Middle: a protocol log - tools/call going through the proxy on :8090, results answered by the trip servers on :3001 and :3002, notifications/resources/updated followed by a refreshed resources/read, and a client-side ui/notifications/tool-input notification whose JSON carries trip_id, phase 'reviewing', client_id 'website' and version 4. Right: the Chrome extension side panel showing 'Continue an unfinished trip' with Kasper's trip at 'Reviewing the itinerary', trip_o2whnfxl, 2 stops, state v4, and buttons to continue it or start a new one.
One trip, two clients: the website (left) is reviewing the itinerary, the protocol log (middle) shows the wire calls - consecutive ones answered by different backends through the :8090 gateway - next to labelled client-side rows like the App bridge, and the side panel (right) has already spotted the unfinished trip.

The setup: a website guides you through building a trip across a few server-provided MCP Apps, and a Chrome side panel can pick up an unfinished trip and continue it as a completely separate client. Nothing passes between them - the panel finds the trip on the server and carries on.

You might expect a second client picking up unfinished work to need the first one's session. It never worked that way: a session belonged to one client, and a second client turning up with the same Mcp-Session-Id is the shape of the thing the old spec called session hijacking. And with the SDK's stateless switch from the opening note there was no session to pick up in the first place. Your application's own state is a different thing: it names the work, not the client, so two clients touching one piece of it is the point rather than the problem - as long as the server checks who is asking.

You could always build this yourself - nothing ever stopped a tool from taking a trip_id argument 😅. What changed isn't that it became possible, it's that the handle stopped being your own thing sitting next to the protocol: state that spans requests must now be referenced by an explicit identifier the client passes on every call, and a whole section on stateful tools shows what that looks like - the server mints the handle and gets it back as an ordinary tool argument. And the security advice is to bind that handle to the authenticated user, not to a client - which is what makes a second client turning up with the same trip an ordinary request.

The handshake going away helps too. initialize came first even when the server minted no session at all, so the side panel had to say hello before it could ask which trips were unfinished. Now the first request can be the actual question.

Picking the trip up in a second client is exactly why every trip carries a phase and a version. With no session, a second client has two questions nothing answers for it anymore: where were we? - that's phase, telling the side panel which App to mount next - and has it changed since I last looked? - that's version, a counter that goes up with every accepted change (the trip's version, not another protocol version).

The demo's gateway never opens the JSON-RPC body. It routes on two new required headers from the transport spec, Mcp-Method and Mcp-Name, which mirror the method and the name or URI sitting in that body.

Mcp-Name only rides along on tools/call, resources/read and prompts/get, so there isn't that much to route on. The demo pins resources/read to a fixed backend by method, pins two tools by name, and round-robins everything else. subscriptions/listen is pinned by method too - a long-lived notification stream has to sit on one backend anyway.

That only works because any server that reads the body must reject a request where header and body disagree - without that rule, the headers would be an easy way to lie to your infrastructure. The headers are spec; the routing rules are just this demo showing them off. A real gateway would do more homework before trusting them: the transport spec tells intermediaries to check the protocol version first, because header-body validation is only required from 2026-07-28 on.

Caching is the next thing you can watch happen. Results like tools/list now carry two hints from the caching spec: ttlMs, how long the result stays fresh, and cacheScope - public for any shared cache, private for one authorization context only.

In the demo the side panel calls tools/list twice on startup, and the second call is served from the SDK's own cache - confirmed by watching a network counter not move. The default is not a generous one: the spec says a missing ttlMs counts as 0, and the SDK treats a missing cacheScope as private, so nothing is served from cache unless the server asked for it. Both fields are required from 2026-07-28 on, so that fallback is really there for older servers. And the scope is decided per resource - the demo's trip data is private, while the App markup (also resources/read) is public, because it's the same HTML for everybody.

Now, interesting questions: what if two clients change the same trip? and what if the same call arrives twice? 🤔

The first one is easy to picture: the website and the side panel are both looking at the same trip, and with no session there's nothing declaring either one the owner. This is exactly what the version counter is for: every change to an existing trip says which version it thinks it's changing (expected_version), and if the trip moved on in the meantime, the write is refused with stale_state instead of quietly overwriting the other client's work. None of those fields are MCP, the spec leaves this entirely to you. It just matters more now that explicit handles are the pattern for cross-request state.

The second: what happens if book_trip gets called twice? You get the same confirmation code back, and nothing gets booked twice.

That's not just politeness - with the new Multi Round-Trip Requests (MRTR), the same tools/call arriving twice is the normal case rather than an anomaly. A server can no longer hold a call open to ask the client something; it returns a result with resultType: "input_required" and the questions attached, and the client answers by calling the tool again as a new, independent request. Nothing is resumable anymore either, so a dropped stream comes back as a fresh call too. That's why, when the server asks "this fare cannot be refunded, book it anyway?", the very first thing the tool does on the retry is check "did I already book this trip?" before doing anything else.

Before you ship an MRTR flow of your own, read the MRTR spec closely: the optional requestState blob that carries server context across that round trip is attacker-controlled input, and the spec has a whole recipe for protecting it.

A few takes

I'll drop a few personal opinions here, take them as exactly that 😅, happy to have a chat about it!

1️⃣ The best thing about this release is how familiar it makes MCP look. Protocols don't win by being clever, they win by looking familiar to what already exists. ttlMs and cacheScope are Cache-Control: max-age and public / private - the caching spec calls them analogous itself. Trace context rides in _meta as traceparent and tracestate, which is the OpenTelemetry convention rather than a new MCP one. And the new headers exist for the same reason HTTP puts the method in the request line: so the box in the middle can route without reading the body 😎.

2️⃣ You can read the whole release as MCP announcing where it thinks it's going. A handshake and a session were fine when MCP was a pipe between your editor and a local dev tool. A stateless core with gateway-friendly headers is built for fleets of servers behind load balancers, serving agents at scale. And look at what the same release deprecated: Roots, Sampling and Logging - local files, the client's own model, the client as your log viewer. They all still work, but new implementations are told not to add them. Specs are roadmaps if you read them that way - and this one points away from the local machine 💡.

3️⃣ MRTR is the most agentic feature here. A server can now ask a human a question mid-tool-call without anyone holding a connection open - the question travels as data, the answer comes back as a retry. That matters once the agent runs in the cloud instead of on your laptop: the human might answer in a minute or in an hour, and nothing has to keep a connection alive while they think. How long that hour can be (😅) is the server's call though - the spec asks for a short expiry inside requestState, so the wait is bounded by whatever the server signed into it.

What's next

MCP Apps is the thing I keep coming back to in this release, and I've been writing about WebMCP for two posts straight. Server-provided UI on one side, browser-native tools on the other... you can probably guess the experiment I want to run next - MCP Apps via WebMCP. No idea yet if it will actually work, but that's half the fun 🤔.

More soon. ✨