"""Auto-reload MCP servers when a tool call fails due to server unavailability.""" from __future__ import annotations import logging from collections.abc import Awaitable, Callable logger = logging.getLogger(__name__) EmitFn = Callable[[int, str, str, dict | None], Awaitable[None]] async def call_tool_resilient( manager, server: str, tool: str, args: dict, *, task_id: int | None = None, emit: EmitFn | None = None, max_retries: int = 1, ) -> dict: """Call MCP tool; on server-unavailable errors, reload that server and retry once.""" last_exc: Exception | None = None for attempt in range(max_retries + 1): try: return await manager.call_tool(server, tool, args) except RuntimeError as exc: last_exc = exc msg = str(exc).lower() recoverable = "not available" in msg or "not loaded" in msg if not recoverable or attempt >= max_retries: raise logger.warning("MCP %s unavailable for %s — reloading (attempt %s)", server, tool, attempt + 1) if task_id and emit: await emit( task_id, "log", f"MCP '{server}' unavailable — reloading server and retrying {tool}…", {"server": server, "tool": tool}, ) try: await manager.reload_server(server) except Exception as reload_exc: # noqa: BLE001 logger.warning("MCP reload %s failed: %s", server, reload_exc) raise exc from reload_exc raise last_exc or RuntimeError(f"MCP call failed: {server}/{tool}")