Repository navigation
Cancel button sends notifications/cancelled for Streamable HTTP instead of closing the SSE stream #2140
Description
Activity
Triage: Priority High (total 10)
- Severity 4 — a core workflow is unusable on the most common transport, and the Inspector reports something false about the server under test: Cancel appears to succeed (202 Accepted) while the request keeps running
- Urgency 5 — affects users on the published
2.3.0release right now - Bonuses: +1
buglabel
Board: #28, Status
Incoming(awaiting maintainer review — no milestone yet). Applied thev2version label at triage.I traced this on current
main(edf54f5) with the locked@modelcontextprotocol/client@2.0.0. The SDK already implements the transport-specific behavior described in the issue: on a modern connection it creates a per-requestAbortControllerwhentransport.hasPerRequestStream === true; aborting the caller signal then abortsTransportSendOptions.requestSignalinstead of sendingnotifications/cancelled. The SDKStreamableHTTPClientTransportadvertises that flag and applies the signal to its POST/SSE fetch.The behavior is lost at Inspector's Web proxy boundary:
core/mcp/remote/remoteClientTransport.tsdoes not advertisehasPerRequestStream, so the browser-side SDK takes its stdio-style notification fallback.RemoteClientTransport.requestSend()does not applyoptions.requestSignalto the browser'sPOST /api/mcp/sendfetch.- The
/api/mcp/sendhandler incore/mcp/remote/node/server.tsdoes not pass the incoming request's abort signal tosession.transport.send(...), so the backend's real Streamable HTTP transport cannot close the upstream per-request SSE response.
A narrow implementation path appears to be:
- advertise
hasPerRequestStreamonly when the proxied server config isstreamable-http; - apply
options.requestSignalto the browser-to-backend send fetch; - propagate the backend request-abort signal into the upstream
transport.sendcall; - leave stdio/SSE shared-channel configurations without the flag, preserving
notifications/cancelledthere.
Suggested regression coverage:
- a Streamable HTTP
RemoteClientTransportadvertises the flag and aborting a call aborts/api/mcp/sendwithout posting a cancellation notification; - the backend forwards that abort to the upstream per-request transport and releases both response waits;
- stdio continues to emit
notifications/cancelled; - the existing user-facing
ToolCallCancelledErrorbehavior remains unchanged.
I ran the existing focused integration test as a control:
npm run test:integration -- src/test/integration/mcp/inspectorClient.test.ts -t cancelToolCallResult: 1 file passed, 2 tests passed, 154 skipped. That direct Node Streamable HTTP path already behaves correctly, which is consistent with the proxy-boundary diagnosis above. The Inspector working tree remained clean.
Exact AI-assisted investigation prompt:
Trace Inspector issue #2140 from the Cancel button through InspectorClient, the browser RemoteClientTransport,
/api/mcp/send, and the backend Streamable HTTP transport. Compare the path with the locked client SDK'shasPerRequestStreamandrequestSignalbehavior. Preserve stdio cancellation, identify only the missing proxy propagation points, run the existing focused cancellation integration test, and do not publish, push, or open a PR.AI assistance disclosure: AI was used to discover this opportunity and draft the change or text. I reviewed the reasoning, diff, and verification results before submission.
- linked a pull request that will close this issuefix: cancel a Streamable HTTP call by closing its stream (#2140) #2185
on Aug 28, 2026 - added a commit that references this issue
on Aug 28, 2026 - added a commit that references this issue
on Sep 2, 2026 - added a commit that references this issue
on Oct 8, 2026
Which version line?
v2 — current (
@modelcontextprotocol/inspector@latest)Which client?
Web
Inspector version
2.3.0
Node version
v22
Operating system (and browser, for the web client)
No response
Transport
Streamable HTTP
MCP server under inspection
The Cancel button sends a
notifications/cancellednotification for all transports. Per the 2026-07-28 spec, for Streamable HTTP the cancellation signal is closing the SSE response stream —notifications/cancelledis the stdio-only mechanism. As a result, clicking Cancel against a Streamable HTTP server does not actually cancel the in-flight task on spec-compliant servers.Spec reference
From Cancellation → Transport-Specific Cancellation:
The repo's own
specification/v2_new_spec_impact.mdalready acknowledges this:But
specification/v2_ux_features.mdstill says:…which is correct for stdio but wrong for Streamable HTTP.
Steps to reproduce
slow_tasktool that sleeps with progress notifications for ~40s). Server: MCP Python SDK 2.1.1.POST /mcp HTTP/1.1" 202 Acceptedfor thenotifications/cancellednotification, but the task keeps running and progress notifications continue until the tool completes.Expected behavior
For Streamable HTTP, clicking Cancel should close/abort the SSE response stream for the in-flight request (the spec'd cancellation signal), not POST a
notifications/cancellednotification. The server then treats the disconnect as cancellation per spec.For stdio, the current behavior (send
notifications/cancelled) is correct and should be preserved.Actual behavior
Cancel sends
notifications/cancelledregardless of transport. On Streamable HTTP, spec-compliant servers acknowledge with 202 and drop the notification (the SDK 2.1.1 behavior); the task is not cancelled. True cancellation only happens if the user manually disconnects the connection.Logs, errors, or screenshots
Tested against a custom MCP server (Python, MCP SDK 2.1.1, Streamable HTTP) deployed on Kubernetes:
Inspector Cancel (broken):
SSE-stream-close (correct, via a custom client using anyio.move_on_after):
Already prototyped a fix?
The Cancel button's behavior should be transport-aware:
notifications/cancelled(current behavior, correct).AbortControlleron the fetch backing the SSE stream), per spec.The
v2_ux_features.mddesign doc should also be updated to reflect the transport-specific behavior.Before you submit