Skip to content

Bug Report: MCP Module Raises CancelledError and Prevents Custom Error Handling #3708

@Arlen122

Description

@Arlen122

Environment

  • OS: Windows 10
  • Python: 3.11 (Anaconda environment)
  • google-adk version: (1.13.0/1.16.0 all tried)

Description

When using the MCP module (McpToolset with StreamableHTTPConnectionParams), my expectation is that connection errors (such as an incorrect URL or network error) would raise a clear, catchable exception, so I could yield a corresponding error event for my application.

However, what I observe is that an asyncio.exceptions.CancelledError is raised due to a cancellation within the library, causing coroutine cancellation conflicts. As a result, I am not able to handle the exception gracefully, and even simple asyncio code to yield an error event cannot execute properly.

Code to Reproduce

import asyncio
from google.adk.tools.mcp_tool.mcp_toolset import McpToolset, StreamableHTTPConnectionParams

async def mock_yield_error_event():
    await asyncio.sleep(0.5)
    print("I want to yield MCP error event when the connection failed.")

async def main():
    try:
        tool = McpToolset(
            connection_params=StreamableHTTPConnectionParams(
                url="http://this-is-a-wrong-mcp-url-to-raise-connection-error",
                headers={"Authorization": "Bearer your-auth-token"}
            ),
        )
        tools = await tool.get_tools()
    except BaseException as e:
        print(f"Catch an MCP connection error: {e}")
        await mock_yield_error_event()

if __name__ == "__main__":
    asyncio.run(main())

Error Output

