Skip to content

[BUG] mount_http() causes request to hang indefinitely #259

@deku0818

Description

@deku0818

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

  1. 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)
  1. 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
        break

Problem: The response routing depends on:

  1. MCP Server receiving the request from read_stream
  2. MCP Server processing the request and sending response to write_stream
  3. message_router (running in the connect() task group) routing the response to request_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 HTTP

Suggested Fix

  1. 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.

  2. Consider using anyio.Event for 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 = True

Additional Context

  • The mount_sse() method works correctly because it uses a simpler streaming model with EventSourceResponse
  • 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions