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:
uncle6me-web
2026-06-06 15:45:35 +08:00
parent 5f381a44a6
commit 3e65e22775
58 changed files with 8608 additions and 74 deletions
+213
View File
@@ -0,0 +1,213 @@
/**
* Introspection / debug MCP tools — LI SDD M2.2
*
* arcrun_validate_yaml — dry-run YAML 校驗,不部署
* arcrun_get_execution_trace — 看 paused workflow statetask_id 細節)
* arcrun_list_paused_executions — 列當前所有等 callback 的 workflow
* arcrun_list_recent_executions — 列某 workflow 最近 N 次執行 verdict
*
* 對應 cypher-executor 新路由(commit 989fbeb+ 既有 /validate。
* 所有 tool 都需要 api_key (ak_xxx) 參數 — 跟 MCP partner-auth 的 pk_live 是兩層 auth。
*/
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";
const apiKeyDesc =
"你 (用戶) 的 arcrun api_key (ak_xxx)。從 https://arcrun.dev/me 取得。注意:跟 MCP 連線用的 pk_live token 是不同層 auth — pk_live 給 MCP 用,ak_xxx 給 workflow 操作用";
export function registerValidateYaml(server: McpServer, env: Env) {
server.tool(
"arcrun_validate_yaml",
"Dry-run YAML 校驗。不部署、無 side effect。回 {valid, errors?, nodeCount, edgeCount}。**永遠先 call 此 tool 再 push_workflow**,避免反覆 deploy 失敗。",
{
api_key: z.string().describe(apiKeyDesc),
graph: z.object({
nodes: z.array(z.unknown()).describe("workflow 節點陣列"),
edges: z.array(z.unknown()).describe("workflow 邊陣列 (cypher binding 三元組)"),
}).passthrough().describe("workflow graph object(已 parse 過 YAML 的結構,非 raw YAML string"),
},
async ({ api_key, graph }) => {
try {
const res = await cypherFetch(env, "/validate", {
apiKey: api_key,
method: "POST",
body: graph,
});
const body = await res.json().catch(() => null) as {
valid?: boolean;
errors?: unknown[];
nodeCount?: number;
edgeCount?: number;
} | null;
if (!res.ok || !body?.valid) {
return errorResponse(
"validation_failed",
body?.errors ? `校驗失敗,${(body.errors as unknown[]).length} 個錯誤` : `校驗失敗 HTTP ${res.status}`,
[
"依 errors 陣列逐項修改 YAML",
"若 errors 提到 '未知關係詞',看 design.md §3 列出的合法關係詞",
"若 errors 提到 'node 不存在',檢查 edges 的 from/to 是否拼錯",
],
JSON.stringify(body?.errors ?? body),
);
}
return successResponse(body, [
`校驗通過:${body.nodeCount} 個節點 / ${body.edgeCount} 條邊`,
"可以 call arcrun_push_workflow 部署了",
]);
} catch (e) {
return errorResponse(
"internal_error",
`validate 內部錯:${e instanceof Error ? e.message : String(e)}`,
["重試一次", "若持續失敗,告訴 leo 並貼錯誤訊息"],
);
}
},
);
}
export function registerListPausedExecutions(server: McpServer, env: Env) {
server.tool(
"arcrun_list_paused_executions",
"列當前 api_key 下所有 paused workflow(等 daemon callback resume 的)。給 debug 用:claude_api 等 async 零件會把 workflow 暫停,此 tool 告訴你哪些還沒回來。",
{
api_key: z.string().describe(apiKeyDesc),
limit: z.number().int().min(1).max(100).optional().describe("最多回幾個(預設 20,最多 100)"),
},
async ({ api_key, limit }) => {
try {
const res = await cypherFetch(env, "/executions/paused", {
apiKey: api_key,
query: limit ? { limit } : undefined,
});
const body = await res.json().catch(() => null);
if (!res.ok) {
return errorResponse(
"fetch_failed",
`撈 paused 列表失敗 HTTP ${res.status}`,
["檢查 api_key 是否正確", "稍後重試"],
JSON.stringify(body),
);
}
return successResponse(body);
} catch (e) {
return errorResponse(
"internal_error",
e instanceof Error ? e.message : String(e),
["重試一次"],
);
}
},
);
}
export function registerGetExecutionTrace(server: McpServer, env: Env) {
server.tool(
"arcrun_get_execution_trace",
"看單一 paused workflow 的 state 細節(trace、graph、context、pending_result)。task_id 從 paused 錯誤訊息或 list_paused_executions 取得。",
{
api_key: z.string().describe(apiKeyDesc),
task_id: z.string().describe(
"Paused workflow 的 task_id。來源:workflow 觸發後若 pausederror 訊息含 'waiting for task task_XXX';或 list_paused_executions 回的 task_id 欄位",
),
},
async ({ api_key, task_id }) => {
try {
const res = await cypherFetch(env, `/executions/${encodeURIComponent(task_id)}`, {
apiKey: api_key,
});
const body = await res.json().catch(() => null);
if (res.status === 404) {
return errorResponse(
"not_found",
`task_id "${task_id}" 沒對應的 paused state`,
[
"call list_paused_executions 看當前所有 paused,確認 task_id 正確",
"若該 workflow 不是 paused 型,看 list_recent_executions 查歷史 verdict",
],
);
}
if (!res.ok) {
return errorResponse(
"fetch_failed",
`撈 execution trace 失敗 HTTP ${res.status}`,
["檢查 task_id 格式是否正確"],
JSON.stringify(body),
);
}
return successResponse(body);
} catch (e) {
return errorResponse(
"internal_error",
e instanceof Error ? e.message : String(e),
["重試一次"],
);
}
},
);
}
export function registerListRecentExecutions(server: McpServer, env: Env) {
server.tool(
"arcrun_list_recent_executions",
"列某 workflow 最近 N 次執行 verdict(成功 / 失敗 / duration)。資料來源是 ANALYTICS_KV 90 天保留期。",
{
api_key: z.string().describe(apiKeyDesc),
workflow_name: z.string().describe("workflow 名稱(acr push 時的 name 欄)"),
limit: z.number().int().min(1).max(100).optional().describe("最多回幾筆(預設 10,最多 100)"),
},
async ({ api_key, workflow_name, limit }) => {
try {
const res = await cypherFetch(
env,
`/workflows/${encodeURIComponent(workflow_name)}/executions`,
{
apiKey: api_key,
query: limit ? { limit } : undefined,
},
);
const body = await res.json().catch(() => null);
if (res.status === 404) {
return errorResponse(
"not_found",
`workflow "${workflow_name}" 不存在或不屬於你`,
[
"call list_workflows 看你有什麼 workflow",
"確認 workflow 名稱拼寫正確",
],
);
}
if (!res.ok) {
return errorResponse(
"fetch_failed",
`撈執行歷史失敗 HTTP ${res.status}`,
["稍後重試"],
JSON.stringify(body),
);
}
return successResponse(body);
} catch (e) {
return errorResponse(
"internal_error",
e instanceof Error ? e.message : String(e),
["重試一次"],
);
}
},
);
}
export function registerAllIntrospectionTools(server: McpServer, env: Env) {
registerValidateYaml(server, env);
registerListPausedExecutions(server, env);
registerGetExecutionTrace(server, env);
registerListRecentExecutions(server, env);
}
+147
View File
@@ -0,0 +1,147 @@
/**
* arcrun_report_feedback — explicit feedback tool for AI agents
*
* 對應 SDD .agents/specs/llm-interface/ M1.3
*
* AI agent 每次完成 workflow / 卡住 / 解掉問題後 **MUST** call 此 tool。
* 結構化 issue_type enum 防自由文字難聚合。寫入 KBDB type=agent-feedback block。
*
* 後續 M4 weekly_review workflow 聚合這些 block 產出 arcrun-roadmap。
*
* 命名注意:M5 全面 rename u6u → arcrun 前,本 tool 直接用新名 arcrun_ prefix
* 立下範例。其他 u6u_* tool 等 M5 一次切。
*/
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import { Env } from "../types.js";
import { kbdbFetch } from "../lib/kbdb-client.js";
const ISSUE_TYPES = [
"success_story", // 順利完成,值得記錄這個 pattern
"doc_unclear", // AGENTS.md / skill / contract 講不清楚
"tool_missing", // 該有的 MCP tool 沒有
"error_unhelpful", // 錯誤訊息看不懂下一步
"unexpected_behavior", // 跟我預期的不一樣
"feature_request", // 我想要 X 功能
] as const;
export function registerReportFeedback(server: McpServer, env: Env, orgNamespace: string) {
server.tool(
"arcrun_report_feedback",
"AI agent 完成 workflow 任務 / 卡住 / 解掉問題後 **必須** call 此 tool 回報。即使順利也要 call (issue_type=success_story),那是告訴平台「這 pattern 已 work,可推廣」。回饋會寫進 KBDB type=agent-feedback,週報自動聚合產出平台改善 roadmap。",
{
issue_type: z.enum(ISSUE_TYPES).describe(
"回報類型。success_story=順利做完 / doc_unclear=文件不清楚 / tool_missing=該有的 MCP tool 缺 / error_unhelpful=錯誤訊息看不懂下一步 / unexpected_behavior=與預期不符 / feature_request=想要新功能"
),
description: z.string().min(10).describe(
"詳述:你做了什麼、發生什麼、為什麼這算 issue / story。至少 10 字。若是 success_story,描述 pattern 與適用情境"
),
workflow_name: z.string().optional().describe("相關 workflow 名稱(若有)"),
retry_count: z.number().int().min(0).optional().describe("為了搞定,你重試了幾次(含修 YAML / 改參數)"),
blocked: z.boolean().optional().describe("是否完全擋住(true = 無法繼續),預設 false"),
suggested_fix: z.string().optional().describe("你建議的修補方向(optional,但很有價值)"),
agent_user_agent: z.string().optional().describe(
"你(AI agent)的 client 識別字串。e.g. 'claude-code/1.x'、'cursor-mcp/0.4'、'mira-bot'。讓平台知道哪個 AI 客戶端踩到問題"
),
},
async ({ issue_type, description, workflow_name, retry_count, blocked, suggested_fix, agent_user_agent }) => {
try {
if (!env.KBDB) {
return { content: [{ type: "text", text: "Error: KBDB service binding unavailable" }], isError: true };
}
const blockBody = {
api_key: env.PLATFORM_API_KEY || undefined, // 若 platform key 在,聚集;否則用用戶 namespace
type: "agent-feedback",
source: "mcp-tool-call",
user_id: orgNamespace,
content: description,
metadata_json: JSON.stringify({
issue_type,
workflow_name,
retry_count,
blocked: blocked ?? false,
suggested_fix,
agent_user_agent,
reported_at: new Date().toISOString(),
}),
tags_json: JSON.stringify([
"agent-feedback",
`issue:${issue_type}`,
...(blocked ? ["blocked"] : []),
...(workflow_name ? [`wf:${workflow_name}`] : []),
]),
};
// 走 KBDB service binding(既有 pattern
const createResp = await kbdbFetch(env, `/blocks`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(blockBody),
});
if (!createResp.ok) {
const errBody = await createResp.text();
return {
content: [
{
type: "text",
text: JSON.stringify({
ok: false,
error_code: "kbdb_write_failed",
human_message: `回饋寫入 KBDB 失敗:HTTP ${createResp.status}`,
next_actions: [
"確認 KBDB 服務在線(試 https://kbdb-get.arcrun.dev/health",
"若持續失敗,可暫先在本地記下回饋,稍後重試",
],
detail: errBody.slice(0, 200),
}, null, 2),
},
],
isError: true,
};
}
const data = await createResp.json().catch(() => null);
return {
content: [
{
type: "text",
text: JSON.stringify({
ok: true,
data: {
reported: true,
issue_type,
block_id: (data as { id?: string } | null)?.id,
},
hints: [
issue_type === "success_story"
? "感謝記錄成功 pattern!這會被納入週報自動推廣。"
: "感謝回報!平台週報會聚合這類問題(M4 完成後可看 arcrun-roadmap block",
"若還有相關問題(例如同 workflow 不同 issue),可繼續 call",
],
}, null, 2),
},
],
};
} catch (error) {
return {
content: [
{
type: "text",
text: JSON.stringify({
ok: false,
error_code: "internal_error",
human_message: `report_feedback 內部錯誤:${error instanceof Error ? error.message : String(error)}`,
next_actions: ["重試一次", "若持續失敗,請告訴用戶這個 issue 並貼錯誤訊息給 leo"],
}, null, 2),
},
],
isError: true,
};
}
}
);
}
+315
View File
@@ -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 searchembedding 比對)+ 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);
}
+379
View File
@@ -0,0 +1,379 @@
/**
* Workflow CRUD tools — LI SDD M2.2
*
* 對應 .agents/specs/llm-interface/ Milestone 2.2。
*
* 取代既有 u6u_deploy_workflow(呼叫 /workflows/deploy — 該 endpoint 不存在,
* 是壞掉的 tool+ u6u_list_workflows / u6u_get_workflow 透過 KBDB 撈 metadata
* 而非直接問 cypher-executor 的真實狀態。
*
* 新 tool 直打 cypher-executor /webhooks/named*
* - arcrun_push_workflow
* - arcrun_list_workflows
* - arcrun_get_workflow
* - arcrun_delete_workflow
* - arcrun_run_workflow
*
* 舊 u6u_* 待 M5 一次 rename + 退場(leo 2026-05-16 拍板)。在此之前,
* AI 看到兩套 tool — 用 arcrun_* 為主,u6u_* 有 deprecation hint。
*/
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";
import { parse as parseYaml } from "yaml";
const apiKeyDesc =
"你(用戶)的 arcrun api_key (ak_xxx)。從 https://arcrun.dev/me 取得";
/**
* arcrun_push_workflow — 部署 YAML workflow
*
* 接受 yaml_content 或 graph object 兩種輸入。yaml_content 內部 parse 成 graph。
*/
export function registerPushWorkflow(server: McpServer, env: Env) {
server.tool(
"arcrun_push_workflow",
"部署 workflow 到 arcrun(取代 u6u_deploy_workflow,後者打不存在的 endpoint)。輸入可以是 YAML 字串或 graph 物件。**建議先 call arcrun_validate_yaml 確認 schema**。",
{
api_key: z.string().describe(apiKeyDesc),
yaml_content: z.string().optional().describe(
"YAML 字串。內部會 parse 成 {name, flow, config}。優先於 graph 參數"
),
graph: z.unknown().optional().describe(
"已 parse 過的 graph 物件(含 nodes, edges)。yaml_content 沒給才用此"
),
name: z.string().optional().describe(
"workflow 名稱(只能 [a-zA-Z0-9_-])。若給 yaml_content 會從 yaml 抽 name 欄"
),
description: z.string().optional().describe("workflow 描述(選填)"),
},
async ({ api_key, yaml_content, graph, name, description }) => {
let workflowName: string | undefined = name;
let workflowGraph: unknown = graph;
let workflowConfig: unknown = undefined;
// 如果有 yaml_contentparse 出 name + graph + config
if (yaml_content) {
try {
const parsed = parseYaml(yaml_content) as {
name?: string;
description?: string;
flow?: string[];
config?: Record<string, unknown>;
nodes?: unknown[];
edges?: unknown[];
};
workflowName = workflowName ?? parsed.name;
description = description ?? parsed.description;
workflowConfig = parsed.config;
// 若 yaml 是已展開的 {nodes, edges} 格式
if (parsed.nodes && parsed.edges) {
workflowGraph = { nodes: parsed.nodes, edges: parsed.edges };
}
// 若 yaml 是 cypher binding {flow, config} 格式,傳 raw 給 cypher-executor parse
else if (parsed.flow && parsed.config) {
workflowGraph = { flow: parsed.flow, config: parsed.config };
}
} catch (e) {
return errorResponse(
"validation_failed",
`YAML parse 失敗:${e instanceof Error ? e.message : String(e)}`,
["檢查 YAML 縮排 / 引號 / 冒號", "用 yamllint 或 validator 先過一次"],
);
}
}
if (!workflowName) {
return errorResponse(
"validation_failed",
"缺少 workflow nameyaml_content 內 'name:' 欄或 name 參數)",
["yaml 加 name: my_workflow 欄", "或直接傳 name 參數"],
);
}
if (!workflowGraph) {
return errorResponse(
"validation_failed",
"缺少 graph 資料(yaml_content 內 flow+config 或 nodes+edges,或直接傳 graph 參數)",
["yaml 至少含 flow: + config: 兩欄", "或直接傳 graph 參數"],
);
}
try {
const res = await cypherFetch(env, "/webhooks/named", {
apiKey: api_key,
method: "POST",
body: {
name: workflowName,
graph: workflowGraph,
config: workflowConfig,
description,
},
});
const body = await res.json().catch(() => ({} as Record<string, unknown>));
if (!res.ok) {
return errorResponse(
"push_failed",
`部署失敗 HTTP ${res.status}: ${(body as { error?: string }).error ?? 'unknown'}`,
[
"先 call arcrun_validate_yaml 確認 graph schema 正確",
"確認 workflow name 符合 [a-zA-Z0-9_-] 格式",
"確認 api_key 是 ak_xxx 格式且有效",
],
JSON.stringify(body),
);
}
const result = body as { name?: string; webhook_url?: string };
return successResponse(result, [
`部署成功!webhook URL: ${result.webhook_url}`,
`下一步:call arcrun_run_workflow('${result.name}', {你的 input}) 測試`,
"或對 webhook URL 直接 curl POST 觸發",
]);
} catch (e) {
return errorResponse(
"internal_error",
e instanceof Error ? e.message : String(e),
["重試一次"],
);
}
},
);
}
export function registerListWorkflows(server: McpServer, env: Env) {
server.tool(
"arcrun_list_workflows",
"列出你 (api_key 對應 namespace) 已部署的所有 workflow。回 [{name}]。",
{
api_key: z.string().describe(apiKeyDesc),
},
async ({ api_key }) => {
try {
const res = await cypherFetch(env, "/webhooks/named", {
apiKey: api_key,
});
const body = await res.json().catch(() => null);
if (!res.ok) {
return errorResponse(
"fetch_failed",
`撈 workflow 列表失敗 HTTP ${res.status}`,
["確認 api_key 正確", "稍後重試"],
JSON.stringify(body),
);
}
const data = body as { workflows?: Array<{ name: string; webhook_url?: string }> };
return successResponse(data, [
`${data.workflows?.length ?? 0} 個 workflow`,
"call arcrun_get_workflow(name) 看單個細節",
"call arcrun_list_recent_executions(workflow_name) 看執行歷史",
]);
} catch (e) {
return errorResponse(
"internal_error",
e instanceof Error ? e.message : String(e),
["重試一次"],
);
}
},
);
}
export function registerGetWorkflow(server: McpServer, env: Env) {
server.tool(
"arcrun_get_workflow",
"看單一 workflow 的完整定義(graph + config)。",
{
api_key: z.string().describe(apiKeyDesc),
name: z.string().describe("workflow 名稱"),
},
async ({ api_key, name }) => {
try {
// cypher-executor 既有 /webhooks/named GET 只回 [{name}] 不含細節,
// 要走 KV 直接讀 — 目前沒有單個 workflow GET endpoint。
// workaround:撈 list 然後 client filterM2.x 加 GET /webhooks/named/:name
const res = await cypherFetch(env, "/webhooks/named", {
apiKey: api_key,
});
const body = await res.json().catch(() => null) as {
workflows?: Array<{ name: string; webhook_url?: string }>;
} | null;
if (!res.ok || !body?.workflows) {
return errorResponse(
"fetch_failed",
`撈 workflow 列表失敗`,
["確認 api_key 正確"],
JSON.stringify(body),
);
}
const found = body.workflows.find((w) => w.name === name);
if (!found) {
return errorResponse(
"not_found",
`workflow "${name}" 不存在或不屬於你`,
[
"call arcrun_list_workflows 看你有什麼 workflow",
"確認名稱拼寫正確(注意大小寫)",
],
);
}
return successResponse(
{
name: found.name,
webhook_url: found.webhook_url,
note: "目前 list endpoint 不回完整 graph,未來會加 GET /webhooks/named/:name",
},
[
"可 call arcrun_list_recent_executions 看執行歷史",
"可 call arcrun_run_workflow 觸發測試",
],
);
} catch (e) {
return errorResponse(
"internal_error",
e instanceof Error ? e.message : String(e),
["重試一次"],
);
}
},
);
}
export function registerDeleteWorkflow(server: McpServer, env: Env) {
server.tool(
"arcrun_delete_workflow",
"刪除 workflow。**不可逆,確認後再做**。會清掉對應 cron index 與 webhook URL。",
{
api_key: z.string().describe(apiKeyDesc),
name: z.string().describe("要刪的 workflow 名稱"),
confirm: z.literal(true).describe("必須傳 true 確認"),
},
async ({ api_key, name, confirm: _confirm }) => {
try {
const res = await cypherFetch(env, `/webhooks/named/${encodeURIComponent(name)}`, {
apiKey: api_key,
method: "DELETE",
});
const body = await res.json().catch(() => ({}));
if (!res.ok) {
return errorResponse(
res.status === 404 ? "not_found" : "delete_failed",
res.status === 404
? `workflow "${name}" 不存在`
: `刪除失敗 HTTP ${res.status}`,
[
"call arcrun_list_workflows 確認名稱",
"若已不存在可忽略此錯誤",
],
JSON.stringify(body),
);
}
return successResponse({ deleted: name }, [
`已刪除 ${name}`,
"若該 workflow 有 cron,索引也已清",
]);
} catch (e) {
return errorResponse(
"internal_error",
e instanceof Error ? e.message : String(e),
["重試一次"],
);
}
},
);
}
export function registerRunWorkflow(server: McpServer, env: Env) {
server.tool(
"arcrun_run_workflow",
"觸發 workflow 執行。input 物件帶進 trigger context。回 {success, data, trace?, duration_ms}。",
{
api_key: z.string().describe(apiKeyDesc),
name: z.string().describe("workflow 名稱"),
input: z.record(z.unknown()).optional().describe(
"trigger context(會塞進 workflow 第一個節點的輸入)。記得帶 api_key 給內部需要的節點用"
),
},
async ({ api_key, name, input }) => {
try {
const triggerBody = input ?? {};
// 若 input 沒帶 api_key,自動補(內部多數零件需要)
if (!('api_key' in triggerBody)) {
(triggerBody as Record<string, unknown>).api_key = api_key;
}
const res = await cypherFetch(env, `/webhooks/named/${encodeURIComponent(name)}/trigger`, {
apiKey: api_key,
method: "POST",
body: triggerBody,
});
const body = await res.json().catch(() => null) as {
success?: boolean;
data?: unknown;
error?: string;
duration_ms?: number;
trace?: unknown;
} | null;
if (res.status === 404) {
return errorResponse(
"not_found",
`workflow "${name}" 不存在`,
["call arcrun_list_workflows 確認名稱", "或先 arcrun_push_workflow 部署"],
);
}
// workflow 自己 success/fail 不算 HTTP 錯誤
const isPaused = body?.error && /workflow paused/i.test(body.error);
if (isPaused) {
return successResponse(
{ ...body, status: "running_async" },
[
"workflow 已接受,正在背景跑(等 claude_api 等 daemon callback",
"call arcrun_list_paused_executions 看當前 running_async 的",
"正常 30-90 秒會 resume 完成(從 user 角度像同步完成)",
],
);
}
if (!body?.success) {
return errorResponse(
"execution_failed",
body?.error ?? `執行失敗 HTTP ${res.status}`,
[
"看 trace 陣列第一個 status=failed 的 node 是哪個",
"call arcrun_list_recent_executions 看歷史趨勢",
],
JSON.stringify(body),
);
}
return successResponse(body, [
`執行成功,耗時 ${body.duration_ms}ms`,
"call arcrun_list_recent_executions 看歷史 verdict",
]);
} catch (e) {
return errorResponse(
"internal_error",
e instanceof Error ? e.message : String(e),
["重試一次"],
);
}
},
);
}
export function registerAllWorkflowCrudTools(server: McpServer, env: Env) {
registerPushWorkflow(server, env);
registerListWorkflows(server, env);
registerGetWorkflow(server, env);
registerDeleteWorkflow(server, env);
registerRunWorkflow(server, env);
}
+49
View File
@@ -0,0 +1,49 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { Env } from "../types.js";
import { registerSearchComponents } from "./u6u_search_components.js";
import { registerExecuteWorkflow } from "./u6u_execute_workflow.js";
import { registerDeployWorkflow } from "./u6u_deploy_workflow.js";
import { registerPublishComponent } from "./u6u_publish_component.js";
import { registerListWorkflows } from "./u6u_list_workflows.js";
import { registerGetWorkflow } from "./u6u_get_workflow.js";
import { registerListComponents } from "./u6u_list_components.js";
import { registerGetComponent } from "./u6u_get_component.js";
import { registerGetComponentGuide } from "./u6u_get_component_guide.js";
import { registerCreateTag } from "./u6u_create_tag.js";
import { registerListTags } from "./u6u_list_tags.js";
import { registerDeleteTag } from "./u6u_delete_tag.js";
import { registerTagResource } from "./u6u_tag_resource.js";
import { registerUntagResource } from "./u6u_untag_resource.js";
import { registerGetGuiContext } from "./u6u_get_gui_context.js";
import { registerReportFeedback } from "./arcrun_report_feedback.js";
import { registerAllIntrospectionTools } from "./arcrun_introspection.js";
import { registerAllWorkflowCrudTools } from "./arcrun_workflow_crud.js";
import { registerAllSkillExampleTools } from "./arcrun_skills_examples.js";
export function registerAllTools(server: McpServer, env: Env, orgNamespace: string, partnerToken: string) {
registerSearchComponents(server, env, orgNamespace);
registerExecuteWorkflow(server, env, orgNamespace, partnerToken);
registerDeployWorkflow(server, env, orgNamespace);
registerPublishComponent(server, env, orgNamespace);
registerListWorkflows(server, env, orgNamespace);
registerGetWorkflow(server, env, orgNamespace);
registerListComponents(server, env, orgNamespace);
registerGetComponent(server, env, orgNamespace);
registerGetComponentGuide(server, env, orgNamespace);
registerCreateTag(server, env, orgNamespace);
registerListTags(server, env, orgNamespace);
registerDeleteTag(server, env, orgNamespace);
registerTagResource(server, env, orgNamespace);
registerUntagResource(server, env, orgNamespace);
registerGetGuiContext(server, env, orgNamespace);
// LI SDD M1.3: explicit feedback tool (新命名規範 arcrun_*)
registerReportFeedback(server, env, orgNamespace);
// LI SDD M2.2: introspection tools (validate / paused / trace / recent executions)
registerAllIntrospectionTools(server, env);
// LI SDD M2.2: workflow CRUD (push / list / get / delete / run)
// 取代既有 u6u_deploy_workflow (打不存在的 /workflows/deploy endpoint)
registerAllWorkflowCrudTools(server, env);
// LI SDD M3.2: skills + examples lookupKBDB-backed
// 走 sync-registry-to-kbdb.py 把 registry/{skills,examples} 同步進 KBDB
registerAllSkillExampleTools(server, env);
}
+47
View File
@@ -0,0 +1,47 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import { Env } from "../types.js";
import { kbdbFetch } from "../lib/kbdb-client.js";
export function registerCreateTag(server: McpServer, env: Env, orgNamespace: string) {
server.tool(
"u6u_create_tag",
"在當前命名空間下建立新的 tag,用於分類工作流與零件。",
{
name: z.string().describe("Tag 名稱(在當前 org_namespace 下唯一)"),
description: z.string().optional().describe("Tag 描述(選填)")
},
async ({ name, description }) => {
try {
if (!env.KBDB) {
return { content: [{ type: "text", text: "Error: KBDB service binding unavailable" }], isError: true };
}
// Check for duplicate
const checkResp = await kbdbFetch(env, `/records/search?template=tag&user_id=${encodeURIComponent(orgNamespace)}&name=${encodeURIComponent(name)}`);
if (checkResp.ok) {
const checkData = await checkResp.json<{ records: unknown[] }>();
if (checkData.records && checkData.records.length > 0) {
return { content: [{ type: "text", text: `Error: Tag '${name}' already exists in this namespace` }], isError: true };
}
}
const recordId = `tag-${orgNamespace}-${name}`;
const createResp = await kbdbFetch(env, `/records`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
template: "tag",
record_id: recordId,
user_id: orgNamespace,
values: { name, description: description ?? "", org_namespace: orgNamespace, created_at: new Date().toISOString() }
})
});
if (!createResp.ok) {
return { content: [{ type: "text", text: `Error creating tag: ${await createResp.text()}` }], isError: true };
}
return { content: [{ type: "text", text: JSON.stringify(await createResp.json(), null, 2) }] };
} catch (error) {
return { content: [{ type: "text", text: `Internal Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true };
}
}
);
}
+30
View File
@@ -0,0 +1,30 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import { Env } from "../types.js";
import { kbdbFetch } from "../lib/kbdb-client.js";
export function registerDeleteTag(server: McpServer, env: Env, orgNamespace: string) {
server.tool(
"u6u_delete_tag",
"刪除當前命名空間下的指定 tag。",
{ tag_name: z.string().describe("要刪除的 Tag 名稱") },
async ({ tag_name }) => {
try {
if (!env.KBDB) {
return { content: [{ type: "text", text: "Error: KBDB service binding unavailable" }], isError: true };
}
const recordId = `tag-${orgNamespace}-${tag_name}`;
const deleteResp = await kbdbFetch(env, `/records/${encodeURIComponent(recordId)}`, { method: "DELETE" });
if (deleteResp.status === 404) {
return { content: [{ type: "text", text: `Error: Tag '${tag_name}' not found` }], isError: true };
}
if (!deleteResp.ok) {
return { content: [{ type: "text", text: `Error deleting tag: ${await deleteResp.text()}` }], isError: true };
}
return { content: [{ type: "text", text: `Tag '${tag_name}' deleted successfully` }] };
} catch (error) {
return { content: [{ type: "text", text: `Internal Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true };
}
}
);
}
+81
View File
@@ -0,0 +1,81 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import { Env } from "../types.js";
import { kbdbFetch } from "../lib/kbdb-client.js";
export function registerDeployWorkflow(server: McpServer, env: Env, orgNamespace: string) {
server.tool(
"u6u_deploy_workflow",
"將工作流 YAML 配置正式部署至雲端引擎,完成註冊與排程設定。",
{
yaml_content: z.string().describe("工作流的 YAML 配置內容")
},
async ({ yaml_content }) => {
try {
if (!env.CYPHER_EXECUTOR) {
return {
content: [{ type: "text", text: "Error: CYPHER_EXECUTOR service binding is not configured." }],
isError: true
};
}
const response = await env.CYPHER_EXECUTOR.fetch("http://cypher-executor/workflows/deploy", {
method: "POST",
headers: { "Content-Type": "application/yaml" },
body: yaml_content
});
if (!response.ok) {
const errorText = await response.text();
return {
content: [{ type: "text", text: `Deployment failed: ${errorText}` }],
isError: true
};
}
const result = await response.json<{ workflow_id?: string; [key: string]: unknown }>();
const workflowId = result.workflow_id ?? crypto.randomUUID();
// Parse workflow name from YAML
const nameMatch = yaml_content.match(/^name:\s*(.+)$/m);
const workflowName = nameMatch ? nameMatch[1].trim() : workflowId;
// Store workflow metadata in KBDB
if (env.KBDB) {
const kbdbResp = await kbdbFetch(env, "/records", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
template: "workflow_metadata",
record_id: `wf-${workflowId}`,
user_id: orgNamespace,
values: {
workflow_id: workflowId,
name: workflowName,
deployed_at: new Date().toISOString(),
org_namespace: orgNamespace
}
})
});
if (!kbdbResp.ok) {
const errText = await kbdbResp.text();
return {
content: [{ type: "text", text: `Deployment succeeded but failed to store metadata: ${errText}` }],
isError: true
};
}
}
return {
content: [{ type: "text", text: `Successfully deployed workflow: ${JSON.stringify(result, null, 2)}` }]
};
} catch (error) {
return {
content: [{ type: "text", text: `Internal Error: ${error instanceof Error ? error.message : String(error)}` }],
isError: true
};
}
}
);
}
+54
View File
@@ -0,0 +1,54 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import { Env } from "../types.js";
export function registerExecuteWorkflow(server: McpServer, env: Env, orgNamespace: string, partnerToken: string) {
server.tool(
"u6u_execute_workflow",
"在沙盒環境中即時執行工作流,驗證 triplets 邏輯是否正確。每個 config 鍵對應 triplets 中的節點名,內含 component(零件 canonical_id)、recipe(prompt_recipe:xxx,選用)、與該節點的其他靜態參數。",
{
triplets: z.array(z.string()).describe("工作流三元組,例:['input >> 完成後 >> synth']"),
context: z.record(z.string(), z.any()).describe("初始變數(測試資料 / 上游節點輸出模擬)"),
config: z.record(z.string(), z.record(z.string(), z.any())).optional().describe("每節點配置:{ node_name: { component, recipe?, ...params } }")
},
async ({ triplets, context, config }) => {
try {
if (!env.CYPHER_EXECUTOR) {
return {
content: [{ type: "text", text: "Error: CYPHER_EXECUTOR service binding is not configured." }],
isError: true
};
}
// KI-12 修:改打 /cypher/execute(吃 triplets+config),原 /execute 是吃完整 graph 的舊路徑
// KI-15 修:轉發 partner token 給 cypher-executor,讓 recipe expander 能用 ak_ key 抓 KBDB
const response = await env.CYPHER_EXECUTOR.fetch("http://cypher-executor/cypher/execute", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Arcrun-API-Key": partnerToken
},
body: JSON.stringify({ triplets, context, config })
});
if (!response.ok) {
const errorText = await response.text();
return {
content: [{ type: "text", text: `Execution failed: ${errorText}` }],
isError: true
};
}
const result = await response.json();
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
};
} catch (error) {
return {
content: [{ type: "text", text: `Internal Error: ${error instanceof Error ? error.message : String(error)}` }],
isError: true
};
}
}
);
}
+59
View File
@@ -0,0 +1,59 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import { Env } from "../types.js";
/**
* u6u_get_component — 取得零件完整合約
* 呼叫 Component Registry GET /components/:id
*/
export function registerGetComponent(server: McpServer, env: Env, orgNamespace: string) {
server.tool(
"u6u_get_component",
"取得指定零件的完整合約,包含 canonical_id、display_name、category、version、stability、input_schema、output_schema、gherkin_tests、評分統計等。",
{
canonical_id: z.string().describe("零件 canonical_id(如 validate_json"),
},
async ({ canonical_id }) => {
try {
if (!env.COMPONENT_REGISTRY) {
return {
content: [{ type: "text", text: "Error: COMPONENT_REGISTRY service binding is not configured." }],
isError: true,
};
}
const response = await env.COMPONENT_REGISTRY.fetch(
`http://component-registry/components/${encodeURIComponent(canonical_id)}`,
{ method: "GET" },
);
if (response.status === 404) {
return {
content: [{ type: "text", text: `零件 '${canonical_id}' 不存在。可用 u6u_search_components 搜尋相似零件。` }],
isError: true,
};
}
if (!response.ok) {
return {
content: [{ type: "text", text: `Error: ${await response.text()}` }],
isError: true,
};
}
const result = await response.json() as { data?: unknown };
return {
content: [{
type: "text",
text: JSON.stringify(result.data ?? result, null, 2),
}],
};
} catch (error) {
return {
content: [{ type: "text", text: `Internal Error: ${error instanceof Error ? error.message : String(error)}` }],
isError: true,
};
}
}
);
}
+47
View File
@@ -0,0 +1,47 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { Env } from "../types.js";
/**
* u6u_get_component_guide — 取得零件開發指引
* 呼叫 Component Registry GET /components/guide
* AI 在開發新零件前應先讀取此指引
*/
export function registerGetComponentGuide(server: McpServer, env: Env, orgNamespace: string) {
server.tool(
"u6u_get_component_guide",
"取得 u6u 零件開發指引(Markdown 格式)。包含 TinyGo 白名單 import、禁止行為、component.contract.yaml 完整範例、本地測試指令。開發新零件前必須先讀取此指引。",
{},
async () => {
try {
if (!env.COMPONENT_REGISTRY) {
return {
content: [{ type: "text", text: "Error: COMPONENT_REGISTRY service binding is not configured." }],
isError: true,
};
}
const response = await env.COMPONENT_REGISTRY.fetch(
"http://component-registry/components/guide",
{ method: "GET" },
);
if (!response.ok) {
return {
content: [{ type: "text", text: `Error fetching guide: ${await response.text()}` }],
isError: true,
};
}
const guide = await response.text();
return {
content: [{ type: "text", text: guide }],
};
} catch (error) {
return {
content: [{ type: "text", text: `Internal Error: ${error instanceof Error ? error.message : String(error)}` }],
isError: true,
};
}
}
);
}
+84
View File
@@ -0,0 +1,84 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import { Env } from "../types.js";
import { kbdbFetch } from "../lib/kbdb-client.js";
interface ActionLogSlots {
org_namespace?: string;
action_type?: string;
payload?: string;
occurred_at?: string;
}
interface ActionLogRecord {
id: string;
slots?: ActionLogSlots;
}
export function registerGetGuiContext(server: McpServer, env: Env, orgNamespace: string) {
server.tool(
"u6u_get_gui_context",
"查詢用戶在 GUI 上的最近操作記錄,了解用戶的當前意圖與操作上下文。" +
"回傳最近 N 條操作記錄(從新到舊),以及用戶當前所在頁面和正在編輯的 Workflow ID。",
{
limit: z.number().int().min(1).max(100).default(20)
.describe("要取回的最近操作數量(預設 20,最大 100)"),
},
async ({ limit }) => {
try {
if (!env.KBDB) {
return {
content: [{ type: "text", text: "Error: KBDB service binding unavailable" }],
isError: true,
};
}
const resp = await kbdbFetch(
env,
`/records/search?template_id=tpl-action-log&user_id=${encodeURIComponent(orgNamespace)}&limit=${limit ?? 20}`
);
if (!resp.ok) {
return {
content: [{ type: "text", text: `Error querying action log: ${await resp.text()}` }],
isError: true,
};
}
const data = await resp.json<{ records: ActionLogRecord[] }>();
const records = data.records ?? [];
// 按 occurred_at 降序排列(最新在前)
const sorted = records
.map(r => ({
action_type: r.slots?.action_type ?? '',
payload: (() => {
try { return JSON.parse(r.slots?.payload ?? '{}'); } catch { return {}; }
})(),
occurred_at: r.slots?.occurred_at ?? '',
}))
.sort((a, b) => b.occurred_at.localeCompare(a.occurred_at))
.slice(0, limit ?? 20);
// 提取當前頁面和正在操作的 Workflow
const lastNavigate = sorted.find(a => a.action_type === 'NAVIGATE');
const lastOpenWorkflow = sorted.find(a => a.action_type === 'OPEN_WORKFLOW');
const context = {
recent_actions: sorted,
current_page: (lastNavigate?.payload as { page?: string })?.page ?? null,
open_workflow_id: (lastOpenWorkflow?.payload as { workflow_id?: string })?.workflow_id ?? null,
};
return {
content: [{ type: "text", text: JSON.stringify(context, null, 2) }],
};
} catch (error) {
return {
content: [{ type: "text", text: `Internal Error: ${error instanceof Error ? error.message : String(error)}` }],
isError: true,
};
}
}
);
}
+33
View File
@@ -0,0 +1,33 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import { Env } from "../types.js";
import { kbdbFetch } from "../lib/kbdb-client.js";
export function registerGetWorkflow(server: McpServer, env: Env, orgNamespace: string) {
server.tool(
"u6u_get_workflow",
"取得指定工作流的 metadata,包含名稱、部署時間與 tag 列表。",
{ workflow_id: z.string().describe("工作流 ID") },
async ({ workflow_id }) => {
try {
if (!env.KBDB) {
return { content: [{ type: "text", text: "Error: KBDB service binding unavailable" }], isError: true };
}
const resp = await kbdbFetch(env, `/records/wf-${encodeURIComponent(workflow_id)}`);
if (resp.status === 404) {
return { content: [{ type: "text", text: `Error: Workflow '${workflow_id}' not found` }], isError: true };
}
if (!resp.ok) {
return { content: [{ type: "text", text: `Error querying workflow: ${await resp.text()}` }], isError: true };
}
const record = await resp.json<{ slots: { workflow_id: string; name: string; deployed_at: string; org_namespace: string } }>();
if (record.slots.org_namespace !== orgNamespace) {
return { content: [{ type: "text", text: `Error: Workflow '${workflow_id}' not found` }], isError: true };
}
return { content: [{ type: "text", text: JSON.stringify(record.slots, null, 2) }] };
} catch (error) {
return { content: [{ type: "text", text: `Internal Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true };
}
}
);
}
+39
View File
@@ -0,0 +1,39 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import { Env } from "../types.js";
import { kbdbFetch } from "../lib/kbdb-client.js";
export function registerListComponents(server: McpServer, env: Env, orgNamespace: string) {
server.tool(
"u6u_list_components",
"列出當前命名空間下所有已發佈的零件,可選擇按 tag 篩選。",
{ tag: z.string().optional().describe("按 tag 名稱篩選(選填)") },
async ({ tag }) => {
try {
if (!env.KBDB) {
return { content: [{ type: "text", text: "Error: KBDB service binding unavailable" }], isError: true };
}
let componentIds: string[] | null = null;
if (tag) {
const tagResp = await kbdbFetch(env, `/records/search?template=resource_tag&user_id=${encodeURIComponent(orgNamespace)}&tag_name=${encodeURIComponent(tag)}&resource_type=component`);
if (!tagResp.ok) {
return { content: [{ type: "text", text: `Error querying tags: ${await tagResp.text()}` }], isError: true };
}
const tagData = await tagResp.json<{ records: Array<{ slots: { resource_id: string } }> }>();
componentIds = tagData.records.map(r => r.slots.resource_id);
if (componentIds.length === 0) return { content: [{ type: "text", text: JSON.stringify([], null, 2) }] };
}
const resp = await kbdbFetch(env, `/records/search?template=component_metadata&user_id=${encodeURIComponent(orgNamespace)}`);
if (!resp.ok) {
return { content: [{ type: "text", text: `Error querying components: ${await resp.text()}` }], isError: true };
}
const data = await resp.json<{ records: Array<{ slots: { component_id: string; name: string; published_at: string; org_namespace: string } }> }>();
let components = data.records.map(r => r.slots);
if (componentIds !== null) components = components.filter(c => componentIds!.includes(c.component_id));
return { content: [{ type: "text", text: JSON.stringify(components, null, 2) }] };
} catch (error) {
return { content: [{ type: "text", text: `Internal Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true };
}
}
);
}
+26
View File
@@ -0,0 +1,26 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { Env } from "../types.js";
import { kbdbFetch } from "../lib/kbdb-client.js";
export function registerListTags(server: McpServer, env: Env, orgNamespace: string) {
server.tool(
"u6u_list_tags",
"列出當前命名空間下所有的 tag。",
{},
async () => {
try {
if (!env.KBDB) {
return { content: [{ type: "text", text: "Error: KBDB service binding unavailable" }], isError: true };
}
const resp = await kbdbFetch(env, `/records/search?template=tag&user_id=${encodeURIComponent(orgNamespace)}`);
if (!resp.ok) {
return { content: [{ type: "text", text: `Error fetching tags: ${await resp.text()}` }], isError: true };
}
const data = await resp.json<{ records: unknown[] }>();
return { content: [{ type: "text", text: JSON.stringify(data.records, null, 2) }] };
} catch (error) {
return { content: [{ type: "text", text: `Internal Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true };
}
}
);
}
+39
View File
@@ -0,0 +1,39 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import { Env } from "../types.js";
import { kbdbFetch } from "../lib/kbdb-client.js";
export function registerListWorkflows(server: McpServer, env: Env, orgNamespace: string) {
server.tool(
"u6u_list_workflows",
"列出當前命名空間下所有已部署的工作流,可選擇按 tag 篩選。",
{ tag: z.string().optional().describe("按 tag 名稱篩選(選填)") },
async ({ tag }) => {
try {
if (!env.KBDB) {
return { content: [{ type: "text", text: "Error: KBDB service binding unavailable" }], isError: true };
}
let workflowIds: string[] | null = null;
if (tag) {
const tagResp = await kbdbFetch(env, `/records/search?template=resource_tag&user_id=${encodeURIComponent(orgNamespace)}&tag_name=${encodeURIComponent(tag)}&resource_type=workflow`);
if (!tagResp.ok) {
return { content: [{ type: "text", text: `Error querying tags: ${await tagResp.text()}` }], isError: true };
}
const tagData = await tagResp.json<{ records: Array<{ slots: { resource_id: string } }> }>();
workflowIds = tagData.records.map(r => r.slots.resource_id);
if (workflowIds.length === 0) return { content: [{ type: "text", text: JSON.stringify([], null, 2) }] };
}
const resp = await kbdbFetch(env, `/records/search?template=workflow_metadata&user_id=${encodeURIComponent(orgNamespace)}`);
if (!resp.ok) {
return { content: [{ type: "text", text: `Error querying workflows: ${await resp.text()}` }], isError: true };
}
const data = await resp.json<{ records: Array<{ slots: { workflow_id: string; name: string; deployed_at: string; org_namespace: string } }> }>();
let workflows = data.records.map(r => r.slots);
if (workflowIds !== null) workflows = workflows.filter(w => workflowIds!.includes(w.workflow_id));
return { content: [{ type: "text", text: JSON.stringify(workflows, null, 2) }] };
} catch (error) {
return { content: [{ type: "text", text: `Internal Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true };
}
}
);
}
+83
View File
@@ -0,0 +1,83 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import { Env } from "../types.js";
/**
* u6u_publish_component — 提交 TinyGo WASM 零件至 Component Registry
*
* AI 工作流:
* 1. 先呼叫 u6u_get_component_guide 取得開發指引
* 2. 依指引用 TinyGo 撰寫零件(stdin/stdout JSON I/O
* 3. 編譯為 .wasmbase64 編碼後提交
* 4. Registry 自動執行沙盒驗收(體積、syscall 掃描、Gherkin 測試)
*/
export function registerPublishComponent(server: McpServer, env: Env, orgNamespace: string) {
server.tool(
"u6u_publish_component",
"提交 TinyGo WASM 零件至 Component Registry。需提供 component.contract.yaml 內容與編譯後的 .wasm base64。提交前請先呼叫 u6u_get_component_guide 取得開發規範。",
{
contract: z.object({
canonical_id: z.string().describe("零件功能名稱(小寫底線,如 validate_json"),
display_name: z.string().describe("顯示名稱(可自由命名)"),
category: z.enum(["logic", "api", "ui", "style", "anim"]).describe("零件分類"),
version: z.string().describe("版本(格式 vN,如 v1"),
wasi_target: z.literal("preview1"),
stability: z.enum(["floating", "stable", "pinned"]).default("floating"),
runtime_compat: z.array(z.string()).describe("相容 runtime,如 [\"cf-workers\",\"wazero\"]"),
constraints: z.object({
max_size_kb: z.number().default(2048),
max_cold_start_ms: z.number().default(50),
no_network_syscall: z.boolean().default(true),
io_model: z.literal("stdin_stdout_json"),
}),
input_schema: z.record(z.unknown()).describe("JSON Schema"),
output_schema: z.record(z.unknown()).describe("JSON Schema"),
gherkin_tests: z.array(z.object({
scenario: z.string(),
given: z.string().describe("JSON 字串"),
then_contains: z.string().describe("預期輸出包含的字串"),
})).min(2).describe("至少一個 happy path 和一個 error path"),
description: z.string().optional(),
tags: z.array(z.string()).optional(),
}).describe("component.contract.yaml 內容"),
wasm_base64: z.string().describe("編譯後的 .wasm 檔案 base64 編碼"),
},
async ({ contract, wasm_base64 }) => {
try {
if (!env.COMPONENT_REGISTRY) {
return {
content: [{ type: "text", text: "Error: COMPONENT_REGISTRY service binding is not configured." }],
isError: true,
};
}
const response = await env.COMPONENT_REGISTRY.fetch("http://component-registry/components", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ contract, wasm_base64 }),
});
if (!response.ok) {
const errorText = await response.text();
return {
content: [{ type: "text", text: `Publish failed: ${errorText}` }],
isError: true,
};
}
const result = await response.json() as Record<string, unknown>;
return {
content: [{
type: "text",
text: `零件 ${contract.canonical_id} v${contract.version} 提交成功:\n${JSON.stringify(result, null, 2)}`,
}],
};
} catch (error) {
return {
content: [{ type: "text", text: `Internal Error: ${error instanceof Error ? error.message : String(error)}` }],
isError: true,
};
}
}
);
}
+65
View File
@@ -0,0 +1,65 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import { Env } from "../types.js";
/**
* u6u_search_components — 語意搜尋零件庫
* 呼叫 Component Registry GET /components/search?q=...
*/
export function registerSearchComponents(server: McpServer, env: Env, orgNamespace: string) {
server.tool(
"u6u_search_components",
"用自然語言語意搜尋零件庫,找出符合需求的零件。例如:「查詢 Google Sheets 資料」、「發送 LINE 訊息」、「驗證 JSON 格式」。回傳零件清單含 canonical_id、描述、評分。",
{
query: z.string().describe("自然語言搜尋詞,如「查詢 Google Sheets 資料」"),
},
async ({ query }) => {
try {
if (!env.COMPONENT_REGISTRY) {
return {
content: [{ type: "text", text: "Error: COMPONENT_REGISTRY service binding is not configured." }],
isError: true,
};
}
const response = await env.COMPONENT_REGISTRY.fetch(
`http://component-registry/components/search?q=${encodeURIComponent(query)}`,
{ method: "GET", headers: { "Content-Type": "application/json" } },
);
if (!response.ok) {
const errorText = await response.text();
return {
content: [{ type: "text", text: `Search failed: ${errorText}` }],
isError: true,
};
}
const result = await response.json() as { data?: { results?: unknown[]; count?: number } };
const results = result.data?.results ?? [];
const count = result.data?.count ?? 0;
if (count === 0) {
return {
content: [{
type: "text",
text: `找不到符合「${query}」的零件。可以用 u6u_publish_component 提交新零件。`,
}],
};
}
return {
content: [{
type: "text",
text: `找到 ${count} 個零件:\n${JSON.stringify(results, null, 2)}`,
}],
};
} catch (error) {
return {
content: [{ type: "text", text: `Internal Error: ${error instanceof Error ? error.message : String(error)}` }],
isError: true,
};
}
}
);
}
+57
View File
@@ -0,0 +1,57 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import { Env } from "../types.js";
import { kbdbFetch } from "../lib/kbdb-client.js";
export function registerTagResource(server: McpServer, env: Env, orgNamespace: string) {
server.tool(
"u6u_tag_resource",
"為當前命名空間下的工作流或零件加上 tag 標籤。",
{
resource_type: z.enum(["workflow", "component"]).describe("資源類型:workflow 或 component"),
resource_id: z.string().describe("資源 ID"),
tag_name: z.string().describe("要套用的 tag 名稱")
},
async ({ resource_type, resource_id, tag_name }) => {
try {
if (!env.KBDB) {
return { content: [{ type: "text", text: "Error: KBDB service binding unavailable" }], isError: true };
}
const tagResp = await kbdbFetch(env, `/records/search?template=tag&user_id=${encodeURIComponent(orgNamespace)}&name=${encodeURIComponent(tag_name)}`);
if (!tagResp.ok) {
return { content: [{ type: "text", text: "Error: KBDB service unavailable" }], isError: true };
}
const tagData = await tagResp.json<{ records: unknown[] }>();
if (!tagData.records || tagData.records.length === 0) {
return { content: [{ type: "text", text: `Error: Tag not found` }], isError: true };
}
const prefix = resource_type === "workflow" ? "wf" : "comp";
const resourceResp = await kbdbFetch(env, `/records/${prefix}-${resource_id}`);
if (!resourceResp.ok) {
return { content: [{ type: "text", text: `Error: Resource not found` }], isError: true };
}
const resourceData = await resourceResp.json<{ slots: { org_namespace: string } }>();
if (!resourceData.slots || resourceData.slots.org_namespace !== orgNamespace) {
return { content: [{ type: "text", text: `Error: Resource not found` }], isError: true };
}
const recordId = `rt-${resource_type}-${resource_id}-${tag_name}`;
const createResp = await kbdbFetch(env, `/records`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
template: "resource_tag",
record_id: recordId,
user_id: orgNamespace,
values: { resource_type, resource_id, tag_name, org_namespace: orgNamespace }
})
});
if (!createResp.ok) {
return { content: [{ type: "text", text: `Error creating resource_tag: ${await createResp.text()}` }], isError: true };
}
return { content: [{ type: "text", text: JSON.stringify(await createResp.json(), null, 2) }] };
} catch (error) {
return { content: [{ type: "text", text: `Internal Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true };
}
}
);
}
+42
View File
@@ -0,0 +1,42 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import { Env } from "../types.js";
import { kbdbFetch } from "../lib/kbdb-client.js";
export function registerUntagResource(server: McpServer, env: Env, orgNamespace: string) {
server.tool(
"u6u_untag_resource",
"移除當前命名空間下工作流或零件的 tag 標籤。",
{
resource_type: z.enum(["workflow", "component"]).describe("資源類型:workflow 或 component"),
resource_id: z.string().describe("資源 ID"),
tag_name: z.string().describe("要移除的 tag 名稱")
},
async ({ resource_type, resource_id, tag_name }) => {
try {
if (!env.KBDB) {
return { content: [{ type: "text", text: "Error: KBDB service binding unavailable" }], isError: true };
}
const recordId = `rt-${resource_type}-${resource_id}-${tag_name}`;
const getResp = await kbdbFetch(env, `/records/${recordId}`);
if (getResp.status === 404) {
return { content: [{ type: "text", text: "Error: Resource tag association not found" }], isError: true };
}
if (!getResp.ok) {
return { content: [{ type: "text", text: "Error: KBDB service unavailable" }], isError: true };
}
const record = await getResp.json<{ slots: { org_namespace: string } }>();
if (!record.slots || record.slots.org_namespace !== orgNamespace) {
return { content: [{ type: "text", text: "Error: Resource tag association not found" }], isError: true };
}
const deleteResp = await kbdbFetch(env, `/records/${recordId}`, { method: "DELETE" });
if (!deleteResp.ok) {
return { content: [{ type: "text", text: "Error: KBDB service unavailable" }], isError: true };
}
return { content: [{ type: "text", text: `Tag '${tag_name}' removed from ${resource_type} '${resource_id}' successfully` }] };
} catch (error) {
return { content: [{ type: "text", text: `Internal Error: ${error instanceof Error ? error.message : String(error)}` }], isError: true };
}
}
);
}