Catch an MCP connection error: Cancelled by cancel scope 1e64b7bf890
an error occurred during closing of asynchronous generator <async_generator object streamablehttp_client at 0x000001E65D95CDC0>
asyncgen: <async_generator object streamablehttp_client at 0x000001E65D95CDC0>
  + Exception Group Traceback (most recent call last):
  |   File "D:\Environment\Anaconda\envs\langgraph_update\Lib\site-packages\anyio\_backends\_asyncio.py", line 772, in __aexit__
  |     raise BaseExceptionGroup(
  | BaseExceptionGroup: unhandled errors in a TaskGroup (2 sub-exceptions)
  +-+---------------- 1 ----------------
    | Traceback (most recent call last):
    |   File "D:\Environment\Anaconda\envs\langgraph_update\Lib\site-packages\mcp\client\streamable_http.py", line 407, in handle_request_async
    |     await self._handle_post_request(ctx)
    |   File "D:\Environment\Anaconda\envs\langgraph_update\Lib\site-packages\mcp\client\streamable_http.py", line 278, in _handle_post_request
    |     response.raise_for_status()
    |   File "D:\Environment\Anaconda\envs\langgraph_update\Lib\site-packages\httpx\_models.py", line 829, in raise_for_status
    |     raise HTTPStatusError(message, request=request, response=self)
    | httpx.HTTPStatusError: Server error '504 Gateway Time-out' for url 'http://mock-wrong-mcp-url'
    | For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/504
    +---------------- 2 ----------------
    | Traceback (most recent call last):
    |   File "D:\Environment\Anaconda\envs\langgraph_update\Lib\site-packages\mcp\client\streamable_http.py", line 500, in streamablehttp_client
    |     yield (
    | GeneratorExit
    +------------------------------------

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "D:\Environment\Anaconda\envs\langgraph_update\Lib\site-packages\mcp\client\streamable_http.py", line 476, in streamablehttp_client
    async with anyio.create_task_group() as tg:
  File "D:\Environment\Anaconda\envs\langgraph_update\Lib\site-packages\anyio\_backends\_asyncio.py", line 778, in __aexit__
    if self.cancel_scope.__exit__(type(exc), exc, exc.__traceback__):
       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "D:\Environment\Anaconda\envs\langgraph_update\Lib\site-packages\anyio\_backends\_asyncio.py", line 457, in __exit__
    raise RuntimeError(
RuntimeError: Attempted to exit cancel scope in a different task than it was entered in
Traceback (most recent call last):
  File "D:\Environment\Anaconda\envs\langgraph_update\Lib\site-packages\anyio\streams\memory.py", line 111, in receive
    return self.receive_nowait()
           ^^^^^^^^^^^^^^^^^^^^^
  File "D:\Environment\Anaconda\envs\langgraph_update\Lib\site-packages\anyio\streams\memory.py", line 106, in receive_nowait
    raise WouldBlock
anyio.WouldBlock

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "D:\InnerSource\agent-sdk\adk_bug.py", line 17, in main
    tools = await tool.get_tools()
            ^^^^^^^^^^^^^^^^^^^^^^
  File "D:\Environment\Anaconda\envs\langgraph_update\Lib\site-packages\google\adk\tools\mcp_tool\mcp_session_manager.py", line 128, in wrapper
    return await func(self, *args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "D:\Environment\Anaconda\envs\langgraph_update\Lib\site-packages\google\adk\tools\mcp_tool\mcp_toolset.py", line 156, in get_tools
    session = await self._mcp_session_manager.create_session()
              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "D:\Environment\Anaconda\envs\langgraph_update\Lib\site-packages\google\adk\tools\mcp_tool\mcp_session_manager.py", line 362, in create_session
    await session.initialize()
  File "D:\Environment\Anaconda\envs\langgraph_update\Lib\site-packages\mcp\client\session.py", line 151, in initialize
    result = await self.send_request(
             ^^^^^^^^^^^^^^^^^^^^^^^^
  File "D:\Environment\Anaconda\envs\langgraph_update\Lib\site-packages\mcp\shared\session.py", line 272, in send_request
    response_or_error = await response_stream_reader.receive()
                        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "D:\Environment\Anaconda\envs\langgraph_update\Lib\site-packages\anyio\streams\memory.py", line 119, in receive
    await receive_event.wait()
  File "D:\Environment\Anaconda\envs\langgraph_update\Lib\asyncio\locks.py", line 213, in wait
    await fut
asyncio.exceptions.CancelledError: Cancelled by cancel scope 1e64b7bf890

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "D:\InnerSource\agent-sdk\adk_bug.py", line 24, in <module>
    asyncio.run(main())
  File "D:\Environment\Anaconda\envs\langgraph_update\Lib\asyncio\runners.py", line 190, in run
    return runner.run(main)
           ^^^^^^^^^^^^^^^^
  File "D:\Environment\Anaconda\envs\langgraph_update\Lib\asyncio\runners.py", line 118, in run
    return self._loop.run_until_complete(task)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "D:\Environment\Anaconda\envs\langgraph_update\Lib\asyncio\base_events.py", line 654, in run_until_complete
    return future.result()
           ^^^^^^^^^^^^^^^
  File "D:\InnerSource\agent-sdk\adk_bug.py", line 20, in main
    await mock_yield_error_event()
  File "D:\Environment\Anaconda\envs\langgraph_update\Lib\asyncio\tasks.py", line 649, in sleep
    return await future
           ^^^^^^^^^^^^
asyncio.exceptions.CancelledError: Cancelled by cancel scope 1e64b7bf890

Process finished with exit code 1

What I Expected

I expected that, upon connection failure, an appropriate exception (like httpx.HTTPStatusError or a custom error) would be raised and could be caught within my try/except block. After catching the error, I should be able to perform custom error handling logic such as yielding a specific error event from my async function.

What Actually Happened

  • The MCP module wraps or replaces the connection error with CancelledError from a cancel scope deep within its internal async handling (possibly AnyIO's TaskGroup).
  • While the error can be caught as BaseException, after catching, any further async code (such as await asyncio.sleep() or yielding anything) immediately fails with CancelledError due to the outer cancellation.
  • The error logs indicate "Attempted to exit cancel scope in a different task than it was entered in," suggesting a broken context or scope within the async error propagation in MCP.

Impact

  • I am unable to trigger any error-handling event after the MCP error, because coroutine cancellation is forced by the library’s error handling mechanics.

What I Would Like

  • The library should propagate connection or HTTP errors as expected Python exceptions (such as those from httpx, or a well-documented wrapper error), not as a cancellation that infects surrounding async code.
  • Catching a connection error should not cause the rest of the async call stack to be cancelled; allow my code to run custom error handlers as needed.
  • If cancellation scopes are necessary for resource cleanup, please ensure they do not leak CancelledError into user code or interfere with coroutine control flow on the library user's side.

Steps to Reproduce

  1. Paste the code provided above into a minimal script.
  2. Configure an MCP URL that will definitely fail (e.g., an invalid domain).
  3. Observe that after catching the error, attempts to yield/await anything else in the except block will fail with CancelledError from the ADK internals.

Thank you for your attention. This behavior makes it very difficult to integrate proper error handling in async flows. Please advise or fix if possible!

Metadata

Metadata

Assignees

No one assigned

    Labels

    tools[Component] This issue is related to tools

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions