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.