MCP resource/prompt/tool names containing spaces or special characters
(e.g. "PostgreSQL System Information") were forwarded verbatim to model
provider APIs, causing validation errors from both Anthropic and OpenAI
which require names matching ^[a-zA-Z0-9_-]{1,128}$.
Add _sanitize_name() that replaces invalid characters with underscores
and collapses consecutive underscores. Applied in MCPToolWrapper,
MCPResourceWrapper, MCPPromptWrapper constructors and the enabled_tools
filtering logic.
Closes#3468
When an MCP server restarts or a network connection drops between
tool calls, the existing session throws ClosedResourceError,
BrokenPipeError, ConnectionResetError, etc. Currently these are
caught as generic exceptions and returned as permanent failures
to the LLM, which then tells the user 'my tools are broken.'
This change adds a single automatic retry with a 1-second backoff
for transient connection-class errors in MCPToolWrapper,
MCPResourceWrapper, and MCPPromptWrapper. Non-transient errors
(ValueError, RuntimeError, McpError, etc.) are not retried.
The retry is conservative:
- Only 1 retry (not configurable, to keep the change minimal)
- Only for a specific set of connection-class exceptions
- Matched by exception class name to avoid importing anyio/etc.
- 1s sleep between attempts to allow the server to recover
- Clear logging distinguishes retried vs permanent failures
In production this eliminates most 'MCP tool call failed:
ClosedResourceError' noise when MCP bridge processes restart
(e.g. after config changes or OOM kills).
Tests: 22 new tests covering retry, exhaustion, non-transient
bypass, timeout bypass, and all three wrapper types.
Each MCP server now connects in its own asyncio.Task to isolate anyio
cancel scopes and prevent 'exit cancel scope in different task' errors
when multiple servers (especially mixed transport types) are configured.
Changes:
- connect_mcp_servers() returns dict[str, AsyncExitStack] instead of None
- Each server runs in separate task via asyncio.gather()
- AgentLoop uses _mcp_stacks dict to track per-server stacks
- Tests updated to handle new API
- Propagate `description` from MCP prompt arguments into the JSON
Schema so LLMs can better understand prompt parameters.
- Align generic-exception error message with tool/resource wrappers
(drop redundant `{exc}` detail).
- Extend test fixture to mock `mcp.shared.exceptions.McpError`.
- Add tests for argument description forwarding and McpError handling.
Made-with: Cursor
Add MCPResourceWrapper and MCPPromptWrapper classes that expose MCP
server resources and prompts as nanobot tools. Resources are read-only
tools that fetch content by URI, and prompts are read-only tools that
return filled prompt templates with optional arguments.
- MCPResourceWrapper: reads resource content (text and binary) via URI
- MCPPromptWrapper: gets prompt templates with typed arguments
- Both handle timeouts, cancellation, and MCP SDK 1.x error types
- Resources and prompts are registered during server connection
- Gracefully handles servers that don't support resources/prompts
Only normalize nullable MCP tool schemas for OpenAI-compatible providers so optional params still work without collapsing unrelated unions. Also teach local validation to honor nullable flags and add regression coverage for nullable and non-nullable schemas.
Made-with: Cursor
MCP SDK's anyio cancel scopes can leak CancelledError on timeout or
failure paths. Since CancelledError is a BaseException (not Exception),
it escapes both MCPToolWrapper.execute() and ToolRegistry.execute(),
crashing the agent loop.
Now catches CancelledError and returns a graceful error to the LLM,
while still re-raising genuine task cancellations from /stop.
Also catches general Exception for other MCP failures (connection
drops, invalid responses, etc.).
Related: #1055
Always provide an explicit httpx client to prevent MCP HTTP transport from inheriting httpx's default 5-second timeout, thereby avoiding conflicts with the upper layer tool's timeout settings.