MCP inside a FastAPI app
A shop already running a FastAPI service does not need a second process. The MCP server can be mounted in the same app, next to its endpoints.
from contextlib import asynccontextmanager
from fastapi import FastAPI
from shop import mcp
mcp_app = mcp.streamable_http_app()
@asynccontextmanager
async def lifespan(app: FastAPI):
async with mcp.session_manager.run():
yield
app = FastAPI(lifespan=lifespan)
@app.get("/health")
def health():
return {"status": "ok"}
app.mount("/", mcp_app)mcp.streamable_http_app() returns the MCP server as a web application, and app.mount("/", mcp_app) puts it inside FastAPI, so /mcp is the MCP endpoint and /health is still yours. The mount goes last, because a mount at / matches every path and FastAPI checks routes in order.
The lifespan line is the one people forget. The MCP app starts its session manager in its own lifespan, and a mounted app's lifespan never runs. The host app has to start it, with async with mcp.session_manager.run(). Without it, the SDK docs show the first MCP request failing with Task group is not initialized.
uvicorn app:app --port 8202 --log-level warning &
sleep 3
curl -s localhost:8202/health
echo
python client.py
kill %1One process, one port: a normal FastAPI endpoint and an MCP server. The session manager's start-up line is the SDK's own log message.
transport_security setting.- Remove the
lifespan=lifespanargument, restart, and run the client. - Move
app.mountabove@app.get("/health")and call/health. - Add a FastAPI endpoint that calls
lookup_orderthroughClient(mcp).
You understood something today that you didn't yesterday.