Skip to content

RuntimeError: Attempted to exit cancel scope in a different task when cleaning up multiple MCPClient instances out-of-order #577

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
HMJiangGatech opened this issue Apr 24, 2025 · 4 comments

Comments

@HMJiangGatech
Copy link

Describe the bug
If two MCPClient objects are instantiated and cleaned up in non-FILO order (i.e., the first-created client is cleaned up before the second), teardown fails with a cascade of RuntimeError/CancelledError exceptions coming from anyio and mcp.client.stdio.

To Reproduce
Minimal repro:

import os, asyncio, json
from typing import Optional
from contextlib import AsyncExitStack
from mcp import ClientSession, StdioServerParameters
from mcp.types import TextContent
from mcp.client.stdio import stdio_client

class MCPClient:
    def __init__(self, command: str, args: list[str], env: Optional[dict] = None):
        self.session: Optional[ClientSession] = None
        self.command, self.args, self.env = command, args, env
        self._cleanup_lock = asyncio.Lock()
        self.exit_stack: Optional[AsyncExitStack] = None

    async def connect_to_server(self):
        await self.cleanup()
        self.exit_stack = AsyncExitStack()

        server_params = StdioServerParameters(
            command=self.command, args=self.args, env=self.env
        )
        stdio_transport = await self.exit_stack.enter_async_context(stdio_client(server_params))
        self.stdio, self.write = stdio_transport
        self.session = await self.exit_stack.enter_async_context(
            ClientSession(self.stdio, self.write)
        )
        await self.session.initialize()

    async def cleanup(self):
        if self.exit_stack:
            async with self._cleanup_lock:
                await self.exit_stack.aclose()
                self.session = None
            self.exit_stack = None

async def main():
    cfg = {
        "command": "npx",
        "args": ["-y", "@adenot/mcp-google-search"],
        "env": {
            "GOOGLE_API_KEY": os.environ["GOOGLE_API_KEY"],
            "GOOGLE_SEARCH_ENGINE_ID": os.environ["GOOGLE_SEARCH_ENGINE_ID"],
        },
    }

    c1, c2 = MCPClient(**cfg), MCPClient(**cfg)
    await c1.connect_to_server()
    await c2.connect_to_server()

    # Works (FILO)
    # await c2.cleanup()
    # await c1.cleanup()

    # Fails (FIFO)
    await c1.cleanup()      # <-- boom
    await c2.cleanup()

if __name__ == "__main__":
    asyncio.run(main())

Expected behavior

cleanup() should succeed regardless of the order in which multiple MCPClient instances are closed, as long as each instance’s own exit_stack is intact. A single client ought to manage its own lifetime without depending on external FILO discipline.

Actual Traceback

RuntimeError: Attempted to exit cancel scope in a different task than it was entered in
...
asyncio.exceptions.CancelledError: Cancelled by cancel scope ...
...
RuntimeError: Attempted to exit a cancel scope that isn't the current task's current cancel scope

Environment

Item Version
mcp 1.6.0
Python 3.12.10
anyio 4.9.0
OS macOS 14.4 (Apple Silicon)
@mashriram
Copy link

Facing the same issue when connectng trying to use FastAPI and async methods with mcp to work with frontend
The error simply like this and it starts at mcp libraries sse.py and flows down to below
``

~~~~~~~~~~~~~~~~~~~~~~~^^
File "/home/mukundan/projects/mcp-sse/.venv/lib/python3.13/site-packages/anyio/_backends/_asyncio.py", line 773, in aexit
if self.cancel_scope.exit(type(exc), exc, exc.traceback):
~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/mukundan/projects/mcp-sse/.venv/lib/python3.13/site-packages/anyio/_backends/_asyncio.py", line 456, in exit
raise RuntimeError(
...<2 lines>...
)
RuntimeError: Attempted to exit cancel scope in a different task than it was entered in```

@pitta-bread
Copy link

pitta-bread commented May 12, 2025

Faced the same issue. My custom context manager class was doing simple iteration of the clients list to attempt cleanup (FIFO). In line with the workaround detailed above, fixed with FILO:

e.g.

finally:
    # Clean up in the same task context
    # MUST BE FILO, FIFO fails
    # https://github.com/modelcontextprotocol/python-sdk/issues/577
    for client in reversed(self.clients):
        try:
            await client.cleanup()

chilang added a commit to chilang/python-sdk that referenced this issue May 16, 2025
… order

Fixed a bug where closing MCPClient instances in FIFO order (first created, first cleaned up)
would raise RuntimeError with 'Attempted to exit cancel scope in a different task'. The issue was
in the stdio_client context manager's cleanup sequence.

The fix properly structures the async context management in stdio_client to ensure resources are
cleaned up in the correct order: first canceling tasks, then closing streams, and finally terminating
the process. Added robust test cases that verify the fix works correctly.

Github-Issue: modelcontextprotocol#577
@CuriousTank
Copy link

How to achieve free management of multiple MCPClient instances under such conditions, enabling instant exit and connection establishment

@jhaoming-oai
Copy link

Trying OpenAI Codex to fix the issue:
https://chatgpt.com/s/cd_682dec27cf788191b2892b8097d45248

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants