Repository navigation
Return 405 on GET when stateless_http=True #2474
Description
Activity
I'd like to pick this up. The fix is straightforward: in _handle_stateless_request (streamable_http_manager.py, line ~153), check the request method before creating a transport. If it's GET (or DELETE), return 405 with Allow: POST immediately instead of spinning up a dead SSE stream.
This keeps StreamableHTTPServerTransport unaware of stateless semantics and avoids touching the stateful path.
The closed PR #2262 had a working implementation. I'll follow the same direction but scope it to the manager layer.
Could this be labeled ready for work so I can proceed?We're hitting this too, and the cost impact is significant.
Setup: a stateless MCP server (
stateless_http=True,mcp1.26.0) on Google Cloud Run, request timeout 3600s, 1 vCPU.An authenticated client opens the standalone
GETSSE stream. In stateless mode this stream can never receive a server-initiated message (no session to push to), so it idles until Cloud Run force-closes it at the ~3600s request timeout — then the client immediately reconnects, looping around the clock.Because Cloud Run bills CPU for the full duration of an in-flight request, one continuously-connected client pins an instance ~24/7 ≈ 86,400 vCPU-seconds/day — about half of Cloud Run's entire monthly free tier (180,000 vCPU-s) burned by a single idle client in one day. Request count is negligible (a few hundred/day); the whole bill is the held GET stream.
Returning 405 on
GETin stateless mode (as proposed here and in #2509) fixes this cleanly — the stream carries no data in stateless mode, and per the Streamable HTTP spec a server MAY decline it with 405, after which clients fall back to POST-only. Would be great to see #2509 rebased and merged; happy to help test.I can reproduce this on current main. A GET with no (or a 2025-handshake)
MCP-Protocol-Versionheader routes throughStreamableHTTPSessionManager._handle_stateless_requestinsrc/mcp/server/streamable_http_manager.py, which calls the transport'shandle_request→_handle_get_requestinsrc/mcp/server/streamable_http.py. That method has no notion of statelessness (the stateless transport is just built withmcp_session_id=None,event_store=None), so it unconditionally opens the standalone SSE stream and idles. Worth noting the modern path already does the right thing:handle_modern_requestinsrc/mcp/server/_streamable_http_modern.pyreturns405withAllow: POSTfor any non-POST method, so the gap is really only in the legacy stateless GET path. A fix could either short-circuit GET in_handle_stateless_requestbefore dispatching, or thread a stateless flag into the transport so_handle_get_requestcan 405 when it can never offer a stream.I'd like to work on this. The fix is straightforward: in _handle_stateless_request()\ (\streamable_http_manager.py), check the HTTP method before creating the transport and return 405 with \Allow: POST\ for GET/DELETE. PR #2262 already has a working implementation that just needs to be reopened and scoped to GET per this issue. Let me know if that sounds right or if there's a preferred direction before I start.
- addedbugSomething isn't workingSomething isn't workingP2Moderate issues affecting some users, edge cases, potentially valuable featureModerate issues affecting some users, edge cases, potentially valuable feature
on Aug 14, 2026 A data point from a production deployment. We run a
stateless_http=Trueserver (mcp 2.3.0) on Google Cloud Run. Each held-open GET stream occupies one Cloud Run request slot until the platform's 300-second request timeout. At 3 instances × 80 concurrent requests, an MCP gateway's GET streams filled every slot, and Cloud Run returned 429 to all requests, includinginitialize.We worked around it by inserting a GET-only Starlette route ahead of the SDK's
/mcproute that returns 405 withAllow: POST. Claude Code handled the 405 without retrying. A built-in 405 in stateless mode would make the workaround unnecessary.- added a commit that references this issue
on Oct 9, 2026
Initial Checks
Description
When a Streamable HTTP server is configured with
stateless_http=True,GET /mcpis still accepted and opens an SSE stream that has no session context and can never receive server-initiated messages — a dead-end.The MCP Streamable HTTP spec explicitly permits returning
405 Method Not Allowedwhen the server does not offer an SSE stream at the endpoint(spec):
A stateless server cannot offer one — there is no session to push to — so GET should 405. The TypeScript SDK already behaves this way in its stateless example.
A prior PR (#2262) proposed essentially this change but was closed for lack of a corresponding issue per CONTRIBUTING.md.
Opening this issue to agree on scope so the fix can be re-submitted.
Example Code
A couple of clarifications for triage:
Current vs expected behavior
Today (v1.27.0):
Expected:
Concrete impact
The idle GET SSE stream holds a long-lived connection per client with no useful payload, exhausting concurrent connection limits on serverless platforms like Cloud Run. Background previously raised in #2232 / #1941.
Proposed resolution
PR #2262 already has a working implementation. Once this issue is labeled
ready for work, reopening it (narrowed to GET per this issue's scope) should be sufficient — no new PR needed.Python & MCP Python SDK