Skip to content

Cancel button sends notifications/cancelled for Streamable HTTP instead of closing the SSE stream #2140

Description

@webus

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/cancelled notification for all transports. Per the 2026-07-28 spec, for Streamable HTTP the cancellation signal is closing the SSE response stream — notifications/cancelled is 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:

Streamable HTTP: Closing the SSE response stream is the cancellation signal. The server MUST treat a client disconnect as cancellation of that request. No notifications/cancelled message is required or expected.

stdio: There is no per-request stream to close. The client MUST send a notifications/cancelled notification referencing the request ID.

The repo's own specification/v2_new_spec_impact.md already acknowledges this:

Closing a request's SSE stream is cancellation (notifications/cancelled is stdio-only now). ... Cancellation switches to stream-abort on modern; calling code unchanged.

But specification/v2_ux_features.md still says:

Cancel button sends notifications/cancelled

…which is correct for stdio but wrong for Streamable HTTP.

Steps to reproduce

  1. Start an MCP server on Streamable HTTP that exposes a long-running tool (e.g. a slow_task tool that sleeps with progress notifications for ~40s). Server: MCP Python SDK 2.1.1.
  2. Connect Inspector (Streamable HTTP, Modern protocol era).
  3. Call the long-running tool.
  4. Click Cancel while the tool is running.
  5. Observe: the server logs show POST /mcp HTTP/1.1" 202 Accepted for the notifications/cancelled notification, 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/cancelled notification. 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/cancelled regardless 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):

# Server log on Cancel click:
10.180.1.39:0 - "POST /mcp HTTP/1.1" 202 Accepted
# Task keeps running, progress notifications continue to completion

SSE-stream-close (correct, via a custom client using anyio.move_on_after):

# Client closes the SSE stream after 3s (20-step task, 2s/step = 40s total)
# Client output:
[1/20] Completed step 1 of 20
Cancelled after 3s
# Server: task stops immediately, no further progress, no errors

Already prototyped a fix?

The Cancel button's behavior should be transport-aware:

  • stdio: send notifications/cancelled (current behavior, correct).
  • Streamable HTTP: abort/close the SSE response stream for the in-flight request (e.g. via an AbortController on the fetch backing the SSE stream), per spec.

The v2_ux_features.md design doc should also be updated to reflect the transport-specific behavior.

Before you submit

  • I searched existing issues and this is not a duplicate.
  • This is not a security vulnerability report (those go through the private advisory process).

Activity

  1. cliffhall commented on Aug 27, 2026

    @cliffhall
    Member

    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.0 release right now
    • Bonuses: +1 bug label

    Board: #28, Status Incoming (awaiting maintainer review — no milestone yet). Applied the v2 version label at triage.

  2. ump45nose commented on Aug 27, 2026

    @ump45nose

    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-request AbortController when transport.hasPerRequestStream === true; aborting the caller signal then aborts TransportSendOptions.requestSignal instead of sending notifications/cancelled. The SDK StreamableHTTPClientTransport advertises that flag and applies the signal to its POST/SSE fetch.

    The behavior is lost at Inspector's Web proxy boundary:

    1. core/mcp/remote/remoteClientTransport.ts does not advertise hasPerRequestStream, so the browser-side SDK takes its stdio-style notification fallback.
    2. RemoteClientTransport.requestSend() does not apply options.requestSignal to the browser's POST /api/mcp/send fetch.
    3. The /api/mcp/send handler in core/mcp/remote/node/server.ts does not pass the incoming request's abort signal to session.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 hasPerRequestStream only when the proxied server config is streamable-http;
    • apply options.requestSignal to the browser-to-backend send fetch;
    • propagate the backend request-abort signal into the upstream transport.send call;
    • leave stdio/SSE shared-channel configurations without the flag, preserving notifications/cancelled there.

    Suggested regression coverage:

    • a Streamable HTTP RemoteClientTransport advertises the flag and aborting a call aborts /api/mcp/send without 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 ToolCallCancelledError behavior remains unchanged.

    I ran the existing focused integration test as a control:

    npm run test:integration -- src/test/integration/mcp/inspectorClient.test.ts -t cancelToolCall

    Result: 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's hasPerRequestStream and requestSignal behavior. 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.

  3. self-assigned this
    on Aug 28, 2026
  4. added this to the v2.5.0 milestone on Aug 28, 2026
  5. added a commit that references this issue on Aug 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingv2Issues and PRs for v2

Type

No type

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions