Files
Arcrun/mcp/src/mcp-handler.ts
T
uncle6me-web 10d150ac2b fix(mcp): MCP 用登入者的身分查詢,不再去找一把服務內部金鑰
leo 2026-08-12:「人類進 Portal 輸入帳密表示你是主人,可以查到你權限所有東西;
AI 透過輸入帳密的 MCP 查詢表示是授權的 AI,可以查到主人允許查的任何東西。」
「掛上 MCP 並輸入帳密,那個動作本身就是授權」⇒ 下游不得再要求第二次認證。

病根(不是金鑰沒同步,是身分沒接住):
  oauth/routes.ts 驗完 Portal 帳密只留下 `loginOk = res.ok` 一個布林值,身分當場丟棄,
  namespace 改從 `MCP_OWNER_NAMESPACE || "leo"` 拿。於是查詢時手上沒有身分可帶,
  只好用 KBDB_INTERNAL_TOKEN 直打 KBDB——那條路繞過 portal 所有庫過濾,
  而且不管誰登入都看到同一格、看到全部。CLI 也從不注入 MCP_OWNER_NAMESPACE,
  所以那個 "leo" 預設值是每台實例的實際行為,不是理論上的邊角。

修法(走既有那條路,不發明新的):
1. 接住身分:/authorize 解析 /portal/login 回應,把 portal session token +
   display_name/role/libraries 存進 authorization code → access token。
   /portal/login 補回 session_expires_in,access_token TTL 夾成
   min(自己的 TTL, portal session TTL)——不讓「MCP 還連著、底下 session 早死」。
   cypher 回 200 但沒給 session_token(舊版)→ 不發碼,不簽一張沒有身分的 token。
2. 攜帶身分:kbdb_* 全部改走 cypher `/portal/data/*`,Authorization 帶登入者的
   session。庫過濾/租戶注入/停用即時生效全在 server 側,與人類走 portal 網頁同一道閘。
   kbdb_graph_neighbors 因此不再需要 kbdb_base(server 自己知道查哪個庫)。
   藏書地圖(含連線時注入 instructions 的那份)同樣只回有權限的庫,快取改 per-session
   分格——地圖本身就是情報,不能讓先連上的人把視野留給下一個。
3. fail-closed:舊 token 沒有身分 → 誠實要求重新連線,不偷偷退回服務金鑰那條老路。
   服務級憑據(static token / partner key)維持既有 KBDB 直連,arcrun_* 零回歸。

新增 cypher portal 資料面端點(能力長在 API,MCP 只暴露;rule 07):
  GET  /portal/data/map、/portal/data/map/:library
  GET  /portal/data/templates、POST /portal/data/templates
  GET  /portal/data/records/by-template/:t、GET /portal/data/records/:id
  POST /portal/data/records
全部:呼叫端自帶 owner_id 一律不生效;越權與不存在同回 404;寫入 owner_id 由 server 定死。

KBDB base:`GET /records/:id` 與 by-template 補回 owner_id 欄位——原本不回,
呼叫端無從判斷「這筆是不是我的」,按 id 直讀等於沒有租戶邊界。

沒動:KBDB fail-closed 閘、任何金鑰、租戶字串仍不下發給呼叫端。

驗證:
  mcp        tsc 綠;vitest 113/113 綠(改前 48 綠 29 紅)
  cypher     vitest 400 綠 / 14 紅,14 紅與 base commit a24f291 逐條相同(既有)
  kbdb       vitest 208 綠 / 5 紅,5 紅同為既有(migrations/*.sql 被 gitignore)
  端到端     ◐ 未驗:需部署到 leo21c,那道閘要 leo 親手解(見 PR)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 19:33:12 +08:00

74 lines
4.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
import { registerAllTools } from "./tools/registry.js";
import { buildLibraryMapInstructions } from "./lib/library-map.js";
import type { KnowledgeIdentity } from "./lib/portal-client.js";
import { Env } from "./types.js";
export async function handleMcpRequest(
request: Request,
env: Env,
orgNamespace: string,
partnerToken: string,
identity: KnowledgeIdentity,
): Promise<Response> {
// library-map SDD M4design §4/§6):連線時把全館藏書地圖嵌進 server instructions
// session 一開就知道館裡有哪些庫(push 零查詢)。builder 內建 timeoutisolate TTL 快取
//(選型理由見 lib/library-map.ts 檔頭);任何失敗回 null → 靜默略過,絕不擋 MCP 連線(鐵律)。
//
// 🔴 2026-08-12:以帳密連線時**改用登入者的身分**組地圖——否則 instructions 會把
// 整個知識庫的庫名一次推給一個可能只有部分權限的帳號(地圖本身就是情報)。
// 快取也因此改成 per-session key(見 lib/library-map.ts)。
const mapInstructions = await buildLibraryMapInstructions(env, identity);
// 2026-07-30leo 問「人類說『幫我用 arcrun 寫 xxx』,Haiku 會知道要用這些資源嗎?
// 如果不會,要寫什麼在外面讓它一聽到就知道?」):
// 實測答案是**不會**——AI 只看到 41 個工具名,會自己猜(很可能直接跳到 push_workflow
// 瞎編,或去讀 registry/examples 那 8/13 引用不存在零件的壞範例)。
// instructions 是唯一「AI 一連上就必看」的欄位 ⇒ 開場就指路,不依賴任何查詢成功。
// ⚠️ 這段是靜態常數:即使 KBDB 掛了、藏書地圖抓不到,它也必須出現(鐵律:不擋連線)。
const startHere = [
"# Arcrun — 你已經配備了這套工具,別上網找",
"",
"**Arcrun 是什麼**:跑在 Cloudflare 上的工作流引擎(類 n8n)。你用 `>>` 寫「意圖」,",
"系統告訴你有哪些現成零件/recipe 可用,你只要填 payload——**不必自己寫程式**。",
"**你現在就有完整能力**:查零件、查 recipe、看實跑過的 workflow、部署、觸發、看執行紀錄。",
"⚠️ **不要上網搜 Arcrun 文件**(網路上沒有/會過時)。答案都在下面的工具裡。",
"",
"【先讀這裡】要在 Arcrun 上做任何事(用戶說「幫我用 Arcrun 做 X」),**照這個順序**",
"",
"1. `arcrun_get_skill('write_intent_workflow')` — **必讀第一支**。",
" 教你用 `>>` 寫「意圖工作流」。你**不需要先知道有哪些零件**,先寫意圖。",
"2. `arcrun_whoami()` — 確認連到哪個帳號(勿自行 curl 猜帳號 URL)。",
"3. 把意圖丟 `POST /cypher/search` 或 `arcrun_validate_yaml` — 系統告訴你哪些零件存在。",
"4. 卡住/不知道該查什麼 → `arcrun_get_skill('INDEX')`(全館導航:什麼問題查哪裡+已知的坑)。",
"5. 缺零件時:缺 API → 寫 recipe`arcrun_recipe_push`);缺能力 → 投稿零件 PR。",
" 🔴 **不要因為查不到零件就改寫成 `code` 節點**——那叫「腹語術」(表面用 Arcrun、",
" 實際全寫 JS)。`code` 只用於局部整形(例:剝掉 LLM 回應的雜訊)。",
"",
"邊:`ON_SUCCESS`(成功往下)、`對每個 <變數>`FOREACH)、",
"以及**條件分支**2026-08-01 起引擎支援):",
"`ON_TRUE``ON_FALSE`(配 `if_control`)、`ON_BRANCH``branch:` 標籤",
"(配 `switch` 的每個 case`try_catch` 的 try·catch)。",
"🔴 **需要判斷時用分支邊,不要寫 code 判斷**——查零件的回應會附 `branch_hint`",
"(哪些邊型+可照抄範例),照著接即可。",
"🔴 **分支跑完怎麼判斷成功**:看 `verdict``arcrun_get_execution_trace` 或",
"`GET /workflows/<name>/executions`)。**走 true 路時 false 路的節點不出現=正確行為**,",
"不是失敗——別因為「只有一條路有輸出」就以為壞掉而改寫成 code(2026-08-01 實撞)。",
"第一個節點固定是 `input`。",
].join("\n");
const instructions = mapInstructions ? `${startHere}\n\n---\n\n${mapInstructions}` : startHere;
const transport = new WebStandardStreamableHTTPServerTransport({ sessionIdGenerator: undefined });
const server = new McpServer(
{ name: "arcrun-mcp-server", version: "1.0.0" },
{ instructions },
);
registerAllTools(server, env, orgNamespace, partnerToken, identity);
await server.connect(transport);
return transport.handleRequest(request);
}