Skip to content

Track mcp SDK v2 migration to fix stdio shutdown bugs: cancel-scope teardown error and stdout protocol pollution #5138

Description

@flyzstu

Background

When nanobot exits after an MCP stdio session, the following warning is printed to stderr:

an error occurred during closing of asynchronous generator <async_generator object stdio_client at 0x...>
asyncgen: <async_generator object stdio_client at 0x...>
  + Exception Group Traceback (most recent call last):
  | ...
  | BaseExceptionGroup: unhandled errors in a TaskGroup (1 sub-exception)
  +-+---------------- 1 ----------------
    | ...
    | GeneratorExit
    +------------------------------------
...
RuntimeError: Attempted to exit cancel scope in a different task than it was entered in

This is a known upstream bug: modelcontextprotocol/python-sdk#577.

Root cause

stdio_client (in mcp SDK ≤ 1.28.1) wraps its reader/writer in an anyio.create_task_group() inside an async generator. anyio enforces that a cancel scope must be exited from the same task that entered it. When the Python event loop shuts down and forcibly closes the async generator, this invariant is violated.

What nanobot already does

nanobot already mitigates this at the application layer via _OwnedMCPConnection (mcp.py L62-L76): each MCP connection is owned by a dedicated asyncio.Task so that the cancel scope is always exited from its creator task. This prevents crashes during normal teardown.

The residual warning comes from Python's own asyncgen finalizer running at interpreter shutdown — a layer below nanobot's error-handling reach.

Upstream status

PR Status Target branch Scope
python-sdk#2484 OPEN (0 reviews, 3 months) main Minimal FIFO-cleanup fix for stdio_client
python-sdk#2773 MERGED 2026-06-05 main (v2.x) Full stdio shutdown rewrite — fixes this issue as a side effect

The fix landed only in the v2.x line (main branch). It was not backported to v1.x because PR #2773 includes breaking changes (removed deprecated API, changed POSIX child-process kill policy). No v1.x-targeted backport PR exists.

What this issue tracks

Bump the nanobot MCP dependency from mcp>=1.26.0,<2.0.0 to mcp>=2.0.0 once the v2 stable release is available, which will pick up the stdio teardown fix.

v2 migration impact on nanobot

nanobot's MCP surface is narrow — only these APIs from the SDK are used:

API v2 status
ClientSession ✅ unchanged
StdioServerParameters ✅ unchanged
stdio_client ✅ unchanged (fixed)
sse_client ⚠️ httpxhttpx2 for httpx_client_factory
streamable_http_client ⚠️ httpxhttpx2 for http_client= arg
mcp.types ✅ unchanged
mcp.shared.exceptions.McpError ✅ unchanged

The only required code change is replacing import httpx with import httpx2 in the SSE/streamable-HTTP client factory inside nanobot/agent/tools/mcp.py. The httpx2 API is a drop-in replacement — only the import name changes.

Additional v2 dependency changes

  • httpx and httpx-sse are replaced by httpx2 as a hard dependency of mcp v2; nanobot currently depends on httpx directly so it must either add httpx2 or rely on it transitively
  • opentelemetry-api>=1.28.0 becomes a new transitive dependency of mcp v2
  • anyio floor raised to >=4.9

Suggested action

  1. Wait for mcp v2.0.0 stable (planned 2026-07-28 per release notes — may already be available)
  2. Update pyproject.toml: mcp>=2.0.0,<3.0.0
  3. In nanobot/agent/tools/mcp.py, replace import httpx with import httpx2 as httpx (or update the two call-sites for sse_client and streamable_http_client)
  4. Add httpx2 to nanobot's own dependencies if needed
  5. Run the test suite to confirm no regressions

The _OwnedMCPConnection isolation mechanism can stay — it is a solid defensive layer regardless of SDK version.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions