Skip to main content
Glama
tanahiro2010

webmcp-bridge-mcp

by tanahiro2010

webmcp-bridge-mcp

Antigravity CLI(や他の MCP クライアント)から、Chrome 上で開いている WebMCP 対応ページの tool を呼び出せるようにする MCP サーバーです。

Antigravity CLI <--stdio(MCP)--> webmcp-bridge-mcp <--WebSocket--> webmcp-bridge-extension <--content/injected script--> Web Page

このサーバー自身はページの DOM には一切触れません。あくまで Chrome Extension (webmcp-bridge-extension)との間の Bridge / Registry / Router です。 拡張機能を先にインストールしていなくても起動できますが、tool 呼び出しには拡張機能の接続が必要です。

インストール

bun install

開発起動

bun run dev
  • MCP サーバーは stdio で待受けます(Antigravity CLI から起動されるのを想定)。

  • 同時に WebSocket サーバーが ws://127.0.0.1:58787 に起動します(127.0.0.1 のみ bind、外部からは接続不可)。 8787 ではなく 58787 なのは、wrangler dev(Cloudflare Workers)のデフォルトポートと衝突するため (Workers開発を並行して行っていると、拡張機能がこちらではなく wrangler 側に接続しにいこうとして ずっと繋がらなくなる不具合があった)。

  • ログはすべて stderr に出力されます(stdout は MCP の JSON-RPC 通信専用のため)。

環境変数 WS_PORT で WebSocket のポートを変更できます。

WS_PORT=9000 bun run dev

ビルド / 本番起動

bun run build
bun run start

テスト

bun test

test/bridge.test.ts が実際の MCP サーバー(src/index.ts)を子プロセスとして起動し、実際の @modelcontextprotocol/sdkClient(= Antigravity CLI のような LLM エージェントの代わりに、 スクリプトで直接 tool を呼ぶ「モックされたエージェント」)を接続します。Chrome Extension の代わりに FakeExtension クラスが同じ WebSocket プロトコルを喋って接続することで、ブラウザ無しに 接続状態・tabs/list の live 問い合わせ・discover_tools のキャッシュ / forceRefresh・ tool 呼び出しの並行実行・切断時のクリーンアップまで、ブリッジのロジック全体を検証します。

拡張機能自体(overlay の表示、DOM 操作を伴う tool 実行など)の検証は webmcp-bridge-extension 側の Playwright テストで行っています。

Antigravity CLI への登録例

npm に公開済みの場合、事前の bun install やパスの指定なしに bunx / npx だけで起動できます (初回実行時に自動でダウンロード・キャッシュされます)。

{
  "mcpServers": {
    "webmcp-bridge": {
      "command": "bunx",
      "args": ["-y", "webmcp-bridge-mcp"]
    }
  }
}

npx でも同様に動きます(bin エントリはプレーンな Node.js 上での動作を確認済みです)。

{
  "mcpServers": {
    "webmcp-bridge": {
      "command": "npx",
      "args": ["-y", "webmcp-bridge-mcp"]
    }
  }
}

このリポジトリから直接ソースで動かしたい場合(開発中・未公開の場合)は、従来どおり cwd を指定してください。

{
  "mcpServers": {
    "webmcp-bridge": {
      "command": "bun",
      "args": ["run", "dev"],
      "cwd": "/absolute/path/to/webmcp-bridge-mcp"
    }
  }
}

npm 公開の準備について

package.jsonbin エントリ(dist/index.js#!/usr/bin/env node シェバン付き)を用意してあり、 bun run build--target node でビルドするため node / bun どちらからも実行できます。 npm pack --dry-run で中身が dist/index.jsREADME.md だけになることを確認済みです。 実際に公開する場合は npm publish(または bun publish)を実行してください。

WebSocket URL

ws://127.0.0.1:58787

WS_PORT 環境変数で変更可)。Chrome Extension 側は起動時にこの URL へ接続しにいきます。

利用できる MCP tools

tool

説明

webmcp_get_status

Extension の接続状態、既知タブ数、アクティブタブ ID を返す。WebSocket サーバー自体が bind に失敗している場合(他プロセスによるポート占有など)は wsListenError にその理由が入る

webmcp_list_tabs

Extension が把握している WebMCP 対応タブ一覧を返す(Extension への live 問い合わせ)

webmcp_discover_tools

指定タブ(省略時はアクティブタブ)の WebMCP tool を検出する。デフォルトはキャッシュを返し、forceRefresh: true で再スキャンする。結果には discoveryToken(後述)が含まれる

webmcp_call_tool

指定タブの WebMCP tool を toolId / args / discoveryToken を指定して実行する。timeoutMs(デフォルト30秒)で待ち時間を調整可能

webmcp_request_submit_approval

[HIGH RISK] toolautosubmit の無い宣言型 tool を送信するための、短命・単回使用の承認トークンを要求する(詳細は後述)

webmcp_submit_tool

[HIGH RISK] webmcp_request_submit_approval が返した approvalToken を使って実際に送信を確定させる

webmcp_ping

Extension との疎通確認(レイテンシ計測)。ページ操作は行わない

discoveryToken: タブ・オリジン・ナビゲーション世代・マニフェストへの束縛

タブスコープの寿命(タブを閉じたりページ遷移したりすれば tool は消える)はそれ自体はセキュリティ上 有用な性質ですが、ブリッジがそれを暗黙のうちに弱めてしまう経路があります。具体的には:

  • webmcp_discover_tools を呼んだ後にタブが別のページへ遷移した

  • SPA が同じ URL のまま DOM 上のフォームを差し替えた(toolname は同じだが中身が変わった)

  • tool の定義(inputSchema など)が webmcp_discover_tools の呼び出しと webmcp_call_tool の呼び出しの間に変化した

こうした場合、"discover した時点の tool" と "実際に実行される tool" が食い違ったまま呼び出しが 通ってしまうと、ページ tool が実質的に「安定した MCP capability」であるかのように振る舞ってしまい、 タブスコープの寿命という前提が壊れます。

これを防ぐため、webmcp_discover_tools の結果には origin / navigationEpoch (Extension がこのタブのナビゲーションごとにインクリメントするカウンタ)/ manifestHash (そのタブの現在の tool 一覧から計算したハッシュ)の3つ組からなる discoveryToken が含まれます。 webmcp_call_tool / webmcp_request_submit_approval / webmcp_submit_tool はこの discoveryToken を要求し、Extension 側 (background.ts) が実行の直前に タブの現在の 生の状態(origin・navigationEpoch・再スキャンした manifestHash)と突き合わせて一致することを 確認します。一致しなければ STALE_DISCOVERY エラーで実行を拒否するので、 webmcp_discover_tools をやり直してから再試行してください。

webmcp_submit_tool: 独立した high-risk capability としての承認フロー

webmcp_submit_tool は WebMCP 仕様が意図する安全機構(toolautosubmit の無い宣言型フォームは 既定で人間の確認を要求する)を迂回するものであり、webmcp_call_tool の単なる兄弟 tool として 扱うべきではありません。そのため2段階に分離しています。

  1. webmcp_request_submit_approval[HIGH RISK])— tabId / toolId / args / discoveryToken を渡すと、Extension がそのタブの現在の生の状態を確認した上で、フォームの 実際に画面上にある現在のフィールド値reviewState)を読み取り、それを含めて {tabId, toolId, origin, navigationEpoch, manifestHash, args, reviewState} に束縛した 短命(既定20秒)・単回使用の approvalToken を発行します。reviewState は承認前に 必ず確認してください。

  2. webmcp_submit_tool[HIGH RISK])— approvalToken を渡して実際に送信します。 Extension は送信の直前に、トークンの有効期限・未使用であること・ タブの origin/navigationEpoch/manifestHash が承認時と変わっていないこと・ フォームの現在の実際のフィールド値が承認時の reviewState と一致することを すべて再チェックします。いずれかが変化していれば拒否するので、 webmcp_request_submit_approval からやり直してください。

承認トークンは Extension の Service Worker のメモリ上にのみ保持され、どこにも永続化されません。 MV3 の Service Worker はアイドル状態でサスペンドされることがあり(README 下部の実機検証の節参照)、 再開後にこの状態が失われるのは意図的な挙動です。サスペンド前に発行された承認をサスペンド後の Service Worker が「復元」して、変わってしまったかもしれないページの状態にそのまま結び付けて しまうことがあってはならないため、再開後の Service Worker は単に「未知のトークン」として 拒否します。

入出力の例

webmcp_discover_tools

// input
{ "tabId": 123, "forceRefresh": true }
// output (content[0].text の JSON)
{
  "tabId": 123,
  "origin": "https://example.com",
  "navigationEpoch": 0,
  "manifestHash": "a1b2c3d4",
  "tools": [ { "id": "reserve_hotel", "name": "reserve_hotel", "source": "imperative", ... } ],
  "discoveryToken": { "origin": "https://example.com", "navigationEpoch": 0, "manifestHash": "a1b2c3d4" }
}

webmcp_call_tool

// input
{
  "toolId": "reserve_hotel",
  "args": { "city": "Osaka" },
  "discoveryToken": { "origin": "https://example.com", "navigationEpoch": 0, "manifestHash": "a1b2c3d4" }
}
// output
{ "ok": true, "result": { "ok": true, "city": "Osaka", "confirmationId": "RES-12345" } }
// or, if the tab navigated / the manifest mutated since discovery:
{ "ok": false, "error": "STALE_DISCOVERY: Tab navigated since discovery." }

webmcp_request_submit_approvalwebmcp_submit_tool

// input (webmcp_request_submit_approval)
{
  "toolId": "search_hotels",
  "args": { "city": "Osaka" },
  "discoveryToken": { "origin": "https://example.com", "navigationEpoch": 0, "manifestHash": "a1b2c3d4" }
}
// output
{ "ok": true, "approvalToken": "1b3e...", "expiresAt": 1735900000000, "reviewState": { "city": "Osaka" } }

// input (webmcp_submit_tool)
{ "toolId": "search_hotels", "approvalToken": "1b3e..." }
// output
{ "ok": true, "result": { "ok": true, "city": "Osaka" } }

sourceWebMCP 仕様 準拠の検出元を表します ("imperative" = document.modelContext.registerTool()"declarative" = annotated <form>)。 詳細は webmcp-bridge-extension の README を参照してください。

Extension が未接続の場合、すべての tool 呼び出しはエラー(ok: false またはエラーメッセージ)を 返すだけで、サーバーがクラッシュしたりハングしたりすることはありません。

注: webmcp_discover_tools の結果で requiresUserGesture: true になっている tool (toolautosubmit の無い宣言型フォーム)は、実機検証の結果、人間が実際に送信ボタンを押すまで webmcp_call_tool が応答を返さないことがあります(ブラウザのネイティブ実装依存)。人が操作しない 自動実行の文脈でこの手の tool を呼ぶ場合は、timeoutMs を十分長く設定するか、そもそも呼び出さずに 人間へ操作を促す設計にしてください。

webmcp_request_submit_approvalwebmcp_submit_tool はこの「人間のクリック待ち」を agent 側から 明示的に上書きするための、2段階に分かれた [HIGH RISK] tool です(詳細は上の節を参照)。 これは WebMCP 仕様が意図している安全機構(宣言型フォームは既定で人間の確認を要求する)を 迂回するものだと理解した上で使ってください。 webmcp_call_tool で入力を埋めて pending: true を 受け取った後、内容(reviewState)を確認して自動的に送信してよいと判断した場合にのみ呼び出す想定です。 なお、これが効くのはこの拡張機能自身の polyfill 実装(document.modelContext が未実装のブラウザ) に対してのみです。ネイティブ実装がある一部のブラウザは executeTool() 自体を人間のクリックまで ブロックする(前述)ため、pending 状態を経由せずそもそも webmcp_call_tool から返ってこず、 この承認フローを使う隙がありません。

注: result は tool の execute() が返した値をそのまま中継したものです。実機の document.modelContext.executeTool() で検証したところ、ページ側が { ok: true, city } のような オブジェクトを返しても、ブラウザ側で JSON 文字列化されて渡ってくるケースがありました(WebMCP 仕様の execute は元々「エージェント向けの文字列サマリ」を返す想定のため)。このサーバーは result を 一切加工せず素通しするので、MCP クライアント側で文字列か構造化データかを判定してください。

動作確認(拡張機能なしで疎通だけ見る場合)

MCP Inspector を使うと拡張機能なしでも サーバー自体の疎通を確認できます。

bunx @modelcontextprotocol/inspector bun run src/index.ts

webmcp_get_status / webmcp_ping を呼び、extensionConnected: false / {"ok":false,"error":"Extension not connected."} が返ってくれば正常です。