On MCP's stdio transport, how does a client cancel an in-flight request?
answer
- nothing to close on a shared pipe
- it travels in-band as a notification
- names the request by its id
- advisory, never a rollback
- the late answer must be discarded
basics
~10 sThe client writes a notifications/cancelled notification naming the requestId of the call it wants abandoned. Cancellation is advisory: the server should stop work, and the client stops waiting whether or not it does.
solid answer
~50 sStdio has one shared pair of pipes and no per-request stream, so there is nothing to close in order to signal "stop" — cancellation has to be an explicit message. The client writes a `notifications/cancelled` notification on stdin identifying the in-flight request by its `requestId`. Being a JSON-RPC notification it carries no `id` and gets no reply. The effect is **best-effort**: the server should stop processing and release resources, but a call already committed to a side effect cannot be un-run, and a response that was already on the wire may still arrive. The client therefore stops waiting on that id and discards any late answer for it. Two things it must not do: kill the subprocess, which abandons every other concurrent request as well, and reuse the cancelled id. This contrasts with the HTTP binding, where the client closes the request's own response stream and the server must treat that closure as cancellation.
code
json · 1 line{"jsonrpc":"2.0","method":"notifications/cancelled","params":{"requestId":42,"reason":"user aborted the operation"}}go deeper
Know that cancelling means sending notifications/cancelled naming the request id, and that you never kill the server process just to abandon one call.
Explain why stdio needs an explicit message — one shared channel, no per-request stream to close — and that it is a notification, so there is no acknowledgement and no reply.
Demonstrate the race handling: settle the call locally, discard a late response for that id, never reuse the id, pair cancellation with your own timeout, and accept that committed side effects are not undone.
Own the semantics you promise users. Decide which tools are safe to cancel and retry, where cooperative task cancellation or compensating actions are needed, and how the UI represents work that may already have happened.
## Why stdio needs an explicit message On the stdio binding both peers share exactly one channel in each direction. Every request, response and notification is interleaved on the same stdin and stdout pipes and correlated only by the JSON-RPC `id`. There is no per-request stream, so there is no per-request thing to close — closing stdin would stop the entire server, not one call. Cancellation therefore has to travel in-band as a message like anything else. That is a genuine difference from the HTTP binding, where each request has its own response stream and closing that stream **is** the cancellation signal, which the server must honour. ## The mechanism The client writes `notifications/cancelled` to the server's stdin, naming the `requestId` of the request it wants abandoned; an optional human-readable reason may accompany it. Because it is a JSON-RPC *notification* it carries no `id` of its own and receives no response — you never get an acknowledgement that the cancellation was honoured. The same notification also serves in the other direction as the way either side steps away from long-lived work, including tearing down a `subscriptions/listen` stream. ## Best-effort semantics Cancellation is advisory, and every serious answer says so: - **The race is unavoidable.** By the time the notification is parsed, the server may already have written the response. Messages cross on the wire, so the client must tolerate a result arriving for a request it cancelled and discard it. - **Side effects are not undone.** A tool that has already sent the email, charged the card or deleted the file cannot un-do it on cancellation. Cancellation stops *further* work; it is not a rollback. - **Cooperation is required.** A server stuck in a blocking syscall may not notice the notification at all. Nothing in the protocol can force it. So the client's own state machine is what actually implements the cancellation: it settles the pending call locally (typically as cancelled), stops charging the user for it, and ignores anything that later arrives bearing that id. ## Hygiene rules **Do not reuse the id.** JSON-RPC ids must stay unique for the connection's lifetime; a cancelled id is spent. If the client wants the work retried it sends a new request with a new id — which is also exactly how 2026-07-28 handles a stream that breaks, since every request is self-contained and nothing is resumable. **Do not cancel what was never sent.** A cancellation for an unknown or already-completed id should be ignored by the receiver rather than turned into an error; it is a normal consequence of the race. **Do not kill the process.** Terminating the subprocess is a shutdown, not a cancellation: it destroys every other concurrent request and forces a relaunch. Reach for it only when the server is genuinely wedged. **Timeouts are the other half.** Because a cancellation may be ignored, a client should also enforce its own per-request timeout, and cancelling on timeout is the normal pairing: give up locally, tell the server so it can stop burning CPU. ## Progress and long calls Long-running work is usually the reason to cancel, and the pieces fit together: a client that supplies a `progressToken` in a request's `_meta` receives `notifications/progress` for that request, giving the user something to watch and a sensible moment to press cancel. Under 2026-07-28 those request-scoped notifications flow on the originating request's own response path — on stdio, that is simply the shared stdout stream, tagged by the token. For genuinely long jobs the better structure is often the `io.modelcontextprotocol/tasks` extension, whose cooperative cancellation is a separate mechanism, but for an ordinary in-flight call on stdio, `notifications/cancelled` is the tool.
- The response for the cancelled request arrives anyway. What should the client do?Discard it. Cancellation is best-effort and the two messages can cross on the wire: the server may have written the response before it parsed the notification. The client has already settled that call locally, so it drops any late result or error bearing that id rather than delivering it to the caller.
- Can the client reuse the cancelled request's id for the retry?No. JSON-RPC ids must remain unique for the life of the connection, and a cancelled id is spent — reusing it makes a late response indistinguishable from the answer to the retry. The retry is an ordinary new request with a new id, which is also how 2026-07-28 handles any lost in-flight request, since nothing is resumable.
- How does this differ on the Streamable HTTP binding?There each request has its own response stream, so the client cancels by closing that stream and the server must treat the closure as cancellation. On stdio there is only one shared pair of pipes, nothing per-request to close, so the intent has to be sent explicitly as notifications/cancelled.
saying these in an interview costs you the question
- Kills the subprocess to cancel one request
- Expects a response confirming the cancellation
- Assumes cancellation rolls back side effects already done
- Reuses the cancelled request id for the retry
- Thinks closing stdin cancels just that call