Skip to content

fix(api): return 405 on GET /mcp to stop idle-killed SSE streams#138

Merged
mgoldsborough merged 2 commits intomainfrom
fix/mcp-get-405
Apr 29, 2026
Merged

fix(api): return 405 on GET /mcp to stop idle-killed SSE streams#138
mgoldsborough merged 2 commits intomainfrom
fix/mcp-get-405

Conversation

@mgoldsborough
Copy link
Copy Markdown
Contributor

Summary

  • GET /mcp is 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's idleTimeout (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 up locally, and an exhausted SDK reconnect budget (default maxRetries: 2) in the iframe.
  • Return 405 Method Not Allowed with Allow: POST, DELETE. The MCP SDK explicitly treats 405 as "server doesn't offer GET-style listening" and proceeds POST-only — see _startOrAuthSse in @modelcontextprotocol/sdk/client/streamableHttp.js. Zero long-lived connections, works behind any proxy.
  • If we ever start emitting standalone-stream notifications (sampling, elicitation, broadcast), switch this back to a real handler and add an SSE heartbeat (src/api/sse-heartbeat.ts has the pattern).

Test plan

  • bun run verify — 2181 unit + 213 web + 443 integration + 17 smoke, all green
  • New regression test: GET /mcp returns 405 with Allow: POST, DELETE
  • Existing client.connect() flow in mcp-server-endpoint.test.ts still passes (SDK gracefully degrades)
  • Local repro: with this branch, bun run dev no longer logs [vite] http proxy error: /mcp after the idle window

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.
@mgoldsborough mgoldsborough added the qa-reviewed QA review completed with no critical issues label Apr 29, 2026
@mgoldsborough mgoldsborough merged commit af01180 into main Apr 29, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

qa-reviewed QA review completed with no critical issues

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant