fix(sdk): close MCP connections from Nanobot facade

The SDK opened MCP connections through AgentLoop.process_direct but
never called close_mcp, leaving stdio MCP generators to be finalized
during asyncio shutdown from a different task, producing a RuntimeError
about exiting a cancel scope in a different task.

Add aclose() that delegates to AgentLoop.close_mcp (which already
drains background tasks and closes MCP stacks), plus __aenter__ and
__aexit__ so the SDK works as an async context manager.

Fixes #4211
This commit is contained in:
axelray-dev 2026-06-06 07:50:39 +08:00 committed by Xubin Ren
parent 6a0368b32f
commit 57fa37dcfe
2 changed files with 46 additions and 0 deletions

View File

@ -101,4 +101,13 @@ class Nanobot:
messages=capture.messages, messages=capture.messages,
) )
async def aclose(self) -> None:
"""Release resources held by this instance (MCP connections, etc.)."""
await self._loop.close_mcp()
async def __aenter__(self) -> Nanobot:
return self
async def __aexit__(self, *exc: object) -> None:
await self.aclose()

View File

@ -326,3 +326,40 @@ async def test_sdk_capture_prefers_run_level_snapshot():
assert hook.tools_used == ["read_file"] assert hook.tools_used == ["read_file"]
assert hook.messages == final_messages assert hook.messages == final_messages
@pytest.mark.asyncio
async def test_aclose_delegates_to_loop_close_mcp(tmp_path):
config_path = _write_config(tmp_path)
bot = Nanobot.from_config(config_path, workspace=tmp_path)
bot._loop.close_mcp = AsyncMock()
await bot.aclose()
bot._loop.close_mcp.assert_awaited_once()
@pytest.mark.asyncio
async def test_context_manager_calls_aclose_on_exit(tmp_path):
config_path = _write_config(tmp_path)
bot = Nanobot.from_config(config_path, workspace=tmp_path)
bot._loop.close_mcp = AsyncMock()
async with bot as b:
assert b is bot
bot._loop.close_mcp.assert_awaited_once()
@pytest.mark.asyncio
async def test_context_manager_does_not_swallow_exceptions(tmp_path):
config_path = _write_config(tmp_path)
bot = Nanobot.from_config(config_path, workspace=tmp_path)
bot._loop.close_mcp = AsyncMock()
with pytest.raises(ValueError):
async with bot as b:
assert b is bot
raise ValueError("boom")
bot._loop.close_mcp.assert_awaited_once()