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"; /** * 【這條連線上有主人的知識庫】——**靜態、必定出現**的指路段(2026-08-13,leo)。 * * leo 原話:「它應該是**你的知識來源**⋯⋯**沒有它你是瞎的**,從你對 Arcrun 的認知就知道 * 你的內建 Memory 是沒用的。」 * * 為什麼要獨立成一段、而且**不准依賴任何查詢**: * 這條連線本來就查得到答案——`kbdb_search(q="Arcrun 是什麼")` 當天實測回 33 筆真答案。 * 但同一天,一個連著這條 MCP 的 session 為了回答同一題,去讀了 682 行原始碼—— * **因為它不知道這裡有知識庫可查**。instructions 裡唯一提到知識的,是下面那段【藏書地圖】, * 而那段是**選配的**(1500ms 逾時、失敗回 null);那條 portal 連線就沒收到它。 * ⇒ **「這裡有知識庫、怎麼查」這句話本身被綁在一個會無聲消失的段落上,才是 AI 開場全瞎的成因。** * 所以它跟下面的 Arcrun 指路段同級:靜態常數,KBDB 掛了、地圖抓不到,它照樣出現。 */ const KNOWLEDGE_FIRST = [ "【這條連線上有主人的知識庫——先查它,再查別的】", "", "這條 MCP 連線後面接著一個 **KBDB 知識庫**:這台實例的主人長期累積的筆記、決策、", "踩過的坑、專案現況、skill 與工作流紀錄,都在裡面。**你不是從零開始的**——", "你對這些專案的內建印象多半是錯的或過時的,庫裡那份才是主人認的版本。", "", "🔴 **有人問你「X 是什麼/為什麼這樣做/之前怎麼決定的/現在做到哪」——", "你的第一個動作是 `kbdb_search`,不是 grep 原始碼、不是上網搜、不是回答「我不知道」。**", "", '- `kbdb_search({ q: "Arcrun 是什麼" })` — 關鍵字查(預設 `mode:\'keyword\'`,基本盤永遠可用)。', " 換幾組講法再放棄;想要語義相似度用 `mode:'semantic'`。**這一支是你的第一站。**", "- `kbdb_get_map()` — 不知道該進哪個庫時先看藏書地圖(下面若有【藏書地圖】就是它的快照)。", '- `kbdb_graph_neighbors({ subject: "Arcrun" })` — 查某個東西跟誰有關係(三元組遍歷)。', "- `kbdb_list_templates` / `kbdb_query` — 按 template 取整批結構化資料。", "", "🔴 **這三件事不可以講成同一句**(講成同一句就是在騙人):", "① 「知識庫裡沒有」 ② 「我沒查」 ③ 「地圖沒取到/某庫顯示 0」。", "查過真的沒有 → 明說「知識庫裡查不到,以下是我從原始碼/網路推的」,再去讀 code 或上網。", "**沒查就回答=拿你的猜測冒充主人的知識,那是這條連線上最嚴重的錯。**", "", "🔴 **地圖是索引,不是庫存清單**:某庫顯示 `0 triplets`、或下面整段【藏書地圖】沒出現,", "都**不代表**沒有知識(可能只是還沒重算、或這次沒抓到)。要知道有沒有,只有一個方法:`kbdb_search` 查過。", "同理,任何工具回 401/連不上/沒權限,那是**讀不到**,不是**不存在**——照它給的 next_actions 修,", "別把它改口講成「這裡沒有」(`arcrun_get_skill` 曾把 KBDB 的 401 講成「skill 不存在」,就是這個病)。", ].join("\n"); /** 地圖沒拿到時的**明講**(不可靜默):「沒取到」和「這裡沒有知識」不可以長得一樣。 */ const MAP_UNAVAILABLE_NOTE = [ "【藏書地圖:這次沒取到】", "地圖抓取逾時/回錯/或它回報的清單是空的(也可能只是還沒重算過)。", "🔴 **這是「地圖沒拿到」,不是「這裡沒有知識」。** 上面那條規則照舊:", "要知道庫裡有什麼,直接 `kbdb_search`;想再抓一次地圖呼叫 `kbdb_get_map()`(它會回報真正的原因)。", ].join("\n"); /** 舊 token(stale):拿不到地圖是**身分問題**,同樣要明講,並給可執行的修法。 */ const MAP_STALE_NOTE = [ "【藏書地圖:拿不到,因為這條連線是舊版簽發的 token】", "這條連線的 token 沒帶登入者身分,`kbdb_*` 會回 `identity_missing`。", "🔴 **這不代表知識庫是空的**——是這條連線還沒認得你。", "仍然先呼叫一次 `kbdb_search` 確認錯誤碼;若真的是 `identity_missing`,", "請使用者到 claude.ai → Settings → Connectors 把這個 connector 重新連線一次(重新輸入 Portal 帳密),", "**不要改口說「查不到資料」或自己去猜答案。**", ].join("\n"); /** * 組這條連線的 server instructions(`initialize` 時送出,**AI 沒辦法自己再要一次** ⇒ 必須一次到位)。 * * 匯出給測試:三種情境(地圖抓得到/抓不到/舊 token)各印一份完整字串,逐份確認 * 「一個什麼都不知道的 AI 讀完,下一個動作會不會是去查知識庫」。 */ export async function buildServerInstructions( env: Env, identity: KnowledgeIdentity, ): Promise { // library-map SDD M4(design §4/§6):連線時把全館藏書地圖嵌進 server instructions, // session 一開就知道館裡有哪些庫(push 零查詢)。builder 內建 timeout+isolate TTL 快取 //(選型理由見 lib/library-map.ts 檔頭);任何失敗回 null → 絕不擋 MCP 連線(鐵律)。 // // 🔴 2026-08-12:以帳密連線時**改用登入者的身分**組地圖——否則 instructions 會把 // 整個知識庫的庫名一次推給一個可能只有部分權限的帳號(地圖本身就是情報)。 // 快取也因此改成 per-session key(見 lib/library-map.ts)。 // // 🔴 2026-08-13:失敗**不再靜默略過**。原本 null → 整段消失,於是「地圖沒取到」與 // 「這裡沒有知識」在 AI 眼裡長得一模一樣(Arcrun#109 同一族:保險拒絕了 vs 程式碼不存在, // 畫面上都是沉默)。現在改成印一句明話。鐵律沒變——**失敗仍然不擋連線**,只是不再無聲。 const mapInstructions = await buildLibraryMapInstructions(env, identity); // 2026-07-30(leo 問「人類說『幫我用 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')` — **必讀第一支**。", " 教你用 `>>` 寫「意圖工作流」。你**不需要先知道有哪些零件**,先寫意圖。", " ⚠️ 這支若回錯(401/連不上/`kbdb_unreachable`),那是**這條連線讀不到 KBDB**,", " **不是 skill 不存在**——同一份內容用 `kbdb_search({ q: 'skill-write_intent_workflow' })` 撈得到。", "2. `arcrun_whoami()` — 確認連到哪個帳號(勿自行 curl 猜帳號 URL)。", "3. 把意圖丟 `POST /cypher/search` 或 `arcrun_validate_yaml` — 系統告訴你哪些零件存在。", "4. 卡住/不知道該查什麼 → 先 `arcrun_list_skills()` **看這台實例真的有哪幾支**,再挑一支讀。", " (2026-08-13 實測:不同實例 seed 的 skill 不一樣,有的實例只有兩支、連 `INDEX` 都沒有。", " **不要照教材直接指名一個 slug** ——先列清單,或 `kbdb_search({ q: 'skill' })` 直接在庫裡找。)", "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//executions`)。**走 true 路時 false 路的節點不出現=正確行為**,", "不是失敗——別因為「只有一條路有輸出」就以為壞掉而改寫成 code(2026-08-01 實撞)。", "第一個節點固定是 `input`。", ].join("\n"); // 地圖那段永遠有東西可印:拿到 → 印地圖;沒拿到 → 印「沒拿到」,不是消失。 // stale 與「抓失敗」分開講,因為修法不同(前者要使用者重新連線,後者只是這次沒抓到)。 const mapSection = mapInstructions ?? (identity.kind === "stale" ? MAP_STALE_NOTE : MAP_UNAVAILABLE_NOTE); // 知識段排在 Arcrun 指路段之前:AI 最常被問的是「X 是什麼」,那題的正解是查庫,不是查零件。 return [KNOWLEDGE_FIRST, startHere, mapSection].join("\n\n---\n\n"); } export async function handleMcpRequest( request: Request, env: Env, orgNamespace: string, partnerToken: string, identity: KnowledgeIdentity, ): Promise { const instructions = await buildServerInstructions(env, identity); 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); }