Skip to content

Commit 738283f

Browse files
billy-pkclaude
andcommitted
fix: follow FastMCP + FastAPI pattern from python-sdk PR #1712
**Problem**: FastMCP doesn't have http_app() method, only streamable_http_app() **Solution**: Follow official pattern from MCP Python SDK PR #1712: modelcontextprotocol/python-sdk#1712 Key insights: 1. Use mcp.streamable_http_app() to get Starlette app 2. Create custom lifespan with mcp.session_manager.run() 3. FastAPI doesn't auto-trigger sub-app lifespans, must manage manually 4. Set streamable_http_path="/" in FastMCP constructor Pattern: ```python @contextlib.asynccontextmanager async def lifespan(app: FastAPI): async with mcp.session_manager.run(): yield app = FastAPI(lifespan=lifespan) mcp_app = mcp.streamable_http_app() app.mount("/mcp", mcp_app) ``` **Changes**: - tools/server.py: Add streamable_http_path="/" to FastMCP - main.py: Use streamable_http_app() + custom lifespan - main.py: Standard app.mount("/mcp", mcp_app) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent b7b3a9c commit 738283f

File tree

2 files changed

+25
-27
lines changed

2 files changed

+25
-27
lines changed

backend/main.py

Lines changed: 20 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -18,29 +18,28 @@
1818
)
1919

2020
# Get MCP app if mounting (must happen before creating FastAPI app)
21+
# Following pattern from: https://github.com/modelcontextprotocol/python-sdk/pull/1712
22+
import contextlib
23+
from typing import AsyncIterator
24+
2125
mcp_app = None
2226
mcp_lifespan = None
27+
2328
if settings.MOUNT_MCP_SERVER:
2429
try:
25-
from tools.server import get_mcp_app
30+
from tools.server import get_mcp_app, mcp as mcp_server
31+
2632
mcp_app = get_mcp_app()
27-
# Extract lifespan from the ASGI app
28-
if hasattr(mcp_app, 'lifespan'):
29-
mcp_lifespan = mcp_app.lifespan
30-
logging.getLogger(__name__).info("✅ MCP app created with lifespan")
31-
else:
32-
logging.getLogger(__name__).warning("⚠️ MCP app has no lifespan attribute, creating custom lifespan")
33-
# Create custom lifespan that runs MCP session manager
34-
import contextlib
35-
from tools.server import mcp as mcp_server
36-
37-
@contextlib.asynccontextmanager
38-
async def mcp_lifespan(app):
39-
async with mcp_server.session_manager.run():
40-
yield
41-
42-
mcp_lifespan = mcp_lifespan
43-
logging.getLogger(__name__).info("✅ Created custom MCP lifespan")
33+
34+
# Create custom lifespan that runs MCP session manager
35+
# FastAPI doesn't automatically trigger lifespan of mounted sub-apps
36+
@contextlib.asynccontextmanager
37+
async def mcp_lifespan(app: FastAPI) -> AsyncIterator[None]:
38+
"""FastAPI lifespan that initializes the MCP session manager."""
39+
async with mcp_server.session_manager.run():
40+
yield
41+
42+
logging.getLogger(__name__).info("✅ MCP app and lifespan created")
4443
except Exception as e:
4544
logging.getLogger(__name__).error(f"❌ Failed to create MCP app: {e}")
4645
import traceback
@@ -121,11 +120,9 @@ async def health_check_api():
121120
# Mount MCP server if MOUNT_MCP_SERVER is enabled (unified deployment mode)
122121
if settings.MOUNT_MCP_SERVER and mcp_app is not None:
123122
try:
124-
# Note: mcp.http_app(path="/mcp") already includes the /mcp path
125-
# So we mount it at root and it will be accessible at /mcp
126-
from starlette.routing import Mount
127-
app.routes.append(Mount("/", app=mcp_app))
128-
logging.getLogger(__name__).info("✅ MCP server mounted (accessible at /mcp)")
123+
# Mount MCP app at /mcp following FastMCP + FastAPI pattern
124+
app.mount("/mcp", mcp_app)
125+
logging.getLogger(__name__).info("✅ MCP server mounted at /mcp (unified deployment mode)")
129126
except Exception as e:
130127
logging.getLogger(__name__).error(f"❌ Failed to mount MCP server: {e}")
131128
import traceback

backend/tools/server.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@
3232
mcp = FastMCP(
3333
"TaskMCPServer",
3434
stateless_http=True,
35-
json_response=True
35+
json_response=True,
36+
streamable_http_path="/" # Path where MCP will be accessible (will be mounted at /mcp)
3637
)
3738

3839

@@ -636,14 +637,14 @@ def delete_task_tool(user_id: str, task_id: str) -> dict:
636637

637638
def get_mcp_app():
638639
"""
639-
Get the MCP ASGI application instance with lifespan support.
640+
Get the MCP ASGI application instance.
640641
This function allows the MCP app to be mounted on the main FastAPI app
641642
or run standalone.
642643
643644
Returns:
644-
ASGI app: The MCP server's ASGI application with lifespan
645+
Starlette: The MCP server's Starlette application
645646
"""
646-
return mcp.http_app(path="/mcp")
647+
return mcp.streamable_http_app()
647648

648649

649650
if __name__ == "__main__":

0 commit comments

Comments
 (0)