fix(streamable-http): return spec-mandated 405 for pre-session GET (Closes #3102)

Closed
IT
2mo ago
+44-3

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-Accept half of the issue (*/* → 406) is already resolved on main — check_accept_headers now honors */* and text/* — 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_405 asserting 405 + Allow: POST for 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 + pyright clean.

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.