Skip to content

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

Open
@Sillocan

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

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    Status

    To triage

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions