fix(api): return 405 on GET /mcp to stop idle-killed SSE streams#138
Merged
mgoldsborough merged 2 commits intomainfrom Apr 29, 2026
Merged
fix(api): return 405 on GET /mcp to stop idle-killed SSE streams#138mgoldsborough merged 2 commits intomainfrom
mgoldsborough merged 2 commits intomainfrom
Conversation
GET /mcp is the spec's *optional* server→client SSE channel for standalone notifications (broadcast, sampling, elicitation). We don't push anything down it — tool responses and task progress ride the POST that started them, and our own server→client signaling flows through /v1/events. Holding the connection open with nothing to write means Bun's idleTimeout (max 255s), Vite's dev proxy, and any L7 proxy in front of the API (ALB at 60s, nginx) silently kill the socket. The client SDK then exhausts its 2-retry reconnect budget and surfaces "socket hang up" / transport errors. Returning 405 is the spec-blessed escape hatch: the SDK treats it as "server doesn't offer GET-style listening" and proceeds POST-only (see _startOrAuthSse in client/streamableHttp.js).
The endpoint table listed POST/GET/DELETE for /mcp; GET now returns 405 by design. Add a one-line CHANGELOG entry under Unreleased.Fixed covering the user-visible symptom.
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.
Summary
GET /mcpis the MCP Streamable HTTP spec's optional server→client SSE channel. We don't push anything down it — tool responses and task progress ride the POST that started them, and our own server→client signaling for the iframe app (data.changed, conversation events, heartbeats) flows through/v1/events. Holding the connection open with nothing to write meant Bun'sidleTimeout(max 255s), Vite's dev proxy, and any L7 proxy in front of the API (ALB's 60s default, nginx) would silently kill the socket — surfacing as[vite] http proxy error: /mcp+socket hang uplocally, and an exhausted SDK reconnect budget (defaultmaxRetries: 2) in the iframe.405 Method Not AllowedwithAllow: POST, DELETE. The MCP SDK explicitly treats 405 as "server doesn't offer GET-style listening" and proceeds POST-only — see_startOrAuthSsein@modelcontextprotocol/sdk/client/streamableHttp.js. Zero long-lived connections, works behind any proxy.src/api/sse-heartbeat.tshas the pattern).Test plan
bun run verify— 2181 unit + 213 web + 443 integration + 17 smoke, all greenGET /mcpreturns 405 withAllow: POST, DELETEclient.connect()flow inmcp-server-endpoint.test.tsstill passes (SDK gracefully degrades)bun run devno longer logs[vite] http proxy error: /mcpafter the idle window