MCP 2026-07-28 one lesson per page
23 lessons
Fails quietlyInteractions & Execution · 26%Track 2 · 9 / 14

A dropped stream loses the request. Closing one cancels it

SSE resumability (Last-Event-ID) is gone. If a response stream breaks, the client re-sends the request with a new id. And on HTTP, hanging up is how a client says stop.

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

The laptop lid at minute two

A user asks their agent for a ten-minute "annual report with charts", then closes their laptop lid two minutes in.

Legacy: nothing told the server. It spent eight more minutes of API credits on a report nobody would read, and if the tool ended with "email it to the team", it sent that too.

Now: the lid closes, the stream closes, and that is the cancellation. The server aborts and spends nothing more. If the report had to survive a closed lid, the server would have returned a Task handle instead, and the client could pick it up the next morning.

  • There are no SSE event IDs and no Last-Event-ID. A server that receives the header ignores it.
  • A broken response stream loses the in-flight request. The client MUST re-issue it as a new request with a new id.
  • On Streamable HTTP, closing the response stream is the cancellation. The server SHOULD stop work as soon as it can and MUST NOT send anything more for that request. (On stdio, notifications/cancelled is still used.)
  • For work that must survive disconnects, use the Tasks extension (a later page).

Resumability meant keeping a replay buffer per stream, which is state again, and pinned to one instance. Dropping it keeps servers stateless. Treating disconnect as cancel removes an extra message type from HTTP and makes cancellation unambiguous, because each request has its own stream.

Same job, two eras
Compare

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

ClientServerPOST tools/call (id 7)notifications/progressstream closes: request 7 is cancelled; server stopsPOST tools/call (id 8), the same call againresult
Solid arrows are requests, dashed are responses or notifications. Red ✕ is gone; green is new.

Recovering from a drop

2026-07-28
// the stream for request 7 dropped, so request 7 is gonePOST /mcp HTTP/1.1Content-Type: application/jsonMCP-Protocol-Version: 2026-07-28Mcp-Method: tools/callMcp-Name: build_report {"jsonrpc": "2.0", "id": 8, "method": "tools/call", "params": { "name": "build_report", "arguments": { "month": "2026-09" },             "_meta": { … per-request fields … } }}
What it used to look like (legacy, for comparison only)
Legacybefore
GET /mcp HTTP/1.1Accept: text/event-streamMcp-Session-Id: 1868a90c-5f4e-4c1a-9d2b-7f3e1c0a9b44Last-Event-ID: 42// the server replays everything after event 42

Same call, new id. If running it twice is unsafe, make the tool idempotent, or return a Task handle.

  • Long tools on flaky networks can run twice. A drop at minute nine of ten means starting again.
  • If your server keeps working after the client hangs up, it wastes resources and may complete actions nobody is waiting for. Wire the request's disconnect signal to an abort.
  • Any replay-buffer or event-ID code can be deleted.
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
# Start a slow tool (10 s), then press Ctrl-C after a few progress eventsmcp tools/call '{"jsonrpc":"2.0","id":7,"method":"tools/call","params":{"name":"build_report","arguments":{"month":"2026-09"},"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{},"progressToken":"p7"}}}' -H 'Mcp-Name: build_report'# expect in the server terminal: "build_report cancelled at step N (stream closed)" # A Last-Event-ID header is ignored (no replay)mcp tools/list '{"jsonrpc":"2.0","id":9,"method":"tools/list","params":{'"$META"'}}' -H 'Last-Event-ID: 42' 

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

Q1Over Streamable HTTP in 2026-07-28, how does a client cancel an in-flight request?

Q2Spot the bug: the SSE stream for tools/call id 7 drops. The client reconnects with GET /mcp and Last-Event-ID: 42 to pick up where it left off.

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.