You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
StatelessHTTPServerTransport: notifications/cancelled leaves the original POST hanging (HTTP exchange never completed) #255
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
Start a server on StatelessHTTPServerTransport with a slow tool handler.
POST a tools/call request with id X; while it is in flight, POST a notifications/cancelled for id X (202 as expected).
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.
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.
@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.
Activity
ianegordon commentedon Jul 6, 2026
Related: #254 — concurrent requests sharing a JSON-RPC id orphan waiters via map overwrite.
sollahiro commentedon Jul 20, 2026
We're hitting what looks like exactly this issue in production. We embed 0.12.1's
StatelessHTTPServerTransportbehind a remote MCP server (Cloudflare Tunnel + Vapor + Postgres on the backend). We instrumented server-side access logging and found: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.
ianegordon commentedon Jul 20, 2026
@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.
Merge pr/260-on-264: complete cancelled requests' HTTP exchanges (ups…
fix: complete the HTTP exchange when a request is cancelled in Statel…
fix: complete the HTTP exchange when a request is cancelled in Statel…