fix(mcp): start FastMCP session manager via Quart serving lifecycle

After fixing the /mcp path forwarding in 1fd303a, requests now reach
FastMCP — but its StreamableHTTPSessionManager raises:

  RuntimeError: Task group is not initialized. Make sure to use run().

The session manager owns a task group that must be running before it
can handle requests. In a stand-alone Starlette app this happens via
the `lifespan` parameter (lifespan = session_manager.run). Hosted
inside Quart, my dispatch wrapper only forwards HTTP events, not
lifespan, so the manager never got its startup signal.

Fix: hook session_manager.run() (an async context manager) into
Quart's @app.before_serving and @app.after_serving so the task group
is alive across the serving window.

The CI integration test was hitting the same crash because it drives
app.asgi_app raw without going through Quart's serving lifecycle —
@before_serving never fires. Updated the test to manually enter
session_manager.run() around the request.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-27 12:59:29 -04:00
parent 1fd303abe3
commit 65d3711a11
2 changed files with 38 additions and 15 deletions
+19 -14
View File
@@ -88,7 +88,11 @@ async def test_mcp_endpoint_valid_token_passes_auth():
"""With a valid Bearer, the request must successfully reach FastMCP's
initialize handler. Asserting `!= 401` is too weak: it lets a 404 from
a path-mismatch (the original bug) through. FastMCP responds 200 to a
well-formed initialize handshake."""
well-formed initialize handshake.
FastMCP's session manager normally starts via Quart's @before_serving
hook in production. This raw-ASGI test doesn't go through Quart's
serving lifecycle, so we manually enter the session manager."""
fake_key = MagicMock()
fake_key.user_id = 7
fake_key.scope = "write"
@@ -101,19 +105,20 @@ async def test_mcp_endpoint_valid_token_passes_auth():
"clientInfo": {"name": "test", "version": "0"},
},
}).encode()
with patch(
"fabledassistant.mcp.auth.lookup_key",
AsyncMock(return_value=fake_key),
):
status, _ = await _send_request(
app, "POST", "/mcp",
headers={
"Authorization": "Bearer fmcp_valid",
"Content-Type": "application/json",
"Accept": "application/json, text/event-stream",
},
body=initialize_body,
)
async with app.mcp_instance.session_manager.run():
with patch(
"fabledassistant.mcp.auth.lookup_key",
AsyncMock(return_value=fake_key),
):
status, _ = await _send_request(
app, "POST", "/mcp",
headers={
"Authorization": "Bearer fmcp_valid",
"Content-Type": "application/json",
"Accept": "application/json, text/event-stream",
},
body=initialize_body,
)
assert status == 200, f"expected 200 from FastMCP initialize, got {status}"