modelcontextprotocol / modelcontextprotocol/python-sdk

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

Open
#577 11 comments 6 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug P1 ready for work
Dominant language
Python
Stars
24.3k
Forks
4k
Avg merge
1d 1h
Merged PRs (30d)
31

Description

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)

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by running the minimal reproduction in the issue, then inspect MCPClient.cleanup(), AsyncExitStack, stdio_client, and ClientSession teardown behavior. The fix is done when independently created clients can be cleaned up in FIFO order without RuntimeError or CancelledError, while FILO cleanup continues to work.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
api, backend
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.