/** * 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)。資料來源是 D1 執行紀錄表,稽核資料——預設保留 90 天(3 個月)過期即清,管理者可在 portal 改保留天數或設為不刪除;用量過大時系統會自動降成只記失敗、甚至暫停記錄——工作流本身執行不受影響。", { 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); }