@runcofactor/cli 0.10.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 (150) hide show
  1. package/README.md +125 -0
  2. package/dist/api-catalog-GFGCLVS4.js +401 -0
  3. package/dist/api-catalog-GFGCLVS4.js.map +1 -0
  4. package/dist/attachment-SJC7D7FY.js +276 -0
  5. package/dist/attachment-SJC7D7FY.js.map +1 -0
  6. package/dist/auth-EN6NQCJY.js +31 -0
  7. package/dist/auth-EN6NQCJY.js.map +1 -0
  8. package/dist/bin.js +7 -0
  9. package/dist/bin.js.map +1 -0
  10. package/dist/chunk-3CZU7XCS.js +272 -0
  11. package/dist/chunk-3CZU7XCS.js.map +1 -0
  12. package/dist/chunk-4NRDONSC.js +1048 -0
  13. package/dist/chunk-4NRDONSC.js.map +1 -0
  14. package/dist/chunk-4PNOV7OV.js +51980 -0
  15. package/dist/chunk-4PNOV7OV.js.map +1 -0
  16. package/dist/chunk-52IO7ACJ.js +2941 -0
  17. package/dist/chunk-52IO7ACJ.js.map +1 -0
  18. package/dist/chunk-6LEZCKKV.js +663 -0
  19. package/dist/chunk-6LEZCKKV.js.map +1 -0
  20. package/dist/chunk-A4OFTUVV.js +170 -0
  21. package/dist/chunk-A4OFTUVV.js.map +1 -0
  22. package/dist/chunk-B2VISN2V.js +2459 -0
  23. package/dist/chunk-B2VISN2V.js.map +1 -0
  24. package/dist/chunk-CLAWT7BJ.js +271 -0
  25. package/dist/chunk-CLAWT7BJ.js.map +1 -0
  26. package/dist/chunk-GKY2KXO5.js +281 -0
  27. package/dist/chunk-GKY2KXO5.js.map +1 -0
  28. package/dist/chunk-I4QLI3JO.js +413 -0
  29. package/dist/chunk-I4QLI3JO.js.map +1 -0
  30. package/dist/chunk-IMPZAX6L.js +180 -0
  31. package/dist/chunk-IMPZAX6L.js.map +1 -0
  32. package/dist/chunk-J6SH7GQT.js +241 -0
  33. package/dist/chunk-J6SH7GQT.js.map +1 -0
  34. package/dist/chunk-JDBCZ3Z2.js +298 -0
  35. package/dist/chunk-JDBCZ3Z2.js.map +1 -0
  36. package/dist/chunk-JICSYFRJ.js +46 -0
  37. package/dist/chunk-JICSYFRJ.js.map +1 -0
  38. package/dist/chunk-JWHYXEA2.js +32 -0
  39. package/dist/chunk-JWHYXEA2.js.map +1 -0
  40. package/dist/chunk-N7HFALX3.js +88 -0
  41. package/dist/chunk-N7HFALX3.js.map +1 -0
  42. package/dist/chunk-Q4BO4Z5V.js +178 -0
  43. package/dist/chunk-Q4BO4Z5V.js.map +1 -0
  44. package/dist/chunk-QFDOWABX.js +195 -0
  45. package/dist/chunk-QFDOWABX.js.map +1 -0
  46. package/dist/chunk-SJJW77WN.js +605 -0
  47. package/dist/chunk-SJJW77WN.js.map +1 -0
  48. package/dist/chunk-VKVL7WBN.js +50 -0
  49. package/dist/chunk-VKVL7WBN.js.map +1 -0
  50. package/dist/chunk-WOHL4FTB.js +353 -0
  51. package/dist/chunk-WOHL4FTB.js.map +1 -0
  52. package/dist/chunk-XQXVG7RA.js +426 -0
  53. package/dist/chunk-XQXVG7RA.js.map +1 -0
  54. package/dist/chunk-ZD3KHESI.js +94 -0
  55. package/dist/chunk-ZD3KHESI.js.map +1 -0
  56. package/dist/credential-COLTBV6D.js +450 -0
  57. package/dist/credential-COLTBV6D.js.map +1 -0
  58. package/dist/credential-template-EEPYFG7R.js +291 -0
  59. package/dist/credential-template-EEPYFG7R.js.map +1 -0
  60. package/dist/debug-workflow-KOVH5GOP.js +259 -0
  61. package/dist/debug-workflow-KOVH5GOP.js.map +1 -0
  62. package/dist/dist-44PZR7BQ.js +423 -0
  63. package/dist/dist-44PZR7BQ.js.map +1 -0
  64. package/dist/docs-3SSORHB5.js +366 -0
  65. package/dist/docs-3SSORHB5.js.map +1 -0
  66. package/dist/doctor-OTSIWV6G.js +132 -0
  67. package/dist/doctor-OTSIWV6G.js.map +1 -0
  68. package/dist/feature-flag-T2LRZME3.js +283 -0
  69. package/dist/feature-flag-T2LRZME3.js.map +1 -0
  70. package/dist/file-type-S3UUBVCT.js +4205 -0
  71. package/dist/file-type-S3UUBVCT.js.map +1 -0
  72. package/dist/index.d.ts +3 -0
  73. package/dist/index.js +982 -0
  74. package/dist/index.js.map +1 -0
  75. package/dist/init-TGWRECT2.js +208 -0
  76. package/dist/init-TGWRECT2.js.map +1 -0
  77. package/dist/install-DXSNWZKO.js +295 -0
  78. package/dist/install-DXSNWZKO.js.map +1 -0
  79. package/dist/issues-XIZOPX6T.js +564 -0
  80. package/dist/issues-XIZOPX6T.js.map +1 -0
  81. package/dist/knobs-NBINZIUB.js +295 -0
  82. package/dist/knobs-NBINZIUB.js.map +1 -0
  83. package/dist/list-3TDHK637.js +59 -0
  84. package/dist/list-3TDHK637.js.map +1 -0
  85. package/dist/local-HICVD6PU.js +100 -0
  86. package/dist/local-HICVD6PU.js.map +1 -0
  87. package/dist/login-UOIUXVUK.js +315 -0
  88. package/dist/login-UOIUXVUK.js.map +1 -0
  89. package/dist/mcp-54IJEOUA.js +1956 -0
  90. package/dist/mcp-54IJEOUA.js.map +1 -0
  91. package/dist/pdf-extract-ABWV3L5X.js +13 -0
  92. package/dist/pdf-extract-ABWV3L5X.js.map +1 -0
  93. package/dist/pdfjs-DQ2YA5S3.js +41561 -0
  94. package/dist/pdfjs-DQ2YA5S3.js.map +1 -0
  95. package/dist/property-UXLOLPTD.js +701 -0
  96. package/dist/property-UXLOLPTD.js.map +1 -0
  97. package/dist/property-skill-guide.md +189 -0
  98. package/dist/psql-BYUUNLGG.js +115 -0
  99. package/dist/psql-BYUUNLGG.js.map +1 -0
  100. package/dist/record-4TR2UIIS.js +233 -0
  101. package/dist/record-4TR2UIIS.js.map +1 -0
  102. package/dist/redis-35VBUYW7.js +116 -0
  103. package/dist/redis-35VBUYW7.js.map +1 -0
  104. package/dist/release-OOLTUBUT.js +354 -0
  105. package/dist/release-OOLTUBUT.js.map +1 -0
  106. package/dist/sandbox-XGSUU6G7.js +2046 -0
  107. package/dist/sandbox-XGSUU6G7.js.map +1 -0
  108. package/dist/scenarios-GWLJEGHA.js +280 -0
  109. package/dist/scenarios-GWLJEGHA.js.map +1 -0
  110. package/dist/session-MYFNZPRP.js +2414 -0
  111. package/dist/session-MYFNZPRP.js.map +1 -0
  112. package/dist/skill-KM34LDMO.js +1839 -0
  113. package/dist/skill-KM34LDMO.js.map +1 -0
  114. package/dist/skills/debug-run/SKILL.md +262 -0
  115. package/dist/skills/debug-run/history/1.0.0.md +244 -0
  116. package/dist/skills/debug-run/history/1.0.1.md +245 -0
  117. package/dist/skills/debug-run/history/1.0.2.md +254 -0
  118. package/dist/skills/debug-run/history/1.1.0.md +262 -0
  119. package/dist/skills/debug-run/version.json +3 -0
  120. package/dist/spec-RA3CFEKG.js +951 -0
  121. package/dist/spec-RA3CFEKG.js.map +1 -0
  122. package/dist/src-LB4HBTJQ.js +1516 -0
  123. package/dist/src-LB4HBTJQ.js.map +1 -0
  124. package/dist/src-N3TPKGTY.js +244 -0
  125. package/dist/src-N3TPKGTY.js.map +1 -0
  126. package/dist/src-UVDX5UP6.js +17 -0
  127. package/dist/src-UVDX5UP6.js.map +1 -0
  128. package/dist/start-PSEQSQA3.js +151 -0
  129. package/dist/start-PSEQSQA3.js.map +1 -0
  130. package/dist/tag-YY6YAABQ.js +22 -0
  131. package/dist/tag-YY6YAABQ.js.map +1 -0
  132. package/dist/task-IQC3YR5K.js +922 -0
  133. package/dist/task-IQC3YR5K.js.map +1 -0
  134. package/dist/test-suite-DZNJFXDW.js +394 -0
  135. package/dist/test-suite-DZNJFXDW.js.map +1 -0
  136. package/dist/trace-HXIW7U2D.js +14 -0
  137. package/dist/trace-HXIW7U2D.js.map +1 -0
  138. package/dist/triage-ORG45R5W.js +16 -0
  139. package/dist/triage-ORG45R5W.js.map +1 -0
  140. package/dist/upgrade-L2VYMP2V.js +61 -0
  141. package/dist/upgrade-L2VYMP2V.js.map +1 -0
  142. package/dist/workflow-XYFH5LTP.js +3708 -0
  143. package/dist/workflow-XYFH5LTP.js.map +1 -0
  144. package/dist/workflow-cache-NQJQFWMV.js +253 -0
  145. package/dist/workflow-cache-NQJQFWMV.js.map +1 -0
  146. package/dist/workflow-debug-BGD4UIXU.js +927 -0
  147. package/dist/workflow-debug-BGD4UIXU.js.map +1 -0
  148. package/dist/workflow-proposals-MTUDX46O.js +382 -0
  149. package/dist/workflow-proposals-MTUDX46O.js.map +1 -0
  150. package/package.json +61 -0
@@ -0,0 +1,1956 @@
1
+ import { createRequire as __cr } from "module"; const require = __cr(import.meta.url);
2
+ import {
3
+ diffSnapshots,
4
+ fromApiSnapshot
5
+ } from "./chunk-JDBCZ3Z2.js";
6
+ import {
7
+ LocalBrowserHost,
8
+ USER_PAUSED_ERROR
9
+ } from "./chunk-I4QLI3JO.js";
10
+ import {
11
+ callTool,
12
+ createSession,
13
+ deleteAllSessions,
14
+ deleteSession,
15
+ resolveTargetSession
16
+ } from "./chunk-J6SH7GQT.js";
17
+ import {
18
+ fetchOrgs,
19
+ openUrl,
20
+ switchOrg
21
+ } from "./chunk-QFDOWABX.js";
22
+ import {
23
+ clearUpdateCache
24
+ } from "./chunk-N7HFALX3.js";
25
+ import {
26
+ packageName
27
+ } from "./chunk-JICSYFRJ.js";
28
+ import {
29
+ ENV_URLS,
30
+ envToProfile,
31
+ readStoredToken,
32
+ resolveAppUrl,
33
+ saveAuth
34
+ } from "./chunk-A4OFTUVV.js";
35
+ import {
36
+ getBrowserToolDefinitionsWithLifecycle
37
+ } from "./chunk-4PNOV7OV.js";
38
+ import "./chunk-JWHYXEA2.js";
39
+ import "./chunk-6LEZCKKV.js";
40
+ import "./chunk-ZD3KHESI.js";
41
+ import "./chunk-VKVL7WBN.js";
42
+
43
+ // src/mcp/index.ts
44
+ import process4 from "process";
45
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
46
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
47
+ import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
48
+
49
+ // ../mcp-tools/src/context.ts
50
+ import { AsyncLocalStorage } from "async_hooks";
51
+ var storage = new AsyncLocalStorage();
52
+ function withToolCtx(ctx, fn) {
53
+ return storage.run(ctx, fn);
54
+ }
55
+ function currentToolCtx() {
56
+ return storage.getStore();
57
+ }
58
+
59
+ // ../mcp-tools/src/api-client.ts
60
+ var DEFAULT_TIMEOUT_MS = 3e4;
61
+ async function mcpApiRequest(method, path, body) {
62
+ const ctx = currentToolCtx();
63
+ if (!ctx) {
64
+ throw new Error("[no-context] mcpApiRequest called outside of withToolCtx()");
65
+ }
66
+ const url = `${ctx.apiUrl}${path}`;
67
+ let res;
68
+ try {
69
+ res = await fetch(url, {
70
+ method,
71
+ headers: {
72
+ Authorization: `Bearer ${ctx.apiToken}`,
73
+ "Content-Type": "application/json"
74
+ },
75
+ signal: AbortSignal.timeout(DEFAULT_TIMEOUT_MS),
76
+ ...body ? { body: JSON.stringify(body) } : {}
77
+ });
78
+ } catch (err) {
79
+ if (err instanceof Error && err.name === "TimeoutError") {
80
+ throw new Error(`[timeout] upstream API did not respond within ${DEFAULT_TIMEOUT_MS}ms`);
81
+ }
82
+ throw new Error(`[network] ${err instanceof Error ? err.message : String(err)}`);
83
+ }
84
+ if (!res.ok) {
85
+ if (res.status === 401) {
86
+ throw new Error("[unauthorized] Your session may have expired.");
87
+ }
88
+ const text = await res.text().catch(() => "");
89
+ throw new Error(`[http-${res.status}] ${text || res.statusText}`);
90
+ }
91
+ return await res.json();
92
+ }
93
+
94
+ // ../mcp-tools/src/tool-helpers.ts
95
+ function toolText(text) {
96
+ return { content: [{ type: "text", text }] };
97
+ }
98
+ function toolJson(data) {
99
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
100
+ }
101
+
102
+ // ../mcp-tools/src/registry.ts
103
+ function buildRegistry(groups) {
104
+ return {
105
+ list() {
106
+ return groups.flatMap((g) => g.definitions());
107
+ },
108
+ async call(tool, args, ctx) {
109
+ return withToolCtx(ctx, async () => {
110
+ for (const group of groups) {
111
+ const result = await group.handle(tool, args);
112
+ if (result) return result;
113
+ }
114
+ return toolText(`Unknown tool: ${tool}`);
115
+ });
116
+ }
117
+ };
118
+ }
119
+
120
+ // ../mcp-tools/src/groups/workflows.ts
121
+ function getWorkflowToolDefinitions() {
122
+ return [
123
+ {
124
+ name: "canary_workflow_list",
125
+ description: "List workflows in the current organization. Supports filtering by name, type, and status with pagination.",
126
+ inputSchema: {
127
+ type: "object",
128
+ properties: {
129
+ search: {
130
+ type: "string",
131
+ description: "Search workflows by name"
132
+ },
133
+ flowType: {
134
+ type: "string",
135
+ enum: ["standard", "setup", "seed", "teardown"],
136
+ description: "Filter by flow type"
137
+ },
138
+ status: {
139
+ type: "string",
140
+ enum: ["draft", "published"],
141
+ description: "Filter by lifecycle status. 'draft' = editable, not run by suites. 'published' = active, included in regression runs. Reliability (passing/flaky/degraded) is the separate `health` dimension."
142
+ },
143
+ page: {
144
+ type: "number",
145
+ description: "Page number (default: 1)"
146
+ },
147
+ pageSize: {
148
+ type: "number",
149
+ description: "Page size (default: 25)"
150
+ }
151
+ }
152
+ }
153
+ },
154
+ {
155
+ name: "canary_workflow_get",
156
+ description: "Get a workflow definition with its steps, nodes, and configuration details.",
157
+ inputSchema: {
158
+ type: "object",
159
+ properties: {
160
+ workflowId: {
161
+ type: "string",
162
+ description: "The workflow UUID"
163
+ }
164
+ },
165
+ required: ["workflowId"]
166
+ }
167
+ },
168
+ {
169
+ name: "canary_workflow_patch",
170
+ description: "Update workflow properties using merge-patch. Only provided fields are updated; omitted fields are left unchanged.",
171
+ inputSchema: {
172
+ type: "object",
173
+ properties: {
174
+ workflowId: {
175
+ type: "string",
176
+ description: "The workflow UUID"
177
+ },
178
+ name: {
179
+ type: "string",
180
+ description: "New workflow name"
181
+ },
182
+ description: {
183
+ type: "string",
184
+ description: "New description"
185
+ },
186
+ status: {
187
+ type: "string",
188
+ enum: ["draft", "published"],
189
+ description: "New lifecycle status. 'draft' = editable, not run by suites. 'published' = active, included in regression runs. Reliability is tracked automatically by the `health` dimension; a flow that keeps failing becomes `degraded` and is skipped in suites until edited or re-enabled."
190
+ },
191
+ flowType: {
192
+ type: "string",
193
+ enum: ["standard", "setup", "seed", "teardown"],
194
+ description: "New flow type"
195
+ }
196
+ },
197
+ required: ["workflowId"]
198
+ }
199
+ },
200
+ {
201
+ name: "canary_workflow_run",
202
+ description: "Trigger a workflow execution. Returns the run ID for tracking status via canary_workflow_runs_get.",
203
+ inputSchema: {
204
+ type: "object",
205
+ properties: {
206
+ workflowId: {
207
+ type: "string",
208
+ description: "The workflow UUID to execute"
209
+ }
210
+ },
211
+ required: ["workflowId"]
212
+ }
213
+ },
214
+ {
215
+ name: "canary_workflow_sync",
216
+ description: "Atomically replace all nodes and edges in a workflow. Automatically creates a draft snapshot of the current state before overwriting so changes can be reverted. Use canary_workflow_get to read the current DAG first, modify nodes/edges, then sync back. When using propose=true on an existing workflow, REUSE each existing node's `id` for steps you keep (omit `id` only for genuinely new steps). Without ID reuse the proposal will diff as a full rewrite \u2014 every step shown as Removed+Added in the review UI even when only configs changed.",
217
+ inputSchema: {
218
+ type: "object",
219
+ properties: {
220
+ workflowId: {
221
+ type: "string",
222
+ description: "The workflow UUID"
223
+ },
224
+ nodes: {
225
+ type: "array",
226
+ description: "Array of node objects with id, nodeType, orderIndex, configJson",
227
+ items: { type: "object" }
228
+ },
229
+ edges: {
230
+ type: "array",
231
+ description: "Array of edge objects with id, sourceNodeId, targetNodeId, edgeType",
232
+ items: { type: "object" }
233
+ },
234
+ createSnapshot: {
235
+ type: "boolean",
236
+ description: "Create a draft snapshot before syncing (default: true)",
237
+ default: true
238
+ },
239
+ propose: {
240
+ type: "boolean",
241
+ description: "Create a proposal instead of applying directly. The proposal can be reviewed and accepted/rejected via the UI. IMPORTANT: when proposing on an existing workflow, reuse each existing node's `id` for steps you intend to keep \u2014 only omit `id` for genuinely new steps. Otherwise the diff will show every step as Removed+Added.",
242
+ default: false
243
+ },
244
+ proposalTitle: {
245
+ type: "string",
246
+ description: "Title for the proposal (used with propose: true)"
247
+ },
248
+ proposalDescription: {
249
+ type: "string",
250
+ description: "Description/rationale for the proposal (used with propose: true)"
251
+ }
252
+ },
253
+ required: ["workflowId", "nodes", "edges"]
254
+ }
255
+ }
256
+ ];
257
+ }
258
+ async function handleWorkflowTool(tool, args) {
259
+ if (tool === "canary_workflow_list") {
260
+ try {
261
+ const params = new URLSearchParams();
262
+ if (args.search) params.set("search", String(args.search));
263
+ if (args.flowType) params.set("flowType", String(args.flowType));
264
+ if (args.status) params.set("status", String(args.status));
265
+ if (args.page) params.set("page", String(args.page));
266
+ if (args.pageSize) params.set("pageSize", String(args.pageSize));
267
+ const qs = params.toString();
268
+ const path = `/workflows${qs ? `?${qs}` : ""}`;
269
+ const result = await mcpApiRequest("GET", path);
270
+ if (!result.ok) {
271
+ return toolJson({ ok: false, error: result.error ?? "Unknown error" });
272
+ }
273
+ return toolJson({ ok: true, data: result.data, pagination: result.pagination });
274
+ } catch (err) {
275
+ return toolJson({ ok: false, error: err instanceof Error ? err.message : String(err) });
276
+ }
277
+ }
278
+ if (tool === "canary_workflow_get") {
279
+ try {
280
+ const workflowId = args.workflowId;
281
+ const result = await mcpApiRequest("GET", `/workflows/${workflowId}`);
282
+ if (result.error) {
283
+ return toolJson({ ok: false, error: result.error });
284
+ }
285
+ return toolJson({ ok: true, workflow: result.workflow });
286
+ } catch (err) {
287
+ return toolJson({ ok: false, error: err instanceof Error ? err.message : String(err) });
288
+ }
289
+ }
290
+ if (tool === "canary_workflow_patch") {
291
+ try {
292
+ const workflowId = args.workflowId;
293
+ const body = {};
294
+ if (args.name) body.name = String(args.name);
295
+ if (args.description) body.description = String(args.description);
296
+ if (args.status) body.status = String(args.status);
297
+ if (args.flowType) body.flowType = String(args.flowType);
298
+ const result = await mcpApiRequest("PUT", `/workflows/${workflowId}`, body);
299
+ if (result.error) {
300
+ return toolJson({ ok: false, error: result.error });
301
+ }
302
+ return toolJson({ ok: true, workflow: result.workflow });
303
+ } catch (err) {
304
+ return toolJson({ ok: false, error: err instanceof Error ? err.message : String(err) });
305
+ }
306
+ }
307
+ if (tool === "canary_workflow_run") {
308
+ try {
309
+ const workflowId = args.workflowId;
310
+ const result = await mcpApiRequest("POST", `/workflows/${workflowId}/execute`);
311
+ if (result.error) {
312
+ return toolJson({ ok: false, error: result.error });
313
+ }
314
+ return toolJson({ ok: true, runId: result.runId, streamId: result.streamId });
315
+ } catch (err) {
316
+ return toolJson({ ok: false, error: err instanceof Error ? err.message : String(err) });
317
+ }
318
+ }
319
+ if (tool === "canary_workflow_sync") {
320
+ try {
321
+ const workflowId = args.workflowId;
322
+ const nodes = args.nodes;
323
+ const edges = args.edges;
324
+ if (args.propose) {
325
+ const title = args.proposalTitle ?? `Sync proposal (${nodes.length} nodes)`;
326
+ const description = args.proposalDescription ?? null;
327
+ const result2 = await mcpApiRequest("POST", `/workflows/${workflowId}/proposals`, {
328
+ title,
329
+ description,
330
+ snapshotJson: { nodes, edges },
331
+ source: "api"
332
+ });
333
+ if (result2.error) {
334
+ return toolJson({ ok: false, error: result2.error });
335
+ }
336
+ return toolJson({
337
+ ok: true,
338
+ proposalId: result2.data?.id,
339
+ title: result2.data?.title,
340
+ status: result2.data?.status
341
+ });
342
+ }
343
+ const createSnapshot = args.createSnapshot !== false;
344
+ const result = await mcpApiRequest("PUT", `/workflows/${workflowId}/draft-state`, {
345
+ nodes,
346
+ edges,
347
+ createSnapshot
348
+ });
349
+ if (result.error) {
350
+ return toolJson({ ok: false, error: result.error });
351
+ }
352
+ return toolJson({
353
+ ok: true,
354
+ ...result.snapshotVersion ? { snapshotVersion: result.snapshotVersion } : {}
355
+ });
356
+ } catch (err) {
357
+ return toolJson({ ok: false, error: err instanceof Error ? err.message : String(err) });
358
+ }
359
+ }
360
+ return null;
361
+ }
362
+
363
+ // ../mcp-tools/src/groups/workflow-proposals.ts
364
+ function getWorkflowProposalsToolDefinitions() {
365
+ return [
366
+ {
367
+ name: "canary_workflow_proposals_list",
368
+ description: 'List workflow proposals. By default returns pending proposals; pass status="all" for every status.',
369
+ inputSchema: {
370
+ type: "object",
371
+ properties: {
372
+ workflowId: { type: "string", description: "Workflow UUID" },
373
+ status: {
374
+ type: "string",
375
+ enum: ["pending", "accepted", "rejected", "stale", "all"],
376
+ description: "Filter by status (default: pending)"
377
+ }
378
+ },
379
+ required: ["workflowId"]
380
+ }
381
+ },
382
+ {
383
+ name: "canary_workflow_proposals_get",
384
+ description: "Fetch a proposal including its full snapshot JSON.",
385
+ inputSchema: {
386
+ type: "object",
387
+ properties: {
388
+ workflowId: { type: "string" },
389
+ proposalId: { type: "string" }
390
+ },
391
+ required: ["workflowId", "proposalId"]
392
+ }
393
+ },
394
+ {
395
+ name: "canary_workflow_proposals_diff",
396
+ description: "Compute a per-node diff between a proposal and the current workflow draft. Returns summary counts plus per-field changes for changed nodes.",
397
+ inputSchema: {
398
+ type: "object",
399
+ properties: {
400
+ workflowId: { type: "string" },
401
+ proposalId: { type: "string" }
402
+ },
403
+ required: ["workflowId", "proposalId"]
404
+ }
405
+ },
406
+ {
407
+ name: "canary_workflow_proposals_accept",
408
+ description: "Apply a proposal to the workflow draft. Omit acceptedNodeIds for atomic (all-or-nothing) accept; provide it to cherry-pick a subset.",
409
+ inputSchema: {
410
+ type: "object",
411
+ properties: {
412
+ workflowId: { type: "string" },
413
+ proposalId: { type: "string" },
414
+ acceptedNodeIds: {
415
+ type: "array",
416
+ items: { type: "string" },
417
+ description: "Optional: node ids from the diff to include. Omit for atomic accept of every change."
418
+ }
419
+ },
420
+ required: ["workflowId", "proposalId"]
421
+ }
422
+ },
423
+ {
424
+ name: "canary_workflow_proposals_reject",
425
+ description: "Reject a pending proposal. Does not change the workflow draft.",
426
+ inputSchema: {
427
+ type: "object",
428
+ properties: {
429
+ workflowId: { type: "string" },
430
+ proposalId: { type: "string" }
431
+ },
432
+ required: ["workflowId", "proposalId"]
433
+ }
434
+ },
435
+ {
436
+ name: "canary_workflow_proposals_revert",
437
+ description: "Revert a previously-accepted proposal back to pending. Only the user who accepted it may revert.",
438
+ inputSchema: {
439
+ type: "object",
440
+ properties: {
441
+ workflowId: { type: "string" },
442
+ proposalId: { type: "string" }
443
+ },
444
+ required: ["workflowId", "proposalId"]
445
+ }
446
+ }
447
+ ];
448
+ }
449
+ async function handleWorkflowProposalsTool(tool, args) {
450
+ if (!tool.startsWith("canary_workflow_proposals_")) return null;
451
+ try {
452
+ switch (tool) {
453
+ case "canary_workflow_proposals_list":
454
+ return await listProposals(args);
455
+ case "canary_workflow_proposals_get":
456
+ return await getProposal(args);
457
+ case "canary_workflow_proposals_diff":
458
+ return await diffProposal(args);
459
+ case "canary_workflow_proposals_accept":
460
+ return await acceptProposal(args);
461
+ case "canary_workflow_proposals_reject":
462
+ return await rejectProposal(args);
463
+ case "canary_workflow_proposals_revert":
464
+ return await revertProposal(args);
465
+ default:
466
+ return null;
467
+ }
468
+ } catch (err) {
469
+ return toolJson({
470
+ ok: false,
471
+ error: err instanceof Error ? err.message : String(err)
472
+ });
473
+ }
474
+ }
475
+ async function listProposals(args) {
476
+ const workflowId = String(args.workflowId);
477
+ const status = typeof args.status === "string" ? args.status : "pending";
478
+ const query = status === "all" ? "" : `?status=${encodeURIComponent(status)}`;
479
+ const res = await mcpApiRequest("GET", `/workflows/${workflowId}/proposals${query}`);
480
+ return toolJson({
481
+ ok: res.ok,
482
+ workflowId,
483
+ count: res.data?.length ?? 0,
484
+ proposals: res.data ?? [],
485
+ error: res.error
486
+ });
487
+ }
488
+ async function getProposal(args) {
489
+ const workflowId = String(args.workflowId);
490
+ const proposalId = String(args.proposalId);
491
+ const res = await mcpApiRequest("GET", `/workflows/${workflowId}/proposals/${proposalId}`);
492
+ return toolJson(res);
493
+ }
494
+ async function diffProposal(args) {
495
+ const workflowId = String(args.workflowId);
496
+ const proposalId = String(args.proposalId);
497
+ const [propRes, wfRes] = await Promise.all([
498
+ mcpApiRequest(
499
+ "GET",
500
+ `/workflows/${workflowId}/proposals/${proposalId}`
501
+ ),
502
+ mcpApiRequest(
503
+ "GET",
504
+ `/workflows/${workflowId}`
505
+ )
506
+ ]);
507
+ if (!propRes.ok || !propRes.data) {
508
+ return toolJson({ ok: false, error: propRes.error ?? "proposal not found" });
509
+ }
510
+ if (!wfRes.workflow) {
511
+ return toolJson({ ok: false, error: wfRes.error ?? "workflow not found" });
512
+ }
513
+ const current = fromApiSnapshot({
514
+ nodes: wfRes.workflow.nodes,
515
+ edges: wfRes.workflow.edges
516
+ });
517
+ const proposed = fromApiSnapshot(propRes.data.snapshotJson);
518
+ const diff = diffSnapshots(current, proposed);
519
+ return toolJson(buildDiffPayload(propRes.data, diff));
520
+ }
521
+ async function acceptProposal(args) {
522
+ const workflowId = String(args.workflowId);
523
+ const proposalId = String(args.proposalId);
524
+ const acceptedNodeIds = Array.isArray(args.acceptedNodeIds) ? args.acceptedNodeIds.filter((s) => typeof s === "string") : null;
525
+ const body = acceptedNodeIds ? { selection: { acceptedNodeIds } } : void 0;
526
+ const res = await mcpApiRequest(
527
+ "POST",
528
+ `/workflows/${workflowId}/proposals/${proposalId}/accept`,
529
+ body
530
+ );
531
+ return toolJson({
532
+ ...res,
533
+ workflowId,
534
+ proposalId,
535
+ partial: acceptedNodeIds !== null
536
+ });
537
+ }
538
+ async function rejectProposal(args) {
539
+ const workflowId = String(args.workflowId);
540
+ const proposalId = String(args.proposalId);
541
+ const res = await mcpApiRequest(
542
+ "POST",
543
+ `/workflows/${workflowId}/proposals/${proposalId}/reject`
544
+ );
545
+ return toolJson({ ...res, workflowId, proposalId });
546
+ }
547
+ async function revertProposal(args) {
548
+ const workflowId = String(args.workflowId);
549
+ const proposalId = String(args.proposalId);
550
+ const res = await mcpApiRequest(
551
+ "POST",
552
+ `/workflows/${workflowId}/proposals/${proposalId}/revert`
553
+ );
554
+ return toolJson({ ...res, workflowId, proposalId });
555
+ }
556
+ function buildDiffPayload(proposal, diff) {
557
+ return {
558
+ proposal: {
559
+ id: proposal.id,
560
+ title: proposal.title,
561
+ source: proposal.source,
562
+ status: proposal.status,
563
+ stale: proposal.status === "stale",
564
+ createdAt: proposal.createdAt,
565
+ decidedAt: proposal.decidedAt
566
+ },
567
+ summary: {
568
+ addedNodes: diff.addedNodes.length,
569
+ removedNodes: diff.removedNodes.length,
570
+ changedNodes: diff.changedNodes.length,
571
+ addedEdges: diff.addedEdges.length,
572
+ removedEdges: diff.removedEdges.length,
573
+ touchedCount: diff.touchedCount,
574
+ isEmpty: diff.isEmpty
575
+ },
576
+ nodes: {
577
+ added: diff.addedNodes.map((n) => ({
578
+ id: n.id,
579
+ nodeType: n.nodeType,
580
+ configJson: n.configJson
581
+ })),
582
+ removed: diff.removedNodes.map((n) => ({
583
+ id: n.id,
584
+ nodeType: n.nodeType,
585
+ configJson: n.configJson
586
+ })),
587
+ changed: diff.changedNodes.map((c) => ({
588
+ id: c.id,
589
+ nodeType: c.nodeType,
590
+ typeChanged: c.typeChanged,
591
+ beforeType: c.beforeType,
592
+ afterType: c.afterType,
593
+ fieldChanges: c.fieldChanges
594
+ }))
595
+ },
596
+ edges: {
597
+ added: diff.addedEdges.map((e) => ({
598
+ sourceNodeId: e.sourceNodeId,
599
+ targetNodeId: e.targetNodeId,
600
+ edgeType: e.edgeType ?? null
601
+ })),
602
+ removed: diff.removedEdges.map((e) => ({
603
+ sourceNodeId: e.sourceNodeId,
604
+ targetNodeId: e.targetNodeId,
605
+ edgeType: e.edgeType ?? null
606
+ }))
607
+ }
608
+ };
609
+ }
610
+
611
+ // ../mcp-tools/src/groups/workflow-runs.ts
612
+ function getWorkflowRunsToolDefinitions() {
613
+ return [
614
+ {
615
+ name: "canary_workflow_runs_list",
616
+ description: "List runs for a specific workflow with status, duration, and pagination.",
617
+ inputSchema: {
618
+ type: "object",
619
+ properties: {
620
+ workflowId: {
621
+ type: "string",
622
+ description: "The workflow UUID"
623
+ },
624
+ page: {
625
+ type: "number",
626
+ description: "Page number (default: 1)"
627
+ },
628
+ pageSize: {
629
+ type: "number",
630
+ description: "Page size (default: 10)"
631
+ },
632
+ mode: {
633
+ type: "string",
634
+ enum: ["browser", "api"],
635
+ description: "Filter by run mode"
636
+ }
637
+ },
638
+ required: ["workflowId"]
639
+ }
640
+ },
641
+ {
642
+ name: "canary_workflow_runs_get",
643
+ description: "Get workflow run detail including steps, status, duration, and error messages. Also serves as a status check for in-progress runs.",
644
+ inputSchema: {
645
+ type: "object",
646
+ properties: {
647
+ workflowId: {
648
+ type: "string",
649
+ description: "The workflow UUID"
650
+ },
651
+ runId: {
652
+ type: "string",
653
+ description: "The workflow run UUID"
654
+ }
655
+ },
656
+ required: ["workflowId", "runId"]
657
+ }
658
+ },
659
+ {
660
+ name: "canary_workflow_runs_diagnostics",
661
+ description: "Get failure diagnostic for a workflow run. Returns AI-generated analysis if available, otherwise falls back to console logs and network requests.",
662
+ inputSchema: {
663
+ type: "object",
664
+ properties: {
665
+ workflowId: {
666
+ type: "string",
667
+ description: "The workflow UUID"
668
+ },
669
+ runId: {
670
+ type: "string",
671
+ description: "The workflow run UUID"
672
+ }
673
+ },
674
+ required: ["workflowId", "runId"]
675
+ }
676
+ }
677
+ ];
678
+ }
679
+ async function handleWorkflowRunsTool(tool, args) {
680
+ if (tool === "canary_workflow_runs_list") {
681
+ try {
682
+ const workflowId = args.workflowId;
683
+ const params = new URLSearchParams();
684
+ params.set("page", String(args.page ?? 1));
685
+ params.set("pageSize", String(args.pageSize ?? 10));
686
+ if (args.mode) params.set("mode", String(args.mode));
687
+ const result = await mcpApiRequest("GET", `/workflows/${workflowId}/runs?${params}`);
688
+ if (!result.ok) {
689
+ return toolJson({ ok: false, error: result.error ?? "Unknown error" });
690
+ }
691
+ return toolJson({ ok: true, runs: result.data.runs, pagination: result.pagination });
692
+ } catch (err) {
693
+ return toolJson({ ok: false, error: err instanceof Error ? err.message : String(err) });
694
+ }
695
+ }
696
+ if (tool === "canary_workflow_runs_get") {
697
+ try {
698
+ const workflowId = args.workflowId;
699
+ const runId = args.runId;
700
+ const result = await mcpApiRequest("GET", `/workflows/${workflowId}/runs/${runId}`);
701
+ if (!result.ok) {
702
+ return toolJson({ ok: false, error: result.error ?? "Unknown error" });
703
+ }
704
+ return toolJson({ ok: true, data: result.data });
705
+ } catch (err) {
706
+ return toolJson({ ok: false, error: err instanceof Error ? err.message : String(err) });
707
+ }
708
+ }
709
+ if (tool === "canary_workflow_runs_diagnostics") {
710
+ try {
711
+ const workflowId = args.workflowId;
712
+ const runId = args.runId;
713
+ const result = await mcpApiRequest("GET", `/workflows/${workflowId}/runs/${runId}/failure-diagnostic`);
714
+ if (result.ok && result.data) {
715
+ return toolJson({ ok: true, data: result.data });
716
+ }
717
+ const rawResult = await mcpApiRequest("GET", `/workflows/${workflowId}/runs/${runId}/diagnostics`);
718
+ if (!rawResult.ok) {
719
+ return toolJson({ ok: true, data: null, message: "No diagnostic available" });
720
+ }
721
+ return toolJson({
722
+ ok: true,
723
+ consoleLogs: rawResult.consoleLogs,
724
+ networkRequests: rawResult.networkRequests
725
+ });
726
+ } catch (err) {
727
+ return toolJson({ ok: false, error: err instanceof Error ? err.message : String(err) });
728
+ }
729
+ }
730
+ return null;
731
+ }
732
+
733
+ // ../mcp-tools/src/groups/test-suites.ts
734
+ function getTestSuiteToolDefinitions() {
735
+ return [
736
+ {
737
+ name: "canary_test_suite_run",
738
+ description: "Trigger a test suite execution. Returns the suite ID and job ID. Use canary_test_suite_get to check results after triggering.",
739
+ inputSchema: {
740
+ type: "object",
741
+ properties: {
742
+ tag: {
743
+ type: "string",
744
+ description: "Filter workflows by tag"
745
+ },
746
+ namePattern: {
747
+ type: "string",
748
+ description: "Filter workflows by name pattern (regex)"
749
+ },
750
+ propertyId: {
751
+ type: "string",
752
+ description: "Target a specific property by UUID"
753
+ },
754
+ environmentId: {
755
+ type: "string",
756
+ description: "Target a specific environment by UUID"
757
+ }
758
+ }
759
+ }
760
+ },
761
+ {
762
+ name: "canary_test_suite_list",
763
+ description: "List recent test suite executions with pass/fail counts, ordered by most recent first.",
764
+ inputSchema: {
765
+ type: "object",
766
+ properties: {
767
+ page: {
768
+ type: "number",
769
+ description: "Page number (default: 1)"
770
+ },
771
+ pageSize: {
772
+ type: "number",
773
+ description: "Page size (default: 20)"
774
+ }
775
+ }
776
+ }
777
+ },
778
+ {
779
+ name: "canary_test_suite_get",
780
+ description: "Get test suite detail including all individual test runs and their results.",
781
+ inputSchema: {
782
+ type: "object",
783
+ properties: {
784
+ executionId: {
785
+ type: "string",
786
+ description: "The test execution UUID"
787
+ }
788
+ },
789
+ required: ["executionId"]
790
+ }
791
+ },
792
+ {
793
+ name: "canary_test_suite_diagnostics",
794
+ description: "Get AI-generated failure diagnostic for a specific workflow run, including root cause analysis and recommendations.",
795
+ inputSchema: {
796
+ type: "object",
797
+ properties: {
798
+ workflowId: {
799
+ type: "string",
800
+ description: "The workflow UUID"
801
+ },
802
+ runId: {
803
+ type: "string",
804
+ description: "The workflow run UUID"
805
+ }
806
+ },
807
+ required: ["workflowId", "runId"]
808
+ }
809
+ },
810
+ {
811
+ name: "canary_test_suite_steps",
812
+ description: "Get the individual steps of a workflow run with their status, duration, and error messages.",
813
+ inputSchema: {
814
+ type: "object",
815
+ properties: {
816
+ workflowId: {
817
+ type: "string",
818
+ description: "The workflow UUID"
819
+ },
820
+ runId: {
821
+ type: "string",
822
+ description: "The workflow run UUID"
823
+ }
824
+ },
825
+ required: ["workflowId", "runId"]
826
+ }
827
+ }
828
+ ];
829
+ }
830
+ async function handleTestSuiteTool(tool, args) {
831
+ if (tool === "canary_test_suite_run") {
832
+ try {
833
+ const params = new URLSearchParams();
834
+ if (args.tag) params.set("tag", String(args.tag));
835
+ if (args.namePattern) params.set("namePattern", String(args.namePattern));
836
+ const queryStr = params.toString();
837
+ const path = `/workflows/test-runs${queryStr ? `?${queryStr}` : ""}`;
838
+ const body = {};
839
+ if (args.propertyId) body.propertyId = String(args.propertyId);
840
+ if (args.environmentId) body.environmentId = String(args.environmentId);
841
+ const data = await mcpApiRequest("POST", path, body);
842
+ if (!data.ok) {
843
+ return toolJson({ ok: false, error: data.error ?? "Unknown error" });
844
+ }
845
+ return toolJson({
846
+ ok: true,
847
+ suiteId: data.suiteId,
848
+ jobId: data.jobId,
849
+ appUrl: data.appUrl,
850
+ message: `Test suite triggered. Use canary_test_suite_get or canary_test_suite_list to check results.`
851
+ });
852
+ } catch (err) {
853
+ return toolJson({ ok: false, error: err instanceof Error ? err.message : String(err) });
854
+ }
855
+ }
856
+ if (tool === "canary_test_suite_list") {
857
+ try {
858
+ const params = new URLSearchParams();
859
+ params.set("page", String(args.page ?? 1));
860
+ params.set("pageSize", String(args.pageSize ?? 20));
861
+ params.set("sortBy", "createdAt");
862
+ params.set("sortOrder", "desc");
863
+ const result = await mcpApiRequest("GET", `/workflows/test-executions?${params}`);
864
+ if (!result.ok) {
865
+ return toolJson({ ok: false, error: result.error ?? "Unknown error" });
866
+ }
867
+ return toolJson({ ok: true, data: result.data, pagination: result.pagination });
868
+ } catch (err) {
869
+ return toolJson({ ok: false, error: err instanceof Error ? err.message : String(err) });
870
+ }
871
+ }
872
+ if (tool === "canary_test_suite_get") {
873
+ try {
874
+ const executionId = args.executionId;
875
+ const result = await mcpApiRequest("GET", `/workflows/test-executions/${executionId}`);
876
+ if (!result.ok) {
877
+ return toolJson({ ok: false, error: result.error ?? "Unknown error" });
878
+ }
879
+ return toolJson({ ok: true, execution: result.execution, testRuns: result.testRuns });
880
+ } catch (err) {
881
+ return toolJson({ ok: false, error: err instanceof Error ? err.message : String(err) });
882
+ }
883
+ }
884
+ if (tool === "canary_test_suite_diagnostics") {
885
+ try {
886
+ const workflowId = args.workflowId;
887
+ const runId = args.runId;
888
+ const result = await mcpApiRequest("GET", `/workflows/${workflowId}/runs/${runId}/failure-diagnostic`);
889
+ if (!result.ok) {
890
+ return toolJson({ ok: false, error: result.error ?? "Unknown error" });
891
+ }
892
+ return toolJson({ ok: true, data: result.data });
893
+ } catch (err) {
894
+ return toolJson({ ok: false, error: err instanceof Error ? err.message : String(err) });
895
+ }
896
+ }
897
+ if (tool === "canary_test_suite_steps") {
898
+ try {
899
+ const workflowId = args.workflowId;
900
+ const runId = args.runId;
901
+ const result = await mcpApiRequest("GET", `/workflows/${workflowId}/runs/${runId}`);
902
+ if (!result.ok) {
903
+ return toolJson({ ok: false, error: result.error ?? "Unknown error" });
904
+ }
905
+ return toolJson({ ok: true, data: result.data });
906
+ } catch (err) {
907
+ return toolJson({ ok: false, error: err instanceof Error ? err.message : String(err) });
908
+ }
909
+ }
910
+ return null;
911
+ }
912
+
913
+ // ../mcp-tools/src/groups/credentials.ts
914
+ function getCredentialToolDefinitions() {
915
+ return [
916
+ {
917
+ name: "canary_credential_list",
918
+ description: "List credentials in the current organization with their property and environment info.",
919
+ inputSchema: {
920
+ type: "object",
921
+ properties: {
922
+ property: {
923
+ type: "string",
924
+ description: "Filter by property name or ID"
925
+ }
926
+ }
927
+ }
928
+ }
929
+ ];
930
+ }
931
+ async function handleCredentialTool(tool, args) {
932
+ if (tool === "canary_credential_list") {
933
+ try {
934
+ const result = await mcpApiRequest("GET", "/org/credentials");
935
+ let items = result.items ?? [];
936
+ items = items.filter((c) => !c.isEphemeral);
937
+ if (args.property) {
938
+ const filter = String(args.property).toLowerCase();
939
+ items = items.filter(
940
+ (c) => c.propertyId === args.property || c.propertyName && c.propertyName.toLowerCase().includes(filter)
941
+ );
942
+ }
943
+ return toolJson({ ok: true, credentials: items });
944
+ } catch (err) {
945
+ return toolJson({ ok: false, error: err instanceof Error ? err.message : String(err) });
946
+ }
947
+ }
948
+ return null;
949
+ }
950
+
951
+ // ../mcp-tools/src/groups/issues.ts
952
+ function getIssueToolDefinitions() {
953
+ return [
954
+ {
955
+ name: "canary_issues_list",
956
+ description: "List and search issues in the current organization. Supports full-text search, severity/status filtering, and pagination.",
957
+ inputSchema: {
958
+ type: "object",
959
+ properties: {
960
+ search: {
961
+ type: "string",
962
+ description: "Full-text search query"
963
+ },
964
+ severity: {
965
+ type: "string",
966
+ enum: ["low", "medium", "high", "unknown"],
967
+ description: "Filter by severity level"
968
+ },
969
+ statuses: {
970
+ type: "string",
971
+ description: "Comma-separated status filter: open, closed, not_a_bug"
972
+ },
973
+ propertyId: {
974
+ type: "string",
975
+ description: "Filter by property UUID"
976
+ },
977
+ page: {
978
+ type: "number",
979
+ description: "Page number (default: 1)"
980
+ },
981
+ pageSize: {
982
+ type: "number",
983
+ description: "Page size (default: 25)"
984
+ }
985
+ }
986
+ }
987
+ },
988
+ {
989
+ name: "canary_issues_get",
990
+ description: "Get issue detail with diagnostics, including console errors and network failures from the latest occurrence.",
991
+ inputSchema: {
992
+ type: "object",
993
+ properties: {
994
+ issueId: {
995
+ type: "string",
996
+ description: "The issue UUID"
997
+ }
998
+ },
999
+ required: ["issueId"]
1000
+ }
1001
+ }
1002
+ ];
1003
+ }
1004
+ async function handleIssueTool(tool, args) {
1005
+ if (tool === "canary_issues_list") {
1006
+ try {
1007
+ const params = new URLSearchParams();
1008
+ if (args.search) params.set("search", String(args.search));
1009
+ if (args.severity) params.set("severity", String(args.severity));
1010
+ if (args.statuses) params.set("statuses", String(args.statuses));
1011
+ if (args.propertyId) params.set("propertyId", String(args.propertyId));
1012
+ if (args.page) params.set("page", String(args.page));
1013
+ if (args.pageSize) params.set("pageSize", String(args.pageSize));
1014
+ const qs = params.toString();
1015
+ const path = `/v2/issues${qs ? `?${qs}` : ""}`;
1016
+ const result = await mcpApiRequest("GET", path);
1017
+ if (!result.ok) {
1018
+ return toolJson({ ok: false, error: result.error ?? "Unknown error" });
1019
+ }
1020
+ return toolJson({ ok: true, data: result.data, pagination: result.pagination });
1021
+ } catch (err) {
1022
+ return toolJson({ ok: false, error: err instanceof Error ? err.message : String(err) });
1023
+ }
1024
+ }
1025
+ if (tool === "canary_issues_get") {
1026
+ try {
1027
+ const issueId = args.issueId;
1028
+ const detailResult = await mcpApiRequest("GET", `/v2/issues/${issueId}`);
1029
+ if (!detailResult.ok) {
1030
+ return toolJson({ ok: false, error: detailResult.error ?? "Unknown error" });
1031
+ }
1032
+ const { issue } = detailResult.data;
1033
+ const occ = issue.latestOccurrence;
1034
+ let consoleErrors = [];
1035
+ let networkErrors = [];
1036
+ if (occ) {
1037
+ try {
1038
+ const [consoleRes, networkRes] = await Promise.all([
1039
+ mcpApiRequest(
1040
+ "GET",
1041
+ `/v2/issues/${issueId}/occurrences/${occ.id}/console-logs`
1042
+ ),
1043
+ mcpApiRequest(
1044
+ "GET",
1045
+ `/v2/issues/${issueId}/occurrences/${occ.id}/network-requests`
1046
+ )
1047
+ ]);
1048
+ if (consoleRes.ok && Array.isArray(consoleRes.data)) {
1049
+ consoleErrors = consoleRes.data.filter(
1050
+ (e) => e.type === "error"
1051
+ );
1052
+ }
1053
+ if (networkRes.ok && Array.isArray(networkRes.data)) {
1054
+ networkErrors = networkRes.data.filter(
1055
+ (e) => e.status === null || e.status === 0 || e.status !== null && e.status >= 400
1056
+ );
1057
+ }
1058
+ } catch {
1059
+ }
1060
+ }
1061
+ return toolJson({
1062
+ ok: true,
1063
+ ...detailResult.data,
1064
+ consoleErrors,
1065
+ networkErrors
1066
+ });
1067
+ } catch (err) {
1068
+ return toolJson({ ok: false, error: err instanceof Error ? err.message : String(err) });
1069
+ }
1070
+ }
1071
+ return null;
1072
+ }
1073
+
1074
+ // ../mcp-tools/src/groups/index.ts
1075
+ var workflowsGroup = {
1076
+ id: "workflows",
1077
+ definitions: getWorkflowToolDefinitions,
1078
+ handle: (tool, args) => handleWorkflowTool(tool, args)
1079
+ };
1080
+ var workflowProposalsGroup = {
1081
+ id: "workflow-proposals",
1082
+ definitions: getWorkflowProposalsToolDefinitions,
1083
+ handle: (tool, args) => handleWorkflowProposalsTool(tool, args)
1084
+ };
1085
+ var workflowRunsGroup = {
1086
+ id: "workflow-runs",
1087
+ definitions: getWorkflowRunsToolDefinitions,
1088
+ handle: (tool, args) => handleWorkflowRunsTool(tool, args)
1089
+ };
1090
+ var testSuitesGroup = {
1091
+ id: "test-suites",
1092
+ definitions: getTestSuiteToolDefinitions,
1093
+ handle: (tool, args) => handleTestSuiteTool(tool, args)
1094
+ };
1095
+ var credentialsGroup = {
1096
+ id: "credentials",
1097
+ definitions: getCredentialToolDefinitions,
1098
+ handle: (tool, args) => handleCredentialTool(tool, args)
1099
+ };
1100
+ var issuesGroup = {
1101
+ id: "issues",
1102
+ definitions: getIssueToolDefinitions,
1103
+ handle: (tool, args) => handleIssueTool(tool, args)
1104
+ };
1105
+ var sharedGroups = [
1106
+ workflowsGroup,
1107
+ workflowProposalsGroup,
1108
+ workflowRunsGroup,
1109
+ testSuitesGroup,
1110
+ credentialsGroup,
1111
+ issuesGroup
1112
+ ];
1113
+
1114
+ // src/mcp/api-url.ts
1115
+ import process from "process";
1116
+ var DEFAULT_API_URL = "https://api.trycanary.ai";
1117
+ function resolveApiUrl() {
1118
+ return process.env.CANARY_API_URL ?? DEFAULT_API_URL;
1119
+ }
1120
+
1121
+ // src/mcp/tool-helpers.ts
1122
+ function toolText2(text) {
1123
+ return { content: [{ type: "text", text }] };
1124
+ }
1125
+ function toolJson2(data) {
1126
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
1127
+ }
1128
+ function formatMCPContent(result) {
1129
+ if (typeof result === "string") {
1130
+ return { content: [{ type: "text", text: result }] };
1131
+ }
1132
+ const content = [];
1133
+ if (result.text) {
1134
+ content.push({ type: "text", text: result.text });
1135
+ }
1136
+ for (const img of result.images ?? []) {
1137
+ content.push({ type: "image", data: img.data, mimeType: img.mimeType });
1138
+ }
1139
+ return { content };
1140
+ }
1141
+
1142
+ // src/mcp/session-tools.ts
1143
+ import process2 from "process";
1144
+ var browserSessions = /* @__PURE__ */ new Map();
1145
+ var DEFAULT_API_URL2 = "https://api.trycanary.ai";
1146
+ function resolveApiUrl2(input) {
1147
+ return input ?? process2.env.CANARY_API_URL ?? DEFAULT_API_URL2;
1148
+ }
1149
+ async function resolveToken() {
1150
+ const token = process2.env.CANARY_API_TOKEN ?? await readStoredToken();
1151
+ if (!token) {
1152
+ throw new Error("Missing token. Run `canary login` first or set CANARY_API_TOKEN.");
1153
+ }
1154
+ return token;
1155
+ }
1156
+ function getSessionToolDefinitions() {
1157
+ return [
1158
+ {
1159
+ name: "local_browser_start",
1160
+ description: "Start a local browser session that connects to the cloud agent. The cloud agent can then control this browser to test local applications. Returns sessionId for tracking.",
1161
+ inputSchema: {
1162
+ type: "object",
1163
+ properties: {
1164
+ mode: {
1165
+ type: "string",
1166
+ enum: ["playwright", "cdp"],
1167
+ description: "Browser mode: 'playwright' for fresh browser, 'cdp' to connect to existing Chrome"
1168
+ },
1169
+ cdpUrl: {
1170
+ type: "string",
1171
+ description: "CDP endpoint URL when mode is 'cdp' (e.g. http://localhost:9222)"
1172
+ },
1173
+ headless: {
1174
+ type: "boolean",
1175
+ description: "Run browser headless (default: true for playwright mode)"
1176
+ },
1177
+ storageStatePath: {
1178
+ type: "string",
1179
+ description: "Path to Playwright storage state JSON for pre-authenticated sessions"
1180
+ },
1181
+ instructions: {
1182
+ type: "string",
1183
+ description: "Instructions for the cloud agent on what to test"
1184
+ }
1185
+ }
1186
+ }
1187
+ },
1188
+ {
1189
+ name: "local_browser_status",
1190
+ description: "Check the status of a local browser session.",
1191
+ inputSchema: {
1192
+ type: "object",
1193
+ properties: {
1194
+ sessionId: { type: "string" }
1195
+ },
1196
+ required: ["sessionId"]
1197
+ }
1198
+ },
1199
+ {
1200
+ name: "local_browser_stop",
1201
+ description: "Stop a local browser session and close the browser.",
1202
+ inputSchema: {
1203
+ type: "object",
1204
+ properties: {
1205
+ sessionId: { type: "string" }
1206
+ },
1207
+ required: ["sessionId"]
1208
+ }
1209
+ },
1210
+ {
1211
+ name: "local_browser_list",
1212
+ description: "List all active local browser sessions.",
1213
+ inputSchema: {
1214
+ type: "object",
1215
+ properties: {}
1216
+ }
1217
+ },
1218
+ {
1219
+ name: "local_browser_run",
1220
+ description: "Start a test run on an active local browser session. The cloud agent will control the local browser according to the instructions.",
1221
+ inputSchema: {
1222
+ type: "object",
1223
+ properties: {
1224
+ sessionId: {
1225
+ type: "string",
1226
+ description: "The session ID from local_browser_start"
1227
+ },
1228
+ instructions: {
1229
+ type: "string",
1230
+ description: "Instructions for the cloud agent on what to test"
1231
+ },
1232
+ startUrl: {
1233
+ type: "string",
1234
+ description: "Optional URL to navigate to before starting"
1235
+ }
1236
+ },
1237
+ required: ["sessionId", "instructions"]
1238
+ }
1239
+ }
1240
+ ];
1241
+ }
1242
+ async function handleSessionTool(tool, args) {
1243
+ const token = await resolveToken();
1244
+ if (tool === "local_browser_start") {
1245
+ const input = args;
1246
+ const apiUrl = resolveApiUrl2();
1247
+ const mode = input.mode ?? "playwright";
1248
+ const sessionResponse = await fetch(`${apiUrl}/local-browser/sessions`, {
1249
+ method: "POST",
1250
+ headers: {
1251
+ "Content-Type": "application/json",
1252
+ Authorization: `Bearer ${token}`
1253
+ },
1254
+ body: JSON.stringify({
1255
+ browserMode: mode,
1256
+ instructions: input.instructions ?? null
1257
+ })
1258
+ });
1259
+ if (!sessionResponse.ok) {
1260
+ const text = await sessionResponse.text();
1261
+ return toolJson2({ ok: false, error: `Failed to create session: ${text}` });
1262
+ }
1263
+ const session = await sessionResponse.json();
1264
+ const host = new LocalBrowserHost({
1265
+ apiUrl,
1266
+ wsToken: session.wsToken,
1267
+ sessionId: session.sessionId,
1268
+ browserMode: mode,
1269
+ cdpUrl: input.cdpUrl,
1270
+ headless: input.headless ?? true,
1271
+ storageStatePath: input.storageStatePath,
1272
+ onLog: (level, message) => {
1273
+ if (level === "error") {
1274
+ console.error(`[LocalBrowser] ${message}`);
1275
+ }
1276
+ }
1277
+ });
1278
+ host.start().catch((err) => {
1279
+ console.error("Failed to start local browser:", err);
1280
+ browserSessions.delete(session.sessionId);
1281
+ });
1282
+ browserSessions.set(session.sessionId, {
1283
+ sessionId: session.sessionId,
1284
+ host,
1285
+ startedAt: Date.now(),
1286
+ mode
1287
+ });
1288
+ return toolJson2({
1289
+ ok: true,
1290
+ sessionId: session.sessionId,
1291
+ mode,
1292
+ expiresAt: session.expiresAt,
1293
+ note: "Browser session started. The cloud agent can now control this browser. Use local_browser_stop to end the session."
1294
+ });
1295
+ }
1296
+ if (tool === "local_browser_status") {
1297
+ const input = args;
1298
+ const session = browserSessions.get(input.sessionId);
1299
+ if (!session) {
1300
+ return toolJson2({ ok: false, error: "Session not found", sessionId: input.sessionId });
1301
+ }
1302
+ return toolJson2({
1303
+ ok: true,
1304
+ sessionId: session.sessionId,
1305
+ mode: session.mode,
1306
+ startedAt: new Date(session.startedAt).toISOString(),
1307
+ uptimeMs: Date.now() - session.startedAt
1308
+ });
1309
+ }
1310
+ if (tool === "local_browser_stop") {
1311
+ const input = args;
1312
+ const session = browserSessions.get(input.sessionId);
1313
+ if (!session) {
1314
+ return toolJson2({ ok: false, error: "Session not found", sessionId: input.sessionId });
1315
+ }
1316
+ await session.host.stop();
1317
+ browserSessions.delete(input.sessionId);
1318
+ return toolJson2({
1319
+ ok: true,
1320
+ sessionId: input.sessionId,
1321
+ note: "Browser session stopped and browser closed."
1322
+ });
1323
+ }
1324
+ if (tool === "local_browser_list") {
1325
+ const sessions = Array.from(browserSessions.values()).map((s) => ({
1326
+ sessionId: s.sessionId,
1327
+ mode: s.mode,
1328
+ startedAt: new Date(s.startedAt).toISOString(),
1329
+ uptimeMs: Date.now() - s.startedAt
1330
+ }));
1331
+ return toolJson2({
1332
+ ok: true,
1333
+ count: sessions.length,
1334
+ sessions
1335
+ });
1336
+ }
1337
+ if (tool === "local_browser_run") {
1338
+ const input = args;
1339
+ const apiUrl = resolveApiUrl2();
1340
+ const session = browserSessions.get(input.sessionId);
1341
+ if (!session) {
1342
+ return toolJson2({
1343
+ ok: false,
1344
+ error: "Session not found locally. Make sure you started it with local_browser_start.",
1345
+ sessionId: input.sessionId
1346
+ });
1347
+ }
1348
+ const response = await fetch(`${apiUrl}/local-browser/sessions/${input.sessionId}/run`, {
1349
+ method: "POST",
1350
+ headers: {
1351
+ "Content-Type": "application/json",
1352
+ Authorization: `Bearer ${token}`
1353
+ },
1354
+ body: JSON.stringify({
1355
+ instructions: input.instructions,
1356
+ startUrl: input.startUrl ?? null
1357
+ })
1358
+ });
1359
+ if (!response.ok) {
1360
+ const text = await response.text();
1361
+ return toolJson2({ ok: false, error: `Failed to start run: ${text}` });
1362
+ }
1363
+ const result = await response.json();
1364
+ return toolJson2({
1365
+ ok: true,
1366
+ jobId: result.jobId,
1367
+ sessionId: result.sessionId,
1368
+ note: "Test run started. The cloud agent is now controlling your local browser. You can watch the browser to see the test in action."
1369
+ });
1370
+ }
1371
+ return null;
1372
+ }
1373
+
1374
+ // src/mcp/browser-tools.ts
1375
+ var activeSessionId = null;
1376
+ function getBrowserMCPToolDefinitions() {
1377
+ return getBrowserToolDefinitionsWithLifecycle();
1378
+ }
1379
+ async function handleBrowserTool(name, args) {
1380
+ if (name === "browser_start") {
1381
+ try {
1382
+ const result = await createSession({
1383
+ headless: args.headless ?? false,
1384
+ storageStatePath: args.storageStatePath
1385
+ });
1386
+ if (!result.ok) {
1387
+ return toolJson2({ ok: false, error: result.error });
1388
+ }
1389
+ activeSessionId = result.data.id;
1390
+ return toolJson2({
1391
+ ok: true,
1392
+ sessionId: activeSessionId,
1393
+ note: "Browser started. You can now use browser_navigate, browser_click, and other browser tools."
1394
+ });
1395
+ } catch (err) {
1396
+ return toolJson2({ ok: false, error: err instanceof Error ? err.message : String(err) });
1397
+ }
1398
+ }
1399
+ if (name === "browser_stop") {
1400
+ try {
1401
+ if (activeSessionId) {
1402
+ await deleteSession(activeSessionId);
1403
+ activeSessionId = null;
1404
+ } else {
1405
+ await deleteAllSessions();
1406
+ }
1407
+ return toolJson2({ ok: true, note: "Browser stopped and cleaned up." });
1408
+ } catch (err) {
1409
+ return toolJson2({ ok: false, error: err instanceof Error ? err.message : String(err) });
1410
+ }
1411
+ }
1412
+ if (!name.startsWith("browser_")) return null;
1413
+ try {
1414
+ let sessionId = activeSessionId;
1415
+ if (!sessionId) {
1416
+ try {
1417
+ const target = await resolveTargetSession();
1418
+ sessionId = target.id;
1419
+ activeSessionId = sessionId;
1420
+ } catch {
1421
+ return toolJson2({
1422
+ ok: false,
1423
+ error: "No active browser session. Start one with browser_start first."
1424
+ });
1425
+ }
1426
+ }
1427
+ const toolName = name.replace(/^browser_/, "");
1428
+ const result = await callTool(sessionId, toolName, args);
1429
+ if (!result.ok) {
1430
+ if (result.error?.includes("not found")) {
1431
+ activeSessionId = null;
1432
+ }
1433
+ if (result.errorCode === USER_PAUSED_ERROR) {
1434
+ return toolText2(result.error ?? "User has taken control of the browser.");
1435
+ }
1436
+ return toolJson2({ ok: false, error: result.error, tool: name });
1437
+ }
1438
+ const toolResult = result.images?.length ? { text: result.text ?? "", images: result.images } : result.text ?? "";
1439
+ return formatMCPContent(toolResult);
1440
+ } catch (err) {
1441
+ const message = err instanceof Error ? err.message : String(err);
1442
+ return toolJson2({ ok: false, error: message, tool: name });
1443
+ }
1444
+ }
1445
+
1446
+ // src/mcp/login-tool.ts
1447
+ function getLoginToolDefinitions() {
1448
+ return [
1449
+ {
1450
+ name: "canary_login",
1451
+ description: "Authenticate with Canary using the device code OAuth flow. Opens a browser window for the user to approve. May take 30-120 seconds while waiting for the user to complete login. Returns the verification URL and user code in case the browser does not open automatically.",
1452
+ inputSchema: {
1453
+ type: "object",
1454
+ properties: {
1455
+ env: {
1456
+ type: "string",
1457
+ enum: ["prod", "dev", "local"],
1458
+ description: "Target environment (default: prod)"
1459
+ },
1460
+ org: {
1461
+ type: "string",
1462
+ description: "Organization name or ID to select if the user belongs to multiple orgs. If omitted, uses the default org."
1463
+ }
1464
+ }
1465
+ }
1466
+ }
1467
+ ];
1468
+ }
1469
+ async function handleLoginTool(tool, args) {
1470
+ if (tool !== "canary_login") return null;
1471
+ try {
1472
+ const env = args.env;
1473
+ const orgFlag = args.org;
1474
+ const envUrls = env ? ENV_URLS[env] : void 0;
1475
+ if (env && !envUrls) {
1476
+ return toolJson2({ ok: false, error: `Unknown environment: ${env}. Valid: prod, dev, local` });
1477
+ }
1478
+ const apiUrl = envUrls?.api ?? resolveApiUrl();
1479
+ const appUrl = envUrls?.app ?? resolveAppUrl(apiUrl);
1480
+ const startRes = await fetch(`${apiUrl}/cli-login/start`, {
1481
+ method: "POST",
1482
+ headers: { "content-type": "application/json" },
1483
+ body: JSON.stringify({ appUrl })
1484
+ });
1485
+ const startJson = await startRes.json();
1486
+ if (!startRes.ok || !startJson.ok || !startJson.deviceCode || !startJson.userCode) {
1487
+ return toolJson2({
1488
+ ok: false,
1489
+ error: `Login start failed: ${startJson.error ?? startRes.statusText}`
1490
+ });
1491
+ }
1492
+ if (startJson.verificationUrl) {
1493
+ try {
1494
+ openUrl(startJson.verificationUrl);
1495
+ } catch {
1496
+ }
1497
+ }
1498
+ const intervalMs = (startJson.intervalSeconds ?? 3) * 1e3;
1499
+ const expiresAt = startJson.expiresAt ? new Date(startJson.expiresAt).getTime() : null;
1500
+ let token;
1501
+ let initialOrgId;
1502
+ while (true) {
1503
+ if (expiresAt && Date.now() > expiresAt) {
1504
+ return toolJson2({
1505
+ ok: false,
1506
+ error: "Login code expired. Please try again.",
1507
+ userCode: startJson.userCode,
1508
+ verificationUrl: startJson.verificationUrl
1509
+ });
1510
+ }
1511
+ await new Promise((resolve) => setTimeout(resolve, intervalMs));
1512
+ const pollRes = await fetch(`${apiUrl}/cli-login/poll`, {
1513
+ method: "POST",
1514
+ headers: { "content-type": "application/json" },
1515
+ body: JSON.stringify({ deviceCode: startJson.deviceCode })
1516
+ });
1517
+ const pollJson = await pollRes.json();
1518
+ if (!pollRes.ok || !pollJson.ok) {
1519
+ return toolJson2({
1520
+ ok: false,
1521
+ error: `Login poll failed: ${pollJson.error ?? pollRes.statusText}`
1522
+ });
1523
+ }
1524
+ if (pollJson.status === "approved" && pollJson.accessToken) {
1525
+ token = pollJson.accessToken;
1526
+ initialOrgId = pollJson.orgId;
1527
+ break;
1528
+ }
1529
+ if (pollJson.status === "rejected") {
1530
+ return toolJson2({ ok: false, error: "Login rejected by user." });
1531
+ }
1532
+ if (pollJson.status === "expired") {
1533
+ return toolJson2({ ok: false, error: "Login expired. Please try again." });
1534
+ }
1535
+ }
1536
+ const orgsData = await fetchOrgs(apiUrl, token);
1537
+ const orgs = orgsData?.organizations ?? [];
1538
+ let finalToken = token;
1539
+ let finalOrgId = initialOrgId;
1540
+ let finalOrgName;
1541
+ if (orgs.length <= 1) {
1542
+ finalOrgName = orgs[0]?.name;
1543
+ } else if (orgFlag) {
1544
+ const selectedOrg = orgs.find(
1545
+ (o) => o.name.toLowerCase() === orgFlag.toLowerCase() || o.id === orgFlag
1546
+ );
1547
+ if (!selectedOrg) {
1548
+ const profileName2 = env ? envToProfile(env) : void 0;
1549
+ await saveAuth({ token: finalToken, apiUrl, orgId: finalOrgId, orgName: orgs.find((o) => o.id === initialOrgId)?.name }, profileName2);
1550
+ return toolJson2({
1551
+ ok: false,
1552
+ error: `Organization "${orgFlag}" not found.`,
1553
+ loggedIn: true,
1554
+ currentOrg: orgs.find((o) => o.id === initialOrgId)?.name,
1555
+ availableOrgs: orgs.map((o) => ({ id: o.id, name: o.name, role: o.role }))
1556
+ });
1557
+ }
1558
+ if (selectedOrg.id !== initialOrgId) {
1559
+ const switchRes = await switchOrg(apiUrl, token, selectedOrg.id);
1560
+ if (!switchRes.ok || !switchRes.accessToken) {
1561
+ return toolJson2({
1562
+ ok: false,
1563
+ error: `Failed to switch organization: ${switchRes.error ?? "Unknown error"}`
1564
+ });
1565
+ }
1566
+ finalToken = switchRes.accessToken;
1567
+ finalOrgId = switchRes.orgId;
1568
+ finalOrgName = switchRes.orgName;
1569
+ } else {
1570
+ finalOrgName = selectedOrg.name;
1571
+ }
1572
+ } else {
1573
+ const defaultOrg = orgs.find((o) => o.id === initialOrgId);
1574
+ finalOrgName = defaultOrg?.name;
1575
+ }
1576
+ const profileName = env ? envToProfile(env) : void 0;
1577
+ await saveAuth({ token: finalToken, apiUrl, orgId: finalOrgId, orgName: finalOrgName }, profileName);
1578
+ return toolJson2({
1579
+ ok: true,
1580
+ message: "Login successful. Token saved.",
1581
+ orgName: finalOrgName,
1582
+ orgId: finalOrgId,
1583
+ ...orgs.length > 1 ? { availableOrgs: orgs.map((o) => ({ id: o.id, name: o.name, role: o.role })) } : {}
1584
+ });
1585
+ } catch (err) {
1586
+ return toolJson2({ ok: false, error: err instanceof Error ? err.message : String(err) });
1587
+ }
1588
+ }
1589
+
1590
+ // src/mcp/upgrade-tool.ts
1591
+ import fs from "fs";
1592
+ import { spawnSync } from "child_process";
1593
+ import process3 from "process";
1594
+ function detectPackageManager() {
1595
+ try {
1596
+ const binPath = fs.realpathSync(process3.argv[1]).replace(/\\/g, "/");
1597
+ if (binPath.includes(".bun/") || binPath.includes("/bun/")) return "bun";
1598
+ if (binPath.includes(".pnpm/") || binPath.includes("/pnpm/")) return "pnpm";
1599
+ } catch {
1600
+ }
1601
+ return "npm";
1602
+ }
1603
+ var UPGRADE_COMMANDS = {
1604
+ bun: ["bun", "add", "-g", `${packageName}@latest`],
1605
+ pnpm: ["pnpm", "add", "-g", `${packageName}@latest`],
1606
+ npm: ["npm", "install", "-g", `${packageName}@latest`]
1607
+ };
1608
+ function getUpgradeToolDefinitions() {
1609
+ return [
1610
+ {
1611
+ name: "canary_upgrade",
1612
+ description: "Upgrade the Canary CLI to the latest version. Detects the package manager (npm, bun, pnpm) and runs the appropriate global install command.",
1613
+ inputSchema: {
1614
+ type: "object",
1615
+ properties: {}
1616
+ }
1617
+ }
1618
+ ];
1619
+ }
1620
+ async function handleUpgradeTool(tool, _args) {
1621
+ if (tool !== "canary_upgrade") return null;
1622
+ try {
1623
+ const pm = detectPackageManager();
1624
+ const cmd = UPGRADE_COMMANDS[pm];
1625
+ const result = spawnSync(cmd[0], cmd.slice(1), {
1626
+ stdio: ["pipe", "pipe", "pipe"],
1627
+ cwd: process3.cwd(),
1628
+ timeout: 12e4,
1629
+ shell: process3.platform === "win32",
1630
+ windowsHide: true
1631
+ });
1632
+ if (result.error) {
1633
+ return toolJson2({
1634
+ ok: false,
1635
+ error: `Failed to run ${pm}: ${result.error.message}`
1636
+ });
1637
+ }
1638
+ const stdout = result.stdout?.toString().trim() ?? "";
1639
+ const stderr = result.stderr?.toString().trim() ?? "";
1640
+ if (result.status !== 0) {
1641
+ return toolJson2({
1642
+ ok: false,
1643
+ error: `Upgrade failed (exit code ${result.status}).`,
1644
+ output: stderr || stdout
1645
+ });
1646
+ }
1647
+ await clearUpdateCache();
1648
+ return toolJson2({
1649
+ ok: true,
1650
+ message: `Successfully upgraded ${packageName} to the latest version.`,
1651
+ packageManager: pm,
1652
+ output: stdout || void 0
1653
+ });
1654
+ } catch (err) {
1655
+ return toolJson2({ ok: false, error: err instanceof Error ? err.message : String(err) });
1656
+ }
1657
+ }
1658
+
1659
+ // src/mcp/instructions.ts
1660
+ var CANARY_MCP_INSTRUCTIONS = `
1661
+ # Canary \u2014 AI-Powered QA Platform
1662
+
1663
+ You have access to the Canary MCP tools for testing web applications, managing test workflows, and investigating issues. This guide teaches you how to use them effectively.
1664
+
1665
+ ## Authentication & Defaults
1666
+
1667
+ - **Always use env: "prod"** when calling canary_login unless the user explicitly asks for dev or local.
1668
+ - After login, if multiple organizations are available, ask the user which one to use. Pass the org name via the \`org\` parameter.
1669
+ - To switch orgs later, call canary_login again with the \`org\` parameter \u2014 no need to re-authenticate.
1670
+
1671
+ ## Browser Sessions \u2014 Exploring Apps
1672
+
1673
+ Use browser tools to interactively explore and test web applications. The workflow is:
1674
+
1675
+ ### Starting a Session
1676
+
1677
+ 1. **List credentials first:** Call \`canary_credential_list\` to see available credentials with their property names and base URLs.
1678
+ 2. **Pick the right credential:**
1679
+ - Prefer the **default credential** (\`isDefault: true\`)
1680
+ - **Never** use a credential with "local" or "manual" in the name \u2014 these are for local development and won't work in deployed environments
1681
+ - If multiple non-local credentials exist and you're unsure, ask the user
1682
+ 3. **Start the browser with a credential:** Use \`browser_start\` to launch a headed browser, then load the credential's storage state. Or use the session tools (\`local_browser_start\`) for cloud-relay sessions.
1683
+
1684
+ ### The Core Interaction Loop
1685
+
1686
+ **snapshot \u2192 read \u2192 act \u2192 snapshot \u2192 verify**
1687
+
1688
+ This is the fundamental pattern. Never skip the verification snapshot.
1689
+
1690
+ 1. **Snapshot the page** with \`browser_snapshot\` to see what's on screen. The accessibility tree shows elements with refs like \`[ref=e15]\`.
1691
+ 2. **Find elements efficiently** using the \`search\` parameter: \`browser_snapshot\` with \`search: "Submit"\` or \`search: "Email|Password"\` (OR-matched, case-insensitive).
1692
+ 3. **Interact using refs** from the snapshot \u2014 click, type, fill, select. Always use refs, not coordinates. Fall back to coordinate-based clicking only when refs fail (overlays, canvas elements).
1693
+ 4. **Snapshot again** after every action to verify it succeeded. Never assume an action worked.
1694
+ 5. **Check for errors** after form submissions or navigation \u2014 look at the page content for error messages, and use console/network tools if available.
1695
+
1696
+ ### Tips
1697
+
1698
+ - Use \`search\` to locate elements quickly rather than scanning the full accessibility tree.
1699
+ - Use \`browser_snapshot\` with \`mode: "screenshot"\` for visual verification when you need to see the actual rendering.
1700
+ - Wait for async operations (API calls, animations) before snapshotting \u2014 look for loading indicators.
1701
+ - Group related observations: take one snapshot and read the full tree rather than searching for every element individually.
1702
+
1703
+ ## Building Tests \u2014 Workflow Authoring
1704
+
1705
+ After exploring an app via browser session, you can persist what you discovered as a reusable test workflow. The flow is: **explore \u2192 author \u2192 create**.
1706
+
1707
+ ### Step 1: Gather Credential Info
1708
+
1709
+ Call \`canary_credential_list\` to get the credential UUID. You'll need it for the login node.
1710
+ - Always use \`credentialId\` (UUID) \u2014 never just the credential name
1711
+ - Set both \`credentialId\` and \`credentialName\` in the login node
1712
+
1713
+ ### Step 2: Design the Workflow
1714
+
1715
+ A workflow is a directed graph of **nodes** connected by **edges**.
1716
+
1717
+ #### Node Types
1718
+
1719
+ | Type | Purpose | Required configJson |
1720
+ |------|---------|-------------------|
1721
+ | \`login\` | Authenticate with a credential | \`credentialId\`, \`credentialName\` |
1722
+ | \`navigate\` | Go to a URL | \`pageUrl\`, \`pageTitle\` |
1723
+ | \`action\` | Interact with elements | \`customActionTitle\`, \`playbookSteps\`, optionally \`inlineAssertions\` |
1724
+ | \`assertion\` | Standalone page verification | \`condition\` text |
1725
+ | \`end\` | Terminal node | (none) |
1726
+
1727
+ #### Action Node Guidelines
1728
+
1729
+ - Every action node **must** have a \`customActionTitle\` \u2014 a short (3-8 word) human-readable title describing the step (e.g., "Create a new invoice", "Fill registration form")
1730
+ - **Group related actions** into a single action node (one node per logical step, not per click)
1731
+ - **Prefer inline assertions** on action nodes over separate assertion nodes. Add \`inlineAssertions\` to verify outcomes right after the action. **Do not author an \`id\` field** \u2014 the server stamps a UUID on persist:
1732
+ \`\`\`json
1733
+ {
1734
+ "inlineAssertions": [
1735
+ {
1736
+ "condition": "Success message 'Invoice created' is visible",
1737
+ "severity": "fail"
1738
+ }
1739
+ ]
1740
+ }
1741
+ \`\`\`
1742
+ - Use separate \`assertion\` nodes only for standalone page-level verifications (e.g., initial page state before any action)
1743
+
1744
+ #### Playbook Steps (inside action nodes)
1745
+
1746
+ Playbook steps describe what to do within an action node. Each step has:
1747
+ - \`order\`: sequence number (1, 2, 3...)
1748
+ - \`action\`: what to do \u2014 \`click\`, \`fill\`, \`fill_form\`, \`select\`, \`navigate\`, \`wait\`, \`scroll\`, \`hover\`, \`upload\`, \`verify\`, \`press_key\`, \`drag\`, \`resize\`, \`capture\`, \`extract_table\`, \`python_execute\`, \`verify_download\`, \`verify_download_content\`, \`screenshot\`, \`agent_loop\`
1749
+ - \`target\`: **semantic description** from the accessibility tree \u2014 button text, field labels, placeholder text. Describe as a human would: "Submit button", "Email input field", "Status dropdown". **Never use refs, CSS selectors, or XPaths.**
1750
+ - \`params\`: for \`fill\` actions, include \`{ "value": "..." }\`
1751
+
1752
+ Each playbook step maps to at most one recorded browser tool call. If the user intent took multiple clicks, fills, key presses, or assertions, write multiple playbook steps instead of bundling them. Do not use \`other\`; if no concrete browser-tool-backed action fits, omit that diagnostic observation from \`playbookSteps\`. Use \`click\` for switches, checkboxes, and click-to-focus behavior. Use \`press_key\` with \`{ "key": "Escape" }\` or an explicit \`click\` to close popovers and panels; there is no \`dismiss\`, \`toggle\`, \`focus\`, or \`handle_native_dialog\` action.
1753
+
1754
+ ### Macros \u2014 Always Prefer Over Hardcoded Data
1755
+
1756
+ Macros generate fresh values each run, preventing flaky tests from stale or colliding data. **Never hardcode emails, names, phone numbers, or dates when a macro exists.**
1757
+
1758
+ | Macro | Output | Example |
1759
+ |-------|--------|---------|
1760
+ | \`$EMAIL()\` | Unique test email | \`test-xk9mq2@example-test.com\` |
1761
+ | \`$FIRST_NAME()\` | Random first name | \`Jordan\` |
1762
+ | \`$LAST_NAME()\` | Random last name | \`Chen\` |
1763
+ | \`$PHONE()\` | US phone (555 prefix) | \`+1-555-382-7491\` |
1764
+ | \`$UUID()\` | Random UUID v4 | \`a1b2c3d4-...\` |
1765
+ | \`$RANDOM_INT(min, max)\` | Random integer | \`$RANDOM_INT(100,999)\` \u2192 \`427\` |
1766
+ | \`$RANDOM_STRING(length)\` | Alphanumeric string | \`$RANDOM_STRING(8)\` \u2192 \`xK9mQ3bR\` |
1767
+ | \`$TODAY()\` | Current date (YYYY-MM-DD) | \`2026-04-06\` |
1768
+ | \`$DATE(days)\` | Date offset from today | \`$DATE(-7)\` \u2192 last week |
1769
+ | \`$NOW()\` | ISO 8601 timestamp | \`2026-04-06T14:30:00.000Z\` |
1770
+ | \`$TIMESTAMP()\` | Unix milliseconds | \`1771520400000\` |
1771
+ | \`$SEQUENCE("name")\` | Auto-incrementing per run | \`1\`, \`2\`, \`3\`... |
1772
+
1773
+ Macros combine inline: \`"QA Test $FIRST_NAME() $LAST_NAME()"\`, \`"order-$RANDOM_STRING(6)"\`
1774
+
1775
+ **Prefix test data with "QA Test"** for identifiability.
1776
+
1777
+ ### Step 3: Create the Workflow
1778
+
1779
+ Use \`canary_workflow_sync\` for existing workflows or construct the full workflow JSON and create via the API. Then verify with \`canary_workflow_get\`.
1780
+
1781
+ ### Example Workflow JSON
1782
+
1783
+ \`\`\`json
1784
+ {
1785
+ "name": "Verify user can create an invoice",
1786
+ "description": "Tests the invoice creation flow end-to-end",
1787
+ "nodes": [
1788
+ {
1789
+ "nodeType": "login",
1790
+ "configJson": {
1791
+ "credentialId": "4f6db838-16be-49e8-8887-8d0be1c83a87",
1792
+ "credentialName": "QA User"
1793
+ }
1794
+ },
1795
+ {
1796
+ "nodeType": "navigate",
1797
+ "configJson": { "pageUrl": "/invoices", "pageTitle": "Invoices" }
1798
+ },
1799
+ {
1800
+ "nodeType": "action",
1801
+ "configJson": {
1802
+ "customActionTitle": "Create a new invoice",
1803
+ "actionDescription": "Fill out the new invoice form and save",
1804
+ "playbookSteps": [
1805
+ { "order": 1, "action": "click", "target": "New Invoice button" },
1806
+ { "order": 2, "action": "fill", "target": "Client name field", "params": { "value": "QA Test $FIRST_NAME()" } },
1807
+ { "order": 3, "action": "fill", "target": "Amount field", "params": { "value": "$RANDOM_INT(100,999)" } },
1808
+ { "order": 4, "action": "click", "target": "Save button" }
1809
+ ],
1810
+ "inlineAssertions": [
1811
+ {
1812
+ "condition": "Success message 'Invoice created' is visible and the new invoice appears in the list",
1813
+ "severity": "fail"
1814
+ }
1815
+ ]
1816
+ }
1817
+ }
1818
+ ]
1819
+ }
1820
+ \`\`\`
1821
+
1822
+ ## Managing Workflows
1823
+
1824
+ - **canary_workflow_list** \u2014 Browse existing tests. Filter by \`status\` (draft = editable, published = active in regression runs), \`flowType\` (standard/setup/seed/teardown), or \`search\` by name. Reliability is tracked separately by the workflow's \`health\`.
1825
+ - **canary_workflow_get** \u2014 Inspect a test's full definition: nodes, edges, playbook steps, assertions.
1826
+ - **canary_workflow_patch** \u2014 Update name, description, status, or flow type. Only provided fields change.
1827
+ - **canary_workflow_run** \u2014 Trigger execution. Returns a \`runId\` \u2014 use it to track progress.
1828
+ - **canary_workflow_sync** \u2014 Atomically replace all nodes and edges. Automatically snapshots current state first so changes are reversible. Read the current DAG with \`canary_workflow_get\` before syncing.
1829
+
1830
+ ## Monitoring Runs & Debugging Failures
1831
+
1832
+ After triggering a workflow run, monitor and debug with:
1833
+
1834
+ - **canary_workflow_runs_list** \u2014 Recent runs for a workflow with status and duration.
1835
+ - **canary_workflow_runs_get** \u2014 Detailed run view: step-by-step status, duration, errors. Also works for in-progress runs to check status.
1836
+ - **canary_workflow_runs_diagnostics** \u2014 AI-generated failure analysis. Returns root cause and recommendations. Falls back to raw console logs and network requests if no AI analysis is available.
1837
+
1838
+ ### Regression Suite
1839
+
1840
+ - **canary_test_suite_run** \u2014 Trigger a regression suite execution. Returns suiteId for tracking. Use canary_test_suite_list or canary_test_suite_get to check results.
1841
+ - **canary_test_suite_list** \u2014 Recent regression suite executions with pass/fail counts.
1842
+ - **canary_test_suite_get** \u2014 Individual test results within a suite execution.
1843
+ - **canary_test_suite_diagnostics** \u2014 Failure diagnostics for a specific test in a suite run.
1844
+ - **canary_test_suite_steps** \u2014 Step-by-step breakdown of a test run within a suite.
1845
+
1846
+ ## Investigating Issues
1847
+
1848
+ Issues are bugs discovered by Canary during test runs.
1849
+
1850
+ - **canary_issues_list** \u2014 Search and filter issues by severity (\`low\`/\`medium\`/\`high\`), status (\`open\`/\`closed\`/\`not_a_bug\`), or property. Supports full-text search.
1851
+ - **canary_issues_get** \u2014 Full issue detail including console errors and network failures from the latest occurrence. Use this to understand what went wrong.
1852
+
1853
+ ## API Sequences (API-only workflows)
1854
+
1855
+ For APIs that need deterministic, browser-free smoke testing \u2014 composed of ordered HTTP calls that chain outputs into inputs.
1856
+
1857
+ - **Create** with the same command as any workflow, with \`--api-only\`:
1858
+ \`canary workflow create --api-only --name "<n>" --sequence-from-file seq.json\`
1859
+ Returns \`{ workflowId, apiSequenceId, nodeId }\`. No \`--property\` \u2014 api-sequences are org-scoped.
1860
+ - **Inspect runs** with the same command as any runs, with \`--side api\` (or \`--side all\` for both):
1861
+ \`canary workflow runs list <wf> --side api\`
1862
+ \`canary workflow runs get <wf> <runId>\`
1863
+ - **Edit the sequence itself** via \`canary workflow api-sequence <verb>\`:
1864
+ - \`get <wf>\` \u2014 dump config + steps
1865
+ - \`update <wf> --from-file seq.json\` \u2014 replace config
1866
+ - \`execute <wf>\` \u2014 run it
1867
+ - \`generate <wf> [--prompt "..."]\` \u2014 AI-draft a config, then refine with \`update\`
1868
+ - **Discover endpoints** before composing steps:
1869
+ \`canary api-catalog sources list\`
1870
+ \`canary api-catalog endpoints list --search <q>\`
1871
+ \`canary api-catalog endpoints create --from-file ep.json\` (add a one-off)
1872
+ - **Read the reference first** for the config/step shape and template syntax:
1873
+ \`canary workflow guide api-sequences\`
1874
+
1875
+ Templates in step fields use \`{{stepId.response.path}}\` to reference prior-step outputs and \`$MACRO()\` for dynamic test data (same macros as browser workflows).
1876
+
1877
+ ## Common Workflows
1878
+
1879
+ ### "Let's explore the app"
1880
+ 1. \`canary_login\` (env: prod) \u2192 switch to the right org
1881
+ 2. \`canary_credential_list\` \u2192 find the right credential
1882
+ 3. \`browser_start\` \u2192 launch browser
1883
+ 4. Load credential and navigate to the app
1884
+ 5. Snapshot, interact, explore
1885
+
1886
+ ### "Write a test for this feature"
1887
+ 1. Explore the feature via browser (above workflow)
1888
+ 2. \`canary_credential_list\` \u2192 get credentialId
1889
+ 3. Construct workflow JSON with nodes, edges, playbook steps, macros
1890
+ 4. Create or sync the workflow
1891
+ 5. \`canary_workflow_run\` \u2192 execute it
1892
+ 6. \`canary_workflow_runs_get\` \u2192 verify it passes
1893
+
1894
+ ### "Why did this test fail?"
1895
+ 1. \`canary_workflow_runs_list\` \u2192 find the failed run
1896
+ 2. \`canary_workflow_runs_get\` \u2192 see which step failed
1897
+ 3. \`canary_workflow_runs_diagnostics\` \u2192 AI analysis of the failure
1898
+ 4. If needed, open a browser session to reproduce and investigate
1899
+
1900
+ ### "What issues have been found?"
1901
+ 1. \`canary_issues_list\` \u2192 browse open issues
1902
+ 2. \`canary_issues_get\` \u2192 deep dive with console/network errors
1903
+ `.trim();
1904
+
1905
+ // src/mcp/index.ts
1906
+ async function resolveCtx() {
1907
+ const apiToken = process4.env.CANARY_API_TOKEN ?? await readStoredToken();
1908
+ if (!apiToken) {
1909
+ throw new Error("Missing token. Call the canary_login tool or set CANARY_API_TOKEN.");
1910
+ }
1911
+ return { apiUrl: resolveApiUrl(), apiToken };
1912
+ }
1913
+ async function runMcp(_argv) {
1914
+ const registry = buildRegistry(sharedGroups);
1915
+ const server = new Server(
1916
+ { name: "canary-cli", version: "0.1.0" },
1917
+ {
1918
+ capabilities: { tools: {} },
1919
+ instructions: CANARY_MCP_INSTRUCTIONS
1920
+ }
1921
+ );
1922
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
1923
+ tools: [
1924
+ ...getLoginToolDefinitions(),
1925
+ ...getUpgradeToolDefinitions(),
1926
+ ...getSessionToolDefinitions(),
1927
+ ...getBrowserMCPToolDefinitions(),
1928
+ ...registry.list()
1929
+ ]
1930
+ }));
1931
+ server.setRequestHandler(CallToolRequestSchema, async (req) => {
1932
+ const tool = req.params.name;
1933
+ const args = req.params.arguments ?? {};
1934
+ const loginResult = await handleLoginTool(tool, args);
1935
+ if (loginResult) return loginResult;
1936
+ const upgradeResult = await handleUpgradeTool(tool, args);
1937
+ if (upgradeResult) return upgradeResult;
1938
+ const browserResult = await handleBrowserTool(tool, args);
1939
+ if (browserResult) return browserResult;
1940
+ const sessionResult = await handleSessionTool(tool, args);
1941
+ if (sessionResult) return sessionResult;
1942
+ try {
1943
+ const ctx = await resolveCtx();
1944
+ return await registry.call(tool, args, ctx);
1945
+ } catch (err) {
1946
+ return toolText2(err instanceof Error ? err.message : String(err));
1947
+ }
1948
+ });
1949
+ const transport = new StdioServerTransport();
1950
+ await server.connect(transport);
1951
+ return new Promise(() => void 0);
1952
+ }
1953
+ export {
1954
+ runMcp
1955
+ };
1956
+ //# sourceMappingURL=mcp-54IJEOUA.js.map