Skip to content

feat: add --stateless-http flag for streamable HTTP - #217

Closed
eculver wants to merge 1 commit into
crystaldba:mainfrom
sfcompute:upstream-stateless-http
Closed

eculver wants to merge 1 commit into
crystaldba:mainfrom
sfcompute:upstream-stateless-http

Conversation

@eculver

@eculver eculver commented Aug 28, 2026

Copy link
Copy Markdown

Problem

In stateful streamable-HTTP mode (the default), the MCP SDK's StreamableHTTPSessionManager retains each session's server-side state until the client sends DELETE /mcp. Many MCP clients — hosted connectors, agent frameworks — create sessions freely and never send that DELETE, so a long-running postgres-mcp server's memory grows without bound.

Concretely, in one production deployment we measured ~720 never-terminated sessions/day at ~110KB retained each — about 77MiB/day of working-set growth (48h of logs showed zero DELETE /mcp requests and zero session terminations). Memory never plateaus and is only reclaimed by restarting the server; we currently run a daily restart as a workaround.

Change

Adds a --stateless-http flag that opts the streamable-http transport into FastMCP's stateless mode (stateless_http=True): the SDK creates a fresh ephemeral transport per request and keeps no session registry, so there is nothing to accumulate. The cost is server-initiated messages and session resumability, which postgres-mcp doesn't use. Default behavior (flag omitted) is unchanged.

Testing

Four new tests in tests/unit/test_transport.py:

  • Flag plumbing (same patch-main() pattern as the existing transport tests): --stateless-http sets mcp.settings.stateless_http = True; omitting it leaves the stateful default.
  • Session contract: a stateless server serves a tools/list POST that carries no session ID and no prior initialize (HTTP 200), while a stateful server rejects the identical request (HTTP 400, "Missing session ID"). These pin the SDK behavior the flag relies on, so a future SDK bump that changes stateless semantics fails in CI instead of silently reintroducing unbounded session growth. They use fresh FastMCP instances (a FastMCP builds its session manager once, and a manager can only run once) and a 127.0.0.1 base URL (the SDK's DNS-rebinding protection rejects the test client's default testserver Host with 421).

uv run pytest tests/unit passes, ruff check / ruff format --check clean. Also smoke-tested live: with the flag, a sessionless tools/list returns the full tool list and no per-session transports are logged, while the same request against a stateful server is rejected with "Missing session ID".

🤖 Generated with Claude Code

In stateful streamable-HTTP mode, StreamableHTTPSessionManager retains
each session's server-side state until the client sends DELETE /mcp.
Many MCP clients (connectors, agents) create sessions freely and never
send it, so a long-running server's memory grows without bound. In one
production deployment we measured ~720 never-terminated sessions/day
retaining ~110KB each — ~77MiB/day of working-set growth, reclaimed
only by restarting the server.

--stateless-http opts the streamable-http transport into FastMCP's
stateless mode (a fresh ephemeral transport per request, no session
registry). It costs server-initiated messages and session resumability,
which postgres-mcp does not use. Default behavior is unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@eculver eculver closed this Sep 15, 2026
@eculver
eculver deleted the upstream-stateless-http branch September 15, 2026 23:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant