Skip to content

AsyncExitStack fails to close at the first attempt #1213

@Muhammad-Mouta

Description

@Muhammad-Mouta

Initial Checks

Description

We use the Python MCP SDK to integrate MCP into our codebase.

We connect to the remote Tavily MCP server using Streamable HTTP transport.

We follow the exit_stack pattern to initialize the read/write streams and the ClientSession, which works fine. However, when trying to close the exit_stack, the first attempt almost always fails, so we made a workaround to try to close it twice, as shown in the following code (this is a portion of our MCP client wrapper):

async def _connect(self):
    read_stream, write_stream = await self._get_read_and_write_streams()
    await self._create_and_initialize_session(read_stream, write_stream)

async def _get_read_and_write_streams(self):
    read_stream, write_stream, _ = await self._exit_stack.enter_async_context(
        streamablehttp_client(
            self._config.url,
            self._config.headers,
        )
    )
    return read_stream, write_stream

async def _create_and_initialize_session(
    self,
    read_stream: MemoryObjectReceiveStream[SessionMessage | Exception],
    write_stream: MemoryObjectSendStream[SessionMessage],
) -> None:
    self._session = await self._exit_stack.enter_async_context(
        ClientSession(
            read_stream,
            write_stream,
            timedelta(seconds=self._config.connection_attempt_timeout),
        )
    )
    await self._session.initialize()

async def _cleanup_exit_stack(self):
    try:
        await self._exit_stack.aclose()
    except (Exception, asyncio.CancelledError):
        logger.exception(
            self._make_error_message(
                "The first attempt to close the exit stack failed. Trying to close it again ..."
            )
        )
        try:
            await self._exit_stack.aclose()
        except (Exception, asyncio.CancelledError):
            logger.exception(
                self._make_error_message(
                    f"Failed to clean up the exit stack. Replacing with a new exit stack ..."
                )
            )
            self._exit_stack = AsyncExitStack()

Our MCP client wrapper tries to connect to the server, and if it fails, it cleans up the exit stack to allow subsequent connection trials. So, I disabled my internet connection and tried to connect to the remote Tavily MCP server, which fails and triggers the _cleanup_exit_stack method.

Here is a full log trace for what happened, which should help understand why the first attempt fails.

