Open
Description
Describe the bug
When making a tools/call
JSON-RPC request to the mcp server, passing the arguments
parameter as a string instead of an object causes the server to stop responding to all HTTP requests. The server does not return an informative error, and requires a restart to recover.
This makes it possible for a single malformed client request to break the server for everyone.
See the MRE at https://github.com/Sillocan/fastmcp-mre-breaking-task-group-with-invalid-arguments/blob/main/python_sdk/simple_echo_mre.py
To Reproduce
Steps to reproduce the behavior:
- Run the following command in your shell:
uv run --with=git+https://github.com/Sillocan/fastmcp-mre-breaking-task-group-with-invalid-arguments#subdirectory=python_sdk mre
- Observe that a single malformed request (
arguments
is a string) returns 200, but further requests return 500 and the server must be restarted. - See error output below.
Expected behavior
- The server should return a clear error response (such as HTTP 400) when a request has an invalid format—specifically, when
arguments
is a string instead of a dict/object. - The server should remain operational and handle all further requests correctly, even after processing an invalid or malformed request.
Logs
$ uv run --with=git+https://github.com/Sillocan/fastmcp-mre-breaking-task-group-with-invalid-arguments#subdirectory=python_sdk mre
Built fastmcp-mre-breaking-task-group-with-invalid-arguments @ git+https://github.com/Sillocan/fastmcp-mre-breaking-task-group-with-invalid-arguments@3eeb959518a5eaa685c7619ebc7b20f5330f6235#subdirectory=python_sdk
Installed 32 packages in 5ms
[05/27/25 12:40:16] DEBUG Using selector: EpollSelector selector_events.py:64
INFO: Started server process [1783696]
INFO: Waiting for application startup.
INFO StreamableHTTP session manager started streamable_http_manager.py:109
INFO: Application startup complete.
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
[05/27/25 12:40:17] DEBUG connect_tcp.started host='127.0.0.1' port=8000 local_address=None timeout=5.0 socket_options=None _trace.py:87
DEBUG connect_tcp.complete return_value=<httpcore._backends.anyio.AnyIOStream object at 0x7fcbb8fe4ec0> _trace.py:87
DEBUG send_request_headers.started request=<Request [b'POST']> _trace.py:87
DEBUG send_request_headers.complete _trace.py:87
DEBUG send_request_body.started request=<Request [b'POST']> _trace.py:87
DEBUG send_request_body.complete _trace.py:87
DEBUG receive_response_headers.started request=<Request [b'POST']> _trace.py:87
DEBUG Stateless mode: Creating new transport for this request streamable_http_manager.py:159
INFO: 127.0.0.1:51112 - "POST /mcp/ HTTP/1.1" 200 OK
DEBUG receive_response_headers.complete return_value=(b'HTTP/1.1', 200, b'OK', [(b'date', b'Tue, 27 May 2025 19:40:16 GMT'), (b'server', b'uvicorn'), (b'cache-control', b'no-cache, no-transform'), _trace.py:87
(b'connection', b'keep-alive'), (b'content-type', b'text/event-stream'), (b'x-accel-buffering', b'no'), (b'Transfer-Encoding', b'chunked')])
INFO HTTP Request: POST http://127.0.0.1:8000/mcp/ "HTTP/1.1 200 OK" _client.py:1740
DEBUG receive_response_body.started request=<Request [b'POST']> _trace.py:87
DEBUG Closing SSE writer streamable_http.py:486
INFO StreamableHTTP session manager shutting down streamable_http_manager.py:113
DEBUG Got event: http.disconnect. Stop streaming. sse.py:182
ERROR: + Exception Group Traceback (most recent call last):
| File "${HOME}/.cache/uv/archive-v0/WBNLPmHcXZu1RxZu0HKOl/lib/python3.13/site-packages/starlette/routing.py", line 692, in lifespan
| async with self.lifespan_context(app) as maybe_state:
| ~~~~~~~~~~~~~~~~~~~~~^^^^^
| File "${HOME}/.local/share/uv/python/cpython-3.13.2-linux-x86_64-gnu/lib/python3.13/contextlib.py", line 235, in __aexit__
| await self.gen.athrow(value)
| File "${HOME}/.cache/uv/archive-v0/WBNLPmHcXZu1RxZu0HKOl/lib/python3.13/site-packages/mcp/server/streamable_http_manager.py", line 106, in run
| async with anyio.create_task_group() as tg:
| ~~~~~~~~~~~~~~~~~~~~~~~^^
| File "${HOME}/.cache/uv/archive-v0/WBNLPmHcXZu1RxZu0HKOl/lib/python3.13/site-packages/anyio/_backends/_asyncio.py", line 772, in __aexit__
| raise BaseExceptionGroup(
| "unhandled errors in a TaskGroup", self._exceptions
| ) from None
| ExceptionGroup: unhandled errors in a TaskGroup (1 sub-exception)
+-+---------------- 1 ----------------
| Exception Group Traceback (most recent call last):
| File "${HOME}/.cache/uv/archive-v0/WBNLPmHcXZu1RxZu0HKOl/lib/python3.13/site-packages/mcp/server/streamable_http_manager.py", line 171, in run_stateless_server
| async with http_transport.connect() as streams:
| ~~~~~~~~~~~~~~~~~~~~~~^^
| File "${HOME}/.local/share/uv/python/cpython-3.13.2-linux-x86_64-gnu/lib/python3.13/contextlib.py", line 235, in __aexit__
| await self.gen.athrow(value)
| File "${HOME}/.cache/uv/archive-v0/WBNLPmHcXZu1RxZu0HKOl/lib/python3.13/site-packages/mcp/server/streamable_http.py", line 843, in connect
| async with anyio.create_task_group() as tg:
| ~~~~~~~~~~~~~~~~~~~~~~~^^
| File "${HOME}/.cache/uv/archive-v0/WBNLPmHcXZu1RxZu0HKOl/lib/python3.13/site-packages/anyio/_backends/_asyncio.py", line 772, in __aexit__
| raise BaseExceptionGroup(
| "unhandled errors in a TaskGroup", self._exceptions
| ) from None
| ExceptionGroup: unhandled errors in a TaskGroup (1 sub-exception)
+-+---------------- 1 ----------------
| Exception Group Traceback (most recent call last):
| File "${HOME}/.cache/uv/archive-v0/WBNLPmHcXZu1RxZu0HKOl/lib/python3.13/site-packages/mcp/server/streamable_http.py", line 916, in connect
| yield read_stream, write_stream
| File "${HOME}/.cache/uv/archive-v0/WBNLPmHcXZu1RxZu0HKOl/lib/python3.13/site-packages/mcp/server/streamable_http_manager.py", line 174, in run_stateless_server
| await self.app.run(
| ...<4 lines>...
| )
| File "${HOME}/.cache/uv/archive-v0/WBNLPmHcXZu1RxZu0HKOl/lib/python3.13/site-packages/mcp/server/lowlevel/server.py", line 495, in run
| async with AsyncExitStack() as stack:
| ~~~~~~~~~~~~~~^^
| File "${HOME}/.local/share/uv/python/cpython-3.13.2-linux-x86_64-gnu/lib/python3.13/contextlib.py", line 768, in __aexit__
| raise exc
| File "${HOME}/.local/share/uv/python/cpython-3.13.2-linux-x86_64-gnu/lib/python3.13/contextlib.py", line 751, in __aexit__
| cb_suppress = await cb(*exc_details)
| ^^^^^^^^^^^^^^^^^^^^^^
| File "${HOME}/.cache/uv/archive-v0/WBNLPmHcXZu1RxZu0HKOl/lib/python3.13/site-packages/mcp/shared/session.py", line 220, in __aexit__
| return await self._task_group.__aexit__(exc_type, exc_val, exc_tb)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
| File "${HOME}/.cache/uv/archive-v0/WBNLPmHcXZu1RxZu0HKOl/lib/python3.13/site-packages/anyio/_backends/_asyncio.py", line 772, in __aexit__
| raise BaseExceptionGroup(
| "unhandled errors in a TaskGroup", self._exceptions
| ) from None
| ExceptionGroup: unhandled errors in a TaskGroup (1 sub-exception)
+-+---------------- 1 ----------------
| Traceback (most recent call last):
| File "${HOME}/.cache/uv/archive-v0/WBNLPmHcXZu1RxZu0HKOl/lib/python3.13/site-packages/mcp/server/session.py", line 147, in _receive_loop
| await super()._receive_loop()
| File "${HOME}/.cache/uv/archive-v0/WBNLPmHcXZu1RxZu0HKOl/lib/python3.13/site-packages/mcp/shared/session.py", line 354, in _receive_loop
| validated_request = self._receive_request_type.model_validate(
| message.message.root.model_dump(
| by_alias=True, mode="json", exclude_none=True
| )
| )
| File "${HOME}/.cache/uv/archive-v0/WBNLPmHcXZu1RxZu0HKOl/lib/python3.13/site-packages/pydantic/main.py", line 705, in model_validate
| return cls.__pydantic_validator__.validate_python(
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^
| obj, strict=strict, from_attributes=from_attributes, context=context, by_alias=by_alias, by_name=by_name
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
| )
| ^
| pydantic_core._pydantic_core.ValidationError: 23 validation errors for ClientRequest
| PingRequest.method
| Input should be 'ping' [type=literal_error, input_value='tools/call', input_type=str]
| For further information visit https://errors.pydantic.dev/2.11/v/literal_error
| InitializeRequest.method
| Input should be 'initialize' [type=literal_error, input_value='tools/call', input_type=str]
| For further information visit https://errors.pydantic.dev/2.11/v/literal_error
| InitializeRequest.params.protocolVersion
| Field required [type=missing, input_value={'name': 'echo', 'argumen...text": "imnestedtext"}'}, input_type=dict]
| For further information visit https://errors.pydantic.dev/2.11/v/missing
| InitializeRequest.params.capabilities
| Field required [type=missing, input_value={'name': 'echo', 'argumen...text": "imnestedtext"}'}, input_type=dict]
| For further information visit https://errors.pydantic.dev/2.11/v/missing
| InitializeRequest.params.clientInfo
| Field required [type=missing, input_value={'name': 'echo', 'argumen...text": "imnestedtext"}'}, input_type=dict]
| For further information visit https://errors.pydantic.dev/2.11/v/missing
| CompleteRequest.method
| Input should be 'completion/complete' [type=literal_error, input_value='tools/call', input_type=str]
| For further information visit https://errors.pydantic.dev/2.11/v/literal_error
| CompleteRequest.params.ref
| Field required [type=missing, input_value={'name': 'echo', 'argumen...text": "imnestedtext"}'}, input_type=dict]
| For further information visit https://errors.pydantic.dev/2.11/v/missing
| CompleteRequest.params.argument
| Field required [type=missing, input_value={'name': 'echo', 'argumen...text": "imnestedtext"}'}, input_type=dict]
| For further information visit https://errors.pydantic.dev/2.11/v/missing
| SetLevelRequest.method
| Input should be 'logging/setLevel' [type=literal_error, input_value='tools/call', input_type=str]
| For further information visit https://errors.pydantic.dev/2.11/v/literal_error
| SetLevelRequest.params.level
| Field required [type=missing, input_value={'name': 'echo', 'argumen...text": "imnestedtext"}'}, input_type=dict]
| For further information visit https://errors.pydantic.dev/2.11/v/missing
| GetPromptRequest.method
| Input should be 'prompts/get' [type=literal_error, input_value='tools/call', input_type=str]
| For further information visit https://errors.pydantic.dev/2.11/v/literal_error
| GetPromptRequest.params.arguments
| Input should be a valid dictionary [type=dict_type, input_value='{"text": "imnestedtext"}', input_type=str]
| For further information visit https://errors.pydantic.dev/2.11/v/dict_type
| ListPromptsRequest.method
| Input should be 'prompts/list' [type=literal_error, input_value='tools/call', input_type=str]
| For further information visit https://errors.pydantic.dev/2.11/v/literal_error
| ListResourcesRequest.method
| Input should be 'resources/list' [type=literal_error, input_value='tools/call', input_type=str]
| For further information visit https://errors.pydantic.dev/2.11/v/literal_error
| ListResourceTemplatesRequest.method
| Input should be 'resources/templates/list' [type=literal_error, input_value='tools/call', input_type=str]
| For further information visit https://errors.pydantic.dev/2.11/v/literal_error
| ReadResourceRequest.method
| Input should be 'resources/read' [type=literal_error, input_value='tools/call', input_type=str]
| For further information visit https://errors.pydantic.dev/2.11/v/literal_error
| ReadResourceRequest.params.uri
| Field required [type=missing, input_value={'name': 'echo', 'argumen...text": "imnestedtext"}'}, input_type=dict]
| For further information visit https://errors.pydantic.dev/2.11/v/missing
| SubscribeRequest.method
| Input should be 'resources/subscribe' [type=literal_error, input_value='tools/call', input_type=str]
| For further information visit https://errors.pydantic.dev/2.11/v/literal_error
| SubscribeRequest.params.uri
| Field required [type=missing, input_value={'name': 'echo', 'argumen...text": "imnestedtext"}'}, input_type=dict]
| For further information visit https://errors.pydantic.dev/2.11/v/missing
| UnsubscribeRequest.method
| Input should be 'resources/unsubscribe' [type=literal_error, input_value='tools/call', input_type=str]
| For further information visit https://errors.pydantic.dev/2.11/v/literal_error
| UnsubscribeRequest.params.uri
| Field required [type=missing, input_value={'name': 'echo', 'argumen...text": "imnestedtext"}'}, input_type=dict]
| For further information visit https://errors.pydantic.dev/2.11/v/missing
| CallToolRequest.params.arguments
| Input should be a valid dictionary [type=dict_type, input_value='{"text": "imnestedtext"}', input_type=str]
| For further information visit https://errors.pydantic.dev/2.11/v/dict_type
| ListToolsRequest.method
| Input should be 'tools/list' [type=literal_error, input_value='tools/call', input_type=str]
| For further information visit https://errors.pydantic.dev/2.11/v/literal_error
+------------------------------------
DEBUG receive_response_body.complete _trace.py:87
DEBUG response_closed.started _trace.py:87
DEBUG response_closed.complete _trace.py:87
breaking_response.status_code=200 breaking_response.text=''
DEBUG send_request_headers.started request=<Request [b'GET']> _trace.py:87
DEBUG send_request_headers.complete _trace.py:87
DEBUG send_request_body.started request=<Request [b'GET']> _trace.py:87
DEBUG send_request_body.complete _trace.py:87
DEBUG receive_response_headers.started request=<Request [b'GET']> _trace.py:87
INFO: 127.0.0.1:51112 - "GET /mcp/ HTTP/1.1" 500 Internal Server Error
ERROR: Exception in ASGI application
Traceback (most recent call last):
File "${HOME}/.cache/uv/archive-v0/WBNLPmHcXZu1RxZu0HKOl/lib/python3.13/site-packages/uvicorn/protocols/http/h11_impl.py", line 403, in run_asgi
result = await app( # type: ignore[func-returns-value]
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
self.scope, self.receive, self.send
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
)
^
File "${HOME}/.cache/uv/archive-v0/WBNLPmHcXZu1RxZu0HKOl/lib/python3.13/site-packages/uvicorn/middleware/proxy_headers.py", line 60, in __call__
return await self.app(scope, receive, send)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "${HOME}/.cache/uv/archive-v0/WBNLPmHcXZu1RxZu0HKOl/lib/python3.13/site-packages/starlette/applications.py", line 112, in __call__
await self.middleware_stack(scope, receive, send)
File "${HOME}/.cache/uv/archive-v0/WBNLPmHcXZu1RxZu0HKOl/lib/python3.13/site-packages/starlette/middleware/errors.py", line 187, in __call__
raise exc
File "${HOME}/.cache/uv/archive-v0/WBNLPmHcXZu1RxZu0HKOl/lib/python3.13/site-packages/starlette/middleware/errors.py", line 165, in __call__
await self.app(scope, receive, _send)
File "${HOME}/.cache/uv/archive-v0/WBNLPmHcXZu1RxZu0HKOl/lib/python3.13/site-packages/starlette/middleware/exceptions.py", line 62, in __call__
await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send)
File "${HOME}/.cache/uv/archive-v0/WBNLPmHcXZu1RxZu0HKOl/lib/python3.13/site-packages/starlette/_exception_handler.py", line 53, in wrapped_app
raise exc
File "${HOME}/.cache/uv/archive-v0/WBNLPmHcXZu1RxZu0HKOl/lib/python3.13/site-packages/starlette/_exception_handler.py", line 42, in wrapped_app
await app(scope, receive, sender)
File "${HOME}/.cache/uv/archive-v0/WBNLPmHcXZu1RxZu0HKOl/lib/python3.13/site-packages/starlette/routing.py", line 714, in __call__
await self.middleware_stack(scope, receive, send)
File "${HOME}/.cache/uv/archive-v0/WBNLPmHcXZu1RxZu0HKOl/lib/python3.13/site-packages/starlette/routing.py", line 734, in app
await route.handle(scope, receive, send)
File "${HOME}/.cache/uv/archive-v0/WBNLPmHcXZu1RxZu0HKOl/lib/python3.13/site-packages/starlette/routing.py", line 460, in handle
await self.app(scope, receive, send)
File "${HOME}/.cache/uv/archive-v0/WBNLPmHcXZu1RxZu0HKOl/lib/python3.13/site-packages/mcp/server/fastmcp/server.py", line 786, in handle_streamable_http
await self.session_manager.handle_request(scope, receive, send)
File "${HOME}/.cache/uv/archive-v0/WBNLPmHcXZu1RxZu0HKOl/lib/python3.13/site-packages/mcp/server/streamable_http_manager.py", line 137, in handle_request
raise RuntimeError("Task group is not initialized. Make sure to use run().")
RuntimeError: Task group is not initialized. Make sure to use run().
DEBUG receive_response_headers.complete return_value=(b'HTTP/1.1', 500, b'Internal Server Error', [(b'date', b'Tue, 27 May 2025 19:40:16 GMT'), (b'server', b'uvicorn'), (b'content-length', b'21'), _trace.py:87
(b'content-type', b'text/plain; charset=utf-8')])
INFO HTTP Request: GET http://127.0.0.1:8000/mcp/ "HTTP/1.1 500 Internal Server Error" _client.py:1740
DEBUG receive_response_body.started request=<Request [b'GET']> _trace.py:87
DEBUG receive_response_body.complete _trace.py:87
DEBUG response_closed.started _trace.py:87
DEBUG response_closed.complete _trace.py:87
DEBUG close.started _trace.py:87
DEBUG close.complete _trace.py:87
get_response.status_code=500 get_response.text='Internal Server Error'
Traceback (most recent call last):
File "${HOME}/.cache/uv/archive-v0/WBNLPmHcXZu1RxZu0HKOl/bin/mre", line 12, in <module>
sys.exit(main())
~~~~^^
File "${HOME}/.cache/uv/archive-v0/WBNLPmHcXZu1RxZu0HKOl/lib/python3.13/site-packages/simple_echo_mre.py", line 69, in main
asyncio.run(run_and_break_the_mcp())
~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^
File "${HOME}/.local/share/uv/python/cpython-3.13.2-linux-x86_64-gnu/lib/python3.13/asyncio/runners.py", line 195, in run
return runner.run(main)
~~~~~~~~~~^^^^^^
File "${HOME}/.local/share/uv/python/cpython-3.13.2-linux-x86_64-gnu/lib/python3.13/asyncio/runners.py", line 118, in run
return self._loop.run_until_complete(task)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^
File "${HOME}/.local/share/uv/python/cpython-3.13.2-linux-x86_64-gnu/lib/python3.13/asyncio/base_events.py", line 725, in run_until_complete
return future.result()
~~~~~~~~~~~~~^^
File "${HOME}/.cache/uv/archive-v0/WBNLPmHcXZu1RxZu0HKOl/lib/python3.13/site-packages/simple_echo_mre.py", line 62, in run_and_break_the_mcp
await breakit()
File "${HOME}/.cache/uv/archive-v0/WBNLPmHcXZu1RxZu0HKOl/lib/python3.13/site-packages/simple_echo_mre.py", line 54, in breakit
assert get_response.status_code < 300, "MCP is fully broken"
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AssertionError: MCP is fully broken
Desktop (please complete the following information):
- OS: Linux
- Browser [e.g. chrome, safari]
- Version: 1.9.1
Additional context
Metadata
Metadata
Assignees
Labels
No labels
Type
Projects
Status
To triage