Skip to content

fix: bound Streamable HTTP session resources - #56

Merged
Jeomon merged 1 commit into
CursorTouch:mainfrom
git-ksk:fix/streamable-http-resource-cleanup
Aug 16, 2026
Merged

fix: bound Streamable HTTP session resources#56
Jeomon merged 1 commit into
CursorTouch:mainfrom
git-ksk:fix/streamable-http-resource-cleanup

Conversation

@git-ksk

@git-ksk git-ksk commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Summary

Run MacOS-MCP's Streamable HTTP transport in FastMCP's supported stateless mode.

MacOS-MCP does not use per-client HTTP session state or server-initiated session features, so keeping SDK transports alive across requests only retains resources when clients reconnect without sending MCP DELETE.

SSE remains unchanged.

Root cause

The growth is specific to stateful Streamable HTTP session lifecycle, not tool execution, PyObjC workers, or raw request volume.

Controls on current main (fastmcp 2.14.4, mcp 1.26.0):

  • stdio, 500 tools/list calls in one process: +224 KB RSS
  • SSE, 500 fresh reconnects: +1.7 MB RSS
  • Streamable HTTP, 500 calls in one session: +816 KB RSS
  • Streamable HTTP, 500 graceful sessions with DELETE: +3.7 MB RSS
  • Streamable HTTP, 500 abandoned reconnects without DELETE: RSS reached 208.1 MB from 155.7 MB (about +52.4 MB from the initial baseline)

The MCP SDK's stateful StreamableHTTPSessionManager keeps transports in _server_instances; sessions that disappear without DELETE stay alive, and current termination paths also have known cleanup gaps. Upgrading only to mcp 1.27.2 did not resolve the abandoned-session growth because idle cleanup is opt-in and FastMCP 2.x does not expose that manager timeout through this run path.

Related upstream work:

This PR intentionally does not monkeypatch FastMCP/MCP internals.

Change

Pass:

stateless_http=transport == "streamable-http"

to the existing FastMCP HTTP run call.

This uses FastMCP's public API and leaves stdio/SSE behavior unchanged.

Verification

Regression tests

Added tests/test_server_transport.py to pin:

  • Streamable HTTP -> stateless_http=True
  • SSE -> stateless_http=False

Full suite:

319 passed

Code quality

Checked the PR branch against the same upstream/main baseline with the same Ruff version:

  • git diff --check — clean
  • ruff check .462 pre-existing findings on both upstream/main and this branch; finding-code counts are identical, so this PR adds no Ruff findings
  • ruff format --check . — the same 26 pre-existing files would be reformatted on both revisions; the new test file is already formatted
  • no added line exceeds the project's 100-character limit

FastMCP compatibility

fastmcp is intentionally not version-pinned by this project, so the change was also exercised against the current FastMCP 3.4.7 release in an isolated environment:

  • server started successfully with stateless_http=True
  • 3 independent Streamable HTTP reconnects completed
  • all 12 MacOS-MCP tools were listed on every reconnect
  • shutdown completed without an ASGI traceback

Bundled bridge compatibility

Both bundled stdio-to-HTTP bridges were exercised against the stateless Streamable HTTP server:

  • Python bridge/mcp-stdio-bridge.py: initialize -> tools/list (12 tools) -> real Shell call (bridge-ok)
  • Swift bridge/MCPStdioBridge.swift: initialize -> tools/list (12 tools) -> real Shell call (bridge-ok)
  • both bridges work when the server does not issue Mcp-Session-Id; after exit, server TCP state returned to listener-only

Functional E2E

On isolated test ports only (production port 8100 was not touched):

  • 5 independent Streamable HTTP reconnects
  • initialize + list all 12 tools each round
  • real Shell tool call each round
  • bearer auth accepted for the valid token
  • invalid bearer token rejected with HTTP 401

RSS stress E2E

Modified CLI, 10,000 fresh reconnects without DELETE:

baseline   146.8 MB
1,000      152.5 MB
2,000      154.8 MB
3,000      154.8 MB
4,000      154.8 MB
5,000      156.0 MB
6,000      156.0 MB
7,000      156.0 MB
8,000      156.0 MB
9,000      156.1 MB
10,000     156.1 MB

RSS plateaus after allocator warm-up; the final 4,000 reconnects add only about 64 KB. File descriptors and TCP connections return to the listener-only baseline.

SIGTERM

Before this change, a live stateful Streamable HTTP GET produced a forced Uvicorn cancellation plus an ASGI CancelledError traceback during SIGTERM. With stateless Streamable HTTP there is no persistent GET/SSE request; shutdown reaches application shutdown without the ASGI traceback.

FastMCP 2.14.4 still configures Uvicorn with timeout_graceful_shutdown=0, so Uvicorn emits its generic Cancel 0 running task(s), timeout graceful shutdown exceeded line. That is separate from the retained Streamable HTTP session bug and is not overridden here.

Compatibility note

Stateless Streamable HTTP cannot provide a persistent per-client back-channel for features that require session continuity. MacOS-MCP currently does not use sampling, elicitation, roots, or other server-initiated session features; its tool set and auth path were exercised successfully in stateless mode.

@git-ksk
git-ksk marked this pull request as draft August 15, 2026 08:17
@git-ksk
git-ksk force-pushed the fix/streamable-http-resource-cleanup branch from a29067a to d1103cc Compare August 15, 2026 08:25
@git-ksk
git-ksk marked this pull request as ready for review August 15, 2026 08:28
@Jeomon
Jeomon merged commit 14a7537 into CursorTouch:main Aug 16, 2026
@git-ksk
git-ksk deleted the fix/streamable-http-resource-cleanup branch August 27, 2026 06:17
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.

2 participants