Skip to content

MCP 接入

源码版本v2026.7.20

职责

MCP(Model Context Protocol)是连接外部工具/数据源的通用协议。Hermes 既作为 MCP client 消费外部 MCP server 暴露的工具(经 mcp_tool 桥接进 ToolRegistry),也可作为 MCP server(mcp_serve.py)把自身工具暴露给其他 agent 客户端。这层让 Hermes 能接入第三方能力,不靠手写适配。

设计动机

为什么挑 MCP 而不是再发明一套工具协议?MCP 是 Anthropic 推的开放标准,Claude Code、Cursor、Codex 已经按它对接,生态现成。Hermes 当 client 可以复用几十个社区 MCP server,当 server 可以被任意 MCP 客户端驱动,免费拿到双向互操作。自己造协议能贴合内部 ToolRegistry,但失去的是整片生态。

关键文件

数据流

  1. 启动期(或运行期热加载)读 MCP 配置,mcp_tool 连接外部 MCP server。
  2. 拉取 server 暴露的工具清单,逐个经 registry.register:365 注册为 ToolEntry(命名空间隔离)。
  3. 命名空间准入由 register_plugin_override_policy:316 控制。
  4. 主循环像对待内置工具一样收集 schema、分发 tool_call、回填结果。
  5. 反向:Hermes 可经 mcp_serve.py 把自身工具暴露给其他 MCP client。

关键代码

桥接 MCP 工具进 ToolRegistry

连接成功后,_register_server_tools 把每个 MCP tool 转成 schema 并注册,handler 用闭包绑定 server 名与超时:

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"])

工具名前缀化保证多个 server 之间不撞名,且和内置工具集做冲突检测:撞到非 mcp- 开头的内置工具集,直接跳过而不是默默替换。

工具调用桥接

主循环调到 MCP 工具时,registry 的 handler 转发到这个闭包。它先看断路器是否已开,再拿连接(必要时触发 stdio 重连),然后把同步调用丢到后台事件循环上等 MCP 返回:

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)

错误信息里专门写「Do NOT retry this tool yet」,这是给模型读的:不让它在断路器开着的时候一直烧迭代重试。

反向:把 Hermes 自己暴露为 MCP server

mcp_serve.py 用 FastMCP 起一个 stdio server,把消息桥接工具注册出去,任何 MCP 客户端都能 list/read/send 消息:

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."""
    ...

@mcp.tool() 装饰器即注册,不需要手写 schema——FastMCP 从类型注解里推断。

边界与失败

  • MCP server 崩了:子进程死掉或 HTTP 端点挂了,MCPServerTask 自动重连,失败次数累计到阈值就跳闸断路器,后续 tool_call 直接返回错误,冷却过后才半开放行一次探针。
  • stdio server 回收:进程闲置太久会被回收省资源,下次调用时 _request_lazy_reconnect 先唤醒再转发,会有几十毫秒到秒级延迟。
  • 工具名冲突:MCP 工具前缀化后撞到内置工具集直接跳过;两个 MCP server 重名则允许覆盖,走 both_mcp 分支并打 debug 日志。

小结

MCP 是 Hermes 的「能力外接总线」。mcp_tool 让外部工具进入统一工具池,mcp_serve.py 让 Hermes 自己也能被外部调用。统一走 ToolRegistry,所以主循环无需关心工具来自内置还是 MCP——它只看到 schema 和 handler。

非官方社区学习站,内容以 MIT 许可的 NousResearch/hermes-agent 源码为依据。