modelcontextprotocol / modelcontextprotocol/python-sdk

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

Offen
#577 11 Kommentare 6 Reaktionen 0 zugewiesene Personen Auf GitHub ansehen

Dieses Issue hat noch niemand übernommen.

bug P1 ready for work
Vorherrschende Sprache
Python
Sterne
24.3k
Forks
4k
Ø Merge
1 T. 1 Std.
Gemergte PRs (30 T.)
31

Beschreibung

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)

Beitragsleitfaden

Beitragsleitfaden öffnen

Erste Schritte

  1. Lies das ganze Issue und danach den Beitragsleitfaden des Projekts.
  2. Schreib ins Issue, dass du es übernimmst — das erspart doppelte Arbeit.
  3. Forke das Repository und arbeite in einem Branch.
  4. Öffne einen Pull Request, der die Issue-Nummer nennt.

Rechercherichtung

Beginne damit, die minimale Reproduktion im Issue auszuführen, und untersuche anschließend das Verhalten von MCPClient.cleanup(), AsyncExitStack, stdio_client und dem Teardown von ClientSession. Der Fix ist abgeschlossen, wenn unabhängig erstellte Clients in FIFO-Reihenfolge ohne RuntimeError oder CancelledError bereinigt werden können und die Bereinigung in FILO-Reihenfolge weiterhin funktioniert.

Vom Indexierungsmodell aus dem Issue-Text verfasst.

Bewertung

Tech-Stack
python
Bereich
api, backend
Issue-Typ
Bug
Schwierigkeit
4/5
Geschätzter Aufwand
3-5 Tage
Aktivitätsstatus
Ruhig
Klarheit
Größtenteils klar
Anfängerfreundlichkeit
48/100

Neue Issues direkt in Ihr Postfach

Eine kurze Übersicht über anfängerfreundliche GitHub-Issues.