You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
- Supports cursor-based pagination for list operations
44
-
- Supports server-side cancellation of in-flight requests (notifications/cancelled)
44
+
- Supports cancellation of in-flight requests on both server and client (notifications/cancelled)
45
45
46
46
### Supported Methods
47
47
@@ -1205,12 +1205,7 @@ poll it to exit early. When a tool returns after cancellation has been observed,
1205
1205
the server suppresses the JSON-RPC response, matching the spec. The `initialize` request
1206
1206
is never cancellable per the spec.
1207
1207
1208
-
> [!NOTE]
1209
-
> Client-initiated cancellation (`Client#cancel` equivalent that would also abort
1210
-
> the calling thread's wait) is not yet implemented. Sending `notifications/cancelled`
1211
-
> from the client side can be done by constructing the notification payload and writing it
1212
-
> directly through the transport, but the calling thread does not yet unwind automatically.
1213
-
> This is tracked as a follow-up.
1208
+
Client-initiated cancellation is also supported: see [Client-Side: Cancelling an In-Flight Request](#client-side-cancelling-an-in-flight-request) below.
1214
1209
1215
1210
#### Server-Side: Handlers that Check for Cancellation
1216
1211
@@ -1319,6 +1314,60 @@ Nested cancellation propagation is supported on `StreamableHTTPTransport` only.
1319
1314
the parent `tools/call` is cancelled. The parent tool itself still observes cancellation
1320
1315
via `server_context.cancelled?` between nested calls.
1321
1316
1317
+
#### Client-Side: Cancelling an In-Flight Request
1318
+
1319
+
`MCP::Client` lets the caller cancel a request it has already issued. The recommended pattern is to pass
1320
+
an `MCP::Cancellation` token into the request method, run the request on a worker thread, and call
1321
+
`cancellation.cancel(reason:)` from another thread. The cancelling thread sends `notifications/cancelled` to
1322
+
the server, and the calling thread is woken up with `MCP::CancelledError`:
Request ids are managed internally, so the token is the only thing a caller needs to cancel a request.
1341
+
1342
+
> [!NOTE]
1343
+
> When a cancel wins the race, the SDK's worker thread that is blocked on the underlying I/O is *not* force-killed;
1344
+
> it stays blocked until the transport actually returns (or the user closes the transport). This matches the server-side
1345
+
> `StreamableHTTPTransport#send_request` trade-off. For `StreamableHTTPTransport#send_request` trade-off. For `Client::HTTP`
1346
+
> the leak resolves as soon as the server sends any response; for `Client::Stdio` you may need to call `client.transport.close`
1347
+
> to free the thread if the server stops responding entirely. The cancel-dispatch thread waits for the worker's send-boundary signal
1348
+
> (`&on_sent` from `send_request`) before issuing `notifications/cancelled`, so the cancel is held until the worker has at
1349
+
> least committed to writing the request; while the worker is wedged the cancel notification is deferred along with it.
1350
+
1351
+
##### Wire-order guarantees
1352
+
1353
+
`Client::Stdio` serializes the request write and any subsequent `notifications/cancelled` write through a single `@write_mutex`,
1354
+
so the server is guaranteed to read the request line before the cancel line.
1355
+
1356
+
`Client::HTTP` cannot offer the same wire-arrival guarantee. Faraday's synchronous `post` does not expose a post-write / pre-response hook,
1357
+
so the SDK yields just before the request POST is dispatched. After the yield, the cancel-dispatch thread issues a separate `notifications/cancelled` POST
1358
+
on its own connection, and the two POSTs may overlap on the network. The spec is satisfied either way: the sender has already issued the request and
1359
+
still believes it to be in-progress when issuing the cancel ([MCP cancellation spec](https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/cancellation)),
1360
+
and on the receiver side, "receivers MAY ignore a cancellation notification whose `requestId` is unknown" covers the case where the cancel POST
1361
+
happens to arrive first. The calling thread raises `MCP::CancelledError` regardless of network ordering.
1362
+
1363
+
##### Custom transports
1364
+
1365
+
Custom transports that want to support `cancellation:` must implement `send_notification(notification:)` so `notifications/cancelled` can be delivered.
1366
+
They should also accept the optional block passed to `send_request(request:, &on_sent)` and call it once the request bytes have been handed off to the wire
1367
+
(under a write-side mutex for stdio-style transports, immediately before the synchronous round-trip for HTTP-style transports).
1368
+
The cancel-dispatch thread waits on this signal before sending `notifications/cancelled`. Transports that do not invoke the block fall back to waiting for
1369
+
the worker thread to terminate, which preserves wire-order at the cost of delaying the cancel notification until the request has fully completed.
0 commit comments