Description
Describe the bug
When using streamablehttp_client
and ClientSession
with an AsyncExitStack
to manage their lifecycle outside of a standard async with
block, I encounter several warnings and RuntimeError
exceptions during teardown:
Connecting and setting up resources...
Exception ignored in: <async_generator object HTTP11ConnectionByteStream.__aiter__ at 0x1072b6d40>
Traceback (most recent call last):
File "/Users/jpuig/Development/git/examples/mcp_servers/.venv/lib/python3.13/site-packages/httpcore/_async/connection_pool.py", line 404, in __aiter__
yield part
RuntimeError: async generator ignored GeneratorExit
Connection successful. Session is ready.
Session is now available for use in the application.
[External function] Using the session...
Exception ignored in: <coroutine object HTTP11ConnectionByteStream.aclose at 0x107311a80>
Traceback (most recent call last):
File "/Users/jpuig/Development/git/examples/mcp_servers/.venv/lib/python3.13/site-packages/httpcore/_async/http11.py", line 348, in aclose
await self._connection._response_closed()
File "/Users/jpuig/Development/git/examples/mcp_servers/.venv/lib/python3.13/site-packages/httpcore/_async/http11.py", line 239, in _response_closed
async with self._state_lock:
File "/Users/jpuig/Development/git/examples/mcp_servers/.venv/lib/python3.13/site-packages/httpcore/_synchronization.py", line 77, in __aenter__
await self._anyio_lock.acquire()
File "/Users/jpuig/Development/git/examples/mcp_servers/.venv/lib/python3.13/site-packages/anyio/_backends/_asyncio.py", line 1799, in acquire
await AsyncIOBackend.cancel_shielded_checkpoint()
File "/Users/jpuig/Development/git/examples/mcp_servers/.venv/lib/python3.13/site-packages/anyio/_backends/_asyncio.py", line 2349, in cancel_shielded_checkpoint
with CancelScope(shield=True):
File "/Users/jpuig/Development/git/examples/mcp_servers/.venv/lib/python3.13/site-packages/anyio/_backends/_asyncio.py", line 457, in __exit__
raise RuntimeError(
RuntimeError: Attempted to exit cancel scope in a different task than it was entered in
The application continues to run and does not crash, but the error traces are silently shown and indicate improper cleanup or lifecycle management.
To Reproduce
Steps to reproduce the behavior:
- Create a class (e.g.
ManagedMcpSession
) that usesAsyncExitStack
to manage the lifecycle ofstreamablehttp_client
andClientSession
. - Manually call
connect()
andclose()
to setup and teardown the session. - Observe that errors occur during teardown after the session is used.
Minimal reproducible example:
from contextlib import AsyncExitStack
from mcp.client.streamable_http import streamablehttp_client
from mcp import ClientSession
import asyncio
class ManagedMcpSession:
def __init__(self, endpoint):
self._endpoint = endpoint
self._stack = AsyncExitStack()
self.session = None
async def connect(self):
read_stream, write_stream, _ = await self._stack.enter_async_context(
streamablehttp_client(self._endpoint)
)
self.session = await self._stack.enter_async_context(
ClientSession(read_stream, write_stream)
)
await self.session.initialize()
return self.session
async def close(self):
await self._stack.aclose()
async def use_the_session(session):
await session.call_tool("echo", {"message": "test"})
async def main():
manager = ManagedMcpSession("example/mcp")
try:
await manager.connect()
await use_the_session(manager.session)
finally:
await manager.close()
# asyncio.run(main())
Expected behavior
I expected the session to close cleanly without raising any warnings or RuntimeError
s. The goal is to encapsulate session management in a reusable class while still properly handling setup and teardown.
Screenshots
N/A – stack traces shown in terminal.
Desktop (please complete the following information):
- OS: macOS 13.6.5 (Apple M1 Pro)
- Python Version: 3.13.2
- mcp Version: 1.9.0
Smartphone (please complete the following information):
- N/A
Additional context
In the readme, the recommended usage pattern for streamablehttp_client
is through async with
, which works fine and does not emit these warnings:
from mcp.client.streamable_http import streamablehttp_client
from mcp import ClientSession
async def main():
# Connect to a streamable HTTP server
async with streamablehttp_client("example/mcp") as (
read_stream,
write_stream,
_,
):
# Create a session using the client streams
async with ClientSession(read_stream, write_stream) as session:
# Initialize the connection
await session.initialize()
# Call a tool
tool_result = await session.call_tool("echo", {"message": "hello"})
My use case requires persisting the session object and managing its lifecycle outside a single async with
block. Is it safe or recommended encapsulating the MCP session this way using AsyncExitStack
, or should a new session be opened for each operation?
I'm using the simple-streamablehttp MCP server for testing the code
Is there a better design pattern to manage long-lived or reusable MCP sessions while ensuring proper cleanup?
Metadata
Metadata
Assignees
Labels
Type
Projects
Status