Skip to content

MCP:双方向ブリッジ

源码版本v2026.6.11

責務

MCP(Model Context Protocol)は OpenClaw 内で双方向です:server として自身のツール/チャネル/承認イベントを外部 MCP client(Claude Code、Codex、Cursor 等)に公開でき,同時に client として外部 MCP server のツールを agent メインループのツールセットに取り込めます。両方向とも同じ SDK(@modelcontextprotocol/sdk)を使いますが,実装パスは完全に異なります。

server として動くとき,OpenClaw は stdio MCP server を立ち上げ,ビルトインツール、プラグインツール、チャネル操作(メッセージ送信、セッション列挙、履歴読み取り、承認待ち)を MCP ツールとして包んで公開します。外部 client がこれらのツールを呼ぶとき,server 内部で Gateway RPC に転送するか,直接ローカルツール実行体を呼びます。client として動くとき,OpenClaw は mcpServers 設定の各 server を stdio/SSE/StreamableHTTP で接続し,相手が公開するツールを agent session のツール Map に合流させます——これらのツールはビルトインツール、プラグインツールと対等です。

MCP サブシステムの核心責務は:プロトコル適合(JSON-RPC <-> MCP schema)、ライフサイクル(接続、切断、再接続、アイドル清理)、ツールブリッジ(両方向のツールメタデータ/呼び出し翻訳)、イベントブリッジ(Gateway イベント -> MCP notification;MCP ツール呼び出し -> Gateway RPC)。

設計動機

なぜ双方向か?OpenClaw の位置づけが agent harness(自身で agent を走らせる)であり,同時にツール供給者(他の agent harness がその能力を呼べるようにする)だからです。Claude Code のような外部 agent が OpenClaw のチャネル能力(Slack 送信、iMessage 履歴読み取り)を使いたい場合,MCP 経由が Claude Code 自身に適合を実装させるよりずっと安価です。逆に,OpenClaw 自身の agent も他者が公開する MCP ツール(例:あるデータベース MCP server)を使いたく,client として振る舞えばこれらのツールが agent のツールセットに直接出現します。

OpenClawChannelBridge(channel-bridge.ts OpenClawChannelBridge:L68-L104) が server としての核心です。これは Gateway への WebSocket 接続を維持し,Gateway イベント(session.message、exec.approval.requested、plugin.approval.requested)を MCP notification に変換して外部 client に送ります。同時に外部 client が MCP tool 呼び出しで起こしたリクエスト(メッセージ送信、履歴読み取り、承認 resolve)を Gateway RPC に転送します。コメントは明確(src/mcp/channel-bridge.ts:L28-L33)です:

typescript
/**
 * Runtime bridge between MCP tools and the OpenClaw Gateway channel APIs.
 *
 * The bridge owns readiness, event cursoring, pending approval state, and the
 * narrow request methods that channel MCP tools expose to external clients.
 */

「owns readiness, event cursoring, pending approval state」——この 3 件事が bridge の全責務です。readyPromise は外部 client がツールを呼ぶ前に Gateway が subscribe 完了していることを保証します。cursor キュー(QUEUE_LIMIT=1000)はイベントを失わず,再生可能であることを保証します。pending approval 表は承認リクエストが外部 client に漏れないことを保証します。

client としての核心は resolveMcpTransport(mcp-transport.ts resolveMcpTransport:L89-L170) と createSessionMcpRuntime(agent-bundle-mcp-runtime.ts createSessionMcpRuntime:L480-L555) です。前者は設定内の mcpServers 項目を SDK Transport インスタンスに解析し,後者は各 server に Client を立ち上げ,connect 後 listTools でツールメタデータを引き session ツールセットに合流させます。

StreamFunction 契約には「エラーは stream に入れスローしない」原則があり,MCP も似ています——createLazyStream の catch がモジュール読み込み失敗を stream イベントにエンコードします。MCP client 側では接続失敗、listTools 失敗を diagnostics に転換し,スローして session を阻断しません(src/agents/agent-bundle-mcp-runtime.ts:L547-L630)。

