Integrate MCP
Corresponding source
| Extension step | Source |
|---|---|
| MCP configuration model | nanobot/config/schema.py::MCPServerConfig |
| Connect, close, and runtime control | nanobot/agent/loop.py::_connect_mcp, close_mcp; nanobot/agent/tools/mcp.py |
| MCP Tool/Resource/Prompt wrappers | nanobot/agent/tools/mcp.py::MCPToolWrapper, MCPResourceWrapper, MCPPromptWrapper |
| Verification and reconnect | tests/agent/test_mcp_transient_retry.py, tests/tools/test_mcp_probe.py, tests/tools/test_mcp_tool.py |
| WebUI preset integration | nanobot/webui/mcp_presets_api.py, tests/webui/test_mcp_presets_api.py |
Learning objectives
After this lesson, you should be able to:
- decide when to configure MCP instead of writing a Tool;
- explain connection and shutdown boundaries for all three transports;
- predict nanobot names for MCP tools, resources, and prompts;
- configure a minimal allowlist and diagnose Windows, SSRF, and reconnection issues.
Prerequisites
- Know that an MCP server exposes tools, resources, and prompts to a client.
- Know that stdio communicates with a local subprocess, while HTTP/SSE communicates over a network.
- Understand ToolRegistry and AsyncExitStack.
Read the official MCP specification for the complete protocol. This lesson covers only nanobot’s client integration.
1. MCP’s place in nanobot
MCP does not change AgentRunner’s model-tool loop. After connection, each MCP capability is wrapped as an ordinary Tool and registered in the same ToolRegistry.
flowchart LR A["MCP server"] --> B["ClientSession"] B --> C["list_tools / resources / prompts"] C --> D["MCP*Wrapper"] D --> E["ToolRegistry"] E --> F["AgentRunner"]
Runner does not need to know whether a Tool is built-in Python or MCP-backed.
2. Configuration model
Current fields in nanobot/config/schema.py::MCPServerConfig:
| Field | Purpose |
|---|---|
type | stdio, sse, or streamableHttp; optional |
command / args / env / cwd | stdio server |
url / headers | HTTP/SSE server |
toolTimeout | timeout for one capability call; default 30 seconds |
enabledTools | names allowed to register; default ["*"] |
Configuration lives under tools.mcpServers.<server-name>.
Automatic inference:
commandpresent → stdio;urlwhose path ends with/sse→ SSE;- other URL → streamable HTTP.
Specify type when practical so endpoint-path changes do not silently change semantics.
3. Name wrapping
Current naming is approximately:
| MCP capability | nanobot Tool name |
|---|---|
tool read_file | mcp_<server>_read_file |
resource schema | mcp_<server>_resource_schema |
prompt review | mcp_<server>_prompt_review |
_sanitize_name() processes the server and capability names, keeps letters, digits, underscores, and hyphens accepted broadly by model Providers, and collapses repeated underscores.
enabledTools accepts:
- raw MCP tool name;
- wrapped nanobot name;
"*";- an empty list to register nothing.
An unknown allowlist item logs the available raw and wrapped names to make spelling errors visible.
4. Independent lifecycle per server
connect_mcp_servers() creates a separate AsyncExitStack for each server. MCP/AnyIO transports own cancel scopes, and sharing one stack among servers can create shutdown conflicts.
After a successful connection:
- establish the transport;
- create
ClientSession; - call
initialize(); - list capabilities;
- wrap and register them;
- store the stack.
AgentLoop.close_mcp() closes these stacks in the Task that owns their connection.
Failure of one server does not prevent other servers from connecting.
5. stdio trust boundary
An stdio configuration authorizes nanobot to start a local command:
{
"tools": {
"mcpServers": {
"example": {
"type": "stdio",
"command": "python",
"args": ["-m", "my_mcp_server"],
"enabledTools": ["read_data"]
}
}
}
}Review:
- whether the command/package is trusted;
- whether
envpasses unnecessary secrets; - whether
cwdexposes files the server should not read; - whether exposed capabilities are minimal;
- whether installation runs arbitrary postinstall code.
The SSRF guard does not apply to stdio because no HTTP request is involved. This does not make stdio safer; it has local-process permissions.
Windows
On Windows, npx, npm, pnpm, yarn, bunx, and .cmd/.bat launchers are normalized to:
cmd.exe /d /c <command> ...An existing exe/com or explicit cmd/PowerShell invocation is not wrapped twice. Related function:
_normalize_windows_stdio_command()
6. HTTP/SSE security boundary
Before connection, nanobot uses validate_url_target() to block:
- localhost/loopback;
- private networks;
- CGNAT;
- link-local;
- metadata endpoints.
An event Hook checks each outgoing HTTP request again, including redirect targets. If a trusted private MCP server is genuinely required, allow only the narrowest CIDR through tools.ssrfWhitelist, such as one host /32 or /128.
Do not allow an entire private network for convenience
MCP capabilities often read or modify data. A broad allowlist can turn prompt injection into private-network access.
7. Transient failure and reconnection
An MCP wrapper retries once for recognized transient connection failures such as a broken stream, broken pipe, connection reset, or connection refused.
If the retry exposes a terminated Session:
- unregister old Tools for the server;
- close the old stack;
- reconnect the server;
- register capabilities again;
- return an available wrapper.
This is not an unbounded retry for arbitrary exceptions. ValueError and ordinary business errors are not transient transport failures.
8. Choose MCP or a native Tool
Prefer MCP when:
- a stable MCP server already exists;
- several Agents/clients should reuse the capability;
- the capability releases independently from nanobot;
- the server owns a complex SDK and authentication.
Prefer a native Tool when:
- the logic is tiny and only serves nanobot;
- it must directly reuse ToolContext or workspace policy;
- an additional process/network lifecycle is undesirable;
- it needs tight integration with core checkpoints/events.
9. Minimum verification
- Configuration passes Pydantic.
- The server connects and lists capabilities.
enabledToolsregisters only expected names.- Argument schemas are normalized.
- Calls return correct results.
- Timeout is enforced.
- HTTP private targets and redirects are blocked.
- Disconnect triggers the expected retry/reconnect.
- Graceful shutdown leaves no process behind.
Source:
nanobot/agent/tools/mcp.pynanobot/config/schema.py::MCPServerConfignanobot/agent/loop.py::_connect_mcpnanobot/agent/loop.py::close_mcp
Tests:
tests/tools/test_mcp_tool.pytests/tools/test_mcp_probe.pytests/agent/test_mcp_transient_retry.pytests/webui/test_mcp_presets_api.py
Official configuration reference:
Self-check
- Why does an MCP Tool need no special AgentRunner branch?
- How do
enabledTools=[]and["*"]differ? - Why does the SSRF guard not protect an stdio server?
- Why does
npxneed acmd.exe /d /cwrapper on Windows?
Next: Add a Provider