d0a49d0a0d
StreamableHTTPSessionManager is an ASGI app — it sends its own
http.response.start and http.response.body messages via the scope/receive/send
triple. Wrapping it inside a @app.api_route FastAPI handler causes FastAPI
to try to finalize the response after the handler returns, emitting a
second http.response.start. uvicorn rejects this with:
RuntimeError: Unexpected ASGI message 'http.response.start' sent,
after response already completed.
Every POST /mcp raises, making the server unreachable from any MCP client
(tested with Claude Code 2.x against mcp==1.27.0).
Fix: mount the session manager as a Starlette Mount so it owns the
response cycle directly. Auth middleware still applies because
add_middleware attaches at the app level, wrapping all mounted sub-apps.
49 lines
1.4 KiB
Python
49 lines
1.4 KiB
Python
from contextlib import asynccontextmanager
|
|
from fastapi import FastAPI
|
|
from mcp.server import Server
|
|
from mcp.server.streamable_http_manager import StreamableHTTPSessionManager
|
|
from starlette.routing import Mount
|
|
|
|
from .auth import ApiKeyMiddleware
|
|
from .backend_client import backend
|
|
from .resources import register_resources
|
|
from .tools import register_tools
|
|
|
|
|
|
mcp_server = Server("homelable")
|
|
register_resources(mcp_server)
|
|
register_tools(mcp_server)
|
|
|
|
session_manager = StreamableHTTPSessionManager(
|
|
app=mcp_server,
|
|
json_response=False,
|
|
stateless=True,
|
|
)
|
|
|
|
|
|
@asynccontextmanager
|
|
async def lifespan(app: FastAPI):
|
|
await backend.start()
|
|
async with session_manager.run():
|
|
yield
|
|
await backend.stop()
|
|
|
|
|
|
# Mount the session manager as an ASGI sub-app instead of wrapping it in a
|
|
# FastAPI @app.api_route handler. Wrapping it in a route handler causes
|
|
# FastAPI to send http.response.start after the session manager has already
|
|
# started the response, raising `RuntimeError: Unexpected ASGI message
|
|
# 'http.response.start' sent, after response already completed` on every
|
|
# POST /mcp — which makes the server unreachable from any MCP client.
|
|
app = FastAPI(
|
|
title="Homelable MCP",
|
|
lifespan=lifespan,
|
|
routes=[Mount("/mcp", app=session_manager.handle_request)],
|
|
)
|
|
app.add_middleware(ApiKeyMiddleware)
|
|
|
|
|
|
@app.get("/health")
|
|
async def health():
|
|
return {"status": "ok"}
|