/** * Skills + Examples lookup MCP tools — LI SDD M3.2 / M3.4 * * 對應 docs/3-specs/llm-interface/ Milestone 3.2 + 3.4。 * * - arcrun_list_skills — 列 KBDB entry_type=agent-skill 全部 * - arcrun_get_skill — 用 slug 拿 skill markdown 全文 * - arcrun_list_examples — 列 KBDB entry_type=workflow-example 全部 * - arcrun_get_example — 用 slug 拿 example yaml + description + tags * - arcrun_search_examples — use case 關鍵字 → 命中相關 example * * Skills / examples 由 arcrun/scripts/sync-registry-to-kbdb.py 從 * arcrun/registry/{skills,examples} 同步進 KBDB。 * * 直接走 KBDB service binding(既有 pattern),不經 cypher-executor。 * * 2026-06-14 重寫:KBDB 降基本盤後(三表 entries/templates/records,無 v3 blocks 表、 * 無語義 search),原打 /blocks /search 的舊路徑全失效。改打基本盤 /entries: * - entry_type 取代 blocks 的 type 欄(entries 表原生有 entry_type/page_name/tags_json/metadata_json) * - GET /blocks?type=X → GET /entries?entry_type=X * - GET /blocks?page_name=Y → GET /entries?page_name=Y(base listEntries 加了 page_name 過濾) * - POST /search(語義) → GET /entries/search?q=(D1 LIKE 關鍵字,基本盤無語義; * 誠實降級:search_examples 現在是「關鍵字」非「語義」。embed 模組(kbdb-base Phase 1) * 上線後只換內部、工具簽名不變。 */ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; import type { Env } from "../types.js"; import { kbdbFetch } from "../lib/kbdb-client.js"; import { errorResponse, successResponse } from "../lib/cypher-client.js"; // 基本盤 entries row(與舊 v3 block 欄位 1:1,差別只在 type→entry_type) interface KbdbBlock { id: string; page_name?: string | null; content?: string | null; entry_type?: string; tags_json?: string; metadata_json?: string | null; source?: string | null; updated_at?: number; } async function kbdbList(env: Env, entryType: string, limit = 100): Promise { const resp = await kbdbFetch(env, `/entries?entry_type=${encodeURIComponent(entryType)}&limit=${limit}`); if (!resp.ok) throw new Error(`KBDB list entry_type=${entryType} HTTP ${resp.status}`); const data = await resp.json<{ entries?: KbdbBlock[] }>(); return data.entries ?? []; } async function kbdbGetByPageName(env: Env, pageName: string): Promise { const resp = await kbdbFetch(env, `/entries?page_name=${encodeURIComponent(pageName)}&limit=1`); if (!resp.ok) return null; const data = await resp.json<{ entries?: KbdbBlock[] }>(); return data.entries?.[0] ?? null; } function parseTags(tagsJson?: string): string[] { if (!tagsJson) return []; try { const arr = JSON.parse(tagsJson); return Array.isArray(arr) ? arr : []; } catch { return []; } } export function registerListSkills(server: McpServer, env: Env) { server.tool( "arcrun_list_skills", "列所有 agent-skill blocks(從 arcrun/registry/skills/ 同步進 KBDB)。每個 skill 是個 markdown playbook,描述 AI 面對 X 問題該怎麼想 + 該用哪個 example。回 [{slug, title, tags}]。call get_skill(slug) 拿完整內文。", { tag: z.string().optional().describe("optional 標籤過濾。如 'rag' / 'watcher' / 'debug'"), }, async ({ tag }) => { try { const blocks = await kbdbList(env, "agent-skill", 100); const skills = blocks .map((b) => { const tags = parseTags(b.tags_json); let title = b.page_name?.replace(/^skill-/, "") ?? "(no title)"; try { const meta = b.metadata_json ? JSON.parse(b.metadata_json) : null; if (meta?.title) title = meta.title; } catch {} return { slug: b.page_name?.replace(/^skill-/, "") ?? "", page_name: b.page_name, title, tags, chars: (b.content ?? "").length, }; }) .filter((s) => !tag || s.tags.includes(`skill:${tag}`) || s.tags.includes(tag) || s.slug.includes(tag)); return successResponse( { count: skills.length, skills }, [ skills.length === 0 ? "沒有 skill 命中。試 list_skills() 不帶 tag 看全部" : "call arcrun_get_skill(slug) 拿單個 skill 完整 markdown", ], ); } catch (e) { return errorResponse( "fetch_failed", e instanceof Error ? e.message : String(e), ["稍後重試", "若持續失敗,告訴 leo"], ); } }, ); } export function registerGetSkill(server: McpServer, env: Env) { server.tool( "arcrun_get_skill", "拿單一 agent-skill 完整 markdown playbook。slug 從 list_skills 取得。", { slug: z.string().describe("skill slug,例如 'build_watcher_workflow' / 'rag_with_arcrun'"), }, async ({ slug }) => { try { const pageName = slug.startsWith("skill-") ? slug : `skill-${slug}`; const block = await kbdbGetByPageName(env, pageName); if (!block) { return errorResponse( "not_found", `skill "${slug}" 不存在`, [ "call arcrun_list_skills() 看可用 slug", "確認拼字正確(不需要 'skill-' prefix)", ], ); } return successResponse({ slug, page_name: block.page_name, content: block.content, tags: parseTags(block.tags_json), }); } catch (e) { return errorResponse( "fetch_failed", e instanceof Error ? e.message : String(e), ["稍後重試"], ); } }, ); } export function registerListExamples(server: McpServer, env: Env) { server.tool( "arcrun_list_examples", "列所有 workflow-example blocks(從 arcrun/registry/examples/ 同步進 KBDB)。每個 example 是可直接 push 的 workflow YAML 範本 + description。回 [{slug, tags}]。call get_example / search_examples 拿細節。", { tag: z.string().optional().describe("optional 標籤過濾。如 'rag' / 'cron' / 'llm' / 'webhook'"), }, async ({ tag }) => { try { const blocks = await kbdbList(env, "workflow-example", 200); const examples = blocks .map((b) => { const tags = parseTags(b.tags_json); return { slug: b.page_name?.replace(/^example-/, "") ?? "", page_name: b.page_name, tags, chars: (b.content ?? "").length, }; }) .filter((e) => !tag || e.tags.includes(tag) || e.tags.includes(`example:${tag}`) || e.slug.includes(tag)); return successResponse( { count: examples.length, examples }, [ examples.length === 0 ? "沒有 example 命中。試 list_examples() 不帶 tag 看全部" : "call arcrun_get_example(slug) 拿單個 YAML + description", ], ); } catch (e) { return errorResponse( "fetch_failed", e instanceof Error ? e.message : String(e), ["稍後重試"], ); } }, ); } export function registerGetExample(server: McpServer, env: Env) { server.tool( "arcrun_get_example", "拿單一 workflow-example 完整 YAML + description。slug 從 list_examples / search_examples 取得。可直接拿 YAML 改成你自己的 → push。", { slug: z.string().describe("example slug,例如 'rag-search-answer' / 'cron-watcher'"), }, async ({ slug }) => { try { const pageName = slug.startsWith("example-") ? slug : `example-${slug}`; const block = await kbdbGetByPageName(env, pageName); if (!block) { return errorResponse( "not_found", `example "${slug}" 不存在`, [ "call arcrun_list_examples() 看可用 slug", "或 arcrun_search_examples(use_case) 用自然語言找", ], ); } let description_md = ""; try { const meta = block.metadata_json ? JSON.parse(block.metadata_json) : null; description_md = meta?.description_md ?? ""; } catch {} return successResponse({ slug, page_name: block.page_name, workflow_yaml: block.content, description_md, tags: parseTags(block.tags_json), }, [ "拿 workflow_yaml 改成你自己的 → call arcrun_push_workflow", "看 description_md 了解設計意圖 / 改造方向", ]); } catch (e) { return errorResponse( "fetch_failed", e instanceof Error ? e.message : String(e), ["稍後重試"], ); } }, ); } export function registerSearchExamples(server: McpServer, env: Env) { server.tool( "arcrun_search_examples", "用 use case 關鍵字搜 workflow examples,回最相關 N 個。" + "注意:基本盤目前是 D1 LIKE 關鍵字搜尋(非語義 embedding;語義是 kbdb-base Phase 1 的 embed 模組,尚未上)。" + "→ 用具體詞('email'、'cron'、'rag')比整句自然語言命中率高。也會比對 slug/tag。", { query: z.string().min(2).describe("use case 關鍵字,例如 'email 摘要' / 'cron 排程' / 'rag'。基本盤是關鍵字非語義,用詞要具體"), top_k: z.number().int().min(1).max(20).optional().describe("回幾個結果(預設 5)"), }, async ({ query, top_k }) => { try { const k = top_k ?? 5; const q = query.trim(); // 基本盤無語義 search:撈全部 workflow-example,用 query 對 content/slug/tag 做關鍵字比對排序。 // (examples 只有 ~10 筆,client 端過濾零負擔;embed 模組上線後可改打語義 search) const blocks = await kbdbList(env, "workflow-example", 200); const ql = q.toLowerCase(); const terms = ql.split(/\s+/).filter(Boolean); const scored = blocks .map((b) => { const slug = b.page_name?.replace(/^example-/, "") ?? ""; const tags = parseTags(b.tags_json); const hay = `${slug} ${tags.join(" ")} ${(b.content ?? "")}`.toLowerCase(); // 每個 term 命中 +1;slug/tag 命中額外加權 let score = 0; for (const t of terms) { if (hay.includes(t)) score += 1; if (slug.toLowerCase().includes(t)) score += 2; if (tags.some((tag) => tag.toLowerCase().includes(t))) score += 2; } return { b, slug, tags, score }; }) .filter((r) => r.score > 0) .sort((a, b) => b.score - a.score) .slice(0, k); const examples = scored.map((r) => ({ slug: r.slug, page_name: r.b.page_name, score: r.score, tags: r.tags, preview: (r.b.content ?? "").slice(0, 200), })); if (examples.length === 0) { return successResponse( { count: 0, examples: [], query: q }, [ "關鍵字沒命中(基本盤是 LIKE 非語義,換更具體/不同的詞再試)", "改用 arcrun_list_examples(tag='...') 走 tag 過濾", "或 arcrun_list_examples() 看全部清單自己挑", ], ); } return successResponse( { count: examples.length, examples, query: q, search_mode: "keyword" }, [ "call arcrun_get_example(slug) 拿完整 YAML", "score 高 = 關鍵字命中越多(slug/tag 命中加權)", "search_mode:keyword — 基本盤無語義,命中靠字面;換具體詞可改善", ], ); } catch (e) { return errorResponse( "internal_error", e instanceof Error ? e.message : String(e), ["重試一次"], ); } }, ); } export function registerAllSkillExampleTools(server: McpServer, env: Env) { registerListSkills(server, env); registerGetSkill(server, env); registerListExamples(server, env); registerGetExample(server, env); registerSearchExamples(server, env); }