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 |
⚠️ httpx → httpx2 for httpx_client_factory |
streamable_http_client |
⚠️ httpx → httpx2 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
- Wait for mcp v2.0.0 stable (planned 2026-07-28 per release notes — may already be available)
- Update
pyproject.toml: mcp>=2.0.0,<3.0.0
- 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)
- Add
httpx2 to nanobot's own dependencies if needed
- Run the test suite to confirm no regressions
The _OwnedMCPConnection isolation mechanism can stay — it is a solid defensive layer regardless of SDK version.
Background
When nanobot exits after an MCP stdio session, the following warning is printed to stderr:
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 ananyio.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 dedicatedasyncio.Taskso 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
mainstdio_clientmain(v2.x)The fix landed only in the v2.x line (
mainbranch). It was not backported tov1.xbecause 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.0tomcp>=2.0.0once 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:
ClientSessionStdioServerParametersstdio_clientsse_clienthttpx→httpx2forhttpx_client_factorystreamable_http_clienthttpx→httpx2forhttp_client=argmcp.typesmcp.shared.exceptions.McpErrorThe only required code change is replacing
import httpxwithimport httpx2in the SSE/streamable-HTTP client factory insidenanobot/agent/tools/mcp.py. Thehttpx2API is a drop-in replacement — only the import name changes.Additional v2 dependency changes
httpxandhttpx-sseare replaced byhttpx2as a hard dependency of mcp v2; nanobot currently depends onhttpxdirectly so it must either addhttpx2or rely on it transitivelyopentelemetry-api>=1.28.0becomes a new transitive dependency of mcp v2anyiofloor raised to>=4.9Suggested action
pyproject.toml:mcp>=2.0.0,<3.0.0nanobot/agent/tools/mcp.py, replaceimport httpxwithimport httpx2 as httpx(or update the two call-sites forsse_clientandstreamable_http_client)httpx2to nanobot's own dependencies if neededThe
_OwnedMCPConnectionisolation mechanism can stay — it is a solid defensive layer regardless of SDK version.