feat(mcp): kbdb_graph_neighbors — graph 鄰居查詢薄殼(#68)

MCP 面補齊 D17 KBDB RAG 套餐第三模式(關鍵字/語義/圖):
- 新 tool kbdb_graph_neighbors:subject(必填)/depth(預設 1)/kbdb_base/
  template(預設 graph_triplet)/directed,形狀對齊
  registry/examples/graph-neighbors/workflow.yaml 的 input
- 薄殼調 GET /q/{orgNamespace}/graph_neighbors 同步查詢端點(#28 地基),
  走既有 CYPHER_EXECUTOR service binding,不新增 binding、不碰 D1、
  不動 workflow 引擎本體
- workflow 未部署 → 404 誠實回 workflow_not_installed +安裝指引,不 crash;
  HTTP 200 但 workflow 層 success:false 也不假綠
- graph_traverse 不加:repo 內無該 workflow 定義可對齊 input 形狀,不猜
- 測試:tests/unit/tools/kbdb-graph.test.ts(7 測,全綠);tsc --noEmit 乾淨;
  mcp 全套 vitest 59/59 綠

merge 後需 gated redeploy arcrun-mcp(leo 閘)。

關聯 #68

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JUmjwkHLVBHM3ydhT1WSW3
This commit is contained in:
Claude
2026-07-19 07:55:16 +00:00
parent 2324537165
commit 7eeab9f26f
4 changed files with 336 additions and 1 deletions
+158
View File
@@ -0,0 +1,158 @@
/**
* KBDB graph 查詢 MCP 薄殼(issue #68D17 KBDB MCP 面)
*
* rule 07 §5(薄殼鐵律):能力長在 workflowgraph_neighborsregistry/examples/graph-neighbors),
* MCP 只做介面轉換 + 暴露,無業務邏輯——不碰 D1、不動 workflow 引擎本體。
*
* D17「KBDB MCPRAG 套餐」三模式:關鍵字(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)、templatetriplet template 名)、namespaceowner_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.yamlcall arcrun_push_workflow 部署(name 必須是 graph_neighbors",
"或用 CLIacr 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_traverserepo 內目前只有 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 workflownode/depth/template/namespace/kbdb_base/directed)。
const query: Record<string, string | number> = {
node: subject,
depth: depth ?? 1,
template: template ?? "graph_triplet",
namespace: orgNamespace,
kbdb_base,
};
if (directed) query.directed = "true";
// /q/:ns/:namenamespace 走 pathcypher opaque-key 模型,orgNamespace 即分區 key),
// 走既有 CYPHER_EXECUTOR service bindingcypherFetch),不新增 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} workflowgraph 查詢無法使用`,
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 fetch1042 陷阱:優先 custom domain",
"確認 template 名對得上實際 triplet templatekbdb_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), ["稍後重試"]);
}
},
);
}
+4
View File
@@ -18,6 +18,7 @@ import { registerAllWorkflowCrudTools } from "./arcrun_workflow_crud.js";
import { registerAllSkillExampleTools } from "./arcrun_skills_examples.js";
import { registerAllRecipeTools } from "./arcrun_recipe.js";
import { registerAllKbdbDataTools } from "./kbdb_data.js";
import { registerAllKbdbGraphTools } from "./kbdb_graph.js";
import { registerWhoami } from "./arcrun_whoami.js";
export function registerAllTools(server: McpServer, env: Env, orgNamespace: string, partnerToken: string) {
@@ -49,6 +50,9 @@ export function registerAllTools(server: McpServer, env: Env, orgNamespace: stri
// kbdb-base Phase 9.1: KBDB 資料層薄殼(template/record/query/searchHANDOFF §2
// 鐵律:不提供建表/SQL toolAI 只有 template+slot 可用(類 Supabase 萬用表)
registerAllKbdbDataTools(server, env);
// issue #68: KBDB graph 查詢薄殼(kbdb_graph_neighbors,調 /q/:ns/graph_neighbors 同步查詢端點)
// 補齊 D17「KBDB MCP=RAG 套餐」第三模式:關鍵字/語義之外的圖(關係遍歷)
registerAllKbdbGraphTools(server, env, orgNamespace);
// §7.8 P1 D2: whoami(與 CLI acr whoami 對齊,AI 不繞 CLI 自己 curl 猜帳號)
registerWhoami(server, env, orgNamespace);
}