From 437ebf4e6eda3711575d71878a92e19b32992912 Mon Sep 17 00:00:00 2001 From: Re-bin Date: Sun, 22 Feb 2026 18:04:13 +0000 Subject: [PATCH] feat(mcp): make tool_timeout configurable per server via config --- README.md | 17 ++++++++++++++++- nanobot/agent/tools/mcp.py | 14 ++++++-------- nanobot/config/schema.py | 1 + 3 files changed, 23 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 99f9681..8399c45 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ ⚡️ Delivers core agent functionality in just **~4,000** lines of code — **99% smaller** than Clawdbot's 430k+ lines. -📏 Real-time line count: **3,806 lines** (run `bash core_agent_lines.sh` to verify anytime) +📏 Real-time line count: **3,862 lines** (run `bash core_agent_lines.sh` to verify anytime) ## 📢 News @@ -776,6 +776,21 @@ Two transport modes are supported: | **Stdio** | `command` + `args` | Local process via `npx` / `uvx` | | **HTTP** | `url` + `headers` (optional) | Remote endpoint (`https://mcp.example.com/sse`) | +Use `toolTimeout` to override the default 30s per-call timeout for slow servers: + +```json +{ + "tools": { + "mcpServers": { + "my-slow-server": { + "url": "https://example.com/mcp/", + "toolTimeout": 120 + } + } + } +} +``` + MCP tools are automatically discovered and registered on startup. The LLM can use them alongside built-in tools — no extra configuration needed. diff --git a/nanobot/agent/tools/mcp.py b/nanobot/agent/tools/mcp.py index a5e619a..0257d52 100644 --- a/nanobot/agent/tools/mcp.py +++ b/nanobot/agent/tools/mcp.py @@ -7,9 +7,6 @@ from typing import Any import httpx from loguru import logger -# Timeout for individual MCP tool calls (seconds). -MCP_TOOL_TIMEOUT = 30 - from nanobot.agent.tools.base import Tool from nanobot.agent.tools.registry import ToolRegistry @@ -17,12 +14,13 @@ from nanobot.agent.tools.registry import ToolRegistry class MCPToolWrapper(Tool): """Wraps a single MCP server tool as a nanobot Tool.""" - def __init__(self, session, server_name: str, tool_def): + def __init__(self, session, server_name: str, tool_def, tool_timeout: int = 30): self._session = session self._original_name = tool_def.name self._name = f"mcp_{server_name}_{tool_def.name}" self._description = tool_def.description or tool_def.name self._parameters = tool_def.inputSchema or {"type": "object", "properties": {}} + self._tool_timeout = tool_timeout @property def name(self) -> str: @@ -41,11 +39,11 @@ class MCPToolWrapper(Tool): try: result = await asyncio.wait_for( self._session.call_tool(self._original_name, arguments=kwargs), - timeout=MCP_TOOL_TIMEOUT, + timeout=self._tool_timeout, ) except asyncio.TimeoutError: - logger.warning("MCP tool '{}' timed out after {}s", self._name, MCP_TOOL_TIMEOUT) - return f"(MCP tool call timed out after {MCP_TOOL_TIMEOUT}s)" + 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)" parts = [] for block in result.content: if isinstance(block, types.TextContent): @@ -94,7 +92,7 @@ async def connect_mcp_servers( tools = await session.list_tools() for tool_def in tools.tools: - wrapper = MCPToolWrapper(session, name, tool_def) + wrapper = MCPToolWrapper(session, name, tool_def, tool_timeout=cfg.tool_timeout) registry.register(wrapper) logger.debug("MCP: registered tool '{}' from server '{}'", wrapper.name, name) diff --git a/nanobot/config/schema.py b/nanobot/config/schema.py index 966d11d..be36536 100644 --- a/nanobot/config/schema.py +++ b/nanobot/config/schema.py @@ -260,6 +260,7 @@ class MCPServerConfig(Base): env: dict[str, str] = Field(default_factory=dict) # Stdio: extra env vars url: str = "" # HTTP: streamable HTTP endpoint URL headers: dict[str, str] = Field(default_factory=dict) # HTTP: Custom HTTP Headers + tool_timeout: int = 30 # Seconds before a tool call is cancelled class ToolsConfig(Base):