-
Notifications
You must be signed in to change notification settings - Fork 905
Description
Summary
When using FastApiMCP.mount_http() to expose MCP endpoints, all POST requests to the /mcp endpoint hang indefinitely without any response. The server receives the request but never sends back a response.
Environment
- fastapi-mcp version: 0.4.0
- mcp version: 1.26.0
- Python version: 3.12
- FastAPI version: (latest)
- OS: Linux
Reproduction Steps
- Create a simple FastAPI app with MCP integration:
from fastapi import FastAPI
from fastapi_mcp import FastApiMCP
app = FastAPI(title="Test App")
@app.get("/test", tags=["test"])
def test_endpoint():
return {"status": "ok"}
mcp = FastApiMCP(app, include_tags=["test"])
mcp.mount_http() # Using HTTP transport
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)- Start the server and send an initialize request:
curl -X POST "http://localhost:8000/mcp" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2024-11-05",
"capabilities": {},
"clientInfo": {"name": "test", "version": "1.0"}
}
}'Observed Behavior
- The request is sent successfully (confirmed by curl verbose output showing "upload completely sent off")
- The server never responds
- After client timeout, server logs show:
Error handling POST request
Traceback (most recent call last):
File ".../mcp/server/streamable_http.py", line 464, in _handle_post_request
body = await request.body()
File ".../starlette/requests.py", line 243, in body
async for chunk in self.stream():
File ".../starlette/requests.py", line 237, in stream
raise ClientDisconnect()
starlette.requests.ClientDisconnect
Expected Behavior
The server should respond with a valid JSON-RPC response containing the initialization result.
Root Cause Analysis
After analyzing the source code, I believe the issue is in fastapi_mcp/transport/http.py:
1. Race Condition in Session Manager Startup
In FastApiHttpSessionManager._ensure_session_manager_started():
# Lines 61-78
async def run_session_manager():
async with self._session_manager.run():
logger.info("StreamableHTTP session manager is running")
await asyncio.Event().wait() # Keep running until cancelled
self._manager_task = asyncio.create_task(run_session_manager())
self._manager_started = True
# Give the session manager a moment to initialize
await asyncio.sleep(0.1) # <-- This is unreliable!Problem: The asyncio.sleep(0.1) does not guarantee that StreamableHTTPSessionManager.run() has fully initialized its internal _task_group. This is a race condition.
2. Message Routing Deadlock
In mcp/server/streamable_http.py, the _handle_post_request method:
# Lines 541-548
await writer.send(session_message) # Send request to MCP server
# Wait for response from request-specific stream
async for event_message in request_stream_reader: # <-- Hangs here forever
if isinstance(event_message.message.root, JSONRPCResponse | JSONRPCError):
response_message = event_message.message
breakProblem: The response routing depends on:
- MCP Server receiving the request from
read_stream - MCP Server processing the request and sending response to
write_stream message_router(running in theconnect()task group) routing the response torequest_stream_reader
If the task group is not properly initialized (due to the race condition above), the message router never runs, causing the request to hang indefinitely.
3. Improper ASGI Send Callback
In FastApiHttpSessionManager.handle_fastapi_request():
async def send_callback(message):
nonlocal response_started, response_status, response_headers, response_body
if message["type"] == "http.response.start":
response_started = True
response_status = message["status"]
response_headers = message.get("headers", [])
elif message["type"] == "http.response.body":
response_body += message.get("body", b"")This callback is passed to StreamableHTTPSessionManager.handle_request(), but the underlying StreamableHTTPServerTransport may expect a proper ASGI send function that handles streaming responses correctly.
Workaround
Using SSE transport instead of HTTP transport works correctly:
mcp = FastApiMCP(app, include_tags=["test"])
mcp.mount_sse() # Use SSE instead of HTTPSuggested Fix
-
Replace
asyncio.sleep(0.1)with a proper synchronization mechanism (e.g.,asyncio.Event) to ensure the session manager is fully initialized before handling requests. -
Consider using
anyio.Eventfor cross-compatibility:
async def _ensure_session_manager_started(self) -> None:
if self._manager_started:
return
async with self._startup_lock:
if self._manager_started:
return
self._ready_event = asyncio.Event()
async def run_session_manager():
async with self._session_manager.run():
self._ready_event.set() # Signal that we're ready
await asyncio.Event().wait()
self._manager_task = asyncio.create_task(run_session_manager())
await self._ready_event.wait() # Wait for actual readiness
self._manager_started = TrueAdditional Context
- The
mount_sse()method works correctly because it uses a simpler streaming model withEventSourceResponse - This issue affects any MCP client trying to connect via HTTP transport
- The issue is reproducible 100% of the time
The bug really exists, the above was generated by claude code