-
Couldn't load subscription status.
- Fork 1.3k
Closed
Labels
Description
Initial Checks
- I confirm that I'm using the latest version of Pydantic AI
- I confirm that I searched for my issue in https://github.com/pydantic/pydantic-ai/issues before opening this issue
Description
What happens?
Firing two or more MCP tools concurrently through the same MCPServerSSE/MCPServerStreamableHTTP (or MCPServerStdio) instance reliably crashes with:
RuntimeError: Attempted to exit cancel scope in a different task than it was entered in
This propagates as an ExceptionGroup that aborts the whole agent run.
Expected Behaviour
Parallel tool calls should finish successfully; the underlying SSE/stdio connection is reference-counted and should close cleanly when the last task exits.
Suspected Root Cause
Suspected Issue Location in Code:
pydantic-ai/pydantic_ai_slim/pydantic_ai/mcp.py
Lines 225 to 230 in 8f20f9b
| async def __aexit__(self, *args: Any) -> bool | None: | |
| async with self._enter_lock: | |
| self._running_count -= 1 | |
| if self._running_count == 0 and self._exit_stack is not None: | |
| await self._exit_stack.aclose() | |
| self._exit_stack = None |
MCPServer.__aenter__()opens the transport client once and keeps anAsyncExitStackinself._exit_stack. It also increments_running_count.- Every concurrent tool call re-enters the same server, re-using the connection and incrementing
_running_count. - The last task to finish runs
__aexit__()with_running_count → 0and tries toawait self._exit_stack.aclose(). - Inside
aclose()AnyIO unwinds aTaskGroupthat was created in the first task, therefore it leads to an error if the last task to finish is not the initial first task to enter:
RuntimeError: Attempted to exit cancel scope in a different task than it was entered in
Potential solutions
- Run tool calls serially (
awaitthem one after another). - Instantiate one
MCPServerper task, avoiding shared connection pools.
Workaround
Wrap the exit-stack shutdown in __aexit__:
try:
await self._exit_stack.aclose()
except RuntimeError as exc:
if "exit cancel scope" not in str(exc):
raise
warnings.warn(
"MCPServer exit stack closed from a different task; "
"safe to ignore but indicates concurrent tool calls.",
RuntimeWarning,
)
finally:
self._exit_stack = NoneExample Code
Python, Pydantic AI & LLM client version
Python >= 3.13
pydantic-ai >= 0.4.6
ivo-1