Skip to content

Errors during cleanup when using streamablehttp_client with AsyncExitStack instead of async with #831

Closed
@joanpuigsanz

Description

@joanpuigsanz

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:

  1. Create a class (e.g. ManagedMcpSession) that uses AsyncExitStack to manage the lifecycle of streamablehttp_client and ClientSession.
  2. Manually call connect() and close() to setup and teardown the session.
  3. 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 RuntimeErrors. 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

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    Status

    Done

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions