diff --git a/navi/mcp/client.py b/navi/mcp/client.py index 4914bfd..fa67410 100644 --- a/navi/mcp/client.py +++ b/navi/mcp/client.py @@ -120,18 +120,23 @@ if not self._connected: return try: - await asyncio.wait_for(self._cleanup(), timeout=5.0) - except asyncio.TimeoutError: - logger.warning("MCP server %r disconnect timed out, forcing cleanup", self.name) - except (asyncio.CancelledError, RuntimeError): - # Graceful shutdown during app teardown — SSE transport teardown - # throws CancelledError / RuntimeError from anyio task scopes. + # Do NOT use asyncio.wait_for here — it spawns a new task, + # and MCP transports (stdio, sse, streamable_http) use anyio + # task groups that require __aexit__ in the same task as __aenter__. + await self._cleanup() + except asyncio.CancelledError: + # Graceful shutdown during app teardown. pass async def _cleanup(self) -> None: try: await self._exit_stack.aclose() - except Exception: + except SystemExit: + raise + except BaseException: + # Graceful shutdown noise from MCP transport task groups — + # anyio raises RuntimeError / BaseExceptionGroup when an async + # generator is closed from a different task than it was created in. pass finally: self._session = None