cheerio-mcp-bridges
README.md
# cheerio-mcp-bridges
四個「窄工具」MCP server,讓一個沒辦法操作終端機 GUI 的 orchestrator agent(例如跑在 Cowork 裡的 Claude)能夠**驅動本機已安裝、已登入的四個 coding CLI**:
| Server | 內部呼叫 | 對外工具 | 語言 |
|--------|----------|----------|------|
| `pi-bridge` | `pi`(earendil-works/pi) | `ask_pi` | Node.js |
| `agy-bridge` | `agy`(Google Antigravity CLI) | `ask_agy` | Node.js |
| `codex-bridge` | `codex`(OpenAI Codex CLI) | `ask_codex` | Node.js |
| `copilot-bridge` | `copilot`(GitHub Copilot CLI) | `ask_copilot` | Node.js |
**五個 bridge 彼此獨立,不需要五個都裝。** 先跑 `npm run doctor` 看這台機器有哪些 CLI 可用,只啟用對應的 bridge 就好。
四個 server 各自暴露**單一、範圍受限**的工具(不是通用 `run_command`)——只能「把一段 prompt 送給那個 agent」。殘餘風險在於底層 CLI 收到 prompt 後自己能做什麼,因此預設姿態偏保守。
## 設計要點
1. **工作目錄由 server 鎖定**:cwd 來自環境變數(`PI_BRIDGE_CWD` / `AGY_BRIDGE_CWD` / `CODEX_BRIDGE_CWD` / `COPILOT_BRIDGE_CWD`),**呼叫端的 prompt 無法變更**。
2. **確定性的 session 續接**:
- pi:server 自己產生 UUID → `--session-id`(pi 支援「不存在就建立」),第一次呼叫就把 id 回傳;之後帶同一個 id 續接,不依賴「continue 最近一個」的模糊語意。
- agy:無法預指定 id,第一次跑完從 `--output-format stream-json` 的 `conversation_id` 撈出來回傳;之後用 `--conversation <id>` 續接。
- codex:第一次跑完從 `thread.started` 事件的 `thread_id` 撈出來回傳;之後用 `codex exec resume <id>` 續接。
- copilot:server 自己產生 UUID → `--session-id`,第一次呼叫就把 id 回傳;之後帶同一個 id 續接。
3. **零 shell 注入**:全部都 `shell:false` 直接 spawn,prompt 當作單一 argv 元素,任何 shell 特殊字元都不會被解讀。
Windows 上的 `.cmd` / `.bat` shim(`codex.cmd`、`copilot.cmd`)不能直接 CreateProcess,走
`lib/win-args.mjs` 自己組 `cmd.exe /d /s /c` 命令列並逐一跳脫,**不用 `shell:true`** ——
Node 在 `shell:true` 下不會替 argv 加引號(DEP0190),prompt 會被 cmd.exe 拆成好幾個參數,
而且帶 `&` 的 prompt 等於命令注入。
4. **保守的權限旗標**:
- **預設允許讀寫**檔案(符合使用者選擇),但寫入/危險能力仍分段控制。
- pi 預設**不**帶專案信任 `-a`(`approve_project` 才開)。
- agy 預設**不**帶 `--dangerously-skip-permissions`;workspace 讀寫自動放行、shell 指令維持 gated,除非 `dangerously_allow_all:true`。
- codex 預設 sandbox 為 `read-only`(`danger-full-access` 要明確指定)。
- copilot 預設只帶 `--allow-all-tools`(非互動必要),**不**帶 `--allow-all`(含 paths + urls),後者要 `dangerously_allow_all:true`。
5. **稽核**:每次呼叫寫一行 JSONL 到 `logs/<pi|agy|codex|copilot>-YYYYMMDD.jsonl`(prompt、session/thread id、exit code、耗時、usage)。
6. **可用性登記表(避免對已知被限流的 model/host 白打)**:四個 bridge 呼叫 CLI **前**都會先查 `state/availability.json`,命中封鎖中就直接回傳清楚錯誤、不 spawn CLI 浪費呼叫;呼叫**後**若偵測到限流訊號(429/quota/rate limit 等)會自動記錄封鎖時間。也支援人類手動回報「某 model 被鎖到幾點」。完整格式、三種 confidence(`exact` / `estimated` / `human-reported`)行為與手動回報做法,見 [`state/README.md`](state/README.md)。
## 踩過的坑(實測得出)
- **stdin 必須關閉**:CLI 都會把 piped stdin 當額外 context,Node spawn 預設留一個開著的 stdin pipe 會讓 CLI **卡住等 EOF**。解法:`stdio: ['ignore','pipe','pipe']`。
- **pi extensions 預設關閉**:互動型 extension(如 `auto-annotate`/plannotator)會在 headless 模式**掛住**(等一個永遠不出現的 UI)。因此預設帶 `--no-extensions`,需要時用 `enable_extensions:true` 開回來。
- **codex 必須帶 `--skip-git-repo-check`**:如果 cwd 不是 git repo(例如 `C:/Cheerio`),不帶這個 flag 會直接報錯退出。
- **copilot 非互動模式一定要 `--allow-all-tools`**:文件明確寫了 non-interactive mode 必須帶這個,否則會卡住等使用者確認權限。bridge 預設就帶 `--allow-all-tools`,但 `--allow-all`(含 paths + urls)只在 `dangerously_allow_all:true` 時才開。
- **copilot MCP server 載入很慢**:非互動模式下 copilot 仍然會載入所有 MCP server(playwright、notion、tavily 等),光是啟動就要 10~30 秒。timeout 設太短會在 MCP 載入階段就被 kill。
- **copilot 預設 auto-routing 可能撞 quota**:不指定 model 時,copilot 的 hydra router 會自動選模型(例如 `gpt-5-mini`),如果該模型的配額用完就會直接失敗。建議呼叫端明確指定 model。
- **Copilot/Codex 都無法非互動查詢剩餘總額度**:
- Copilot:`copilot billing` / `copilot limits` 是 help topic,只在互動模式 UI 裡有用。非互動 CLI 沒有 `copilot usage` 之類的指令。bridge 只能從 `model.call_failure` 事件裡的 `quotaSnapshots` 拿到「這次失敗時的快照」,無法主動查詢剩餘。
- Codex:`codex login status` 只顯示登入方式(`Logged in using ChatGPT`),沒有用量/配額查詢。`codex doctor` 只做安裝診斷。bridge 的 `turn.completed.usage` 只有當次 token 用量,無剩餘額度。
- **claude 的 `-p` 是布林旗標,prompt 是位置參數**:跟 pi/agy/copilot 不同(那三支的 `-p` 吃 prompt 當值)。所以 prompt 必須放在最後、而且前面要有一個裸的 `--`,否則 `--version is not a question` 這種開頭是 dash 的 prompt 會被當成旗標解析。
- **claude 的 stdin 會等 3 秒**:實測 stderr 會印 `Warning: no stdin data received in 3s, proceeding without it`。bridge 用 `stdio:['ignore',...]`(關閉的 stdin)正好避開,但這也是上面「stdin 必須關閉」那條的另一個例子。
- **claude 的 `subtype` 不能當錯誤訊號**:unknown model 打回 404 時,`is_error` 是 `true`、`api_error_status` 是 `404`,但 `subtype` 還是 `"success"`。要看 `is_error` 和 `api_error_status`。
- **claude 的 rate limit 是結構化事件**:`rate_limit_event` 帶 `status` 和精確的 `resetsAt`,不用像其他四支那樣比對錯誤字串。**但同一個事件裡 `status:"allowed"` 可以跟 `overageStatus:"rejected"` 並存** —— 只能看 `status`,寬鬆比對 `"rejected"` 會把健康的帳號鎖掉。
- **企業級 TLS 攔截 Proxy 可能導致 `npm install` 失敗**:某些組織會使用 TLS 檢查型 Proxy(例如資安廠商的憑證攔截方案)對 HTTPS 流量做中間人解密。這會讓 Node.js 的 TLS 驗證失敗,`npm install` 報 `UNABLE_TO_GET_ISSUER_CERT_LOCALLY` 或 `certificate chain incomplete` 之類的錯誤。解法:設定環境變數 `NODE_EXTRA_CA_CERTS` 指向公司的完整憑證鏈檔案(PEM 格式),注意需要的是 CA 中繼憑證(intermediate cert),不是只有 leaf cert。
- **企業級 IP allow list 擋 CLI 存取**(bridge 預設已處理):如果你的帳號所屬的企業級方案(例如 GitHub Copilot Enterprise)啟用了 IP allow list,`ask_copilot` 可能會被 API 擋下(錯誤訊息類似 "enterprise has an IP allow list enabled, and your IP address is not permitted")。**根本原因通常是**:bridge server 的父進程環境帶有 `HTTP_PROXY`/`HTTPS_PROXY` 之類的 proxy 設定,子進程的 CLI 工具繼承了這些 proxy 變數,導致 OAuth 請求繞經企業 proxy 出去,而 proxy 的出口 IP 不在對方的 allow list 內。**bridge 從 v0.2.0 開始預設會自動 strip 掉子進程 env 裡的 proxy 環境變數**(`HTTP_PROXY`/`HTTPS_PROXY`/`ALL_PROXY` 含大小寫變體),讓 CLI 直接對外連線來避開這個問題。如果你的環境情況相反(必須經過 proxy 才連得到目標服務),可以設 `BRIDGE_BYPASS_PROXY=false` 切回讓子進程繼承 proxy 設定。
---
## 跨機器安裝(從零開始)
> 五個 bridge 彼此獨立。先跑 `npm run doctor` 看這台機器有哪些 CLI 可用,**只把對應的 bridge 註冊進 MCP client 設定就好**,其他沒裝的不要加。
### 前置需求
- **Node.js** ≥ 18(需要支援 `node:test` 和 ES module)
- **npm** ≥ 9
### Step 1:Clone & 安裝
```bash
git clone https://github.com/CheerioCorner/cheerio-mcp-bridges.git
cd cheerio-mcp-bridges
npm install
```
### Step 2:檢查哪些 CLI 可用
```bash
npm run doctor # 完整檢查(含 smoke test)
npm run doctor -- --no-smoke # 只解析入口與 --version,完全不花額度
```
會輸出一個表格,告訴你 5 支 CLI 各自:
1. **入口從哪裡解析出來的** — `*_BRIDGE_ENTRY` 環境變數、套件 `package.json` 的 `bin` 欄位、
還是 PATH。doctor 不會猜路徑,三個來源都沒有就明確報「找不到」。
2. **`--version` 過不過**
3. **smoke test 過不過** — 真的送一個極短 prompt 跑完整條路徑,確認 exit code 是 0
而且有非空輸出。
第 3 項是重點:`--version` 在某些 CLI 上會在載入完整相依之前就先印版本退出,所以
「`--version` 過了」不等於「這支 CLI 能用」。2026-09-05 pi 全面失效那次就是這樣 ——
入口檔一 import 就 `ERR_MODULE_NOT_FOUND`,但 `--version` 照樣過。smoke test 每支 CLI
只送一個 prompt,成本可忽略;真的不想花就加 `--no-smoke`。
有任何 bridge 檢查失敗時 doctor 會以 exit code 1 結束,方便掛進 CI 或啟動前檢查。
### Step 3:安裝你需要的 CLI(如果還沒裝)
以下是各 CLI 的安裝與登入方式,**沒裝的跳過就好,不用全部裝**:
#### pi(earendil-works/pi)
```bash
npm install -g @earendil-works/pi-coding-agent
pi # 首次啟動會引導登入
```
驗證:`pi --version` 或 `pi --help`
#### agy(Google Antigravity CLI)
```bash
# 請參考官方文件安裝,通常是一個獨立執行檔
# https://github.com/nicholasareed/antigravity
agy # 首次啟動會引導 Google 帳號授權
```
驗證:`agy --version`
#### codex(OpenAI Codex CLI)
```bash
# 請參考 OpenAI 官方文件安裝
# Windows 通常安裝在 %LOCALAPPDATA%/Programs/OpenAI/Codex/
codex login # 會引導 ChatGPT 帳號授權
```
驗證:`codex --version`、`codex login status`
#### copilot(GitHub Copilot CLI)
```bash
npm install -g @github/copilot-cli
copilot login # 會引導 GitHub 帳號授權
```
驗證:`copilot --version`
#### claude(Anthropic Claude Code CLI)
```bash
npm install -g @anthropic-ai/claude-code
claude # 首次啟動會引導登入(訂閱制走 OAuth)
```
驗證:`claude --version`
> `claude` 是**原生執行檔**(npm 套件的 `bin` 指向 `bin/claude.exe`,各平台的二進位放在
> `optionalDependencies`)。`CLAUDE_BRIDGE_ENTRY` 直接填那個執行檔的絕對路徑,**前面不要加 `node`**。
> 官方安裝腳本會裝到 `%USERPROFILE%\.local\bin\claude.exe`。
### Step 4:選擇性啟用 bridge
把你需要的 bridge 設定從 `mcp-config.example.json` 複製進你的 MCP client 設定(例如 `~/.mcp.json` 或 `.mcp.json`)。
**不要四個全抄。** 只複製你這台機器有裝且有登入的 CLI 對應的區塊,然後調整路徑與環境變數。
例如你只裝了 pi 和 copilot,就只加 `pi-bridge` 和 `copilot-bridge` 兩個區塊。
### Step 5:驗證 bridge 正常運作
啟動你的 MCP client 後,用對應的工具送一個很小的 prompt 測試:
- `ask_pi`:`{ "prompt": "Reply only: pong" }`
- `ask_agy`:`{ "prompt": "Reply only: pong" }`
- `ask_codex`:`{ "prompt": "Reply only: pong" }`
- `ask_copilot`:`{ "prompt": "Reply only: pong" }`
應該要收到 `pong` 回覆和一行 bridge metadata。如果收到錯誤訊息,檢查:
- CLI 執行檔路徑(環境變數 `*_BRIDGE_ENTRY`)是否正確
- CLI 是否已登入
- cwd 環境變數(`*_BRIDGE_CWD`)是否存在
## 快速開始
```bash
cd C:/Cheerio/CheerioCorner/mcp-bridges # 或你 clone 的路徑
npm install
npm run doctor # 檢查哪些 CLI 可用(含 smoke test;--no-smoke 可跳過)
npm test # 執行 parser/arg-builder 單元測試(不花 API 額度)
```
## 註冊到 MCP client
見 `mcp-config.example.json`。那是一份**菜單**——照你這台機器實際有的 CLI,只挑對應的區塊複製進你的 MCP client 設定(`.mcp.json`),並依實際路徑調整。不是四個都要抄。
## 工具介面
### `ask_pi`
| 參數 | 型別 | 預設 | 說明 |
|------|------|------|------|
| `prompt` | string | — | 要送給 pi 的指令(必填) |
| `session_id` | string | 自動產生 | 帶上一次回傳的值即可續接同一對話 |
| `read_only` | boolean | false | true 時只給 `read,grep,find,ls`,禁 edit/write/bash |
| `model` | string | — | 覆寫 model |
| `approve_project` | boolean | false | 是否信任專案本地資源(pi -a) |
| `enable_extensions` | boolean | false | 是否載入 extensions(有掛住風險) |
| `timeout_ms` | number | 300000 | 硬性逾時 |
回傳:pi 的最終文字 + 一行 `pi-bridge metadata`(含 `session_id`)。
### `ask_agy`
| 參數 | 型別 | 預設 | 說明 |
|------|------|------|------|
| `prompt` | string | — | 要送給 agy 的指令(必填) |
| `conversation_id` | string | 自動擷取 | 帶上一次回傳的值即可續接 |
| `model` | string | — | model slug(見 `agy models`) |
| `effort` | low\|medium\|high | — | 推理強度 |
| `sandbox` | boolean | false | 開終端沙箱限制(--sandbox) |
| `dangerously_allow_all` | boolean | false | **危險**:自動批准所有工具權限(含 shell) |
| `timeout_ms` | number | 300000 | 硬性逾時(同步作為 agy --print-timeout) |
回傳:agy 的最終回應 + 一行 `agy-bridge metadata`(含 `conversation_id`、`status`)。
### `ask_codex`
| 參數 | 型別 | 預設 | 說明 |
|------|------|------|------|
| `prompt` | string | — | 要送給 Codex 的指令(必填) |
| `session_id` | string | 自動產生 | 帶上一次回傳的 thread_id 即可續接 |
| `model` | string | — | 覆寫 model(如 `o3`、`codex-mini`) |
| `sandbox` | read-only\|workspace-write\|danger-full-access | read-only | 沙箱策略 |
| `timeout_ms` | number | 300000 | 硬性逾時 |
回傳:Codex 的最終文字 + 一行 `codex-bridge metadata`(含 `thread_id`、`usage`)。
### `ask_copilot`
| 參數 | 型別 | 預設 | 說明 |
|------|------|------|------|
| `prompt` | string | — | 要送給 Copilot 的指令(必填) |
| `session_id` | string | 自動產生 | 帶上一次回傳的值即可續接同一對話 |
| `model` | string | — | 覆寫 model(如 `claude-haiku-4.5`) |
| `effort` | none\|minimal\|low\|medium\|high\|xhigh\|max | — | 推理強度 |
| `max_ai_credits` | number | — | 單次呼叫的花費上限(安全閥) |
| `dangerously_allow_all` | boolean | false | **危險**:加 `--allow-all`(含 paths + urls) |
| `timeout_ms` | number | 300000 | 硬性逾時 |
回傳:Copilot 的最終回應 + 一行 `copilot-bridge metadata`(含 `session_id`、`usage`、`quota_snapshots`)。
### `ask_claude`
| 參數 | 型別 | 預設 | 說明 |
|------|------|------|------|
| `prompt` | string | — | 要送給 Claude 的指令(必填) |
| `session_id` | string | 自動產生 | 帶上一次回傳的值即可續接同一對話 |
| `read_only` | boolean | false | 加 `--restricted`:拿掉 Bash/PowerShell/REPL 與 WebFetch,檔案工具限制在工作目錄內 |
| `allow_edits` | boolean | false | 自動核准檔案編輯(`--permission-mode acceptEdits`) |
| `dangerously_allow_all` | boolean | false | **危險**:`--permission-mode bypassPermissions`,完全不做權限檢查 |
| `model` | string | — | `haiku` / `sonnet` / `opus` 或完整 model id |
| `effort` | low\|medium\|high\|xhigh\|max | — | 推理強度 |
| `max_budget_usd` | number | — | 單次呼叫的花費上限(`--max-budget-usd`) |
| `timeout_ms` | number | 300000 | 硬性逾時 |
回傳:Claude 的最終回應 + 一行 `claude-bridge metadata`(含 `session_id`、`tools_used`、
`permission_denials`、`num_turns`、`cost_usd`、`usage`、`rate_limit_status`)。
> **預設是唯讀的**:不帶任何 flag 時 Claude 讀得到檔案,但任何寫入/執行都會被自動拒絕
> (`--permission-mode manual` + `--permission-prompts none`)。要它真的動手改檔案,
> 必須明確帶 `allow_edits:true`。
> **旗標會依 CLI 版本自動降級**:Claude Code 的旗標名稱會在小版號之間變動 ——
> 2.1.263 有 `--permission-prompts`,2.1.258 沒有,送過去就是
> `unknown option`,整支 bridge 全掛。所以 bridge 啟動時會跑一次 `claude --help`
> (不花額度、不用登入),只送這個版本認得的旗標;被拿掉或替換掉的每一個都會寫一行
> `[claude-bridge] …` 到 stderr,也放在回傳的 `flagWarnings` 裡。權限旗標被拔掉是會改變
> 這次執行能做什麼的事,不能安靜地發生。`npm run doctor` 用同一套機制,並在報表裡列出
> 「版本差異(已自動降級)」。
> **被拒絕的工具呼叫會講出來**:Claude 被擋下寫入時,`is_error` 是 `false`、exit code 是 `0`、
> 回應裡還是會很有自信地說「我改好了」。bridge 會把 `permission_denials` 同時放進 metadata
> **和回應本文**,避免呼叫端相信一件沒有發生的事。這是這個 repo 最在意的那種靜默失敗。
> **不會載入你的 MCP 設定**:bridge 固定帶 `--strict-mcp-config` 和 `--safe-mode`,所以你的
> hooks、plugins、CLAUDE.md 和 MCP servers 都不會載入。**這是必要的** —— 否則 `ask_claude`
> 呼叫出去的 claude 會把 `.mcp.json` 裡的五個 bridge 全部再載一遍。
> 用的是 `--safe-mode` 而不是 `--bare`:`--bare` 的認證只吃 `ANTHROPIC_API_KEY`/`apiKeyHelper`,
> 完全不讀 OAuth 和 keychain,訂閱制登入會直接失敗。
> **額度查詢限制**:Copilot CLI 沒有非互動模式的指令能查詢「剩餘總額度」。`copilot billing` / `copilot limits` 只在互動模式的 UI 裡有用。bridge 只能回報「這次呼叫消耗多少」(`usage` + 當次 `quotaSnapshots`),無法回報剩餘總額度。Codex 同理,`codex login status` 只顯示登入狀態,無用量查詢。
## 環境變數
| 變數 | 必填 | 預設 | 說明 |
|------|------|------|------|
| `*_BRIDGE_CWD`(PI / AGY / CODEX / COPILOT / CLAUDE) | ✅ | 無 | bridge 釘死的工作目錄,絕對路徑。沒設會在啟動時直接報錯 |
| `PI_BRIDGE_ENTRY` | ✅ | 無 | pi 的入口。**必須是 pi 套件 `package.json` 的 `bin` 指向的檔案**(目前是 `dist/bundle/cli.js`),不是 `dist/cli.js` — 詳見下方 |
| `AGY_BRIDGE_ENTRY` | ✅ | 無 | `agy.exe` 絕對路徑 |
| `CODEX_BRIDGE_ENTRY` | ✅ | 無 | `codex.exe` 絕對路徑 |
| `COPILOT_BRIDGE_ENTRY` | ✅ | 無 | `copilot.cmd` 絕對路徑 |
| `CLAUDE_BRIDGE_ENTRY` | ✅ | 無 | `claude.exe` 絕對路徑。**是原生執行檔不是 JS**,不要前面加 node |
| `*_BRIDGE_TIMEOUT_MS`(PI / AGY / CODEX / COPILOT / CLAUDE) | | `300000` | 硬性逾時 |
| `MCP_BRIDGE_LOG_DIR` | | `<repo>/logs` | 稽核 log 目錄 |
| `BRIDGE_BYPASS_PROXY` | | `true` | 見下方說明 |
| `DOCTOR_SMOKE_TIMEOUT_MS` | | `120000` | `npm run doctor` 的 smoke test 逾時 |
所有 `*_BRIDGE_CWD` / `*_BRIDGE_ENTRY` 都**沒有內建預設值**:這些是各機器不同的路徑,
埋一個 fallback 只會讓設錯的人以為自己設對了,然後跑到別人的目錄或不存在的執行檔上。
沒設就在啟動時報錯,錯誤會直接出現在 MCP 連線狀態裡。
### `PI_BRIDGE_ENTRY` 為什麼特別容易設錯
pi 的 `dist/` 底下同時有兩個入口:
- `dist/cli.js` — **未打包**,會 `import` 沒有安裝的 `@earendil-works/pi-server`,
一啟動就 `ERR_MODULE_NOT_FOUND`(2026-09-05 的 3 小時全面失效就是這樣來的)
- `dist/bundle/cli.js` — 相依已打包,這才是 `package.json` 的 `bin` 真正指向的檔案
規則很簡單:**入口以套件自己的 `package.json` `bin` 欄位為準**,不要看 `dist/` 底下
有什麼就填什麼。`npm run doctor` 會替你動態解析出正確路徑,並實際送一個 prompt
驗證它真的跑得起來。
`BRIDGE_BYPASS_PROXY`:bridge 預設會在呼叫底層 CLI 前 strip 掉子進程 env 裡的 `HTTP_PROXY`/`HTTPS_PROXY`/`ALL_PROXY`(含大小寫變體),避免子進程被父進程繼承到的企業 proxy 設定牽連。只有明確設成字串 `"false"` 時才關閉 strip 行為。
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues