Skip to content

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

Description

@HMJiangGatech

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:

importos, asyncio, jsonfromtypingimportOptionalfromcontextlibimportAsyncExitStackfrommcpimportClientSession, StdioServerParametersfrommcp.typesimportTextContentfrommcp.client.stdioimportstdio_clientclassMCPClient:
def__init__(self, command: str, args: list[str], env: Optional[dict] =None):
self.session: Optional[ClientSession] =Noneself.command, self.args, self.env=command, args, envself._cleanup_lock=asyncio.Lock()
self.exit_stack: Optional[AsyncExitStack] =Noneasyncdefconnect_to_server(self):
awaitself.cleanup()
self.exit_stack=AsyncExitStack()
server_params=StdioServerParameters(
command=self.command, args=self.args, env=self.env
)
stdio_transport=awaitself.exit_stack.enter_async_context(stdio_client(server_params))
self.stdio, self.write=stdio_transportself.session=awaitself.exit_stack.enter_async_context(
ClientSession(self.stdio, self.write)
)
awaitself.session.initialize()
asyncdefcleanup(self):
ifself.exit_stack:
asyncwithself._cleanup_lock:
awaitself.exit_stack.aclose()
self.session=Noneself.exit_stack=Noneasyncdefmain():
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)
awaitc1.connect_to_server()
awaitc2.connect_to_server()
# Works (FILO)# await c2.cleanup()# await c1.cleanup()# Fails (FIFO)awaitc1.cleanup() # <-- boomawaitc2.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

ItemVersion
mcp1.6.0
Python3.12.10
anyio4.9.0
OSmacOS 14.4 (Apple Silicon)

Metadata

Metadata

Assignees

No one assigned

    Labels

    P1Significant bug affecting many users, highly requested featurebugSomething isn't workingready for workEnough information for someone to start working on

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions