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

Open
#255 3 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Assessment

Difficulty
3/5
Estimated time
1-2 days
Newbie friendliness
64/100
Issue type
Bug
Clarity
Mostly clear
Activity status
Quiet
Tech stack
swift

Research direction

Start in StatelessHTTPServerTransport.swift at handleJSONRPCRequest around lines 230-233, then trace how responseWaiters is resumed in send(_:) and cleared by terminate(). Follow the notifications/cancelled path and determine how the matching waiter should be completed without violating cancellation semantics. Done means the original cancelled POST no longer hangs indefinitely, with the related waiter behavior covered by transport tests.

Written by the indexing model from the issue text.

Description

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.

Dominant language
Swift
Stars
1.5k
Forks
243
PR merge metrics
No merged PRs in 30d

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from modelcontextprotocol/swift-sdk

All issues in modelcontextprotocol/swift-sdk

Similar issues

More Swift issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.