-
Notifications
You must be signed in to change notification settings - Fork 1.1k
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 anAsyncExitStack
inself._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 → 0
and tries toawait self._exit_stack.aclose()
. - Inside
aclose()
AnyIO unwinds aTaskGroup
that 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 (
await
them one after another). - Instantiate one
MCPServer
per 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 = None
Example Code
Python, Pydantic AI & LLM client version
Python >= 3.13
pydantic-ai >= 0.4.6
ivo-1