Skip to content

tools/call with string arguments breaks mcp server until restart #820

Description

@Sillocan

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:

  1. 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
  2. Observe that a single malformed request (arguments is a string) returns 200, but further requests return 500 and the server must be restarted.
  3. 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
returnfuture.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

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    Status
    Done

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions