MCP 2026-07-28 one lesson per page
23 lessons
Breaks immediatelyInteractions · SecurityTrack 2 · 5 / 14

Sessions are gone. State becomes a handle you pass around

The Mcp-Session-Id header and protocol-level sessions are removed. If a tool needs to remember something between calls, it hands the client an explicit ID.

Listen to this lessonAudio overview in Gemini Notebook · about 15–25 min · opens in a new tab

The order that survived a deploy

A food-delivery assistant builds an order over six tool calls in ten minutes: restaurant, two mains, a drink, an address, a tip. Halfway through, your Kubernetes cluster rolls out a new version and every pod is replaced.

Legacy: the order lived in the old pod's session map. The user hears "Sorry, let's start again."

Now: the first call returned orderId: "ord_7Kq…", stored in a database. The new pods look it up and carry on. Because the handle is visible to the model, the user can switch from phone to laptop and say "add fries to my order", and it still works.

The twist: an attacker notices the handles look like ord_0001, ord_0002… and tries other people's orders. That's state handle hijacking. Unguessable IDs, plus an ownership check on every call, stop it.

  • There's no Mcp-Session-Id. A modern-only server SHOULD ignore the header and never mint session IDs, and SHOULD answer HTTP GET or DELETE on the endpoint with 405.
  • State that spans requests MUST be referenced by an explicit identifier that the client passes on each request: a server-minted handle, sent as an ordinary tool argument.
  • tools/list, resources/list and prompts/list no longer vary by connection. They may still vary by the caller's authorization.

A session ties a client to whichever process holds its state. Handles move the state somewhere every instance can reach, such as a database, and put the reference in plain sight. The model can see cartId in a tool result and reason about it.

Same job, two eras
Compare

This is how it works in 2026-07-28. Switch to Legacy only to see what it replaced.

ClientServer AServer Btools/call create_cartresult {cartId: "cart_9fK2…"}Cart stored in a shared database, keyed by cartId + usertools/call add_to_cart {cartId, sku}result (B looked the cart up)
Solid arrows are requests, dashed are responses or notifications. Red ✕ is gone; green is new.

Keeping a shopping cart between calls

2026-07-28
// ① the first call mints a handle and returns it{"jsonrpc": "2.0", "id": 1, "result": {  "resultType": "complete",  "content": [{ "type": "text", "text": "Cart created" }],  "structuredContent": { "cartId": "cart_9fK2xQ7Lw0" }}}// ② every later call passes it back as an ordinary argument{"jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": {   "name": "add_to_cart",   "arguments": { "cartId": "cart_9fK2xQ7Lw0", "sku": "MUG-01" },   "_meta": { … per-request fields … } }}
What it used to look like (legacy, for comparison only)
Legacybefore
POST /mcp HTTP/1.1Mcp-Session-Id: 1868a90c-5f4e-4c1a-9d2b-7f3e1c0a9b44Content-Type: application/json {"jsonrpc": "2.0", "id": 7, "method": "tools/call", "params": { "name": "add_to_cart",             "arguments": { "sku": "MUG-01" } }}// the server looks up sessions["1868a90c…"].cart, which lives in ONE process's memory

The handle name and shape are yours to choose. The spec only requires that cross-call state be referenced explicitly on each request.

  • Anything kept in a session map is unreachable: the working directory, open DB connections, half-built objects, cached auth decisions.
  • It can fail silently. If you keyed state by connection, two unrelated conversations sharing one stdio process will see each other's data.
  • Secure the handle: generate it with real randomness, give it an expiry, and on every call check that the authenticated caller owns it. Skip that, and you've built the attack the spec names state handle hijacking.
First time here? Set up the test helper (once per terminal)
ShellSetup
# 1. In a SECOND terminal, start the reference server (Node 18+, no dependencies)curl -sO https://www.diegozuluaga.dev/mcpa/reference-server.mjsnode reference-server.mjs                 # http://localhost:3000/mcp, logs appear here # 2. In THIS terminal, define the helper every test uses (bash or zsh)export MCP=http://localhost:3000/mcpMETA='"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{}}'mcp() {  # usage: mcp <method> '<json body>' [extra curl args...]  curl -sS -N "$MCP" \    -H 'Content-Type: application/json' \    -H 'Accept: application/json, text/event-stream' \    -H 'MCP-Protocol-Version: 2026-07-28' \    -H "Authorization: Bearer ${MCP_USER:-alice}" \    -H "Mcp-Method: $1" "${@:3}" -d "$2" \    -w '\nHTTP %{http_code}\n'}# Demo auth: the reference server treats the bearer token as the user's name.# Prefix a command with MCP_USER=bob to act as someone else.
ShellTests
# 1. The old endpoints are gonecurl -sS -X GET "$MCP" -o /dev/null -w 'GET -> %{http_code}\n'        # expect 405curl -sS -X DELETE "$MCP" -o /dev/null -w 'DELETE -> %{http_code}\n'  # expect 405 # 2. A stray session header is ignored, and no session ID comes backmcp tools/list '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{'"$META"'}}' -H 'Mcp-Session-Id: abc' -D - | grep -i mcp-session-id# expect: no output # 3. Alice creates a cart and gets a handle backCART=$(mcp tools/call '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"create_cart","arguments":{},'"$META"'}}' -H 'Mcp-Name: create_cart' | sed -n 's/.*"cartId":"\([^"]*\)".*/\1/p'); echo "$CART" # 4. Alice uses it: expect "Added MUG-01"mcp tools/call '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"add_to_cart","arguments":{"cartId":"'"$CART"'","sku":"MUG-01"},'"$META"'}}' -H 'Mcp-Name: add_to_cart' # 5. Hijack attempt: Bob replays Alice's handle. Expect isError true, "Unknown cart"MCP_USER=bob mcp tools/call '{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"add_to_cart","arguments":{"cartId":"'"$CART"'","sku":"TV-85IN"},'"$META"'}}' -H 'Mcp-Name: add_to_cart' # On your own server, the real test: run TWO instances with no sticky routing,# create the cart on one and add to it on the other. (The reference server keeps# carts in memory to stay dependency-free; a production server would use a shared store.)

Answer all 3 correctly and this lesson is marked as learned.

Q1Your tool builds a report across several calls. Where should the partial report be referenced?

Q2What should a 2026-07-28-only server do with an HTTP GET to its MCP endpoint?

Q3Your handles are random UUIDs. Is that enough to prevent state handle hijacking?

Feedback or a correction? Email diego [at] diegozuluaga [dot] dev or open an issue on GitHub.

Content CC BY 4.0 · Code MIT

Tip: ← and → move between lessons. Hover any heading and press # to copy a link to it.