fix(streamable-http): return spec-mandated 405 for pre-session GET (Closes #3102)
Summary
Fixes #3102. A GET that the Streamable HTTP server will not serve as a standalone SSE stream must be answered with 405 Method Not Allowed, per the transport spec:
The server MUST either return Content-Type: text/event-stream in response to this HTTP GET, or else return HTTP 405 Method Not Allowed, indicating that the server does not offer an SSE stream at this endpoint.
— Streamable HTTP § Listening for Messages from the Server
In stateful mode a session-less GET previously fell through to session validation and returned 400 "Missing session ID". Spec-compliant clients (e.g. the TypeScript SDK's _startOrAuthSse) probe with a pre-initialize GET to ask "do you offer a standalone SSE stream?" and treat only 405 as the graceful "no SSE, fall through to POST" signal — so the 400 aborted the handshake before initialize was ever sent.
Change
In _handle_get_request, before session validation: if the server is stateful (self.mcp_session_id set) and the request carries no session id, return 405 with Allow: POST. This is the only pre-session request shape that could not previously get 405.
- Scoped and minimal: only the session-less GET path changes. Legitimate clients only GET after
initialize(with a session id) and are unaffected. - The wildcard-
Accepthalf of the issue (*/*→ 406) is already resolved onmain—check_accept_headersnow honors*/*andtext/*— so this PR is only the 405 contract. - I kept the existing 406 arm for clients that genuinely do not accept
text/event-stream, since that is standard HTTP content-negotiation and distinct from the spec's 405 signal. Happy to fold that in too if you'd prefer.
Tests
- New
test_get_without_session_returns_405asserting 405 +Allow: POSTfor a pre-session GET that accepts SSE. - Updated
test_streamable_http_security_get_request: its post-Host-check assertion exercised the old 400 path; the Host-ordering intent (evil host → 421 before any session handling) is preserved. - Full streamable-HTTP suite (156 tests) passes;
ruff+pyrightclean.
AI-assistance disclosure
Per CONTRIBUTING: this change was drafted with AI assistance. The human contributor (@itguruhaseeb) reviewed the diff, understands it, and owns the change and any follow-up. Open to adjusting the response shape (status text, Allow value, or extending to the 406 arm) to match your intended contract.