Description
This is originally found in issue#788, but after some debugging, I believe this related to how ClientSession is implemented.
import contextlib
async def sessions():
context1 = contextlib.AsyncExitStack()
client1 = stdio_client(server_params_list['filesystem'])
read1, write1 = await context1.enter_async_context(client1)
session1 = await context1.enter_async_context(ClientSession(read1, write1))
await session1.initialize()
tools1 = await session1.list_tools()
print(f"Session1 has {len(tools1.tools)} tools")
context2 = contextlib.AsyncExitStack()
client2 = sse_client(url = server_params_list['excel'].url)
read2, write2 = await context2.enter_async_context(client2)
session2 = await context2.enter_async_context(ClientSession(read2, write2))
await session2.initialize()
tools2 = await session2.list_tools()
print(f"Session2 has {len(tools2.tools)} tools")
await context1.aclose()
await context2.aclose()
This is actually the simplified way how ClientSessionGroup() did it. This code will except in "await context1.aclose()". Please note if change the order:
await context2.aclose()
await context1.aclose()
This will go through without exception.
After some further looking, the ClientSession is use BaseSession, iwhere n its aenter(), BaseSession created a Task Group (to run receive_loop), and try to aexit() the task group during BaseSession's aexit()
The Task Group, among creation, will bind a "cancel scope" with "current_task". So when second ClientSession created, a new "cancel scope 2" is binding the "current_task", replace the first one. When you try to tear down the session1 now, evantually it try to call task_group.aexit() which lead to CancelScope().exit(), there it find the current_task() is binding to a different cancel_scope, thus the runtime.
The code below, demostrate the task group issue (without any mcp releated code):
async def taskgroup():
import anyio
from contextlib import AsyncExitStack
async def task1(id):
print(f"Task {id} started")
await anyio.sleep(1)
print(f"Task {id} running...")
tg1 = anyio.create_task_group()
await tg1.__aenter__()
tg1.start_soon(task1, 1)
tg2 = anyio.create_task_group()
await tg2.__aenter__()
tg2.start_soon(task1, 2)
await anyio.sleep(3)
await tg1.__aexit__(None, None, None)
await tg2.__aexit__(None, None, None)
Maybe BaseSession could consider other way instead of use anyio.create_task_group()