# Design：MCP 統一帳號來源 — 單一 remote MCP + .env 切 MCP URL

> 2026-06-06 richblack 拍板（推翻本檔初版的「①工具帶參數 / ②自架 worker / ③平台 routing」三方案）。
> `sdk-and-website/design.md` 的單檔補充（規則 02 §4.3 允許）。
> 來源：壓測報告 §5.1/§5.2/§5.4（薄殼原則）+ richblack 對話釐清。
> 對應鐵律：`.claude/rules/07-thin-shell.md` §4（統一帳號來源）。

---

## 1. 問題（壓測 §5.2）

CLI 已能讀三層 config（env > 專案 `.arcrun.yaml` > 全域）切帳號；**MCP 不能**——MCP 用 Cloudflare
service binding 焊死平台 `arcrun-cypher-executor`，self-hosted 用戶用 MCP 連不到自己的 cypher。

**這違反薄殼鐵律**：「切換帳號」這能力做在了 CLI（介面層），MCP 沒跟上 → 證明它不在 API/共用層。

## 2. 關鍵洞見（richblack）：不需要兩套 transport

> 「self-hosted 用戶也有 CF，把 MCP 放上網對他也沒困難。接案時進客戶專案讀 `.env` 連的是
> 客戶專案的雲端 MCP — 這比保留 stdio + 雲端兩套更單純。」

推論：**所有人都用 remote Worker MCP，差別只在「連哪台 MCP」**。

- **我自己**：全域 config 的 `mcp_url` 指向我自己的 MCP Worker。
- **接案幫客戶**：客戶資料夾 `.arcrun.yaml` / `.env` 放客戶的 `mcp_url`（那台 MCP 綁客戶 cypher）。
  進客戶資料夾 → 自動連客戶那套。
- **SaaS 用戶**：不設 `mcp_url` → fallback 平台預設 MCP，AI 幫他帶 api_key。

→ **不需要 stdio 本機 MCP、不需要 transport 抽象層、不需要把 Worker 改成讀本機檔。**
現有 remote HTTP Worker MCP 形態完全保留。

## 3. 薄殼原則怎麼落地

「身份解析（讀哪個帳號/哪台 MCP）」本質在 client（帳號設定是本機檔，cypher/MCP Worker 讀不到）。
正解不是「API 去讀 .env」（做不到），而是：

> **MCP URL 與 cypher URL 一樣，由同一份 config 解析模組（env > 專案 > 全域）決定。**
> CLI 讀 `cypher_url`，Claude Code 的 MCP 連線讀 `mcp_url`，**同一份 `.arcrun.yaml` / env、同一個解析邏輯**。
> 「切換帳號」這能力只實作一次（config 解析），不再綁死在 CLI。

### 接線：mcp_url → Claude Code 的 MCP 設定
Claude Code 看 `.mcp.json`（專案層）決定連哪台 MCP。所以需要一個東西把
「arcrun config 解析出的 `mcp_url`」寫進專案 `.mcp.json`：

- **`acr mcp-setup`**（新指令）：依三層 config 解析出 `mcp_url`，在 cwd 寫 / 更新 `.mcp.json`。
- `acr init` 順帶呼叫（裝好就有）。
- 「切帳號」= 在客戶資料夾跑 `acr mcp-setup`（讀該資料夾的 `.arcrun.yaml`）→ 產對的 `.mcp.json`。

## 4. 動到的檔案

| 檔案 | 動作 | 狀態 |
|---|---|---|
| `cli/src/lib/config.ts` | `ArcrunConfig` 加 `mcp_url`；`ENV_MAP` 加 `ARCRUN_MCP_URL`；`resolveConfigSources` 含 mcp_url；新增 `DEFAULT_MCP_URL` + `getMcpUrl()` | ✅ 完成 |
| `cli/src/commands/mcp-setup.ts`（新增） | `acr mcp-setup`：依 `getMcpUrl()` 寫專案 `.mcp.json` | 進行中 |
| `cli/src/index.ts` | 註冊 `acr mcp-setup` | 進行中 |
| `cli/src/commands/init.ts` | init 尾端順帶 `acr mcp-setup`（裝好即有） | 進行中 |
| **`arcrun/mcp/`（新目錄）** | MCP 從 sibling repo `matrix/arcrun-mcp` **搬進主庫**（與 cli/ 並列）。形態不變（remote Worker），只是進主庫 → 同 repo、同 deploy 掃描、與 cypher API 對齊 | 進行中 |
| `mcp/wrangler.toml` | name/route 對齊 arcrun 部署慣例（`workers_dev=true`，deploy.yml/local-deploy.sh 自動掃到） | 進行中 |

## 5. MCP 搬進 arcrun/mcp/（不改形態，只進主庫）

- **搬 verbatim**：`src/tools/*`、`src/lib/*`、`src/types.ts`、`src/mcp-handler.ts`、`src/index.ts`、`tests/`。
- **形態不變**：仍是 Hono + `WebStandardStreamableHTTPServerTransport` 的 remote Worker（不加 stdio）。
- **進主庫的理由**：(a) 與 cypher-executor 同 repo → 改 API 時 MCP 薄殼同步可見、一起 review；
  (b) 被 deploy 掃描（wrangler.toml）自動部署，納入 release.feature「推送=全部到位」；
  (c) self-host 用戶 codeload 主庫即得 MCP，能部署自己的 MCP Worker。
- **sibling repo `matrix/arcrun-mcp` 去留**：搬進來後，原 sibling 為歷史/過渡；新開發在 `arcrun/mcp/`。

## 5.5 self-hosted MCP 認證對齊（2026-06-14，HANDOFF §3b）

> 症狀（mira CC 實測 `arcrun-mcp.leo21c.workers.dev`）：self-hosted 用 namespace 明碼連 MCP 一律 401；
> CLL 全通。根因：MCP `middleware/partner-auth.ts` 把 Bearer 拿去 KBDB `/partners/:token/info`
> **驗證**，namespace 明碼非註冊 partner → 401。而 cypher-executor 的 `X-Arcrun-API-Key`
> **不驗證、直接當分區 key**（webhooks-named.ts triggerNamed）→ 這就是「CLI 通、MCP 401」的分歧。

**決策：① MCP self-hosted 繞 partner 驗證 + ② mcp-setup 寫 headers（兩者缺一不可）。**

| | 修法 | 為何必須 |
|---|---|---|
| ① | `partner-auth.ts`：`MULTI_TENANT === 'false'` 時 Bearer = namespace 明碼直接當 `org_namespace`，不打 KBDB partner 驗證（對齊 cypher 的 opaque-key 模型）。官方 SaaS（不設/`"true"`）行為不變 → 官方與 self-host 共用同一份程式碼 | 只做②也沒用：partner 驗證仍擋明碼 |
| ② | `mcp-setup.ts`：把 `config.api_key`（self-hosted 存 namespace 明碼）寫進 `.mcp.json` 的 `headers.Authorization: Bearer …`（與 CLI 同一份身份，rule 07 §4） | 只做①也沒用：裸 `.mcp.json` 不送任何 header |

判定旗標：worker `[vars] MULTI_TENANT`（與 cypher 同名）。

### 5.5.1 部署注入修補（2026-06-15，HANDOFF §3b-2）

> 症狀：①②code 正確、官方帳號測綠，但 mira 推 leo21c 端到端**仍 401**。
> **根因（非 code bug，是部署注入缺口）**：partner-auth.ts `if (c.env.MULTI_TENANT === 'false')`
> 邏輯對，但 worker env 裡 `MULTI_TENANT === undefined`——因為：
> - mcp/wrangler.toml 的 `MULTI_TENANT` 原本是**註解掉的**；
> - `cli/src/lib/deploy.ts` 的 `injectWranglerConfig` 部署時注入了 KV id / WORKER_SUBDOMAIN / D1 id，
>   **但沒注入 MULTI_TENANT** → 部署後 `c.env.MULTI_TENANT===undefined ≠ 'false'` → 走 partner-key → 401。
> - config 源頭早有（init.ts `multi_tenant:false` + `mode:'self-hosted'`），只是沒被注進 worker。
>
> **只取消註解 mcp/wrangler.toml 不夠**——那只修「手動 fork」，沒修「acr update 自動部署」（mira 走後者）。
> 根因要修在 deploy.ts 注入邏輯。

**修法（方案①：注 vars 非 secret，符合 self-hosted 零填寫契約）**：

| 檔案 | 動作 | 狀態 |
|---|---|---|
| `cli/src/lib/deploy.ts` | `DeployContext` 加 `selfHosted?`；新增 export `injectMultiTenant(toml)`（處理 active/註解/無行三態，加進 `[vars]`）；`injectWranglerConfig` 在 `selfHosted` 時呼叫——與 WORKER_SUBDOMAIN/KV 注入同層級 | ✅ 完成 |
| `cli/src/commands/init.ts` | deployCtx 帶 `selfHosted: true`（init 本就是 --self-hosted 分支） | ✅ 完成 |
| `cli/src/commands/update.ts` | ctx 帶 `selfHosted: config.mode==='self-hosted' \|\| config.multi_tenant===false`（mira 走這條） | ✅ 完成 |
| `mcp/wrangler.toml` | `# [vars]`/`# MULTI_TENANT` 改 active `[vars]`（官方不含 MULTI_TENANT=多租戶；注入走 case-3 加行，結構正確在 [vars] 下） | ✅ 完成 |

**本地驗注入（dry-run，真實 export 函式）**：mcp / cypher-executor 注入後各恰 1 行 active `MULTI_TENANT = "false"` 且在 active `[vars]` 之下 → ✓ PASS。cli tsc exit 0。
**端到端（交棒回 mira）**：mira 在 leo21c 重跑 `acr update` 重部 MCP worker（這次帶 MULTI_TENANT=false）→ `curl -H "Authorization: Bearer leo" https://<mcp>.leo21c.workers.dev/mcp` 應 **200 非 401**。官方帳號測不到（不設 MULTI_TENANT）。

### 5.5.0 原始 code 修法（2026-06-14，①②）

| 檔案 | 動作 | 狀態 |
|---|---|---|
| `mcp/src/types.ts` | `Env` 加 `MULTI_TENANT?` | ✅ 完成 |
| `mcp/src/middleware/partner-auth.ts` | self-hosted 分支：Bearer 明碼直接當 org_namespace | ✅ 完成 |
| `cli/src/commands/mcp-setup.ts` | `.mcp.json` 寫 `headers.Authorization` | ✅ 完成 |

驗收：mira 用 namespace 明碼連 self-hosted MCP（待 leo21c 部署後實測，HANDOFF §3）；官方 SaaS MCP partner-key 路徑回歸不變。

## 6. 不在範圍（明確排除）

- ❌ 不加 stdio transport（richblack §2：不需要）。
- ❌ 不把 init / config 搬進 MCP（init 是本機 CF 部署動作，MCP Worker 做不到）。
- ❌ 不在 MCP 重實作 credential 加密 / workflow 執行（server 職責，rule 02 §3.4）。

## 7. 驗收（客觀證據，mindset §7）

1. `cli/` + `mcp/` 各自 `tsc --noEmit` exit 0。
2. `acr mcp-setup` 在含 `.arcrun.yaml`（mcp_url=X）的資料夾 → 產出 `.mcp.json` 指向 X；無 mcp_url → 指向 DEFAULT_MCP_URL。
3. `acr config --where` 顯示 mcp_url 來源層。
4. `mcp/` 被 deploy 掃描掃到（`find wrangler.toml` 命中）。
5. self-hosted 用戶在客戶資料夾 `acr mcp-setup` → Claude Code 連客戶 MCP（端到端待 richblack 實測）。
