Skip to content

Commit 9250f7d

Browse files
committed
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
1 parent 86739b1 commit 9250f7d

2 files changed

Lines changed: 36 additions & 0 deletions

File tree

src/agents/mcp/server.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1010,6 +1010,10 @@ async def cleanup(self):
10101010
finally:
10111011
self.session = None
10121012
self._get_session_id = None
1013+
# AsyncExitStack cannot be reused after aclose(); reconnect() and second connect()
1014+
# need a fresh stack or enter_async_context will fail / leak resources (#618).
1015+
self.exit_stack = AsyncExitStack()
1016+
self.server_initialize_result = None
10131017

10141018

10151019
class MCPServerStdioParams(TypedDict):

tests/mcp/test_connect_disconnect.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,3 +67,35 @@ async def test_manual_connect_disconnect_works(
6767

6868
await server.cleanup()
6969
assert server.session is None, "Server should be disconnected"
70+
71+
72+
@pytest.mark.asyncio
73+
@patch("mcp.client.stdio.stdio_client", return_value=DummyStreamsContextManager())
74+
@patch("mcp.client.session.ClientSession.initialize", new_callable=AsyncMock, return_value=None)
75+
@patch("mcp.client.session.ClientSession.list_tools")
76+
async def test_connect_after_cleanup_uses_fresh_exit_stack(
77+
mock_list_tools: AsyncMock, mock_initialize: AsyncMock, mock_stdio_client
78+
):
79+
"""Reconnect must work: cleanup() closes AsyncExitStack, so connect() needs a new stack."""
80+
server = MCPServerStdio(
81+
params={
82+
"command": tee,
83+
},
84+
cache_tools_list=True,
85+
)
86+
87+
tools = [
88+
MCPTool(name="tool1", inputSchema={}),
89+
MCPTool(name="tool2", inputSchema={}),
90+
]
91+
mock_list_tools.return_value = ListToolsResult(tools=tools)
92+
93+
await server.connect()
94+
assert server.session is not None
95+
await server.cleanup()
96+
assert server.session is None
97+
98+
await server.connect()
99+
assert server.session is not None
100+
await server.cleanup()
101+
assert server.session is None

0 commit comments

Comments
 (0)