@kadoa/mcp 0.7.0-rc.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +2 -2
  2. package/dist/index.js +1093 -472
  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",
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",
@@ -104,6 +104,11 @@ function createKadoaClient(auth) {
104
104
  var MCP_SCRAPE_REQUEST_TIMEOUT_MS = 310000;
105
105
  var init_client = () => {};
106
106
 
107
+ // src/default-product.ts
108
+ function parseDefaultProductType(value) {
109
+ return value === "realtime" || value === "workflow" ? value : undefined;
110
+ }
111
+
107
112
  // src/error-summary.ts
108
113
  function redactCredentialText(value) {
109
114
  return value.replace(/Bearer\s+[^\s,;]+/gi, "Bearer [redacted]").replace(/\b(?:kcs_|tk-)[A-Za-z0-9._~-]+\b/g, "[redacted]").replace(/\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/g, "[redacted]");
@@ -207,19 +212,252 @@ function withVariableDashboardUrl(variable) {
207
212
  return { ...variable, dashboardUrl: variableSettingsDashboardUrl() };
208
213
  }
209
214
 
215
+ // src/tools/shared/output-schemas.ts
216
+ import { z } from "zod";
217
+ var apiField, nullableString, PaginationOutput, WorkflowRowOutput, ListWorkflowsOutput, GetWorkflowOutput, FetchDataOutput, ASSISTANT_ACTIVITIES, WorkflowAssistantOutput, AssistantTimelineOutput, AssistantAnswerOutput, AssistantControlOutput, WorkflowStrategyOutput;
218
+ var init_output_schemas = __esm(() => {
219
+ apiField = z.unknown().optional();
220
+ nullableString = z.string().nullable();
221
+ PaginationOutput = z.object({
222
+ totalCount: z.number().optional(),
223
+ hasMore: z.boolean(),
224
+ limit: z.number().optional(),
225
+ skip: z.number().optional(),
226
+ offset: z.number().optional(),
227
+ page: z.number().optional(),
228
+ totalPages: z.number().optional(),
229
+ nextCursor: nullableString.optional(),
230
+ next: z.record(z.string(), z.unknown()).optional()
231
+ }).loose();
232
+ WorkflowRowOutput = z.object({
233
+ id: apiField,
234
+ name: apiField,
235
+ dashboardUrl: z.string(),
236
+ status: z.string(),
237
+ state: nullableString,
238
+ displayState: nullableString,
239
+ runState: nullableString,
240
+ urls: apiField,
241
+ totalRecords: apiField,
242
+ startedAt: apiField,
243
+ finishedAt: apiField,
244
+ createdAt: apiField,
245
+ assistant: z.object({
246
+ eligible: z.boolean(),
247
+ dataStale: z.boolean(),
248
+ session: z.object({ id: z.string(), threadId: apiField, status: apiField }).loose().nullable()
249
+ }).loose(),
250
+ template: z.object({ id: z.string(), pinnedVersion: apiField, latestVersion: apiField, isOutdated: z.boolean() }).loose().nullable(),
251
+ realtime: z.object({ enabled: z.boolean(), lastDataChangedAt: apiField, health: apiField }).loose(),
252
+ awaitingUserInput: apiField
253
+ }).loose();
254
+ ListWorkflowsOutput = z.object({ workflows: z.array(WorkflowRowOutput), pagination: PaginationOutput }).loose();
255
+ GetWorkflowOutput = z.object({
256
+ id: z.string(),
257
+ name: apiField,
258
+ dashboardUrl: z.string(),
259
+ description: apiField,
260
+ status: z.string(),
261
+ state: nullableString,
262
+ displayState: nullableString,
263
+ runState: nullableString,
264
+ urls: apiField,
265
+ prompt: nullableString,
266
+ intentSource: z.enum(["extraction_spec", "legacy_prompt"]).nullable(),
267
+ extractionSpec: apiField,
268
+ entity: apiField,
269
+ schema: apiField,
270
+ navigationMode: apiField,
271
+ dataStale: apiField,
272
+ assistantEligible: z.boolean(),
273
+ assistantSessionId: apiField,
274
+ assistantThreadId: apiField,
275
+ assistantSessionStatus: apiField,
276
+ assistant: z.object({ eligible: z.boolean(), sessionId: apiField, threadId: apiField, sessionStatus: apiField }).loose(),
277
+ template: z.object({
278
+ id: apiField,
279
+ name: apiField,
280
+ version: apiField,
281
+ controlledParts: z.array(z.string()),
282
+ hasSchemaValidationRules: apiField
283
+ }).loose().nullable(),
284
+ updateInterval: apiField,
285
+ schedules: apiField,
286
+ scheduleTimezone: apiField,
287
+ nextInvocation: apiField,
288
+ location: apiField,
289
+ limit: apiField,
290
+ tags: apiField,
291
+ totalRecords: apiField,
292
+ job: z.object({ id: apiField }).loose().nullable(),
293
+ startedAt: apiField,
294
+ finishedAt: apiField,
295
+ estimatedFinishedAtUtc: apiField,
296
+ errors: apiField,
297
+ createdAt: apiField,
298
+ notificationConfig: apiField,
299
+ channelCounts: apiField,
300
+ dataValidation: apiField,
301
+ validationStatistics: apiField,
302
+ realtime: z.object({ lastDataChangedAt: apiField, observerHealth: apiField }).loose().nullable()
303
+ }).loose();
304
+ FetchDataOutput = z.object({
305
+ data: z.array(z.record(z.string(), z.unknown())),
306
+ pagination: PaginationOutput,
307
+ totalCount: z.number().optional(),
308
+ hint: z.string().optional()
309
+ }).loose();
310
+ ASSISTANT_ACTIVITIES = [
311
+ "unsupported",
312
+ "not_started",
313
+ "working",
314
+ "waiting_for_input",
315
+ "idle",
316
+ "interrupted",
317
+ "failed",
318
+ "closed",
319
+ "unknown"
320
+ ];
321
+ WorkflowAssistantOutput = z.object({
322
+ workflowId: z.string(),
323
+ assistantEligible: z.boolean(),
324
+ sessionId: nullableString,
325
+ threadId: apiField,
326
+ sessionStatus: apiField,
327
+ activity: z.enum(ASSISTANT_ACTIVITIES),
328
+ canAcceptUpdate: z.boolean(),
329
+ isTerminal: z.boolean(),
330
+ paused: apiField,
331
+ pause: apiField,
332
+ pendingQuestion: apiField
333
+ }).loose();
334
+ AssistantTimelineOutput = z.object({
335
+ workflowId: z.string(),
336
+ sessionId: apiField,
337
+ items: z.array(z.unknown()),
338
+ pagination: PaginationOutput,
339
+ dashboardUrl: z.string()
340
+ }).loose();
341
+ AssistantAnswerOutput = z.object({
342
+ success: z.literal(true),
343
+ workflowId: z.string(),
344
+ sessionId: z.string(),
345
+ threadId: apiField,
346
+ questionId: z.string(),
347
+ message: z.string()
348
+ }).loose();
349
+ AssistantControlOutput = z.object({
350
+ success: z.boolean(),
351
+ status: z.literal("accepted"),
352
+ action: z.enum(["interrupt", "resume", "stop"]),
353
+ workflowId: z.string(),
354
+ sessionId: z.string(),
355
+ delivery: apiField,
356
+ jobId: apiField,
357
+ message: z.string()
358
+ }).loose();
359
+ WorkflowStrategyOutput = z.object({
360
+ workflowId: z.string(),
361
+ sessionId: nullableString,
362
+ source: z.enum(["assistant_build", "workflow_metadata"]).nullable(),
363
+ status: z.enum(["ready", "building", "unavailable"]),
364
+ strategy: apiField,
365
+ message: z.string().optional()
366
+ }).loose();
367
+ });
368
+
369
+ // src/tools/shared/pagination.ts
370
+ import { z as z2 } from "zod";
371
+ function limitInput(options) {
372
+ const range = `limit must be a whole number from 1 to ${options.max}`;
373
+ const overMax = `${range}. ${options.overMaxHint ?? "Page with pagination.next."}`;
374
+ 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})`);
375
+ }
376
+ function offsetInput(param, noun) {
377
+ const range = `${param} must be a whole number from 0 to ${MAX_OFFSET}`;
378
+ return z2.preprocess(coerceNumber(), z2.number().int(range).min(0, range).max(MAX_OFFSET, range)).optional().describe(`Number of ${noun} to skip (default 0)`);
379
+ }
380
+ function pageInput() {
381
+ const range = `page must be a whole number from 1 to ${MAX_PAGE}`;
382
+ return z2.preprocess(coerceNumber(), z2.number().int(range).min(1, range).max(MAX_PAGE, range)).optional().describe("Page number, 1-based (default 1)");
383
+ }
384
+ function nextArgs(args, patch) {
385
+ const next = {};
386
+ for (const [key, value] of Object.entries({ ...args, ...patch })) {
387
+ if (value !== undefined)
388
+ next[key] = value;
389
+ }
390
+ return next;
391
+ }
392
+ function withTotal(totalCount, hasMore) {
393
+ return totalCount !== undefined ? { totalCount, hasMore } : { hasMore };
394
+ }
395
+ function offsetPage(options) {
396
+ const nextOffset = options.offset + options.returned;
397
+ const more = options.hasMore ?? (options.totalCount !== undefined ? nextOffset < options.totalCount : options.returned >= options.limit);
398
+ const hasMore = options.returned > 0 && more;
399
+ const meta = withTotal(options.totalCount, hasMore);
400
+ meta.limit = options.limit;
401
+ meta[options.param] = options.offset;
402
+ if (hasMore)
403
+ meta.next = nextArgs(options.args, { [options.param]: nextOffset, limit: options.limit });
404
+ return meta;
405
+ }
406
+ function numberedPage(options) {
407
+ const more = options.totalPages !== undefined ? options.page < options.totalPages : options.totalCount !== undefined ? options.page * options.limit < options.totalCount : options.returned >= options.limit;
408
+ const hasMore = options.returned > 0 && more;
409
+ const meta = withTotal(options.totalCount, hasMore);
410
+ meta.limit = options.limit;
411
+ meta.page = options.page;
412
+ if (options.totalPages !== undefined)
413
+ meta.totalPages = options.totalPages;
414
+ if (hasMore)
415
+ meta.next = nextArgs(options.args, { page: options.page + 1, limit: options.limit });
416
+ return meta;
417
+ }
418
+ function cursorPage(options) {
419
+ const cursor = typeof options.nextCursor === "string" && options.nextCursor.length > 0 ? options.nextCursor : null;
420
+ const meta = { hasMore: options.hasMore && cursor !== null, nextCursor: cursor };
421
+ if (meta.hasMore)
422
+ meta.next = nextArgs(options.args, { cursor });
423
+ return meta;
424
+ }
425
+ function completeList(totalCount) {
426
+ return { totalCount, hasMore: false };
427
+ }
428
+ var MAX_OFFSET = 1e6, MAX_PAGE = 1e5;
429
+ var init_pagination = () => {};
430
+
210
431
  // src/tools/shared/results.ts
432
+ function asStructuredObject(data) {
433
+ return data !== null && !Array.isArray(data) ? data : { result: data };
434
+ }
435
+ function structuredResult(data, extra = []) {
436
+ const structuredContent = asStructuredObject(data);
437
+ return {
438
+ structuredContent,
439
+ content: [{ type: "text", text: JSON.stringify(structuredContent) }, ...extra]
440
+ };
441
+ }
211
442
  function jsonResult(data) {
212
- return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
443
+ return structuredResult(data);
444
+ }
445
+ function textOnlyResult(data, extra = []) {
446
+ const structuredContent = asStructuredObject(data);
447
+ return { content: [{ type: "text", text: JSON.stringify(structuredContent) }, ...extra] };
213
448
  }
214
- function errorResult(message) {
449
+ function errorResult(message, details = {}) {
215
450
  return {
216
- content: [{ type: "text", text: JSON.stringify({ error: message }, null, 2) }],
451
+ content: [{ type: "text", text: JSON.stringify({ error: message, ...details }) }],
217
452
  isError: true
218
453
  };
219
454
  }
455
+ function withExtraContent(result, ...blocks) {
456
+ return { ...result, content: [...result.content, ...blocks] };
457
+ }
220
458
 
221
459
  // src/tools/shared/schemas.ts
222
- import { z } from "zod";
460
+ import { z as z3 } from "zod";
223
461
  function isPlainObject(value) {
224
462
  return value !== null && typeof value === "object" && !Array.isArray(value);
225
463
  }
@@ -281,7 +519,7 @@ function stampSchemaValidationRules(rules, attribution, previous) {
281
519
  return Object.fromEntries(Object.entries(rules).map(([name, field]) => [name, stampField(field, previousFields[name])]));
282
520
  }
283
521
  function strictSchema(shape) {
284
- return z.object(shape).strict();
522
+ return z3.object(shape).strict();
285
523
  }
286
524
  function withoutDefIds(schema) {
287
525
  const jsonSchema = schema["~standard"].jsonSchema;
@@ -317,7 +555,7 @@ function normalizeDataFilters(val, ctx) {
317
555
  code: "custom",
318
556
  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
557
  });
320
- return z.NEVER;
558
+ return z3.NEVER;
321
559
  }
322
560
  return [v];
323
561
  }
@@ -325,7 +563,7 @@ function normalizeDataFilters(val, ctx) {
325
563
  }
326
564
  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
565
  var init_schemas = __esm(() => {
328
- UpdateIntervalSchema = z.enum([
566
+ UpdateIntervalSchema = z3.enum([
329
567
  "ONLY_ONCE",
330
568
  "EVERY_10_MINUTES",
331
569
  "HALF_HOURLY",
@@ -359,8 +597,8 @@ var init_schemas = __esm(() => {
359
597
  "credits_low",
360
598
  "free_trial_ending"
361
599
  ];
362
- NotificationEventTypeSchema = z.enum(NOTIFICATION_EVENT_TYPES);
363
- SchemaFieldDataTypeSchema = z.enum([
600
+ NotificationEventTypeSchema = z3.enum(NOTIFICATION_EVENT_TYPES);
601
+ SchemaFieldDataTypeSchema = z3.enum([
364
602
  "STRING",
365
603
  "NUMBER",
366
604
  "BOOLEAN",
@@ -373,61 +611,61 @@ var init_schemas = __esm(() => {
373
611
  "ARRAY"
374
612
  ]);
375
613
  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"),
614
+ name: z3.string().describe("Field name"),
615
+ description: z3.string().optional().describe("What this field contains"),
616
+ example: z3.string().describe("Example value"),
379
617
  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).")
618
+ 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
619
  };
382
- SchemaValidationPresenceRule = z.object({
383
- target: z.number().int().min(0).max(100).step(20).describe("Expected percentage of rows with a value")
620
+ SchemaValidationPresenceRule = z3.object({
621
+ target: z3.number().int().min(0).max(100).step(20).describe("Expected percentage of rows with a value")
384
622
  }).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")
623
+ SchemaValidationUniquenessRule = z3.object({
624
+ target: z3.number().int().min(0).max(100).step(20).describe("Expected percentage of rows with a unique value")
387
625
  }).strict().meta({ id: "UniquenessRule" });
388
- SchemaValidationBoundRule = z.object({
389
- value: z.number().int().min(0).describe("Length or count bound")
626
+ SchemaValidationBoundRule = z3.object({
627
+ value: z3.number().int().min(0).describe("Length or count bound")
390
628
  }).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"])
629
+ SchemaValidationStringFormatRule = z3.discriminatedUnion("kind", [
630
+ z3.object({
631
+ kind: z3.literal("FREE_TEXT"),
632
+ charset: z3.discriminatedUnion("kind", [
633
+ z3.object({
634
+ kind: z3.literal("PRESET"),
635
+ preset: z3.enum(["natural_language", "alphanumeric", "alpha"])
398
636
  }).strict()
399
637
  ])
400
638
  }).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"])
639
+ z3.object({
640
+ kind: z3.literal("FORMAT"),
641
+ source: z3.discriminatedUnion("kind", [
642
+ z3.object({
643
+ kind: z3.literal("PRESET"),
644
+ preset: z3.enum(["url", "email", "phone", "date", "datetime", "time", "uuid", "slug"])
407
645
  }).strict(),
408
- z.object({
409
- kind: z.literal("CUSTOM"),
410
- pattern: z.string().min(1).describe("Regular expression pattern")
646
+ z3.object({
647
+ kind: z3.literal("CUSTOM"),
648
+ pattern: z3.string().min(1).describe("Regular expression pattern")
411
649
  }).strict()
412
650
  ])
413
651
  }).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"])
652
+ z3.object({
653
+ kind: z3.literal("LIST"),
654
+ source: z3.discriminatedUnion("kind", [
655
+ z3.object({
656
+ kind: z3.literal("PRESET"),
657
+ preset: z3.enum(["language2", "country2", "country3", "currency3", "month3", "usState2"])
420
658
  }).strict(),
421
- z.object({
422
- kind: z.literal("CUSTOM"),
423
- values: z.array(z.string().min(1)).min(1).describe("Allowed string values")
659
+ z3.object({
660
+ kind: z3.literal("CUSTOM"),
661
+ values: z3.array(z3.string().min(1)).min(1).describe("Allowed string values")
424
662
  }).strict()
425
663
  ])
426
664
  }).strict()
427
665
  ]);
428
- SchemaValidationFieldRulesSchema = z.lazy(() => z.discriminatedUnion("kind", [
429
- z.object({
430
- kind: z.literal("STRING"),
666
+ SchemaValidationFieldRulesSchema = z3.lazy(() => z3.discriminatedUnion("kind", [
667
+ z3.object({
668
+ kind: z3.literal("STRING"),
431
669
  presence: SchemaValidationPresenceRule.optional(),
432
670
  uniqueness: SchemaValidationUniquenessRule.optional(),
433
671
  minLength: SchemaValidationBoundRule.optional(),
@@ -436,27 +674,27 @@ var init_schemas = __esm(() => {
436
674
  maxHtmlElements: SchemaValidationBoundRule.optional(),
437
675
  format: SchemaValidationStringFormatRule.optional()
438
676
  }).strict(),
439
- z.object({
440
- kind: z.literal("NUMBER"),
677
+ z3.object({
678
+ kind: z3.literal("NUMBER"),
441
679
  presence: SchemaValidationPresenceRule.optional(),
442
680
  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()
681
+ minimum: z3.object({ value: z3.number().describe("Minimum numeric value") }).strict().optional(),
682
+ maximum: z3.object({ value: z3.number().describe("Maximum numeric value") }).strict().optional(),
683
+ maxDecimalPlaces: z3.object({ value: z3.number().int().min(0).max(16) }).strict().optional()
446
684
  }).strict(),
447
- z.object({
448
- kind: z.literal("OTHER"),
685
+ z3.object({
686
+ kind: z3.literal("OTHER"),
449
687
  presence: SchemaValidationPresenceRule.optional(),
450
688
  uniqueness: SchemaValidationUniquenessRule.optional()
451
689
  }).strict(),
452
- z.object({
453
- kind: z.literal("OBJECT"),
690
+ z3.object({
691
+ kind: z3.literal("OBJECT"),
454
692
  presence: SchemaValidationPresenceRule.optional(),
455
693
  uniqueness: SchemaValidationUniquenessRule.optional(),
456
- properties: z.record(z.string(), SchemaValidationFieldRulesSchema)
694
+ properties: z3.record(z3.string(), SchemaValidationFieldRulesSchema)
457
695
  }).strict(),
458
- z.object({
459
- kind: z.literal("ARRAY"),
696
+ z3.object({
697
+ kind: z3.literal("ARRAY"),
460
698
  presence: SchemaValidationPresenceRule.optional(),
461
699
  uniqueness: SchemaValidationUniquenessRule.optional(),
462
700
  minItems: SchemaValidationBoundRule.optional(),
@@ -464,21 +702,21 @@ var init_schemas = __esm(() => {
464
702
  items: SchemaValidationFieldRulesSchema.optional()
465
703
  }).strict()
466
704
  ]));
467
- SchemaValidationRulesSchema = z.record(z.string(), SchemaValidationFieldRulesSchema);
705
+ SchemaValidationRulesSchema = z3.record(z3.string(), SchemaValidationFieldRulesSchema);
468
706
  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()
707
+ LocationSchema = z3.object({
708
+ type: z3.enum(["auto", "manual"]),
709
+ isoCode: z3.string().trim().min(2).optional()
472
710
  }).strict().superRefine((location, ctx) => {
473
711
  if (location.type === "manual" && !location.isoCode) {
474
712
  ctx.addIssue({
475
- code: z.ZodIssueCode.custom,
713
+ code: z3.ZodIssueCode.custom,
476
714
  path: ["isoCode"],
477
715
  message: "isoCode is required for manual location"
478
716
  });
479
717
  }
480
718
  if (location.type === "auto" && location.isoCode) {
481
- ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["isoCode"], message: "omit isoCode for auto location" });
719
+ ctx.addIssue({ code: z3.ZodIssueCode.custom, path: ["isoCode"], message: "omit isoCode for auto location" });
482
720
  }
483
721
  });
484
722
  MonitoringValueOperators = [
@@ -500,81 +738,81 @@ var init_schemas = __esm(() => {
500
738
  "WITHIN_LAST_DAYS"
501
739
  ];
502
740
  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(),
741
+ MonitoringConditionOperatorSchema = z3.enum([...MonitoringValueOperators, ...MonitoringValuelessOperators]);
742
+ MonitoringConditionSchema = z3.object({
743
+ type: z3.enum(["SINGLE", "ANY"]),
744
+ field: z3.string().trim().min(1).optional(),
507
745
  operator: MonitoringConditionOperatorSchema,
508
- value: z.union([z.string(), z.number(), z.boolean()]).optional()
746
+ value: z3.union([z3.string(), z3.number(), z3.boolean()]).optional()
509
747
  }).strict().superRefine((condition, ctx) => {
510
748
  if (condition.type === "SINGLE" && !condition.field) {
511
749
  ctx.addIssue({
512
- code: z.ZodIssueCode.custom,
750
+ code: z3.ZodIssueCode.custom,
513
751
  path: ["field"],
514
752
  message: "field is required for SINGLE conditions"
515
753
  });
516
754
  }
517
755
  const needsValue = MonitoringValueOperators.includes(condition.operator);
518
756
  if (needsValue && condition.value === undefined) {
519
- ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["value"], message: `${condition.operator} requires a value` });
757
+ ctx.addIssue({ code: z3.ZodIssueCode.custom, path: ["value"], message: `${condition.operator} requires a value` });
520
758
  }
521
759
  if (!needsValue && condition.value !== undefined) {
522
760
  ctx.addIssue({
523
- code: z.ZodIssueCode.custom,
761
+ code: z3.ZodIssueCode.custom,
524
762
  path: ["value"],
525
763
  message: `${condition.operator} does not accept a value`
526
764
  });
527
765
  }
528
766
  });
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()
767
+ MonitoringSchema = z3.object({
768
+ fields: z3.preprocess(coerceArray(), z3.array(z3.object({
769
+ fieldName: z3.string().trim().min(1),
770
+ operator: z3.enum(["changed", "added", "removed"]),
771
+ isKeyField: z3.preprocess(coerceBoolean(), z3.boolean()).optional()
534
772
  }).strict()).min(1)),
535
- conditions: z.object({
536
- logicalOperator: z.enum(["AND", "OR"]),
537
- conditions: z.preprocess(coerceArray(), z.array(MonitoringConditionSchema).min(1))
773
+ conditions: z3.object({
774
+ logicalOperator: z3.enum(["AND", "OR"]),
775
+ conditions: z3.preprocess(coerceArray(), z3.array(MonitoringConditionSchema).min(1))
538
776
  }).strict().optional()
539
777
  }).strict();
540
778
  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.")
779
+ url: z3.string().optional().describe("Single URL - prefer using 'urls' instead. If both are provided, 'urls' takes precedence."),
780
+ 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
781
  };
544
- extractionPromptInput = z.string().optional();
782
+ extractionPromptInput = z3.string().optional();
545
783
  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.")
784
+ name: z3.string().optional().describe("Optional name for the workflow"),
785
+ entity: z3.string().optional().describe("Entity name for extraction (e.g., 'Product', 'Job Posting')"),
786
+ 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
787
  };
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)")
788
+ webhookAuthShape = z3.object({
789
+ type: z3.enum(["bearer", "basic", "header"]).describe("Authentication type"),
790
+ token: z3.string().optional().describe("Bearer token (required for 'bearer' type)"),
791
+ username: z3.string().optional().describe("Username (required for 'basic' type)"),
792
+ password: z3.string().optional().describe("Password (required for 'basic' type)"),
793
+ headers: z3.object({}).passthrough().optional().describe("Custom headers as key-value pairs (required for 'header' type)")
556
794
  }).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.")
795
+ notificationChannelsSchema = z3.object({
796
+ email: z3.object({
797
+ recipients: z3.array(z3.string().email()).optional().describe("Email addresses to notify. Omit to use account default email.")
560
798
  }).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)"),
799
+ webhook: z3.object({
800
+ url: z3.string().url().describe("Webhook endpoint URL"),
801
+ httpMethod: z3.enum(["POST", "GET", "PUT", "PATCH"]).optional().describe("HTTP method (defaults to POST)"),
564
802
  auth: webhookAuthShape
565
803
  }).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)")
804
+ slack: z3.object({
805
+ webhookUrl: z3.string().url().optional().describe("Slack incoming webhook URL (legacy)"),
806
+ slackChannelId: z3.string().optional().describe("Slack channel ID from OAuth integration (e.g. C01ABCDEF)"),
807
+ slackChannelName: z3.string().optional().describe("Slack channel name (e.g. #alerts)")
570
808
  }).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")
809
+ websocket: z3.boolean().optional().describe("Enable WebSocket notifications for programmatic real-time consumption")
572
810
  });
573
811
  notificationsInputShape = {
574
- notifications: z.preprocess(coerceJson(), notificationChannelsSchema.optional()).optional().describe("Optional notification channels to alert when data changes.")
812
+ notifications: z3.preprocess(coerceJson(), notificationChannelsSchema.optional()).optional().describe("Optional notification channels to alert when data changes.")
575
813
  };
576
814
  requiredNotificationsInputShape = {
577
- notifications: z.preprocess(coerceJson(), notificationChannelsSchema).describe("Notification channels to alert when data changes.")
815
+ notifications: z3.preprocess(coerceJson(), notificationChannelsSchema).describe("Notification channels to alert when data changes.")
578
816
  };
579
817
  DATA_FILTER_OPERATORS = [
580
818
  "EQUALS",
@@ -599,10 +837,10 @@ var init_schemas = __esm(() => {
599
837
  ];
600
838
  DATA_FILTER_VALUELESS_OPERATORS = new Set(["IS_NULL", "IS_NOT_NULL", "IS_EMPTY", "IS_NOT_EMPTY"]);
601
839
  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")
840
+ DataFilterSchema = z3.object({
841
+ field: z3.string().trim().min(1).describe("Field name from the workflow schema; one nested level is allowed, e.g. classification.match"),
842
+ operator: z3.preprocess((val) => typeof val === "string" ? val.trim().toUpperCase() : val, z3.enum(DATA_FILTER_OPERATORS)),
843
+ 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
844
  }).strict().superRefine((filter, ctx) => {
607
845
  if (DATA_FILTER_VALUELESS_OPERATORS.has(filter.operator)) {
608
846
  if (filter.value !== undefined) {
@@ -627,12 +865,12 @@ var init_schemas = __esm(() => {
627
865
  });
628
866
  }
629
867
  });
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.');
868
+ 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
869
  });
632
870
 
633
871
  // src/tools/assistant.ts
634
872
  import { FREEFORM_ASSISTANT_ANSWER_KEY } from "@kadoa/node-sdk";
635
- import { z as z2 } from "zod";
873
+ import { z as z4 } from "zod";
636
874
  function isAssistantSessionResumable(status) {
637
875
  return status != null && RESUMABLE_ASSISTANT_STATUSES.has(status);
638
876
  }
@@ -700,8 +938,9 @@ function registerAssistantTools(server, ctx, helpers) {
700
938
  title: "Get Workflow Assistant",
701
939
  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
940
  inputSchema: strictSchema({
703
- workflowId: z2.string().min(1).describe("The workflow ID")
941
+ workflowId: z4.string().min(1).describe("The workflow ID")
704
942
  }),
943
+ outputSchema: WorkflowAssistantOutput,
705
944
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
706
945
  }, withErrorHandling("get_workflow_assistant", async (args) => {
707
946
  const workflow = await ctx.client.workflow.get(args.workflowId);
@@ -733,10 +972,11 @@ function registerAssistantTools(server, ctx, helpers) {
733
972
  title: "Get Workflow Assistant Timeline",
734
973
  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
974
  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)")
975
+ workflowId: z4.string().min(1).describe("The workflow ID"),
976
+ cursor: z4.string().min(1).optional().describe("Opaque nextCursor from the previous page"),
977
+ limit: limitInput({ noun: "History items", defaultLimit: 50, max: 100 })
739
978
  }),
979
+ outputSchema: AssistantTimelineOutput,
740
980
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
741
981
  }, withErrorHandling("get_workflow_assistant_timeline", async (args) => {
742
982
  const timeline = await ctx.client.assistant.getTimeline(args.workflowId, {
@@ -749,8 +989,12 @@ function registerAssistantTools(server, ctx, helpers) {
749
989
  return jsonResult({
750
990
  workflowId: timeline.workflowId,
751
991
  sessionId: timeline.sessionId,
752
- items: timeline.items,
753
- pagination: timeline.pagination,
992
+ items: timeline.items ?? [],
993
+ pagination: cursorPage({
994
+ args,
995
+ nextCursor: timeline.pagination?.nextCursor,
996
+ hasMore: timeline.pagination?.hasMore === true
997
+ }),
754
998
  dashboardUrl: workflowDashboardUrl(timeline.workflowId)
755
999
  });
756
1000
  }));
@@ -758,11 +1002,12 @@ function registerAssistantTools(server, ctx, helpers) {
758
1002
  title: "Answer Assistant Question",
759
1003
  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
1004
  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")
1005
+ workflowId: z4.string().min(1).describe("The workflow ID"),
1006
+ questionId: z4.string().min(1).describe("The pending question ID returned by get_workflow_assistant"),
1007
+ answer: z4.string().min(1).optional().describe("Freeform answer"),
1008
+ 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
1009
  }),
1010
+ outputSchema: AssistantAnswerOutput,
766
1011
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }
767
1012
  }, withErrorHandling("answer_workflow_assistant_question", async (args) => {
768
1013
  const hasFreeform = args.answer != null;
@@ -804,8 +1049,9 @@ function registerAssistantTools(server, ctx, helpers) {
804
1049
  title: "Interrupt Workflow Assistant",
805
1050
  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
1051
  inputSchema: strictSchema({
807
- workflowId: z2.string().min(1).describe("Workflow whose Assistant session should be interrupted")
1052
+ workflowId: z4.string().min(1).describe("Workflow whose Assistant session should be interrupted")
808
1053
  }),
1054
+ outputSchema: AssistantControlOutput,
809
1055
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }
810
1056
  }, withErrorHandling("interrupt_workflow_assistant", async (args) => {
811
1057
  const workflow = await ctx.client.workflow.get(args.workflowId);
@@ -838,8 +1084,9 @@ function registerAssistantTools(server, ctx, helpers) {
838
1084
  title: "Resume Workflow Assistant",
839
1085
  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
1086
  inputSchema: strictSchema({
841
- workflowId: z2.string().min(1).describe("Workflow whose Assistant session should resume")
1087
+ workflowId: z4.string().min(1).describe("Workflow whose Assistant session should resume")
842
1088
  }),
1089
+ outputSchema: AssistantControlOutput,
843
1090
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }
844
1091
  }, withErrorHandling("resume_workflow_assistant", async (args) => {
845
1092
  const workflow = await ctx.client.workflow.get(args.workflowId);
@@ -872,8 +1119,9 @@ function registerAssistantTools(server, ctx, helpers) {
872
1119
  title: "Stop Workflow Assistant",
873
1120
  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
1121
  inputSchema: strictSchema({
875
- workflowId: z2.string().min(1).describe("Workflow whose active Assistant session should stop")
1122
+ workflowId: z4.string().min(1).describe("Workflow whose active Assistant session should stop")
876
1123
  }),
1124
+ outputSchema: AssistantControlOutput,
877
1125
  annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false }
878
1126
  }, withErrorHandling("stop_workflow_assistant", async (args) => {
879
1127
  const workflow = await ctx.client.workflow.get(args.workflowId);
@@ -905,8 +1153,9 @@ function registerAssistantTools(server, ctx, helpers) {
905
1153
  title: "Get Workflow Strategy",
906
1154
  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
1155
  inputSchema: strictSchema({
908
- workflowId: z2.string().min(1).describe("The workflow ID")
1156
+ workflowId: z4.string().min(1).describe("The workflow ID")
909
1157
  }),
1158
+ outputSchema: WorkflowStrategyOutput,
910
1159
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
911
1160
  }, withErrorHandling("get_workflow_strategy", async (args) => {
912
1161
  const workflow = await ctx.client.workflow.get(args.workflowId);
@@ -937,6 +1186,8 @@ function registerAssistantTools(server, ctx, helpers) {
937
1186
  }
938
1187
  var RESUMABLE_ASSISTANT_STATUSES, ACTIVE_ASSISTANT_STATUSES, IDLE_ASSISTANT_STATUSES, CLOSED_ASSISTANT_STATUSES;
939
1188
  var init_assistant = __esm(() => {
1189
+ init_output_schemas();
1190
+ init_pagination();
940
1191
  init_schemas();
941
1192
  RESUMABLE_ASSISTANT_STATUSES = new Set(["pending", "created", "active", "idle", "inactive", "interrupted"]);
942
1193
  ACTIVE_ASSISTANT_STATUSES = new Set(["pending", "created", "active"]);
@@ -945,6 +1196,7 @@ var init_assistant = __esm(() => {
945
1196
  });
946
1197
 
947
1198
  // src/notification-channels.ts
1199
+ import { createHash } from "node:crypto";
948
1200
  function recipientKey(recipients) {
949
1201
  return [...new Set(recipients.map((r) => r.trim().toLowerCase()))].sort().join(",");
950
1202
  }
@@ -968,6 +1220,14 @@ function destinationOf(channelType, rawConfig) {
968
1220
  return `url:${webhookUrl}`;
969
1221
  return null;
970
1222
  }
1223
+ function mcpChannelName(channelType, config) {
1224
+ const name = `mcp-${channelType.toLowerCase()}`;
1225
+ const slackName = config.slackChannelName;
1226
+ const destination = destinationOf(channelType, config) ?? (channelType === "SLACK" && typeof slackName === "string" && slackName.trim() ? `slack-name:${slackName.trim().toLowerCase()}` : null);
1227
+ if (destination === null)
1228
+ return name;
1229
+ return `${name}-${createHash("sha256").update(destination).digest("hex").slice(0, 8)}`;
1230
+ }
971
1231
  function findReusableChannel(channels, channelType, config) {
972
1232
  const wanted = destinationOf(channelType, config);
973
1233
  if (wanted === null)
@@ -1012,16 +1272,23 @@ function redactChannelConfig(config) {
1012
1272
  ...auth !== undefined && { auth: redactAuth(auth) }
1013
1273
  };
1014
1274
  }
1275
+ function redactChannelRefs(channels) {
1276
+ return channels?.map((channel) => {
1277
+ const { config } = channel;
1278
+ return config === undefined ? channel : { ...channel, config: redactChannelConfig(config) };
1279
+ });
1280
+ }
1281
+ var init_notification_channels = () => {};
1015
1282
 
1016
1283
  // src/tools/notifications.ts
1017
- import { z as z3 } from "zod";
1284
+ import { z as z5 } from "zod";
1018
1285
  function registerNotificationChannelTools(server, ctx, helpers) {
1019
1286
  const { withErrorHandling } = helpers;
1020
1287
  server.registerTool("list_notification_channels", {
1021
1288
  title: "List Notification Channels",
1022
1289
  description: "List notification channels configured for a workflow or across the workspace. Channels are destinations (email, Slack, webhook, websocket) where notifications are sent.",
1023
1290
  inputSchema: strictSchema({
1024
- workflowId: z3.string().optional().describe("Filter channels linked to a specific workflow. Omit to list all workspace channels.")
1291
+ workflowId: z5.string().optional().describe("Filter channels linked to a specific workflow. Omit to list all workspace channels.")
1025
1292
  }),
1026
1293
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
1027
1294
  }, withErrorHandling("list_notification_channels", async (args) => {
@@ -1036,27 +1303,28 @@ function registerNotificationChannelTools(server, ctx, helpers) {
1036
1303
  createdAt: ch.createdAt,
1037
1304
  dashboardUrl: notificationSettingsDashboardUrl()
1038
1305
  })),
1039
- total: channels.length
1306
+ total: channels.length,
1307
+ pagination: completeList(channels.length)
1040
1308
  });
1041
1309
  }));
1042
1310
  server.registerTool("create_notification_channel", {
1043
1311
  title: "Create Notification Channel",
1044
1312
  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
1313
  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)")
1314
+ channelType: z5.enum(["EMAIL", "WEBHOOK", "SLACK", "WEBSOCKET"]).describe("Type of notification channel"),
1315
+ name: z5.string().optional().describe("Name for the channel (defaults to 'default')"),
1316
+ config: z5.object({
1317
+ recipients: z5.array(z5.string()).optional().describe("Email recipients (required for EMAIL channels)"),
1318
+ webhookUrl: z5.string().optional().describe("Webhook/Slack endpoint URL (required for WEBHOOK, optional for SLACK)"),
1319
+ httpMethod: z5.enum(["POST", "GET", "PUT", "PATCH"]).optional().describe("HTTP method for WEBHOOK channels (defaults to POST)"),
1320
+ slackChannelId: z5.string().optional().describe("Slack channel ID from OAuth integration (for SLACK channels, e.g. C01ABCDEF)"),
1321
+ slackChannelName: z5.string().optional().describe("Slack channel name (for SLACK channels, e.g. #alerts)"),
1322
+ auth: z5.object({
1323
+ type: z5.enum(["bearer", "basic", "header"]).describe("Authentication type"),
1324
+ token: z5.string().optional().describe("Bearer token (for 'bearer' type)"),
1325
+ username: z5.string().optional().describe("Username (for 'basic' type)"),
1326
+ password: z5.string().optional().describe("Password (for 'basic' type)"),
1327
+ headers: z5.object({}).passthrough().optional().describe("Custom headers as key-value pairs (for 'header' type)")
1060
1328
  }).optional().describe("Authentication for WEBHOOK channels")
1061
1329
  }).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
1330
  }),
@@ -1103,8 +1371,8 @@ function registerNotificationSettingsTools(server, ctx, helpers) {
1103
1371
  title: "Delete Notification Channel",
1104
1372
  description: "Delete a notification channel by ID. " + "Call first without confirmed=true to preview, then with confirmed=true after user agrees.",
1105
1373
  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.")
1374
+ channelId: z5.string().min(1).describe("The channel ID to delete"),
1375
+ confirmed: z5.preprocess(coerceBoolean(), z5.boolean()).optional().describe("Set to true only after the user has explicitly confirmed deletion.")
1108
1376
  }),
1109
1377
  annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false }
1110
1378
  }, withErrorHandling("delete_notification_channel", async (args) => {
@@ -1125,7 +1393,7 @@ function registerNotificationSettingsTools(server, ctx, helpers) {
1125
1393
  title: "List Notification Settings",
1126
1394
  description: "List notification settings (event-to-channel mappings) for a workflow or workspace. Shows which events trigger notifications and through which channels.",
1127
1395
  inputSchema: strictSchema({
1128
- workflowId: z3.string().optional().describe("Filter settings for a specific workflow. Omit to list workspace-level settings."),
1396
+ workflowId: z5.string().optional().describe("Filter settings for a specific workflow. Omit to list workspace-level settings."),
1129
1397
  eventType: NotificationEventTypeSchema.optional().describe("Filter by event type")
1130
1398
  }),
1131
1399
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
@@ -1142,33 +1410,34 @@ function registerNotificationSettingsTools(server, ctx, helpers) {
1142
1410
  workflowId: s.workflowId,
1143
1411
  eventType: s.eventType,
1144
1412
  enabled: s.enabled,
1145
- channels: s.channels,
1413
+ channels: redactChannelRefs(s.channels),
1146
1414
  createdAt: s.createdAt
1147
1415
  })),
1148
- total: settings.length
1416
+ total: settings.length,
1417
+ pagination: completeList(settings.length)
1149
1418
  });
1150
1419
  }));
1151
1420
  server.registerTool("configure_notifications", {
1152
1421
  title: "Configure Notifications",
1153
1422
  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
1423
  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.")
1424
+ workflowId: z5.string().optional().describe("Workflow ID to configure notifications for. Omit for workspace-level notifications."),
1425
+ 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.'),
1426
+ channels: z5.object({
1427
+ email: z5.object({
1428
+ recipients: z5.array(z5.string().email()).optional().describe("Email addresses to notify. Omit to use the account default email.")
1160
1429
  }).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)"),
1430
+ webhook: z5.object({
1431
+ url: z5.string().url().describe("Webhook endpoint URL"),
1432
+ httpMethod: z5.enum(["POST", "GET", "PUT", "PATCH"]).optional().describe("HTTP method (defaults to POST)"),
1164
1433
  auth: webhookAuthShape
1165
1434
  }).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)")
1435
+ slack: z5.object({
1436
+ webhookUrl: z5.string().url().optional().describe("Slack incoming webhook URL (legacy)"),
1437
+ slackChannelId: z5.string().optional().describe("Slack channel ID from OAuth integration (e.g. C01ABCDEF)"),
1438
+ slackChannelName: z5.string().optional().describe("Slack channel name (e.g. #alerts)")
1170
1439
  }).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")
1440
+ websocket: z5.boolean().optional().describe("Enable WebSocket notifications")
1172
1441
  }).describe("At least one channel must be specified")
1173
1442
  }),
1174
1443
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }
@@ -1178,7 +1447,7 @@ function registerNotificationSettingsTools(server, ctx, helpers) {
1178
1447
  const ch = args.channels;
1179
1448
  if (ch.email) {
1180
1449
  if (ch.email.recipients?.length) {
1181
- channelSetup.EMAIL = { name: "mcp-email", recipients: ch.email.recipients };
1450
+ channelSetup.EMAIL = { name: mcpChannelName("EMAIL", ch.email), recipients: ch.email.recipients };
1182
1451
  } else {
1183
1452
  channelSetup.EMAIL = true;
1184
1453
  }
@@ -1187,7 +1456,7 @@ function registerNotificationSettingsTools(server, ctx, helpers) {
1187
1456
  channelSetup.WEBSOCKET = true;
1188
1457
  if (ch.webhook) {
1189
1458
  const webhookChannel = {
1190
- name: "mcp-webhook",
1459
+ name: mcpChannelName("WEBHOOK", { webhookUrl: ch.webhook.url }),
1191
1460
  webhookUrl: ch.webhook.url,
1192
1461
  httpMethod: ch.webhook.httpMethod || "POST"
1193
1462
  };
@@ -1196,7 +1465,7 @@ function registerNotificationSettingsTools(server, ctx, helpers) {
1196
1465
  channelSetup.WEBHOOK = webhookChannel;
1197
1466
  }
1198
1467
  if (ch.slack) {
1199
- const slackChannel = { name: "mcp-slack" };
1468
+ const slackChannel = { name: mcpChannelName("SLACK", ch.slack) };
1200
1469
  if (ch.slack.webhookUrl)
1201
1470
  slackChannel.webhookUrl = ch.slack.webhookUrl;
1202
1471
  if (ch.slack.slackChannelId)
@@ -1222,7 +1491,7 @@ function registerNotificationSettingsTools(server, ctx, helpers) {
1222
1491
  id: s.id,
1223
1492
  eventType: s.eventType,
1224
1493
  enabled: s.enabled,
1225
- channels: s.channels
1494
+ channels: redactChannelRefs(s.channels)
1226
1495
  }))
1227
1496
  });
1228
1497
  }));
@@ -1230,8 +1499,8 @@ function registerNotificationSettingsTools(server, ctx, helpers) {
1230
1499
  title: "Delete Notification Setting",
1231
1500
  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
1501
  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.")
1502
+ settingId: z5.string().min(1).describe("The notification setting ID to delete (the id from list_notification_settings)"),
1503
+ confirmed: z5.preprocess(coerceBoolean(), z5.boolean()).optional().describe("Set to true only after the user has explicitly confirmed deletion.")
1235
1504
  }),
1236
1505
  annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false }
1237
1506
  }, withErrorHandling("delete_notification_setting", async (args) => {
@@ -1250,17 +1519,14 @@ function registerNotificationSettingsTools(server, ctx, helpers) {
1250
1519
  }));
1251
1520
  }
1252
1521
  var init_notifications = __esm(() => {
1522
+ init_notification_channels();
1523
+ init_pagination();
1253
1524
  init_schemas();
1254
1525
  });
1255
1526
 
1256
1527
  // src/tools/observability.ts
1257
1528
  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
- }
1529
+ import { z as z6 } from "zod";
1264
1530
  function mapActivityEvent(event, includeDetails) {
1265
1531
  const details = event.details ?? {};
1266
1532
  const summary = details._changeSummary;
@@ -1333,10 +1599,10 @@ function registerObservabilityTools(server, ctx, helpers) {
1333
1599
  title: "Get Observability Metrics",
1334
1600
  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
1601
  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).")
1602
+ 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."),
1603
+ includeWorkflows: z6.preprocess(coerceBoolean(), z6.boolean()).optional().describe("Also return every workflow ranked by severity, not only the down and degraded ones (default false)."),
1604
+ includeDailySeries: z6.preprocess(coerceBoolean(), z6.boolean()).optional().describe("Also return the team-level daily series of successful runs, failed runs, and uptime (default false)."),
1605
+ 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
1606
  }),
1341
1607
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
1342
1608
  }, withErrorHandling("get_observability", async (args) => {
@@ -1392,22 +1658,23 @@ function registerObservabilityTools(server, ctx, helpers) {
1392
1658
  title: "List Activity",
1393
1659
  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
1660
  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)")
1661
+ limit: limitInput({ noun: "Events", defaultLimit: ACTIVITY_DEFAULT_LIMIT, max: ACTIVITY_MAX_LIMIT }),
1662
+ offset: offsetInput("offset", "events"),
1663
+ workflowId: z6.string().optional().describe("Only events about this workflow (runs, config changes, data changes, notifications)"),
1664
+ userId: z6.string().optional().describe("Only events by this user id. Resolve an email to an id from a previous list_activity result."),
1665
+ startDate: z6.string().optional().describe("ISO 8601 lower bound. Ignored when relativeTime is set."),
1666
+ endDate: z6.string().optional().describe("ISO 8601 upper bound. Ignored when relativeTime is set."),
1667
+ 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."),
1668
+ 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."),
1669
+ interfaces: z6.preprocess(coerceList({ upperCase: true }), z6.array(z6.enum(ACTIVITY_INTERFACES))).optional().describe("Interfaces the action came from"),
1670
+ resourceTypes: z6.preprocess(coerceList({ upperCase: true }), z6.array(z6.enum(ACTIVITY_RESOURCE_TYPES))).optional().describe("Resource types to keep"),
1671
+ includeDetails: z6.preprocess(coerceBoolean(), z6.boolean()).optional().describe("Include sanitized previousValue/newValue snapshots per event (default false; large)")
1406
1672
  }),
1407
1673
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
1408
1674
  }, withErrorHandling("list_activity", async (args) => {
1675
+ const limit = args.limit ?? ACTIVITY_DEFAULT_LIMIT;
1409
1676
  const result = await ctx.client.activity.list({
1410
- limit: boundActivityLimit(args.limit),
1677
+ limit,
1411
1678
  offset: args.offset,
1412
1679
  workflowId: args.workflowId,
1413
1680
  userId: args.userId,
@@ -1421,7 +1688,15 @@ function registerObservabilityTools(server, ctx, helpers) {
1421
1688
  const events = result.events.map((event) => mapActivityEvent(event, args.includeDetails === true));
1422
1689
  return jsonResult({
1423
1690
  events,
1424
- pagination: result.pagination,
1691
+ pagination: offsetPage({
1692
+ args,
1693
+ param: "offset",
1694
+ offset: args.offset ?? 0,
1695
+ limit,
1696
+ returned: events.length,
1697
+ totalCount: result.pagination?.total,
1698
+ hasMore: result.pagination?.hasMore
1699
+ }),
1425
1700
  ...events.length === 0 ? {
1426
1701
  hint: "No activity matched these filters. Widen relativeTime or drop a filter before concluding nothing happened."
1427
1702
  } : {}
@@ -1430,6 +1705,7 @@ function registerObservabilityTools(server, ctx, helpers) {
1430
1705
  }
1431
1706
  var ACTIVITY_DEFAULT_LIMIT = 25, ACTIVITY_MAX_LIMIT = 200, ACTIVITY_EVENT_TYPES, ACTIVITY_INTERFACES, ACTIVITY_RESOURCE_TYPES, HEALTH_SEVERITY;
1432
1707
  var init_observability = __esm(() => {
1708
+ init_pagination();
1433
1709
  init_schemas();
1434
1710
  ACTIVITY_EVENT_TYPES = [
1435
1711
  "CREATED",
@@ -1470,30 +1746,174 @@ var init_observability = __esm(() => {
1470
1746
  HEALTH_SEVERITY = { down: 0, degraded: 1, idle: 2, healthy: 3 };
1471
1747
  });
1472
1748
 
1749
+ // src/tools/shared/budget.ts
1750
+ function jsonBytes(value) {
1751
+ const text = JSON.stringify(value);
1752
+ return text === undefined ? 0 : Buffer.byteLength(text, "utf8");
1753
+ }
1754
+ function truncateUtf8(text, maxBytes) {
1755
+ if (Buffer.byteLength(text, "utf8") <= maxBytes)
1756
+ return text;
1757
+ const { read } = new TextEncoder().encodeInto(text, new Uint8Array(Math.max(0, maxBytes)));
1758
+ return text.slice(0, read);
1759
+ }
1760
+ function previewValue(value, maxBytes = VALUE_PREVIEW_BYTES) {
1761
+ if (value === undefined || value === null)
1762
+ return null;
1763
+ const bytes = Buffer.byteLength(JSON.stringify(value), "utf8");
1764
+ if (bytes <= maxBytes)
1765
+ return value;
1766
+ if (typeof value === "string")
1767
+ return `${truncateUtf8(value, maxBytes)}… [cut, ${bytes} bytes]`;
1768
+ if (Array.isArray(value))
1769
+ return `[array of ${value.length} items, ${bytes} bytes]`;
1770
+ return `[${typeof value}, ${bytes} bytes]`;
1771
+ }
1772
+ function fitItems(options) {
1773
+ const budget = options.budgetBytes ?? RESULT_BUDGET_BYTES;
1774
+ const all = [...options.items];
1775
+ if (jsonBytes(options.build(all)) <= budget)
1776
+ return { kept: all, omitted: 0, truncated: false };
1777
+ const limit = budget - TRUNCATION_RESERVE_BYTES - (options.reserveBytes ?? 0);
1778
+ let lo = 0;
1779
+ let hi = all.length - 1;
1780
+ while (lo < hi) {
1781
+ const mid = Math.ceil((lo + hi) / 2);
1782
+ if (jsonBytes(options.build(all.slice(0, mid))) <= limit)
1783
+ lo = mid;
1784
+ else
1785
+ hi = mid - 1;
1786
+ }
1787
+ return { kept: all.slice(0, lo), omitted: all.length - lo, truncated: true };
1788
+ }
1789
+ function truncationNotice(options) {
1790
+ const budgetBytes = options.budgetBytes ?? RESULT_BUDGET_BYTES;
1791
+ const total = options.kept + options.omitted;
1792
+ return {
1793
+ truncated: true,
1794
+ truncation: { kept: options.kept, omitted: options.omitted, budgetBytes },
1795
+ hint: `Result cut to ${options.kept} of ${total} ${options.noun} to stay under ${Math.round(budgetBytes / 1000)} KB. ${options.hint}`
1796
+ };
1797
+ }
1798
+ function highlyDivisibleFloor(n) {
1799
+ if (n <= 0)
1800
+ return 0;
1801
+ if (n >= 10)
1802
+ return Math.floor(n / 10) * 10;
1803
+ let p = 1;
1804
+ while (p * 2 <= n)
1805
+ p *= 2;
1806
+ return p;
1807
+ }
1808
+ function alignedKeep(offset, fit) {
1809
+ if (offset === 0)
1810
+ return highlyDivisibleFloor(fit);
1811
+ for (let keep = fit;keep >= 1; keep--) {
1812
+ if (offset % keep === 0 && offset / keep + 2 <= MAX_PAGE)
1813
+ return keep;
1814
+ }
1815
+ return 0;
1816
+ }
1817
+ function resumeNumberedPage(meta, options) {
1818
+ const { next: _replaced, totalPages: _droppedTotalPages, ...rest } = meta;
1819
+ if (options.kept === 0)
1820
+ return { ...rest, hasMore: false };
1821
+ const offset = (options.page - 1) * options.limit;
1822
+ return {
1823
+ ...rest,
1824
+ hasMore: true,
1825
+ next: nextArgs(options.args, { page: offset / options.kept + 2, limit: options.kept })
1826
+ };
1827
+ }
1828
+ function budgetNumberedPage(options) {
1829
+ const offset = (options.page - 1) * options.limit;
1830
+ const fit = fitItems({
1831
+ items: options.items,
1832
+ build: (kept) => options.build(kept, options.pagination),
1833
+ reserveBytes: jsonBytes({ pagination: { next: options.args } })
1834
+ });
1835
+ if (!fit.truncated)
1836
+ return options.build([...options.items], options.pagination);
1837
+ const buildCut = (fitCount) => {
1838
+ const keep = alignedKeep(offset, fitCount);
1839
+ const alignmentImpossible = keep === 0 && fitCount > 0;
1840
+ const pagination = resumeNumberedPage(options.pagination, {
1841
+ args: options.args,
1842
+ page: options.page,
1843
+ limit: options.limit,
1844
+ kept: keep
1845
+ });
1846
+ const hint = keep > 0 ? options.resumeHint : alignmentImpossible ? options.alignmentImpossibleHint ?? DEFAULT_ALIGNMENT_IMPOSSIBLE_HINT : options.emptyHint;
1847
+ const result = {
1848
+ ...options.build(options.items.slice(0, keep), pagination),
1849
+ ...truncationNotice({ kept: keep, omitted: options.items.length - keep, noun: options.noun, hint })
1850
+ };
1851
+ return { result, keep };
1852
+ };
1853
+ let fitCount = fit.kept.length;
1854
+ let { result, keep } = buildCut(fitCount);
1855
+ while (jsonBytes(result) > RESULT_BUDGET_BYTES && keep > 0) {
1856
+ fitCount = keep - 1;
1857
+ ({ result, keep } = buildCut(fitCount));
1858
+ }
1859
+ return result;
1860
+ }
1861
+ 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.";
1862
+ var init_budget = __esm(() => {
1863
+ init_pagination();
1864
+ });
1865
+
1473
1866
  // src/tools/scrape.ts
1474
- import { z as z5 } from "zod";
1475
- function registerScrapeTools(server, ctx, helpers) {
1867
+ import { z as z7 } from "zod";
1868
+ function scrapeResult(response, format) {
1869
+ const { content, ...metadata } = response;
1870
+ if (typeof content !== "string" || content.length === 0)
1871
+ return textOnlyResult(metadata);
1872
+ const contentBytes = Buffer.byteLength(content, "utf8");
1873
+ if (response.isBase64) {
1874
+ return textOnlyResult({
1875
+ ...metadata,
1876
+ contentBytes,
1877
+ contentOmitted: "binary",
1878
+ hint: `The URL returned binary content (${response.contentType ?? "unknown type"}), which scrape does not return inline.`
1879
+ });
1880
+ }
1881
+ const text = truncateUtf8(content, SCRAPE_CONTENT_BUDGET_BYTES);
1882
+ const keptBytes = Buffer.byteLength(text, "utf8");
1883
+ const cut = keptBytes < contentBytes;
1884
+ 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.";
1885
+ const meta = {
1886
+ ...metadata,
1887
+ contentBytes,
1888
+ ...cut && {
1889
+ truncated: true,
1890
+ truncation: { keptBytes, omittedBytes: contentBytes - keptBytes, budgetBytes: SCRAPE_CONTENT_BUDGET_BYTES },
1891
+ hint
1892
+ }
1893
+ };
1894
+ return withExtraContent(textOnlyResult(meta), { type: "text", text });
1895
+ }
1896
+ function registerScrapeTools(server, ctx, helpers, defaultProductType) {
1476
1897
  const { withErrorHandling } = helpers;
1477
1898
  server.registerTool("scrape", {
1478
1899
  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.",
1900
+ 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. " + (defaultProductType === "realtime" ? "Use create_realtime_monitor for monitoring, and create_workflow for one-time or scheduled structured extraction or navigation-heavy jobs. " : "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
1901
  inputSchema: strictSchema({
1481
- url: z5.url().refine((value) => {
1902
+ url: z7.url().refine((value) => {
1482
1903
  const protocol = new URL(value).protocol;
1483
1904
  return protocol === "http:" || protocol === "https:";
1484
1905
  }, "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.")
1906
+ format: z7.enum(["html", "markdown"]).optional().describe("Output format. Defaults to markdown for compact LLM consumption; use html for raw source.")
1486
1907
  }),
1487
1908
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }
1488
1909
  }, 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);
1910
+ const format = args.format ?? "markdown";
1911
+ const response = await ctx.client.scrape.fetch({ url: args.url, format });
1912
+ return scrapeResult(response, format);
1494
1913
  }));
1495
1914
  }
1496
1915
  var init_scrape = __esm(() => {
1916
+ init_budget();
1497
1917
  init_schemas();
1498
1918
  });
1499
1919
 
@@ -1649,7 +2069,7 @@ var init_errors = () => {};
1649
2069
 
1650
2070
  // src/tools/support.ts
1651
2071
  import { KadoaHttpException as KadoaHttpException2 } from "@kadoa/node-sdk";
1652
- import { z as z6 } from "zod";
2072
+ import { z as z8 } from "zod";
1653
2073
  function supportTicketSkipMessage(reason) {
1654
2074
  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
2075
  }
@@ -1673,11 +2093,11 @@ function registerSupportTools(server, ctx, helpers) {
1673
2093
  title: "Create Support Ticket",
1674
2094
  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
2095
  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.")
2096
+ workflowId: z8.string().min(1).describe("The workflow the problem is about"),
2097
+ title: z8.string().describe("One-line summary of the problem"),
2098
+ description: z8.string().describe("What is wrong, what was observed, and what was already tried"),
2099
+ category: z8.enum(["bug", "feature", "workflow_issue", "template_issue", "integration", "billing", "data_diff"]).optional().describe("Support category; defaults to a workflow issue"),
2100
+ jobId: z8.string().optional().describe("The failing run's id (from list_workflow_runs). Required when filing from the Kadoa Assistant.")
1681
2101
  }),
1682
2102
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }
1683
2103
  }, withErrorHandling("create_support_ticket", async (args) => {
@@ -1741,29 +2161,36 @@ var init_support = __esm(() => {
1741
2161
  });
1742
2162
 
1743
2163
  // src/tools/team.ts
1744
- import { z as z7 } from "zod";
2164
+ import { z as z9 } from "zod";
1745
2165
  function registerWhoamiTool(server, ctx, helpers) {
1746
2166
  const { withErrorHandling } = helpers;
1747
2167
  server.registerTool("whoami", {
1748
2168
  title: "Show Current User",
1749
- description: "Show current user details: email, authentication method, and team memberships",
2169
+ description: "Show current user details: email, authentication method, team memberships, and the team's default product",
1750
2170
  inputSchema: strictSchema({}),
1751
2171
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
1752
2172
  }, withErrorHandling("whoami", async () => {
2173
+ const defaultProductTypePromise = ctx.client.user.getFeatures().then((features) => parseDefaultProductType(features.defaultProductType), () => {
2174
+ return;
2175
+ });
1753
2176
  if (ctx.principal.connector) {
1754
- const teams = await ctx.client.listTeams();
2177
+ const [teams, defaultProductType] = await Promise.all([
2178
+ ctx.client.listTeams(),
2179
+ defaultProductTypePromise
2180
+ ]);
1755
2181
  const activeTeam = teams.find((team) => team.id === ctx.principal.teamId);
1756
2182
  if (!activeTeam) {
1757
2183
  throw new Error("The active team is unavailable for this credential. Reconnect and choose a team.");
1758
2184
  }
1759
2185
  return jsonResult({
1760
2186
  authMethod: "Shared organization service account",
1761
- team: { name: activeTeam.name }
2187
+ team: { name: activeTeam.name, ...defaultProductType ? { defaultProductType } : {} }
1762
2188
  });
1763
2189
  }
1764
- const [user, teams] = await Promise.all([
2190
+ const [user, teams, defaultProductType] = await Promise.all([
1765
2191
  ctx.client.user.getCurrentUser(),
1766
- ctx.client.listTeams()
2192
+ ctx.client.listTeams(),
2193
+ defaultProductTypePromise
1767
2194
  ]);
1768
2195
  const activeTeam = teams.find((team) => team.id === ctx.principal.teamId);
1769
2196
  if (!activeTeam) {
@@ -1774,7 +2201,8 @@ function registerWhoamiTool(server, ctx, helpers) {
1774
2201
  authMethod: ctx.principal.actor ? `Kadoa Assistant (delegated via ${ctx.principal.actor.sub})` : "Kadoa access token",
1775
2202
  team: {
1776
2203
  name: activeTeam.name,
1777
- ...activeTeam.memberRole ? { memberRole: activeTeam.memberRole } : {}
2204
+ ...activeTeam.memberRole ? { memberRole: activeTeam.memberRole } : {},
2205
+ ...defaultProductType ? { defaultProductType } : {}
1778
2206
  },
1779
2207
  scopes: ctx.principal.scopes
1780
2208
  });
@@ -1803,7 +2231,7 @@ function registerTeamTools(server, ctx, helpers) {
1803
2231
  title: "Switch Team",
1804
2232
  description: "Switch the active team by name or ID. After switching, all subsequent API calls will use the new team context.",
1805
2233
  inputSchema: strictSchema({
1806
- teamIdentifier: z7.string().min(1).describe("Team name or team ID to switch to")
2234
+ teamIdentifier: z9.string().min(1).describe("Team name or team ID to switch to")
1807
2235
  }),
1808
2236
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false }
1809
2237
  }, withErrorHandling("team_switch", async () => errorResult("team_switch is not available for this credential. Reconnect and choose the team at sign-in.")));
@@ -1814,7 +2242,7 @@ var init_team = __esm(() => {
1814
2242
  });
1815
2243
 
1816
2244
  // src/tools/templates.ts
1817
- import { z as z8 } from "zod";
2245
+ import { z as z10 } from "zod";
1818
2246
  function normalizeTemplateSchemaField(val) {
1819
2247
  if (val === null || typeof val !== "object" || Array.isArray(val))
1820
2248
  return val;
@@ -1844,6 +2272,7 @@ function registerTemplateTools(server, ctx, helpers) {
1844
2272
  return jsonResult({
1845
2273
  templates: templates.map(withTemplateDashboardUrl),
1846
2274
  count: templates.length,
2275
+ pagination: completeList(templates.length),
1847
2276
  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
2277
  });
1849
2278
  }));
@@ -1851,7 +2280,7 @@ function registerTemplateTools(server, ctx, helpers) {
1851
2280
  title: "Get Template",
1852
2281
  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
2282
  inputSchema: strictSchema({
1854
- templateId: z8.string().min(1).describe("The template ID")
2283
+ templateId: z10.string().min(1).describe("The template ID")
1855
2284
  }),
1856
2285
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
1857
2286
  }, withErrorHandling("get_template", async (args) => {
@@ -1865,8 +2294,8 @@ function registerTemplateTools(server, ctx, helpers) {
1865
2294
  title: "Create Template",
1866
2295
  description: "Create a new template. After creation, use create_template_version to publish a version with prompt, schema, and notifications.",
1867
2296
  inputSchema: strictSchema({
1868
- name: z8.string().describe("Template name (1-255 chars)"),
1869
- description: z8.string().optional().describe("Template description (max 2000 chars)")
2297
+ name: z10.string().describe("Template name (1-255 chars)"),
2298
+ description: z10.string().optional().describe("Template description (max 2000 chars)")
1870
2299
  }),
1871
2300
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }
1872
2301
  }, withErrorHandling("create_template", async (args) => {
@@ -1886,9 +2315,9 @@ function registerTemplateTools(server, ctx, helpers) {
1886
2315
 
1887
2316
  ` + "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
2317
  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")
2318
+ templateId: z10.string().min(1).describe("The template ID to update"),
2319
+ name: z10.preprocess(coerceNull(), z10.string().optional()).optional().describe("New template name"),
2320
+ description: z10.preprocess(coerceNull(), z10.string().optional()).optional().describe("New template description")
1892
2321
  }),
1893
2322
  annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false }
1894
2323
  }, withErrorHandling("update_template", async (args) => {
@@ -1904,8 +2333,8 @@ function registerTemplateTools(server, ctx, helpers) {
1904
2333
  title: "Delete Template",
1905
2334
  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
2335
  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.")
2336
+ templateId: z10.string().min(1).describe("The template ID to delete"),
2337
+ 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
2338
  }),
1910
2339
  annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false }
1911
2340
  }, withErrorHandling("delete_template", async (args) => {
@@ -1929,15 +2358,15 @@ function registerTemplateTools(server, ctx, helpers) {
1929
2358
  });
1930
2359
  }));
1931
2360
  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"),
2361
+ name: z10.string().describe("Field name"),
2362
+ description: z10.string().optional().describe("Field description"),
2363
+ fieldType: z10.enum(["SCHEMA", "CLASSIFICATION"]).optional().describe("Field role: SCHEMA (default) for extracted data, CLASSIFICATION for a field with predefined categories"),
2364
+ example: z10.string().optional().describe("Example value"),
1936
2365
  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")
2366
+ isKey: z10.preprocess(coerceBoolean(), z10.boolean()).optional().describe("Whether the field is a key field"),
2367
+ categories: z10.preprocess(coerceArray(), z10.array(z10.object({
2368
+ title: z10.string().describe("Short title/label of the category"),
2369
+ definition: z10.string().describe("Full definition/description of the category")
1941
2370
  }))).optional().describe("Predefined categories for a CLASSIFICATION field ({title, definition}[]). Required for fieldType=CLASSIFICATION; omitted otherwise.")
1942
2371
  };
1943
2372
  async function latestTemplateVersionRules(templateId) {
@@ -1962,25 +2391,25 @@ function registerTemplateTools(server, ctx, helpers) {
1962
2391
 
1963
2392
  ` + "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
2393
  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()
2394
+ templateId: z10.string().min(1).describe("The template ID to publish a version for"),
2395
+ prompt: z10.string().optional().describe("User prompt to copy into workflow config"),
2396
+ schemaId: z10.string().optional().describe("Existing schema ID to reference (mutually exclusive with schemaFields)"),
2397
+ 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)"),
2398
+ schemaEntity: z10.string().optional().describe("Entity name for the inline schema"),
2399
+ 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.'),
2400
+ notifications: z10.preprocess(coerceArray(), z10.array(z10.object({
2401
+ eventType: z10.string().describe("Notification event type"),
2402
+ eventConfiguration: z10.preprocess(coerceJson(), z10.record(z10.string(), z10.unknown())).optional(),
2403
+ enabled: z10.preprocess(coerceBoolean(), z10.boolean()).optional(),
2404
+ channelIds: z10.preprocess(coerceArray(), z10.array(z10.string().min(1))).optional(),
2405
+ channels: z10.preprocess(coerceArray(), z10.array(z10.object({ channelId: z10.string() }))).optional()
1977
2406
  }))).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')")
2407
+ frequency: z10.preprocess(coerceJson(), z10.object({
2408
+ interval: z10.enum([...UpdateIntervalSchema.options, "REAL_TIME"]).optional().describe("Scheduling interval the template controls for linked workflows. Use CUSTOM with 'schedules' for cron."),
2409
+ schedules: z10.preprocess(coerceArray(), z10.array(z10.object({
2410
+ type: z10.enum(["repeat", "cron"]).describe("'repeat' (interval token) or 'cron' (cron expressions)"),
2411
+ interval: z10.string().optional().describe("Repeat token (e.g. '1h', '1d') when type='repeat'"),
2412
+ expressions: z10.preprocess(coerceArray(true), z10.array(z10.string())).optional().describe("Cron expressions when type='cron' (e.g. '0 8 * * 2')")
1984
2413
  }))).optional().describe("Schedule specs. Required when interval='CUSTOM'.")
1985
2414
  })).optional().describe("Template-controlled scheduling for linked workflows ({interval, schedules}). Linked workflows inherit and cannot override this when set.")
1986
2415
  })),
@@ -2010,11 +2439,11 @@ function registerTemplateTools(server, ctx, helpers) {
2010
2439
  title: "Save Workflow as Template",
2011
2440
  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
2441
  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.")
2442
+ workflowId: z10.string().min(1).describe("Source workflow ID to extract config from"),
2443
+ name: z10.string().optional().describe("Name for the new template (required if templateId is not set)"),
2444
+ description: z10.string().optional().describe("Description for the new template"),
2445
+ templateId: z10.string().optional().describe("Existing template ID to add a new version to (mutually exclusive with name)"),
2446
+ 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
2447
  }),
2019
2448
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }
2020
2449
  }, withErrorHandling("save_workflow_as_template", async (args) => {
@@ -2035,20 +2464,20 @@ function registerTemplateTools(server, ctx, helpers) {
2035
2464
  title: "List Template Schemas",
2036
2465
  description: "List schemas associated with a template.",
2037
2466
  inputSchema: strictSchema({
2038
- templateId: z8.string().min(1).describe("The template ID")
2467
+ templateId: z10.string().min(1).describe("The template ID")
2039
2468
  }),
2040
2469
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
2041
2470
  }, withErrorHandling("list_template_schemas", async (args) => {
2042
2471
  const schemas = await ctx.client.template.listSchemas(args.templateId);
2043
- return jsonResult({ schemas, count: schemas.length });
2472
+ return jsonResult({ schemas, count: schemas.length, pagination: completeList(schemas.length) });
2044
2473
  }));
2045
2474
  server.registerTool("link_template_to_workflows", {
2046
2475
  title: "Link Template to Workflows",
2047
2476
  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
2477
  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.")
2478
+ templateId: z10.string().min(1).describe("The template ID to link workflows to"),
2479
+ 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."),
2480
+ force: z10.preprocess(coerceBoolean(), z10.boolean()).optional().describe("If true, relink workflows that are already linked to another template. Defaults to false.")
2052
2481
  }),
2053
2482
  annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false }
2054
2483
  }, withErrorHandling("link_template_to_workflows", async (args) => {
@@ -2075,9 +2504,9 @@ function registerTemplateTools(server, ctx, helpers) {
2075
2504
  title: "Apply Template Version",
2076
2505
  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
2506
  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.")
2507
+ templateId: z10.string().min(1).describe("The template ID whose version to apply"),
2508
+ targetVersion: z10.preprocess(coerceNumber(), z10.number()).describe("The template version number to apply to the workflows"),
2509
+ 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
2510
  }),
2082
2511
  annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false }
2083
2512
  }, withErrorHandling("apply_template_version", async (args) => {
@@ -2097,19 +2526,19 @@ function registerTemplateTools(server, ctx, helpers) {
2097
2526
  title: "List Template Workflows",
2098
2527
  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
2528
  inputSchema: strictSchema({
2100
- templateId: z8.string().min(1).describe("The template ID")
2529
+ templateId: z10.string().min(1).describe("The template ID")
2101
2530
  }),
2102
2531
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
2103
2532
  }, withErrorHandling("list_template_workflows", async (args) => {
2104
2533
  const workflows = await ctx.client.template.getLinkedWorkflows(args.templateId);
2105
- return jsonResult({ workflows, count: workflows.length });
2534
+ return jsonResult({ workflows, count: workflows.length, pagination: completeList(workflows.length) });
2106
2535
  }));
2107
2536
  server.registerTool("unlink_template_from_workflows", {
2108
2537
  title: "Unlink Template from Workflows",
2109
2538
  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
2539
  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.")
2540
+ templateId: z10.string().min(1).describe("The template ID to unlink workflows from"),
2541
+ 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
2542
  }),
2114
2543
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false }
2115
2544
  }, withErrorHandling("unlink_template_from_workflows", async (args) => {
@@ -2126,21 +2555,22 @@ function registerTemplateTools(server, ctx, helpers) {
2126
2555
  }
2127
2556
  var LEGACY_DATA_TYPE_ALIASES;
2128
2557
  var init_templates = __esm(() => {
2558
+ init_pagination();
2129
2559
  init_schemas();
2130
2560
  LEGACY_DATA_TYPE_ALIASES = { TEXT: "STRING", URL: "LINK" };
2131
2561
  });
2132
2562
 
2133
2563
  // src/tools/variables.ts
2134
- import { z as z9 } from "zod";
2564
+ import { z as z11 } from "zod";
2135
2565
  function registerVariableTools(server, ctx, helpers) {
2136
2566
  const { withErrorHandling } = helpers;
2137
2567
  server.registerTool("create_variable", {
2138
2568
  title: "Create Variable",
2139
2569
  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
2570
  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)")
2571
+ key: z11.string().describe("Variable key (must be unique within scope). Referenced in prompts as @key."),
2572
+ value: z11.string().describe("Variable value"),
2573
+ dataType: z11.enum(["STRING", "SECRET", "OBJECT", "ARRAY"]).optional().describe("Data type of the value (defaults to STRING)")
2144
2574
  }),
2145
2575
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }
2146
2576
  }, withErrorHandling("create_variable", async (args) => {
@@ -2164,14 +2594,15 @@ function registerVariableTools(server, ctx, helpers) {
2164
2594
  const variables = await ctx.client.variable.list();
2165
2595
  return jsonResult({
2166
2596
  variables: variables.map(withVariableDashboardUrl),
2167
- count: variables.length
2597
+ count: variables.length,
2598
+ pagination: completeList(variables.length)
2168
2599
  });
2169
2600
  }));
2170
2601
  server.registerTool("get_variable", {
2171
2602
  title: "Get Variable",
2172
2603
  description: "Get a specific variable by its ID.",
2173
2604
  inputSchema: strictSchema({
2174
- variableId: z9.string().min(1).describe("The variable ID")
2605
+ variableId: z11.string().min(1).describe("The variable ID")
2175
2606
  }),
2176
2607
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
2177
2608
  }, withErrorHandling("get_variable", async (args) => {
@@ -2182,10 +2613,10 @@ function registerVariableTools(server, ctx, helpers) {
2182
2613
  title: "Update Variable",
2183
2614
  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
2615
  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")
2616
+ variableId: z11.string().min(1).describe("The variable ID to update"),
2617
+ key: z11.string().optional().describe("New variable key"),
2618
+ value: z11.string().optional().describe("New variable value"),
2619
+ dataType: z11.enum(["STRING", "SECRET", "OBJECT", "ARRAY"]).optional().describe("New data type")
2189
2620
  }),
2190
2621
  annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false }
2191
2622
  }, withErrorHandling("update_variable", async (args) => {
@@ -2201,8 +2632,8 @@ function registerVariableTools(server, ctx, helpers) {
2201
2632
  title: "Delete Variable",
2202
2633
  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
2634
  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.")
2635
+ variableId: z11.string().min(1).describe("The variable ID to delete"),
2636
+ 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
2637
  }),
2207
2638
  annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false }
2208
2639
  }, withErrorHandling("delete_variable", async (args) => {
@@ -2227,12 +2658,13 @@ function registerVariableTools(server, ctx, helpers) {
2227
2658
  }));
2228
2659
  }
2229
2660
  var init_variables = __esm(() => {
2661
+ init_pagination();
2230
2662
  init_schemas();
2231
2663
  });
2232
2664
 
2233
2665
  // src/tools/workflows.ts
2234
2666
  import { WorkflowStatusFilter } from "@kadoa/node-sdk";
2235
- import { z as z10 } from "zod";
2667
+ import { z as z12 } from "zod";
2236
2668
  function isRealTimeInterval(interval) {
2237
2669
  return interval?.toUpperCase().replace("-", "_") === "REAL_TIME";
2238
2670
  }
@@ -2289,6 +2721,49 @@ function mapRunStatus(state) {
2289
2721
  return state ?? "unknown";
2290
2722
  }
2291
2723
  }
2724
+ function changeResult(change) {
2725
+ const build = (data, differences) => ({
2726
+ id: change.id,
2727
+ workflowId: change.workflowId,
2728
+ url: change.url,
2729
+ summary: change.summary,
2730
+ screenshotUrl: change.screenshotUrl,
2731
+ data,
2732
+ differences,
2733
+ createdAt: change.createdAt
2734
+ });
2735
+ const full = build(change.data, change.differences);
2736
+ if (jsonBytes(full) <= RESULT_BUDGET_BYTES)
2737
+ return full;
2738
+ const rows = change.data ?? [];
2739
+ const exportHint = `For the full current dataset use export_data with workflowId ${change.workflowId}.`;
2740
+ if (jsonBytes(build([], change.differences)) <= RESULT_BUDGET_BYTES - TRUNCATION_RESERVE_BYTES) {
2741
+ const fit = fitItems({ items: rows, build: (kept) => build(kept, change.differences) });
2742
+ if (!fit.truncated)
2743
+ return build(fit.kept, change.differences);
2744
+ return {
2745
+ ...build(fit.kept, change.differences),
2746
+ ...truncationNotice({
2747
+ kept: fit.kept.length,
2748
+ omitted: fit.omitted,
2749
+ noun: "data rows",
2750
+ hint: `differences is complete. ${exportHint}`
2751
+ })
2752
+ };
2753
+ }
2754
+ const fit = fitItems({ items: change.differences ?? [], build: (kept) => build([], kept) });
2755
+ if (!fit.truncated)
2756
+ return build([], fit.kept);
2757
+ return {
2758
+ ...build([], fit.kept),
2759
+ ...truncationNotice({
2760
+ kept: fit.kept.length,
2761
+ omitted: fit.omitted,
2762
+ noun: "differences",
2763
+ hint: `All ${rows.length} data rows omitted. ${exportHint}`
2764
+ })
2765
+ };
2766
+ }
2292
2767
  function jsonEq(a, b) {
2293
2768
  return JSON.stringify(a) === JSON.stringify(b);
2294
2769
  }
@@ -2302,13 +2777,32 @@ function diffWorkflowAuditEntry(entry) {
2302
2777
  if (!jsonEq(prev[key], next[key]))
2303
2778
  changed.push(key);
2304
2779
  }
2305
- const prevPrompt = prev["additionalData"]?.["userPrompt"];
2306
- const nextPrompt = next["additionalData"]?.["userPrompt"];
2307
- if (!jsonEq(prevPrompt, nextPrompt))
2780
+ if (!jsonEq(auditFieldValue(prev, "additionalData.userPrompt"), auditFieldValue(next, "additionalData.userPrompt"))) {
2308
2781
  changed.push("additionalData.userPrompt");
2782
+ }
2309
2783
  return changed;
2310
2784
  }
2311
- function registerWorkflowCoreTools(server, ctx, helpers) {
2785
+ function auditFieldValue(snapshot, field) {
2786
+ if (field === "additionalData.userPrompt") {
2787
+ return snapshot["additionalData"]?.["userPrompt"];
2788
+ }
2789
+ return snapshot[field];
2790
+ }
2791
+ function summarizeWorkflowAuditEntry(entry) {
2792
+ const changedFields = diffWorkflowAuditEntry(entry);
2793
+ const prev = entry.previousValue ?? {};
2794
+ const next = entry.newValue ?? {};
2795
+ const changes = changedFields.map((field) => ({
2796
+ field,
2797
+ before: previewValue(auditFieldValue(prev, field)),
2798
+ after: previewValue(auditFieldValue(next, field))
2799
+ }));
2800
+ return { changedFields, changes };
2801
+ }
2802
+ function historyEmptyHint(includeSnapshots) {
2803
+ 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.";
2804
+ }
2805
+ function registerWorkflowCoreTools(server, ctx, helpers, defaultProductType) {
2312
2806
  const { withErrorHandling } = helpers;
2313
2807
  function resolveUrls(args) {
2314
2808
  const urls = args.urls ?? (args.url ? [args.url] : []);
@@ -2359,7 +2853,7 @@ function registerWorkflowCoreTools(server, ctx, helpers) {
2359
2853
  const channels = {};
2360
2854
  if (n.email) {
2361
2855
  if (typeof n.email === "object" && n.email.recipients?.length) {
2362
- channels.EMAIL = { name: "mcp-email", recipients: n.email.recipients };
2856
+ channels.EMAIL = { name: mcpChannelName("EMAIL", n.email), recipients: n.email.recipients };
2363
2857
  } else {
2364
2858
  channels.EMAIL = true;
2365
2859
  }
@@ -2368,7 +2862,7 @@ function registerWorkflowCoreTools(server, ctx, helpers) {
2368
2862
  channels.WEBSOCKET = true;
2369
2863
  if (n.webhook) {
2370
2864
  const webhookChannel = {
2371
- name: "mcp-webhook",
2865
+ name: mcpChannelName("WEBHOOK", { webhookUrl: n.webhook.url }),
2372
2866
  webhookUrl: n.webhook.url,
2373
2867
  httpMethod: n.webhook.httpMethod || "POST"
2374
2868
  };
@@ -2377,7 +2871,7 @@ function registerWorkflowCoreTools(server, ctx, helpers) {
2377
2871
  channels.WEBHOOK = webhookChannel;
2378
2872
  }
2379
2873
  if (n.slack) {
2380
- const slackChannel = { name: "mcp-slack" };
2874
+ const slackChannel = { name: mcpChannelName("SLACK", n.slack) };
2381
2875
  if (n.slack.webhookUrl)
2382
2876
  slackChannel.webhookUrl = n.slack.webhookUrl;
2383
2877
  if (n.slack.slackChannelId)
@@ -2416,18 +2910,18 @@ function registerWorkflowCoreTools(server, ctx, helpers) {
2416
2910
 
2417
2911
  ` + "Create a data extraction workflow using agentic navigation. Supports one-time or scheduled runs. " + "If entity and schema are provided, they guide the extraction; otherwise the AI agent auto-detects the schema from the page. " + "The workflow runs asynchronously and may take several minutes. Do NOT poll or sleep-wait for completion. " + `Return the workflow ID to the user and let them check back later with get_workflow or fetch_data.
2418
2912
 
2419
- ` + "PREFER TEMPLATES: If the user's request matches an existing template, instantiate it via `templateId` instead of writing a fresh prompt/schema. " + "Use `list_templates` to discover available templates and `get_template` to inspect schemas before deciding. " + "When creating from a template, call this tool with the template's `templateId`, the source `urls`, optional `templateVersion`, and optional `prompt` containing only workflow-specific additional instructions. Never copy the template prompt, entity, or schema into the call, and never silently match or rewrite inline configuration.\n\n" + "NOTE: This tool is for one-time or scheduled extraction ONLY. " + "For continuous realtime monitoring that watches a page for data changes and sends alerts, use the create_realtime_monitor tool instead.",
2913
+ ` + "PREFER TEMPLATES: If the user's request matches an existing template, instantiate it via `templateId` instead of writing a fresh prompt/schema. " + "Use `list_templates` to discover available templates and `get_template` to inspect schemas before deciding. " + "When creating from a template, call this tool with the template's `templateId`, the source `urls`, optional `templateVersion`, and optional `prompt` containing only workflow-specific additional instructions. Never copy the template prompt, entity, or schema into the call, and never silently match or rewrite inline configuration.\n\n" + "NOTE: This tool is for one-time or scheduled extraction ONLY. " + (defaultProductType === "realtime" ? `This team's default product is realtime monitoring. Do not call this tool unless the user explicitly asked for a one-time run (e.g. "once", "just the current list") or a schedule (e.g. "daily"). A request to get, extract, or scrape data that names no frequency is not explicit: call create_realtime_monitor instead.` : "For continuous realtime monitoring that watches a page for data changes and sends alerts, use the create_realtime_monitor tool instead."),
2420
2914
  inputSchema: strictSchema({
2421
2915
  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
2916
  ...extractionInputShape,
2423
2917
  ...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."),
2918
+ 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."),
2919
+ 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."),
2920
+ description: z12.string().max(500).optional().describe("Description of what this workflow does (max 500 characters)"),
2921
+ tags: z12.preprocess(coerceArray(true), z12.array(z12.string())).optional().describe("Tags for organizing workflows"),
2922
+ limit: z12.preprocess(coerceNumber(), z12.number()).optional().describe("Maximum number of records to extract per run. Useful for limiting scope or cost control."),
2429
2923
  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'."),
2924
+ 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
2925
  ...notificationsInputShape
2432
2926
  }),
2433
2927
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }
@@ -2441,13 +2935,25 @@ function registerWorkflowCoreTools(server, ctx, helpers) {
2441
2935
  }
2442
2936
  const n = args.notifications;
2443
2937
  const hasNotifications = !!(n && (n.email || n.webhook || n.slack || n.websocket));
2938
+ async function createOrUnknown(create) {
2939
+ try {
2940
+ return await create();
2941
+ } catch (error) {
2942
+ if (!isAmbiguousFailure(error))
2943
+ throw error;
2944
+ console.error("[Tool Error] create_workflow:", safeErrorSummary(error));
2945
+ return {
2946
+ 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.`)
2947
+ };
2948
+ }
2949
+ }
2444
2950
  let workflowId;
2445
2951
  const warnings = [];
2446
2952
  if (args.templateId) {
2447
2953
  if (args.entity || args.schema) {
2448
2954
  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
2955
  }
2450
- const { id } = await ctx.client.workflow.create({
2956
+ const created = await createOrUnknown(() => ctx.client.workflow.create({
2451
2957
  urls,
2452
2958
  name: args.name || "Untitled Workflow",
2453
2959
  description: args.description,
@@ -2458,8 +2964,10 @@ function registerWorkflowCoreTools(server, ctx, helpers) {
2458
2964
  templateId: args.templateId,
2459
2965
  templateVersion: args.templateVersion,
2460
2966
  userPrompt: args.prompt
2461
- });
2462
- workflowId = id;
2967
+ }));
2968
+ if ("outcomeUnknown" in created)
2969
+ return created.outcomeUnknown;
2970
+ workflowId = created.id;
2463
2971
  } else {
2464
2972
  if (!args.prompt) {
2465
2973
  return errorResult("'prompt' is required unless 'templateId' is provided.");
@@ -2473,7 +2981,9 @@ function registerWorkflowCoreTools(server, ctx, helpers) {
2473
2981
  description: args.description,
2474
2982
  schedules: args.schedules
2475
2983
  });
2476
- const workflow = await builder.create();
2984
+ const workflow = await createOrUnknown(() => builder.create());
2985
+ if ("outcomeUnknown" in workflow)
2986
+ return workflow.outcomeUnknown;
2477
2987
  workflowId = workflow.workflowId;
2478
2988
  const needsUpdate = args.limit !== undefined || args.tags && args.tags.length > 0;
2479
2989
  if (needsUpdate) {
@@ -2546,31 +3056,22 @@ function registerWorkflowCoreTools(server, ctx, helpers) {
2546
3056
  } else {
2547
3057
  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
3058
  }
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
- };
3059
+ return errorResult(text, {
3060
+ notificationChannelIds,
3061
+ ...workflowId ? { workflowId, dashboardUrl: workflowDashboardUrl(workflowId) } : {}
3062
+ });
2562
3063
  }
2563
3064
  server.registerTool("create_realtime_monitor", {
2564
3065
  title: "Create Realtime Monitor",
2565
- description: "Create a realtime monitoring workflow that continuously watches a page for data changes and sends alerts. " + "Use this when the user wants to monitor, track, or watch a page in realtime. " + "At least one notification channel (email, webhook, slack, or websocket) is required; notification settings are persisted before Assistant creation is dispatched. " + `Realtime monitors CANNOT be converted to/from one-time or scheduled extraction workflows after creation.
3066
+ description: "Create a realtime monitoring workflow that continuously watches a page for data changes and sends alerts. " + (defaultProductType === "realtime" ? "This team's default product is realtime monitoring: use this tool whenever the user asks to monitor, track, or watch a page, or to get or extract data from it, unless they explicitly ask for a one-time run or a schedule. A request that names no frequency counts as realtime. " : "Use this when the user wants to monitor, track, or watch a page in realtime. ") + "At least one notification channel (email, webhook, slack, or websocket) is required; notification settings are persisted before Assistant creation is dispatched. " + `Realtime monitors CANNOT be converted to/from one-time or scheduled extraction workflows after creation.
2566
3067
 
2567
3068
  ` + "If entity and schema are provided, they guide what the Assistant should monitor; otherwise the Assistant determines what to watch. " + "Creation is asynchronous and the response includes workflow, session, thread, and job IDs plus a dashboard URL. " + "Use generic workflow Assistant tools (get_workflow_assistant, answer_workflow_assistant_question, interrupt_workflow_assistant, resume_workflow_assistant, stop_workflow_assistant) for follow-up status, questions, and controls. " + "Do NOT poll or sleep-wait for completion.",
2568
3069
  inputSchema: strictSchema({
2569
3070
  prompt: extractionPromptInput.describe("Optional natural language instructions describing what to monitor and which changes matter. If omitted, the Assistant determines what to watch."),
2570
3071
  ...extractionInputShape,
2571
3072
  ...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"),
3073
+ description: z12.string().max(500).optional().describe("Description of what this monitor watches (max 500 characters)"),
3074
+ tags: z12.preprocess(coerceArray(true), z12.array(z12.string())).optional().describe("Tags for organizing monitors"),
2574
3075
  ...requiredNotificationsInputShape
2575
3076
  }),
2576
3077
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }
@@ -2621,20 +3122,27 @@ function registerWorkflowCoreTools(server, ctx, helpers) {
2621
3122
  title: "List Workflows",
2622
3123
  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
3124
  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.")
3125
+ limit: limitInput({
3126
+ noun: "Workflows",
3127
+ defaultLimit: LIST_WORKFLOWS_DEFAULT_LIMIT,
3128
+ max: LIST_WORKFLOWS_MAX_LIMIT
3129
+ }),
3130
+ skip: offsetInput("skip", "workflows"),
3131
+ state: z12.enum(["ACTIVE", "FAILED", "PAUSED", "PREVIEW"]).optional().describe("Filter by state (ACTIVE, FAILED, PAUSED, PREVIEW)"),
3132
+ search: z12.string().optional().describe("Free-text filter matching workflow name, URL, or workflow ID"),
3133
+ tags: z12.array(z12.string()).optional().describe("Filter to workflows having any of these tags"),
3134
+ monitoring: z12.preprocess(coerceBoolean(), z12.boolean()).optional().describe("Filter by monitoring/real-time status"),
3135
+ updateInterval: z12.enum(["HOURLY", "DAILY", "WEEKLY", "MONTHLY", "REAL_TIME"]).optional().describe("Filter by scheduled update frequency or realtime workflows"),
3136
+ templateId: z12.string().optional().describe("Filter to workflows instantiated from this template ID"),
3137
+ 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
3138
  }),
3139
+ outputSchema: ListWorkflowsOutput,
2634
3140
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
2635
3141
  }, withErrorHandling("list_workflows", async (args) => {
2636
- const workflows = await ctx.client.workflow.list({
2637
- limit: args.limit || 50,
3142
+ const limit = args.limit ?? LIST_WORKFLOWS_DEFAULT_LIMIT;
3143
+ const skip = args.skip ?? 0;
3144
+ const rows = await ctx.client.workflow.list({
3145
+ limit: limit + 1,
2638
3146
  state: args.state,
2639
3147
  ...args.skip !== undefined && { skip: args.skip },
2640
3148
  ...args.search !== undefined && { search: args.search },
@@ -2644,6 +3152,7 @@ function registerWorkflowCoreTools(server, ctx, helpers) {
2644
3152
  ...args.templateId !== undefined && { templateId: args.templateId },
2645
3153
  ...args.statusFilters !== undefined && { statusFilters: args.statusFilters }
2646
3154
  });
3155
+ const workflows = rows.slice(0, limit);
2647
3156
  return jsonResult({
2648
3157
  workflows: workflows.map((w) => ({
2649
3158
  id: w.id,
@@ -2680,15 +3189,23 @@ function registerWorkflowCoreTools(server, ctx, helpers) {
2680
3189
  },
2681
3190
  awaitingUserInput: w.awaitingUserInput ?? null
2682
3191
  })),
2683
- total: workflows.length
3192
+ pagination: offsetPage({
3193
+ args,
3194
+ param: "skip",
3195
+ offset: skip,
3196
+ limit,
3197
+ returned: workflows.length,
3198
+ hasMore: rows.length > limit
3199
+ })
2684
3200
  });
2685
3201
  }));
2686
3202
  server.registerTool("get_workflow", {
2687
3203
  title: "Get Workflow",
2688
3204
  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
3205
  inputSchema: strictSchema({
2690
- workflowId: z10.string().min(1).describe("The workflow ID")
3206
+ workflowId: z12.string().min(1).describe("The workflow ID")
2691
3207
  }),
3208
+ outputSchema: GetWorkflowOutput,
2692
3209
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
2693
3210
  }, withErrorHandling("get_workflow", async (args) => {
2694
3211
  const workflow = await ctx.client.workflow.get(args.workflowId);
@@ -2705,9 +3222,9 @@ function registerWorkflowCoreTools(server, ctx, helpers) {
2705
3222
  const hasTemplate = workflow.templateId != null || workflow.templateVersion != null;
2706
3223
  const isRealtime = workflow.isRealTime === true || isRealTimeInterval(workflow.updateInterval);
2707
3224
  return jsonResult({
2708
- id: workflow.id,
3225
+ id: workflow.id ?? args.workflowId,
2709
3226
  name: workflow.name,
2710
- dashboardUrl: workflowDashboardUrl(workflow.id),
3227
+ dashboardUrl: workflowDashboardUrl(workflow.id ?? args.workflowId),
2711
3228
  description: workflow.description,
2712
3229
  status: deriveStatusLabel(workflow),
2713
3230
  state: workflow.state ?? null,
@@ -2775,9 +3292,9 @@ function registerWorkflowCoreTools(server, ctx, helpers) {
2775
3292
  title: "Request Workflow Update",
2776
3293
  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
3294
  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")
3295
+ workflowId: z12.string().min(1).describe("The existing workflow ID; it will not change"),
3296
+ instructions: z12.string().min(1).describe("Natural-language instructions for the workflow Assistant"),
3297
+ threadId: z12.string().optional().describe("Optional Assistant thread ID to continue")
2781
3298
  }),
2782
3299
  annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true }
2783
3300
  }, withErrorHandling("request_workflow_update", async (args) => {
@@ -2805,69 +3322,102 @@ function registerWorkflowHistoryTools(server, ctx, helpers) {
2805
3322
  const { withErrorHandling } = helpers;
2806
3323
  server.registerTool("get_workflow_history", {
2807
3324
  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).",
3325
+ 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
3326
  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)")
3327
+ workflowId: z12.string().min(1).describe("The workflow ID"),
3328
+ page: pageInput(),
3329
+ limit: limitInput({
3330
+ noun: "Entries",
3331
+ defaultLimit: WORKFLOW_HISTORY_DEFAULT_LIMIT,
3332
+ max: WORKFLOW_HISTORY_MAX_LIMIT
3333
+ }),
3334
+ includeSnapshots: z12.preprocess(coerceBoolean(), z12.boolean()).optional().describe("Include full previousValue/newValue workflow snapshots per entry (default false; large)")
2813
3335
  }),
2814
3336
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
2815
3337
  }, 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({
3338
+ const page = args.page ?? 1;
3339
+ const limit = args.limit ?? WORKFLOW_HISTORY_DEFAULT_LIMIT;
3340
+ const response = await ctx.client.workflow.getAuditLog(args.workflowId, { page, limit });
3341
+ const includeSnapshots = args.includeSnapshots === true;
3342
+ const entries = (response.logEntries ?? []).map((e) => {
3343
+ const summary = summarizeWorkflowAuditEntry({
2829
3344
  previousValue: e.previousValue,
2830
3345
  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
3346
+ });
3347
+ return {
3348
+ id: e.id,
3349
+ operationType: e.operationType,
3350
+ createdAt: e.createdAt,
3351
+ userEmail: e.userEmail,
3352
+ userId: e.userId,
3353
+ requestSource: e.requestSource,
3354
+ authMethod: e.authMethod,
3355
+ changedFields: summary.changedFields,
3356
+ changes: summary.changes,
3357
+ ...includeSnapshots && { previousValue: e.previousValue ?? null, newValue: e.newValue ?? null }
3358
+ };
3359
+ });
3360
+ const totalCount = response.pagination?.totalCount ?? response.logEntriesCount;
3361
+ const totalPages = response.pagination?.totalPages;
3362
+ const result = budgetNumberedPage({
3363
+ items: entries,
3364
+ pagination: numberedPage({ args, page, limit, returned: entries.length, totalCount, totalPages }),
3365
+ args,
3366
+ page,
3367
+ limit,
3368
+ noun: "entries",
3369
+ 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.",
3370
+ emptyHint: historyEmptyHint(includeSnapshots),
3371
+ build: (kept, meta) => ({
3372
+ workflowId: response.id,
3373
+ ...totalCount !== undefined && { totalCount },
3374
+ page,
3375
+ ...totalPages !== undefined && kept.length === entries.length && { totalPages },
3376
+ entries: kept,
3377
+ pagination: meta
3378
+ })
2841
3379
  });
3380
+ return jsonResult(result);
2842
3381
  }));
2843
3382
  server.registerTool("list_workflow_runs", {
2844
3383
  title: "List Workflow Runs",
2845
3384
  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
3385
  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)")
3386
+ workflowId: z12.string().min(1).describe("The workflow ID"),
3387
+ status: z12.enum(["success", "failed", "in_progress"]).optional().describe("Filter runs by outcome"),
3388
+ page: pageInput(),
3389
+ limit: limitInput({ noun: "Runs", defaultLimit: WORKFLOW_RUNS_DEFAULT_LIMIT, max: WORKFLOW_RUNS_MAX_LIMIT })
2851
3390
  }),
2852
3391
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
2853
3392
  }, withErrorHandling("list_workflow_runs", async (args) => {
3393
+ const page = args.page ?? 1;
3394
+ const limit = args.limit ?? WORKFLOW_RUNS_DEFAULT_LIMIT;
2854
3395
  const response = await ctx.client.workflow.listWorkflowRuns(args.workflowId, {
2855
- page: args.page,
2856
- limit: args.limit,
3396
+ page,
3397
+ limit,
2857
3398
  status: args.status
2858
3399
  });
3400
+ const runs = (response.workflowRuns ?? []).map((r) => ({
3401
+ id: r.id,
3402
+ status: mapRunStatus(r.state),
3403
+ startedAt: r.startedAt,
3404
+ finishedAt: r.finishedAt,
3405
+ records: r.records,
3406
+ errors: r.errors
3407
+ }));
3408
+ const totalCount = response.totalCount ?? response.pagination?.totalCount;
2859
3409
  return jsonResult({
2860
3410
  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
3411
+ runs,
3412
+ ...totalCount !== undefined && { totalCount },
3413
+ pagination: numberedPage({
3414
+ args,
3415
+ page,
3416
+ limit,
3417
+ returned: runs.length,
3418
+ totalCount,
3419
+ totalPages: response.pagination?.totalPages
3420
+ })
2871
3421
  });
2872
3422
  }));
2873
3423
  }
@@ -2877,8 +3427,8 @@ function registerWorkflowRunTools(server, ctx, helpers) {
2877
3427
  title: "Run Workflow",
2878
3428
  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
3429
  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)")
3430
+ workflowId: z12.string().min(1).describe("The workflow ID to run"),
3431
+ limit: z12.preprocess(coerceNumber(), z12.number()).optional().describe("Maximum number of records to extract (default: 1000)")
2882
3432
  }),
2883
3433
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }
2884
3434
  }, withErrorHandling("run_workflow", async (args) => {
@@ -2901,49 +3451,72 @@ function registerWorkflowRunTools(server, ctx, helpers) {
2901
3451
  title: "Fetch Workflow Data",
2902
3452
  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
3453
  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)"),
3454
+ workflowId: z12.string().min(1).describe("The workflow ID"),
3455
+ limit: limitInput({
3456
+ noun: "Rows",
3457
+ defaultLimit: FETCH_DATA_DEFAULT_LIMIT,
3458
+ max: FETCH_DATA_MAX_LIMIT,
3459
+ overMaxHint: "Use export_data for full datasets."
3460
+ }),
3461
+ page: pageInput(),
2907
3462
  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")
3463
+ sortBy: z12.string().optional().describe("Field name to sort by"),
3464
+ order: z12.enum(["asc", "desc"]).optional().describe("Sort order: asc or desc")
2910
3465
  }),
3466
+ outputSchema: FetchDataOutput,
2911
3467
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
2912
3468
  }, 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
- }
3469
+ const limit = args.limit ?? FETCH_DATA_DEFAULT_LIMIT;
3470
+ const page = args.page ?? 1;
2917
3471
  const data = await ctx.client.extraction.fetchData({
2918
3472
  workflowId: args.workflowId,
2919
- limit: requestedLimit,
2920
- page: args.page || 1,
3473
+ limit,
3474
+ page,
2921
3475
  filters: args.filters === undefined ? undefined : JSON.stringify(args.filters),
2922
3476
  sortBy: args.sortBy,
2923
3477
  order: args.order
2924
3478
  });
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
- }
3479
+ const totalCount = data.pagination?.totalCount;
3480
+ const pagination = numberedPage({
3481
+ args,
3482
+ page,
3483
+ limit,
3484
+ returned: data.data.length,
3485
+ totalCount,
3486
+ totalPages: data.pagination?.totalPages
3487
+ });
3488
+ const result = budgetNumberedPage({
3489
+ items: data.data,
3490
+ pagination,
3491
+ args,
3492
+ page,
3493
+ limit,
3494
+ noun: "rows",
3495
+ resumeHint: "Call fetch_data again with pagination.next for the following rows, or use export_data for the full dataset.",
3496
+ emptyHint: "Rows are too large to return inline. Use export_data for this data.",
3497
+ alignmentImpossibleHint: "Page offset too deep to resume inline; use export_data instead.",
3498
+ build: (rows, meta) => ({
3499
+ data: rows,
3500
+ pagination: meta,
3501
+ ...totalCount !== undefined && { totalCount },
3502
+ ...totalCount !== undefined && rows.length < totalCount && {
3503
+ hint: `Showing ${rows.length} of ${totalCount} rows. Use export_data for the full dataset.`
3504
+ }
3505
+ })
3506
+ });
2934
3507
  return jsonResult(result);
2935
3508
  }));
2936
3509
  server.registerTool("export_data", {
2937
3510
  title: "Export Workflow Data",
2938
3511
  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
3512
  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."),
3513
+ workflowId: z12.string().min(1).describe("The workflow ID"),
3514
+ format: z12.enum(["csv", "json"]).optional().describe("Export format. Default 'csv'."),
3515
+ runId: z12.string().optional().describe("Specific run ID to export. Defaults to the latest completed run."),
2943
3516
  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)")
3517
+ sortBy: z12.string().optional().describe("Field name to sort by"),
3518
+ order: z12.enum(["asc", "desc"]).optional().describe("Sort order: asc or desc"),
3519
+ rowIds: z12.preprocess(coerceList(), z12.array(z12.string().trim().min(1)).max(1000)).optional().describe("Row ids to include (max 1000)")
2947
3520
  }),
2948
3521
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
2949
3522
  }, withErrorHandling("export_data", async (args) => {
@@ -2956,7 +3529,7 @@ function registerWorkflowRunTools(server, ctx, helpers) {
2956
3529
  order: args.order,
2957
3530
  rowIds: joinList(args.rowIds)
2958
3531
  });
2959
- return jsonResult({
3532
+ return withExtraContent(jsonResult({
2960
3533
  url: result.url,
2961
3534
  rowCount: result.rowCount,
2962
3535
  format: result.format,
@@ -2964,26 +3537,34 @@ function registerWorkflowRunTools(server, ctx, helpers) {
2964
3537
  workflowId: result.workflowId,
2965
3538
  runId: result.runId,
2966
3539
  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}.`
3540
+ }), {
3541
+ type: "resource_link",
3542
+ uri: result.url,
3543
+ name: `${result.workflowId}-${result.runId}.${result.format}`,
3544
+ title: `Workflow export (${result.rowCount} rows)`,
3545
+ mimeType: result.format === "json" ? "application/json" : "text/csv",
3546
+ description: `Signed download URL, no auth needed. Expires ${result.expiresAt}.`
2967
3547
  });
2968
3548
  }));
2969
3549
  server.registerTool("list_changes", {
2970
3550
  title: "List Detected Changes",
2971
3551
  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
3552
  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")
3553
+ 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."),
3554
+ startDate: z12.string().optional().describe("Start date filter (ISO format, e.g. 2025-01-01)"),
3555
+ endDate: z12.string().optional().describe("End date filter (ISO format, e.g. 2025-12-31)"),
3556
+ skip: offsetInput("skip", "changes"),
3557
+ limit: limitInput({ noun: "Changes", defaultLimit: LIST_CHANGES_DEFAULT_LIMIT, max: LIST_CHANGES_MAX_LIMIT })
2978
3558
  }),
2979
3559
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
2980
3560
  }, withErrorHandling("list_changes", async (args) => {
3561
+ const limit = args.limit ?? LIST_CHANGES_DEFAULT_LIMIT;
2981
3562
  const result = await ctx.client.changes.list({
2982
3563
  workflowIds: args.workflowIds?.join(","),
2983
3564
  startDate: args.startDate,
2984
3565
  endDate: args.endDate,
2985
3566
  skip: args.skip,
2986
- limit: args.limit ?? 25
3567
+ limit
2987
3568
  });
2988
3569
  return jsonResult({
2989
3570
  changes: result.changes.map((c) => ({
@@ -2994,36 +3575,34 @@ function registerWorkflowRunTools(server, ctx, helpers) {
2994
3575
  differences: c.differences,
2995
3576
  createdAt: c.createdAt
2996
3577
  })),
2997
- pagination: result.pagination,
3578
+ pagination: offsetPage({
3579
+ args,
3580
+ param: "skip",
3581
+ offset: args.skip ?? 0,
3582
+ limit,
3583
+ returned: result.changes.length,
3584
+ totalCount: result.pagination?.totalCount
3585
+ }),
2998
3586
  changesCount: result.changesCount
2999
3587
  });
3000
3588
  }));
3001
3589
  server.registerTool("get_change", {
3002
3590
  title: "Get Detected Change",
3003
- description: "Get detailed information about a specific detected change, including the full data snapshot and diff.",
3591
+ 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
3592
  inputSchema: strictSchema({
3005
- changeId: z10.string().min(1).describe("The change ID")
3593
+ changeId: z12.string().min(1).describe("The change ID")
3006
3594
  }),
3007
3595
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
3008
3596
  }, withErrorHandling("get_change", async (args) => {
3009
3597
  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
- });
3598
+ return jsonResult(changeResult(change));
3020
3599
  }));
3021
3600
  server.registerTool("delete_workflow", {
3022
3601
  title: "Delete Workflow",
3023
3602
  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
3603
  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.")
3604
+ workflowId: z12.string().min(1).describe("The workflow ID to delete"),
3605
+ 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
3606
  }),
3028
3607
  annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false }
3029
3608
  }, withErrorHandling("delete_workflow", async (args) => {
@@ -3053,7 +3632,7 @@ function registerWorkflowRunTools(server, ctx, helpers) {
3053
3632
  title: "Approve Workflow",
3054
3633
  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
3634
  inputSchema: strictSchema({
3056
- workflowId: z10.string().min(1).describe("The workflow ID to approve/activate")
3635
+ workflowId: z12.string().min(1).describe("The workflow ID to approve/activate")
3057
3636
  }),
3058
3637
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }
3059
3638
  }, withErrorHandling("approve_workflow", async (args) => {
@@ -3068,7 +3647,7 @@ function registerWorkflowRunTools(server, ctx, helpers) {
3068
3647
  title: "Pause Workflow",
3069
3648
  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
3649
  inputSchema: strictSchema({
3071
- workflowId: z10.string().min(1).describe("The workflow ID to pause")
3650
+ workflowId: z12.string().min(1).describe("The workflow ID to pause")
3072
3651
  }),
3073
3652
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }
3074
3653
  }, withErrorHandling("pause_workflow", async (args) => {
@@ -3087,20 +3666,20 @@ function registerWorkflowRunTools(server, ctx, helpers) {
3087
3666
 
3088
3667
  ` + "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
3668
  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)"),
3669
+ workflowId: z12.string().min(1).describe("The workflow ID to update"),
3670
+ name: z12.string().optional().describe("New name for the workflow"),
3671
+ 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."),
3672
+ entity: z12.string().optional().describe("Entity name for extraction (e.g., 'Product', 'Job Posting')"),
3673
+ 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."),
3674
+ description: z12.string().max(500).optional().describe("Workflow description (max 500 characters)"),
3675
+ tags: z12.preprocess(coerceArray(true), z12.array(z12.string())).optional().describe("Tags for organizing workflows"),
3676
+ userPrompt: z12.string().optional().describe("Navigation prompt for agentic-navigation mode (10-5000 characters)"),
3098
3677
  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.")
3678
+ schedules: z12.preprocess(coerceArray(true), z12.array(z12.string())).optional().describe("Cron expressions for CUSTOM update interval"),
3679
+ 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."),
3680
+ location: z12.preprocess(coerceJson(), LocationSchema).optional().describe("Scraping location: {type:'auto'} or {type:'manual', isoCode:'US'}"),
3681
+ 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."),
3682
+ 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
3683
  }),
3105
3684
  annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false }
3106
3685
  }, withErrorHandling("update_workflow", async (args) => {
@@ -3136,9 +3715,13 @@ function registerWorkflowRunTools(server, ctx, helpers) {
3136
3715
  });
3137
3716
  }));
3138
3717
  }
3139
- var WORKFLOW_AUDIT_WATCHED_KEYS;
3718
+ 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
3719
  var init_workflows = __esm(() => {
3720
+ init_notification_channels();
3721
+ init_budget();
3141
3722
  init_errors();
3723
+ init_output_schemas();
3724
+ init_pagination();
3142
3725
  init_schemas();
3143
3726
  WORKFLOW_AUDIT_WATCHED_KEYS = [
3144
3727
  "name",
@@ -3157,9 +3740,9 @@ var init_workflows = __esm(() => {
3157
3740
  function registerTools(server, ctx, capabilities) {
3158
3741
  const helpers = { withErrorHandling: createWithErrorHandling(ctx) };
3159
3742
  if (capabilities.scrape)
3160
- registerScrapeTools(server, ctx, helpers);
3743
+ registerScrapeTools(server, ctx, helpers, capabilities.defaultProductType);
3161
3744
  registerWhoamiTool(server, ctx, helpers);
3162
- registerWorkflowCoreTools(server, ctx, helpers);
3745
+ registerWorkflowCoreTools(server, ctx, helpers, capabilities.defaultProductType);
3163
3746
  registerAssistantTools(server, ctx, helpers);
3164
3747
  registerWorkflowHistoryTools(server, ctx, helpers);
3165
3748
  registerObservabilityTools(server, ctx, helpers);
@@ -3843,12 +4426,41 @@ var init_request_log = __esm(() => {
3843
4426
 
3844
4427
  // src/server.ts
3845
4428
  import { McpServer as McpServer2 } from "@modelcontextprotocol/server";
4429
+ function lifecycleInstruction2(defaultProductType) {
4430
+ return defaultProductType === "realtime" ? "For one-time or scheduled workflows, the lifecycle is create_workflow → get_workflow (check status) → fetch_data (get results). Workflows run asynchronously - never poll or sleep-wait." : "Workflow lifecycle: create_workflow → get_workflow (check status) → fetch_data (get results). Workflows run asynchronously - never poll or sleep-wait.";
4431
+ }
4432
+ function routingInstructions2(defaultProductType) {
4433
+ if (defaultProductType === "realtime") {
4434
+ return [
4435
+ `This team's default product is realtime monitoring. Use create_realtime_monitor whenever the user asks to monitor, track, or watch a page, or to get or extract data from it, unless they explicitly ask for a one-time run (e.g. "once") or a schedule (e.g. "daily"). A request that names no frequency is not explicit: default to realtime without asking the user to choose. It first persists notification channels, then asynchronously returns workflow/session/thread/job IDs; use the workflow Assistant tools for follow-up status, questions, and controls.`,
4436
+ "Use create_workflow only when the user explicitly asks for a one-time run or a schedule. Use scrape for an immediate raw HTML or markdown fetch from one URL."
4437
+ ];
4438
+ }
4439
+ return [
4440
+ "Use create_realtime_monitor only when the user wants continuous realtime change detection with alerts. It first persists notification channels, then asynchronously returns workflow/session/thread/job IDs; use the workflow Assistant tools for follow-up status, questions, and controls.",
4441
+ "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."
4442
+ ];
4443
+ }
3846
4444
  async function createServer3(auth, options) {
3847
4445
  const ctx = {
3848
4446
  client: createKadoaClient(auth),
3849
4447
  principal: auth.principal,
3850
4448
  teamId: auth.principal.teamId
3851
4449
  };
4450
+ let capabilities = options?.capabilities;
4451
+ if (!capabilities) {
4452
+ try {
4453
+ const response = await ctx.client.user.getFeatures();
4454
+ capabilities = {
4455
+ scrape: response.features.scrape,
4456
+ defaultProductType: parseDefaultProductType(response.defaultProductType)
4457
+ };
4458
+ } catch (error) {
4459
+ const message = error instanceof Error ? error.message : "unknown error";
4460
+ console.error(`[CAPABILITIES] Failed to resolve team capabilities; gated tools will be hidden: ${message}`);
4461
+ capabilities = { scrape: false };
4462
+ }
4463
+ }
3852
4464
  const server = new McpServer2({ name: "kadoa", version: package_default.version }, {
3853
4465
  instructions: [
3854
4466
  "IMPORTANT: One workflow = one source. A single workflow can extract many different fields, tables, and sections from the same URL(s) using a rich schema.",
@@ -3857,12 +4469,13 @@ async function createServer3(auth, options) {
3857
4469
  "Kadoa workflows use agentic navigation: the AI agent can browse pages, click buttons, fill forms, select dropdowns, paginate, open detail pages, and handle multi-step interactions.",
3858
4470
  "Describe the full navigation steps in the prompt (e.g., 'select year 2022 from the dropdown, click Search, extract the table, then repeat for 2023').",
3859
4471
  "",
3860
- "Workflow lifecycle: create_workflow → get_workflow (check status) → fetch_data (get results). Workflows run asynchronously - never poll or sleep-wait.",
4472
+ lifecycleInstruction2(capabilities.defaultProductType),
3861
4473
  "",
3862
- "Use create_realtime_monitor only when the user wants continuous realtime change detection with alerts. It first persists notification channels, then asynchronously returns workflow/session/thread/job IDs; use the workflow Assistant tools for follow-up status, questions, and controls.",
3863
- "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.",
4474
+ ...routingInstructions2(capabilities.defaultProductType),
3864
4475
  "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
4476
  "Use list_changes and get_change to retrieve detected diffs from realtime monitoring workflows.",
4477
+ "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.",
4478
+ "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
4479
  "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
4480
  "",
3868
4481
  "Schema tips: Use descriptive field names and examples. Group related data under one entity.",
@@ -3872,17 +4485,6 @@ async function createServer3(auth, options) {
3872
4485
  ].join(`
3873
4486
  `)
3874
4487
  });
3875
- let capabilities = options?.capabilities;
3876
- if (!capabilities) {
3877
- try {
3878
- const response = await ctx.client.user.getFeatures();
3879
- capabilities = { scrape: response.features.scrape };
3880
- } catch (error) {
3881
- const message = error instanceof Error ? error.message : "unknown error";
3882
- console.error(`[CAPABILITIES] Failed to resolve team capabilities; gated tools will be hidden: ${message}`);
3883
- capabilities = { scrape: false };
3884
- }
3885
- }
3886
4488
  registerTools(server, ctx, capabilities);
3887
4489
  server.server.onerror = (error) => console.error("[MCP Error]", safeErrorSummary(error));
3888
4490
  return server;
@@ -3962,7 +4564,7 @@ async function requestNeedsCapabilityLookup(request) {
3962
4564
  return false;
3963
4565
  }
3964
4566
  }
3965
- return method === "tools/list" || method === "tools/call" && name === "scrape";
4567
+ return method === "initialize" || method === "server/discover" || method === "tools/list" || method === "tools/call" && name === "scrape";
3966
4568
  }
3967
4569
  function isClientCausedHandlerError(error) {
3968
4570
  if (error instanceof ProtocolError)
@@ -4302,12 +4904,41 @@ init_package();
4302
4904
  init_client();
4303
4905
  init_tools();
4304
4906
  import { McpServer } from "@modelcontextprotocol/server";
4907
+ function lifecycleInstruction(defaultProductType) {
4908
+ return defaultProductType === "realtime" ? "For one-time or scheduled workflows, the lifecycle is create_workflow → get_workflow (check status) → fetch_data (get results). Workflows run asynchronously - never poll or sleep-wait." : "Workflow lifecycle: create_workflow → get_workflow (check status) → fetch_data (get results). Workflows run asynchronously - never poll or sleep-wait.";
4909
+ }
4910
+ function routingInstructions(defaultProductType) {
4911
+ if (defaultProductType === "realtime") {
4912
+ return [
4913
+ `This team's default product is realtime monitoring. Use create_realtime_monitor whenever the user asks to monitor, track, or watch a page, or to get or extract data from it, unless they explicitly ask for a one-time run (e.g. "once") or a schedule (e.g. "daily"). A request that names no frequency is not explicit: default to realtime without asking the user to choose. It first persists notification channels, then asynchronously returns workflow/session/thread/job IDs; use the workflow Assistant tools for follow-up status, questions, and controls.`,
4914
+ "Use create_workflow only when the user explicitly asks for a one-time run or a schedule. Use scrape for an immediate raw HTML or markdown fetch from one URL."
4915
+ ];
4916
+ }
4917
+ return [
4918
+ "Use create_realtime_monitor only when the user wants continuous realtime change detection with alerts. It first persists notification channels, then asynchronously returns workflow/session/thread/job IDs; use the workflow Assistant tools for follow-up status, questions, and controls.",
4919
+ "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."
4920
+ ];
4921
+ }
4305
4922
  async function createServer(auth, options) {
4306
4923
  const ctx = {
4307
4924
  client: createKadoaClient(auth),
4308
4925
  principal: auth.principal,
4309
4926
  teamId: auth.principal.teamId
4310
4927
  };
4928
+ let capabilities = options?.capabilities;
4929
+ if (!capabilities) {
4930
+ try {
4931
+ const response = await ctx.client.user.getFeatures();
4932
+ capabilities = {
4933
+ scrape: response.features.scrape,
4934
+ defaultProductType: parseDefaultProductType(response.defaultProductType)
4935
+ };
4936
+ } catch (error) {
4937
+ const message = error instanceof Error ? error.message : "unknown error";
4938
+ console.error(`[CAPABILITIES] Failed to resolve team capabilities; gated tools will be hidden: ${message}`);
4939
+ capabilities = { scrape: false };
4940
+ }
4941
+ }
4311
4942
  const server = new McpServer({ name: "kadoa", version: package_default.version }, {
4312
4943
  instructions: [
4313
4944
  "IMPORTANT: One workflow = one source. A single workflow can extract many different fields, tables, and sections from the same URL(s) using a rich schema.",
@@ -4316,12 +4947,13 @@ async function createServer(auth, options) {
4316
4947
  "Kadoa workflows use agentic navigation: the AI agent can browse pages, click buttons, fill forms, select dropdowns, paginate, open detail pages, and handle multi-step interactions.",
4317
4948
  "Describe the full navigation steps in the prompt (e.g., 'select year 2022 from the dropdown, click Search, extract the table, then repeat for 2023').",
4318
4949
  "",
4319
- "Workflow lifecycle: create_workflow → get_workflow (check status) → fetch_data (get results). Workflows run asynchronously - never poll or sleep-wait.",
4950
+ lifecycleInstruction(capabilities.defaultProductType),
4320
4951
  "",
4321
- "Use create_realtime_monitor only when the user wants continuous realtime change detection with alerts. It first persists notification channels, then asynchronously returns workflow/session/thread/job IDs; use the workflow Assistant tools for follow-up status, questions, and controls.",
4322
- "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.",
4952
+ ...routingInstructions(capabilities.defaultProductType),
4323
4953
  "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
4954
  "Use list_changes and get_change to retrieve detected diffs from realtime monitoring workflows.",
4955
+ "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.",
4956
+ "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
4957
  "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
4958
  "",
4327
4959
  "Schema tips: Use descriptive field names and examples. Group related data under one entity.",
@@ -4331,17 +4963,6 @@ async function createServer(auth, options) {
4331
4963
  ].join(`
4332
4964
  `)
4333
4965
  });
4334
- let capabilities = options?.capabilities;
4335
- if (!capabilities) {
4336
- try {
4337
- const response = await ctx.client.user.getFeatures();
4338
- capabilities = { scrape: response.features.scrape };
4339
- } catch (error) {
4340
- const message = error instanceof Error ? error.message : "unknown error";
4341
- console.error(`[CAPABILITIES] Failed to resolve team capabilities; gated tools will be hidden: ${message}`);
4342
- capabilities = { scrape: false };
4343
- }
4344
- }
4345
4966
  registerTools(server, ctx, capabilities);
4346
4967
  server.server.onerror = (error) => console.error("[MCP Error]", safeErrorSummary(error));
4347
4968
  return server;