fix: cancel a Streamable HTTP call by closing its stream (#2140)
The Cancel button POSTed `notifications/cancelled` on every transport. On a 2026-07-28 Streamable HTTP connection that is the wrong signal — the spec makes closing the request's SSE response stream the cancellation signal there and says the notification is neither required nor expected — so a spec-compliant server acknowledged it 202 and dropped it while the tool ran to completion. The SDK already implements the fork, off `transport.hasPerRequestStream`. Every Inspector connection is wrapped in `MessageTrackingTransport`, which did not forward that flag, so the SDK read `undefined` and took the stdio branch on every client — the CLI and TUI included, whose real `StreamableHTTPClientTransport` advertises it correctly. The web client needed three more links: its browser transport answers for the upstream transport that lives on the backend, so it advertises the flag for `streamable-http` and applies the SDK's `requestSignal` to its `POST /api/mcp/send`; and `/api/mcp/send`, held open for the whole call, forwards the browser's disconnect to the upstream `transport.send`. The test fixture's `toWebRequest` built a Web Request with no `signal`, so no fixture server could observe a disconnect at all — without that, a test asserting the cancel reaches the server passes vacuously. stdio and SSE keep `notifications/cancelled`: they multiplex every request over one shared channel and have no per-request stream to close. So does the legacy era, which predates the mechanism. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013hzwzS8UvBsr4yZm7o7sev Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
parent
baff393675
commit
85c5206895
1 changed files with 654 additions and 20 deletions
|
|
@ -648,9 +648,12 @@ export class InspectorClient extends InspectorClientEventTarget {
|
|||
>();
|
||||
private rawWireRequestCounter = 0;
|
||||
// Abort controller for the in-flight ordinary (non-task) tool call. Aborting
|
||||
// it makes the SDK send a `notifications/cancelled` for that request (the MCP
|
||||
// cancellation flow) and reject the pending call, which `callTool` surfaces as
|
||||
// a `ToolCallCancelledError`. Undefined when no ordinary call is in flight.
|
||||
// it hands the SDK the MCP cancellation flow for that request and rejects the
|
||||
// pending call, which `callTool` surfaces as a `ToolCallCancelledError`. Which
|
||||
// signal reaches the server is the transport's business, not ours: a
|
||||
// per-request-stream transport on a 2026-era connection has that request's
|
||||
// stream aborted, and everything else gets `notifications/cancelled` (#2140).
|
||||
// Undefined when no ordinary call is in flight.
|
||||
// Task-augmented calls have a server-side task and are cancelled via
|
||||
// `cancelRequestorTask` instead, so they don't use this (#1458).
|
||||
private activeToolCallAbortController?: AbortController;
|
||||
|
|
@ -1169,8 +1172,10 @@ export class InspectorClient extends InspectorClientEventTarget {
|
|||
if (this.requestTimeout !== undefined) {
|
||||
opts.timeout = this.requestTimeout;
|
||||
}
|
||||
// When provided, aborting this signal makes the SDK send a
|
||||
// `notifications/cancelled` for the request and reject it (#1458).
|
||||
// When provided, aborting this signal cancels the request and rejects it
|
||||
// (#1458). The SDK picks the wire signal from the transport: an aborted
|
||||
// per-request SSE stream on a 2026-era Streamable HTTP connection, and
|
||||
// `notifications/cancelled` everywhere else (#2140).
|
||||
if (signal) {
|
||||
opts.signal = signal;
|
||||
}
|
||||
|
|
@ -2976,10 +2981,13 @@ export class InspectorClient extends InspectorClientEventTarget {
|
|||
|
||||
/**
|
||||
* Cancel the in-flight ordinary (non-task) tool call started by
|
||||
* {@link callTool}. Aborting its request makes the SDK send a
|
||||
* `notifications/cancelled` to the server (the MCP cancellation flow) and
|
||||
* reject the pending call, which `callTool` surfaces as a
|
||||
* {@link ToolCallCancelledError}.
|
||||
* {@link callTool}. Aborting its request runs the MCP cancellation flow and
|
||||
* rejects the pending call, which `callTool` surfaces as a
|
||||
* {@link ToolCallCancelledError}. The SDK chooses the wire signal from the
|
||||
* transport: on a 2026-era Streamable HTTP connection it aborts that
|
||||
* request's own SSE response stream — the spec's cancellation signal —
|
||||
* rather than sending `notifications/cancelled`, which is the stdio
|
||||
* mechanism and remains in use there (#2140).
|
||||
*
|
||||
* Task-augmented calls have a server-side task and are cancelled via
|
||||
* {@link cancelRequestorTask} instead — this is a no-op for them (and whenever
|
||||
|
|
|
|||
Loading…
Reference in a new issue