An agent calls a tool to search a large index. The user changes their mind, or the upstream service stalls. If the client simply stops waiting, the server may keep burning CPU or holding a database connection. If the client retries a write, it may create a second record. A deadline and a cancellation path solve different parts of this problem: the deadline bounds the wait, while cancellation tells the other side to abandon work when possible.

The MCP cancellation specification distinguishes the cancellation signal by transport. That distinction matters more than memorizing one JSON message.

Set two deadlines, not one

A client should set a timeout for each request it sends. A short metadata lookup might have a tighter budget than a report-generation tool. MCP recommends configurable per-request timeouts and a maximum timeout even when progress messages arrive. Progress can show that work is active; it must not turn a slow request into an infinite one.

Choose an operational budget for the client and a corresponding budget for the server’s dependencies. For example, a client might allow a search tool several seconds while the server reserves a smaller share of that budget for its database query. Those values are your application policy, not defaults prescribed by MCP. Make sure the server has enough time to return a useful error before the client gives up.

Timeout is not proof of failure. The operation may have finished just as the deadline expired, or may still be running remotely. For a read-only search, retrying is often tolerable. For a tool that charges a card, creates a ticket, or sends a message, first determine whether the side effect happened. Use an idempotency key or an operation-status lookup where the underlying service supports one. Do not treat a timed-out write as safe to replay.

Cancel over the transport you actually use

With stdio, the client sends a notifications/cancelled notification containing the ID of its in-progress request. Here is an illustrative notification for a request whose ID was 123:

{
  "jsonrpc": "2.0",
  "method": "notifications/cancelled",
  "params": {
    "requestId": "123",
    "reason": "User requested cancellation"
  }
}

The request ID must match the original request; it is not the tool name or a new notification ID. Only cancel requests that this client issued and believes are still running. The reason is optional, but a useful one can make incident logs easier to read. A notification does not have its own response.

With Streamable HTTP, close the response stream for the in-flight request. The current specification says the server must treat that client disconnect as cancellation. Do not send a separate notifications/cancelled message for this transport. If your HTTP stack hides disconnects, check how its request-abort signal reaches your handler rather than assuming that closing the browser tab is enough to stop a backend job.

See stdio transport in MCP and Streamable HTTP transport before wiring a cancellation hook to the wrong connection lifecycle.

Make the handler cooperative

Cancellation is a request to stop, not a rewind button. Arrange your handler so it can observe cancellation between bounded units of work. Pass the SDK’s cancellation or abort signal down to database, HTTP, and subprocess calls where those dependencies support it. Release connections and temporary files in cleanup paths. A handler that starts a detached job and immediately returns cannot rely on request cancellation to stop that job; give such jobs their own lifecycle and status interface.

If the handler notices cancellation before a side effect, it can stop without committing anything. Once a side effect has committed, report or record its actual state rather than implying cancellation rolled it back. This is especially important in the multi-server agent workflow: stopping one tool request does not automatically undo another server’s completed action.

The spec says a server receiving a valid cancellation notification should stop processing, free resources, and avoid sending a response for that request. It also allows the server to ignore a notification if work has already finished, the ID is unknown, or the operation cannot be cancelled. Clients should ignore a late response to a request they cancelled. Race conditions here are normal, not necessarily a protocol fault.

Test the race, not just the happy path

Use the local MCP debugging guide to exercise a deliberately slow, read-only tool. Start a call, cancel it while it is working, and check that its resources are released. Then cancel just after it completes. Try an unknown request ID and verify that it does not affect another request. For HTTP, close the specific response stream and inspect server logs for the disconnect. For stdio, verify the notification references the original request ID.

Keep logs useful without dumping user input or secrets: request ID, operation type, elapsed time, cancellation reason, and whether a side effect had already committed. If a tool returns huge results while it runs, paginate tool output rather than trying to fix excessive response size with a longer timeout.

FAQ

Does cancelling an MCP call undo a payment or database write? No. Cancellation asks the server to stop work; it does not supply transaction rollback across external services. Check the authoritative state before retrying.

Should progress notifications keep a request alive forever? No. An implementation may reset an inactivity timeout on progress, but the specification recommends enforcing a maximum timeout regardless of progress.

Why did I get a response after cancelling? Cancellation can arrive after processing finishes. The protocol expects both sides to handle that race; the client should ignore a late response for a request it cancelled.