Integrate MCP

Corresponding source

Extension stepSource
MCP configuration modelnanobot/config/schema.py::MCPServerConfig
Connect, close, and runtime controlnanobot/agent/loop.py::_connect_mcp, close_mcp; nanobot/agent/tools/mcp.py
MCP Tool/Resource/Prompt wrappersnanobot/agent/tools/mcp.py::MCPToolWrapper, MCPResourceWrapper, MCPPromptWrapper
Verification and reconnecttests/agent/test_mcp_transient_retry.py, tests/tools/test_mcp_probe.py, tests/tools/test_mcp_tool.py
WebUI preset integrationnanobot/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:

FieldPurpose
typestdio, sse, or streamableHttp; optional
command / args / env / cwdstdio server
url / headersHTTP/SSE server
toolTimeouttimeout for one capability call; default 30 seconds
enabledToolsnames allowed to register; default ["*"]

Configuration lives under tools.mcpServers.<server-name>.

Automatic inference:

  • command present → stdio;
  • url whose 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 capabilitynanobot Tool name
tool read_filemcp_<server>_read_file
resource schemamcp_<server>_resource_schema
prompt reviewmcp_<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:

  1. establish the transport;
  2. create ClientSession;
  3. call initialize();
  4. list capabilities;
  5. wrap and register them;
  6. 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 env passes unnecessary secrets;
  • whether cwd exposes 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:

  1. unregister old Tools for the server;
  2. close the old stack;
  3. reconnect the server;
  4. register capabilities again;
  5. 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

  1. Configuration passes Pydantic.
  2. The server connects and lists capabilities.
  3. enabledTools registers only expected names.
  4. Argument schemas are normalized.
  5. Calls return correct results.
  6. Timeout is enforced.
  7. HTTP private targets and redirects are blocked.
  8. Disconnect triggers the expected retry/reconnect.
  9. Graceful shutdown leaves no process behind.

Source:

  • nanobot/agent/tools/mcp.py
  • nanobot/config/schema.py::MCPServerConfig
  • nanobot/agent/loop.py::_connect_mcp
  • nanobot/agent/loop.py::close_mcp

Tests:

  • tests/tools/test_mcp_tool.py
  • tests/tools/test_mcp_probe.py
  • tests/agent/test_mcp_transient_retry.py
  • tests/webui/test_mcp_presets_api.py

Official configuration reference:

Self-check

  1. Why does an MCP Tool need no special AgentRunner branch?
  2. How do enabledTools=[] and ["*"] differ?
  3. Why does the SSRF guard not protect an stdio server?
  4. Why does npx need a cmd.exe /d /c wrapper on Windows?

Next: Add a Provider