Merge PR #950: fix(mcp): add configurable timeout to MCP tool calls
This commit is contained in:
17
README.md
17
README.md
@@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
⚡️ Delivers core agent functionality in just **~4,000** lines of code — **99% smaller** than Clawdbot's 430k+ lines.
|
⚡️ 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
|
## 📢 News
|
||||||
|
|
||||||
@@ -776,6 +776,21 @@ Two transport modes are supported:
|
|||||||
| **Stdio** | `command` + `args` | Local process via `npx` / `uvx` |
|
| **Stdio** | `command` + `args` | Local process via `npx` / `uvx` |
|
||||||
| **HTTP** | `url` + `headers` (optional) | Remote endpoint (`https://mcp.example.com/sse`) |
|
| **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.
|
MCP tools are automatically discovered and registered on startup. The LLM can use them alongside built-in tools — no extra configuration needed.
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
"""MCP client: connects to MCP servers and wraps their tools as native nanobot tools."""
|
"""MCP client: connects to MCP servers and wraps their tools as native nanobot tools."""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
from contextlib import AsyncExitStack
|
from contextlib import AsyncExitStack
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
@@ -13,12 +14,13 @@ from nanobot.agent.tools.registry import ToolRegistry
|
|||||||
class MCPToolWrapper(Tool):
|
class MCPToolWrapper(Tool):
|
||||||
"""Wraps a single MCP server tool as a nanobot 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._session = session
|
||||||
self._original_name = tool_def.name
|
self._original_name = tool_def.name
|
||||||
self._name = f"mcp_{server_name}_{tool_def.name}"
|
self._name = f"mcp_{server_name}_{tool_def.name}"
|
||||||
self._description = tool_def.description or tool_def.name
|
self._description = tool_def.description or tool_def.name
|
||||||
self._parameters = tool_def.inputSchema or {"type": "object", "properties": {}}
|
self._parameters = tool_def.inputSchema or {"type": "object", "properties": {}}
|
||||||
|
self._tool_timeout = tool_timeout
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self) -> str:
|
def name(self) -> str:
|
||||||
@@ -34,7 +36,14 @@ class MCPToolWrapper(Tool):
|
|||||||
|
|
||||||
async def execute(self, **kwargs: Any) -> str:
|
async def execute(self, **kwargs: Any) -> str:
|
||||||
from mcp import types
|
from mcp import types
|
||||||
result = await self._session.call_tool(self._original_name, arguments=kwargs)
|
try:
|
||||||
|
result = await asyncio.wait_for(
|
||||||
|
self._session.call_tool(self._original_name, arguments=kwargs),
|
||||||
|
timeout=self._tool_timeout,
|
||||||
|
)
|
||||||
|
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)"
|
||||||
parts = []
|
parts = []
|
||||||
for block in result.content:
|
for block in result.content:
|
||||||
if isinstance(block, types.TextContent):
|
if isinstance(block, types.TextContent):
|
||||||
@@ -83,7 +92,7 @@ async def connect_mcp_servers(
|
|||||||
|
|
||||||
tools = await session.list_tools()
|
tools = await session.list_tools()
|
||||||
for tool_def in tools.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)
|
registry.register(wrapper)
|
||||||
logger.debug("MCP: registered tool '{}' from server '{}'", wrapper.name, name)
|
logger.debug("MCP: registered tool '{}' from server '{}'", wrapper.name, name)
|
||||||
|
|
||||||
|
|||||||
@@ -260,6 +260,7 @@ class MCPServerConfig(Base):
|
|||||||
env: dict[str, str] = Field(default_factory=dict) # Stdio: extra env vars
|
env: dict[str, str] = Field(default_factory=dict) # Stdio: extra env vars
|
||||||
url: str = "" # HTTP: streamable HTTP endpoint URL
|
url: str = "" # HTTP: streamable HTTP endpoint URL
|
||||||
headers: dict[str, str] = Field(default_factory=dict) # HTTP: Custom HTTP Headers
|
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):
|
class ToolsConfig(Base):
|
||||||
|
|||||||
Reference in New Issue
Block a user