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
36
README.md
36
README.md
|
|
@ -163,6 +163,42 @@ Each config below is a ready-made server for exercising one feature by hand. Loa
|
|||
| `subscriptions-{legacy,modern}-http.json` | Resource subscriptions, both eras | [#1630](https://github.com/modelcontextprotocol/inspector/issues/1630) |
|
||||
| `subscriptions-never-acknowledged-http.json` | A `subscriptions/listen` answered with a bare result | [#2097](https://github.com/modelcontextprotocol/inspector/issues/2097) |
|
||||
| `tasks-{legacy,modern}-http.json` | Tasks, both eras | [#1631](https://github.com/modelcontextprotocol/inspector/issues/1631) |
|
||||
| `cancellation-modern-http.json` | Cancelling a call by closing its response stream | [#2140](https://github.com/modelcontextprotocol/inspector/issues/2140) |
|
||||
|
||||
#### Cancelling a call
|
||||
|
||||
`cancellation-modern-http.json` serves `slow_task`, which reports progress once
|
||||
a second for up to 60 seconds and returns early — saying how far it got — if it
|
||||
is cancelled. Connect with **Protocol Era = Modern**.
|
||||
|
||||
Run `slow_task` from the Tools tab and click **Cancel** after a few seconds. The
|
||||
progress must stop immediately, and the result must read `Cancelled after Ns`.
|
||||
On the broken build the progress kept arriving until the tool completed all 60
|
||||
seconds, because the Inspector was sending the wrong cancellation signal
|
||||
([#2140](https://github.com/modelcontextprotocol/inspector/issues/2140)).
|
||||
|
||||
The 2026-07-28 spec makes this
|
||||
[transport-specific](https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/cancellation#transport-specific-cancellation):
|
||||
for Streamable HTTP, **closing the request's SSE response stream is the
|
||||
cancellation signal**, and a `notifications/cancelled` is "neither required nor
|
||||
expected"; stdio, which has no per-request stream to close, keeps the
|
||||
notification. A spec-compliant server therefore answers the notification `202
|
||||
Accepted` and drops it — which is precisely what the reporter observed, with the
|
||||
task running on to completion while the Inspector reported it cancelled.
|
||||
|
||||
The SDK already implements that fork, off `transport.hasPerRequestStream`. Every
|
||||
Inspector connection is wrapped in `MessageTrackingTransport` (it feeds the
|
||||
Protocol and Network tabs), which did not forward the flag — so the SDK saw
|
||||
`undefined` and took the stdio branch on **every** client, CLI and TUI included.
|
||||
The web client needed two more links in the chain: its browser-side transport
|
||||
answers for the real upstream one that lives on the Node backend, and the abort
|
||||
has to survive the `POST /api/mcp/send` hop to reach it.
|
||||
|
||||
Watch it on the wire in the **Protocol** tab: cancelling now emits no
|
||||
`notifications/cancelled` frame at all, and the `tools/call` entry ends as an
|
||||
aborted request rather than a completed one. Switch the same server to
|
||||
**Legacy** and the notification comes back — that era has no per-stream
|
||||
mechanism, so it is still the correct signal there.
|
||||
|
||||
#### MCP Apps
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue