@minicor/mcp-server 3.3.5 → 3.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/README.md +104 -4
  2. package/dist/__tests__/middleware-service-client.test.d.ts +2 -0
  3. package/dist/__tests__/middleware-service-client.test.d.ts.map +1 -0
  4. package/dist/__tests__/middleware-service-client.test.js +293 -0
  5. package/dist/__tests__/middleware-service-client.test.js.map +1 -0
  6. package/dist/helpers.d.ts +10 -0
  7. package/dist/helpers.d.ts.map +1 -1
  8. package/dist/helpers.js +59 -0
  9. package/dist/helpers.js.map +1 -1
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +4 -0
  12. package/dist/index.js.map +1 -1
  13. package/dist/middleware-service-client.d.ts +266 -0
  14. package/dist/middleware-service-client.d.ts.map +1 -0
  15. package/dist/middleware-service-client.js +272 -0
  16. package/dist/middleware-service-client.js.map +1 -0
  17. package/dist/prompts/build-rpa.d.ts.map +1 -1
  18. package/dist/prompts/build-rpa.js +17 -0
  19. package/dist/prompts/build-rpa.js.map +1 -1
  20. package/dist/prompts/nestjs-to-job.d.ts +11 -0
  21. package/dist/prompts/nestjs-to-job.d.ts.map +1 -0
  22. package/dist/prompts/nestjs-to-job.js +110 -0
  23. package/dist/prompts/nestjs-to-job.js.map +1 -0
  24. package/dist/tools/jobs.d.ts +21 -0
  25. package/dist/tools/jobs.d.ts.map +1 -0
  26. package/dist/tools/jobs.js +624 -0
  27. package/dist/tools/jobs.js.map +1 -0
  28. package/dist/tools/vm-rpa.d.ts.map +1 -1
  29. package/dist/tools/vm-rpa.js +109 -25
  30. package/dist/tools/vm-rpa.js.map +1 -1
  31. package/dist/tools/workflow-ops.d.ts.map +1 -1
  32. package/dist/tools/workflow-ops.js +2 -55
  33. package/dist/tools/workflow-ops.js.map +1 -1
  34. package/package.json +1 -1
  35. package/skills/general/nestjs-middleware-to-job.md +82 -0
@@ -0,0 +1,624 @@
1
+ /**
2
+ * MCP tools for Jobs + Test Cases.
3
+ *
4
+ * A Job is a middleware route whose behavior is a structured `definition`
5
+ * (step graph) instead of freeform handler code. Jobs sit *above* workflows: a
6
+ * job step calls an entire Minicor workflow (by workflowId) or runs a code
7
+ * block. Test cases attach to a job (route): a named input + code-block
8
+ * assertions `[{ name, expr }]` evaluated against the execution.
9
+ *
10
+ * See middlewares/JOBS-DESIGN.md for the full model + API contract (v1).
11
+ *
12
+ * Auth/transport is handled by MiddlewareServiceClient, which by default calls
13
+ * the region's public middleware-service Cloud Run directly with the MCP's user
14
+ * bearer token. Set MIDDLEWARE_SERVICE_PROXY_BASE to route through a deployed
15
+ * frontend proxy instead, or MIDDLEWARE_SERVICE_URL[_US|_CA] to hit a specific
16
+ * reachable instance (dev/local). See middleware-service-client.ts for the full
17
+ * precedence.
18
+ */
19
+ import { z } from "zod";
20
+ import { ok, text, analyzeRpaFailure } from "../helpers.js";
21
+ import { MiddlewareServiceClient } from "../middleware-service-client.js";
22
+ import { getWorkspaceRegion } from "../state.js";
23
+ const regionParam = z
24
+ .enum(["us", "ca"])
25
+ .optional()
26
+ .describe("Region (us or ca). Auto-detected from workspace if omitted.");
27
+ // ── Zod schemas mirroring the JobDefinition entities ───────────
28
+ const jobStepSchema = z
29
+ .object({
30
+ id: z.string().describe("Stable step id (referenced by goto/saveAs)"),
31
+ name: z.string().describe("Human-readable step name"),
32
+ kind: z.enum(["workflow", "code"]),
33
+ saveAs: z
34
+ .string()
35
+ .optional()
36
+ .describe("ctx key to store this step's result under"),
37
+ when: z
38
+ .string()
39
+ .optional()
40
+ .describe("Conditional-skip: TS expression over ctx that must be truthy"),
41
+ forEach: z
42
+ .string()
43
+ .optional()
44
+ .describe("Fan-out: TS expression over ctx yielding a collection"),
45
+ onError: z
46
+ .union([
47
+ z.enum(["abort", "skip", "continue"]),
48
+ z.object({ goto: z.string() }),
49
+ ])
50
+ .optional(),
51
+ retry: z
52
+ .object({ max: z.number(), backoffMs: z.number().optional() })
53
+ .optional(),
54
+ cache: z
55
+ .object({ key: z.string(), ttlMs: z.number() })
56
+ .optional()
57
+ .describe("Login / smart-launch gate — reuse a cached result"),
58
+ // workflow
59
+ workflowId: z
60
+ .number()
61
+ .optional()
62
+ .describe("kind=workflow: the Minicor workflow id to call"),
63
+ configStoreId: z
64
+ .string()
65
+ .optional()
66
+ .describe("kind=workflow: override resource.configStoreId"),
67
+ input: z
68
+ .union([z.record(z.string(), z.unknown()), z.string()])
69
+ .optional()
70
+ .describe("kind=workflow: input object with {{ctx.x}} templates, OR a CodeBlock '(ctx) => ({...})'"),
71
+ // code
72
+ code: z
73
+ .string()
74
+ .optional()
75
+ .describe("kind=code: a CodeBlock '(ctx) => { ... }' run in the vm sandbox"),
76
+ })
77
+ .describe("A single job step — calls a whole workflow or runs a code block");
78
+ const jobDefinitionSchema = z
79
+ .object({
80
+ region: z.enum(["us", "ca"]),
81
+ resource: z
82
+ .object({
83
+ configStoreId: z.string().optional(),
84
+ lockKey: z
85
+ .string()
86
+ .optional()
87
+ .describe("Serialize execution per desktop/VM"),
88
+ })
89
+ .optional(),
90
+ inputSchema: z
91
+ .record(z.string(), z.unknown())
92
+ .describe("JSON Schema for the job input"),
93
+ outputSchema: z
94
+ .record(z.string(), z.unknown())
95
+ .describe("JSON Schema for the job output"),
96
+ steps: z.array(jobStepSchema).describe("Ordered step graph"),
97
+ output: z
98
+ .string()
99
+ .optional()
100
+ .describe("CodeBlock '(ctx) => response' producing the final output; also where partial/review status is decided"),
101
+ })
102
+ .describe("The structured job definition (step graph)");
103
+ const assertionSchema = z.object({
104
+ name: z.string().describe("Assertion name shown in the report"),
105
+ expr: z
106
+ .string()
107
+ .describe("TS boolean expression over `{ input, output, ctx, steps }`, e.g. 'output.status === \"ok\"'"),
108
+ });
109
+ export function register(deps) {
110
+ const { server } = deps;
111
+ function getClient(region) {
112
+ const token = deps.getAuthToken?.(region) || "";
113
+ return new MiddlewareServiceClient(deps.getApiBase(region), token);
114
+ }
115
+ function resolveRegion(workspaceId, explicit) {
116
+ if (explicit)
117
+ return explicit;
118
+ return getWorkspaceRegion(workspaceId) ?? undefined;
119
+ }
120
+ /**
121
+ * Fetch the workspace's first API key value via the Laminar core API
122
+ * (`GET /workspaces/:ws/api-keys` -> `[{ id, apiKey, name }]`). Returns
123
+ * undefined if the workspace has no keys.
124
+ */
125
+ async function firstWorkspaceApiKey(workspaceId, region) {
126
+ const keys = await deps.client(region).listApiKeys(workspaceId);
127
+ const list = Array.isArray(keys)
128
+ ? keys
129
+ : (keys?.content ?? keys?.apiKeys ?? []);
130
+ const first = list.find((k) => !!k?.apiKey);
131
+ return first?.apiKey;
132
+ }
133
+ // ── list_middlewares ───────────────────────────────────────
134
+ server.tool("list_middlewares", "List the routers (middlewares) in a workspace. A Job is a route on a router, so you need a middlewareId (from here) before register_job. If none exist, create one with register_middleware.", {
135
+ workspaceId: z.number().describe("Workspace ID"),
136
+ region: regionParam,
137
+ }, async ({ workspaceId, region }) => {
138
+ const r = resolveRegion(workspaceId, region);
139
+ try {
140
+ const middlewares = await getClient(r).listMiddlewares(workspaceId);
141
+ return ok({ middlewares });
142
+ }
143
+ catch (e) {
144
+ return text(`Error listing middlewares: ${e.message}`);
145
+ }
146
+ });
147
+ // ── register_middleware ────────────────────────────────────
148
+ server.tool("register_middleware", "Create a router (middleware) that Jobs attach to. Returns the middleware (use its id with register_job). The router needs a workspaceApiKey so its job workflow steps can call the Laminar API: pass `workspaceApiKey` explicitly, or leave autoConfigure=true (default) to fetch + set the workspace's first API key automatically. `workspaceApiKeyConfigured` in the result reflects whether the key actually stuck.", {
149
+ workspaceId: z.number().describe("Workspace ID"),
150
+ name: z.string().describe("Human-readable router name"),
151
+ slug: z
152
+ .string()
153
+ .describe("URL-safe slug (used in the runtime path /m/<slug>/*)"),
154
+ description: z.string().optional(),
155
+ workspaceApiKey: z
156
+ .string()
157
+ .optional()
158
+ .describe("Explicit workspace API key to set on the router. Takes precedence over autoConfigure. Use list_workspace_api_keys to find one."),
159
+ autoConfigure: z
160
+ .boolean()
161
+ .optional()
162
+ .describe("Fetch + set the workspace's first API key after creating (default true). Ignored if workspaceApiKey is provided."),
163
+ region: regionParam,
164
+ }, async ({ workspaceId, name, slug, description, workspaceApiKey, autoConfigure, region }) => {
165
+ const r = resolveRegion(workspaceId, region);
166
+ try {
167
+ const client = getClient(r);
168
+ const middleware = await client.createMiddleware(workspaceId, {
169
+ name,
170
+ slug,
171
+ description,
172
+ });
173
+ // Resolve the key to set: explicit param wins; otherwise (autoConfigure)
174
+ // fetch the workspace's first API key.
175
+ let keyToSet = workspaceApiKey;
176
+ if (!keyToSet && autoConfigure !== false) {
177
+ try {
178
+ keyToSet = await firstWorkspaceApiKey(workspaceId, r);
179
+ }
180
+ catch {
181
+ /* non-fatal: fall through to "not configured" below */
182
+ }
183
+ }
184
+ let configured = false;
185
+ if (keyToSet) {
186
+ try {
187
+ const updated = await client.setWorkspaceApiKey(workspaceId, middleware.id, keyToSet);
188
+ configured = !!updated?.workspaceApiKey;
189
+ if (updated)
190
+ Object.assign(middleware, updated);
191
+ }
192
+ catch {
193
+ /* non-fatal: report not-configured below */
194
+ }
195
+ }
196
+ if (configured) {
197
+ return ok({ status: "created", middleware, workspaceApiKeyConfigured: true });
198
+ }
199
+ return ok({
200
+ status: "created",
201
+ middleware,
202
+ workspaceApiKeyConfigured: false,
203
+ note: "Router has no workspace API key set. Set one with set_middleware_api_key (find a key via list_workspace_api_keys) so job workflow steps can run.",
204
+ });
205
+ }
206
+ catch (e) {
207
+ return text(`Error creating middleware: ${e.message}`);
208
+ }
209
+ });
210
+ // ── list_workspace_api_keys ────────────────────────────────
211
+ server.tool("list_workspace_api_keys", "List a workspace's API keys (id, apiKey, name) via the Laminar core API. Use to find a key for set_middleware_api_key or register_middleware's workspaceApiKey param.", {
212
+ workspaceId: z.number().describe("Workspace ID"),
213
+ region: regionParam,
214
+ }, async ({ workspaceId, region }) => {
215
+ const r = resolveRegion(workspaceId, region);
216
+ try {
217
+ const apiKeys = await deps.client(r).listApiKeys(workspaceId);
218
+ return ok({ apiKeys });
219
+ }
220
+ catch (e) {
221
+ return text(`Error listing workspace API keys: ${e.message}`);
222
+ }
223
+ });
224
+ // ── set_middleware_api_key ─────────────────────────────────
225
+ server.tool("set_middleware_api_key", "Set a router (middleware)'s workspaceApiKey. PUTs the key to the middleware and re-reads it, returning the updated middleware so you can confirm workspaceApiKey is now set.", {
226
+ workspaceId: z.number().describe("Workspace ID"),
227
+ middlewareId: z.string().describe("Router (middleware) ID"),
228
+ workspaceApiKey: z
229
+ .string()
230
+ .describe("Workspace API key to set on the router (from list_workspace_api_keys)"),
231
+ region: regionParam,
232
+ }, async ({ workspaceId, middlewareId, workspaceApiKey, region }) => {
233
+ const r = resolveRegion(workspaceId, region);
234
+ try {
235
+ const middleware = await getClient(r).setWorkspaceApiKey(workspaceId, middlewareId, workspaceApiKey);
236
+ if (!middleware) {
237
+ return text(`No middleware found with id ${middlewareId} after update.`);
238
+ }
239
+ return ok({
240
+ status: "updated",
241
+ middleware,
242
+ workspaceApiKeyConfigured: !!middleware.workspaceApiKey,
243
+ });
244
+ }
245
+ catch (e) {
246
+ return text(`Error setting middleware API key: ${e.message}`);
247
+ }
248
+ });
249
+ // ── register_job ───────────────────────────────────────────
250
+ server.tool("register_job", "Create a Job — a middleware route backed by a structured `definition` (step graph) instead of freeform handler code. A job step calls an entire Minicor workflow (by workflowId) or runs a code block; the job layer never touches a workflow's internal steps. Returns the created route (the job). Use add_test_case + run_tests to verify it.", {
251
+ workspaceId: z.number().describe("Workspace ID"),
252
+ middlewareId: z
253
+ .string()
254
+ .describe("Router (middleware) ID this job/route belongs to"),
255
+ path: z
256
+ .string()
257
+ .describe("Route path for the job, e.g. '/coi' or '/account-lookup'"),
258
+ method: z
259
+ .string()
260
+ .optional()
261
+ .describe("HTTP method (default POST)"),
262
+ description: z.string().optional().describe("What this job does"),
263
+ definition: jobDefinitionSchema,
264
+ region: regionParam,
265
+ }, async ({ workspaceId, middlewareId, path, method, description, definition, region }) => {
266
+ const r = resolveRegion(workspaceId, region);
267
+ try {
268
+ const job = await getClient(r).createJob(workspaceId, middlewareId, {
269
+ path,
270
+ method,
271
+ description,
272
+ definition: definition,
273
+ });
274
+ return ok({ status: "created", job });
275
+ }
276
+ catch (e) {
277
+ return text(`Error registering job: ${e.message}`);
278
+ }
279
+ });
280
+ // ── update_job ─────────────────────────────────────────────
281
+ server.tool("update_job", "Update a Job's definition (step graph), path, method, or description. Versioning a job creates a new route_version snapshot.", {
282
+ workspaceId: z.number().describe("Workspace ID"),
283
+ middlewareId: z.string().describe("Router (middleware) ID"),
284
+ routeId: z.string().describe("Route ID (the job) to update"),
285
+ path: z.string().optional(),
286
+ method: z.string().optional(),
287
+ description: z.string().optional(),
288
+ definition: jobDefinitionSchema.optional(),
289
+ region: regionParam,
290
+ }, async ({ workspaceId, middlewareId, routeId, path, method, description, definition, region }) => {
291
+ const r = resolveRegion(workspaceId, region);
292
+ try {
293
+ const body = {};
294
+ if (path !== undefined)
295
+ body.path = path;
296
+ if (method !== undefined)
297
+ body.method = method;
298
+ if (description !== undefined)
299
+ body.description = description;
300
+ if (definition !== undefined)
301
+ body.definition = definition;
302
+ const job = await getClient(r).updateJob(workspaceId, middlewareId, routeId, body);
303
+ return ok({ status: "updated", job });
304
+ }
305
+ catch (e) {
306
+ return text(`Error updating job: ${e.message}`);
307
+ }
308
+ });
309
+ // ── get_job ────────────────────────────────────────────────
310
+ server.tool("get_job", "Get a Job (route) including its `definition`. The management API has no single-route GET, so this lists the router's routes and filters by routeId. Omit routeId to list all routes (job-backed ones have `definition` set).", {
311
+ workspaceId: z.number().describe("Workspace ID"),
312
+ middlewareId: z.string().describe("Router (middleware) ID"),
313
+ routeId: z
314
+ .string()
315
+ .optional()
316
+ .describe("Route ID (the job). Omit to list all routes on the router."),
317
+ region: regionParam,
318
+ }, async ({ workspaceId, middlewareId, routeId, region }) => {
319
+ const r = resolveRegion(workspaceId, region);
320
+ try {
321
+ const client = getClient(r);
322
+ if (!routeId) {
323
+ const routes = await client.listRoutes(workspaceId, middlewareId);
324
+ return ok({ routes });
325
+ }
326
+ const job = await client.getJob(workspaceId, middlewareId, routeId);
327
+ if (!job)
328
+ return text(`No route found with id ${routeId} on router ${middlewareId}.`);
329
+ return ok({ job });
330
+ }
331
+ catch (e) {
332
+ return text(`Error fetching job: ${e.message}`);
333
+ }
334
+ });
335
+ // ── delete_job ─────────────────────────────────────────────
336
+ server.tool("delete_job", "Delete a Job (route) from a router. Destructive: also removes the job's version snapshots and test cases (cascade). The router (middleware) itself is left intact. Use get_job (omit routeId) first to confirm which route you're deleting.", {
337
+ workspaceId: z.number().describe("Workspace ID"),
338
+ middlewareId: z.string().describe("Router (middleware) ID"),
339
+ routeId: z.string().describe("Route ID (the job) to delete"),
340
+ region: regionParam,
341
+ }, async ({ workspaceId, middlewareId, routeId, region }) => {
342
+ const r = resolveRegion(workspaceId, region);
343
+ try {
344
+ const result = await getClient(r).deleteJob(workspaceId, middlewareId, routeId);
345
+ return ok({ status: "deleted", routeId, ...result });
346
+ }
347
+ catch (e) {
348
+ return text(`Error deleting job: ${e.message}`);
349
+ }
350
+ });
351
+ // ── delete_middleware ──────────────────────────────────────
352
+ server.tool("delete_middleware", "Delete a router (middleware) AND every Job/route on it. Destructive and cascading — all of the router's routes, versions, and test cases go with it. Use list_middlewares / get_job to confirm scope before calling.", {
353
+ workspaceId: z.number().describe("Workspace ID"),
354
+ middlewareId: z.string().describe("Router (middleware) ID to delete"),
355
+ region: regionParam,
356
+ }, async ({ workspaceId, middlewareId, region }) => {
357
+ const r = resolveRegion(workspaceId, region);
358
+ try {
359
+ const result = await getClient(r).deleteMiddleware(workspaceId, middlewareId);
360
+ return ok({ status: "deleted", middlewareId, ...result });
361
+ }
362
+ catch (e) {
363
+ return text(`Error deleting middleware: ${e.message}`);
364
+ }
365
+ });
366
+ // ── run_job ────────────────────────────────────────────────
367
+ server.tool("run_job", "Run a Job once with the given input, then poll the job execution to completion and return the full JobExecution (status, output, per-step results with Minicor deep-links). Async under the hood — this tool waits for the result.", {
368
+ workspaceId: z.number().describe("Workspace ID"),
369
+ middlewareId: z.string().describe("Router (middleware) ID"),
370
+ routeId: z.string().describe("Route ID (the job) to run"),
371
+ input: z
372
+ .record(z.string(), z.unknown())
373
+ .describe("Input payload for the job (validated against inputSchema)"),
374
+ trigger: z
375
+ .enum(["mcp", "api", "test"])
376
+ .optional()
377
+ .describe("Trigger label recorded on the execution (default mcp)"),
378
+ timeoutMs: z
379
+ .number()
380
+ .optional()
381
+ .describe("Max ms to poll before giving up (default 300000)"),
382
+ region: regionParam,
383
+ }, async ({ workspaceId, middlewareId, routeId, input, trigger, timeoutMs, region }) => {
384
+ const r = resolveRegion(workspaceId, region);
385
+ try {
386
+ const client = getClient(r);
387
+ const { jobExecutionId } = await client.runJob(workspaceId, middlewareId, routeId, input, trigger ?? "mcp");
388
+ const execution = await client.pollExecution(workspaceId, middlewareId, routeId, jobExecutionId, { timeoutMs });
389
+ return ok({ jobExecutionId, execution });
390
+ }
391
+ catch (e) {
392
+ return text(`Error running job: ${e.message}`);
393
+ }
394
+ });
395
+ // ── get_job_execution ──────────────────────────────────────
396
+ server.tool("get_job_execution", "Fetch one past JobExecution by id — the full trace for playing detective on a test case or run: status, input, the final output, the accumulated `context` (the through-line of data between steps), and per-step results (status ran/skipped/failed, the runtime input/output each step received, `workflowRuns[]` = one Minicor execution + replay per forEach iteration, and any logs). Use the jobExecutionId from run_job / run_tests / a test result.", {
397
+ workspaceId: z.number().describe("Workspace ID"),
398
+ middlewareId: z.string().describe("Router (middleware) ID"),
399
+ routeId: z.string().describe("Route ID (the job)"),
400
+ executionId: z.string().describe("Job execution ID to fetch"),
401
+ region: regionParam,
402
+ }, async ({ workspaceId, middlewareId, routeId, executionId, region }) => {
403
+ const r = resolveRegion(workspaceId, region);
404
+ try {
405
+ const execution = await getClient(r).getExecution(workspaceId, middlewareId, routeId, executionId);
406
+ return ok({ execution });
407
+ }
408
+ catch (e) {
409
+ return text(`Error fetching job execution: ${e.message}`);
410
+ }
411
+ });
412
+ // ── inspect_job_execution ──────────────────────────────────
413
+ server.tool("inspect_job_execution", "Deep-drill a JobExecution end to end: returns the nested tree job -> steps -> (for workflow steps) the underlying Minicor workflow execution's INTERNAL flow-runs (each flow-run's status, error, response, and — on failures — RPA failure-pattern analysis). This is the 'all the way down to the step' view: it stitches the job step's workflowExecutionId to the job definition's workflowId for you, so you don't have to call diagnose_execution manually. Use the jobExecutionId from run_job / run_tests / a test result.", {
414
+ workspaceId: z.number().describe("Workspace ID"),
415
+ middlewareId: z.string().describe("Router (middleware) ID"),
416
+ routeId: z.string().describe("Route ID (the job)"),
417
+ executionId: z.string().describe("Job execution ID to inspect"),
418
+ includeFlowRunPrograms: z
419
+ .boolean()
420
+ .optional()
421
+ .describe("Include each internal flow-run's program source (verbose). Default false."),
422
+ region: regionParam,
423
+ }, async ({ workspaceId, middlewareId, routeId, executionId, includeFlowRunPrograms, region, }) => {
424
+ const r = resolveRegion(workspaceId, region);
425
+ try {
426
+ const msClient = getClient(r);
427
+ const [execution, job] = await Promise.all([
428
+ msClient.getExecution(workspaceId, middlewareId, routeId, executionId),
429
+ msClient.getJob(workspaceId, middlewareId, routeId),
430
+ ]);
431
+ const defSteps = job?.definition?.steps ?? [];
432
+ const execSteps = Array.isArray(execution.steps)
433
+ ? execution.steps
434
+ : [];
435
+ const steps = await Promise.all(execSteps.map(async (se) => {
436
+ const base = {
437
+ stepId: se.stepId,
438
+ name: se.name,
439
+ kind: se.kind,
440
+ status: se.status,
441
+ attempts: se.attempts,
442
+ error: se.error,
443
+ durationMs: se.durationMs,
444
+ };
445
+ if (se.kind !== "workflow") {
446
+ return { ...base, output: se.output };
447
+ }
448
+ const def = defSteps.find((d) => d.id === se.stepId);
449
+ const workflowId = def?.workflowId;
450
+ // A plain workflow step has one run; a forEach step has one per item.
451
+ const runs = Array.isArray(se.workflowRuns) && se.workflowRuns.length
452
+ ? se.workflowRuns
453
+ : [
454
+ {
455
+ workflowExecutionId: se.workflowExecutionId,
456
+ region: se.region,
457
+ recordingUrl: se.recordingUrl,
458
+ },
459
+ ];
460
+ const workflowExecutions = await Promise.all(runs
461
+ .filter((run) => run.workflowExecutionId != null)
462
+ .map(async (run) => {
463
+ const runRegion = (run.region ?? se.region ?? r);
464
+ if (workflowId == null) {
465
+ return {
466
+ workflowExecutionId: run.workflowExecutionId,
467
+ region: runRegion,
468
+ error: "Could not resolve workflowId from the job definition for this step.",
469
+ };
470
+ }
471
+ try {
472
+ const wfExec = await deps
473
+ .client(runRegion)
474
+ .getExecution(workflowId, run.workflowExecutionId);
475
+ const flowRuns = (wfExec?.flowRuns ?? []).map((fr) => {
476
+ const failed = fr.status === "FAILED";
477
+ const errorStr = JSON.stringify(fr.executionLog || fr.response || "");
478
+ const programStr = JSON.stringify(fr.program || "");
479
+ const rpa = failed
480
+ ? analyzeRpaFailure(errorStr, programStr)
481
+ : null;
482
+ return {
483
+ executionOrder: fr.executionOrder,
484
+ name: fr.flowName,
485
+ status: fr.status,
486
+ durationMs: fr.durationMs,
487
+ ...(failed
488
+ ? { error: fr.executionLog || fr.response }
489
+ : { response: fr.response }),
490
+ ...(includeFlowRunPrograms
491
+ ? { program: fr.program }
492
+ : {}),
493
+ ...(rpa ? { rpaFailureAnalysis: rpa } : {}),
494
+ };
495
+ });
496
+ return {
497
+ workflowId,
498
+ workflowExecutionId: run.workflowExecutionId,
499
+ region: runRegion,
500
+ status: wfExec?.status,
501
+ recordingUrl: run.recordingUrl,
502
+ flowRuns,
503
+ };
504
+ }
505
+ catch (e) {
506
+ return {
507
+ workflowId,
508
+ workflowExecutionId: run.workflowExecutionId,
509
+ region: runRegion,
510
+ error: `Failed to load workflow execution: ${e.message}`,
511
+ };
512
+ }
513
+ }));
514
+ return { ...base, workflowId, workflowExecutions };
515
+ }));
516
+ return ok({
517
+ execution: {
518
+ id: execution.id,
519
+ status: execution.status,
520
+ trigger: execution.trigger,
521
+ input: execution.input,
522
+ output: execution.output,
523
+ error: execution.error,
524
+ startedAt: execution.startedAt,
525
+ completedAt: execution.completedAt,
526
+ durationMs: execution.durationMs,
527
+ },
528
+ steps,
529
+ });
530
+ }
531
+ catch (e) {
532
+ return text(`Error inspecting job execution: ${e.message}`);
533
+ }
534
+ });
535
+ // ── add_test_case ──────────────────────────────────────────
536
+ server.tool("add_test_case", "Attach a test case to a Job (route): a named input + code-block assertions `[{ name, expr }]`. Each `expr` is a TS boolean over `{ input, output, ctx, steps }` evaluated against the job execution. This is the green-gate for the build loop — define the expected behavior, then build the job to satisfy it.", {
537
+ workspaceId: z.number().describe("Workspace ID"),
538
+ middlewareId: z.string().describe("Router (middleware) ID"),
539
+ routeId: z.string().describe("Route ID (the job) the case belongs to"),
540
+ name: z.string().describe("Test case name"),
541
+ description: z.string().optional(),
542
+ input: z
543
+ .record(z.string(), z.unknown())
544
+ .describe("Request payload to run the job with"),
545
+ assertions: z
546
+ .array(assertionSchema)
547
+ .describe("Code-block assertions: [{ name, expr }]"),
548
+ tags: z.array(z.string()).optional(),
549
+ enabled: z.boolean().optional().describe("Default true"),
550
+ region: regionParam,
551
+ }, async ({ workspaceId, middlewareId, routeId, name, description, input, assertions, tags, enabled, region }) => {
552
+ const r = resolveRegion(workspaceId, region);
553
+ try {
554
+ const testCase = await getClient(r).addTestCase(workspaceId, middlewareId, routeId, {
555
+ name,
556
+ description,
557
+ input,
558
+ assertions,
559
+ tags,
560
+ enabled,
561
+ });
562
+ return ok({ status: "created", testCase });
563
+ }
564
+ catch (e) {
565
+ return text(`Error adding test case: ${e.message}`);
566
+ }
567
+ });
568
+ // ── run_tests ──────────────────────────────────────────────
569
+ server.tool("run_tests", "Run a Job's test suite (all enabled cases, or a subset via testCaseIds), poll the test run to completion, and return the report (totals + per-case results with drill-down into the job execution). Use as the green-gate before declaring a job done.", {
570
+ workspaceId: z.number().describe("Workspace ID"),
571
+ middlewareId: z.string().describe("Router (middleware) ID"),
572
+ routeId: z.string().describe("Route ID (the job) to test"),
573
+ testCaseIds: z
574
+ .array(z.string())
575
+ .optional()
576
+ .describe("Specific test case IDs to run (omit = all enabled)"),
577
+ timeoutMs: z
578
+ .number()
579
+ .optional()
580
+ .describe("Max ms to poll before giving up (default 600000)"),
581
+ region: regionParam,
582
+ }, async ({ workspaceId, middlewareId, routeId, testCaseIds, timeoutMs, region }) => {
583
+ const r = resolveRegion(workspaceId, region);
584
+ try {
585
+ const client = getClient(r);
586
+ const { testRunId } = await client.startTestRun(workspaceId, middlewareId, routeId, testCaseIds);
587
+ const report = await client.pollTestRun(workspaceId, middlewareId, routeId, testRunId, { timeoutMs });
588
+ const passed = report.testRun.status === "passed";
589
+ return ok({
590
+ testRunId,
591
+ status: report.testRun.status,
592
+ green: passed,
593
+ totals: report.testRun.totals,
594
+ results: report.results,
595
+ });
596
+ }
597
+ catch (e) {
598
+ return text(`Error running tests: ${e.message}`);
599
+ }
600
+ });
601
+ // ── get_test_report ────────────────────────────────────────
602
+ server.tool("get_test_report", "Fetch a test run report by id (totals + per-case results, each linking to its job execution for step-level drill-down). Use after run_tests, or to re-read an older run.", {
603
+ workspaceId: z.number().describe("Workspace ID"),
604
+ middlewareId: z.string().describe("Router (middleware) ID"),
605
+ routeId: z.string().describe("Route ID (the job)"),
606
+ testRunId: z.string().describe("Test run ID"),
607
+ region: regionParam,
608
+ }, async ({ workspaceId, middlewareId, routeId, testRunId, region }) => {
609
+ const r = resolveRegion(workspaceId, region);
610
+ try {
611
+ const report = await getClient(r).getTestRun(workspaceId, middlewareId, routeId, testRunId);
612
+ return ok({
613
+ status: report.testRun.status,
614
+ green: report.testRun.status === "passed",
615
+ totals: report.testRun.totals,
616
+ results: report.results,
617
+ });
618
+ }
619
+ catch (e) {
620
+ return text(`Error fetching test report: ${e.message}`);
621
+ }
622
+ });
623
+ }
624
+ //# sourceMappingURL=jobs.js.map