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