Repository navigation
MCPToolset "Attempted to exit cancel scope in a different task" under to_a2a() multi-agent (A2A) β scales with concurrent McpToolset count; SSE transport is race-freeΒ #5729
Description
π΄ Required Information
Describe the Bug:
When an LlmAgent is served via to_a2a() and holds one or more McpToolsets using StreamableHTTPServerParams, MCP session creation during tool discovery intermittently fails with:
Failed to create MCP session: ... unhandled errors in a TaskGroup (1 sub-exception) whose inner exception is RuntimeError: Attempted to exit cancel scope in a different task than it was entered in.
ADK retries get_tools, but under load the agent proceeds with an incomplete tool list β the agent's own MCP tools silently disappear from the spec, the LLM then calls a now-missing tool and the A2A request hard-fails (ValueError: Tool '<name>' not found).
This is the same root cause as #4454, but on a different and very common surface: not adk web + Cloud Run, but to_a2a() ASGI agents in a hub-and-spoke multi-agent system (a manager dispatches to A2A sub-agents; each sub-agent owns its MCP server). We also add quantified new findings: the failure rate is ~linear in the number of concurrent McpToolset sessions per process, and the SSE transport does not exhibit the race at all when there is exactly one session.
Steps to Reproduce:
- Stand up an MCP server with Streamable HTTP transport.
- Build an
LlmAgentwhosetoolsinclude NMcpToolset(connection_params=StreamableHTTPServerParams(url=...))(N β₯ 1; the rate rises sharply with N β₯ 2). - Serve it with
to_a2a(agent)(uvicorn ASGI). - Drive it with repeated A2A requests from a separate manager agent (each request triggers tool discovery /
get_tools). - Observe intermittent MCP-session-creation failures; with N β₯ 2 the agent regularly proceeds with a truncated tool list and the request fails with
Tool '<own-tool>' not found.
Expected Behavior:
McpToolset consistently creates its MCP session and returns the full tool list per A2A request, regardless of how many McpToolsets the agent holds or how it is served.
Observed Behavior:
RuntimeError: Attempted to exit cancel scope in a different task than it was entered in, surfaced as unhandled errors in a TaskGroup (1 sub-exception), in a tight Retrying get_tools loop. Occurrence count per single A2A task, swept by concurrent session count (real production measurements, identical workload):
| concurrent McpToolset sessions in the process | race occurrences / task | task outcome |
|---|---|---|
| 3 (streamable-http) | 84 | survived only via ADK retry |
| 2 (streamable-http) | 40 | HARD FAIL β own tools dropped, Tool 'run_python_code' not found |
| 1 (SSE transport) | 0 | clean, reproduced 0 across many runs |
| 1 (streamable-http) | still races | β |
Environment Details:
- ADK Library Version (pip show google-adk): 1.33.0
- Desktop OS: Linux (Debian 13 / Docker container; host macOS)
- Python Version (python -V): 3.12.13
Model Information:
- Are you using LiteLLM: No
- Which model is being used: gemini-2.5-flash (model-independent β this is a tool-discovery transport bug; it reproduces regardless of model)
π‘ Optional Information
Regression:
Per #4454, worked on ADK 1.14.0; broken from 1.24.x onward. We confirm it is still present on 1.33.0. Bisecting to ADK 1.31.1 did not help.
Logs:
INFO mcp_session_manager: Retrying get_tools due to error: Failed to
create MCP session: Failed to create MCP session: unhandled errors
in a TaskGroup (1 sub-exception)
WARN session_context: Error on session runner task: unhandled errors in
a TaskGroup (1 sub-exception)
WARN llm_agent: Failed to get tools from toolset McpToolset: Failed to
create MCP session: ... (1 sub-exception)
ERROR a2a_agent_executor_impl: Error handling A2A request:
Tool 'run_python_code' not found.
ValueError: Tool 'run_python_code' not found.
(The inner RuntimeError: Attempted to exit cancel scope in a different task is collapsed by the task-group exception-squashing; the message string matches #4454 exactly.)
Additional Context:
- Reproduces with the agent's own MCP server only (single tool source) β it is per-connection, not specific to any one MCP server.
- Tried and did not fix it: anyio 3.x downgrade (hard dependency conflict β starlette/adk require anyioβ₯4.9), A2A Executor V2 /
force_new_version, PR fix(mcp): add use_isolated_event_loop to McpToolset for Vertex AI Agent Engine compatibility Β #5509 (use_isolated_event_loop, would not apply cleanly to 1.33), ADK 1.31.1. - The only reliable mitigation we found: switch the
McpToolsetto the SSE transport (SseConnectionParams) and keep exactly oneMcpToolsetper agent process (move any shared/cross-agent tools to a plain blocking HTTP client instead of a 2ndMcpToolset). With that, race occurrences went 84 β 0 and stayed 0 across many production runs. - Likely upstream root cause: modelcontextprotocol/python-sdk test: Add unit tests for CLI functionalityΒ #577 and # Bug Report: Event History Corruption in Nested ADK Agent WorkflowsΒ #1805 (
streamablehttp_clienttask group entered/exited across asyncio tasks). Cross-reference: ADK 1.24.x MCP StreamableHTTP cancel scope error with adk web + Cloud RunΒ #4454, PR fix(mcp): add use_isolated_event_loop to McpToolset for Vertex AI Agent Engine compatibility Β #5509.
Minimal Reproduction Code:
from google.adk.agents import LlmAgent
from google.adk.a2a.utils.agent_to_a2a import to_a2a
from google.adk.tools.mcp_tool import McpToolset
from google.adk.tools.mcp_tool.mcp_session_manager import (
StreamableHTTPServerParams,
)
agent = LlmAgent(
name="leaf",
model="gemini-2.5-flash",
tools=[
McpToolset(connection_params=StreamableHTTPServerParams(
url="http://mcp-a:8082/mcp")),
McpToolset(connection_params=StreamableHTTPServerParams(
url="http://mcp-b:8081/mcp")), # 2+ sessions -> high rate
],
disallow_transfer_to_parent=True,
disallow_transfer_to_peers=True,
)
a2a_app = to_a2a(agent) # serve with uvicorn; drive with repeated A2A requestsHow often has this issue occurred?:
- Intermittently per attempt, but effectively Always under realistic multi-agent concurrency (84 occurrences in a single task at 3 concurrent sessions).
Activity
sanketpatil06 commentedon May 18, 2026
Hi @LiuYuWei , Thanks for reporting and providing details.
When deploying an agent via
to_a2a()and subjecting it to concurrent A2A requests, globally instantiatedMcpToolsetobjects encounter asynchronous context management conflicts. This causes the tools to fail during initialization and the agent falls back to answering the prompt using only its baseline LLM capabilities.Workaround: You can bypass this by avoiding global
McpToolsetinstantiation. wrap the MCP interaction inside a standard ADKFunctionToolso the toolset initializes dynamically per request. This safely isolates the session to the active request's execution context.Could you please give it a try and let us know if this resolves the issue at your end? Thanks.
LiuYuWei commentedon May 19, 2026
Results from testing your per-request workaround (production:
to_a2a()multi-agent, stock ADK 1.33, anyio 4.x, MCP streamable-http, with one 3rd-party streamable-http hub we don't control).Setup of a clean A/B β one agent, same task, same tool surface; the only variable is how the 3rd-party hub's MCP session is held. (We adapted your snippet: instead of a global
McpToolset, and instead of a nested temp-LlmAgent+nl-prompt β which would lose the precise tool args our workflow needs β each tool is a nativeFunctionToolthat opens and closes its own streamable-httpClientSessionstrictly inside the awaited call body, i.e. one task. Tool names/schemas unchanged.)McpToolsetClientSession(your pattern)unhandled errors in a TaskGroupFailed to create MCP sessionFailed to get tools from MCP server:(empty msg)Two distinct failure classes, and your workaround clears both:
unhandled errors in a TaskGroup) β scales with concurrent globalMcpToolsetcount (we measured 3 β ~84, 2 β ~40 still fatal, 1 β ~0; transport-independent). Collapsing to a single globalMcpToolsetalready drives this to ~0.McpToolset,McpToolset._execute_with_sessionrepeatedly fails withFailed to get tools from MCP server:(empty message, ~10Γ/run) while the underlyingPOST /mcp/returns200and an out-of-bandtools/listwith the same bearer returns200+ the full tool list (the hub is healthy). This is not the cancel-scope race, and single-session does not fix it.Constructing the session per request inside the awaited tool body (your recommendation) eliminated both (1) and (2), and the agent fetched real data on a clean single pass. So the actionable lever is request-task-scoped construction, not merely reducing global
McpToolsetcount β a globalMcpToolsetunderto_a2a()is problematic even at count = 1 (count only modulates failure (1); failure (2) persists until the session is request-scoped). This aligns with theuse_isolated_event_loopdirection in #5509.(The single residual Traceback in B was an unrelated bug in our own in-process port, not ADK/MCP.)
Cross-refs: #4454, #5509, modelcontextprotocol/python-sdk #577 / #1805.
LiuYuWei commentedon May 21, 2026
Follow-up data: tested under ADK v2.0.0 GA (released 2026-05-19; PR #5509 did not merge into it). I re-ran the same controlled session-count experiment on 2.0 in our setup (
to_a2a()multi-agent, streamable-http, the same 3rd-party hub).McpToolset/ agent (Manager)TaskGroup+ 27Failed to create MCP sessionin ~12 min; only the first sub-agent ever armed, downstream tasks never picked up;[pc-inject]fired 4/5 times (race disrupted injection on one task). Aborted at 12 min, restored single-session, race count dropped back to ~0 immediately.So the cancel-scope race scaling with concurrent global
McpToolsetcount is unchanged in 2.0 GA. Single-session (or the per-requestFunctionToolpattern you suggested) remains the load-bearing mitigation; reducing global toolset count alone is what keeps our system race-free, not the version bump.One genuine improvement in 2.0 worth flagging: the graceful-handling path is noticeably softer. The same race storm under 1.33 + 2-McpToolset surfaced as
Failed to get tools from MCP server:(empty message, ~10Γ/run) + HTTP 500 errors propagating to the channel (users saw "system busy"). Under 2.0 + 2-McpToolset we observed 0 Tracebacks, 0 HTTP 500s, 0 container crash-loops, and 0Failed to get toolsβ just a continuous stream ofunhandled errors in a TaskGroupwarnings + retry. The underlying cancel scope is still being crossed across tasks; the surfaced failure mode is now "continuously warn and retry" rather than hard-fail. Tasks no longer crash the channel β they just stop progressing silently.Net: PR #5509's
use_isolated_event_loopdirection and the per-requestFunctionToolworkaround both remain necessary. We're keeping all our workarounds in place on 2.0 (single ownMcpToolsetper agent, the 3rd-party hub via per-requestFunctionTool, plushttpxshim for our shared core tools). Happy to share the exact reproducer / harness if useful.Cross-refs: #4454, #5509, modelcontextprotocol/python-sdk #577 / #1805.
sanketpatil06 commentedon Jul 22, 2026
Hi @LiuYuWei , Thank you for the thorough follow-up and confirming that the FunctionTool workaround resolves the issue!
It is incredibly helpful to know this approach successfully stabilizes tool fetching and mitigates the context leak across both ADK 1.33 and 2.0.0 GA