/** * KBDB graph 查詢 MCP 薄殼(issue #68,D17 KBDB MCP 面) * * rule 07 §5(薄殼鐵律):能力長在 workflow(graph_neighbors,registry/examples/graph-neighbors), * MCP 只做介面轉換 + 暴露,無業務邏輯——不碰 D1、不動 workflow 引擎本體。 * * D17「KBDB MCP=RAG 套餐」三模式:關鍵字(kbdb_search keyword)/語義(kbdb_search semantic) * /圖(本檔)。本檔補上 MCP 面缺的第三模式:關係遍歷(1-hop/N-hop 鄰居)。 * * 走 #28 地基的同步查詢端點(cypher-executor webhooks-named.ts): * GET /q/:ns/:name — 同步執行 workflow,直接回「最終節點輸出」本身(200 直出,非 202、非信封)。 * 本工具打 GET /q/{orgNamespace}/graph_neighbors(經既有 CYPHER_EXECUTOR service binding, * 不新增 binding),namespace 用 MCP token 解析出的 orgNamespace(與 whoami 同源)。 * * workflow 未部署(用戶沒裝 graph_neighbors)→ 404 → 誠實回錯誤+怎麼裝,不 crash(鐵律)。 * * graph_neighbors workflow 的 input 形狀(registry/examples/graph-neighbors/workflow.yaml): * node(起點)、depth(預設 1)、template(triplet template 名)、namespace(owner_id)、 * kbdb_base(呼叫者自己的 KBDB 對外 URL——workflow 刻意不寫死任何一家的庫)、directed。 */ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; import type { Env } from "../types.js"; import { cypherFetch, errorResponse, successResponse } from "../lib/cypher-client.js"; /** graph 查詢 workflow 名(與 registry/examples/graph-neighbors/workflow.yaml 的 name 一致)。 */ export const GRAPH_NEIGHBORS_WORKFLOW = "graph_neighbors"; /** 未裝 workflow 時的安裝指引(404 與內層錯誤共用,訊息一致)。 */ const INSTALL_HINTS = [ `graph 查詢由「${GRAPH_NEIGHBORS_WORKFLOW}」workflow 承載(薄殼設計,MCP 本身不算圖)`, "安裝:拿 registry/examples/graph-neighbors/workflow.yaml,call arcrun_push_workflow 部署(name 必須是 graph_neighbors)", "或用 CLI:acr push registry/examples/graph-neighbors/workflow.yaml", ]; /** 註冊全部 KBDB graph 查詢工具(issue #68)。 */ export function registerAllKbdbGraphTools(server: McpServer, env: Env, orgNamespace: string) { registerGraphNeighbors(server, env, orgNamespace); // graph_traverse:repo 內目前只有 graph-neighbors 有 workflow 定義(registry/examples/), // traverse 尚無可對齊的 input 形狀 → 不猜、不過度工程;等 workflow 進 registry 再加薄殼。 } /** * kbdb_graph_neighbors — knowledge graph 1-hop/N-hop 鄰居查詢。 * 薄殼調 GET /q/{ns}/graph_neighbors,結果(最終節點輸出)原樣回給 MCP client。 */ export function registerGraphNeighbors(server: McpServer, env: Env, orgNamespace: string) { server.tool( "kbdb_graph_neighbors", "knowledge graph 鄰居查詢(1-hop/N-hop 關係遍歷):給一個節點名,沿 KBDB triplet" + "(subject-predicate-object)記錄做 BFS,回傳 depth 跳內的鄰居清單" + "([{node, predicate, from, depth}])。與 kbdb_search(關鍵字/語義)互補:" + "找「跟 X 有關係的東西」用本工具,找「內容含關鍵字的東西」用 kbdb_search。" + "需要 namespace 裡已部署 graph_neighbors workflow(沒裝會回安裝指引,不會 crash)。", { subject: z.string().min(1).describe( "起點節點名(graph triplet 的 subject/object 值),如 'Arcrun'", ), depth: z.number().int().min(1).max(10).optional().describe( "最大跳數(N-hop),預設 1(只看直接鄰居)", ), kbdb_base: z.string().min(1).describe( "你自己部署的 KBDB 對外 base URL(如 https://arcrun-kbdb.<你的subdomain>.workers.dev " + "或 KBDB custom domain)。workflow 刻意不寫死任何一家的庫——" + "帶錯(或照抄別人的值)=查詢打進別人的庫", ), template: z.string().optional().describe( "triplet 記錄的 template 名,預設 'graph_triplet'(以實際部署的 kbdb-graph-plugin " + "triplet template 為準,不確定可 kbdb_list_templates 查)", ), directed: z.boolean().optional().describe( "true=只走 subject→object 有向邊;預設 false(把 triplet 當雙向邊,無向鄰居)", ), }, async ({ subject, depth, kbdb_base, template, directed }) => { if (!orgNamespace) { return errorResponse( "no_namespace", "此 MCP 連線沒有解析出 namespace,無法定位 graph workflow", ["call arcrun_whoami 確認身份", "確認 MCP token / OAuth 設定正確"], ); } try { // input 形狀對齊 graph_neighbors workflow(node/depth/template/namespace/kbdb_base/directed)。 const query: Record = { node: subject, depth: depth ?? 1, template: template ?? "graph_triplet", namespace: orgNamespace, kbdb_base, }; if (directed) query.directed = "true"; // /q/:ns/:name:namespace 走 path(cypher opaque-key 模型,orgNamespace 即分區 key), // 走既有 CYPHER_EXECUTOR service binding(cypherFetch),不新增 binding。 const res = await cypherFetch( env, `/q/${encodeURIComponent(orgNamespace)}/${GRAPH_NEIGHBORS_WORKFLOW}`, { apiKey: orgNamespace, query }, ); if (res.status === 404) { // workflow 沒裝 → 誠實 + 給安裝路徑(鐵律:不 crash、不假綠)。 return errorResponse( "workflow_not_installed", `namespace「${orgNamespace}」尚未部署 ${GRAPH_NEIGHBORS_WORKFLOW} workflow,graph 查詢無法使用`, INSTALL_HINTS, ); } const bodyText = await res.text(); let data: unknown = null; try { data = bodyText ? JSON.parse(bodyText) : null; } catch { data = bodyText; } if (!res.ok) { // 409=paused(無法同步查詢)、413=輸出過大、500=執行失敗,端點都回 {error,...}。 const err = (data ?? {}) as { error?: string }; return errorResponse( "graph_query_failed", `graph 查詢失敗 HTTP ${res.status}: ${err.error ?? "unknown"}`, [ "確認 kbdb_base 是你自己 KBDB 的對外 URL 且可被 cypher-executor fetch(1042 陷阱:優先 custom domain)", "確認 template 名對得上實際 triplet template(kbdb_list_templates)", "call arcrun_list_recent_executions('graph_neighbors') 看 trace", ], typeof data === "string" ? data : JSON.stringify(data), ); } // 200 = workflow 跑完,body 即最終節點輸出本身。但 workflow 內部仍可能回 // { success:false, error }(如缺參數)——不把它假裝成成功結果。 const out = data as { success?: boolean; error?: string; count?: number } | null; if (out && typeof out === "object" && out.success === false) { return errorResponse( "graph_query_failed", out.error ?? "graph_neighbors workflow 回報失敗", ["確認 subject 非空", "確認參數形狀(depth 為正整數)"], JSON.stringify(out), ); } // 原樣回給 MCP client(薄殼:不加工、不重排)。 return successResponse(out, [ `${out?.count ?? 0} 個鄰居(depth 上限 ${depth ?? 1})`, "count=0 且不確定資料有沒有進圖:kbdb_query(template='graph_triplet') 看 triplet 記錄", "找關鍵字內容改用 kbdb_search;取單筆全文用 kbdb_get_record", ]); } catch (e) { return errorResponse("internal_error", e instanceof Error ? e.message : String(e), ["稍後重試"]); } }, ); }