skip to content

If an MCP response stream breaks mid-request, how does a 2026-07-28 client recover?

level: middleimportance: should knowfreq 56%

answer

  1. nothing is replayed
  2. the id is not reusable
  3. a repeat, not a resume
  4. closing the stream reads as cancellation
  5. the tool owns duplicate-safety

basics

~10 s

It cannot resume. Revision 2026-07-28 removed stream resumability, so a broken stream loses the in-flight request outright and the client must re-issue the same call as a brand-new request with a new JSON-RPC id.

solid answer

~40 s

There is no resumption path. MCP revision 2026-07-28 removed SSE event ids and `Last-Event-ID` replay, so when a response stream drops the in-flight request is simply lost: the client MUST send the call again as a **new request with a new JSON-RPC id**, not retry under the old id and not ask for the missed portion. That makes duplicate execution a real possibility, which is why the retry safety of a tool is the tool's own responsibility — an idempotent operation, or an idempotency key accepted in its arguments. `idempotentHint` in `ToolAnnotations` does not solve it: annotations are untrusted hints for the client's benefit, not a guarantee. On stdio there is also a companion rule — after a reconnect the client MUST re-send `subscriptions/listen`, because nothing about the previous listen survives.

code

json · 16 lines
json
{
  "jsonrpc": "2.0",
  "id": 1002,
  "method": "tools/call",
  "params": {
    "name": "create_invoice",
    "arguments": {
      "amount": 100,
      "clientRequestId": "a41f-6b2d-4c19"
    },
    "_meta": {
      "io.modelcontextprotocol/protocolVersion": "2026-07-28",
      "io.modelcontextprotocol/clientCapabilities": {}
    }
  }
}

go deeper

for a junior

Remember that MCP has no resume: if the connection carrying a call dies, you send the call again as a new request with a new id, and there is no way to ask for the part you missed.

for a middle

Explain what was removed in 2026-07-28 — SSE event ids and Last-Event-ID replay — and why the id must be fresh: the id correlates one request with one response and nothing ties it to abandoned work.

for a senior

Show you have designed for it: idempotent tools or an idempotency key as a tool argument, bounded retry counts, servers safe under abandonment given that a closed stream is cancellation, and no reliance on untrusted annotations for retry safety.

for a principal

Argue the tradeoff — replay buffers were per-request server state that forced session affinity, so removing them bought stateless fleets and moved duplicate-safety into the tool contract, and set the org-wide convention for how mutating tools express deduplication.

## What was removed Through revision 2025-11-25, MCP over HTTP could make a dropped stream survivable. The server attached ids to the events it emitted on a response stream, and a client that lost the connection could reconnect quoting `Last-Event-ID`; the server replayed from that point and the original request completed as though nothing had happened. Revision 2026-07-28 deleted that machinery — no SSE event ids, no `Last-Event-ID`, no replay — and put nothing in its place. ## The rule today A broken stream loses the in-flight request. The client's only recovery is to issue the call **again, as a new request, with a new JSON-RPC id**. Re-sending under the original id is wrong: the id is the correlation handle for one request/response pair, and a server that never finished the first one has no notion of resuming it. There is likewise nothing to ask for a partial result with — no `ping` (removed in the same revision), no per-request status probe in the core protocol. The same revision made stream closure meaningful in the other direction: on Streamable HTTP, closing the response stream **is** cancellation and the server MUST treat it as such. So a network drop and a deliberate client cancel look identical to the server. That is intentional and it is why the server side should be written to tolerate abandonment: the work may be halfway done, and the client may show up again with an identical call under a fresh id. ## Why this is acceptable Resumability was expensive for what it bought. To replay, a server has to buffer emitted events per request for some window, which is state — precisely the connection-scoped state the stateless redesign was trying to eliminate — and it forced load balancers into affinity so a reconnect reached the buffering node. Dropping it made an MCP endpoint an ordinary stateless POST endpoint, at the cost of pushing retry semantics up into the tool contract, where the tool author actually knows whether a repeat is safe. ## Making retries safe Since the client's only recovery is a repeat, retry safety has to be designed in: - **Prefer idempotent operations.** A read, a search, a query — repeating them costs latency and nothing else. - **Accept an idempotency key** for anything that mutates. Let the caller pass a stable key as an ordinary tool argument, deduplicate on it server-side, and return the original result for a repeat. This is the same technique any payments API uses, expressed as a tool parameter because MCP has no protocol-level slot for it. - **Do not lean on annotations.** `ToolAnnotations` offers `readOnlyHint` (default false), `destructiveHint` (default true), `idempotentHint` (default false) and `openWorldHint` (default true). They are hints for a client's UI and policy decisions, and clients MUST treat them as untrusted unless the server is trusted. They describe intent; they do not deduplicate anything. - **Bound the blast radius.** Cap how many times a client re-issues, and surface the failure to the user rather than silently hammering a tool whose stream keeps dying. ## The stdio companion rule Stdio has no per-request streams to break, but it has the same underlying principle: nothing survives a reconnect. If the subprocess connection is re-established, the client MUST re-send `subscriptions/listen` — the previous listen and its filter are gone. Notifications scoped to a specific request (`notifications/progress` and, when the caller asked for logs via `io.modelcontextprotocol/logLevel`, `notifications/message`) flow on the originating request's own response stream, so when that stream dies those notifications die with it and reappear only on the re-issued request. ## Answering well State the removal, state the exact replacement behaviour — new request, new id — and then move straight to the consequence the interviewer is actually probing: duplicate side effects, and where responsibility for them lives. Candidates who reach for `Last-Event-ID` are describing the 2025-11-25-and-earlier protocol.

  • Can the client reuse the original JSON-RPC id when it re-issues the call?
    No. Revision 2026-07-28 requires a new id. The id correlates exactly one request with exactly one response; the server has no record tying the old id to resumable work, and reusing it invites a collision if the first execution is still running and eventually tries to answer. Treat the re-issue as an entirely fresh request that happens to carry the same parameters.
  • How does a server distinguish a client that cancelled from a client whose network dropped?
    It does not, over Streamable HTTP: closing the response stream is cancellation, and the server MUST treat a closed stream as such. Both look the same on the wire. Write the server so abandonment is safe at any point — release resources, avoid half-committed writes — and expect that an identical call may arrive shortly afterwards under a new id.
  • Doesn't idempotentHint tell the client the retry is safe?
    It hints, it does not guarantee. `idempotentHint` is one of the `ToolAnnotations` (alongside `readOnlyHint`, `destructiveHint` and `openWorldHint`), and clients MUST treat annotations as untrusted unless the server itself is trusted. It is useful for deciding whether to auto-retry without asking the user; it is not a substitute for the server actually deduplicating.

saying these in an interview costs you the question

  • Reconnects with Last-Event-ID to resume the stream
  • Re-sends the request under the original JSON-RPC id
  • Assumes the server buffers and replays missed events
  • Believes idempotentHint makes duplicate execution impossible
  • Thinks a dropped stream leaves the request queued server-side

context