feat: 薄殼原則落地 + seed 下沉 API + MCP 進主庫 + 部署一致性
壓測四橫向問題修正(docs 壓測報告):
① 薄殼原則成鐵律:能力長在 API,CLI/MCP/lib 只暴露
- seed 下沉成 API 行為:cypher-executor POST /init/seed(一次灌 API+auth recipe),
種子資料移到 server src/lib/api-recipe-seeds.ts,CLI 改薄殼一次呼叫
- 解除 deployFullyOk 連坐 + init 補 seed auth recipe + update 補 seed/全 KV
- registry SUBMISSIONS_KV 補進 REQUIRED_KV_NAMESPACES(修 20/21)
② MCP 統一帳號來源(單一 remote MCP + .env 切 MCP URL)
- MCP 從 sibling repo 搬進 arcrun/mcp/(remote Worker,route 改 mcp.arcrun.dev)
- config 加 mcp_url 三層解析 + getMcpUrl + DEFAULT_MCP_URL
- 新增 acr mcp-setup:依 config 寫專案 .mcp.json(接案切資料夾自動切 MCP)
- acr --version 改動態讀 package.json(根治漂移)
③ Deploy 一致性
- tests/release.feature + scripts/check-release.sh
- local-deploy.sh:CLI npm publish + auto patch bump + CHANGELOG
- local-deploy.sh bash 3.2 相容修正(mapfile / 空陣列 set -u)
- builtins/pnpm-lock.yaml
④ README self-hosted 同步現況(移除 R2 殘留、加 flag/env、多帳號)
CLI bump → 1.3.0
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,315 @@
|
||||
/**
|
||||
* Skills + Examples lookup MCP tools — LI SDD M3.2
|
||||
*
|
||||
* 對應 .agents/specs/llm-interface/ Milestone 3.2 + 3.4。
|
||||
*
|
||||
* - arcrun_list_skills — 列 KBDB type=agent-skill 全部
|
||||
* - arcrun_get_skill — 用 slug 拿 skill markdown 全文
|
||||
* - arcrun_list_examples — 列 KBDB 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。
|
||||
*/
|
||||
|
||||
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";
|
||||
|
||||
interface KbdbBlock {
|
||||
id: string;
|
||||
page_name?: string | null;
|
||||
content?: string | null;
|
||||
type?: string;
|
||||
tags_json?: string;
|
||||
metadata_json?: string | null;
|
||||
source?: string | null;
|
||||
updated_at?: number;
|
||||
}
|
||||
|
||||
async function kbdbList(env: Env, type: string, limit = 100): Promise<KbdbBlock[]> {
|
||||
const resp = await kbdbFetch(env, `/blocks?type=${type}&limit=${limit}`);
|
||||
if (!resp.ok) throw new Error(`KBDB list type=${type} HTTP ${resp.status}`);
|
||||
const data = await resp.json<{ blocks?: KbdbBlock[] }>();
|
||||
return data.blocks ?? [];
|
||||
}
|
||||
|
||||
async function kbdbGetByPageName(env: Env, pageName: string): Promise<KbdbBlock | null> {
|
||||
const resp = await kbdbFetch(env, `/blocks?page_name=${encodeURIComponent(pageName)}&limit=1`);
|
||||
if (!resp.ok) return null;
|
||||
const data = await resp.json<{ blocks?: KbdbBlock[] }>();
|
||||
return data.blocks?.[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 個。內部走 KBDB semantic search(embedding 比對)+ tag 過濾。",
|
||||
{
|
||||
query: z.string().min(3).describe("用 use case 描述,例如 '每天早上發 email 摘要' / 'RAG 從文件回答問題'"),
|
||||
top_k: z.number().int().min(1).max(20).optional().describe("回幾個結果(預設 5)"),
|
||||
},
|
||||
async ({ query, top_k }) => {
|
||||
try {
|
||||
const k = top_k ?? 5;
|
||||
// KBDB /search 是 unified semantic search(既有),filter type=workflow-example
|
||||
const resp = await kbdbFetch(env, `/search`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
query,
|
||||
topK: k * 3, // overfetch 後 filter type
|
||||
}),
|
||||
});
|
||||
|
||||
if (!resp.ok) {
|
||||
return errorResponse(
|
||||
"fetch_failed",
|
||||
`KBDB search HTTP ${resp.status}`,
|
||||
["稍後重試", "改用 arcrun_list_examples(tag=...) 過濾"],
|
||||
await resp.text().catch(() => ""),
|
||||
);
|
||||
}
|
||||
|
||||
const data = await resp.json<{ results?: Array<{ block?: KbdbBlock; score?: number }> }>();
|
||||
const all = data.results ?? [];
|
||||
const examples = all
|
||||
.filter((r) => r.block?.type === "workflow-example")
|
||||
.slice(0, k)
|
||||
.map((r) => {
|
||||
const b = r.block!;
|
||||
return {
|
||||
slug: b.page_name?.replace(/^example-/, "") ?? "",
|
||||
page_name: b.page_name,
|
||||
score: r.score,
|
||||
tags: parseTags(b.tags_json),
|
||||
preview: (b.content ?? "").slice(0, 200),
|
||||
};
|
||||
});
|
||||
|
||||
if (examples.length === 0) {
|
||||
return successResponse(
|
||||
{ count: 0, examples: [], query },
|
||||
[
|
||||
"沒命中。可能 KBDB /search 還在等 embedding 建好(剛 sync 完要 1-5 分鐘)",
|
||||
"改用 arcrun_list_examples(tag='...') 走 tag 過濾",
|
||||
"或 arcrun_list_examples() 看全部清單自己挑",
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return successResponse(
|
||||
{ count: examples.length, examples, query },
|
||||
[
|
||||
"call arcrun_get_example(slug) 拿完整 YAML",
|
||||
"score 高 = 跟你 query 更相關",
|
||||
],
|
||||
);
|
||||
} 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);
|
||||
}
|
||||
Reference in New Issue
Block a user