[07/29/25 14:51:28] DEBUG    Connecting to StreamableHTTP endpoint: https://mcp.tavily.com/mcp/?tavilyApiKey=******************************************                             streamable_http.py:476
                    DEBUG    Sending client message: root=JSONRPCRequest(method='initialize', params={'protocolVersion': '2025-06-18', 'capabilities': {}, 'clientInfo': {'name':   streamable_http.py:385
                             'mcp', 'version': '0.1.0'}}, jsonrpc='2.0', id=0)                                                                                                                 main.py:265
                    DEBUG    connect_tcp.started host='mcp.tavily.com' port=443 local_address=None timeout=30 socket_options=None                                                             _trace.py:87
                    DEBUG    connect_tcp.failed exception=ConnectError(gaierror(11001, 'getaddrinfo failed'))                                                                                 _trace.py:87
                    ERROR    Error in MCP client (tavily_mcp): The first attempt to close the exit stack failed. Trying to close it again ...                                           base_client.py:242
                             ╭────────────────────────────────────────────────────────── Traceback (most recent call last) ───────────────────────────────────────────────────────────╮
                             │ C:************************************************************************************************\base_client.py:240 in _cleanup_exit_stack           │
                             │                                                                                                                                                        │
                             │   237 │                                                                                                                                                │
                             │   238 │   async def _cleanup_exit_stack(self):                                                                                                         │
                             │   239 │   │   try:                                                                                                                                     │
                             │ ❱ 240 │   │   │   await self._exit_stack.aclose()                                                                                                      │
                             │   241 │   │   except (Exception, asyncio.CancelledError):                                                                                              │
                             │   242 │   │   │   logger.exception(                                                                                                                    │
                             │   243 │   │   │   │   self._make_error_message(                                                                                                        │
                             │                                                                                                                                                        │
                             │ C:\*********************************************************************************************\Lib\contextlib.py:696 in aclose                       │
                             │                                                                                                                                                        │
                             │   693 │                                                                                                                                                │
                             │   694 │   async def aclose(self):                                                                                                                      │
                             │   695 │   │   """Immediately unwind the context stack."""                                                                                              │
                             │ ❱ 696 │   │   await self.__aexit__(None, None, None)                                                                                                   │
                             │   697 │                                                                                                                                                │
                             │   698 │   def _push_async_cm_exit(self, cm, cm_exit):                                                                                                  │
                             │   699 │   │   """Helper to correctly register coroutine function to __aexit__                                                                          │
                             │                                                                                                                                                        │
                             │ C:\*********************************************************************************************\Lib\contextlib.py:754 in __aexit__                    │
                             │                                                                                                                                                        │
                             │   751 │   │   │   │   # bare "raise exc_details[1]" replaces our carefully                                                                             │
                             │   752 │   │   │   │   # set-up context                                                                                                                 │
                             │   753 │   │   │   │   fixed_ctx = exc_details[1].__context__                                                                                           │
                             │ ❱ 754 │   │   │   │   raise exc_details[1]                                                                                                             │
                             │   755 │   │   │   except BaseException:                                                                                                                │
                             │   756 │   │   │   │   exc_details[1].__context__ = fixed_ctx                                                                                           │
                             │   757 │   │   │   │   raise                                                                                                                            │
                             │                                                                                                                                                        │
                             │ C:\*********************************************************************************************\Lib\contextlib.py:737 in __aexit__                    │
                             │                                                                                                                                                        │
                             │   734 │   │   │   │   if is_sync:                                                                                                                      │
                             │   735 │   │   │   │   │   cb_suppress = cb(*exc_details)                                                                                               │
                             │   736 │   │   │   │   else:                                                                                                                            │
                             │ ❱ 737 │   │   │   │   │   cb_suppress = await cb(*exc_details)                                                                                         │
                             │   738 │   │   │   │                                                                                                                                    │
                             │   739 │   │   │   │   if cb_suppress:                                                                                                                  │
                             │   740 │   │   │   │   │   suppressed_exc = True                                                                                                        │
                             │                                                                                                                                                        │
                             │ C:\*********************************************************************************************\Lib\contextlib.py:231 in __aexit__                    │
                             │                                                                                                                                                        │
                             │   228 │   │   │   │   # tell if we get the same exception back                                                                                         │
                             │   229 │   │   │   │   value = typ()                                                                                                                    │
                             │   230 │   │   │   try:                                                                                                                                 │
                             │ ❱ 231 │   │   │   │   await self.gen.athrow(value)                                                                                                     │
                             │   232 │   │   │   except StopAsyncIteration as exc:                                                                                                    │
                             │   233 │   │   │   │   # Suppress StopIteration *unless* it's the same exception that                                                                   │
                             │   234 │   │   │   │   # was passed to throw().  This prevents a StopIteration                                                                          │
                             │                                                                                                                                                        │
                             │ C:\********************************\.venv\Lib\site-packages\mcp\client\streamable_http.py:474 in streamablehttp_client                                 │
                             │                                                                                                                                                        │
                             │   471 │   read_stream_writer, read_stream = anyio.create_memory_object_stream[SessionMessage |                                                         │
                             │       Exception](0)                                                                                                                                    │
                             │   472 │   write_stream, write_stream_reader =                                                                                                          │
                             │       anyio.create_memory_object_stream[SessionMessage](0)                                                                                             │
                             │   473 │                                                                                                                                                │
                             │ ❱ 474 │   async with anyio.create_task_group() as tg:                                                                                                  │
                             │   475 │   │   try:                                                                                                                                     │
                             │   476 │   │   │   logger.debug(f"Connecting to StreamableHTTP endpoint: {url}")                                                                        │
                             │   477                                                                                                                                                  │
                             │                                                                                                                                                        │
                             │ C:\********************************\.venv\Lib\site-packages\anyio\_backends\_asyncio.py:772 in __aexit__                                               │
                             │                                                                                                                                                        │
                             │    769 │   │   │   │   │   # added to self._exceptions so it's ok to break exception                                                                   │
                             │    770 │   │   │   │   │   # chaining and avoid adding a "During handling of above..."
                             │    771 │   │   │   │   │   # for each nesting level.                                                                                                   │
                             │ ❱  772 │   │   │   │   │   raise BaseExceptionGroup(                                                                                                   │
                             │    773 │   │   │   │   │   │   "unhandled errors in a TaskGroup", self._exceptions                                                                     │
                             │    774 │   │   │   │   │   ) from None                                                                                                                 │
                             │    775 │   │   │   │   elif exc_val:                                                                                                                   │
                             ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
                             ExceptionGroup: unhandled errors in a TaskGroup (1 sub-exception)

                             ╭─────────────────────────────────────────────────────────────────── Sub-exception #1 ───────────────────────────────────────────────────────────────────╮
                             │ ╭──────────────────────────────────────────────────────── Traceback (most recent call last) ─────────────────────────────────────────────────────────╮ │
                             │ │ C:\*********************************************************************************************\Lib\asyncio\tasks.py:316 in                       │ │
                             │ │ __step_run_and_handle_result                                                                                                                       │ │
                             │ │                                                                                                                                                    │ │
                             │ │    313 │   │   │   │   # don't have `__iter__` and `__next__` methods.                                                                             │ │
                             │ │    314 │   │   │   │   result = coro.send(None)                                                                                                    │ │
                             │ │    315 │   │   │   else:                                                                                                                           │ │
                             │ │ ❱  316 │   │   │   │   result = coro.throw(exc)                                                                                                    │ │
                             │ │    317 │   │   except StopIteration as exc:                                                                                                        │ │
                             │ │    318 │   │   │   if self._must_cancel:                                                                                                           │ │
                             │ │    319 │   │   │   │   # Task is cancelled right before coro stops.                                                                                │ │
                             │ │                                                                                                                                                    │ │
                             │ │ C:\********************************\.venv\Lib\site-packages\mcp\client\streamable_http.py:405 in handle_request_async                              │ │
                             │ │                                                                                                                                                    │ │
                             │ │   402 │   │   │   │   │   │   if is_resumption:                                                                                                    │ │
                             │ │   403 │   │   │   │   │   │   │   await self._handle_resumption_request(ctx)                                                                       │ │
                             │ │   404 │   │   │   │   │   │   else:                                                                                                                │ │
                             │ │ ❱ 405 │   │   │   │   │   │   │   await self._handle_post_request(ctx)                                                                             │ │
                             │ │   406 │   │   │   │   │                                                                                                                            │ │
                             │ │   407 │   │   │   │   │   # If this is a request, start a new task to handle it                                                                    │ │
                             │ │   408 │   │   │   │   │   if isinstance(message.root, JSONRPCRequest):                                                                             │ │
                             │ │                                                                                                                                                    │ │
                             │ │ C:\********************************\.venv\Lib\site-packages\mcp\client\streamable_http.py:259 in _handle_post_request                              │ │
                             │ │                                                                                                                                                    │ │
                             │ │   256 │   │   message = ctx.session_message.message                                                                                                │ │
                             │ │   257 │   │   is_initialization = self._is_initialization_request(message)                                                                         │ │
                             │ │   258 │   │                                                                                                                                        │ │
                             │ │ ❱ 259 │   │   async with ctx.client.stream(                                                                                                        │ │
                             │ │   260 │   │   │   "POST",                                                                                                                          │ │
                             │ │   261 │   │   │   self.url,                                                                                                                        │ │
                             │ │   262 │   │   │   json=message.model_dump(by_alias=True, mode="json", exclude_none=True),                                                          │ │
                             │ │                                                                                                                                                    │ │
                             │ │ C:\*********************************************************************************************\Lib\contextlib.py:210 in __aenter__               │ │
                             │ │                                                                                                                                                    │ │
                             │ │   207 │   │   # they are only needed for recreation, which is not possible anymore                                                                 │ │
                             │ │   208 │   │   del self.args, self.kwds, self.func                                                                                                  │ │
                             │ │   209 │   │   try:                                                                                                                                 │ │
                             │ │ ❱ 210 │   │   │   return await anext(self.gen)                                                                                                     │ │
                             │ │   211 │   │   except StopAsyncIteration:                                                                                                           │ │
                             │ │   212 │   │   │   raise RuntimeError("generator didn't yield") from None                                                                           │ │
                             │ │   213                                                                                                                                              │ │
                             │ │                                                                                                                                                    │ │
                             │ │ C:\********************************\.venv\Lib\site-packages\httpx\_client.py:1583 in stream                                                        │ │
                             │ │                                                                                                                                                    │ │
                             │ │   1580 │   │   │   timeout=timeout,                                                                                                                │ │
                             │ │   1581 │   │   │   extensions=extensions,                                                                                                          │ │
                             │ │   1582 │   │   )                                                                                                                                   │ │
                             │ │ ❱ 1583 │   │   response = await self.send(                                                                                                         │ │
                             │ │   1584 │   │   │   request=request,                                                                                                                │ │
                             │ │   1585 │   │   │   auth=auth,                                                                                                                      │ │
                             │ │   1586 │   │   │   follow_redirects=follow_redirects,                                                                                              │ │
                             │ │                                                                                                                                                    │ │
                             │ │ C:\********************************\.venv\Lib\site-packages\httpx\_client.py:1629 in send                                                          │ │
                             │ │                                                                                                                                                    │ │
                             │ │   1626 │   │                                                                                                                                       │ │
                             │ │   1627 │   │   auth = self._build_request_auth(request, auth)                                                                                      │ │
                             │ │   1628 │   │                                                                                                                                       │ │
                             │ │ ❱ 1629 │   │   response = await self._send_handling_auth(                                                                                          │ │
                             │ │   1630 │   │   │   request,                                                                                                                        │ │
                             │ │   1631 │   │   │   auth=auth,                                                                                                                      │ │
                             │ │   1632 │   │   │   follow_redirects=follow_redirects,                                                                                              │ │
                             │ │                                                                                                                                                    │ │
                             │ │ C:\********************************\.venv\Lib\site-packages\httpx\_client.py:1657 in _send_handling_auth                                           │ │
                             │ │                                                                                                                                                    │ │
                             │ │   1654 │   │   │   request = await auth_flow.__anext__()                                                                                           │ │
                             │ │   1655 │   │   │                                                                                                                                   │ │
                             │ │   1656 │   │   │   while True:                                                                                                                     │ │
                             │ │ ❱ 1657 │   │   │   │   response = await self._send_handling_redirects(                                                                             │ │
                             │ │   1658 │   │   │   │   │   request,                                                                                                                │ │
                             │ │   1659 │   │   │   │   │   follow_redirects=follow_redirects,                                                                                      │ │
                             │ │   1660 │   │   │   │   │   history=history,                                                                                                        │ │
                             │ │                                                                                                                                                    │ │
                             │ │ C:\********************************\.venv\Lib\site-packages\httpx\_client.py:1694 in _send_handling_redirects                                      │ │
                             │ │                                                                                                                                                    │ │
                             │ │   1691 │   │   │   for hook in self._event_hooks["request"]:                                                                                       │ │
                             │ │   1692 │   │   │   │   await hook(request)                                                                                                         │ │
                             │ │   1693 │   │   │                                                                                                                                   │ │
                             │ │ ❱ 1694 │   │   │   response = await self._send_single_request(request)                                                                             │ │
                             │ │   1695 │   │   │   try:                                                                                                                            │ │
                             │ │   1696 │   │   │   │   for hook in self._event_hooks["response"]:                                                                                  │ │
                             │ │   1697 │   │   │   │   │   await hook(response)                                                                                                    │ │
                             │ │                                                                                                                                                    │ │
                             │ │ C:\********************************\.venv\Lib\site-packages\httpx\_client.py:1730 in _send_single_request                                          │ │
                             │ │                                                                                                                                                    │ │
                             │ │   1727 │   │   │   )                                                                                                                               │ │
                             │ │   1728 │   │                                                                                                                                       │ │
                             │ │   1729 │   │   with request_context(request=request):                                                                                              │ │
                             │ │ ❱ 1730 │   │   │   response = await transport.handle_async_request(request)                                                                        │ │
                             │ │   1731 │   │                                                                                                                                       │ │
                             │ │   1732 │   │   assert isinstance(response.stream, AsyncByteStream)                                                                                 │ │
                             │ │   1733 │   │   response.request = request                                                                                                          │ │
                             │ │                                                                                                                                                    │ │
                             │ │ C:\********************************\.venv\Lib\site-packages\httpx\_transports\default.py:393 in handle_async_request                               │ │
                             │ │                                                                                                                                                    │ │
                             │ │   390 │   │   │   content=request.stream,                                                                                                          │ │
                             │ │   391 │   │   │   extensions=request.extensions,                                                                                                   │ │
                             │ │   392 │   │   )                                                                                                                                    │ │
                             │ │ ❱ 393 │   │   with map_httpcore_exceptions():                                                                                                      │ │
                             │ │   394 │   │   │   resp = await self._pool.handle_async_request(req)                                                                                │ │
                             │ │   395 │   │                                                                                                                                        │ │
                             │ │   396 │   │   assert isinstance(resp.stream, typing.AsyncIterable)                                                                                 │ │
                             │ │                                                                                                                                                    │ │
                             │ │ C:\*********************************************************************************************\Lib\contextlib.py:158 in __exit__                 │ │
                             │ │                                                                                                                                                    │ │
                             │ │   155 │   │   │   │   # tell if we get the same exception back                                                                                     │ │
                             │ │   156 │   │   │   │   value = typ()                                                                                                                │ │
                             │ │   157 │   │   │   try:                                                                                                                             │ │
                             │ │ ❱ 158 │   │   │   │   self.gen.throw(value)                                                                                                        │ │
                             │ │   159 │   │   │   except StopIteration as exc:                                                                                                     │ │
                             │ │   160 │   │   │   │   # Suppress StopIteration *unless* it's the same exception that                                                               │ │
                             │ │   161 │   │   │   │   # was passed to throw().  This prevents a StopIteration                                                                      │ │
                             │ │                                                                                                                                                    │ │
                             │ │ C:\********************************\.venv\Lib\site-packages\httpx\_transports\default.py:118 in map_httpcore_exceptions                            │ │
                             │ │                                                                                                                                                    │ │
                             │ │   115 │   │   │   raise                                                                                                                            │ │
                             │ │   116 │   │                                                                                                                                        │ │
                             │ │   117 │   │   message = str(exc)                                                                                                                   │ │
                             │ │ ❱ 118 │   │   raise mapped_exc(message) from exc                                                                                                   │ │
                             │ │   119                                                                                                                                              │ │
                             │ │   120                                                                                                                                              │ │
                             │ │   121 class ResponseStream(SyncByteStream):                                                                                                        │ │
                             │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ │
                             │ ConnectError: [Errno 11001] getaddrinfo failed                                                                                                         │
                             ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯

From my understanding, I think the error happens because the httpx client is waiting for stream chunks from the server, and then we try to close it.

I think this should be handled on the MCP SDK level so that trying the exit_stack twice is not necessary.

Example Code

Python & MCP Python SDK

Python: 3.12.10
MCP: 1.12.2

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingneeds reproneeds additional information to be able to reproduce bug

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions