@kadoa/mcp 0.7.0-rc.1 → 0.7.0-rc.2

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 (3) hide show
  1. package/README.md +2 -2
  2. package/dist/index.js +1005 -431
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -17,7 +17,7 @@ var package_default;
17
17
  var init_package = __esm(() => {
18
18
  package_default = {
19
19
  name: "@kadoa/mcp",
20
- version: "0.7.0-rc.1",
20
+ version: "0.7.0-rc.2",
21
21
  description: "Kadoa MCP Server — manage workflows from Claude Desktop, Cursor, and other MCP clients",
22
22
  type: "module",
23
23
  main: "dist/index.js",
@@ -207,19 +207,252 @@ function withVariableDashboardUrl(variable) {
207
207
  return { ...variable, dashboardUrl: variableSettingsDashboardUrl() };
208
208
  }
209
209
 
210
+ // src/tools/shared/output-schemas.ts
211
+ import { z } from "zod";
212
+ var apiField, nullableString, PaginationOutput, WorkflowRowOutput, ListWorkflowsOutput, GetWorkflowOutput, FetchDataOutput, ASSISTANT_ACTIVITIES, WorkflowAssistantOutput, AssistantTimelineOutput, AssistantAnswerOutput, AssistantControlOutput, WorkflowStrategyOutput;
213
+ var init_output_schemas = __esm(() => {
214
+ apiField = z.unknown().optional();
215
+ nullableString = z.string().nullable();
216
+ PaginationOutput = z.object({
217
+ totalCount: z.number().optional(),
218
+ hasMore: z.boolean(),
219
+ limit: z.number().optional(),
220
+ skip: z.number().optional(),
221
+ offset: z.number().optional(),
222
+ page: z.number().optional(),
223
+ totalPages: z.number().optional(),
224
+ nextCursor: nullableString.optional(),
225
+ next: z.record(z.string(), z.unknown()).optional()
226
+ }).loose();
227
+ WorkflowRowOutput = z.object({
228
+ id: apiField,
229
+ name: apiField,
230
+ dashboardUrl: z.string(),
231
+ status: z.string(),
232
+ state: nullableString,
233
+ displayState: nullableString,
234
+ runState: nullableString,
235
+ urls: apiField,
236
+ totalRecords: apiField,
237
+ startedAt: apiField,
238
+ finishedAt: apiField,
239
+ createdAt: apiField,
240
+ assistant: z.object({
241
+ eligible: z.boolean(),
242
+ dataStale: z.boolean(),
243
+ session: z.object({ id: z.string(), threadId: apiField, status: apiField }).loose().nullable()
244
+ }).loose(),
245
+ template: z.object({ id: z.string(), pinnedVersion: apiField, latestVersion: apiField, isOutdated: z.boolean() }).loose().nullable(),
246
+ realtime: z.object({ enabled: z.boolean(), lastDataChangedAt: apiField, health: apiField }).loose(),
247
+ awaitingUserInput: apiField
248
+ }).loose();
249
+ ListWorkflowsOutput = z.object({ workflows: z.array(WorkflowRowOutput), pagination: PaginationOutput }).loose();
250
+ GetWorkflowOutput = z.object({
251
+ id: z.string(),
252
+ name: apiField,
253
+ dashboardUrl: z.string(),
254
+ description: apiField,
255
+ status: z.string(),
256
+ state: nullableString,
257
+ displayState: nullableString,
258
+ runState: nullableString,
259
+ urls: apiField,
260
+ prompt: nullableString,
261
+ intentSource: z.enum(["extraction_spec", "legacy_prompt"]).nullable(),
262
+ extractionSpec: apiField,
263
+ entity: apiField,
264
+ schema: apiField,
265
+ navigationMode: apiField,
266
+ dataStale: apiField,
267
+ assistantEligible: z.boolean(),
268
+ assistantSessionId: apiField,
269
+ assistantThreadId: apiField,
270
+ assistantSessionStatus: apiField,
271
+ assistant: z.object({ eligible: z.boolean(), sessionId: apiField, threadId: apiField, sessionStatus: apiField }).loose(),
272
+ template: z.object({
273
+ id: apiField,
274
+ name: apiField,
275
+ version: apiField,
276
+ controlledParts: z.array(z.string()),
277
+ hasSchemaValidationRules: apiField
278
+ }).loose().nullable(),
279
+ updateInterval: apiField,
280
+ schedules: apiField,
281
+ scheduleTimezone: apiField,
282
+ nextInvocation: apiField,
283
+ location: apiField,
284
+ limit: apiField,
285
+ tags: apiField,
286
+ totalRecords: apiField,
287
+ job: z.object({ id: apiField }).loose().nullable(),
288
+ startedAt: apiField,
289
+ finishedAt: apiField,
290
+ estimatedFinishedAtUtc: apiField,
291
+ errors: apiField,
292
+ createdAt: apiField,
293
+ notificationConfig: apiField,
294
+ channelCounts: apiField,
295
+ dataValidation: apiField,
296
+ validationStatistics: apiField,
297
+ realtime: z.object({ lastDataChangedAt: apiField, observerHealth: apiField }).loose().nullable()
298
+ }).loose();
299
+ FetchDataOutput = z.object({
300
+ data: z.array(z.record(z.string(), z.unknown())),
301
+ pagination: PaginationOutput,
302
+ totalCount: z.number().optional(),
303
+ hint: z.string().optional()
304
+ }).loose();
305
+ ASSISTANT_ACTIVITIES = [
306
+ "unsupported",
307
+ "not_started",
308
+ "working",
309
+ "waiting_for_input",
310
+ "idle",
311
+ "interrupted",
312
+ "failed",
313
+ "closed",
314
+ "unknown"
315
+ ];
316
+ WorkflowAssistantOutput = z.object({
317
+ workflowId: z.string(),
318
+ assistantEligible: z.boolean(),
319
+ sessionId: nullableString,
320
+ threadId: apiField,
321
+ sessionStatus: apiField,
322
+ activity: z.enum(ASSISTANT_ACTIVITIES),
323
+ canAcceptUpdate: z.boolean(),
324
+ isTerminal: z.boolean(),
325
+ paused: apiField,
326
+ pause: apiField,
327
+ pendingQuestion: apiField
328
+ }).loose();
329
+ AssistantTimelineOutput = z.object({
330
+ workflowId: z.string(),
331
+ sessionId: apiField,
332
+ items: z.array(z.unknown()),
333
+ pagination: PaginationOutput,
334
+ dashboardUrl: z.string()
335
+ }).loose();
336
+ AssistantAnswerOutput = z.object({
337
+ success: z.literal(true),
338
+ workflowId: z.string(),
339
+ sessionId: z.string(),
340
+ threadId: apiField,
341
+ questionId: z.string(),
342
+ message: z.string()
343
+ }).loose();
344
+ AssistantControlOutput = z.object({
345
+ success: z.boolean(),
346
+ status: z.literal("accepted"),
347
+ action: z.enum(["interrupt", "resume", "stop"]),
348
+ workflowId: z.string(),
349
+ sessionId: z.string(),
350
+ delivery: apiField,
351
+ jobId: apiField,
352
+ message: z.string()
353
+ }).loose();
354
+ WorkflowStrategyOutput = z.object({
355
+ workflowId: z.string(),
356
+ sessionId: nullableString,
357
+ source: z.enum(["assistant_build", "workflow_metadata"]).nullable(),
358
+ status: z.enum(["ready", "building", "unavailable"]),
359
+ strategy: apiField,
360
+ message: z.string().optional()
361
+ }).loose();
362
+ });
363
+
364
+ // src/tools/shared/pagination.ts
365
+ import { z as z2 } from "zod";
366
+ function limitInput(options) {
367
+ const range = `limit must be a whole number from 1 to ${options.max}`;
368
+ const overMax = `${range}. ${options.overMaxHint ?? "Page with pagination.next."}`;
369
+ return z2.preprocess(coerceNumber(), z2.number().int(range).min(1, range).max(options.max, overMax)).optional().describe(`${options.noun} per page, 1-${options.max} (default ${options.defaultLimit})`);
370
+ }
371
+ function offsetInput(param, noun) {
372
+ const range = `${param} must be a whole number from 0 to ${MAX_OFFSET}`;
373
+ return z2.preprocess(coerceNumber(), z2.number().int(range).min(0, range).max(MAX_OFFSET, range)).optional().describe(`Number of ${noun} to skip (default 0)`);
374
+ }
375
+ function pageInput() {
376
+ const range = `page must be a whole number from 1 to ${MAX_PAGE}`;
377
+ return z2.preprocess(coerceNumber(), z2.number().int(range).min(1, range).max(MAX_PAGE, range)).optional().describe("Page number, 1-based (default 1)");
378
+ }
379
+ function nextArgs(args, patch) {
380
+ const next = {};
381
+ for (const [key, value] of Object.entries({ ...args, ...patch })) {
382
+ if (value !== undefined)
383
+ next[key] = value;
384
+ }
385
+ return next;
386
+ }
387
+ function withTotal(totalCount, hasMore) {
388
+ return totalCount !== undefined ? { totalCount, hasMore } : { hasMore };
389
+ }
390
+ function offsetPage(options) {
391
+ const nextOffset = options.offset + options.returned;
392
+ const more = options.hasMore ?? (options.totalCount !== undefined ? nextOffset < options.totalCount : options.returned >= options.limit);
393
+ const hasMore = options.returned > 0 && more;
394
+ const meta = withTotal(options.totalCount, hasMore);
395
+ meta.limit = options.limit;
396
+ meta[options.param] = options.offset;
397
+ if (hasMore)
398
+ meta.next = nextArgs(options.args, { [options.param]: nextOffset, limit: options.limit });
399
+ return meta;
400
+ }
401
+ function numberedPage(options) {
402
+ const more = options.totalPages !== undefined ? options.page < options.totalPages : options.totalCount !== undefined ? options.page * options.limit < options.totalCount : options.returned >= options.limit;
403
+ const hasMore = options.returned > 0 && more;
404
+ const meta = withTotal(options.totalCount, hasMore);
405
+ meta.limit = options.limit;
406
+ meta.page = options.page;
407
+ if (options.totalPages !== undefined)
408
+ meta.totalPages = options.totalPages;
409
+ if (hasMore)
410
+ meta.next = nextArgs(options.args, { page: options.page + 1, limit: options.limit });
411
+ return meta;
412
+ }
413
+ function cursorPage(options) {
414
+ const cursor = typeof options.nextCursor === "string" && options.nextCursor.length > 0 ? options.nextCursor : null;
415
+ const meta = { hasMore: options.hasMore && cursor !== null, nextCursor: cursor };
416
+ if (meta.hasMore)
417
+ meta.next = nextArgs(options.args, { cursor });
418
+ return meta;
419
+ }
420
+ function completeList(totalCount) {
421
+ return { totalCount, hasMore: false };
422
+ }
423
+ var MAX_OFFSET = 1e6, MAX_PAGE = 1e5;
424
+ var init_pagination = () => {};
425
+
210
426
  // src/tools/shared/results.ts
427
+ function asStructuredObject(data) {
428
+ return data !== null && !Array.isArray(data) ? data : { result: data };
429
+ }
430
+ function structuredResult(data, extra = []) {
431
+ const structuredContent = asStructuredObject(data);
432
+ return {
433
+ structuredContent,
434
+ content: [{ type: "text", text: JSON.stringify(structuredContent) }, ...extra]
435
+ };
436
+ }
211
437
  function jsonResult(data) {
212
- return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
438
+ return structuredResult(data);
439
+ }
440
+ function textOnlyResult(data, extra = []) {
441
+ const structuredContent = asStructuredObject(data);
442
+ return { content: [{ type: "text", text: JSON.stringify(structuredContent) }, ...extra] };
213
443
  }
214
- function errorResult(message) {
444
+ function errorResult(message, details = {}) {
215
445
  return {
216
- content: [{ type: "text", text: JSON.stringify({ error: message }, null, 2) }],
446
+ content: [{ type: "text", text: JSON.stringify({ error: message, ...details }) }],
217
447
  isError: true
218
448
  };
219
449
  }
450
+ function withExtraContent(result, ...blocks) {
451
+ return { ...result, content: [...result.content, ...blocks] };
452
+ }
220
453
 
221
454
  // src/tools/shared/schemas.ts
222
- import { z } from "zod";
455
+ import { z as z3 } from "zod";
223
456
  function isPlainObject(value) {
224
457
  return value !== null && typeof value === "object" && !Array.isArray(value);
225
458
  }
@@ -281,7 +514,7 @@ function stampSchemaValidationRules(rules, attribution, previous) {
281
514
  return Object.fromEntries(Object.entries(rules).map(([name, field]) => [name, stampField(field, previousFields[name])]));
282
515
  }
283
516
  function strictSchema(shape) {
284
- return z.object(shape).strict();
517
+ return z3.object(shape).strict();
285
518
  }
286
519
  function withoutDefIds(schema) {
287
520
  const jsonSchema = schema["~standard"].jsonSchema;
@@ -317,7 +550,7 @@ function normalizeDataFilters(val, ctx) {
317
550
  code: "custom",
318
551
  message: "Grouped filters ({logicalOperator, conditions}) are not supported. Send a flat array of " + "{field, operator, value} filters instead; multiple filters are combined with OR."
319
552
  });
320
- return z.NEVER;
553
+ return z3.NEVER;
321
554
  }
322
555
  return [v];
323
556
  }
@@ -325,7 +558,7 @@ function normalizeDataFilters(val, ctx) {
325
558
  }
326
559
  var UpdateIntervalSchema, NOTIFICATION_EVENT_TYPES, NotificationEventTypeSchema, SchemaFieldDataTypeSchema, SchemaFieldShape, SchemaValidationPresenceRule, SchemaValidationUniquenessRule, SchemaValidationBoundRule, SchemaValidationStringFormatRule, SchemaValidationFieldRulesSchema, SchemaValidationRulesSchema, SCHEMA_VALIDATION_ATTRIBUTION_KEYS, LocationSchema, MonitoringValueOperators, MonitoringValuelessOperators, MonitoringConditionOperatorSchema, MonitoringConditionSchema, MonitoringSchema, urlInputShape, extractionPromptInput, extractionInputShape, webhookAuthShape, notificationChannelsSchema, notificationsInputShape, requiredNotificationsInputShape, DATA_FILTER_OPERATORS, DATA_FILTER_VALUELESS_OPERATORS, DATA_FILTER_LIST_OPERATORS, DataFilterSchema, dataFiltersInput;
327
560
  var init_schemas = __esm(() => {
328
- UpdateIntervalSchema = z.enum([
561
+ UpdateIntervalSchema = z3.enum([
329
562
  "ONLY_ONCE",
330
563
  "EVERY_10_MINUTES",
331
564
  "HALF_HOURLY",
@@ -359,8 +592,8 @@ var init_schemas = __esm(() => {
359
592
  "credits_low",
360
593
  "free_trial_ending"
361
594
  ];
362
- NotificationEventTypeSchema = z.enum(NOTIFICATION_EVENT_TYPES);
363
- SchemaFieldDataTypeSchema = z.enum([
595
+ NotificationEventTypeSchema = z3.enum(NOTIFICATION_EVENT_TYPES);
596
+ SchemaFieldDataTypeSchema = z3.enum([
364
597
  "STRING",
365
598
  "NUMBER",
366
599
  "BOOLEAN",
@@ -373,61 +606,61 @@ var init_schemas = __esm(() => {
373
606
  "ARRAY"
374
607
  ]);
375
608
  SchemaFieldShape = {
376
- name: z.string().describe("Field name"),
377
- description: z.string().optional().describe("What this field contains"),
378
- example: z.string().describe("Example value"),
609
+ name: z3.string().describe("Field name"),
610
+ description: z3.string().optional().describe("What this field contains"),
611
+ example: z3.string().describe("Example value"),
379
612
  dataType: SchemaFieldDataTypeSchema.optional().describe("Data type for the field"),
380
- isKey: z.preprocess(coerceBoolean(), z.boolean()).optional().describe("Marks this field as a key field - the stable identity used to match records across runs. Change detection diffs records by their key fields, so a monitored or real-time workflow without one cannot tell an updated record from a new one. Set it on whatever uniquely identifies a row (a detail URL, an ID, a ticker).")
613
+ isKey: z3.preprocess(coerceBoolean(), z3.boolean()).optional().describe("Marks this field as a key field - the stable identity used to match records across runs. Change detection diffs records by their key fields, so a monitored or real-time workflow without one cannot tell an updated record from a new one. Set it on whatever uniquely identifies a row (a detail URL, an ID, a ticker).")
381
614
  };
382
- SchemaValidationPresenceRule = z.object({
383
- target: z.number().int().min(0).max(100).step(20).describe("Expected percentage of rows with a value")
615
+ SchemaValidationPresenceRule = z3.object({
616
+ target: z3.number().int().min(0).max(100).step(20).describe("Expected percentage of rows with a value")
384
617
  }).strict().meta({ id: "PresenceRule" });
385
- SchemaValidationUniquenessRule = z.object({
386
- target: z.number().int().min(0).max(100).step(20).describe("Expected percentage of rows with a unique value")
618
+ SchemaValidationUniquenessRule = z3.object({
619
+ target: z3.number().int().min(0).max(100).step(20).describe("Expected percentage of rows with a unique value")
387
620
  }).strict().meta({ id: "UniquenessRule" });
388
- SchemaValidationBoundRule = z.object({
389
- value: z.number().int().min(0).describe("Length or count bound")
621
+ SchemaValidationBoundRule = z3.object({
622
+ value: z3.number().int().min(0).describe("Length or count bound")
390
623
  }).strict().meta({ id: "BoundRule" });
391
- SchemaValidationStringFormatRule = z.discriminatedUnion("kind", [
392
- z.object({
393
- kind: z.literal("FREE_TEXT"),
394
- charset: z.discriminatedUnion("kind", [
395
- z.object({
396
- kind: z.literal("PRESET"),
397
- preset: z.enum(["natural_language", "alphanumeric", "alpha"])
624
+ SchemaValidationStringFormatRule = z3.discriminatedUnion("kind", [
625
+ z3.object({
626
+ kind: z3.literal("FREE_TEXT"),
627
+ charset: z3.discriminatedUnion("kind", [
628
+ z3.object({
629
+ kind: z3.literal("PRESET"),
630
+ preset: z3.enum(["natural_language", "alphanumeric", "alpha"])
398
631
  }).strict()
399
632
  ])
400
633
  }).strict(),
401
- z.object({
402
- kind: z.literal("FORMAT"),
403
- source: z.discriminatedUnion("kind", [
404
- z.object({
405
- kind: z.literal("PRESET"),
406
- preset: z.enum(["url", "email", "phone", "date", "datetime", "time", "uuid", "slug"])
634
+ z3.object({
635
+ kind: z3.literal("FORMAT"),
636
+ source: z3.discriminatedUnion("kind", [
637
+ z3.object({
638
+ kind: z3.literal("PRESET"),
639
+ preset: z3.enum(["url", "email", "phone", "date", "datetime", "time", "uuid", "slug"])
407
640
  }).strict(),
408
- z.object({
409
- kind: z.literal("CUSTOM"),
410
- pattern: z.string().min(1).describe("Regular expression pattern")
641
+ z3.object({
642
+ kind: z3.literal("CUSTOM"),
643
+ pattern: z3.string().min(1).describe("Regular expression pattern")
411
644
  }).strict()
412
645
  ])
413
646
  }).strict(),
414
- z.object({
415
- kind: z.literal("LIST"),
416
- source: z.discriminatedUnion("kind", [
417
- z.object({
418
- kind: z.literal("PRESET"),
419
- preset: z.enum(["language2", "country2", "country3", "currency3", "month3", "usState2"])
647
+ z3.object({
648
+ kind: z3.literal("LIST"),
649
+ source: z3.discriminatedUnion("kind", [
650
+ z3.object({
651
+ kind: z3.literal("PRESET"),
652
+ preset: z3.enum(["language2", "country2", "country3", "currency3", "month3", "usState2"])
420
653
  }).strict(),
421
- z.object({
422
- kind: z.literal("CUSTOM"),
423
- values: z.array(z.string().min(1)).min(1).describe("Allowed string values")
654
+ z3.object({
655
+ kind: z3.literal("CUSTOM"),
656
+ values: z3.array(z3.string().min(1)).min(1).describe("Allowed string values")
424
657
  }).strict()
425
658
  ])
426
659
  }).strict()
427
660
  ]);
428
- SchemaValidationFieldRulesSchema = z.lazy(() => z.discriminatedUnion("kind", [
429
- z.object({
430
- kind: z.literal("STRING"),
661
+ SchemaValidationFieldRulesSchema = z3.lazy(() => z3.discriminatedUnion("kind", [
662
+ z3.object({
663
+ kind: z3.literal("STRING"),
431
664
  presence: SchemaValidationPresenceRule.optional(),
432
665
  uniqueness: SchemaValidationUniquenessRule.optional(),
433
666
  minLength: SchemaValidationBoundRule.optional(),
@@ -436,27 +669,27 @@ var init_schemas = __esm(() => {
436
669
  maxHtmlElements: SchemaValidationBoundRule.optional(),
437
670
  format: SchemaValidationStringFormatRule.optional()
438
671
  }).strict(),
439
- z.object({
440
- kind: z.literal("NUMBER"),
672
+ z3.object({
673
+ kind: z3.literal("NUMBER"),
441
674
  presence: SchemaValidationPresenceRule.optional(),
442
675
  uniqueness: SchemaValidationUniquenessRule.optional(),
443
- minimum: z.object({ value: z.number().describe("Minimum numeric value") }).strict().optional(),
444
- maximum: z.object({ value: z.number().describe("Maximum numeric value") }).strict().optional(),
445
- maxDecimalPlaces: z.object({ value: z.number().int().min(0).max(16) }).strict().optional()
676
+ minimum: z3.object({ value: z3.number().describe("Minimum numeric value") }).strict().optional(),
677
+ maximum: z3.object({ value: z3.number().describe("Maximum numeric value") }).strict().optional(),
678
+ maxDecimalPlaces: z3.object({ value: z3.number().int().min(0).max(16) }).strict().optional()
446
679
  }).strict(),
447
- z.object({
448
- kind: z.literal("OTHER"),
680
+ z3.object({
681
+ kind: z3.literal("OTHER"),
449
682
  presence: SchemaValidationPresenceRule.optional(),
450
683
  uniqueness: SchemaValidationUniquenessRule.optional()
451
684
  }).strict(),
452
- z.object({
453
- kind: z.literal("OBJECT"),
685
+ z3.object({
686
+ kind: z3.literal("OBJECT"),
454
687
  presence: SchemaValidationPresenceRule.optional(),
455
688
  uniqueness: SchemaValidationUniquenessRule.optional(),
456
- properties: z.record(z.string(), SchemaValidationFieldRulesSchema)
689
+ properties: z3.record(z3.string(), SchemaValidationFieldRulesSchema)
457
690
  }).strict(),
458
- z.object({
459
- kind: z.literal("ARRAY"),
691
+ z3.object({
692
+ kind: z3.literal("ARRAY"),
460
693
  presence: SchemaValidationPresenceRule.optional(),
461
694
  uniqueness: SchemaValidationUniquenessRule.optional(),
462
695
  minItems: SchemaValidationBoundRule.optional(),
@@ -464,21 +697,21 @@ var init_schemas = __esm(() => {
464
697
  items: SchemaValidationFieldRulesSchema.optional()
465
698
  }).strict()
466
699
  ]));
467
- SchemaValidationRulesSchema = z.record(z.string(), SchemaValidationFieldRulesSchema);
700
+ SchemaValidationRulesSchema = z3.record(z3.string(), SchemaValidationFieldRulesSchema);
468
701
  SCHEMA_VALIDATION_ATTRIBUTION_KEYS = new Set(["editedBy", "editedByLabel", "editedAt"]);
469
- LocationSchema = z.object({
470
- type: z.enum(["auto", "manual"]),
471
- isoCode: z.string().trim().min(2).optional()
702
+ LocationSchema = z3.object({
703
+ type: z3.enum(["auto", "manual"]),
704
+ isoCode: z3.string().trim().min(2).optional()
472
705
  }).strict().superRefine((location, ctx) => {
473
706
  if (location.type === "manual" && !location.isoCode) {
474
707
  ctx.addIssue({
475
- code: z.ZodIssueCode.custom,
708
+ code: z3.ZodIssueCode.custom,
476
709
  path: ["isoCode"],
477
710
  message: "isoCode is required for manual location"
478
711
  });
479
712
  }
480
713
  if (location.type === "auto" && location.isoCode) {
481
- ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["isoCode"], message: "omit isoCode for auto location" });
714
+ ctx.addIssue({ code: z3.ZodIssueCode.custom, path: ["isoCode"], message: "omit isoCode for auto location" });
482
715
  }
483
716
  });
484
717
  MonitoringValueOperators = [
@@ -500,81 +733,81 @@ var init_schemas = __esm(() => {
500
733
  "WITHIN_LAST_DAYS"
501
734
  ];
502
735
  MonitoringValuelessOperators = ["IS_NULL", "IS_NOT_NULL", "IS_EMPTY", "IS_NOT_EMPTY"];
503
- MonitoringConditionOperatorSchema = z.enum([...MonitoringValueOperators, ...MonitoringValuelessOperators]);
504
- MonitoringConditionSchema = z.object({
505
- type: z.enum(["SINGLE", "ANY"]),
506
- field: z.string().trim().min(1).optional(),
736
+ MonitoringConditionOperatorSchema = z3.enum([...MonitoringValueOperators, ...MonitoringValuelessOperators]);
737
+ MonitoringConditionSchema = z3.object({
738
+ type: z3.enum(["SINGLE", "ANY"]),
739
+ field: z3.string().trim().min(1).optional(),
507
740
  operator: MonitoringConditionOperatorSchema,
508
- value: z.union([z.string(), z.number(), z.boolean()]).optional()
741
+ value: z3.union([z3.string(), z3.number(), z3.boolean()]).optional()
509
742
  }).strict().superRefine((condition, ctx) => {
510
743
  if (condition.type === "SINGLE" && !condition.field) {
511
744
  ctx.addIssue({
512
- code: z.ZodIssueCode.custom,
745
+ code: z3.ZodIssueCode.custom,
513
746
  path: ["field"],
514
747
  message: "field is required for SINGLE conditions"
515
748
  });
516
749
  }
517
750
  const needsValue = MonitoringValueOperators.includes(condition.operator);
518
751
  if (needsValue && condition.value === undefined) {
519
- ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["value"], message: `${condition.operator} requires a value` });
752
+ ctx.addIssue({ code: z3.ZodIssueCode.custom, path: ["value"], message: `${condition.operator} requires a value` });
520
753
  }
521
754
  if (!needsValue && condition.value !== undefined) {
522
755
  ctx.addIssue({
523
- code: z.ZodIssueCode.custom,
756
+ code: z3.ZodIssueCode.custom,
524
757
  path: ["value"],
525
758
  message: `${condition.operator} does not accept a value`
526
759
  });
527
760
  }
528
761
  });
529
- MonitoringSchema = z.object({
530
- fields: z.preprocess(coerceArray(), z.array(z.object({
531
- fieldName: z.string().trim().min(1),
532
- operator: z.enum(["changed", "added", "removed"]),
533
- isKeyField: z.preprocess(coerceBoolean(), z.boolean()).optional()
762
+ MonitoringSchema = z3.object({
763
+ fields: z3.preprocess(coerceArray(), z3.array(z3.object({
764
+ fieldName: z3.string().trim().min(1),
765
+ operator: z3.enum(["changed", "added", "removed"]),
766
+ isKeyField: z3.preprocess(coerceBoolean(), z3.boolean()).optional()
534
767
  }).strict()).min(1)),
535
- conditions: z.object({
536
- logicalOperator: z.enum(["AND", "OR"]),
537
- conditions: z.preprocess(coerceArray(), z.array(MonitoringConditionSchema).min(1))
768
+ conditions: z3.object({
769
+ logicalOperator: z3.enum(["AND", "OR"]),
770
+ conditions: z3.preprocess(coerceArray(), z3.array(MonitoringConditionSchema).min(1))
538
771
  }).strict().optional()
539
772
  }).strict();
540
773
  urlInputShape = {
541
- url: z.string().optional().describe("Single URL - prefer using 'urls' instead. If both are provided, 'urls' takes precedence."),
542
- urls: z.preprocess(coerceArray(true), z.array(z.string()).min(1)).optional().describe("Starting URLs for the workflow (array of strings). Also accepts a single URL string.")
774
+ url: z3.string().optional().describe("Single URL - prefer using 'urls' instead. If both are provided, 'urls' takes precedence."),
775
+ urls: z3.preprocess(coerceArray(true), z3.array(z3.string()).min(1)).optional().describe("Starting URLs for the workflow (array of strings). Also accepts a single URL string.")
543
776
  };
544
- extractionPromptInput = z.string().optional();
777
+ extractionPromptInput = z3.string().optional();
545
778
  extractionInputShape = {
546
- name: z.string().optional().describe("Optional name for the workflow"),
547
- entity: z.string().optional().describe("Entity name for extraction (e.g., 'Product', 'Job Posting')"),
548
- schema: z.preprocess(coerceArray(), z.array(z.object(SchemaFieldShape).strict())).optional().describe("Extraction schema fields. If omitted, the AI agent auto-detects the schema. When you do supply fields, mark the one that identifies a record with isKey - change detection needs it.")
779
+ name: z3.string().optional().describe("Optional name for the workflow"),
780
+ entity: z3.string().optional().describe("Entity name for extraction (e.g., 'Product', 'Job Posting')"),
781
+ schema: z3.preprocess(coerceArray(), z3.array(z3.object(SchemaFieldShape).strict())).optional().describe("Extraction schema fields. If omitted, the AI agent auto-detects the schema. When you do supply fields, mark the one that identifies a record with isKey - change detection needs it.")
549
782
  };
550
- webhookAuthShape = z.object({
551
- type: z.enum(["bearer", "basic", "header"]).describe("Authentication type"),
552
- token: z.string().optional().describe("Bearer token (required for 'bearer' type)"),
553
- username: z.string().optional().describe("Username (required for 'basic' type)"),
554
- password: z.string().optional().describe("Password (required for 'basic' type)"),
555
- headers: z.object({}).passthrough().optional().describe("Custom headers as key-value pairs (required for 'header' type)")
783
+ webhookAuthShape = z3.object({
784
+ type: z3.enum(["bearer", "basic", "header"]).describe("Authentication type"),
785
+ token: z3.string().optional().describe("Bearer token (required for 'bearer' type)"),
786
+ username: z3.string().optional().describe("Username (required for 'basic' type)"),
787
+ password: z3.string().optional().describe("Password (required for 'basic' type)"),
788
+ headers: z3.object({}).passthrough().optional().describe("Custom headers as key-value pairs (required for 'header' type)")
556
789
  }).optional().describe("Authentication configuration for the webhook endpoint");
557
- notificationChannelsSchema = z.object({
558
- email: z.object({
559
- recipients: z.array(z.string().email()).optional().describe("Email addresses to notify. Omit to use account default email.")
790
+ notificationChannelsSchema = z3.object({
791
+ email: z3.object({
792
+ recipients: z3.array(z3.string().email()).optional().describe("Email addresses to notify. Omit to use account default email.")
560
793
  }).optional().describe("Send email notifications. Pass {} for account default email, or {recipients: [...]} for custom addresses."),
561
- webhook: z.object({
562
- url: z.string().url().describe("Webhook endpoint URL"),
563
- httpMethod: z.enum(["POST", "GET", "PUT", "PATCH"]).optional().describe("HTTP method (defaults to POST)"),
794
+ webhook: z3.object({
795
+ url: z3.string().url().describe("Webhook endpoint URL"),
796
+ httpMethod: z3.enum(["POST", "GET", "PUT", "PATCH"]).optional().describe("HTTP method (defaults to POST)"),
564
797
  auth: webhookAuthShape
565
798
  }).optional().describe("Send notifications via HTTP webhook"),
566
- slack: z.object({
567
- webhookUrl: z.string().url().optional().describe("Slack incoming webhook URL (legacy)"),
568
- slackChannelId: z.string().optional().describe("Slack channel ID from OAuth integration (e.g. C01ABCDEF)"),
569
- slackChannelName: z.string().optional().describe("Slack channel name (e.g. #alerts)")
799
+ slack: z3.object({
800
+ webhookUrl: z3.string().url().optional().describe("Slack incoming webhook URL (legacy)"),
801
+ slackChannelId: z3.string().optional().describe("Slack channel ID from OAuth integration (e.g. C01ABCDEF)"),
802
+ slackChannelName: z3.string().optional().describe("Slack channel name (e.g. #alerts)")
570
803
  }).optional().describe("Send notifications to a Slack channel. Use slackChannelId/slackChannelName for OAuth integration, or webhookUrl for legacy incoming webhooks."),
571
- websocket: z.boolean().optional().describe("Enable WebSocket notifications for programmatic real-time consumption")
804
+ websocket: z3.boolean().optional().describe("Enable WebSocket notifications for programmatic real-time consumption")
572
805
  });
573
806
  notificationsInputShape = {
574
- notifications: z.preprocess(coerceJson(), notificationChannelsSchema.optional()).optional().describe("Optional notification channels to alert when data changes.")
807
+ notifications: z3.preprocess(coerceJson(), notificationChannelsSchema.optional()).optional().describe("Optional notification channels to alert when data changes.")
575
808
  };
576
809
  requiredNotificationsInputShape = {
577
- notifications: z.preprocess(coerceJson(), notificationChannelsSchema).describe("Notification channels to alert when data changes.")
810
+ notifications: z3.preprocess(coerceJson(), notificationChannelsSchema).describe("Notification channels to alert when data changes.")
578
811
  };
579
812
  DATA_FILTER_OPERATORS = [
580
813
  "EQUALS",
@@ -599,10 +832,10 @@ var init_schemas = __esm(() => {
599
832
  ];
600
833
  DATA_FILTER_VALUELESS_OPERATORS = new Set(["IS_NULL", "IS_NOT_NULL", "IS_EMPTY", "IS_NOT_EMPTY"]);
601
834
  DATA_FILTER_LIST_OPERATORS = new Set(["IN", "NOT_IN"]);
602
- DataFilterSchema = z.object({
603
- field: z.string().trim().min(1).describe("Field name from the workflow schema; one nested level is allowed, e.g. classification.match"),
604
- operator: z.preprocess((val) => typeof val === "string" ? val.trim().toUpperCase() : val, z.enum(DATA_FILTER_OPERATORS)),
605
- value: z.union([z.string(), z.number(), z.boolean(), z.array(z.union([z.string(), z.number()]))]).optional().describe("Omit for IS_NULL, IS_NOT_NULL, IS_EMPTY, IS_NOT_EMPTY; an array for IN/NOT_IN; days for WITHIN_LAST_DAYS")
835
+ DataFilterSchema = z3.object({
836
+ field: z3.string().trim().min(1).describe("Field name from the workflow schema; one nested level is allowed, e.g. classification.match"),
837
+ operator: z3.preprocess((val) => typeof val === "string" ? val.trim().toUpperCase() : val, z3.enum(DATA_FILTER_OPERATORS)),
838
+ value: z3.union([z3.string(), z3.number(), z3.boolean(), z3.array(z3.union([z3.string(), z3.number()]))]).optional().describe("Omit for IS_NULL, IS_NOT_NULL, IS_EMPTY, IS_NOT_EMPTY; an array for IN/NOT_IN; days for WITHIN_LAST_DAYS")
606
839
  }).strict().superRefine((filter, ctx) => {
607
840
  if (DATA_FILTER_VALUELESS_OPERATORS.has(filter.operator)) {
608
841
  if (filter.value !== undefined) {
@@ -627,12 +860,12 @@ var init_schemas = __esm(() => {
627
860
  });
628
861
  }
629
862
  });
630
- dataFiltersInput = z.preprocess(normalizeDataFilters, z.array(DataFilterSchema).optional()).optional().describe('Row filters, e.g. [{"field":"title","operator":"CONTAINS","value":"Manager"}]. Multiple filters are combined with OR. BEFORE, AFTER and WITHIN_LAST_DAYS only apply to DATE/DATETIME fields.');
863
+ dataFiltersInput = z3.preprocess(normalizeDataFilters, z3.array(DataFilterSchema).optional()).optional().describe('Row filters, e.g. [{"field":"title","operator":"CONTAINS","value":"Manager"}]. Multiple filters are combined with OR. BEFORE, AFTER and WITHIN_LAST_DAYS only apply to DATE/DATETIME fields.');
631
864
  });
632
865
 
633
866
  // src/tools/assistant.ts
634
867
  import { FREEFORM_ASSISTANT_ANSWER_KEY } from "@kadoa/node-sdk";
635
- import { z as z2 } from "zod";
868
+ import { z as z4 } from "zod";
636
869
  function isAssistantSessionResumable(status) {
637
870
  return status != null && RESUMABLE_ASSISTANT_STATUSES.has(status);
638
871
  }
@@ -700,8 +933,9 @@ function registerAssistantTools(server, ctx, helpers) {
700
933
  title: "Get Workflow Assistant",
701
934
  description: "Get the current Assistant lifecycle for a workflow, including whether it is working, waiting for input, idle, interrupted, or closed and any pending customer question. Works for one-time or scheduled extraction workflows and realtime monitoring workflows.",
702
935
  inputSchema: strictSchema({
703
- workflowId: z2.string().min(1).describe("The workflow ID")
936
+ workflowId: z4.string().min(1).describe("The workflow ID")
704
937
  }),
938
+ outputSchema: WorkflowAssistantOutput,
705
939
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
706
940
  }, withErrorHandling("get_workflow_assistant", async (args) => {
707
941
  const workflow = await ctx.client.workflow.get(args.workflowId);
@@ -733,10 +967,11 @@ function registerAssistantTools(server, ctx, helpers) {
733
967
  title: "Get Workflow Assistant Timeline",
734
968
  description: "Read the customer-visible conversation history for a workflow Assistant, including user messages, Assistant messages, and clarification questions with answers. " + "Use this when the user asks what the Assistant said or asked, how a clarification was answered, or to show the latest conversation for a workflow. " + "Supports one-time or scheduled extraction workflows and realtime monitoring workflows. Use get_workflow_history instead for configuration revisions. " + "Results are chronological within each page; use nextCursor to request older items when hasMore is true.",
735
969
  inputSchema: strictSchema({
736
- workflowId: z2.string().min(1).describe("The workflow ID"),
737
- cursor: z2.string().min(1).optional().describe("Opaque nextCursor from the previous page"),
738
- limit: z2.preprocess(coerceNumber(), z2.number().int().min(1).max(100)).optional().describe("Maximum history items to return (default: 50, maximum: 100)")
970
+ workflowId: z4.string().min(1).describe("The workflow ID"),
971
+ cursor: z4.string().min(1).optional().describe("Opaque nextCursor from the previous page"),
972
+ limit: limitInput({ noun: "History items", defaultLimit: 50, max: 100 })
739
973
  }),
974
+ outputSchema: AssistantTimelineOutput,
740
975
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
741
976
  }, withErrorHandling("get_workflow_assistant_timeline", async (args) => {
742
977
  const timeline = await ctx.client.assistant.getTimeline(args.workflowId, {
@@ -749,8 +984,12 @@ function registerAssistantTools(server, ctx, helpers) {
749
984
  return jsonResult({
750
985
  workflowId: timeline.workflowId,
751
986
  sessionId: timeline.sessionId,
752
- items: timeline.items,
753
- pagination: timeline.pagination,
987
+ items: timeline.items ?? [],
988
+ pagination: cursorPage({
989
+ args,
990
+ nextCursor: timeline.pagination?.nextCursor,
991
+ hasMore: timeline.pagination?.hasMore === true
992
+ }),
754
993
  dashboardUrl: workflowDashboardUrl(timeline.workflowId)
755
994
  });
756
995
  }));
@@ -758,11 +997,12 @@ function registerAssistantTools(server, ctx, helpers) {
758
997
  title: "Answer Assistant Question",
759
998
  description: "Answer the workflow Assistant's currently pending clarification question and resume it. " + "Use `answer` for a freeform response, or `answers` for the structured key/value response described by get_workflow_assistant. Provide exactly one. The question ID must still be current.",
760
999
  inputSchema: strictSchema({
761
- workflowId: z2.string().min(1).describe("The workflow ID"),
762
- questionId: z2.string().min(1).describe("The pending question ID returned by get_workflow_assistant"),
763
- answer: z2.string().min(1).optional().describe("Freeform answer"),
764
- answers: z2.preprocess(coerceJson(), z2.record(z2.string(), z2.string())).optional().describe("Structured answer map; keys identify questions and values contain the selected/freeform answers")
1000
+ workflowId: z4.string().min(1).describe("The workflow ID"),
1001
+ questionId: z4.string().min(1).describe("The pending question ID returned by get_workflow_assistant"),
1002
+ answer: z4.string().min(1).optional().describe("Freeform answer"),
1003
+ answers: z4.preprocess(coerceJson(), z4.record(z4.string(), z4.string())).optional().describe("Structured answer map; keys identify questions and values contain the selected/freeform answers")
765
1004
  }),
1005
+ outputSchema: AssistantAnswerOutput,
766
1006
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }
767
1007
  }, withErrorHandling("answer_workflow_assistant_question", async (args) => {
768
1008
  const hasFreeform = args.answer != null;
@@ -804,8 +1044,9 @@ function registerAssistantTools(server, ctx, helpers) {
804
1044
  title: "Interrupt Workflow Assistant",
805
1045
  description: "Safely interrupt the active Assistant working on a workflow. This pauses Assistant reasoning/update work; it does not pause the workflow's extraction schedule. Use pause_workflow for scheduling instead.",
806
1046
  inputSchema: strictSchema({
807
- workflowId: z2.string().min(1).describe("Workflow whose Assistant session should be interrupted")
1047
+ workflowId: z4.string().min(1).describe("Workflow whose Assistant session should be interrupted")
808
1048
  }),
1049
+ outputSchema: AssistantControlOutput,
809
1050
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }
810
1051
  }, withErrorHandling("interrupt_workflow_assistant", async (args) => {
811
1052
  const workflow = await ctx.client.workflow.get(args.workflowId);
@@ -838,8 +1079,9 @@ function registerAssistantTools(server, ctx, helpers) {
838
1079
  title: "Resume Workflow Assistant",
839
1080
  description: "Resume an idle or interrupted workflow Assistant using the role and conversation persisted on its session. Works for one-time or scheduled extraction workflows and realtime monitoring workflows. This does not resume a paused workflow schedule; use approve_workflow for that.",
840
1081
  inputSchema: strictSchema({
841
- workflowId: z2.string().min(1).describe("Workflow whose Assistant session should resume")
1082
+ workflowId: z4.string().min(1).describe("Workflow whose Assistant session should resume")
842
1083
  }),
1084
+ outputSchema: AssistantControlOutput,
843
1085
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }
844
1086
  }, withErrorHandling("resume_workflow_assistant", async (args) => {
845
1087
  const workflow = await ctx.client.workflow.get(args.workflowId);
@@ -872,8 +1114,9 @@ function registerAssistantTools(server, ctx, helpers) {
872
1114
  title: "Stop Workflow Assistant",
873
1115
  description: "Stop the active Assistant update for a workflow. This cancels Assistant work but does not delete, pause, or replace the workflow and does not change workflowId. Use pause_workflow separately to stop scheduled extraction runs.",
874
1116
  inputSchema: strictSchema({
875
- workflowId: z2.string().min(1).describe("Workflow whose active Assistant session should stop")
1117
+ workflowId: z4.string().min(1).describe("Workflow whose active Assistant session should stop")
876
1118
  }),
1119
+ outputSchema: AssistantControlOutput,
877
1120
  annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false }
878
1121
  }, withErrorHandling("stop_workflow_assistant", async (args) => {
879
1122
  const workflow = await ctx.client.workflow.get(args.workflowId);
@@ -905,8 +1148,9 @@ function registerAssistantTools(server, ctx, helpers) {
905
1148
  title: "Get Workflow Strategy",
906
1149
  description: "Get the current customer-safe extraction/build strategy for a workflow: source approach, endpoints, interaction style, selectors, and realtime interval when applicable. Returns building rather than an error when an Assistant session has no build strategy yet.",
907
1150
  inputSchema: strictSchema({
908
- workflowId: z2.string().min(1).describe("The workflow ID")
1151
+ workflowId: z4.string().min(1).describe("The workflow ID")
909
1152
  }),
1153
+ outputSchema: WorkflowStrategyOutput,
910
1154
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
911
1155
  }, withErrorHandling("get_workflow_strategy", async (args) => {
912
1156
  const workflow = await ctx.client.workflow.get(args.workflowId);
@@ -937,6 +1181,8 @@ function registerAssistantTools(server, ctx, helpers) {
937
1181
  }
938
1182
  var RESUMABLE_ASSISTANT_STATUSES, ACTIVE_ASSISTANT_STATUSES, IDLE_ASSISTANT_STATUSES, CLOSED_ASSISTANT_STATUSES;
939
1183
  var init_assistant = __esm(() => {
1184
+ init_output_schemas();
1185
+ init_pagination();
940
1186
  init_schemas();
941
1187
  RESUMABLE_ASSISTANT_STATUSES = new Set(["pending", "created", "active", "idle", "inactive", "interrupted"]);
942
1188
  ACTIVE_ASSISTANT_STATUSES = new Set(["pending", "created", "active"]);
@@ -945,6 +1191,7 @@ var init_assistant = __esm(() => {
945
1191
  });
946
1192
 
947
1193
  // src/notification-channels.ts
1194
+ import { createHash } from "node:crypto";
948
1195
  function recipientKey(recipients) {
949
1196
  return [...new Set(recipients.map((r) => r.trim().toLowerCase()))].sort().join(",");
950
1197
  }
@@ -968,6 +1215,14 @@ function destinationOf(channelType, rawConfig) {
968
1215
  return `url:${webhookUrl}`;
969
1216
  return null;
970
1217
  }
1218
+ function mcpChannelName(channelType, config) {
1219
+ const name = `mcp-${channelType.toLowerCase()}`;
1220
+ const slackName = config.slackChannelName;
1221
+ const destination = destinationOf(channelType, config) ?? (channelType === "SLACK" && typeof slackName === "string" && slackName.trim() ? `slack-name:${slackName.trim().toLowerCase()}` : null);
1222
+ if (destination === null)
1223
+ return name;
1224
+ return `${name}-${createHash("sha256").update(destination).digest("hex").slice(0, 8)}`;
1225
+ }
971
1226
  function findReusableChannel(channels, channelType, config) {
972
1227
  const wanted = destinationOf(channelType, config);
973
1228
  if (wanted === null)
@@ -1012,16 +1267,23 @@ function redactChannelConfig(config) {
1012
1267
  ...auth !== undefined && { auth: redactAuth(auth) }
1013
1268
  };
1014
1269
  }
1270
+ function redactChannelRefs(channels) {
1271
+ return channels?.map((channel) => {
1272
+ const { config } = channel;
1273
+ return config === undefined ? channel : { ...channel, config: redactChannelConfig(config) };
1274
+ });
1275
+ }
1276
+ var init_notification_channels = () => {};
1015
1277
 
1016
1278
  // src/tools/notifications.ts
1017
- import { z as z3 } from "zod";
1279
+ import { z as z5 } from "zod";
1018
1280
  function registerNotificationChannelTools(server, ctx, helpers) {
1019
1281
  const { withErrorHandling } = helpers;
1020
1282
  server.registerTool("list_notification_channels", {
1021
1283
  title: "List Notification Channels",
1022
1284
  description: "List notification channels configured for a workflow or across the workspace. Channels are destinations (email, Slack, webhook, websocket) where notifications are sent.",
1023
1285
  inputSchema: strictSchema({
1024
- workflowId: z3.string().optional().describe("Filter channels linked to a specific workflow. Omit to list all workspace channels.")
1286
+ workflowId: z5.string().optional().describe("Filter channels linked to a specific workflow. Omit to list all workspace channels.")
1025
1287
  }),
1026
1288
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
1027
1289
  }, withErrorHandling("list_notification_channels", async (args) => {
@@ -1036,27 +1298,28 @@ function registerNotificationChannelTools(server, ctx, helpers) {
1036
1298
  createdAt: ch.createdAt,
1037
1299
  dashboardUrl: notificationSettingsDashboardUrl()
1038
1300
  })),
1039
- total: channels.length
1301
+ total: channels.length,
1302
+ pagination: completeList(channels.length)
1040
1303
  });
1041
1304
  }));
1042
1305
  server.registerTool("create_notification_channel", {
1043
1306
  title: "Create Notification Channel",
1044
1307
  description: "Create a notification channel (email, webhook, slack, or websocket). If a channel of the same type already delivers to that destination it is reused and returned with reused=true, rather than creating a duplicate. The channel can then be linked to notification settings for specific events. " + "For SLACK: use slackChannelId + slackChannelName for OAuth integration, or webhookUrl for legacy incoming webhooks. " + "For WEBHOOK: auth is optional and supports bearer, basic, or header authentication.",
1045
1308
  inputSchema: strictSchema({
1046
- channelType: z3.enum(["EMAIL", "WEBHOOK", "SLACK", "WEBSOCKET"]).describe("Type of notification channel"),
1047
- name: z3.string().optional().describe("Name for the channel (defaults to 'default')"),
1048
- config: z3.object({
1049
- recipients: z3.array(z3.string()).optional().describe("Email recipients (required for EMAIL channels)"),
1050
- webhookUrl: z3.string().optional().describe("Webhook/Slack endpoint URL (required for WEBHOOK, optional for SLACK)"),
1051
- httpMethod: z3.enum(["POST", "GET", "PUT", "PATCH"]).optional().describe("HTTP method for WEBHOOK channels (defaults to POST)"),
1052
- slackChannelId: z3.string().optional().describe("Slack channel ID from OAuth integration (for SLACK channels, e.g. C01ABCDEF)"),
1053
- slackChannelName: z3.string().optional().describe("Slack channel name (for SLACK channels, e.g. #alerts)"),
1054
- auth: z3.object({
1055
- type: z3.enum(["bearer", "basic", "header"]).describe("Authentication type"),
1056
- token: z3.string().optional().describe("Bearer token (for 'bearer' type)"),
1057
- username: z3.string().optional().describe("Username (for 'basic' type)"),
1058
- password: z3.string().optional().describe("Password (for 'basic' type)"),
1059
- headers: z3.object({}).passthrough().optional().describe("Custom headers as key-value pairs (for 'header' type)")
1309
+ channelType: z5.enum(["EMAIL", "WEBHOOK", "SLACK", "WEBSOCKET"]).describe("Type of notification channel"),
1310
+ name: z5.string().optional().describe("Name for the channel (defaults to 'default')"),
1311
+ config: z5.object({
1312
+ recipients: z5.array(z5.string()).optional().describe("Email recipients (required for EMAIL channels)"),
1313
+ webhookUrl: z5.string().optional().describe("Webhook/Slack endpoint URL (required for WEBHOOK, optional for SLACK)"),
1314
+ httpMethod: z5.enum(["POST", "GET", "PUT", "PATCH"]).optional().describe("HTTP method for WEBHOOK channels (defaults to POST)"),
1315
+ slackChannelId: z5.string().optional().describe("Slack channel ID from OAuth integration (for SLACK channels, e.g. C01ABCDEF)"),
1316
+ slackChannelName: z5.string().optional().describe("Slack channel name (for SLACK channels, e.g. #alerts)"),
1317
+ auth: z5.object({
1318
+ type: z5.enum(["bearer", "basic", "header"]).describe("Authentication type"),
1319
+ token: z5.string().optional().describe("Bearer token (for 'bearer' type)"),
1320
+ username: z5.string().optional().describe("Username (for 'basic' type)"),
1321
+ password: z5.string().optional().describe("Password (for 'basic' type)"),
1322
+ headers: z5.object({}).passthrough().optional().describe("Custom headers as key-value pairs (for 'header' type)")
1060
1323
  }).optional().describe("Authentication for WEBHOOK channels")
1061
1324
  }).optional().describe("Channel-specific configuration. " + "EMAIL: provide recipients. " + "WEBHOOK: provide webhookUrl, httpMethod, and optionally auth. " + "SLACK: provide slackChannelId + slackChannelName (OAuth) or webhookUrl (legacy). " + "WEBSOCKET: no config needed.")
1062
1325
  }),
@@ -1103,8 +1366,8 @@ function registerNotificationSettingsTools(server, ctx, helpers) {
1103
1366
  title: "Delete Notification Channel",
1104
1367
  description: "Delete a notification channel by ID. " + "Call first without confirmed=true to preview, then with confirmed=true after user agrees.",
1105
1368
  inputSchema: strictSchema({
1106
- channelId: z3.string().min(1).describe("The channel ID to delete"),
1107
- confirmed: z3.preprocess(coerceBoolean(), z3.boolean()).optional().describe("Set to true only after the user has explicitly confirmed deletion.")
1369
+ channelId: z5.string().min(1).describe("The channel ID to delete"),
1370
+ confirmed: z5.preprocess(coerceBoolean(), z5.boolean()).optional().describe("Set to true only after the user has explicitly confirmed deletion.")
1108
1371
  }),
1109
1372
  annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false }
1110
1373
  }, withErrorHandling("delete_notification_channel", async (args) => {
@@ -1125,7 +1388,7 @@ function registerNotificationSettingsTools(server, ctx, helpers) {
1125
1388
  title: "List Notification Settings",
1126
1389
  description: "List notification settings (event-to-channel mappings) for a workflow or workspace. Shows which events trigger notifications and through which channels.",
1127
1390
  inputSchema: strictSchema({
1128
- workflowId: z3.string().optional().describe("Filter settings for a specific workflow. Omit to list workspace-level settings."),
1391
+ workflowId: z5.string().optional().describe("Filter settings for a specific workflow. Omit to list workspace-level settings."),
1129
1392
  eventType: NotificationEventTypeSchema.optional().describe("Filter by event type")
1130
1393
  }),
1131
1394
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
@@ -1142,33 +1405,34 @@ function registerNotificationSettingsTools(server, ctx, helpers) {
1142
1405
  workflowId: s.workflowId,
1143
1406
  eventType: s.eventType,
1144
1407
  enabled: s.enabled,
1145
- channels: s.channels,
1408
+ channels: redactChannelRefs(s.channels),
1146
1409
  createdAt: s.createdAt
1147
1410
  })),
1148
- total: settings.length
1411
+ total: settings.length,
1412
+ pagination: completeList(settings.length)
1149
1413
  });
1150
1414
  }));
1151
1415
  server.registerTool("configure_notifications", {
1152
1416
  title: "Configure Notifications",
1153
1417
  description: "Set up notifications for a workflow or workspace. Creates channels and settings in one step. " + "Specify which events to listen for and which channels to deliver to (email, webhook, slack, websocket).",
1154
1418
  inputSchema: strictSchema({
1155
- workflowId: z3.string().optional().describe("Workflow ID to configure notifications for. Omit for workspace-level notifications."),
1156
- events: z3.preprocess(coerceList(), z3.array(z3.enum(["all", ...NOTIFICATION_EVENT_TYPES])).min(1)).describe('Event types to notify on. Pass ["all"] for every event type, or list specific event types.'),
1157
- channels: z3.object({
1158
- email: z3.object({
1159
- recipients: z3.array(z3.string().email()).optional().describe("Email addresses to notify. Omit to use the account default email.")
1419
+ workflowId: z5.string().optional().describe("Workflow ID to configure notifications for. Omit for workspace-level notifications."),
1420
+ events: z5.preprocess(coerceList(), z5.array(z5.enum(["all", ...NOTIFICATION_EVENT_TYPES])).min(1)).describe('Event types to notify on. Pass ["all"] for every event type, or list specific event types.'),
1421
+ channels: z5.object({
1422
+ email: z5.object({
1423
+ recipients: z5.array(z5.string().email()).optional().describe("Email addresses to notify. Omit to use the account default email.")
1160
1424
  }).optional().describe("Send email notifications. Pass {} for default email or {recipients: [...]} for custom addresses."),
1161
- webhook: z3.object({
1162
- url: z3.string().url().describe("Webhook endpoint URL"),
1163
- httpMethod: z3.enum(["POST", "GET", "PUT", "PATCH"]).optional().describe("HTTP method (defaults to POST)"),
1425
+ webhook: z5.object({
1426
+ url: z5.string().url().describe("Webhook endpoint URL"),
1427
+ httpMethod: z5.enum(["POST", "GET", "PUT", "PATCH"]).optional().describe("HTTP method (defaults to POST)"),
1164
1428
  auth: webhookAuthShape
1165
1429
  }).optional().describe("Send via HTTP webhook"),
1166
- slack: z3.object({
1167
- webhookUrl: z3.string().url().optional().describe("Slack incoming webhook URL (legacy)"),
1168
- slackChannelId: z3.string().optional().describe("Slack channel ID from OAuth integration (e.g. C01ABCDEF)"),
1169
- slackChannelName: z3.string().optional().describe("Slack channel name (e.g. #alerts)")
1430
+ slack: z5.object({
1431
+ webhookUrl: z5.string().url().optional().describe("Slack incoming webhook URL (legacy)"),
1432
+ slackChannelId: z5.string().optional().describe("Slack channel ID from OAuth integration (e.g. C01ABCDEF)"),
1433
+ slackChannelName: z5.string().optional().describe("Slack channel name (e.g. #alerts)")
1170
1434
  }).optional().describe("Send to Slack channel. Use slackChannelId/slackChannelName for OAuth integration, or webhookUrl for legacy incoming webhooks."),
1171
- websocket: z3.boolean().optional().describe("Enable WebSocket notifications")
1435
+ websocket: z5.boolean().optional().describe("Enable WebSocket notifications")
1172
1436
  }).describe("At least one channel must be specified")
1173
1437
  }),
1174
1438
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }
@@ -1178,7 +1442,7 @@ function registerNotificationSettingsTools(server, ctx, helpers) {
1178
1442
  const ch = args.channels;
1179
1443
  if (ch.email) {
1180
1444
  if (ch.email.recipients?.length) {
1181
- channelSetup.EMAIL = { name: "mcp-email", recipients: ch.email.recipients };
1445
+ channelSetup.EMAIL = { name: mcpChannelName("EMAIL", ch.email), recipients: ch.email.recipients };
1182
1446
  } else {
1183
1447
  channelSetup.EMAIL = true;
1184
1448
  }
@@ -1187,7 +1451,7 @@ function registerNotificationSettingsTools(server, ctx, helpers) {
1187
1451
  channelSetup.WEBSOCKET = true;
1188
1452
  if (ch.webhook) {
1189
1453
  const webhookChannel = {
1190
- name: "mcp-webhook",
1454
+ name: mcpChannelName("WEBHOOK", { webhookUrl: ch.webhook.url }),
1191
1455
  webhookUrl: ch.webhook.url,
1192
1456
  httpMethod: ch.webhook.httpMethod || "POST"
1193
1457
  };
@@ -1196,7 +1460,7 @@ function registerNotificationSettingsTools(server, ctx, helpers) {
1196
1460
  channelSetup.WEBHOOK = webhookChannel;
1197
1461
  }
1198
1462
  if (ch.slack) {
1199
- const slackChannel = { name: "mcp-slack" };
1463
+ const slackChannel = { name: mcpChannelName("SLACK", ch.slack) };
1200
1464
  if (ch.slack.webhookUrl)
1201
1465
  slackChannel.webhookUrl = ch.slack.webhookUrl;
1202
1466
  if (ch.slack.slackChannelId)
@@ -1222,7 +1486,7 @@ function registerNotificationSettingsTools(server, ctx, helpers) {
1222
1486
  id: s.id,
1223
1487
  eventType: s.eventType,
1224
1488
  enabled: s.enabled,
1225
- channels: s.channels
1489
+ channels: redactChannelRefs(s.channels)
1226
1490
  }))
1227
1491
  });
1228
1492
  }));
@@ -1230,8 +1494,8 @@ function registerNotificationSettingsTools(server, ctx, helpers) {
1230
1494
  title: "Delete Notification Setting",
1231
1495
  description: "Delete a notification setting by ID. This removes the event-to-channel mapping but does not delete the channels themselves. " + "Call first without confirmed=true to preview, then with confirmed=true after user agrees.",
1232
1496
  inputSchema: strictSchema({
1233
- settingId: z3.string().min(1).describe("The notification setting ID to delete (the id from list_notification_settings)"),
1234
- confirmed: z3.preprocess(coerceBoolean(), z3.boolean()).optional().describe("Set to true only after the user has explicitly confirmed deletion.")
1497
+ settingId: z5.string().min(1).describe("The notification setting ID to delete (the id from list_notification_settings)"),
1498
+ confirmed: z5.preprocess(coerceBoolean(), z5.boolean()).optional().describe("Set to true only after the user has explicitly confirmed deletion.")
1235
1499
  }),
1236
1500
  annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false }
1237
1501
  }, withErrorHandling("delete_notification_setting", async (args) => {
@@ -1250,17 +1514,14 @@ function registerNotificationSettingsTools(server, ctx, helpers) {
1250
1514
  }));
1251
1515
  }
1252
1516
  var init_notifications = __esm(() => {
1517
+ init_notification_channels();
1518
+ init_pagination();
1253
1519
  init_schemas();
1254
1520
  });
1255
1521
 
1256
1522
  // src/tools/observability.ts
1257
1523
  import { KadoaSdkException as KadoaSdkException2 } from "@kadoa/node-sdk";
1258
- import { z as z4 } from "zod";
1259
- function boundActivityLimit(limit) {
1260
- if (limit === undefined || Number.isNaN(limit))
1261
- return ACTIVITY_DEFAULT_LIMIT;
1262
- return Math.min(Math.max(Math.trunc(limit), 1), ACTIVITY_MAX_LIMIT);
1263
- }
1524
+ import { z as z6 } from "zod";
1264
1525
  function mapActivityEvent(event, includeDetails) {
1265
1526
  const details = event.details ?? {};
1266
1527
  const summary = details._changeSummary;
@@ -1333,10 +1594,10 @@ function registerObservabilityTools(server, ctx, helpers) {
1333
1594
  title: "Get Observability Metrics",
1334
1595
  description: "Workflow health for the active team over a window of days (default 30): overall uptime in percent, " + "successful and failed runs, mean time to recovery in minutes, records delivered, counts of healthy, " + "degraded, down, and idle workflows, the same numbers for the previous window, and an 'attention' list " + "of down and degraded workflows ranked worst first. This is the data behind the Observability page. " + "Use for 'how healthy are my workflows', 'what is my uptime', 'which workflows are down or failing', " + "'how fast do we recover', 'how did this month compare to last'. " + "Do NOT use it to explain why one workflow failed: call list_workflow_runs (errors per run) or " + "get_workflow for that. For what needs the user's action right now use list_workflows with " + "statusFilters group:attention. " + "Health comes from each workflow's last run in the window: healthy means the last run finished, down " + "means the last run failed, idle means no finished or failed run. uptimePercentage is null when no runs " + "happened. mttrMinutes is null when nothing failed and then recovered. openSupportRequests is a count " + "only. " + "Output is bounded: pass includeWorkflows for every workflow ranked by severity (up to limit, default " + "25, max 200) and includeDailySeries for the team-level daily uptime series; per-workflow daily series " + "are never returned. Quote workflows by name; ids are stripped from customer-facing text. Large teams " + "can take up to 90 seconds; prefer 30 or 90 days over 365.",
1335
1596
  inputSchema: strictSchema({
1336
- days: z4.preprocess(coerceNumber(), z4.number().int().min(1).max(365)).optional().describe("Window length in days, 1 to 365 (default 30). The dashboard offers 30, 90, and 365."),
1337
- includeWorkflows: z4.preprocess(coerceBoolean(), z4.boolean()).optional().describe("Also return every workflow ranked by severity, not only the down and degraded ones (default false)."),
1338
- includeDailySeries: z4.preprocess(coerceBoolean(), z4.boolean()).optional().describe("Also return the team-level daily series of successful runs, failed runs, and uptime (default false)."),
1339
- limit: z4.preprocess(coerceNumber(), z4.number().int().min(1).max(OBSERVABILITY_MAX_LIMIT)).optional().describe("Maximum workflows in the attention list and in workflows (default 25, max 200).")
1597
+ days: z6.preprocess(coerceNumber(), z6.number().int().min(1).max(365)).optional().describe("Window length in days, 1 to 365 (default 30). The dashboard offers 30, 90, and 365."),
1598
+ includeWorkflows: z6.preprocess(coerceBoolean(), z6.boolean()).optional().describe("Also return every workflow ranked by severity, not only the down and degraded ones (default false)."),
1599
+ includeDailySeries: z6.preprocess(coerceBoolean(), z6.boolean()).optional().describe("Also return the team-level daily series of successful runs, failed runs, and uptime (default false)."),
1600
+ limit: z6.preprocess(coerceNumber(), z6.number().int().min(1).max(OBSERVABILITY_MAX_LIMIT)).optional().describe("Maximum workflows in the attention list and in workflows (default 25, max 200).")
1340
1601
  }),
1341
1602
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
1342
1603
  }, withErrorHandling("get_observability", async (args) => {
@@ -1392,22 +1653,23 @@ function registerObservabilityTools(server, ctx, helpers) {
1392
1653
  title: "List Activity",
1393
1654
  description: "List the team's Activity log (audit trail): who did what, when, and from which interface, across " + "every workflow and resource in the active team. Each event carries the actor's email, the resource " + "name and type, the action (CREATED, UPDATED, DELETED, APPROVED, FINISHED, FAILED, DATA_CHANGED, ...), " + "the interface label, and a changeSummary when the backend recorded one. Use it for team-wide questions " + "such as 'who changed anything last week?', 'what did alice do yesterday?', or 'which workflows were " + "deleted this month?'. For one workflow's configuration revisions with field-level before/after diffs " + "(prompt, schema, schedule), use get_workflow_history instead. Newest events come first. Refer to " + "people by email and resources by name in answers; ids are for follow-up tool calls.",
1394
1655
  inputSchema: strictSchema({
1395
- limit: z4.preprocess(coerceNumber(), z4.number()).optional().describe(`Events per page (default ${ACTIVITY_DEFAULT_LIMIT}, max ${ACTIVITY_MAX_LIMIT})`),
1396
- offset: z4.preprocess(coerceNumber(), z4.number()).optional().describe("Number of events to skip (default 0)"),
1397
- workflowId: z4.string().optional().describe("Only events about this workflow (runs, config changes, data changes, notifications)"),
1398
- userId: z4.string().optional().describe("Only events by this user id. Resolve an email to an id from a previous list_activity result."),
1399
- startDate: z4.string().optional().describe("ISO 8601 lower bound. Ignored when relativeTime is set."),
1400
- endDate: z4.string().optional().describe("ISO 8601 upper bound. Ignored when relativeTime is set."),
1401
- relativeTime: z4.string().regex(/^\d+(m|h|d|w)$/i, "Use <number><unit> with unit m, h, d or w, e.g. 6h or 2w").optional().describe("Window counted back from now, e.g. 30m, 6h, 1d, 2w. Preferred for 'last week' style questions."),
1402
- eventTypes: z4.preprocess(coerceList({ upperCase: true }), z4.array(z4.enum(ACTIVITY_EVENT_TYPES))).optional().describe("Actions to keep. Workflow run outcomes (FINISHED, FAILED, ...) count as actions."),
1403
- interfaces: z4.preprocess(coerceList({ upperCase: true }), z4.array(z4.enum(ACTIVITY_INTERFACES))).optional().describe("Interfaces the action came from"),
1404
- resourceTypes: z4.preprocess(coerceList({ upperCase: true }), z4.array(z4.enum(ACTIVITY_RESOURCE_TYPES))).optional().describe("Resource types to keep"),
1405
- includeDetails: z4.preprocess(coerceBoolean(), z4.boolean()).optional().describe("Include sanitized previousValue/newValue snapshots per event (default false; large)")
1656
+ limit: limitInput({ noun: "Events", defaultLimit: ACTIVITY_DEFAULT_LIMIT, max: ACTIVITY_MAX_LIMIT }),
1657
+ offset: offsetInput("offset", "events"),
1658
+ workflowId: z6.string().optional().describe("Only events about this workflow (runs, config changes, data changes, notifications)"),
1659
+ userId: z6.string().optional().describe("Only events by this user id. Resolve an email to an id from a previous list_activity result."),
1660
+ startDate: z6.string().optional().describe("ISO 8601 lower bound. Ignored when relativeTime is set."),
1661
+ endDate: z6.string().optional().describe("ISO 8601 upper bound. Ignored when relativeTime is set."),
1662
+ relativeTime: z6.string().regex(/^\d+(m|h|d|w)$/i, "Use <number><unit> with unit m, h, d or w, e.g. 6h or 2w").optional().describe("Window counted back from now, e.g. 30m, 6h, 1d, 2w. Preferred for 'last week' style questions."),
1663
+ eventTypes: z6.preprocess(coerceList({ upperCase: true }), z6.array(z6.enum(ACTIVITY_EVENT_TYPES))).optional().describe("Actions to keep. Workflow run outcomes (FINISHED, FAILED, ...) count as actions."),
1664
+ interfaces: z6.preprocess(coerceList({ upperCase: true }), z6.array(z6.enum(ACTIVITY_INTERFACES))).optional().describe("Interfaces the action came from"),
1665
+ resourceTypes: z6.preprocess(coerceList({ upperCase: true }), z6.array(z6.enum(ACTIVITY_RESOURCE_TYPES))).optional().describe("Resource types to keep"),
1666
+ includeDetails: z6.preprocess(coerceBoolean(), z6.boolean()).optional().describe("Include sanitized previousValue/newValue snapshots per event (default false; large)")
1406
1667
  }),
1407
1668
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
1408
1669
  }, withErrorHandling("list_activity", async (args) => {
1670
+ const limit = args.limit ?? ACTIVITY_DEFAULT_LIMIT;
1409
1671
  const result = await ctx.client.activity.list({
1410
- limit: boundActivityLimit(args.limit),
1672
+ limit,
1411
1673
  offset: args.offset,
1412
1674
  workflowId: args.workflowId,
1413
1675
  userId: args.userId,
@@ -1421,7 +1683,15 @@ function registerObservabilityTools(server, ctx, helpers) {
1421
1683
  const events = result.events.map((event) => mapActivityEvent(event, args.includeDetails === true));
1422
1684
  return jsonResult({
1423
1685
  events,
1424
- pagination: result.pagination,
1686
+ pagination: offsetPage({
1687
+ args,
1688
+ param: "offset",
1689
+ offset: args.offset ?? 0,
1690
+ limit,
1691
+ returned: events.length,
1692
+ totalCount: result.pagination?.total,
1693
+ hasMore: result.pagination?.hasMore
1694
+ }),
1425
1695
  ...events.length === 0 ? {
1426
1696
  hint: "No activity matched these filters. Widen relativeTime or drop a filter before concluding nothing happened."
1427
1697
  } : {}
@@ -1430,6 +1700,7 @@ function registerObservabilityTools(server, ctx, helpers) {
1430
1700
  }
1431
1701
  var ACTIVITY_DEFAULT_LIMIT = 25, ACTIVITY_MAX_LIMIT = 200, ACTIVITY_EVENT_TYPES, ACTIVITY_INTERFACES, ACTIVITY_RESOURCE_TYPES, HEALTH_SEVERITY;
1432
1702
  var init_observability = __esm(() => {
1703
+ init_pagination();
1433
1704
  init_schemas();
1434
1705
  ACTIVITY_EVENT_TYPES = [
1435
1706
  "CREATED",
@@ -1470,30 +1741,174 @@ var init_observability = __esm(() => {
1470
1741
  HEALTH_SEVERITY = { down: 0, degraded: 1, idle: 2, healthy: 3 };
1471
1742
  });
1472
1743
 
1744
+ // src/tools/shared/budget.ts
1745
+ function jsonBytes(value) {
1746
+ const text = JSON.stringify(value);
1747
+ return text === undefined ? 0 : Buffer.byteLength(text, "utf8");
1748
+ }
1749
+ function truncateUtf8(text, maxBytes) {
1750
+ if (Buffer.byteLength(text, "utf8") <= maxBytes)
1751
+ return text;
1752
+ const { read } = new TextEncoder().encodeInto(text, new Uint8Array(Math.max(0, maxBytes)));
1753
+ return text.slice(0, read);
1754
+ }
1755
+ function previewValue(value, maxBytes = VALUE_PREVIEW_BYTES) {
1756
+ if (value === undefined || value === null)
1757
+ return null;
1758
+ const bytes = Buffer.byteLength(JSON.stringify(value), "utf8");
1759
+ if (bytes <= maxBytes)
1760
+ return value;
1761
+ if (typeof value === "string")
1762
+ return `${truncateUtf8(value, maxBytes)}… [cut, ${bytes} bytes]`;
1763
+ if (Array.isArray(value))
1764
+ return `[array of ${value.length} items, ${bytes} bytes]`;
1765
+ return `[${typeof value}, ${bytes} bytes]`;
1766
+ }
1767
+ function fitItems(options) {
1768
+ const budget = options.budgetBytes ?? RESULT_BUDGET_BYTES;
1769
+ const all = [...options.items];
1770
+ if (jsonBytes(options.build(all)) <= budget)
1771
+ return { kept: all, omitted: 0, truncated: false };
1772
+ const limit = budget - TRUNCATION_RESERVE_BYTES - (options.reserveBytes ?? 0);
1773
+ let lo = 0;
1774
+ let hi = all.length - 1;
1775
+ while (lo < hi) {
1776
+ const mid = Math.ceil((lo + hi) / 2);
1777
+ if (jsonBytes(options.build(all.slice(0, mid))) <= limit)
1778
+ lo = mid;
1779
+ else
1780
+ hi = mid - 1;
1781
+ }
1782
+ return { kept: all.slice(0, lo), omitted: all.length - lo, truncated: true };
1783
+ }
1784
+ function truncationNotice(options) {
1785
+ const budgetBytes = options.budgetBytes ?? RESULT_BUDGET_BYTES;
1786
+ const total = options.kept + options.omitted;
1787
+ return {
1788
+ truncated: true,
1789
+ truncation: { kept: options.kept, omitted: options.omitted, budgetBytes },
1790
+ hint: `Result cut to ${options.kept} of ${total} ${options.noun} to stay under ${Math.round(budgetBytes / 1000)} KB. ${options.hint}`
1791
+ };
1792
+ }
1793
+ function highlyDivisibleFloor(n) {
1794
+ if (n <= 0)
1795
+ return 0;
1796
+ if (n >= 10)
1797
+ return Math.floor(n / 10) * 10;
1798
+ let p = 1;
1799
+ while (p * 2 <= n)
1800
+ p *= 2;
1801
+ return p;
1802
+ }
1803
+ function alignedKeep(offset, fit) {
1804
+ if (offset === 0)
1805
+ return highlyDivisibleFloor(fit);
1806
+ for (let keep = fit;keep >= 1; keep--) {
1807
+ if (offset % keep === 0 && offset / keep + 2 <= MAX_PAGE)
1808
+ return keep;
1809
+ }
1810
+ return 0;
1811
+ }
1812
+ function resumeNumberedPage(meta, options) {
1813
+ const { next: _replaced, totalPages: _droppedTotalPages, ...rest } = meta;
1814
+ if (options.kept === 0)
1815
+ return { ...rest, hasMore: false };
1816
+ const offset = (options.page - 1) * options.limit;
1817
+ return {
1818
+ ...rest,
1819
+ hasMore: true,
1820
+ next: nextArgs(options.args, { page: offset / options.kept + 2, limit: options.kept })
1821
+ };
1822
+ }
1823
+ function budgetNumberedPage(options) {
1824
+ const offset = (options.page - 1) * options.limit;
1825
+ const fit = fitItems({
1826
+ items: options.items,
1827
+ build: (kept) => options.build(kept, options.pagination),
1828
+ reserveBytes: jsonBytes({ pagination: { next: options.args } })
1829
+ });
1830
+ if (!fit.truncated)
1831
+ return options.build([...options.items], options.pagination);
1832
+ const buildCut = (fitCount) => {
1833
+ const keep = alignedKeep(offset, fitCount);
1834
+ const alignmentImpossible = keep === 0 && fitCount > 0;
1835
+ const pagination = resumeNumberedPage(options.pagination, {
1836
+ args: options.args,
1837
+ page: options.page,
1838
+ limit: options.limit,
1839
+ kept: keep
1840
+ });
1841
+ const hint = keep > 0 ? options.resumeHint : alignmentImpossible ? options.alignmentImpossibleHint ?? DEFAULT_ALIGNMENT_IMPOSSIBLE_HINT : options.emptyHint;
1842
+ const result = {
1843
+ ...options.build(options.items.slice(0, keep), pagination),
1844
+ ...truncationNotice({ kept: keep, omitted: options.items.length - keep, noun: options.noun, hint })
1845
+ };
1846
+ return { result, keep };
1847
+ };
1848
+ let fitCount = fit.kept.length;
1849
+ let { result, keep } = buildCut(fitCount);
1850
+ while (jsonBytes(result) > RESULT_BUDGET_BYTES && keep > 0) {
1851
+ fitCount = keep - 1;
1852
+ ({ result, keep } = buildCut(fitCount));
1853
+ }
1854
+ return result;
1855
+ }
1856
+ var RESULT_BUDGET_BYTES = 80000, TRUNCATION_RESERVE_BYTES = 1024, SCRAPE_CONTENT_BUDGET_BYTES = 70000, VALUE_PREVIEW_BYTES = 1000, DEFAULT_ALIGNMENT_IMPOSSIBLE_HINT = "Page offset too deep to resume inline at this limit; retry with a smaller limit or from an earlier page.";
1857
+ var init_budget = __esm(() => {
1858
+ init_pagination();
1859
+ });
1860
+
1473
1861
  // src/tools/scrape.ts
1474
- import { z as z5 } from "zod";
1862
+ import { z as z7 } from "zod";
1863
+ function scrapeResult(response, format) {
1864
+ const { content, ...metadata } = response;
1865
+ if (typeof content !== "string" || content.length === 0)
1866
+ return textOnlyResult(metadata);
1867
+ const contentBytes = Buffer.byteLength(content, "utf8");
1868
+ if (response.isBase64) {
1869
+ return textOnlyResult({
1870
+ ...metadata,
1871
+ contentBytes,
1872
+ contentOmitted: "binary",
1873
+ hint: `The URL returned binary content (${response.contentType ?? "unknown type"}), which scrape does not return inline.`
1874
+ });
1875
+ }
1876
+ const text = truncateUtf8(content, SCRAPE_CONTENT_BUDGET_BYTES);
1877
+ const keptBytes = Buffer.byteLength(text, "utf8");
1878
+ const cut = keptBytes < contentBytes;
1879
+ const hint = format === "html" ? "Page HTML was cut at 70 KB. Retry with format markdown (much smaller), or use create_workflow to extract structured data from the whole page." : "Page markdown was cut at 70 KB. Use create_workflow to extract structured data from the whole page.";
1880
+ const meta = {
1881
+ ...metadata,
1882
+ contentBytes,
1883
+ ...cut && {
1884
+ truncated: true,
1885
+ truncation: { keptBytes, omittedBytes: contentBytes - keptBytes, budgetBytes: SCRAPE_CONTENT_BUDGET_BYTES },
1886
+ hint
1887
+ }
1888
+ };
1889
+ return withExtraContent(textOnlyResult(meta), { type: "text", text });
1890
+ }
1475
1891
  function registerScrapeTools(server, ctx, helpers) {
1476
1892
  const { withErrorHandling } = helpers;
1477
1893
  server.registerTool("scrape", {
1478
1894
  title: "Scrape URL",
1479
- description: "Immediately fetch one URL as LLM-friendly markdown or raw HTML without creating a workflow. " + "Markdown is the default. Routing is fully automatic: it starts cheap and escalates through stealth tiers on its own when a site blocks bots, and remembers the working method per host. " + "Use create_workflow instead for structured extraction, recurring runs, monitoring, or navigation-heavy jobs.",
1895
+ description: "Immediately fetch one URL as LLM-friendly markdown or raw HTML without creating a workflow. " + "Markdown is the default. Routing is fully automatic: it starts cheap and escalates through stealth tiers on its own when a site blocks bots, and remembers the working method per host. " + "Use create_workflow instead for structured extraction, recurring runs, monitoring, or navigation-heavy jobs. " + "The result is a JSON metadata block followed by the page text as its own block, capped at 70 KB.",
1480
1896
  inputSchema: strictSchema({
1481
- url: z5.url().refine((value) => {
1897
+ url: z7.url().refine((value) => {
1482
1898
  const protocol = new URL(value).protocol;
1483
1899
  return protocol === "http:" || protocol === "https:";
1484
1900
  }, "URL must use HTTP or HTTPS").describe("HTTP or HTTPS URL to fetch"),
1485
- format: z5.enum(["html", "markdown"]).optional().describe("Output format. Defaults to markdown for compact LLM consumption; use html for raw source.")
1901
+ format: z7.enum(["html", "markdown"]).optional().describe("Output format. Defaults to markdown for compact LLM consumption; use html for raw source.")
1486
1902
  }),
1487
1903
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }
1488
1904
  }, withErrorHandling("scrape", async (args) => {
1489
- const response = await ctx.client.scrape.fetch({
1490
- url: args.url,
1491
- format: args.format ?? "markdown"
1492
- });
1493
- return jsonResult(response);
1905
+ const format = args.format ?? "markdown";
1906
+ const response = await ctx.client.scrape.fetch({ url: args.url, format });
1907
+ return scrapeResult(response, format);
1494
1908
  }));
1495
1909
  }
1496
1910
  var init_scrape = __esm(() => {
1911
+ init_budget();
1497
1912
  init_schemas();
1498
1913
  });
1499
1914
 
@@ -1649,7 +2064,7 @@ var init_errors = () => {};
1649
2064
 
1650
2065
  // src/tools/support.ts
1651
2066
  import { KadoaHttpException as KadoaHttpException2 } from "@kadoa/node-sdk";
1652
- import { z as z6 } from "zod";
2067
+ import { z as z8 } from "zod";
1653
2068
  function supportTicketSkipMessage(reason) {
1654
2069
  return (reason && SUPPORT_TICKET_SKIP_MESSAGES[reason]) ?? "No support ticket was filed because this workflow isn't eligible for a support ticket right now.";
1655
2070
  }
@@ -1673,11 +2088,11 @@ function registerSupportTools(server, ctx, helpers) {
1673
2088
  title: "Create Support Ticket",
1674
2089
  description: "File a support ticket for one workflow so the Kadoa team investigates it. " + "Only file after the user has agreed. If the workflow already has an open ticket, that ticket is " + "returned instead of a duplicate. Filing never pauses the workflow.",
1675
2090
  inputSchema: strictSchema({
1676
- workflowId: z6.string().min(1).describe("The workflow the problem is about"),
1677
- title: z6.string().describe("One-line summary of the problem"),
1678
- description: z6.string().describe("What is wrong, what was observed, and what was already tried"),
1679
- category: z6.enum(["bug", "feature", "workflow_issue", "template_issue", "integration", "billing", "data_diff"]).optional().describe("Support category; defaults to a workflow issue"),
1680
- jobId: z6.string().optional().describe("The failing run's id (from list_workflow_runs). Required when filing from the Kadoa Assistant.")
2091
+ workflowId: z8.string().min(1).describe("The workflow the problem is about"),
2092
+ title: z8.string().describe("One-line summary of the problem"),
2093
+ description: z8.string().describe("What is wrong, what was observed, and what was already tried"),
2094
+ category: z8.enum(["bug", "feature", "workflow_issue", "template_issue", "integration", "billing", "data_diff"]).optional().describe("Support category; defaults to a workflow issue"),
2095
+ jobId: z8.string().optional().describe("The failing run's id (from list_workflow_runs). Required when filing from the Kadoa Assistant.")
1681
2096
  }),
1682
2097
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }
1683
2098
  }, withErrorHandling("create_support_ticket", async (args) => {
@@ -1741,7 +2156,7 @@ var init_support = __esm(() => {
1741
2156
  });
1742
2157
 
1743
2158
  // src/tools/team.ts
1744
- import { z as z7 } from "zod";
2159
+ import { z as z9 } from "zod";
1745
2160
  function registerWhoamiTool(server, ctx, helpers) {
1746
2161
  const { withErrorHandling } = helpers;
1747
2162
  server.registerTool("whoami", {
@@ -1803,7 +2218,7 @@ function registerTeamTools(server, ctx, helpers) {
1803
2218
  title: "Switch Team",
1804
2219
  description: "Switch the active team by name or ID. After switching, all subsequent API calls will use the new team context.",
1805
2220
  inputSchema: strictSchema({
1806
- teamIdentifier: z7.string().min(1).describe("Team name or team ID to switch to")
2221
+ teamIdentifier: z9.string().min(1).describe("Team name or team ID to switch to")
1807
2222
  }),
1808
2223
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false }
1809
2224
  }, withErrorHandling("team_switch", async () => errorResult("team_switch is not available for this credential. Reconnect and choose the team at sign-in.")));
@@ -1814,7 +2229,7 @@ var init_team = __esm(() => {
1814
2229
  });
1815
2230
 
1816
2231
  // src/tools/templates.ts
1817
- import { z as z8 } from "zod";
2232
+ import { z as z10 } from "zod";
1818
2233
  function normalizeTemplateSchemaField(val) {
1819
2234
  if (val === null || typeof val !== "object" || Array.isArray(val))
1820
2235
  return val;
@@ -1844,6 +2259,7 @@ function registerTemplateTools(server, ctx, helpers) {
1844
2259
  return jsonResult({
1845
2260
  templates: templates.map(withTemplateDashboardUrl),
1846
2261
  count: templates.length,
2262
+ pagination: completeList(templates.length),
1847
2263
  instructions: "To instantiate a listed template, call create_workflow with the matching templateId, source URLs, optional templateVersion, and optional prompt containing only workflow-specific additional instructions. Do not copy the template prompt, entity, or schema into the call."
1848
2264
  });
1849
2265
  }));
@@ -1851,7 +2267,7 @@ function registerTemplateTools(server, ctx, helpers) {
1851
2267
  title: "Get Template",
1852
2268
  description: "Get a template by ID, including all published versions and their schemas. After inspecting it, instantiate the template with create_workflow using templateId, source URLs, optional templateVersion, and optional prompt containing only workflow-specific additional instructions. Never copy the returned prompt, entity, or schema into the call - those shared values are inherited when templateId is passed.",
1853
2269
  inputSchema: strictSchema({
1854
- templateId: z8.string().min(1).describe("The template ID")
2270
+ templateId: z10.string().min(1).describe("The template ID")
1855
2271
  }),
1856
2272
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
1857
2273
  }, withErrorHandling("get_template", async (args) => {
@@ -1865,8 +2281,8 @@ function registerTemplateTools(server, ctx, helpers) {
1865
2281
  title: "Create Template",
1866
2282
  description: "Create a new template. After creation, use create_template_version to publish a version with prompt, schema, and notifications.",
1867
2283
  inputSchema: strictSchema({
1868
- name: z8.string().describe("Template name (1-255 chars)"),
1869
- description: z8.string().optional().describe("Template description (max 2000 chars)")
2284
+ name: z10.string().describe("Template name (1-255 chars)"),
2285
+ description: z10.string().optional().describe("Template description (max 2000 chars)")
1870
2286
  }),
1871
2287
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }
1872
2288
  }, withErrorHandling("create_template", async (args) => {
@@ -1886,9 +2302,9 @@ function registerTemplateTools(server, ctx, helpers) {
1886
2302
 
1887
2303
  ` + "This tool CANNOT change what linked workflows inherit - prompt, schema, validation rules, notifications and frequency all live in template *versions*, which are immutable snapshots. " + "To change any of those, publish a new version with `create_template_version` (the version IS the edit), then roll it out to linked workflows with `apply_template_version`. " + "There is no in-place edit of a published version, and never report a prompt or schema change as impossible - versioning is the supported path.",
1888
2304
  inputSchema: strictSchema({
1889
- templateId: z8.string().min(1).describe("The template ID to update"),
1890
- name: z8.preprocess(coerceNull(), z8.string().optional()).optional().describe("New template name"),
1891
- description: z8.preprocess(coerceNull(), z8.string().optional()).optional().describe("New template description")
2305
+ templateId: z10.string().min(1).describe("The template ID to update"),
2306
+ name: z10.preprocess(coerceNull(), z10.string().optional()).optional().describe("New template name"),
2307
+ description: z10.preprocess(coerceNull(), z10.string().optional()).optional().describe("New template description")
1892
2308
  }),
1893
2309
  annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false }
1894
2310
  }, withErrorHandling("update_template", async (args) => {
@@ -1904,8 +2320,8 @@ function registerTemplateTools(server, ctx, helpers) {
1904
2320
  title: "Delete Template",
1905
2321
  description: "Delete (archive) a template. " + "You MUST first call this tool without confirmed=true to preview what will be deleted, " + "then show the user the template name and ask for confirmation, " + "then call again with confirmed=true only after the user explicitly agrees.",
1906
2322
  inputSchema: strictSchema({
1907
- templateId: z8.string().min(1).describe("The template ID to delete"),
1908
- confirmed: z8.preprocess(coerceBoolean(), z8.boolean()).optional().describe("Set to true only after the user has explicitly confirmed deletion. Omit or set to false for the initial preview call.")
2323
+ templateId: z10.string().min(1).describe("The template ID to delete"),
2324
+ confirmed: z10.preprocess(coerceBoolean(), z10.boolean()).optional().describe("Set to true only after the user has explicitly confirmed deletion. Omit or set to false for the initial preview call.")
1909
2325
  }),
1910
2326
  annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false }
1911
2327
  }, withErrorHandling("delete_template", async (args) => {
@@ -1929,15 +2345,15 @@ function registerTemplateTools(server, ctx, helpers) {
1929
2345
  });
1930
2346
  }));
1931
2347
  const TemplateSchemaFieldShape = {
1932
- name: z8.string().describe("Field name"),
1933
- description: z8.string().optional().describe("Field description"),
1934
- fieldType: z8.enum(["SCHEMA", "CLASSIFICATION"]).optional().describe("Field role: SCHEMA (default) for extracted data, CLASSIFICATION for a field with predefined categories"),
1935
- example: z8.string().optional().describe("Example value"),
2348
+ name: z10.string().describe("Field name"),
2349
+ description: z10.string().optional().describe("Field description"),
2350
+ fieldType: z10.enum(["SCHEMA", "CLASSIFICATION"]).optional().describe("Field role: SCHEMA (default) for extracted data, CLASSIFICATION for a field with predefined categories"),
2351
+ example: z10.string().optional().describe("Example value"),
1936
2352
  dataType: SchemaFieldDataTypeSchema.optional().describe("Data type for SCHEMA fields"),
1937
- isKey: z8.preprocess(coerceBoolean(), z8.boolean()).optional().describe("Whether the field is a key field"),
1938
- categories: z8.preprocess(coerceArray(), z8.array(z8.object({
1939
- title: z8.string().describe("Short title/label of the category"),
1940
- definition: z8.string().describe("Full definition/description of the category")
2353
+ isKey: z10.preprocess(coerceBoolean(), z10.boolean()).optional().describe("Whether the field is a key field"),
2354
+ categories: z10.preprocess(coerceArray(), z10.array(z10.object({
2355
+ title: z10.string().describe("Short title/label of the category"),
2356
+ definition: z10.string().describe("Full definition/description of the category")
1941
2357
  }))).optional().describe("Predefined categories for a CLASSIFICATION field ({title, definition}[]). Required for fieldType=CLASSIFICATION; omitted otherwise.")
1942
2358
  };
1943
2359
  async function latestTemplateVersionRules(templateId) {
@@ -1962,25 +2378,25 @@ function registerTemplateTools(server, ctx, helpers) {
1962
2378
 
1963
2379
  ` + "THIS IS HOW YOU EDIT A TEMPLATE'S PROMPT OR SCHEMA. Published versions are immutable, so 'changing the template prompt' means publishing a new version here and then calling `apply_template_version` to push it onto linked workflows. " + "`update_template` only renames a template; it cannot touch prompt or schema.",
1964
2380
  inputSchema: withoutDefIds(strictSchema({
1965
- templateId: z8.string().min(1).describe("The template ID to publish a version for"),
1966
- prompt: z8.string().optional().describe("User prompt to copy into workflow config"),
1967
- schemaId: z8.string().optional().describe("Existing schema ID to reference (mutually exclusive with schemaFields)"),
1968
- schemaFields: z8.preprocess(coerceArray(), z8.array(z8.preprocess(normalizeTemplateSchemaField, z8.object(TemplateSchemaFieldShape).strict()))).optional().describe("Inline schema fields to create a new schema (mutually exclusive with schemaId)"),
1969
- schemaEntity: z8.string().optional().describe("Entity name for the inline schema"),
1970
- schemaValidationRules: z8.preprocess((val) => stripSchemaValidationAttribution(coerceJson()(val)), SchemaValidationRulesSchema).optional().describe('Per-field schema validation rules keyed by field name. Each rule uses kind STRING, NUMBER, OTHER, OBJECT, or ARRAY and nested rules are supported. Edit attribution is stamped server-side, so send only rule values. Example: { "price": { "kind": "NUMBER", "minimum": { "value": 0 } } }. Not inherited from prior versions - omitting this on a new version drops any rules the previous version had.'),
1971
- notifications: z8.preprocess(coerceArray(), z8.array(z8.object({
1972
- eventType: z8.string().describe("Notification event type"),
1973
- eventConfiguration: z8.preprocess(coerceJson(), z8.record(z8.string(), z8.unknown())).optional(),
1974
- enabled: z8.preprocess(coerceBoolean(), z8.boolean()).optional(),
1975
- channelIds: z8.preprocess(coerceArray(), z8.array(z8.string().min(1))).optional(),
1976
- channels: z8.preprocess(coerceArray(), z8.array(z8.object({ channelId: z8.string() }))).optional()
2381
+ templateId: z10.string().min(1).describe("The template ID to publish a version for"),
2382
+ prompt: z10.string().optional().describe("User prompt to copy into workflow config"),
2383
+ schemaId: z10.string().optional().describe("Existing schema ID to reference (mutually exclusive with schemaFields)"),
2384
+ schemaFields: z10.preprocess(coerceArray(), z10.array(z10.preprocess(normalizeTemplateSchemaField, z10.object(TemplateSchemaFieldShape).strict()))).optional().describe("Inline schema fields to create a new schema (mutually exclusive with schemaId)"),
2385
+ schemaEntity: z10.string().optional().describe("Entity name for the inline schema"),
2386
+ schemaValidationRules: z10.preprocess((val) => stripSchemaValidationAttribution(coerceJson()(val)), SchemaValidationRulesSchema).optional().describe('Per-field schema validation rules keyed by field name. Each rule uses kind STRING, NUMBER, OTHER, OBJECT, or ARRAY and nested rules are supported. Edit attribution is stamped server-side, so send only rule values. Example: { "price": { "kind": "NUMBER", "minimum": { "value": 0 } } }. Not inherited from prior versions - omitting this on a new version drops any rules the previous version had.'),
2387
+ notifications: z10.preprocess(coerceArray(), z10.array(z10.object({
2388
+ eventType: z10.string().describe("Notification event type"),
2389
+ eventConfiguration: z10.preprocess(coerceJson(), z10.record(z10.string(), z10.unknown())).optional(),
2390
+ enabled: z10.preprocess(coerceBoolean(), z10.boolean()).optional(),
2391
+ channelIds: z10.preprocess(coerceArray(), z10.array(z10.string().min(1))).optional(),
2392
+ channels: z10.preprocess(coerceArray(), z10.array(z10.object({ channelId: z10.string() }))).optional()
1977
2393
  }))).optional().describe("Notification configuration for this version"),
1978
- frequency: z8.preprocess(coerceJson(), z8.object({
1979
- interval: z8.enum([...UpdateIntervalSchema.options, "REAL_TIME"]).optional().describe("Scheduling interval the template controls for linked workflows. Use CUSTOM with 'schedules' for cron."),
1980
- schedules: z8.preprocess(coerceArray(), z8.array(z8.object({
1981
- type: z8.enum(["repeat", "cron"]).describe("'repeat' (interval token) or 'cron' (cron expressions)"),
1982
- interval: z8.string().optional().describe("Repeat token (e.g. '1h', '1d') when type='repeat'"),
1983
- expressions: z8.preprocess(coerceArray(true), z8.array(z8.string())).optional().describe("Cron expressions when type='cron' (e.g. '0 8 * * 2')")
2394
+ frequency: z10.preprocess(coerceJson(), z10.object({
2395
+ interval: z10.enum([...UpdateIntervalSchema.options, "REAL_TIME"]).optional().describe("Scheduling interval the template controls for linked workflows. Use CUSTOM with 'schedules' for cron."),
2396
+ schedules: z10.preprocess(coerceArray(), z10.array(z10.object({
2397
+ type: z10.enum(["repeat", "cron"]).describe("'repeat' (interval token) or 'cron' (cron expressions)"),
2398
+ interval: z10.string().optional().describe("Repeat token (e.g. '1h', '1d') when type='repeat'"),
2399
+ expressions: z10.preprocess(coerceArray(true), z10.array(z10.string())).optional().describe("Cron expressions when type='cron' (e.g. '0 8 * * 2')")
1984
2400
  }))).optional().describe("Schedule specs. Required when interval='CUSTOM'.")
1985
2401
  })).optional().describe("Template-controlled scheduling for linked workflows ({interval, schedules}). Linked workflows inherit and cannot override this when set.")
1986
2402
  })),
@@ -2010,11 +2426,11 @@ function registerTemplateTools(server, ctx, helpers) {
2010
2426
  title: "Save Workflow as Template",
2011
2427
  description: "Create a template from an existing workflow's configuration (prompt, schema, notifications). " + "Provide 'name' to create a new template, or 'templateId' to add a new version to an existing template. " + "Use 'includeParts' to save only specific parts of the configuration.",
2012
2428
  inputSchema: strictSchema({
2013
- workflowId: z8.string().min(1).describe("Source workflow ID to extract config from"),
2014
- name: z8.string().optional().describe("Name for the new template (required if templateId is not set)"),
2015
- description: z8.string().optional().describe("Description for the new template"),
2016
- templateId: z8.string().optional().describe("Existing template ID to add a new version to (mutually exclusive with name)"),
2017
- includeParts: z8.preprocess(coerceArray(), z8.array(z8.enum(["prompt", "schema", "schemaValidationRules", "notifications", "frequency"])).optional()).describe("Which parts of the workflow config to include: prompt, schema, schemaValidationRules, notifications, frequency. Omit to include all. At least 1 required if provided.")
2429
+ workflowId: z10.string().min(1).describe("Source workflow ID to extract config from"),
2430
+ name: z10.string().optional().describe("Name for the new template (required if templateId is not set)"),
2431
+ description: z10.string().optional().describe("Description for the new template"),
2432
+ templateId: z10.string().optional().describe("Existing template ID to add a new version to (mutually exclusive with name)"),
2433
+ includeParts: z10.preprocess(coerceArray(), z10.array(z10.enum(["prompt", "schema", "schemaValidationRules", "notifications", "frequency"])).optional()).describe("Which parts of the workflow config to include: prompt, schema, schemaValidationRules, notifications, frequency. Omit to include all. At least 1 required if provided.")
2018
2434
  }),
2019
2435
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }
2020
2436
  }, withErrorHandling("save_workflow_as_template", async (args) => {
@@ -2035,20 +2451,20 @@ function registerTemplateTools(server, ctx, helpers) {
2035
2451
  title: "List Template Schemas",
2036
2452
  description: "List schemas associated with a template.",
2037
2453
  inputSchema: strictSchema({
2038
- templateId: z8.string().min(1).describe("The template ID")
2454
+ templateId: z10.string().min(1).describe("The template ID")
2039
2455
  }),
2040
2456
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
2041
2457
  }, withErrorHandling("list_template_schemas", async (args) => {
2042
2458
  const schemas = await ctx.client.template.listSchemas(args.templateId);
2043
- return jsonResult({ schemas, count: schemas.length });
2459
+ return jsonResult({ schemas, count: schemas.length, pagination: completeList(schemas.length) });
2044
2460
  }));
2045
2461
  server.registerTool("link_template_to_workflows", {
2046
2462
  title: "Link Template to Workflows",
2047
2463
  description: "Link one or more EXISTING workflows to a template in a single call. " + "This is the bulk equivalent of creating a workflow with `templateId`: linked workflows adopt the template's configuration (prompt, schema, notifications) and stay in sync with it. " + "The template ENFORCES its schema on linked workflows - their extracted output conforms to the template's declared field names, so this is the way to make many workflows produce a consistent, canonical schema. " + "Use `list_templates`/`get_template` to find the template, and `list_workflows` to find the workflow IDs. " + "Set `force: true` to relink workflows already linked to a different template.",
2048
2464
  inputSchema: strictSchema({
2049
- templateId: z8.string().min(1).describe("The template ID to link workflows to"),
2050
- workflowIds: z8.preprocess(coerceArray(true), z8.array(z8.string().min(1)).min(1)).describe("Workflow IDs to link to the template (array of strings). Also accepts a single ID string."),
2051
- force: z8.preprocess(coerceBoolean(), z8.boolean()).optional().describe("If true, relink workflows that are already linked to another template. Defaults to false.")
2465
+ templateId: z10.string().min(1).describe("The template ID to link workflows to"),
2466
+ workflowIds: z10.preprocess(coerceArray(true), z10.array(z10.string().min(1)).min(1)).describe("Workflow IDs to link to the template (array of strings). Also accepts a single ID string."),
2467
+ force: z10.preprocess(coerceBoolean(), z10.boolean()).optional().describe("If true, relink workflows that are already linked to another template. Defaults to false.")
2052
2468
  }),
2053
2469
  annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false }
2054
2470
  }, withErrorHandling("link_template_to_workflows", async (args) => {
@@ -2075,9 +2491,9 @@ function registerTemplateTools(server, ctx, helpers) {
2075
2491
  title: "Apply Template Version",
2076
2492
  description: "Push a template version onto its linked workflows. Updates the given workflows to the template's `targetVersion`, applying the template-controlled parts (prompt, schema, validation rules, notifications, frequency). " + "Use this after publishing a new template version to bring linked workflows up to date (see list_template_workflows for which workflows are outdated). " + "Returns how many workflows were updated and which parts the template controls.",
2077
2493
  inputSchema: strictSchema({
2078
- templateId: z8.string().min(1).describe("The template ID whose version to apply"),
2079
- targetVersion: z8.preprocess(coerceNumber(), z8.number()).describe("The template version number to apply to the workflows"),
2080
- workflowIds: z8.preprocess(coerceArray(true), z8.array(z8.string().min(1)).min(1)).describe("Workflow IDs to update to the target version (array of strings). Also accepts a single ID string.")
2494
+ templateId: z10.string().min(1).describe("The template ID whose version to apply"),
2495
+ targetVersion: z10.preprocess(coerceNumber(), z10.number()).describe("The template version number to apply to the workflows"),
2496
+ workflowIds: z10.preprocess(coerceArray(true), z10.array(z10.string().min(1)).min(1)).describe("Workflow IDs to update to the target version (array of strings). Also accepts a single ID string.")
2081
2497
  }),
2082
2498
  annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false }
2083
2499
  }, withErrorHandling("apply_template_version", async (args) => {
@@ -2097,19 +2513,19 @@ function registerTemplateTools(server, ctx, helpers) {
2097
2513
  title: "List Template Workflows",
2098
2514
  description: "List the workflows linked to a template, with each workflow's pinned template version and an `isOutdated` flag (true when a newer template version exists). " + "Use this to verify which workflows are bound to a template and to find ones that need apply_template_version.",
2099
2515
  inputSchema: strictSchema({
2100
- templateId: z8.string().min(1).describe("The template ID")
2516
+ templateId: z10.string().min(1).describe("The template ID")
2101
2517
  }),
2102
2518
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
2103
2519
  }, withErrorHandling("list_template_workflows", async (args) => {
2104
2520
  const workflows = await ctx.client.template.getLinkedWorkflows(args.templateId);
2105
- return jsonResult({ workflows, count: workflows.length });
2521
+ return jsonResult({ workflows, count: workflows.length, pagination: completeList(workflows.length) });
2106
2522
  }));
2107
2523
  server.registerTool("unlink_template_from_workflows", {
2108
2524
  title: "Unlink Template from Workflows",
2109
2525
  description: "Unlink one or more workflows from a template in a single call. " + "Unlinked workflows keep their current configuration but stop tracking the template (future template version changes no longer apply to them).",
2110
2526
  inputSchema: strictSchema({
2111
- templateId: z8.string().min(1).describe("The template ID to unlink workflows from"),
2112
- workflowIds: z8.preprocess(coerceArray(true), z8.array(z8.string().min(1)).min(1)).describe("Workflow IDs to unlink from the template (array of strings). Also accepts a single ID string.")
2527
+ templateId: z10.string().min(1).describe("The template ID to unlink workflows from"),
2528
+ workflowIds: z10.preprocess(coerceArray(true), z10.array(z10.string().min(1)).min(1)).describe("Workflow IDs to unlink from the template (array of strings). Also accepts a single ID string.")
2113
2529
  }),
2114
2530
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false }
2115
2531
  }, withErrorHandling("unlink_template_from_workflows", async (args) => {
@@ -2126,21 +2542,22 @@ function registerTemplateTools(server, ctx, helpers) {
2126
2542
  }
2127
2543
  var LEGACY_DATA_TYPE_ALIASES;
2128
2544
  var init_templates = __esm(() => {
2545
+ init_pagination();
2129
2546
  init_schemas();
2130
2547
  LEGACY_DATA_TYPE_ALIASES = { TEXT: "STRING", URL: "LINK" };
2131
2548
  });
2132
2549
 
2133
2550
  // src/tools/variables.ts
2134
- import { z as z9 } from "zod";
2551
+ import { z as z11 } from "zod";
2135
2552
  function registerVariableTools(server, ctx, helpers) {
2136
2553
  const { withErrorHandling } = helpers;
2137
2554
  server.registerTool("create_variable", {
2138
2555
  title: "Create Variable",
2139
2556
  description: "Create a new variable. Variables are key-value pairs that can be referenced in workflow prompts using @variableKey syntax. Keys must be unique within the team scope.",
2140
2557
  inputSchema: strictSchema({
2141
- key: z9.string().describe("Variable key (must be unique within scope). Referenced in prompts as @key."),
2142
- value: z9.string().describe("Variable value"),
2143
- dataType: z9.enum(["STRING", "SECRET", "OBJECT", "ARRAY"]).optional().describe("Data type of the value (defaults to STRING)")
2558
+ key: z11.string().describe("Variable key (must be unique within scope). Referenced in prompts as @key."),
2559
+ value: z11.string().describe("Variable value"),
2560
+ dataType: z11.enum(["STRING", "SECRET", "OBJECT", "ARRAY"]).optional().describe("Data type of the value (defaults to STRING)")
2144
2561
  }),
2145
2562
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }
2146
2563
  }, withErrorHandling("create_variable", async (args) => {
@@ -2164,14 +2581,15 @@ function registerVariableTools(server, ctx, helpers) {
2164
2581
  const variables = await ctx.client.variable.list();
2165
2582
  return jsonResult({
2166
2583
  variables: variables.map(withVariableDashboardUrl),
2167
- count: variables.length
2584
+ count: variables.length,
2585
+ pagination: completeList(variables.length)
2168
2586
  });
2169
2587
  }));
2170
2588
  server.registerTool("get_variable", {
2171
2589
  title: "Get Variable",
2172
2590
  description: "Get a specific variable by its ID.",
2173
2591
  inputSchema: strictSchema({
2174
- variableId: z9.string().min(1).describe("The variable ID")
2592
+ variableId: z11.string().min(1).describe("The variable ID")
2175
2593
  }),
2176
2594
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
2177
2595
  }, withErrorHandling("get_variable", async (args) => {
@@ -2182,10 +2600,10 @@ function registerVariableTools(server, ctx, helpers) {
2182
2600
  title: "Update Variable",
2183
2601
  description: "Update an existing variable. At least one field must be provided. If the variable key is referenced in workflow prompts (@variableKey), those workflows will automatically use the new value on next run.",
2184
2602
  inputSchema: strictSchema({
2185
- variableId: z9.string().min(1).describe("The variable ID to update"),
2186
- key: z9.string().optional().describe("New variable key"),
2187
- value: z9.string().optional().describe("New variable value"),
2188
- dataType: z9.enum(["STRING", "SECRET", "OBJECT", "ARRAY"]).optional().describe("New data type")
2603
+ variableId: z11.string().min(1).describe("The variable ID to update"),
2604
+ key: z11.string().optional().describe("New variable key"),
2605
+ value: z11.string().optional().describe("New variable value"),
2606
+ dataType: z11.enum(["STRING", "SECRET", "OBJECT", "ARRAY"]).optional().describe("New data type")
2189
2607
  }),
2190
2608
  annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false }
2191
2609
  }, withErrorHandling("update_variable", async (args) => {
@@ -2201,8 +2619,8 @@ function registerVariableTools(server, ctx, helpers) {
2201
2619
  title: "Delete Variable",
2202
2620
  description: "Delete a variable permanently. This is irreversible. " + "You MUST first call this tool without confirmed=true to preview what will be deleted, " + "then show the user the variable key and ask for confirmation, " + "then call again with confirmed=true only after the user explicitly agrees.",
2203
2621
  inputSchema: strictSchema({
2204
- variableId: z9.string().min(1).describe("The variable ID to delete"),
2205
- confirmed: z9.preprocess(coerceBoolean(), z9.boolean()).optional().describe("Set to true only after the user has explicitly confirmed deletion. Omit or set to false for the initial preview call.")
2622
+ variableId: z11.string().min(1).describe("The variable ID to delete"),
2623
+ confirmed: z11.preprocess(coerceBoolean(), z11.boolean()).optional().describe("Set to true only after the user has explicitly confirmed deletion. Omit or set to false for the initial preview call.")
2206
2624
  }),
2207
2625
  annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false }
2208
2626
  }, withErrorHandling("delete_variable", async (args) => {
@@ -2227,12 +2645,13 @@ function registerVariableTools(server, ctx, helpers) {
2227
2645
  }));
2228
2646
  }
2229
2647
  var init_variables = __esm(() => {
2648
+ init_pagination();
2230
2649
  init_schemas();
2231
2650
  });
2232
2651
 
2233
2652
  // src/tools/workflows.ts
2234
2653
  import { WorkflowStatusFilter } from "@kadoa/node-sdk";
2235
- import { z as z10 } from "zod";
2654
+ import { z as z12 } from "zod";
2236
2655
  function isRealTimeInterval(interval) {
2237
2656
  return interval?.toUpperCase().replace("-", "_") === "REAL_TIME";
2238
2657
  }
@@ -2289,6 +2708,49 @@ function mapRunStatus(state) {
2289
2708
  return state ?? "unknown";
2290
2709
  }
2291
2710
  }
2711
+ function changeResult(change) {
2712
+ const build = (data, differences) => ({
2713
+ id: change.id,
2714
+ workflowId: change.workflowId,
2715
+ url: change.url,
2716
+ summary: change.summary,
2717
+ screenshotUrl: change.screenshotUrl,
2718
+ data,
2719
+ differences,
2720
+ createdAt: change.createdAt
2721
+ });
2722
+ const full = build(change.data, change.differences);
2723
+ if (jsonBytes(full) <= RESULT_BUDGET_BYTES)
2724
+ return full;
2725
+ const rows = change.data ?? [];
2726
+ const exportHint = `For the full current dataset use export_data with workflowId ${change.workflowId}.`;
2727
+ if (jsonBytes(build([], change.differences)) <= RESULT_BUDGET_BYTES - TRUNCATION_RESERVE_BYTES) {
2728
+ const fit = fitItems({ items: rows, build: (kept) => build(kept, change.differences) });
2729
+ if (!fit.truncated)
2730
+ return build(fit.kept, change.differences);
2731
+ return {
2732
+ ...build(fit.kept, change.differences),
2733
+ ...truncationNotice({
2734
+ kept: fit.kept.length,
2735
+ omitted: fit.omitted,
2736
+ noun: "data rows",
2737
+ hint: `differences is complete. ${exportHint}`
2738
+ })
2739
+ };
2740
+ }
2741
+ const fit = fitItems({ items: change.differences ?? [], build: (kept) => build([], kept) });
2742
+ if (!fit.truncated)
2743
+ return build([], fit.kept);
2744
+ return {
2745
+ ...build([], fit.kept),
2746
+ ...truncationNotice({
2747
+ kept: fit.kept.length,
2748
+ omitted: fit.omitted,
2749
+ noun: "differences",
2750
+ hint: `All ${rows.length} data rows omitted. ${exportHint}`
2751
+ })
2752
+ };
2753
+ }
2292
2754
  function jsonEq(a, b) {
2293
2755
  return JSON.stringify(a) === JSON.stringify(b);
2294
2756
  }
@@ -2302,12 +2764,31 @@ function diffWorkflowAuditEntry(entry) {
2302
2764
  if (!jsonEq(prev[key], next[key]))
2303
2765
  changed.push(key);
2304
2766
  }
2305
- const prevPrompt = prev["additionalData"]?.["userPrompt"];
2306
- const nextPrompt = next["additionalData"]?.["userPrompt"];
2307
- if (!jsonEq(prevPrompt, nextPrompt))
2767
+ if (!jsonEq(auditFieldValue(prev, "additionalData.userPrompt"), auditFieldValue(next, "additionalData.userPrompt"))) {
2308
2768
  changed.push("additionalData.userPrompt");
2769
+ }
2309
2770
  return changed;
2310
2771
  }
2772
+ function auditFieldValue(snapshot, field) {
2773
+ if (field === "additionalData.userPrompt") {
2774
+ return snapshot["additionalData"]?.["userPrompt"];
2775
+ }
2776
+ return snapshot[field];
2777
+ }
2778
+ function summarizeWorkflowAuditEntry(entry) {
2779
+ const changedFields = diffWorkflowAuditEntry(entry);
2780
+ const prev = entry.previousValue ?? {};
2781
+ const next = entry.newValue ?? {};
2782
+ const changes = changedFields.map((field) => ({
2783
+ field,
2784
+ before: previewValue(auditFieldValue(prev, field)),
2785
+ after: previewValue(auditFieldValue(next, field))
2786
+ }));
2787
+ return { changedFields, changes };
2788
+ }
2789
+ function historyEmptyHint(includeSnapshots) {
2790
+ return includeSnapshots ? "A single entry is too large to return inline. Retry without includeSnapshots for the diff summary." : "A single entry is too large to return inline even without snapshots. Retry with a narrower page (e.g. limit=1) or fewer fields.";
2791
+ }
2311
2792
  function registerWorkflowCoreTools(server, ctx, helpers) {
2312
2793
  const { withErrorHandling } = helpers;
2313
2794
  function resolveUrls(args) {
@@ -2359,7 +2840,7 @@ function registerWorkflowCoreTools(server, ctx, helpers) {
2359
2840
  const channels = {};
2360
2841
  if (n.email) {
2361
2842
  if (typeof n.email === "object" && n.email.recipients?.length) {
2362
- channels.EMAIL = { name: "mcp-email", recipients: n.email.recipients };
2843
+ channels.EMAIL = { name: mcpChannelName("EMAIL", n.email), recipients: n.email.recipients };
2363
2844
  } else {
2364
2845
  channels.EMAIL = true;
2365
2846
  }
@@ -2368,7 +2849,7 @@ function registerWorkflowCoreTools(server, ctx, helpers) {
2368
2849
  channels.WEBSOCKET = true;
2369
2850
  if (n.webhook) {
2370
2851
  const webhookChannel = {
2371
- name: "mcp-webhook",
2852
+ name: mcpChannelName("WEBHOOK", { webhookUrl: n.webhook.url }),
2372
2853
  webhookUrl: n.webhook.url,
2373
2854
  httpMethod: n.webhook.httpMethod || "POST"
2374
2855
  };
@@ -2377,7 +2858,7 @@ function registerWorkflowCoreTools(server, ctx, helpers) {
2377
2858
  channels.WEBHOOK = webhookChannel;
2378
2859
  }
2379
2860
  if (n.slack) {
2380
- const slackChannel = { name: "mcp-slack" };
2861
+ const slackChannel = { name: mcpChannelName("SLACK", n.slack) };
2381
2862
  if (n.slack.webhookUrl)
2382
2863
  slackChannel.webhookUrl = n.slack.webhookUrl;
2383
2864
  if (n.slack.slackChannelId)
@@ -2421,13 +2902,13 @@ function registerWorkflowCoreTools(server, ctx, helpers) {
2421
2902
  prompt: extractionPromptInput.describe('Natural language description of what to extract (e.g., "Extract product prices and names"). Required unless templateId is provided. With templateId, use it only for workflow-specific instructions to add to the template prompt.'),
2422
2903
  ...extractionInputShape,
2423
2904
  ...urlInputShape,
2424
- templateId: z10.string().optional().describe("Instantiate this workflow from a published template. Pass the templateId, source urls, optional templateVersion, and optional prompt with workflow-specific additional instructions. Do not copy the template's shared prompt, entity, or schema into this call - they are inherited from the published template version, and the workflow's output conforms to its declared schema. Discover templates via list_templates and get_template."),
2425
- templateVersion: z10.preprocess(coerceNumber(), z10.number()).optional().describe("Specific published template version (integer) to instantiate. Defaults to the latest published version when templateId is set."),
2426
- description: z10.string().max(500).optional().describe("Description of what this workflow does (max 500 characters)"),
2427
- tags: z10.preprocess(coerceArray(true), z10.array(z10.string())).optional().describe("Tags for organizing workflows"),
2428
- limit: z10.preprocess(coerceNumber(), z10.number()).optional().describe("Maximum number of records to extract per run. Useful for limiting scope or cost control."),
2905
+ templateId: z12.string().optional().describe("Instantiate this workflow from a published template. Pass the templateId, source urls, optional templateVersion, and optional prompt with workflow-specific additional instructions. Do not copy the template's shared prompt, entity, or schema into this call - they are inherited from the published template version, and the workflow's output conforms to its declared schema. Discover templates via list_templates and get_template."),
2906
+ templateVersion: z12.preprocess(coerceNumber(), z12.number()).optional().describe("Specific published template version (integer) to instantiate. Defaults to the latest published version when templateId is set."),
2907
+ description: z12.string().max(500).optional().describe("Description of what this workflow does (max 500 characters)"),
2908
+ tags: z12.preprocess(coerceArray(true), z12.array(z12.string())).optional().describe("Tags for organizing workflows"),
2909
+ limit: z12.preprocess(coerceNumber(), z12.number()).optional().describe("Maximum number of records to extract per run. Useful for limiting scope or cost control."),
2429
2910
  updateInterval: UpdateIntervalSchema.optional().describe("How often the workflow runs. Defaults to ONLY_ONCE. Use CUSTOM with the 'schedules' field for cron-based scheduling."),
2430
- schedules: z10.preprocess(coerceArray(true), z10.array(z10.string())).optional().describe("Cron expressions for CUSTOM updateInterval (e.g. '0 8 * * 2' for Tuesdays at 8am UTC). Requires updateInterval='CUSTOM'."),
2911
+ schedules: z12.preprocess(coerceArray(true), z12.array(z12.string())).optional().describe("Cron expressions for CUSTOM updateInterval (e.g. '0 8 * * 2' for Tuesdays at 8am UTC). Requires updateInterval='CUSTOM'."),
2431
2912
  ...notificationsInputShape
2432
2913
  }),
2433
2914
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }
@@ -2441,13 +2922,25 @@ function registerWorkflowCoreTools(server, ctx, helpers) {
2441
2922
  }
2442
2923
  const n = args.notifications;
2443
2924
  const hasNotifications = !!(n && (n.email || n.webhook || n.slack || n.websocket));
2925
+ async function createOrUnknown(create) {
2926
+ try {
2927
+ return await create();
2928
+ } catch (error) {
2929
+ if (!isAmbiguousFailure(error))
2930
+ throw error;
2931
+ console.error("[Tool Error] create_workflow:", safeErrorSummary(error));
2932
+ return {
2933
+ outcomeUnknown: errorResult(`Workflow creation failed (${shortErrorReason(error)}). It is unknown whether the workflow was created. Check list_workflows for it before retrying, and do NOT create it again if it exists.`)
2934
+ };
2935
+ }
2936
+ }
2444
2937
  let workflowId;
2445
2938
  const warnings = [];
2446
2939
  if (args.templateId) {
2447
2940
  if (args.entity || args.schema) {
2448
2941
  return errorResult("When 'templateId' is set, 'entity' and 'schema' must NOT be supplied - they are inherited from the template version. Use 'prompt' only for workflow-specific additional instructions.");
2449
2942
  }
2450
- const { id } = await ctx.client.workflow.create({
2943
+ const created = await createOrUnknown(() => ctx.client.workflow.create({
2451
2944
  urls,
2452
2945
  name: args.name || "Untitled Workflow",
2453
2946
  description: args.description,
@@ -2458,8 +2951,10 @@ function registerWorkflowCoreTools(server, ctx, helpers) {
2458
2951
  templateId: args.templateId,
2459
2952
  templateVersion: args.templateVersion,
2460
2953
  userPrompt: args.prompt
2461
- });
2462
- workflowId = id;
2954
+ }));
2955
+ if ("outcomeUnknown" in created)
2956
+ return created.outcomeUnknown;
2957
+ workflowId = created.id;
2463
2958
  } else {
2464
2959
  if (!args.prompt) {
2465
2960
  return errorResult("'prompt' is required unless 'templateId' is provided.");
@@ -2473,7 +2968,9 @@ function registerWorkflowCoreTools(server, ctx, helpers) {
2473
2968
  description: args.description,
2474
2969
  schedules: args.schedules
2475
2970
  });
2476
- const workflow = await builder.create();
2971
+ const workflow = await createOrUnknown(() => builder.create());
2972
+ if ("outcomeUnknown" in workflow)
2973
+ return workflow.outcomeUnknown;
2477
2974
  workflowId = workflow.workflowId;
2478
2975
  const needsUpdate = args.limit !== undefined || args.tags && args.tags.length > 0;
2479
2976
  if (needsUpdate) {
@@ -2546,19 +3043,10 @@ function registerWorkflowCoreTools(server, ctx, helpers) {
2546
3043
  } else {
2547
3044
  text = `${cause}. The realtime monitor was not created. ${channels} Delete one with delete_notification_channel only after confirming via list_notification_settings that no other workflow uses it.`;
2548
3045
  }
2549
- return {
2550
- content: [
2551
- {
2552
- type: "text",
2553
- text: JSON.stringify({
2554
- error: text,
2555
- notificationChannelIds,
2556
- ...workflowId ? { workflowId, dashboardUrl: workflowDashboardUrl(workflowId) } : {}
2557
- }, null, 2)
2558
- }
2559
- ],
2560
- isError: true
2561
- };
3046
+ return errorResult(text, {
3047
+ notificationChannelIds,
3048
+ ...workflowId ? { workflowId, dashboardUrl: workflowDashboardUrl(workflowId) } : {}
3049
+ });
2562
3050
  }
2563
3051
  server.registerTool("create_realtime_monitor", {
2564
3052
  title: "Create Realtime Monitor",
@@ -2569,8 +3057,8 @@ function registerWorkflowCoreTools(server, ctx, helpers) {
2569
3057
  prompt: extractionPromptInput.describe("Optional natural language instructions describing what to monitor and which changes matter. If omitted, the Assistant determines what to watch."),
2570
3058
  ...extractionInputShape,
2571
3059
  ...urlInputShape,
2572
- description: z10.string().max(500).optional().describe("Description of what this monitor watches (max 500 characters)"),
2573
- tags: z10.preprocess(coerceArray(true), z10.array(z10.string())).optional().describe("Tags for organizing monitors"),
3060
+ description: z12.string().max(500).optional().describe("Description of what this monitor watches (max 500 characters)"),
3061
+ tags: z12.preprocess(coerceArray(true), z12.array(z12.string())).optional().describe("Tags for organizing monitors"),
2574
3062
  ...requiredNotificationsInputShape
2575
3063
  }),
2576
3064
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }
@@ -2621,20 +3109,27 @@ function registerWorkflowCoreTools(server, ctx, helpers) {
2621
3109
  title: "List Workflows",
2622
3110
  description: `List workflows with customer-visible status, raw workflow and run states, Assistant session availability, template version drift, realtime freshness or health, and awaitingUserInput when the requesting user must act. Uses one paginated list request; use get_workflow only when you need full details for a selected workflow. To answer "what needs my attention", pass statusFilters: ["group:attention"]; this returns exactly the dashboard's "Needs your attention" group. In group:attention, a row with awaitingUserInput.prompt is an Assistant question: use get_workflow_assistant then answer_workflow_assistant_question. A row with awaitingUserInput but no prompt is a setup the user left unfinished: use get_workflow_assistant to see where it stopped, then resume_workflow_assistant or request_workflow_update. A row with displayState PREVIEW and no awaitingUserInput is preview data: use approve_workflow. Workflows with an open support ticket are excluded from group:attention and appear under group:support.`,
2623
3111
  inputSchema: strictSchema({
2624
- limit: z10.preprocess(coerceNumber(), z10.number()).optional().describe("Maximum number of workflows to return"),
2625
- skip: z10.preprocess(coerceNumber(), z10.number()).optional().describe("Number of workflows to skip (pagination offset; pairs with limit)"),
2626
- state: z10.enum(["ACTIVE", "FAILED", "PAUSED", "PREVIEW"]).optional().describe("Filter by state (ACTIVE, FAILED, PAUSED, PREVIEW)"),
2627
- search: z10.string().optional().describe("Free-text filter matching workflow name, URL, or workflow ID"),
2628
- tags: z10.array(z10.string()).optional().describe("Filter to workflows having any of these tags"),
2629
- monitoring: z10.preprocess(coerceBoolean(), z10.boolean()).optional().describe("Filter by monitoring/real-time status"),
2630
- updateInterval: z10.enum(["HOURLY", "DAILY", "WEEKLY", "MONTHLY", "REAL_TIME"]).optional().describe("Filter by scheduled update frequency or realtime workflows"),
2631
- templateId: z10.string().optional().describe("Filter to workflows instantiated from this template ID"),
2632
- statusFilters: z10.array(z10.enum(Object.values(WorkflowStatusFilter))).optional().describe("Dashboard category filters. group:attention = workflows waiting on the requesting user (Assistant question, unfinished setup, or preview approval). Other groups: working, support, failed, active, complete, paused.")
3112
+ limit: limitInput({
3113
+ noun: "Workflows",
3114
+ defaultLimit: LIST_WORKFLOWS_DEFAULT_LIMIT,
3115
+ max: LIST_WORKFLOWS_MAX_LIMIT
3116
+ }),
3117
+ skip: offsetInput("skip", "workflows"),
3118
+ state: z12.enum(["ACTIVE", "FAILED", "PAUSED", "PREVIEW"]).optional().describe("Filter by state (ACTIVE, FAILED, PAUSED, PREVIEW)"),
3119
+ search: z12.string().optional().describe("Free-text filter matching workflow name, URL, or workflow ID"),
3120
+ tags: z12.array(z12.string()).optional().describe("Filter to workflows having any of these tags"),
3121
+ monitoring: z12.preprocess(coerceBoolean(), z12.boolean()).optional().describe("Filter by monitoring/real-time status"),
3122
+ updateInterval: z12.enum(["HOURLY", "DAILY", "WEEKLY", "MONTHLY", "REAL_TIME"]).optional().describe("Filter by scheduled update frequency or realtime workflows"),
3123
+ templateId: z12.string().optional().describe("Filter to workflows instantiated from this template ID"),
3124
+ statusFilters: z12.array(z12.enum(Object.values(WorkflowStatusFilter))).optional().describe("Dashboard category filters. group:attention = workflows waiting on the requesting user (Assistant question, unfinished setup, or preview approval). Other groups: working, support, failed, active, complete, paused.")
2633
3125
  }),
3126
+ outputSchema: ListWorkflowsOutput,
2634
3127
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
2635
3128
  }, withErrorHandling("list_workflows", async (args) => {
2636
- const workflows = await ctx.client.workflow.list({
2637
- limit: args.limit || 50,
3129
+ const limit = args.limit ?? LIST_WORKFLOWS_DEFAULT_LIMIT;
3130
+ const skip = args.skip ?? 0;
3131
+ const rows = await ctx.client.workflow.list({
3132
+ limit: limit + 1,
2638
3133
  state: args.state,
2639
3134
  ...args.skip !== undefined && { skip: args.skip },
2640
3135
  ...args.search !== undefined && { search: args.search },
@@ -2644,6 +3139,7 @@ function registerWorkflowCoreTools(server, ctx, helpers) {
2644
3139
  ...args.templateId !== undefined && { templateId: args.templateId },
2645
3140
  ...args.statusFilters !== undefined && { statusFilters: args.statusFilters }
2646
3141
  });
3142
+ const workflows = rows.slice(0, limit);
2647
3143
  return jsonResult({
2648
3144
  workflows: workflows.map((w) => ({
2649
3145
  id: w.id,
@@ -2680,15 +3176,23 @@ function registerWorkflowCoreTools(server, ctx, helpers) {
2680
3176
  },
2681
3177
  awaitingUserInput: w.awaitingUserInput ?? null
2682
3178
  })),
2683
- total: workflows.length
3179
+ pagination: offsetPage({
3180
+ args,
3181
+ param: "skip",
3182
+ offset: skip,
3183
+ limit,
3184
+ returned: workflows.length,
3185
+ hasMore: rows.length > limit
3186
+ })
2684
3187
  });
2685
3188
  }));
2686
3189
  server.registerTool("get_workflow", {
2687
3190
  title: "Get Workflow",
2688
3191
  description: "Get canonical workflow details, including Assistant/session linkage, extraction intent, template-controlled parts, stale-data state, job/run state, schedule timezone, location, monitoring, validation, and realtime health. For Assistant-built workflows, extractionSpec - not the legacy prompt - is authoritative.",
2689
3192
  inputSchema: strictSchema({
2690
- workflowId: z10.string().min(1).describe("The workflow ID")
3193
+ workflowId: z12.string().min(1).describe("The workflow ID")
2691
3194
  }),
3195
+ outputSchema: GetWorkflowOutput,
2692
3196
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
2693
3197
  }, withErrorHandling("get_workflow", async (args) => {
2694
3198
  const workflow = await ctx.client.workflow.get(args.workflowId);
@@ -2705,9 +3209,9 @@ function registerWorkflowCoreTools(server, ctx, helpers) {
2705
3209
  const hasTemplate = workflow.templateId != null || workflow.templateVersion != null;
2706
3210
  const isRealtime = workflow.isRealTime === true || isRealTimeInterval(workflow.updateInterval);
2707
3211
  return jsonResult({
2708
- id: workflow.id,
3212
+ id: workflow.id ?? args.workflowId,
2709
3213
  name: workflow.name,
2710
- dashboardUrl: workflowDashboardUrl(workflow.id),
3214
+ dashboardUrl: workflowDashboardUrl(workflow.id ?? args.workflowId),
2711
3215
  description: workflow.description,
2712
3216
  status: deriveStatusLabel(workflow),
2713
3217
  state: workflow.state ?? null,
@@ -2775,9 +3279,9 @@ function registerWorkflowCoreTools(server, ctx, helpers) {
2775
3279
  title: "Request Workflow Update",
2776
3280
  description: "Request an Assistant-driven change to an existing workflow while preserving its workflowId. " + "Use for extraction intent, navigation, pagination, data sourcing, generated-script changes, and repair after a site redesign. " + "Use update_workflow instead for deterministic metadata such as name, schedule, location, tags, and limits. " + "For template-controlled prompt or schema changes, create and apply a template version instead. " + "The update is asynchronous: do not poll or sleep-wait; use get_workflow_assistant later. Never delete and recreate the workflow as a workaround.",
2777
3281
  inputSchema: strictSchema({
2778
- workflowId: z10.string().min(1).describe("The existing workflow ID; it will not change"),
2779
- instructions: z10.string().min(1).describe("Natural-language instructions for the workflow Assistant"),
2780
- threadId: z10.string().optional().describe("Optional Assistant thread ID to continue")
3282
+ workflowId: z12.string().min(1).describe("The existing workflow ID; it will not change"),
3283
+ instructions: z12.string().min(1).describe("Natural-language instructions for the workflow Assistant"),
3284
+ threadId: z12.string().optional().describe("Optional Assistant thread ID to continue")
2781
3285
  }),
2782
3286
  annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true }
2783
3287
  }, withErrorHandling("request_workflow_update", async (args) => {
@@ -2805,69 +3309,102 @@ function registerWorkflowHistoryTools(server, ctx, helpers) {
2805
3309
  const { withErrorHandling } = helpers;
2806
3310
  server.registerTool("get_workflow_history", {
2807
3311
  title: "Get Workflow History",
2808
- description: "Get the configuration revision history (audit log) for a workflow. Each entry captures who changed the workflow, when, from which channel (UI/API/SDK/MCP/CLI/SYSTEM), and a `changedFields` list summarizing what changed between revisions. Use this to answer questions like 'did this workflow's prompt or schema change, when, and from what to what?'. Note: CREATE entries return null `previousValue`/`newValue` and an empty `changedFields` (no initial-state snapshot is captured).",
3312
+ description: "Get the configuration revision history (audit log) for a workflow. Each entry captures who changed the workflow, when, from which channel (UI/API/SDK/MCP/CLI/SYSTEM), a `changedFields` list, and `changes` with the before/after value of each changed field (values over 1 KB are summarized). Use this to answer questions like 'did this workflow's prompt or schema change, when, and from what to what?'. Pass includeSnapshots: true only when you need the full previousValue/newValue workflow snapshots (large). Note: CREATE entries have empty `changedFields`/`changes` and null snapshots (no initial-state snapshot is captured).",
2809
3313
  inputSchema: strictSchema({
2810
- workflowId: z10.string().min(1).describe("The workflow ID"),
2811
- page: z10.preprocess(coerceNumber(), z10.number()).optional().describe("Page number, 1-based (default: 1)"),
2812
- limit: z10.preprocess(coerceNumber(), z10.number()).optional().describe("Entries per page (default: 25)")
3314
+ workflowId: z12.string().min(1).describe("The workflow ID"),
3315
+ page: pageInput(),
3316
+ limit: limitInput({
3317
+ noun: "Entries",
3318
+ defaultLimit: WORKFLOW_HISTORY_DEFAULT_LIMIT,
3319
+ max: WORKFLOW_HISTORY_MAX_LIMIT
3320
+ }),
3321
+ includeSnapshots: z12.preprocess(coerceBoolean(), z12.boolean()).optional().describe("Include full previousValue/newValue workflow snapshots per entry (default false; large)")
2813
3322
  }),
2814
3323
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
2815
3324
  }, withErrorHandling("get_workflow_history", async (args) => {
2816
- const response = await ctx.client.workflow.getAuditLog(args.workflowId, {
2817
- page: args.page,
2818
- limit: args.limit
2819
- });
2820
- const entries = (response.logEntries ?? []).map((e) => ({
2821
- id: e.id,
2822
- operationType: e.operationType,
2823
- createdAt: e.createdAt,
2824
- userEmail: e.userEmail,
2825
- userId: e.userId,
2826
- requestSource: e.requestSource,
2827
- authMethod: e.authMethod,
2828
- changedFields: diffWorkflowAuditEntry({
3325
+ const page = args.page ?? 1;
3326
+ const limit = args.limit ?? WORKFLOW_HISTORY_DEFAULT_LIMIT;
3327
+ const response = await ctx.client.workflow.getAuditLog(args.workflowId, { page, limit });
3328
+ const includeSnapshots = args.includeSnapshots === true;
3329
+ const entries = (response.logEntries ?? []).map((e) => {
3330
+ const summary = summarizeWorkflowAuditEntry({
2829
3331
  previousValue: e.previousValue,
2830
3332
  newValue: e.newValue
2831
- }),
2832
- previousValue: e.previousValue,
2833
- newValue: e.newValue
2834
- }));
2835
- return jsonResult({
2836
- workflowId: response.id,
2837
- totalCount: response.pagination?.totalCount ?? response.logEntriesCount ?? entries.length,
2838
- page: response.pagination?.page ?? 1,
2839
- totalPages: response.pagination?.totalPages ?? 1,
2840
- entries
3333
+ });
3334
+ return {
3335
+ id: e.id,
3336
+ operationType: e.operationType,
3337
+ createdAt: e.createdAt,
3338
+ userEmail: e.userEmail,
3339
+ userId: e.userId,
3340
+ requestSource: e.requestSource,
3341
+ authMethod: e.authMethod,
3342
+ changedFields: summary.changedFields,
3343
+ changes: summary.changes,
3344
+ ...includeSnapshots && { previousValue: e.previousValue ?? null, newValue: e.newValue ?? null }
3345
+ };
3346
+ });
3347
+ const totalCount = response.pagination?.totalCount ?? response.logEntriesCount;
3348
+ const totalPages = response.pagination?.totalPages;
3349
+ const result = budgetNumberedPage({
3350
+ items: entries,
3351
+ pagination: numberedPage({ args, page, limit, returned: entries.length, totalCount, totalPages }),
3352
+ args,
3353
+ page,
3354
+ limit,
3355
+ noun: "entries",
3356
+ resumeHint: includeSnapshots ? "Call again with pagination.next for the following entries, or drop includeSnapshots to get diff summaries only." : "Call again with pagination.next for the following entries.",
3357
+ emptyHint: historyEmptyHint(includeSnapshots),
3358
+ build: (kept, meta) => ({
3359
+ workflowId: response.id,
3360
+ ...totalCount !== undefined && { totalCount },
3361
+ page,
3362
+ ...totalPages !== undefined && kept.length === entries.length && { totalPages },
3363
+ entries: kept,
3364
+ pagination: meta
3365
+ })
2841
3366
  });
3367
+ return jsonResult(result);
2842
3368
  }));
2843
3369
  server.registerTool("list_workflow_runs", {
2844
3370
  title: "List Workflow Runs",
2845
3371
  description: "List a workflow's execution run history - each run's status, start/finish time, " + "record count, and errors. Use to answer 'when did this last succeed?' " + "(status='success', limit=1) or 'recent success/failure pattern?'. Distinct from " + "get_workflow_history, which is the config audit log. `status` is normally " + "success | failed | in_progress; a run whose backend state this server does not " + "recognize reports that raw state verbatim, so treat any other value as unknown " + "rather than as a failure.",
2846
3372
  inputSchema: strictSchema({
2847
- workflowId: z10.string().min(1).describe("The workflow ID"),
2848
- status: z10.enum(["success", "failed", "in_progress"]).optional().describe("Filter runs by outcome"),
2849
- page: z10.preprocess(coerceNumber(), z10.number()).optional().describe("Page number, 1-based (default: 1)"),
2850
- limit: z10.preprocess(coerceNumber(), z10.number()).optional().describe("Runs per page (default: 25)")
3373
+ workflowId: z12.string().min(1).describe("The workflow ID"),
3374
+ status: z12.enum(["success", "failed", "in_progress"]).optional().describe("Filter runs by outcome"),
3375
+ page: pageInput(),
3376
+ limit: limitInput({ noun: "Runs", defaultLimit: WORKFLOW_RUNS_DEFAULT_LIMIT, max: WORKFLOW_RUNS_MAX_LIMIT })
2851
3377
  }),
2852
3378
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
2853
3379
  }, withErrorHandling("list_workflow_runs", async (args) => {
3380
+ const page = args.page ?? 1;
3381
+ const limit = args.limit ?? WORKFLOW_RUNS_DEFAULT_LIMIT;
2854
3382
  const response = await ctx.client.workflow.listWorkflowRuns(args.workflowId, {
2855
- page: args.page,
2856
- limit: args.limit,
3383
+ page,
3384
+ limit,
2857
3385
  status: args.status
2858
3386
  });
3387
+ const runs = (response.workflowRuns ?? []).map((r) => ({
3388
+ id: r.id,
3389
+ status: mapRunStatus(r.state),
3390
+ startedAt: r.startedAt,
3391
+ finishedAt: r.finishedAt,
3392
+ records: r.records,
3393
+ errors: r.errors
3394
+ }));
3395
+ const totalCount = response.totalCount ?? response.pagination?.totalCount;
2859
3396
  return jsonResult({
2860
3397
  workflowId: response.workflowId,
2861
- runs: (response.workflowRuns ?? []).map((r) => ({
2862
- id: r.id,
2863
- status: mapRunStatus(r.state),
2864
- startedAt: r.startedAt,
2865
- finishedAt: r.finishedAt,
2866
- records: r.records,
2867
- errors: r.errors
2868
- })),
2869
- totalCount: response.totalCount,
2870
- pagination: response.pagination
3398
+ runs,
3399
+ ...totalCount !== undefined && { totalCount },
3400
+ pagination: numberedPage({
3401
+ args,
3402
+ page,
3403
+ limit,
3404
+ returned: runs.length,
3405
+ totalCount,
3406
+ totalPages: response.pagination?.totalPages
3407
+ })
2871
3408
  });
2872
3409
  }));
2873
3410
  }
@@ -2877,8 +3414,8 @@ function registerWorkflowRunTools(server, ctx, helpers) {
2877
3414
  title: "Run Workflow",
2878
3415
  description: "Run a workflow to extract fresh data, including retrying a failed one-time or scheduled workflow. Preserve the existing workflow ID and configuration - do not delete and recreate the workflow solely to retry it. The run is asynchronous and may take several minutes. Do NOT poll or sleep-wait for completion. Return the workflow ID to the user and let them check status with get_workflow or fetch results later with fetch_data. Realtime workflows cannot be manually run.",
2879
3416
  inputSchema: strictSchema({
2880
- workflowId: z10.string().min(1).describe("The workflow ID to run"),
2881
- limit: z10.preprocess(coerceNumber(), z10.number()).optional().describe("Maximum number of records to extract (default: 1000)")
3417
+ workflowId: z12.string().min(1).describe("The workflow ID to run"),
3418
+ limit: z12.preprocess(coerceNumber(), z12.number()).optional().describe("Maximum number of records to extract (default: 1000)")
2882
3419
  }),
2883
3420
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }
2884
3421
  }, withErrorHandling("run_workflow", async (args) => {
@@ -2901,49 +3438,72 @@ function registerWorkflowRunTools(server, ctx, helpers) {
2901
3438
  title: "Fetch Workflow Data",
2902
3439
  description: "Get a PAGE of extracted data from a workflow. Use ONLY for previews, sorted/filtered slices, or explicit 'first N rows' / 'top N' queries (capped at 500 rows per call). Do NOT use this to retrieve a full dataset, 'all rows', or anything the user wants to analyze in Excel / pandas / duckdb - use export_data for those. Data is only available after the workflow run has completed (status is no longer 'Running' or 'Validating'). Do NOT poll or sleep-wait for completion.",
2903
3440
  inputSchema: strictSchema({
2904
- workflowId: z10.string().min(1).describe("The workflow ID"),
2905
- limit: z10.preprocess(coerceNumber(), z10.number()).optional().describe(`Maximum number of records to return. Default ${FETCH_DATA_DEFAULT_LIMIT}, max ${FETCH_DATA_MAX_LIMIT}.`),
2906
- page: z10.preprocess(coerceNumber(), z10.number()).optional().describe("Page number for pagination (1-based)"),
3441
+ workflowId: z12.string().min(1).describe("The workflow ID"),
3442
+ limit: limitInput({
3443
+ noun: "Rows",
3444
+ defaultLimit: FETCH_DATA_DEFAULT_LIMIT,
3445
+ max: FETCH_DATA_MAX_LIMIT,
3446
+ overMaxHint: "Use export_data for full datasets."
3447
+ }),
3448
+ page: pageInput(),
2907
3449
  filters: dataFiltersInput,
2908
- sortBy: z10.string().optional().describe("Field name to sort by"),
2909
- order: z10.enum(["asc", "desc"]).optional().describe("Sort order: asc or desc")
3450
+ sortBy: z12.string().optional().describe("Field name to sort by"),
3451
+ order: z12.enum(["asc", "desc"]).optional().describe("Sort order: asc or desc")
2910
3452
  }),
3453
+ outputSchema: FetchDataOutput,
2911
3454
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
2912
3455
  }, withErrorHandling("fetch_data", async (args) => {
2913
- const requestedLimit = args.limit ?? FETCH_DATA_DEFAULT_LIMIT;
2914
- if (requestedLimit > FETCH_DATA_MAX_LIMIT) {
2915
- return errorResult(`limit ${requestedLimit} exceeds max ${FETCH_DATA_MAX_LIMIT}. Use export_data for full datasets.`);
2916
- }
3456
+ const limit = args.limit ?? FETCH_DATA_DEFAULT_LIMIT;
3457
+ const page = args.page ?? 1;
2917
3458
  const data = await ctx.client.extraction.fetchData({
2918
3459
  workflowId: args.workflowId,
2919
- limit: requestedLimit,
2920
- page: args.page || 1,
3460
+ limit,
3461
+ page,
2921
3462
  filters: args.filters === undefined ? undefined : JSON.stringify(args.filters),
2922
3463
  sortBy: args.sortBy,
2923
3464
  order: args.order
2924
3465
  });
2925
- const totalCount = data.pagination?.totalCount ?? data.data.length;
2926
- const result = {
2927
- data: data.data,
2928
- pagination: data.pagination,
2929
- totalCount
2930
- };
2931
- if (data.data.length < totalCount) {
2932
- result.hint = `Showing ${data.data.length} of ${totalCount} rows. Use export_data for the full dataset.`;
2933
- }
3466
+ const totalCount = data.pagination?.totalCount;
3467
+ const pagination = numberedPage({
3468
+ args,
3469
+ page,
3470
+ limit,
3471
+ returned: data.data.length,
3472
+ totalCount,
3473
+ totalPages: data.pagination?.totalPages
3474
+ });
3475
+ const result = budgetNumberedPage({
3476
+ items: data.data,
3477
+ pagination,
3478
+ args,
3479
+ page,
3480
+ limit,
3481
+ noun: "rows",
3482
+ resumeHint: "Call fetch_data again with pagination.next for the following rows, or use export_data for the full dataset.",
3483
+ emptyHint: "Rows are too large to return inline. Use export_data for this data.",
3484
+ alignmentImpossibleHint: "Page offset too deep to resume inline; use export_data instead.",
3485
+ build: (rows, meta) => ({
3486
+ data: rows,
3487
+ pagination: meta,
3488
+ ...totalCount !== undefined && { totalCount },
3489
+ ...totalCount !== undefined && rows.length < totalCount && {
3490
+ hint: `Showing ${rows.length} of ${totalCount} rows. Use export_data for the full dataset.`
3491
+ }
3492
+ })
3493
+ });
2934
3494
  return jsonResult(result);
2935
3495
  }));
2936
3496
  server.registerTool("export_data", {
2937
3497
  title: "Export Workflow Data",
2938
3498
  description: "PREFERRED tool for retrieving a workflow's FULL dataset. Materializes the data to object storage and returns a signed download URL. Use this whenever the user wants 'all rows', 'the full dataset', 'everything', an export, or anything destined for Excel / pandas / duckdb / a CSV file - even for small workflows. The URL is self-authenticating (open with `fetch(url)`, no Authorization header). Use fetch_data ONLY when the user explicitly asks for a small preview slice (e.g., 'first 10', 'top N sorted by X').",
2939
3499
  inputSchema: strictSchema({
2940
- workflowId: z10.string().min(1).describe("The workflow ID"),
2941
- format: z10.enum(["csv", "json"]).optional().describe("Export format. Default 'csv'."),
2942
- runId: z10.string().optional().describe("Specific run ID to export. Defaults to the latest completed run."),
3500
+ workflowId: z12.string().min(1).describe("The workflow ID"),
3501
+ format: z12.enum(["csv", "json"]).optional().describe("Export format. Default 'csv'."),
3502
+ runId: z12.string().optional().describe("Specific run ID to export. Defaults to the latest completed run."),
2943
3503
  filters: dataFiltersInput,
2944
- sortBy: z10.string().optional().describe("Field name to sort by"),
2945
- order: z10.enum(["asc", "desc"]).optional().describe("Sort order: asc or desc"),
2946
- rowIds: z10.preprocess(coerceList(), z10.array(z10.string().trim().min(1)).max(1000)).optional().describe("Row ids to include (max 1000)")
3504
+ sortBy: z12.string().optional().describe("Field name to sort by"),
3505
+ order: z12.enum(["asc", "desc"]).optional().describe("Sort order: asc or desc"),
3506
+ rowIds: z12.preprocess(coerceList(), z12.array(z12.string().trim().min(1)).max(1000)).optional().describe("Row ids to include (max 1000)")
2947
3507
  }),
2948
3508
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
2949
3509
  }, withErrorHandling("export_data", async (args) => {
@@ -2956,7 +3516,7 @@ function registerWorkflowRunTools(server, ctx, helpers) {
2956
3516
  order: args.order,
2957
3517
  rowIds: joinList(args.rowIds)
2958
3518
  });
2959
- return jsonResult({
3519
+ return withExtraContent(jsonResult({
2960
3520
  url: result.url,
2961
3521
  rowCount: result.rowCount,
2962
3522
  format: result.format,
@@ -2964,26 +3524,34 @@ function registerWorkflowRunTools(server, ctx, helpers) {
2964
3524
  workflowId: result.workflowId,
2965
3525
  runId: result.runId,
2966
3526
  hint: `Full dataset ready at the URL above. Open in Excel via 'Get Data from Web' (no auth needed), ` + `or load with code execution (pandas.read_csv(url) / pandas.read_json(url) / duckdb.read_csv(url)). ` + `URL expires ${result.expiresAt}.`
3527
+ }), {
3528
+ type: "resource_link",
3529
+ uri: result.url,
3530
+ name: `${result.workflowId}-${result.runId}.${result.format}`,
3531
+ title: `Workflow export (${result.rowCount} rows)`,
3532
+ mimeType: result.format === "json" ? "application/json" : "text/csv",
3533
+ description: `Signed download URL, no auth needed. Expires ${result.expiresAt}.`
2967
3534
  });
2968
3535
  }));
2969
3536
  server.registerTool("list_changes", {
2970
3537
  title: "List Detected Changes",
2971
3538
  description: "List detected data changes across one or more realtime monitoring workflows. " + "Returns structured diffs showing added, removed, and changed records. " + "Only works for workflows with realtime monitoring enabled.",
2972
3539
  inputSchema: strictSchema({
2973
- workflowIds: z10.preprocess(coerceArray(true), z10.array(z10.string().min(1))).optional().describe("Workflow IDs to filter by. If omitted, returns changes for all ACTIVE monitoring workflows."),
2974
- startDate: z10.string().optional().describe("Start date filter (ISO format, e.g. 2025-01-01)"),
2975
- endDate: z10.string().optional().describe("End date filter (ISO format, e.g. 2025-12-31)"),
2976
- skip: z10.preprocess(coerceNumber(), z10.number()).optional().describe("Number of records to skip for pagination"),
2977
- limit: z10.preprocess(coerceNumber(), z10.number()).optional().describe("Maximum number of changes to return")
3540
+ workflowIds: z12.preprocess(coerceArray(true), z12.array(z12.string().min(1))).optional().describe("Workflow IDs to filter by. If omitted, returns changes for all ACTIVE monitoring workflows."),
3541
+ startDate: z12.string().optional().describe("Start date filter (ISO format, e.g. 2025-01-01)"),
3542
+ endDate: z12.string().optional().describe("End date filter (ISO format, e.g. 2025-12-31)"),
3543
+ skip: offsetInput("skip", "changes"),
3544
+ limit: limitInput({ noun: "Changes", defaultLimit: LIST_CHANGES_DEFAULT_LIMIT, max: LIST_CHANGES_MAX_LIMIT })
2978
3545
  }),
2979
3546
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
2980
3547
  }, withErrorHandling("list_changes", async (args) => {
3548
+ const limit = args.limit ?? LIST_CHANGES_DEFAULT_LIMIT;
2981
3549
  const result = await ctx.client.changes.list({
2982
3550
  workflowIds: args.workflowIds?.join(","),
2983
3551
  startDate: args.startDate,
2984
3552
  endDate: args.endDate,
2985
3553
  skip: args.skip,
2986
- limit: args.limit ?? 25
3554
+ limit
2987
3555
  });
2988
3556
  return jsonResult({
2989
3557
  changes: result.changes.map((c) => ({
@@ -2994,36 +3562,34 @@ function registerWorkflowRunTools(server, ctx, helpers) {
2994
3562
  differences: c.differences,
2995
3563
  createdAt: c.createdAt
2996
3564
  })),
2997
- pagination: result.pagination,
3565
+ pagination: offsetPage({
3566
+ args,
3567
+ param: "skip",
3568
+ offset: args.skip ?? 0,
3569
+ limit,
3570
+ returned: result.changes.length,
3571
+ totalCount: result.pagination?.totalCount
3572
+ }),
2998
3573
  changesCount: result.changesCount
2999
3574
  });
3000
3575
  }));
3001
3576
  server.registerTool("get_change", {
3002
3577
  title: "Get Detected Change",
3003
- description: "Get detailed information about a specific detected change, including the full data snapshot and diff.",
3578
+ description: "Get detailed information about a specific detected change, including the data snapshot and diff. Results over ~80 KB are cut to whole rows and marked truncated.",
3004
3579
  inputSchema: strictSchema({
3005
- changeId: z10.string().min(1).describe("The change ID")
3580
+ changeId: z12.string().min(1).describe("The change ID")
3006
3581
  }),
3007
3582
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
3008
3583
  }, withErrorHandling("get_change", async (args) => {
3009
3584
  const change = await ctx.client.changes.get(args.changeId);
3010
- return jsonResult({
3011
- id: change.id,
3012
- workflowId: change.workflowId,
3013
- url: change.url,
3014
- summary: change.summary,
3015
- screenshotUrl: change.screenshotUrl,
3016
- data: change.data,
3017
- differences: change.differences,
3018
- createdAt: change.createdAt
3019
- });
3585
+ return jsonResult(changeResult(change));
3020
3586
  }));
3021
3587
  server.registerTool("delete_workflow", {
3022
3588
  title: "Delete Workflow",
3023
3589
  description: "Delete a workflow permanently. This is irreversible. " + "You MUST first call this tool without confirmed=true to preview what will be deleted, " + "then show the user the workflow name and ask for confirmation, " + "then call again with confirmed=true only after the user explicitly agrees.",
3024
3590
  inputSchema: strictSchema({
3025
- workflowId: z10.string().min(1).describe("The workflow ID to delete"),
3026
- confirmed: z10.preprocess(coerceBoolean(), z10.boolean()).optional().describe("Set to true only after the user has explicitly confirmed deletion. Omit or set to false for the initial preview call.")
3591
+ workflowId: z12.string().min(1).describe("The workflow ID to delete"),
3592
+ confirmed: z12.preprocess(coerceBoolean(), z12.boolean()).optional().describe("Set to true only after the user has explicitly confirmed deletion. Omit or set to false for the initial preview call.")
3027
3593
  }),
3028
3594
  annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false }
3029
3595
  }, withErrorHandling("delete_workflow", async (args) => {
@@ -3053,7 +3619,7 @@ function registerWorkflowRunTools(server, ctx, helpers) {
3053
3619
  title: "Approve Workflow",
3054
3620
  description: "Approve and activate a workflow that is in PREVIEW, PAUSED, or ERROR state. Transitions the workflow to ACTIVE so it can be run.",
3055
3621
  inputSchema: strictSchema({
3056
- workflowId: z10.string().min(1).describe("The workflow ID to approve/activate")
3622
+ workflowId: z12.string().min(1).describe("The workflow ID to approve/activate")
3057
3623
  }),
3058
3624
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }
3059
3625
  }, withErrorHandling("approve_workflow", async (args) => {
@@ -3068,7 +3634,7 @@ function registerWorkflowRunTools(server, ctx, helpers) {
3068
3634
  title: "Pause Workflow",
3069
3635
  description: "Pause an ACTIVE workflow so it stops running on its schedule. Requires the workflow to be in ACTIVE state - pausing a workflow that is currently running, already paused, or in PREVIEW will return an error. Use approve_workflow to resume a paused workflow.",
3070
3636
  inputSchema: strictSchema({
3071
- workflowId: z10.string().min(1).describe("The workflow ID to pause")
3637
+ workflowId: z12.string().min(1).describe("The workflow ID to pause")
3072
3638
  }),
3073
3639
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }
3074
3640
  }, withErrorHandling("pause_workflow", async (args) => {
@@ -3087,20 +3653,20 @@ function registerWorkflowRunTools(server, ctx, helpers) {
3087
3653
 
3088
3654
  ` + "ASSISTANT-OWNED INTENT: Use request_workflow_update - not `userPrompt` - for Assistant-built workflow changes to extraction intent, navigation, pagination, data sourcing, repair, or generated scripts. The API may reject `userPrompt` because these workflows' canonical intent is owned by the Assistant. " + "Call get_workflow first. If its template.controlledParts contains the setting, create and apply a template version instead of overriding the workflow directly. NEVER delete and recreate a workflow to work around an update limitation - that changes workflowId, breaks downstream tables/connectors, and discards history.",
3089
3655
  inputSchema: strictSchema({
3090
- workflowId: z10.string().min(1).describe("The workflow ID to update"),
3091
- name: z10.string().optional().describe("New name for the workflow"),
3092
- urls: z10.preprocess(coerceArray(true), z10.array(z10.string()).min(1)).optional().describe("New target URLs for the workflow (array of strings). Also accepts a single URL string."),
3093
- entity: z10.string().optional().describe("Entity name for extraction (e.g., 'Product', 'Job Posting')"),
3094
- schema: z10.preprocess(coerceArray(), z10.array(z10.object(SchemaFieldShape).strict())).optional().describe("New extraction schema fields. This REPLACES the whole schema - list every field you want to keep, each with its isKey flag, or the omitted ones and their flags are lost. Call get_workflow first and edit the schema it returns."),
3095
- description: z10.string().max(500).optional().describe("Workflow description (max 500 characters)"),
3096
- tags: z10.preprocess(coerceArray(true), z10.array(z10.string())).optional().describe("Tags for organizing workflows"),
3097
- userPrompt: z10.string().optional().describe("Navigation prompt for agentic-navigation mode (10-5000 characters)"),
3656
+ workflowId: z12.string().min(1).describe("The workflow ID to update"),
3657
+ name: z12.string().optional().describe("New name for the workflow"),
3658
+ urls: z12.preprocess(coerceArray(true), z12.array(z12.string()).min(1)).optional().describe("New target URLs for the workflow (array of strings). Also accepts a single URL string."),
3659
+ entity: z12.string().optional().describe("Entity name for extraction (e.g., 'Product', 'Job Posting')"),
3660
+ schema: z12.preprocess(coerceArray(), z12.array(z12.object(SchemaFieldShape).strict())).optional().describe("New extraction schema fields. This REPLACES the whole schema - list every field you want to keep, each with its isKey flag, or the omitted ones and their flags are lost. Call get_workflow first and edit the schema it returns."),
3661
+ description: z12.string().max(500).optional().describe("Workflow description (max 500 characters)"),
3662
+ tags: z12.preprocess(coerceArray(true), z12.array(z12.string())).optional().describe("Tags for organizing workflows"),
3663
+ userPrompt: z12.string().optional().describe("Navigation prompt for agentic-navigation mode (10-5000 characters)"),
3098
3664
  updateInterval: UpdateIntervalSchema.optional().describe("How often the workflow should run. CUSTOM requires schedules. Note: REAL_TIME is NOT allowed here - realtime workflows must be created via create_realtime_monitor."),
3099
- schedules: z10.preprocess(coerceArray(true), z10.array(z10.string())).optional().describe("Cron expressions for CUSTOM update interval"),
3100
- timezone: z10.string().min(1).optional().describe("IANA timezone for cron schedules, for example America/New_York. A timezone-only update preserves existing cron expressions."),
3101
- location: z10.preprocess(coerceJson(), LocationSchema).optional().describe("Scraping location: {type:'auto'} or {type:'manual', isoCode:'US'}"),
3102
- monitoring: z10.preprocess(coerceJson(), MonitoringSchema).optional().describe("Complete monitoring replacement: fields to monitor and optional AND/OR conditions. Call get_workflow first because this replaces the existing monitoring configuration."),
3103
- limit: z10.preprocess(coerceNumber(), z10.number().positive().nullable()).optional().describe("Maximum records per run. Pass null to clear the limit and extract all rows.")
3665
+ schedules: z12.preprocess(coerceArray(true), z12.array(z12.string())).optional().describe("Cron expressions for CUSTOM update interval"),
3666
+ timezone: z12.string().min(1).optional().describe("IANA timezone for cron schedules, for example America/New_York. A timezone-only update preserves existing cron expressions."),
3667
+ location: z12.preprocess(coerceJson(), LocationSchema).optional().describe("Scraping location: {type:'auto'} or {type:'manual', isoCode:'US'}"),
3668
+ monitoring: z12.preprocess(coerceJson(), MonitoringSchema).optional().describe("Complete monitoring replacement: fields to monitor and optional AND/OR conditions. Call get_workflow first because this replaces the existing monitoring configuration."),
3669
+ limit: z12.preprocess(coerceNumber(), z12.number().positive().nullable()).optional().describe("Maximum records per run. Pass null to clear the limit and extract all rows.")
3104
3670
  }),
3105
3671
  annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false }
3106
3672
  }, withErrorHandling("update_workflow", async (args) => {
@@ -3136,9 +3702,13 @@ function registerWorkflowRunTools(server, ctx, helpers) {
3136
3702
  });
3137
3703
  }));
3138
3704
  }
3139
- var WORKFLOW_AUDIT_WATCHED_KEYS;
3705
+ var WORKFLOW_AUDIT_WATCHED_KEYS, LIST_WORKFLOWS_DEFAULT_LIMIT = 50, LIST_WORKFLOWS_MAX_LIMIT = 100, LIST_CHANGES_DEFAULT_LIMIT = 25, LIST_CHANGES_MAX_LIMIT = 100, WORKFLOW_HISTORY_DEFAULT_LIMIT = 25, WORKFLOW_HISTORY_MAX_LIMIT = 50, WORKFLOW_RUNS_DEFAULT_LIMIT = 25, WORKFLOW_RUNS_MAX_LIMIT = 100;
3140
3706
  var init_workflows = __esm(() => {
3707
+ init_notification_channels();
3708
+ init_budget();
3141
3709
  init_errors();
3710
+ init_output_schemas();
3711
+ init_pagination();
3142
3712
  init_schemas();
3143
3713
  WORKFLOW_AUDIT_WATCHED_KEYS = [
3144
3714
  "name",
@@ -3863,6 +4433,8 @@ async function createServer3(auth, options) {
3863
4433
  "For one-time or scheduled extraction, use create_workflow. Use scrape for an immediate raw HTML or markdown fetch from one URL. Use create_workflow for structured extraction, recurring runs, monitoring, or navigation-heavy jobs.",
3864
4434
  "If a one-time or scheduled workflow fails, retry it with run_workflow using the existing workflow ID. Do NOT delete and recreate the workflow just to retry it - preserve its workflow ID and configuration. Realtime workflows cannot be manually run.",
3865
4435
  "Use list_changes and get_change to retrieve detected diffs from realtime monitoring workflows.",
4436
+ "List tools, plus fetch_data, get_workflow_history, and get_workflow_assistant_timeline, all return pagination {totalCount?, hasMore, next?}. When hasMore is true, call the same tool again with pagination.next as its arguments to get the next page. totalCount is present only when the total is known; never treat the page size as the total.",
4437
+ "Large results (fetch_data, get_workflow_history, get_change) are cut to whole rows or entries under about 80 KB and marked truncated: true with a hint; follow pagination.next, narrow the filters, or use export_data, and never conclude the data ends there.",
3866
4438
  "When the user asks what needs their attention, call list_workflows with statusFilters group:attention. A row with awaitingUserInput.prompt is the Assistant's pending question: answer it with get_workflow_assistant then answer_workflow_assistant_question. A row with awaitingUserInput but no prompt is a setup the user left unfinished: use get_workflow_assistant to see where it stopped, then resume_workflow_assistant or request_workflow_update. A row with displayState PREVIEW and no awaitingUserInput is preview data to review or approve with approve_workflow. Workflows with an open support ticket are excluded from group:attention and appear under group:support.",
3867
4439
  "",
3868
4440
  "Schema tips: Use descriptive field names and examples. Group related data under one entity.",
@@ -4322,6 +4894,8 @@ async function createServer(auth, options) {
4322
4894
  "For one-time or scheduled extraction, use create_workflow. Use scrape for an immediate raw HTML or markdown fetch from one URL. Use create_workflow for structured extraction, recurring runs, monitoring, or navigation-heavy jobs.",
4323
4895
  "If a one-time or scheduled workflow fails, retry it with run_workflow using the existing workflow ID. Do NOT delete and recreate the workflow just to retry it - preserve its workflow ID and configuration. Realtime workflows cannot be manually run.",
4324
4896
  "Use list_changes and get_change to retrieve detected diffs from realtime monitoring workflows.",
4897
+ "List tools, plus fetch_data, get_workflow_history, and get_workflow_assistant_timeline, all return pagination {totalCount?, hasMore, next?}. When hasMore is true, call the same tool again with pagination.next as its arguments to get the next page. totalCount is present only when the total is known; never treat the page size as the total.",
4898
+ "Large results (fetch_data, get_workflow_history, get_change) are cut to whole rows or entries under about 80 KB and marked truncated: true with a hint; follow pagination.next, narrow the filters, or use export_data, and never conclude the data ends there.",
4325
4899
  "When the user asks what needs their attention, call list_workflows with statusFilters group:attention. A row with awaitingUserInput.prompt is the Assistant's pending question: answer it with get_workflow_assistant then answer_workflow_assistant_question. A row with awaitingUserInput but no prompt is a setup the user left unfinished: use get_workflow_assistant to see where it stopped, then resume_workflow_assistant or request_workflow_update. A row with displayState PREVIEW and no awaitingUserInput is preview data to review or approve with approve_workflow. Workflows with an open support ticket are excluded from group:attention and appear under group:support.",
4326
4900
  "",
4327
4901
  "Schema tips: Use descriptive field names and examples. Group related data under one entity.",