Skip to content

StatelessHTTPServerTransport: notifications/cancelled leaves the original POST hanging (HTTP exchange never completed) #255

Description

@ianegordon

Summary

In StatelessHTTPServerTransport (0.12.1), a request that is cancelled via notifications/cancelled leaves its original HTTP POST hanging indefinitely.

The cancellation flow correctly suppresses the JSON-RPC response (per base-protocol cancellation semantics, the server must not reply to a cancelled request). But the transport's HTTP waiter for that id is only ever resumed by two paths:

  • a matching JSON-RPC response arriving through send(_:) (responseWaiters.removeValue(forKey: id) + resume), or
  • full transport terminate() (responseWaiters.removeAll() after failing them).

Since a cancelled request produces no response and nothing else resumes the waiter, the withCheckedThrowingContinuation in handleJSONRPCRequest (…/StatelessHTTPServerTransport.swift:230-233) never returns, and the original POST's HTTP exchange never completes.

Spec conflict

MCP 2025-11-25, Streamable HTTP, Sending Messages to the Server #5:

"If the input is a JSON-RPC request, the server MUST either return Content-Type: text/event-stream, to initiate an SSE stream, or Content-Type: application/json, to return one JSON object."

And cancellation is the spec's own recommended flow (same section, #6):

"To cancel, the client SHOULD explicitly send an MCP CancelledNotification."

So a spec-conformant client that cancels an in-flight request drives the server into violating the MUST above — the POST receives neither an SSE stream nor a JSON object, ever.

Reproduction sketch

  1. Start a server on StatelessHTTPServerTransport with a slow tool handler.
  2. POST a tools/call request with id X; while it is in flight, POST a notifications/cancelled for id X (202 as expected).
  3. Observe: the first POST never completes.

Suggested direction

  • When processing notifications/cancelled, resume the matching responseWaiters entry so the HTTP exchange completes (e.g. with a JSON-RPC error object such as "request cancelled", keeping the HTTP layer satisfied while the JSON-RPC layer stays silent-by-design is admittedly awkward — an error response for the cancelled id seems the pragmatic choice given the transport MUST above).
  • Independently, a waiter deadline would bound any orphaned exchange.

Related: filed separately, concurrent requests sharing a JSON-RPC id also orphan waiters via map overwrite.

Found during an MCP 2025-11-25 conformance audit of an app embedding this SDK. Happy to provide more detail.

Activity

  1. ianegordon commented on Jul 6, 2026

    @ianegordon
    Author

    Related: #254 — concurrent requests sharing a JSON-RPC id orphan waiters via map overwrite.

  2. sollahiro commented on Jul 20, 2026

    @sollahiro

    We're hitting what looks like exactly this issue in production. We embed 0.12.1's StatelessHTTPServerTransport behind a remote MCP server (Cloudflare Tunnel + Vapor + Postgres on the backend). We instrumented server-side access logging and found:

    • Server-side request handling is consistently fast (1-160ms, including DB round trips).
    • Client-observed round trips for the same tool calls range from 8s to 79s.
    • On at least one occurrence, the client eventually received a successful response after ~78.8s, but there is no corresponding server access-log entry for that request at all — consistent with the original POST hanging indefinitely rather than ever completing.
    • We ruled out Cloudflare Tunnel/Access overhead as the cause: a direct REST endpoint (non-MCP) through the identical tunnel/auth path returns in 0.5-2.3s for equivalent payloads, while MCP tool calls over the same path are 10-100x slower.

    This matches your description precisely — a client that times out and cancels (or gives up and retries) leaves the original POST hung server-side, since nothing resumes the waiter short of full transport teardown. A waiter deadline would bound this significantly for us. Wanted to add a corroborating production data point (Swift server, remote/hosted MCP endpoint, not local stdio) — happy to share more logs if useful.

  3. ianegordon commented on Jul 20, 2026

    @ianegordon
    Author

    @sollahiro if possible, would you take a look at PR #260 and give it a +1 if it helps address your issue. Please let me know if it does not. I want to get this issue resolved before I dive into #254.

  4. added a commit that references this issue on Sep 10, 2026
    ae41da5
  5. added 2 commits that reference this issue on Oct 22, 2026
    0ca6c96
    f853eed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions