webmcp-bridge-mcp
by tanahiro2010
README.md
# 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](../webmcp-bridge-extension))との間の Bridge / Registry / Router です。
拡張機能を先にインストールしていなくても起動できますが、tool 呼び出しには拡張機能の接続が必要です。
## インストール
```bash
bun install
```
## 開発起動
```bash
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 のポートを変更できます。
```bash
WS_PORT=9000 bun run dev
```
## ビルド / 本番起動
```bash
bun run build
bun run start
```
## テスト
```bash
bun test
```
`test/bridge.test.ts` が実際の MCP サーバー(`src/index.ts`)を子プロセスとして起動し、実際の
`@modelcontextprotocol/sdk` の `Client`(= Antigravity CLI のような LLM エージェントの代わりに、
スクリプトで直接 tool を呼ぶ「モックされたエージェント」)を接続します。Chrome Extension の代わりに
`FakeExtension` クラスが同じ WebSocket プロトコルを喋って接続することで、ブラウザ無しに
接続状態・`tabs/list` の live 問い合わせ・`discover_tools` のキャッシュ / `forceRefresh`・
tool 呼び出しの並行実行・切断時のクリーンアップまで、ブリッジのロジック全体を検証します。
拡張機能自体(overlay の表示、DOM 操作を伴う tool 実行など)の検証は
[webmcp-bridge-extension](../webmcp-bridge-extension) 側の Playwright テストで行っています。
## Antigravity CLI への登録例
npm に公開済みの場合、事前の `bun install` やパスの指定なしに `bunx` / `npx` だけで起動できます
(初回実行時に自動でダウンロード・キャッシュされます)。
```json
{
"mcpServers": {
"webmcp-bridge": {
"command": "bunx",
"args": ["-y", "webmcp-bridge-mcp"]
}
}
}
```
`npx` でも同様に動きます(`bin` エントリはプレーンな Node.js 上での動作を確認済みです)。
```json
{
"mcpServers": {
"webmcp-bridge": {
"command": "npx",
"args": ["-y", "webmcp-bridge-mcp"]
}
}
}
```
このリポジトリから直接ソースで動かしたい場合(開発中・未公開の場合)は、従来どおり `cwd` を指定してください。
```json
{
"mcpServers": {
"webmcp-bridge": {
"command": "bun",
"args": ["run", "dev"],
"cwd": "/absolute/path/to/webmcp-bridge-mcp"
}
}
}
```
### npm 公開の準備について
`package.json` に `bin` エントリ(`dist/index.js`、`#!/usr/bin/env node` シェバン付き)を用意してあり、
`bun run build` は `--target node` でビルドするため `node` / `bun` どちらからも実行できます。
`npm pack --dry-run` で中身が `dist/index.js` と `README.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`
```json
// 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`
```json
// 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_approval` → `webmcp_submit_tool`
```json
// 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" } }
```
`source` は [WebMCP 仕様](https://webmachinelearning.github.io/webmcp/) 準拠の検出元を表します
(`"imperative"` = `document.modelContext.registerTool()`、`"declarative"` = annotated `<form>`)。
詳細は [webmcp-bridge-extension の README](../webmcp-bridge-extension/README.md#webmcp-検出仕様) を参照してください。
Extension が未接続の場合、すべての tool 呼び出しはエラー(`ok: false` またはエラーメッセージ)を
返すだけで、サーバーがクラッシュしたりハングしたりすることはありません。
> **注:** `webmcp_discover_tools` の結果で `requiresUserGesture: true` になっている tool
> (`toolautosubmit` の無い宣言型フォーム)は、実機検証の結果、人間が実際に送信ボタンを押すまで
> `webmcp_call_tool` が応答を返さないことがあります(ブラウザのネイティブ実装依存)。人が操作しない
> 自動実行の文脈でこの手の tool を呼ぶ場合は、`timeoutMs` を十分長く設定するか、そもそも呼び出さずに
> 人間へ操作を促す設計にしてください。
>
> `webmcp_request_submit_approval` → `webmcp_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](https://github.com/modelcontextprotocol/inspector) を使うと拡張機能なしでも
サーバー自体の疎通を確認できます。
```bash
bunx @modelcontextprotocol/inspector bun run src/index.ts
```
`webmcp_get_status` / `webmcp_ping` を呼び、`extensionConnected: false` /
`{"ok":false,"error":"Extension not connected."}` が返ってくれば正常です。
This server cannot be deployed
Maintenance
ActivitySlowing
ResponsivenessNo issues