5fc7277c3c
零 u6u 洩漏(leo 要求 1):connector tool 清單只剩 arcrun_*,並移除壞掉/
重複的 u6u_ workflow tool(deploy/execute/get/list_workflows 由 arcrun_
push/run/get/list_workflows 取代);MCP server name 也由 u6u-mcp-server
改 arcrun-mcp-server。
前綴單一真相源(leo 要求 2):新增 mcp/src/brand.ts(TOOL_PREFIX + toolName()
helper),所有 server.tool() 註冊一律走 toolName("suffix"),前綴不再明碼散寫。
未來 rebrand 只改 brand.ts 一行。
- 12 個獨有能力 tool rename(component/tag/gui/search_workflows)
- 4 個重複/壞掉 tool 移除
- inter-tool hint、描述字串、註解內 u6u_ 名字全數更新
- tsc exit 0;vitest 19/19 綠
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015d5jDbuqT5Htwv3Q88XXKk
215 lines
7.8 KiB
TypeScript
215 lines
7.8 KiB
TypeScript
/**
|
||
* 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 { toolName } from "../brand.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(
|
||
toolName("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(
|
||
toolName("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(
|
||
toolName("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(
|
||
toolName("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);
|
||
}
|