Description
Describe the bug
The MCP server receives Received request before initialization was complete
after a second deployment when a client is trying to list the different tools or even when it tries to run a tool on the server.
To Reproduce
SImple main.py which is one of your examples
import httpx
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("My App")
@mcp.tool()
def calculate_bmi(weight_kg: float, height_m: float) -> float:
"""Calculate BMI given weight in kg and height in meters"""
return weight_kg / (height_m**2)
@mcp.tool()
async def fetch_weather(city: str) -> str:
"""Fetch current weather for a city"""
async with httpx.AsyncClient() as client:
response = await client.get(f"https://api.weather.com/{city}")
return response.text
app = mcp.sse_app()
-
Start using uvicorn
FASTMCP_LOG_LEVEL=DEBUG uv run uvicorn main:app --port=8001 --host=0.0.0.0 --timeout-graceful-shutdown 5
-
Run the inspector in SSE mode and listen on
http://localhost:8001/see
. You will be able to list tools. -
But now, reload you server to simulate a new deployment using the same command:
FASTMCP_LOG_LEVEL=DEBUG uv run uvicorn main:app --port=8001 --host=0.0.0.0 --timeout-graceful-shutdown 5
-
Go to the inspector and try to list the tools, it will timeout and you'll get the following server logs
FASTMCP_LOG_LEVEL=DEBUG uv run uvicorn main:app --port=8001 --host=0.0.0.0 --timeout-graceful-shutdown 5
[04/03/25 23:48:40] DEBUG SseServerTransport initialized with endpoint: /messages/ sse.py:79
INFO: Started server process [89042]
INFO: Waiting for application startup.
INFO: Application startup complete.
INFO: Uvicorn running on http://0.0.0.0:8001 (Press CTRL+C to quit)
[04/03/25 23:48:41] DEBUG Setting up SSE connection sse.py:87
DEBUG Created new session with ID: f55c6f64-be08-464c-bbab-203223adb44c sse.py:100
DEBUG Starting SSE response task sse.py:127
DEBUG Yielding read and write streams sse.py:130
INFO: 127.0.0.1:62930 - "GET /sse HTTP/1.1" 200 OK
DEBUG Starting SSE writer sse.py:107
DEBUG Sent endpoint event: /messages/?session_id=f55c6f64be08464cbbab203223adb44c sse.py:110
DEBUG chunk: b'event: endpoint\r\ndata: /messages/?session_id=f55c6f64be08464cbbab203223adb44c\r\n\r\n' sse.py:156
[04/03/25 23:48:45] DEBUG Handling POST message sse.py:136
DEBUG Parsed session ID: f55c6f64-be08-464c-bbab-203223adb44c sse.py:147
writer: MemoryObjectSendStream(_state=MemoryObjectStreamState(max_buffer_size=0, buffer=deque([]), open_send_channels=1, open_receive_channels=1, waiting_receivers=OrderedDict({<anyio._backends._asyncio.Event object at 0x103ff6f00>: MemoryObjectItemReceiver(task_info=AsyncIOTaskInfo(id=4386289792, name='mcp.shared.session.BaseSession._receive_loop'), item=None)}), waiting_senders=OrderedDict()), _closed=False)
DEBUG Received JSON: b'{"jsonrpc":"2.0","id":3,"method":"tools/list","params":{}}' sse.py:161
DEBUG Validated client message: root=JSONRPCRequest(method='tools/list', params={}, jsonrpc='2.0', id=3) sse.py:165
DEBUG Sending message to writer: root=JSONRPCRequest(method='tools/list', params={}, jsonrpc='2.0', id=3) sse.py:173
INFO: 127.0.0.1:62936 - "POST /messages/?session_id=f55c6f64be08464cbbab203223adb44c HTTP/1.1" 202 Accepted
DEBUG Got event: http.disconnect. Stop streaming. sse.py:177
ERROR: Exception in ASGI application
+ Exception Group Traceback (most recent call last):
| File "/Users/xxxxx/.venv/lib/python3.12/site-packages/uvicorn/protocols/http/h11_impl.py", line 403, in run_asgi
| result = await app( # type: ignore[func-returns-value]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
| File "/Users/xxxxx/.venv/lib/python3.12/site-packages/uvicorn/middleware/proxy_headers.py", line 60, in __call__
| return await self.app(scope, receive, send)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
| File "/Users/xxxxx/.venv/lib/python3.12/site-packages/starlette/applications.py", line 112, in __call__
| await self.middleware_stack(scope, receive, send)
| File "/Users/xxxxx/.venv/lib/python3.12/site-packages/starlette/middleware/errors.py", line 187, in __call__
| raise exc
| File "/Users/xxxxx/.venv/lib/python3.12/site-packages/starlette/middleware/errors.py", line 165, in __call__
| await self.app(scope, receive, _send)
| File "/Users/xxxxx/.venv/lib/python3.12/site-packages/starlette/middleware/exceptions.py", line 62, in __call__
| await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send)
| File "/Users/xxxxx/.venv/lib/python3.12/site-packages/starlette/_exception_handler.py", line 53, in wrapped_app
| raise exc
| File "/Users/xxxxx/.venv/lib/python3.12/site-packages/starlette/_exception_handler.py", line 42, in wrapped_app
| await app(scope, receive, sender)
| File "/Users/xxxxx/.venv/lib/python3.12/site-packages/starlette/routing.py", line 714, in __call__
| await self.middleware_stack(scope, receive, send)
| File "/Users/xxxxx/.venv/lib/python3.12/site-packages/starlette/routing.py", line 734, in app
| await route.handle(scope, receive, send)
| File "/Users/xxxxx/.venv/lib/python3.12/site-packages/starlette/routing.py", line 288, in handle
| await self.app(scope, receive, send)
| File "/Users/xxxxx/.venv/lib/python3.12/site-packages/starlette/routing.py", line 76, in app
| await wrap_app_handling_exceptions(app, request)(scope, receive, send)
| File "/Users/xxxxx/.venv/lib/python3.12/site-packages/starlette/_exception_handler.py", line 53, in wrapped_app
| raise exc
| File "/Users/xxxxx/.venv/lib/python3.12/site-packages/starlette/_exception_handler.py", line 42, in wrapped_app
| await app(scope, receive, sender)
| File "/Users/xxxxx/.venv/lib/python3.12/site-packages/starlette/routing.py", line 73, in app
| response = await f(request)
| ^^^^^^^^^^^^^^^^
| File "/Users/xxxxx/.venv/lib/python3.12/site-packages/mcp/server/fastmcp/server.py", line 485, in handle_sse
| async with sse.connect_sse(
| File "/Users/xxxxx/.pyenv/versions/3.12.2/lib/python3.12/contextlib.py", line 231, in __aexit__
| await self.gen.athrow(value)
| File "/Users/xxxxx/.venv/lib/python3.12/site-packages/mcp/server/sse.py", line 123, in connect_sse
| async with anyio.create_task_group() as tg:
| File "/Users/xxxxx/.venv/lib/python3.12/site-packages/anyio/_backends/_asyncio.py", line 772, in __aexit__
| raise BaseExceptionGroup(
| ExceptionGroup: unhandled errors in a TaskGroup (1 sub-exception)
+-+---------------- 1 ----------------
| Exception Group Traceback (most recent call last):
| File "/Users/xxxxx/.venv/lib/python3.12/site-packages/mcp/server/sse.py", line 131, in connect_sse
| yield (read_stream, write_stream)
| File "/Users/xxxxx.venv/lib/python3.12/site-packages/mcp/server/fastmcp/server.py", line 490, in handle_sse
| await self._mcp_server.run(
| File "/Users/xxxxx/.venv/lib/python3.12/site-packages/mcp/server/lowlevel/server.py", line 483, in run
| async with AsyncExitStack() as stack:
| File "/Users/xxxxx/.pyenv/versions/3.12.2/lib/python3.12/contextlib.py", line 754, in __aexit__
| raise exc_details[1]
| File "/Users/xxxxx/.pyenv/versions/3.12.2/lib/python3.12/contextlib.py", line 737, in __aexit__
| cb_suppress = await cb(*exc_details)
| ^^^^^^^^^^^^^^^^^^^^^^
| File "/Users/xxxxx/.venv/lib/python3.12/site-packages/mcp/shared/session.py", line 210, in __aexit__
| return await self._task_group.__aexit__(exc_type, exc_val, exc_tb)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
| File "/Users/xxxxx/.venv/lib/python3.12/site-packages/anyio/_backends/_asyncio.py", line 772, in __aexit__
| raise BaseExceptionGroup(
| ExceptionGroup: unhandled errors in a TaskGroup (1 sub-exception)
+-+---------------- 1 ----------------
| Traceback (most recent call last):
| File "/Users/xxxxx/.venv/lib/python3.12/site-packages/mcp/shared/session.py", line 324, in _receive_loop
| await self._received_request(responder)
| File "/Users/xxxxx/.venv/lib/python3.12/site-packages/mcp/server/session.py", line 163, in _received_request
| raise RuntimeError(
| RuntimeError: Received request before initialization was complete
+------------------------------------
[04/03/25 23:48:48] DEBUG Setting up SSE connection sse.py:87
DEBUG Created new session with ID: 1541ccf4-7bf9-407b-b2a2-94552715a527 sse.py:100
DEBUG Starting SSE response task sse.py:127
DEBUG Yielding read and write streams sse.py:130
INFO: 127.0.0.1:62938 - "GET /sse HTTP/1.1" 200 OK
DEBUG Starting SSE writer sse.py:107
DEBUG Sent endpoint event: /messages/?session_id=1541ccf47bf9407bb2a294552715a527 sse.py:110
DEBUG chunk: b'event: endpoint\r\ndata: /messages/?session_id=1541ccf47bf9407bb2a294552715a527\r\n\r\n' sse.py:156
[04/03/25 23:48:55] DEBUG Handling POST message sse.py:136
DEBUG Parsed session ID: 1541ccf4-7bf9-407b-b2a2-94552715a527 sse.py:147
writer: MemoryObjectSendStream(_state=MemoryObjectStreamState(max_buffer_size=0, buffer=deque([]), open_send_channels=1, open_receive_channels=1, waiting_receivers=OrderedDict({<anyio._backends._asyncio.Event object at 0x105916510>: MemoryObjectItemReceiver(task_info=AsyncIOTaskInfo(id=4386290368, name='mcp.shared.session.BaseSession._receive_loop'), item=None)}), waiting_senders=OrderedDict()), _closed=False)
DEBUG Received JSON: b'{"jsonrpc":"2.0","method":"notifications/cancelled","params":{"requestId":3,"reason":"Request timed out"}}' sse.py:161
DEBUG Validated client message: root=JSONRPCNotification(method='notifications/cancelled', params={'requestId': 3, 'reason': 'Request timed out'}, jsonrpc='2.0') sse.py:165
DEBUG Sending message to writer: root=JSONRPCNotification(method='notifications/cancelled', params={'requestId': 3, 'reason': 'Request timed out'}, sse.py:173
jsonrpc='2.0')
INFO: 127.0.0.1:62945 - "POST /messages/?session_id=1541ccf47bf9407bb2a294552715a527 HTTP/1.1" 202 Accepted
[04/03/25 23:49:03] DEBUG ping: b': ping - 2025-04-03 21:49:03.579244+00:00\r\n\r\n'
Expected behavior
Somehow, a POST message is accepted while it should not. Is it correct to accept such request?
Shouldn't we have a 404 here instead of a 200 after the second reload? 🤔
python-sdk/src/mcp/server/sse.py
Lines 153 to 157 in c2ca8e0
EDIT: I actually tested to comment this line and it works correctly now!
python-sdk/src/mcp/server/session.py
Lines 162 to 165 in c2ca8e0
Tested with fast-agent, LibreChat and Inspector. Even after a server reload, call tools are still working.
If I understand correctly the global mechanism It should not be possible to have, at this point (after a server reload), a session-id corresponding to a SSE transport that should not have been initialized. So this comment actually is a patch on something that should not happen 😬
Desktop:
- OS: MacOS 15.3
- Browser chrome latest
- Python 3.12
"mcp[cli]>=1.6.0",
"starlette>=0.46.1",
"uvicorn>=0.34.0",