Repository navigation
Bug Report: MCP Module Raises CancelledError and Prevents Custom Error Handling #3708
Description
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
CancelledErrorfrom 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 asawait 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
- Paste the code provided above into a minimal script.
- Configure an MCP URL that will definitely fail (e.g., an invalid domain).
- 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!
Activity
kushalbhabra commentedon Nov 25, 2025
I've kind of memorized this error that it means MCP server is not reachable, probably a network issue.
Thanks for raising this as a explicit problem.
Arlen122 commentedon Nov 26, 2025
Thank you for your reply, I agree this usually happens when the MCP server cannot be reached.
However, the current error handling is neither normal nor elegant—the error comes as a low-level coroutine cancellation, which causes conflicts in async code. This is not standard Python async behavior, and it makes proper error handling in my code very difficult.
It would be much better if a clear and catchable exception could be raised instead.
nagaem commentedon Dec 2, 2025
I'm running into this same problem - in my case, the MCP server is unreachable due to a credential timeout. I'd love to catch the error so I can regenerate credentials to try again, but the agent is hanging after the initial
RuntimeError: Attempted to exit cancel scope in a different task than it was entered inlog.Do you have a workaround for this?
fix(mcp): handle CancelledError in MCPSessionManager.create_session
fix(mcp): handle CancelledError in MCPSessionManager.create_session
fix(mcp): handle CancelledError in MCPSessionManager.create_session
fix(mcp): handle CancelledError in MCPSessionManager.create_session
surajksharma07 commentedon Dec 23, 2025
@Arlen122 The issue occurs because
create_session()inmcp_session_manager.py:366only catchesException, butasyncio.CancelledErrorinherits fromBaseExceptionin Python 3.8+, so it escapes and breaks your async context.PR #3835 fixes this by adding an explicit CancelledError handler that converts it to
ConnectionErrorbefore the general exception handler.The fix is currently under review and should resolve the issue once merged - it will allow you to properly catch connection errors and handle them gracefully in your async code.
Thanks!
challenger71498 commentedon Dec 26, 2025
Hi @Arlen122 ,
I also encountered this issue, and it seems like the McpSessionManager needs to handle ClientSession of MCP SDK in a single task.
I made a PR #4025 to fix this issue, you may look into it if you are interested in.
surajksharma07 commentedon Dec 29, 2025
@challenger71498 Thanks for PR #4025 and the alternative approach to handling ClientSession in a single task!
Both your PR and PR #3835 are currently under review - they offer different solutions to the same CancelledError issue.
The bug persists in v1.21.0 (mcp_session_manager.py:366 still only catches Exception, not CancelledError).
We appreciate your contribution - this is affecting multiple users and blocking critical error handling patterns like credential refresh and retry logic.
challenger71498 commentedon Dec 29, 2025
@surajksharma07 Thank you for the reply!
I added a simple test case at #3835 to explain the issue I have mentioned at that PR, would appreciate it if you check it out.
challenger71498 commentedon Dec 29, 2025
Also I'd like to demonstrate the issue little more to descirbe the situation.
As @Arlen122 mentioned, the issue occurs when:
Also, the error-handling task will close the task group, but this task may not the task which actually has started the task group.
This violates the structured concurrency of AnyIO, and task group throws
RuntimeError: Attempted to exit cancel scope in a different task than it was entered ineventually.This is why a dedicated background task is required for the MCP client, which is the main content of #4025.
Since the background task handles the client, even if an error has been raised, it will not crash the whole application, only that background task will stop.
surajksharma07 commentedon Dec 29, 2025
@challenger71498 Thank you for the detailed technical explanation and for adding the test case to PR #3835!
Your analysis of the structured concurrency violation is spot-on - the issue goes deeper than just catching CancelledError.
PR #3835 provides a defensive symptom fix (converting CancelledError to ConnectionError), while PR #4025 addresses the architectural root cause (dedicated background task preventing error propagation to main event loop).
The team will review both approaches and your test case to determine the best path forward - your contribution highlighting the AnyIO task group lifecycle issue is very valuable.
feat: start and close ClientSession in a single task in McpSessionMan…
challenger71498 commentedon Jan 31, 2026
@Arlen122 Hi, it seems the fix has been merged and released in v1.23.0.
You can try to reproduce the issue with the latest ADK and check whether it is resolved.
@surajksharma07 Hi, it seems that the Co-authored-by trailer was missing from the merged commit, so I'm not being credited as a contributor. I know the PR is already merged, but is there any way to update the contribution record or add me to the contributors list?
nagaem commentedon Feb 3, 2026
It seems that while the CancelledError is now being converted to a ConnectionError, the main cause of the error is still being obscured: the error message looks like
'Failed to create MCP session: Failed to create MCP session: Attempted to exit cancel scope in a different task than it was entered in'. We can catch the ConnectionError, but the root cause isn't clear.surajksharma07 commentedon Feb 5, 2026
@nagaem Thanks for confirming the fix works!
The obscure error message (
'Failed to create MCP session: Failed to create MCP session: ...') is a valid UX concern - the nested message pattern could be cleaner.If you'd like this improved, please open a separate enhancement issue.
@challenger71498 Apologies for the missing co-authorship credit - that was an oversight during the merge process. Unfortunately, amending the commit history post-release isn't straightforward, but I'll flag this internally to see if we can add recognition elsewhere (contributors list, release notes).
Your contribution to solving this issue was significant.
The core issue (CancelledError preventing custom error handling) is resolved in v1.23.0 via PR #4025's
SessionContextapproach - users can now properly catchConnectionErrorand implement retry/credential refresh logic.Closing this issue.
challenger71498 commentedon Feb 7, 2026
Hi @nagaem, you have a point 👍 The exception message is obscure and could be improved. If you create an enhancement issue for it, I may look into it.
@surajksharma07 Thank you for your response. I appreciate you looking into alternative ways for recognition. I’ve got a few more PRs on the go anyway, so I’m looking forward to earning the contributor badge for the next ones too :)
feat: start and close ClientSession in a single task in McpSessionMan…