Skip to content

Commit d025f1d

Browse files
feat(mcp): unify FastAPI + MCP via route-merge, adopt Streamable HTTP
CHECKLIST - One integration pattern only (route-merge) - MCP sub-app lives at `/mcp` - Streamable HTTP transport everywhere - Stateless in prod by default; Event Store ready - No duplicate MCP routes
1 parent a0b0c76 commit d025f1d

3 files changed

Lines changed: 19 additions & 12 deletions

File tree

app/main.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -131,9 +131,9 @@ def service_status_check():
131131
# --- MCP Integration ---
132132
mcp_server = create_mcp_server(app)
133133
mode = (
134-
McpAppModes.EVENT_STORE # Use Redis-backed event store for production
134+
McpAppModes.STATELESS
135135
if settings.CONF_ENV == Environments.PROD
136-
else McpAppModes.STATEFUL # Dev uses in-memory session management
136+
else McpAppModes.STATEFUL # uses in-memory session management
137137
)
138138
mcp_app = create_mcp_app(mcp_server, mode=mode)
139139

@@ -168,7 +168,6 @@ async def combined_lifespan(app: FastAPI):
168168
lifespan=combined_lifespan,
169169
)
170170

171-
combined_app.mount("/mcp", mcp_app)
172171

173172

174173
# Doc: https://gofastmcp.com/integrations/fastapi#offering-an-llm-friendly-api

app/mcp/client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ async def get_tools(client: Client):
4646
tools = await client.list_tools()
4747
logger.info("Available tools:")
4848
for tool in tools:
49-
logger.info(f"- {tool}")
49+
logger.info(f"- {tool.name}")
5050
return tools
5151

5252

app/mcp/server.py

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44

55
# Ussing the experimental parser as FASTMCP_EXPERIMENTAL_ENABLE_NEW_OPENAPI_PARSER=true is set in env
66
from fastmcp.experimental.server.openapi import RouteMap, MCPType
7-
from fastmcp.server.http import create_streamable_http_app, StarletteWithLifespan, EventStore
7+
from fastmcp.server.http import create_streamable_http_app, StarletteWithLifespan
8+
9+
from mcp.server.streamable_http import EventStore
810

911
from app.mcp.redis_event_store import RedisEventStore
1012
from app.utils.logging_config import get_logger
@@ -65,25 +67,31 @@ def create_mcp_app(
6567
sticky routing by Mcp-Session-Id, or you will see 400 “No valid session ID”.
6668
- `EVENT_STORE`:
6769
* True stateful sessions shared across workers via a central event store (e.g., Redis).
68-
* Optional: still add sticky routing to reduce cross-worker chatter.
70+
* Experimental: still add sticky routing to reduce cross-worker chatter.
71+
* sticky routing should be keyed on the Mcp-Session-Id from header;
72+
MCP Streamable HTTP uses this header to keep sessions.
73+
74+
Docs:
75+
- [MCP Transports](https://modelcontextprotocol.io/specification/2025-06-18/basic/transports)
6976
"""
70-
# Important: expose sub-app at "/" and MOUNT it at "/mcp" later to avoid double-prefixing.
71-
# path is "/" here; we'll mount under "/mcp" in the main app.
77+
# Sine we are using the Route-merge pattern it expects the MCP app itself to be mounted at /mcp
78+
# (i.e., the sub-app's internal path is /mcp)
7279
logger.info(f"Creating MCP App with {mode}")
7380
match mode:
7481
case McpAppModes.STATELESS:
7582
return mcp_server.http_app(
76-
path="/",
83+
path="/mcp",
7784
json_response=True,
7885
stateless_http=True, # multi-worker friendly
79-
transport="http", # or "streamable-http" (both hit the same factory)
86+
transport="streamable-http", # or "http" (both hit the same factory)
8087
)
8188

8289
case McpAppModes.STATEFUL:
8390
return mcp_server.http_app(
84-
path="/",
91+
path="/mcp",
8592
json_response=True,
8693
# stateless_http defaults to False => stateful per-process
94+
transport="streamable-http",
8795
)
8896

8997
case McpAppModes.EVENT_STORE:
@@ -94,7 +102,7 @@ def create_mcp_app(
94102

95103
return create_streamable_http_app(
96104
server=mcp_server,
97-
streamable_http_path="/",
105+
streamable_http_path="/mcp",
98106
event_store=redis_store, # enable cross-worker resumability
99107
json_response=True,
100108
# stateless_http=False # default (stateful)

0 commit comments

Comments
 (0)