Skip to content

Parallel MCP tool calls cause Runtime Error #2355

@tradeqvest

Description

@tradeqvest

Initial Checks

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:

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

  1. MCPServer.__aenter__() opens the transport client once and keeps an AsyncExitStack in self._exit_stack. It also increments _running_count.
  2. Every concurrent tool call re-enters the same server, re-using the connection and incrementing _running_count.
  3. The last task to finish runs __aexit__() with _running_count → 0 and tries to await self._exit_stack.aclose().
  4. Inside aclose() AnyIO unwinds a TaskGroup 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

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions