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
| Item | Version |
|---|
| mcp | 1.6.0 |
| Python | 3.12.10 |
| anyio | 4.9.0 |
| OS | macOS 14.4 (Apple Silicon) |
Describe the bug
If two
MCPClientobjects 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 ofRuntimeError/CancelledErrorexceptions coming fromanyioandmcp.client.stdio.To Reproduce
Minimal repro:
Expected behavior
cleanup()should succeed regardless of the order in which multipleMCPClientinstances 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
Environment