From 9250f7df3f6cfb79d2225aa509b0f3993a7c62c5 Mon Sep 17 00:00:00 2001 From: Abdeltoto Date: Mon, 13 Apr 2026 21:58:06 -0400 Subject: [PATCH] fix(mcp): reset AsyncExitStack after cleanup for reconnect (#618) AsyncExitStack cannot be reused after aclose(). Second connect()/reconnect() must use a fresh stack. Clear server_initialize_result on teardown. Add regression test for connect->cleanup->connect->cleanup. Made-with: Cursor --- src/agents/mcp/server.py | 4 ++++ tests/mcp/test_connect_disconnect.py | 32 ++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/src/agents/mcp/server.py b/src/agents/mcp/server.py index b8c7a69d04..352ec5c7a3 100644 --- a/src/agents/mcp/server.py +++ b/src/agents/mcp/server.py @@ -1010,6 +1010,10 @@ async def cleanup(self): finally: self.session = None self._get_session_id = None + # AsyncExitStack cannot be reused after aclose(); reconnect() and second connect() + # need a fresh stack or enter_async_context will fail / leak resources (#618). + self.exit_stack = AsyncExitStack() + self.server_initialize_result = None class MCPServerStdioParams(TypedDict): diff --git a/tests/mcp/test_connect_disconnect.py b/tests/mcp/test_connect_disconnect.py index b001303974..ab25c8b2cc 100644 --- a/tests/mcp/test_connect_disconnect.py +++ b/tests/mcp/test_connect_disconnect.py @@ -67,3 +67,35 @@ async def test_manual_connect_disconnect_works( await server.cleanup() assert server.session is None, "Server should be disconnected" + + +@pytest.mark.asyncio +@patch("mcp.client.stdio.stdio_client", return_value=DummyStreamsContextManager()) +@patch("mcp.client.session.ClientSession.initialize", new_callable=AsyncMock, return_value=None) +@patch("mcp.client.session.ClientSession.list_tools") +async def test_connect_after_cleanup_uses_fresh_exit_stack( + mock_list_tools: AsyncMock, mock_initialize: AsyncMock, mock_stdio_client +): + """Reconnect must work: cleanup() closes AsyncExitStack, so connect() needs a new stack.""" + server = MCPServerStdio( + params={ + "command": tee, + }, + cache_tools_list=True, + ) + + tools = [ + MCPTool(name="tool1", inputSchema={}), + MCPTool(name="tool2", inputSchema={}), + ] + mock_list_tools.return_value = ListToolsResult(tools=tools) + + await server.connect() + assert server.session is not None + await server.cleanup() + assert server.session is None + + await server.connect() + assert server.session is not None + await server.cleanup() + assert server.session is None