diff --git a/mcp/app/main.py b/mcp/app/main.py index ca40953..7beafa2 100644 --- a/mcp/app/main.py +++ b/mcp/app/main.py @@ -1,7 +1,8 @@ from contextlib import asynccontextmanager -from fastapi import FastAPI, Request +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 @@ -28,15 +29,20 @@ async def lifespan(app: FastAPI): await backend.stop() -app = FastAPI(title="Homelable MCP", lifespan=lifespan) +# 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.api_route("/mcp", methods=["GET", "POST", "DELETE"]) -async def mcp_endpoint(request: Request): - await session_manager.handle_request(request.scope, request.receive, request._send) - - @app.get("/health") async def health(): return {"status": "ok"}