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 は第三者の能力を手書きアダプタ (adapter) なしで接入できる。

設計動機

なぜ独自のツールプロトコルを再発明せず 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 をディスパッチ (dispatch) し、結果を戻す。
  5. 逆方向:Hermes は mcp_serve.py で自身のツールを他の MCP client に露出する。

主要コード

MCP ツールの ToolRegistry への橋渡し

接続成功後、_register_server_tools が各 MCP ツールを 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- で始まらない内蔵ツールセットとぶつかったら黙って置き換えずスキップする。

ツール呼び出し (tool dispatch) の橋渡し

主ループが 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」と書くのは、モデルに読ませるためだ——サーキットブレーカーが開いている間にイテレーション (iteration) を焼き尽くして再試行し続けるのを止める。

逆方向: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 かを意識しなくてよい。

非公式コミュニティ学習サイト。MIT ライセンスの NousResearch/hermes-agent ソースに基づく。