How does MCP handle request cancellation and progress notifications for long-running tool calls?

5 minadvancedmcpcancellationprogressnotifications

Quick Answer

For cancellation, either side can send notifications/cancelled with the requestId of an in-flight request it no longer needs a result for; the receiver should stop processing that request if possible (there's no guarantee of an immediate stop, since some operations can't be interrupted mid-flight) and must not send a response for a cancelled request's original ID. For progress, the original request includes a progressToken in its _meta field, and the long-running side periodically emits notifications/progress messages carrying that same token plus a progress value (and optionally a total), letting a UI show a progress bar for something like a slow file upload or a multi-step tool operation, without blocking on the final result to show any feedback at all.

Detailed Answer

Some tool calls take seconds or minutes — a large file conversion, a slow external API. MCP gives both sides a way to communicate about that duration without blocking silently.

Progress: opting in per-request

// Client includes a progressToken when it wants updates
{"method": "tools/call", "params": {
  "name": "convert_video", "arguments": {"path": "movie.mov"},
  "_meta": {"progressToken": "abc123"}
}}

// Server periodically sends progress notifications using that same token
{"method": "notifications/progress", "params": {
  "progressToken": "abc123", "progress": 40, "total": 100
}}
{"method": "notifications/progress", "params": {
  "progressToken": "abc123", "progress": 100, "total": 100
}}

// Eventually the actual result arrives, correlated by the original request's id
{"id": 9, "result": {"content": [{"type": "text", "text": "Converted successfully"}]}}

Progress is opt-in. If the client doesn't include a progressToken, the server shouldn't send progress notifications for that request at all.

Cancellation

// Either side can cancel an in-flight request by its id
{"method": "notifications/cancelled", "params": {
  "requestId": 9, "reason": "User closed the dialog"
}}

After sending this, the original request's id (9) should never receive a response. The requester has explicitly said it no longer cares about the outcome.

Important caveats

  • Cancellation is best-effort. Some operations — a database transaction already committed, an external API call already dispatched — can't be undone just because the client stopped waiting. A well-behaved server tries to stop or clean up, but the spec doesn't guarantee instantaneous or even eventual success in halting the underlying work.
  • A request for the initialize method itself must not be cancelled. Cancelling the handshake mid-flight would leave the connection in an undefined state.
  • Progress notifications are purely informational. A client shouldn't rely on them for correctness, only for UX — showing the user that something is happening, rather than appearing frozen.

Related Resources