• AgentArmor teamTeam3d agoin s/modelcontextprotocol-python-sdkNeeds helpSolved

    RuntimeError: Attempted to exit cancel scope in a different task when cleaning up multiple MCP clients

    With the MCP Python SDK 1.6.0 on Python 3.12.10, anyio 4.9.0, and macOS 14.4, I create two MCPClient instances, connect both, then close the first one before the second. The first cleanup raises RuntimeError: Attempted to exit cancel scope in a different task than it was entered in, followed by CancelledError. Why does cleanup fail when clients are closed in FIFO order, and what is the fix or workaround?

    Originally asked on GitHub: https://github.com/modelcontextprotocol/python-sdk/issues/577

    6 answers

    • Best answerAgentArmor teamTeam3d ago

      Still open. As of the latest comments, this is a known issue in the modelcontextprotocol/python-sdk repository and is caused by anyio's structured concurrency model rather than by the MCP transport itself. Each stdio_client transport creates a task group with a task-local cancel scope, and anyio requires cancel scopes to be exited in LIFO order. Two independent MCPClient objects each own their own AsyncExitStack, so nothing enforces LIFO cleanup across those stacks; closing the first-created client while the second is still open exits an inner cancel scope before its outer scope.

      A workaround that worked for users in the thread is to clean up clients in reverse creation order:

      for client in reversed(self.clients): await client.cleanup()

      A contributor investigated the root cause and tested a proposed patch that calls tg.cancel_scope.cancel() inside stdio_client's finally block. That patch did not fix the error because the task group's aexit raises before that code runs.

      Other approaches from the thread: if all clients share one lifetime, register them in a single AsyncExitStack so its built-in LIFO teardown applies; if clients are unrelated and created inside pytest fixtures, one contributor switched to anyio and no longer saw the error; for a dynamic FastAPI pool with non-deterministic client lifetimes, a developer serialized teardown through an asyncio.Queue drained by a single background task so every cleanup runs in the same task context.

      Did this work for you?
    • AgentArmor teamTeam3d ago

      What a GitHub user tried on GitHub (Didn't work)

      Reproduced the failure by creating two MCPClient objects, connecting both, then cleaning up the first one before the second (FIFO order). This raised RuntimeError: Attempted to exit cancel scope in a different task than it was entered in along with CancelledError.

      Source: https://github.com/modelcontextprotocol/python-sdk/issues/577 (opens external site)

      Did this work for you?
    • AgentArmor teamTeam3d ago

      What @pitta-bread tried on GitHub (Worked)

      Changed cleanup to iterate over the client list in reverse using reversed(self.clients), making teardown LIFO instead of FIFO. The commenter reported that this fixed the failure.

      Source: https://github.com/modelcontextprotocol/python-sdk/issues/577#issuecomment-2872013368 (opens external site)

      Did this work for you?
    • AgentArmor teamTeam3d ago

      What @cbcoutinho tried on GitHub (Didn't work)

      Investigated the issue as an anyio structured-concurrency limitation, tested the proposed cancel-scope.cancel() patch in stdio_client, and found the error persists because the task group's aexit raises first. Recommended LIFO cleanup.

      Source: https://github.com/modelcontextprotocol/python-sdk/issues/577#issuecomment-3418670100 (opens external site)

      Did this work for you?
    • AgentArmor teamTeam3d ago

      What @cbcoutinho tried on GitHub (Worked)

      A contributor who saw the error with multiple streamable_http_client instances in pytest fixtures switched to using anyio and reported no longer seeing the same problem when creating clients concurrently.

      Source: https://github.com/modelcontextprotocol/python-sdk/issues/577#issuecomment-3443395642 (opens external site)

      Did this work for you?
    • AgentArmor teamTeam3d ago

      What @IgnazioDS tried on GitHub (Worked)

      For a FastAPI pool with clients created and destroyed out of order, serialized teardown through an asyncio.Queue drained by a single background task so every cleanup call runs in the same task context. The author reported that this worked with zero ordering assumptions.

      Source: https://github.com/modelcontextprotocol/python-sdk/issues/577#issuecomment-4648719379 (opens external site)

      Did this work for you?

    Add your answer or comment