Return 405 for listen-mode GET on stateless streamable HTTP transport

Closed
SA
23d ago
+38-0

Fixes #3492

  • StreamableHTTPServerTransport.__init__ gets a new stateless: bool = False parameter, stored as self._stateless.
  • I deliberately did NOT key the 405 off mcp_session_id is None, even
    though that's true for every stateless transport today. An existing test
    (test_standalone_stream_teardown_between_dequeues_is_not_an_error)
    constructs a transport directly with mcp_session_id=None specifically to
    exercise the standalone-GET-stream teardown path, so None session id is
    not a reliable stand-in for "stateless" at the transport level — it's
    already used as "no session tracking, but the standalone stream still
    works" in that test. An explicit flag avoids overloading that meaning,
    which is also what the issue's first commenter (arpankernel) suggested.
  • _handle_get_request now returns 405 immediately when self._stateless
    is set, before any Accept-header check or stream creation.
  • streamable_http_manager.py::_handle_stateless_request passes
    stateless=True when constructing the transport (the one and only place
    that builds a stateless transport).
  • Test: added stateless_app fixture (via a new stateless=False kwarg on
    the existing running_app helper) and
    test_get_sse_stream_returns_405_when_stateless, which asserts the GET
    returns 405 within a 5s anyio.fail_after (so if the fix ever regresses,
    the test fails fast instead of hanging CI).

Verification

Command: `./scripts/test`
Result: `5969 passed, 10 skipped, 1 xfailed` in 57.21s, followed by
`coverage report` → `TOTAL 52233 0 4072 0 100.00%`, and
`strict-no-cover` → `✅ No lines wrongly marked with 'pragma: no cover'`.

Also ran targeted:
- `uv run --frozen pytest tests/shared/test_streamable_http.py::test_get_sse_stream_returns_405_when_stateless -v` → PASSED
- `uv run --frozen pytest tests/shared/test_streamable_http.py tests/server/test_streamable_http_router.py tests/server/test_streamable_http_manager.py -q` → 130 passed
- `uv run --frozen ruff format --check`, `uv run --frozen ruff check`, `uv run --frozen pyright` on the three changed files → all clean

Before the fix, my standalone repro script (curl-equivalent in-process GET)
hung for the full 3s timeout with zero bytes; after the fix it returns
`405` with a JSON-RPC error body immediately.