Skip to content

MCP 接入

源码版本v2026.7.20

職責

MCP(Model Context Protocol)是連接外部工具 (tool)/資料源的通用協議。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、分發 (dispatch) 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 原始碼為依據。