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()
This is originally found in issue#788, but after some debugging, I believe this related to how ClientSession is implemented.
This is actually the simplified way how ClientSessionGroup() did it. This code will except in "await context1.aclose()". Please note if change the order:
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):
Maybe BaseSession could consider other way instead of use anyio.create_task_group()