Development Choices

Cancel in-flight MCP requests safely

Author
Joseph TrasattiMember of technical staff
Published
Section
MCP
Length
5 min read3 sources cited

Send a `notifications/cancelled` notification containing the original request ID only from the party that issued that request. Treat cancellation as advisory: stop costly work when possible, suppress the original response after accepting cancellation, and tolerate completion winning the race before the notification arrives.

Prerequisites

An MCP request starts work and may receive a cancellation notice before or after completion
Implement cancellation as best effort and make late arrival harmless.

Maintain a registry of in-flight requests at both ends of the connection. Each entry needs the request ID, its direction, its current state, and a way to ask the underlying operation to stop. Without the direction, one party can accidentally cancel a request issued by the other party that happens to use the same ID.

Use this procedure for ordinary in-flight requests. A client must not cancel initialize; let the connection reach normal operation as described in the MCP initialization lifecycle. Task-augmented requests use tasks/cancel instead because tasks have a separate cancellation flow and final state.

Steps

  1. Allow each party to cancel only its own outgoing requests.

    Either MCP party can send notifications/cancelled, but only for a request that party previously issued and believes is still in flight. The MCP cancellation rules dated 2025-11-25 require the referenced request to have travelled in the same direction as the cancellation.

    Keep separate outbound and inbound registries, or include the direction in every registry key. For example, if a server issued request 42 to a client, the server may cancel it. The client may not use that ID to cancel an unrelated client-to-server request.

    Before sending cancellation, check that the outbound entry still has a pending state. If it is already completed, there is no work left to cancel. This check reduces stale notifications, but it cannot eliminate them because the remote party can finish between the local check and the notification arriving.

  2. Give every running operation a cancellation path.

    When receiving a request, register its ID before starting the expensive part of the operation. Associate it with whatever cancellation primitive the implementation uses: an abort signal, a cancelled flag checked between stages, or a handle supplied by a worker or subprocess.

    Put cancellation checks around meaningful boundaries such as remote calls, queues, transformation stages, or result assembly. A flag checked only after all expensive work finishes satisfies the shape of an implementation but saves no resources.

    Keep request IDs distinct from progress tokens. Progress tokens correlate notifications/progress messages, while cancellation names the original request through requestId. If the operation reports intermediate state, follow the separate flow for MCP progress notifications.

  3. Send a notification containing the original request ID.

    Construct the message using the CancelledNotification schema:

    {
      "jsonrpc": "2.0",
      "method": "notifications/cancelled",
      "params": {
        "requestId": "req-42",
        "reason": "User stopped the operation"
      }
    }

    requestId identifies the non-task request being cancelled. reason is optional and may be logged or shown to the user. Do not put an id at the top level. That would make the cancellation message look like a request rather than a notification.

    This distinction has a concrete consequence: the JSON-RPC 2.0 notification rules say a notification has no top-level id and receives no response. The receiver therefore must not acknowledge notifications/cancelled with a result or error. Keep this protocol message separate from transport and connection state; MCP session management over Streamable HTTP addresses that different layer.

  4. Stop expensive work when the notification wins the race.

    On receipt, look up the corresponding inbound request. If it is still running and can be interrupted, mark it cancelled before triggering its cancellation handle. Stop expensive work when possible and release resources associated with the operation.

    Make the state transition the gate for later response handling. A simple model is:

    running --completion wins--> responding --> completed
    running --cancel wins------> cancelled

    Only one transition out of running may succeed. If cancellation wins, completion code must see cancelled and discard any result it produces. If completion wins, cancellation finds that the operation is no longer running and does nothing.

    Once the receiver acts on cancellation, it must not send a normal response for the original request—not a successful result and not a JSON-RPC error. Cancellation is not a failed method invocation, so do not turn it into one of the JSON-RPC protocol errors. It is also not enough to stop the worker while leaving an outer response callback free to serialize a result. Both the work and the response path need to consult the same state.

  5. Treat cancellation as advisory at the sender.

    Sending the notification does not prove that remote work stopped. The receiver may have completed the operation, and may even have sent its response, before the notice arrives. Cancellation is therefore advisory rather than a rollback or confirmation mechanism.

    After sending notifications/cancelled, mark the local outbound entry as cancellation requested and make the caller stop waiting for a useful result. If a response arrives afterward, ignore it for application purposes. Do not assume that its arrival means the receiver violated cancellation: completion may have won the network race before the receiver could act.

    The receiver may likewise ignore a cancellation for an unknown request ID, an already completed request, a request it cannot cancel, or a malformed notification. Because the cancellation message is a notification, none of those cases produces a reply to the cancellation sender.

  6. Test both outcomes of the race.

    Cover the cases that determine whether the implementation sends a response:

    • Cancel while an interruptible operation is running. The stop handle runs, associated work is released where possible, and no result or error is sent for the original request.
    • Complete immediately before cancellation is processed. The normal response remains valid, and the late cancellation is ignored.
    • Deliver a response after the issuer has requested cancellation. The issuer accepts the message at the protocol layer but does not use its result.
    • Send cancellation with an unknown, completed, or wrong-direction request ID. The receiver ignores it and sends no acknowledgement.
    • Reproduce completion and cancellation concurrently. Exactly one state transition wins; the implementation never both accepts cancellation and emits a response for that request.

Expected result

Each party can cancel a non-task request it issued by sending notifications/cancelled with the original request ID. A receiver that accepts the cancellation stops expensive work where possible and sends no response for the original request. If completion wins first, the response may still arrive, and the cancellation sender ignores it.

Sources

  1. MCP cancellation rules dated 2025-11-25modelcontextprotocol.io
  2. `CancelledNotification` schemamodelcontextprotocol.io
  3. JSON-RPC 2.0 notification rulesjsonrpc.org

See also