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
|
|
@ -203,6 +203,30 @@ export class MessageTrackingTransport implements Transport {
|
|||
return this.baseTransport.sessionId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Forward the base transport's per-request-stream capability (#2140).
|
||||
*
|
||||
* The SDK's `Protocol.request` reads this off the transport it was handed —
|
||||
* which is always this wrapper — to decide how a cancelled request is
|
||||
* signalled: abort that request's own stream (the 2026-07-28 signal for
|
||||
* Streamable HTTP) or POST a `notifications/cancelled` (the stdio one).
|
||||
*
|
||||
* Not forwarding it made the wrapper *answer* the question, `undefined`, for
|
||||
* every client. So the Cancel button POSTed a notification even on a modern
|
||||
* Streamable HTTP connection, where a spec-compliant server acknowledges it
|
||||
* `202` and drops it — the tool kept running and only a disconnect really
|
||||
* cancelled anything. That hit the CLI and TUI as much as the web client:
|
||||
* their real `StreamableHTTPClientTransport` advertises the flag correctly
|
||||
* and this wrapper hid it.
|
||||
*
|
||||
* A base transport that says nothing stays `undefined` rather than becoming
|
||||
* `false`, since the SDK's check is `=== true` either way and inventing a
|
||||
* value here would misreport what the transport claimed.
|
||||
*/
|
||||
get hasPerRequestStream(): boolean | undefined {
|
||||
return this.baseTransport.hasPerRequestStream;
|
||||
}
|
||||
|
||||
// Implemented as a concrete method (rather than delegating the base
|
||||
// transport's optional `setProtocolVersion`) so the SDK Client always
|
||||
// invokes it after the initialize handshake — including for stdio, whose
|
||||
|
|
|
|||
Loading…
Reference in a new issue