Skip to content

Streamable HTTP stateless servers hang clients on the listen-mode GET (_handle_get_request opens a hold-open SSE stream) #3492

Description

@jzhuge

Environment

mcp Python SDK 1.29.1 / 1.30.0, FastMCP with stateless_http=True and streamable_http_path="/", mounted in a Starlette app.

Symptom

A client that opens the listen-mode GET (Accept: text/event-stream) receives HTTP 200 with Content-Type: text/event-stream but the response never writes a single byte; the connection holds open indefinitely. MCP clients (e.g. Pi, Claude Code) that open this GET at connect time time out with messages like "Timed out connecting to SSE MCP server".

Root cause

StreamableHTTPServerTransport._handle_get_request (mcp/server/streamable_http.py) unconditionally creates an SSE stream and waits for server-initiated messages. For a stateless server there is no session and the server never pushes, so the stream stays open with zero bytes. The handler and its dispatch branch are marked # pragma: no cover.

Spec

MCP Streamable HTTP (2025-03-26) requires a server that does not support server-initiated messages to answer the listen-mode GET with 405 Method Not Allowed. There is no transport configuration that produces that 405 today; current workarounds are app-layer interception (return 405 before the stream handler) or monkey-patching the private handler.

Related

#1764 is a different stateless-mode hang (SSE-writer zero-buffer deadlock). This is the GET-listen path.

Suggested fix

At the top of _handle_get_request (or in handle_request), return 405 when the transport is stateless — or more generally whenever the server publishes no server-initiated messages — before any stream creation.

Repro

Minimal FastMCP: FastMCP("x", stateless_http=True, streamable_http_path="/"), serve, then curl -i -H 'Accept: text/event-stream' http://localhost:PORT/mcp/ — observe 200 + zero bytes, held open (hang), instead of 405.

Activity

  1. added
    v2Affects the v2 line (2.x on main)
    v1Affects the v1.x maintenance line
    on Sep 11, 2026
  2. arpankernel commented on Sep 11, 2026

    @arpankernel

    Confirmed on main. A stateless_http=True server routes the listen-mode GET through _handle_get_request, which builds the SSE stream unconditionally and then waits on a channel that never produces anything, so the response flushes 200 text/event-stream and holds the connection open with zero bytes. curl -i -H 'Accept: text/event-stream' http://localhost:PORT/mcp/ reproduces it: headers come back, then it just hangs.

    This mirrors the DELETE path, which already returns 405 when there's no session. Cleanest fix looks like a stateless flag on the transport (the manager builds the stateless transport in one spot) plus an early 405 in _handle_get_request before any stream creation, matching the 2025-03-26 rule that a server not supporting server-initiated messages answers the listen GET with 405. Happy to send a PR if that's useful.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingv1Affects the v1.x maintenance linev2Affects the v2 line (2.x on main)

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions