Skip to content

MCP Integration ​

源码版本v2026.7.20

Responsibility ​

MCP (Model Context Protocol) is a universal protocol for connecting external tools/data sources. Hermes acts both as an MCP client consuming tools exposed by external MCP servers (bridged into ToolRegistry via mcp_tool) and as an MCP server (mcp_serve.py) exposing its own tools to other agent clients. This lets Hermes pull in third-party capabilities without baking them into the core.

Design Motivation ​

Why pick MCP instead of inventing another tool protocol? MCP is an open standard pushed by Anthropic; Claude Code, Cursor, and Codex already speak it, so the ecosystem is already there. As a client, Hermes can reuse dozens of community MCP servers; as a server, Hermes can be driven by any MCP client — bidirectional interop for free. A homegrown protocol might fit the internal ToolRegistry more snugly, but you lose the whole ecosystem.

Key Files ​

Data Flow ​

  1. At startup (or via runtime hot reload) MCP config is read and mcp_tool connects to external MCP servers.
  2. The list of tools exposed by a server is pulled and each is registered as a ToolEntry via registry.register:365 (namespaced to avoid collisions with built-in tools).
  3. Namespace admission is controlled by register_plugin_override_policy:316.
  4. The main loop gathers schemas, dispatches tool_calls, and backfills results just as for built-in tools.
  5. Reverse direction: Hermes can expose its own tools to other MCP clients via mcp_serve.py, letting editors/other agents call them.

Key Code ​

Bridging MCP tools into ToolRegistry ​

Once connected, _register_server_tools converts each MCP tool to a schema and registers it; the handler is a closure binding the server name and timeout:

python
for mcp_tool in server._tools:
    if not _should_register(mcp_tool.name):
        continue
    schema = _convert_mcp_schema(name, mcp_tool)
    tool_name_prefixed = schema["name"]
    existing_toolset = registry.get_toolset_for_tool(tool_name_prefixed)
    if existing_toolset and not existing_toolset.startswith("mcp-"):
        logger.warning("MCP '%s': tool '%s' collides with built-in — skipping",
                       name, mcp_tool.name)
        continue
    registry.register(name=tool_name_prefixed, toolset=toolset_name, schema=schema,
                      handler=_make_tool_handler(name, mcp_tool.name, server.tool_timeout),
                      check_fn=_make_check_fn(name), is_async=False,
                      description=schema["description"])

Tool names are prefixed so multiple servers never collide, and conflict detection runs against the built-in toolset: if it hits a non-mcp- built-in toolset, it skips rather than silently replacing.

Tool-call bridge ​

When the main loop invokes an MCP tool, the registry handler forwards to this closure. It first checks whether the circuit breaker is open, then grabs a connection (triggering a stdio reconnect if needed), then submits the sync call onto a background event loop to wait for the MCP reply:

python
def _handler(args: dict, **kwargs) -> str:
    if _server_error_counts.get(server_name, 0) >= _CIRCUIT_BREAKER_THRESHOLD:
        age = time.monotonic() - _server_breaker_opened_at.get(server_name, 0.0)
        if age < _CIRCUIT_BREAKER_COOLDOWN_SEC:
            remaining = max(1, int(_CIRCUIT_BREAKER_COOLDOWN_SEC - age))
            return json.dumps({"error": (
                f"MCP server '{server_name}' unreachable after "
                f"{_CIRCUIT_BREAKER_THRESHOLD} failures. Retry in ~{remaining}s. "
                f"Do NOT retry this tool yet — use alternative approaches.")},
                ensure_ascii=False)
    server = _get_connected_server_for_call(server_name)
    if not server:
        _bump_server_error(server_name)
        return json.dumps({"error": f"MCP server '{server_name}' is not connected"},
                          ensure_ascii=False)

The error message explicitly says "Do NOT retry this tool yet" — that is for the model to read, so it does not keep burning turns retrying while the breaker is open.

Reverse: exposing Hermes itself as an MCP server ​

mcp_serve.py uses FastMCP to start a stdio server and registers the messaging bridge tools, so any MCP client can list/read/send messages:

python
mcp = FastMCP("hermes", instructions=(
    "Hermes Agent messaging bridge. Use these tools to interact with "
    "conversations across Telegram, Discord, Slack, WhatsApp, Signal, Matrix."))

@mcp.tool()
def conversations_list(platform=None, limit=50, search=None) -> str:
    """List active messaging conversations across connected platforms."""
    ...

The @mcp.tool() decorator is the registration — no manual schema writing; FastMCP infers it from type annotations.

Boundaries and Failure ​

  • MCP server crashes: if a subprocess dies or an HTTP endpoint goes down, MCPServerTask reconnects automatically; once failure counts cross a threshold the circuit breaker trips, subsequent tool_calls return an error immediately, and only after cooldown does a single probe call go through.
  • stdio server reclamation: an idle process gets reclaimed to save resources; the next call triggers _request_lazy_reconnect to wake it before forwarding, adding tens of milliseconds to seconds of latency.
  • Tool name conflicts: a prefixed MCP tool that hits a built-in toolset is skipped; two MCP servers with the same name are allowed to override, going through the both_mcp branch with a debug log.

Summary ​

MCP is Hermes's "capability bus". mcp_tool lets external tools enter the same tool pool as built-ins; mcp_serve.py lets Hermes itself be called by external clients. Everything goes through ToolRegistry, so the main loop does not care whether a tool is built-in or MCP.

Unofficial community learning site. Content based on the MIT-licensed NousResearch/hermes-agent source. · Privacy · Terms · About