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,213 @@
|
||||
/**
|
||||
* Introspection / debug MCP tools — LI SDD M2.2
|
||||
*
|
||||
* arcrun_validate_yaml — dry-run YAML 校驗,不部署
|
||||
* arcrun_get_execution_trace — 看 paused workflow state(task_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 觸發後若 paused,error 訊息含 '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);
|
||||
}
|
||||
Reference in New Issue
Block a user