fix(transport): avoid stall on servers with idle standalone SSE streams#234
Merged
steipete merged 3 commits intoJul 21, 2026
Merged
Conversation
Streamable HTTP servers may hold the standalone GET SSE stream open indefinitely without emitting any bytes. That is a spec-legal idle server-to-client notification channel, but under undici the open response occupies the connection for its origin and every subsequent same-origin request queues behind it forever, so tools/list and all tool calls hang until they time out. mcp.paddle.com behaves this way: initialize and notifications/initialized succeed, the GET stream returns 200 and then sends nothing, and the next POST never completes. `mcporter list paddle-*` timed out at any timeout value while curl against the same endpoint returned in ~0.2s. Reproduced with plain fetch and no SDK involved. The block is origin-scoped (requests to other origins from the same process are unaffected) and occurs whether or not the SSE body is read. Raising `connections` or enabling `allowH2` on a shared dispatcher does not help; only a separate connection pool does. nodeHttp1Fetch already sidesteps the shared pool, and api.sunsama.com was already special-cased for what looks like the same failure. Generalize that bare hostname check into a documented constant set and add mcp.paddle.com, so both work without configuration. Users can still override per server with `httpFetch`.
Co-authored-by: Umut Keltek <umut.keltek@gmail.com>
Co-authored-by: Umut Keltek <umut.keltek@gmail.com>
Collaborator
|
Landed as Proof on exact PR head
The merged implementation isolates only Node's standalone SSE receive channel, preserves ordinary/default fetch traffic and explicit overrides, and keeps Bun on its native fetch stack. @umutkeltek's authorship is preserved via the squash commit co-author trailer. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Streamable HTTP servers may hold the standalone
GETSSE stream open indefinitely without emitting bytes. That is a spec-legal server-to-client notification channel, but affected Node fetch pools can let the idle response occupy the origin pool and queue later same-origin requests forever. In practice,tools/listand tool calls time out even though the server is healthy.@umutkeltek traced this against Paddle at the wire level: initialization and the idle SSE
GETcompleted, but the following POST never reached the server. The same behavior reproduced without the MCP SDK, while separate HTTP connections continued to work.Fix
GETrequests through mcporter's separate Node HTTP/1.1 connection path.httpFetch: "default"andhttpFetch: "node-http1"behavior, plus the existing all-request Sunsama compatibility path.This fixes the server class generically instead of adding a Paddle hostname exception.
Verification
GET; this branch sendstools/listwhile that stream remains open and exits cleanly.pnpm exec vitest run tests/node-http-fetch.test.ts tests/runtime-transport.test.ts tests/cli-idle-sse.integration.test.ts tests/generate-cli.test.ts— 44 passed.pnpm check— passed.pnpm test— 837 passed, 3 skipped.pnpm docs:site— passed.c5e17d3936a5a3e1549c4c503cc038bd1899dbfe.Contributor credit is preserved in the original commit and maintainer follow-ups.