diff --git a/nanobot/agent/tools/mcp.py b/nanobot/agent/tools/mcp.py index 2cbffd0..cf0a842 100644 --- a/nanobot/agent/tools/mcp.py +++ b/nanobot/agent/tools/mcp.py @@ -34,7 +34,7 @@ class MCPToolWrapper(Tool): def parameters(self) -> dict[str, Any]: return self._parameters - async def execute(self, **kwargs: Any) -> str: +async def execute(self, **kwargs: Any) -> str: from mcp import types try: result = await asyncio.wait_for( @@ -44,13 +44,24 @@ class MCPToolWrapper(Tool): except asyncio.TimeoutError: logger.warning("MCP tool '{}' timed out after {}s", self._name, self._tool_timeout) return f"(MCP tool call timed out after {self._tool_timeout}s)" + except asyncio.CancelledError: + # MCP SDK's anyio cancel scopes can leak CancelledError on timeout/failure. + # Re-raise only if our task was externally cancelled (e.g. /stop). + task = asyncio.current_task() + if task is not None and task.cancelling() > 0: + raise + logger.warning("MCP tool '{}' was cancelled by server/SDK", self._name) + return f"(MCP tool call was cancelled)" + except Exception as exc: + logger.warning("MCP tool '{}' failed: {}: {}", self._name, type(exc).__name__, exc) + return f"(MCP tool call failed: {type(exc).__name__})" parts = [] for block in result.content: if isinstance(block, types.TextContent): parts.append(block.text) else: parts.append(str(block)) - return "\n".join(parts) or "(no output)" + return "\n".join(parts) or "(no output) async def connect_mcp_servers(