主要ファイル

データフロー

server として(channel-server.ts 組み立て:L37-L60):

typescript
export async function createOpenClawChannelMcpServer(opts: OpenClawMcpServeOptions = {}): Promise<{
  server: McpServer;
  bridge: OpenClawChannelBridge;
  start: () => Promise<void>;
  close: () => Promise<void>;
}> {
  const cfg = await resolveMcpConfig(opts.config);
  const claudeChannelMode = opts.claudeChannelMode ?? "auto";
  const capabilities = getChannelMcpCapabilities(claudeChannelMode);
  const server = new McpServer(
    { name: "openclaw", version: VERSION },
    capabilities ? { capabilities } : undefined,
  );
  const bridge = new OpenClawChannelBridge(cfg, {
    gatewayUrl: opts.gatewayUrl,
    gatewayToken: opts.gatewayToken,
    gatewayPassword: opts.gatewayPassword,
    claudeChannelMode,
    verbose: opts.verbose ?? false,
  });
  bridge.setServer(server);

  server.server.setNotificationHandler(ClaudePermissionRequestSchema, async ({ params }) => {
    await bridge.handleClaudePermissionRequest({ /* ... */ });
  });

bridge 自身が Gateway WebSocket 接続を持ち,イベント来時に handleGatewayEvent(src/mcp/channel-bridge.ts:L524-L569) に入ります:

typescript
private async handleGatewayEvent(event: EventFrame): Promise<void> {
  switch (event.event) {
    case "session.message":
      await this.handleSessionMessageEvent(event.payload as SessionMessagePayload);
      return;
    case "exec.approval.requested": {
      const raw = (event.payload ?? {}) as Record<string, unknown>;
      this.trackApproval("exec", raw);
      this.enqueue({
        cursor: this.nextCursor(),
        type: "exec_approval_requested",
        raw,
      });
      return;
    }
    case "exec.approval.resolved": { /* ... */ }
    case "plugin.approval.requested": { /* ... */ }
    case "plugin.approval.resolved": { /* ... */ }
  }
}

trackApproval は承認リクエストを pendingApprovals 表(TTL 付き)に格納し,外部 client は listPendingApprovals / respondToApproval ツールで照会・応答できます。enqueue はイベントをキューにpush(QUEUE_LIMIT=1000 超過時は最も古いものを捨て),すべての waitForEvent 待機者を喚起します。handleSessionMessageEventyes/no <code> 形式のユーザ返信もマッチさせ(src/mcp/channel-bridge.ts:L586-L602) notifications/claude/channel/permission に変換して外部 client に送ります——これは Claude Code の permission メカニズムをチャット返信にブリッジする巧妙な設計です。

client として(bundle-mcp 接続:L562-L625):

typescript
for (const [serverName, rawServer] of Object.entries(loaded.mcpServers)) {
  failIfDisposed();
  const resolved = resolveMcpTransport(serverName, rawServer);
  if (!resolved) {
    continue;
  }
  const safeServerName = sanitizeServerName(serverName, usedServerNames);
  // ...
  let session = sessions.get(serverName);
  const reusedSession = Boolean(session);
  let connected = Boolean(session);
  if (!session) {
    const client = new Client(
      { name: "openclaw-bundle-mcp", version: "0.0.0" },
      {
        jsonSchemaValidator: createBundleMcpJsonSchemaValidator(),
        listChanged: {
          tools: {
            autoRefresh: false,
            debounceMs: 0,
            onChanged: (error) => {
              if (error) {
                logWarn(`bundle-mcp: failed to refresh changed tool list for server "${serverName}": ${redactErrorUrls(error)}`);
              }
              catalogInvalidationGeneration += 1;
              catalog = null;
              catalogInFlight = undefined;
            },
          },
        },
      },
    );
    session = {
      serverName,
      client,
      transport: resolved.transport,
      transportType: resolved.transportType,
      requestTimeoutMs: resolved.requestTimeoutMs,
      supportsParallelToolCalls: resolved.supportsParallelToolCalls,
      detachStderr: resolved.detachStderr,
    };
    sessions.set(serverName, session);
  }

  try {
    failIfDisposed();
    if (!connected) {
      await connectWithTimeout(
        session.client,
        session.transport,
        resolved.connectionTimeoutMs,
      );
      connected = true;
    }
    failIfDisposed();
    const capabilities = summarizeServerCapabilities(
      session.client.getServerCapabilities(),
    );
    const listedTools = await listAllToolsBestEffort({
      client: session.client,
      timeoutMs: getCatalogListTimeoutMs(rawServer, resolved.requestTimeoutMs),
      // ...
    });

いくつかの設計に注意:server name sanitize(sanitizeServerName)は異なる server のツール名プレフィックスが衝突しないことを保証します。listChanged 監視は server がツールリスト変更を push したとき OpenClaw がローカル catalog を失効して再取得します。connectWithTimeout は遅い server が起動を hang させるのを防ぎます。listAllToolsBestEffort は server が listChanged をサポートしないときグレードダウンします。session 級キャッシュ(sessions.get(serverName))で繰り返し読み込み時に再 connect しません。

server としてプラグインツールを公開するエントリは plugin-tools-serve.ts:L57-L81:

typescript
export function createPluginToolsMcpServer(
  params: {
    config?: OpenClawConfig;
    tools?: AnyAgentTool[];
  } = {},
): Server {
  const cfg = params.config ?? getRuntimeConfig();
  const tools = params.tools ?? resolveTools(cfg);
  return createToolsMcpServer({ name: "openclaw-plugin-tools", tools });
}

createToolsMcpServer(src/mcp/tools-stdio-server.ts:L10-L23) は ListToolsRequestSchemaCallToolRequestSchema の 2 つの handler を登録し,前者はツールメタデータを返し,後者は createPluginToolsMcpHandlers(src/mcp/plugin-tools-handlers.ts:L24-L73) を走らせます。

callTool handler には重要な設計があります——各ツールは wrapToolWithBeforeToolCallHook で beforeToolCall hook を付け(src/mcp/plugin-tools-handlers.ts:L25-L32)ます:

typescript
const wrappedTools = tools.map((tool) => {
  if (isToolWrappedWithBeforeToolCallHook(tool)) {
    return rewrapToolWithBeforeToolCallHook(tool, undefined, { approvalMode: "report" });
  }
  // The ACPX MCP bridge should enforce the same pre-execution hook boundary
  // as the agent and HTTP tool execution paths.
  return wrapToolWithBeforeToolCallHook(tool, undefined, { approvalMode: "report" });
});

コメントは明確:「ACPX MCP bridge should enforce the same pre-execution hook boundary as the agent and HTTP tool execution paths」——外部 client がツールを呼ぶとき,agent メインループと同じ beforeToolCall 境界を走らなければならず,「MCP から入ってきたから」と言って承認/hook をバイパスできません。これはセキュリティ継ぎ目です。

境界と失敗

  • 就緒門:OpenClawChannelBridge.start() は Gateway WebSocket が sessions.subscribe 完了した後にやっと resolveReadyOnce します(src/mcp/channel-bridge.ts:L410-L418)。外部 client はどのツールを呼ぶ前にもまず waitUntilReady し,さもなくばイベントを取得できません。
  • 初期接続再試行:shouldRetryInitialMcpGatewayConnect(src/mcp/channel-bridge.ts:L650-L663) はエラーが再試行可能か(gateway request timeout for connect / gateway connect challenge timeout)を判断し,再試行可能なら rejectReadyOnce しません——これで Gateway リスタート時に MCP server が短時間の不可達を耐えられます。
  • キュー長制限:QUEUE_LIMIT = 1_000(src/mcp/channel-bridge.ts:L51-L58)。イベントキューが長すぎるとき最も古いものを捨て——長い MCP session のメモリ爆発を保証。pending approval には TTL(60min / 30min)もあり,sweepPendingExpired が定時清理し,さらに interval を unref() して MCP stdio プロセスが client 退出後に hang しないようにします(src/mcp/channel-bridge.ts:L482-L491)。
  • notification 失敗はスローしない:sendNotification はすべてのエラーを catch し,stderr に 1 行書くだけ(src/mcp/channel-bridge.ts:L389-L408)。--verbose 時は詳細エラーを出力,さもなくば 1 行だけ——notification 失敗の連鎖が主フローに影響するのを避けます。
  • stdio stderr 隔離:attachStderrLogging(src/agents/mcp-transport.ts:L34-L60) は stdio MCP server の stderr を logDebug に転換します。MCP stdio プロトコルは stdout がプロトコル専用であることを要求し,任意のログは stderr へ;OpenClaw はさらに stderr 内容を自身のログシステムにキャプチャし,ノイズが client に逃げないようにします。
  • server name sanitize:sanitizeServerName(serverName, usedServerNames)(src/agents/agent-bundle-mcp-runtime.ts:L568-L573) は同名 server にサフィックスを付け,ツール名プレフィックスが衝突しないことを保証します。同時に warn でユーザに設定変更を促します。
  • listChanged で catalog 失効:tools.listChanged イベントトリガー時,catalog を null に,catalogInFlight を undefined に,catalogInvalidationGeneration をインクリメント(src/agents/agent-bundle-mcp-runtime.ts:L590-L600)。次回ツール呼び出しでツールリストを再取得します。
  • 失敗による server 一時停止:ある server が繰り返し tool call 失敗すると「一時停止」され,retryAfterMs を記録(src/agents/agent-bundle-mcp-runtime.ts:L519-L520)。これで 1 つの壊れた MCP server が agent メインループ全体を引きずり遅くするのを防ぎます。
  • beforeToolCall hook 境界:server としてツールを公開するとき,すべてのツールに wrapToolWithBeforeToolCallHook で hook を付け(src/mcp/plugin-tools-handlers.ts:L25-L32)ます。これはセキュリティ継ぎ目:外部 client のツール呼び出しは agent メインループと同じ承認/hook 境界を走らなければなりません。
  • dispose 後のすべての呼び出しは失敗:failIfDisposed() が各主要ポイントでチェック(src/agents/agent-bundle-mcp-runtime.ts:L469-L470)。session 終了後のすべての MCP ツール呼び出しは直接スローし,ぶら下がった client 呼び出しが資源リークを起こさないようにします。
  • MCP はツールシステムの一部ではない:toolRegistryツールシステム の Map で,MCP ツールはブリッジされた後この Map の項目として存在します——ビルトインツール、プラグインツールと対等です。MCP サブシステムは「接続+翻訳」を担当し,「呼び出し」は担当しません——呼び出しは toolRegistry の executor を経由します。MCP server が公開するツールも プラグイン 登録と同じ beforeToolCall hook を走ります。MCP server の stdio 起動と agent メインループの相互作用は Embedded runner で説明します。

まとめ

MCP は OpenClaw で双方向ブリッジです:server としてチャネル/承認/プラグインツールを外部 client に公開し,client として外部 MCP server のツールを session ツール Map に取り込みます。OpenClawChannelBridge は Gateway 接続、イベントキュー、pending approval 表を持ち,Gateway イベントを MCP notification に翻訳します。createSessionMcpRuntime は各設定項目に Client を立ち上げ,connect 後にツールリストを引きます。両辺とも「エラーは stream/キューに入れスローしない」原則に従い,接続失敗、listTools 失敗は diagnostics に転換しハードエラーにしません。server として公開するツールは wrapToolWithBeforeToolCallHook を走らなければなりません——これは agent メインループと共用のセキュリティ継ぎ目で,外部 client は承認/hook をバイパスできません。MCP ツールは最終的に ツールシステム の Map に合流し,ビルトインツール、プラグイン ツールと対等になります。session 起動時に mcpServers を読み込む流れは Embedded runner で説明します。

公式資料:MCP ドキュメント · README