@kadoa/mcp 0.5.20 → 0.5.21

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 +12 -7
  2. package/dist/index.js +139 -34
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -127,9 +127,10 @@ to retrieve the extracted records and display them as a table.
127
127
  > You: Use my "Product Scraper" template to scrape https://example-shop.com.
128
128
 
129
129
  Claude calls list_templates to find the matching template, then
130
- create_workflow with `templateId` and `urls` only the prompt and
131
- schema are inherited from the template version. Returns the workflow
132
- ID for follow-up with get_workflow or fetch_data.
130
+ create_workflow with `templateId` and `urls` only - never copy the prompt,
131
+ entity, or schema into standalone creation. Those values are inherited from
132
+ the template version. Returns the workflow ID for follow-up with get_workflow
133
+ or fetch_data.
133
134
  ```
134
135
 
135
136
  ### Update a workflow and re-run
@@ -148,8 +149,10 @@ changes, and shows the updated field list.
148
149
 
149
150
  > You: Run it again with the new schema.
150
151
 
151
- Claude calls run_workflow and waits for completion, then fetches
152
- the latest data with fetch_data so you can verify the changes.
152
+ Claude calls run_workflow and returns the workflow ID while the run proceeds
153
+ asynchronously. Check back later with get_workflow or fetch_data. If a
154
+ one-time or scheduled workflow fails, call run_workflow again with the same
155
+ workflow ID and configuration - never delete and recreate it just to retry.
153
156
  ```
154
157
 
155
158
  ### Update deterministic workflow settings
@@ -247,14 +250,16 @@ Relevant files in `kadoa-backend`:
247
250
 
248
251
  1. Merge PRs to `main` using Conventional Commits (`feat:`, `fix:`, etc.). Release Please opens/maintains a `chore(main): release mcp x.y.z` PR.
249
252
  2. Merge the release PR. The [`release-please.yml`](.github/workflows/release-please.yml) workflow tags, drafts a GitHub Release, and publishes to npm (`latest` dist-tag).
250
- 3. In `kadoa-backend`, bump `infra/docker/mcp/package.json` `@kadoa/mcp` to the new version, run `bun install` to refresh `bun.lock`, open a PR.
251
- 4. Merge to `main`. CI (`main-build-deploy.yml`) builds and pushes `europe-west3-docker.pkg.dev/oceanic-base-310208/kadoa-artifacts/mcp-server:<IMAGE_TAG>` (tag shown in the build summary).
253
+ 3. After npm publication, `release-please.yml` opens or updates a deterministic PR in `kadoa-backend` that bumps `infra/docker/mcp/package.json` and refreshes `bun.lock`. The PR validates a frozen production install and Docker image build, and remains manually mergeable.
254
+ 4. Merge the backend PR to `main`. CI (`main-build-deploy.yml`) builds and pushes `europe-west3-docker.pkg.dev/oceanic-base-310208/kadoa-artifacts/mcp-server:<IMAGE_TAG>` (tag shown in the build summary).
252
255
  5. Trigger the **Deploy to Production** workflow ([`deploy-prod.yml`](https://github.com/kadoa-org/kadoa-backend/actions/workflows/deploy-prod.yml)) with:
253
256
  - **Target cluster:** `gcp`
254
257
  - **Deployment scope:** `mcp`
255
258
  - **Image tag:** the tag from step 4
256
259
  - **Method:** `kubectl`
257
260
 
261
+ The cross-repository bump requires the `BACKEND_REPO_TOKEN` secret in this repository. The selected authentication approach is a fine-grained PAT scoped only to `kadoa-org/kadoa-backend` with Contents read/write and Pull requests read/write permissions (Metadata read is automatic). If the secret is not provisioned, the release workflow skips the backend bump with a warning rather than failing npm releases; an administrator must provision it before relying on automatic hosted-server updates.
262
+
258
263
  ### RC / test release
259
264
 
260
265
  Use this when you want to validate a change end-to-end against real clients (Claude Desktop, Cursor, ChatGPT) before promoting to `latest` / prod. The flow mirrors the prod one, but every step targets `rc` channels.
package/dist/index.js CHANGED
@@ -52699,7 +52699,7 @@ function extractApiMessage(responseBody) {
52699
52699
  if (body.validationErrors && typeof body.validationErrors === "object" && body.validationErrors !== null) {
52700
52700
  const details = Object.entries(body.validationErrors).map(([field, err]) => `${field}: "${err}"`).join(", ");
52701
52701
  if (details) {
52702
- msg = msg ? `${msg} Details: ${details}` : `Validation failed Details: ${details}`;
52702
+ msg = msg ? `${msg} - Details: ${details}` : `Validation failed - Details: ${details}`;
52703
52703
  }
52704
52704
  }
52705
52705
  if (!msg && Array.isArray(body.issues)) {
@@ -52709,7 +52709,7 @@ function extractApiMessage(responseBody) {
52709
52709
  return path ? `${path}: "${message}"` : `"${message}"`;
52710
52710
  }).join(", ");
52711
52711
  if (details)
52712
- msg = `Validation failed Details: ${details}`;
52712
+ msg = `Validation failed - Details: ${details}`;
52713
52713
  }
52714
52714
  return msg;
52715
52715
  }
@@ -52895,14 +52895,14 @@ function registerTools(server, ctx, capabilities) {
52895
52895
  });
52896
52896
  }));
52897
52897
  const urlInputShape = {
52898
- url: exports_external.string().optional().describe("Single URL prefer using 'urls' instead. If both are provided, 'urls' takes precedence."),
52898
+ url: exports_external.string().optional().describe("Single URL - prefer using 'urls' instead. If both are provided, 'urls' takes precedence."),
52899
52899
  urls: exports_external.preprocess(coerceArray(true), exports_external.array(exports_external.string()).min(1)).optional().describe("Starting URLs for the workflow (array of strings). Also accepts a single URL string.")
52900
52900
  };
52901
52901
  const extractionInputShape = {
52902
52902
  prompt: exports_external.string().optional().describe('Natural language description of what to extract (e.g., "Extract product prices and names"). Required unless templateId is provided.'),
52903
52903
  name: exports_external.string().optional().describe("Optional name for the workflow"),
52904
52904
  entity: exports_external.string().optional().describe("Entity name for extraction (e.g., 'Product', 'Job Posting')"),
52905
- schema: exports_external.preprocess(coerceArray(), exports_external.array(exports_external.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.")
52905
+ schema: exports_external.preprocess(coerceArray(), exports_external.array(exports_external.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.")
52906
52906
  };
52907
52907
  const webhookAuthShape = exports_external.object({
52908
52908
  type: exports_external.enum(["bearer", "basic", "header"]).describe("Authentication type"),
@@ -53030,15 +53030,15 @@ function registerTools(server, ctx, capabilities) {
53030
53030
  return channels;
53031
53031
  }
53032
53032
  server.registerTool("create_workflow", {
53033
- description: "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. " + `Do NOT create separate workflows for different data points on the same page instead, define one workflow with a multi-field schema covering everything needed.
53033
+ description: "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. " + `Do NOT create separate workflows for different data points on the same page - instead, define one workflow with a multi-field schema covering everything needed.
53034
53034
 
53035
53035
  ` + "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.
53036
53036
 
53037
- ` + "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 `templateId` is set, only `urls` is required `prompt`, `entity`, and `schema` must NOT be supplied; they are inherited from the template version.\n\n" + "NOTE: This tool is for one-time or scheduled extraction ONLY. " + "For continuous Julie realtime monitoring (watching a page for data changes and alerting), use the create_realtime_monitor tool instead.",
53037
+ ` + "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`, and optional `templateVersion` only. Never copy the template prompt, entity, or schema into standalone creation, and never silently match or rewrite inline configuration.\n\n" + "NOTE: This tool is for one-time or scheduled extraction ONLY. " + "For continuous Julie realtime monitoring (watching a page for data changes and alerting), use the create_realtime_monitor tool instead.",
53038
53038
  inputSchema: strictSchema({
53039
53039
  ...extractionInputShape,
53040
53040
  ...urlInputShape,
53041
- templateId: exports_external.string().optional().describe("Instantiate this workflow from a published template. When set, only 'urls' is required prompt/entity/schema must NOT be supplied; they are inherited from the template version, and the workflow's output conforms to the template's declared schema (field names are enforced, not drifted). Discover templates via list_templates."),
53041
+ templateId: exports_external.string().optional().describe("Instantiate this workflow from a published template. Pass the templateId, source urls, and optional templateVersion. Do not copy prompt/entity/schema from the template 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."),
53042
53042
  templateVersion: exports_external.preprocess(coerceNumber(), exports_external.number()).optional().describe("Specific published template version (integer) to instantiate. Defaults to the latest published version when templateId is set."),
53043
53043
  description: exports_external.string().max(500).optional().describe("Description of what this workflow does (max 500 characters)"),
53044
53044
  tags: exports_external.preprocess(coerceArray(true), exports_external.array(exports_external.string())).optional().describe("Tags for organizing workflows"),
@@ -53079,7 +53079,7 @@ function registerTools(server, ctx, capabilities) {
53079
53079
  let workflowId;
53080
53080
  if (args.templateId) {
53081
53081
  if (args.prompt || args.entity || args.schema) {
53082
- return errorResult("When 'templateId' is set, 'prompt', 'entity', and 'schema' must NOT be supplied they are inherited from the template version.");
53082
+ return errorResult("When 'templateId' is set, 'prompt', 'entity', and 'schema' must NOT be supplied - they are inherited from the template version.");
53083
53083
  }
53084
53084
  const { id } = await ctx.client.workflow.create({
53085
53085
  urls,
@@ -53235,7 +53235,7 @@ function registerTools(server, ctx, capabilities) {
53235
53235
  });
53236
53236
  }));
53237
53237
  server.registerTool("get_workflow", {
53238
- 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, extractionSpecnot the legacy promptis authoritative.",
53238
+ 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.",
53239
53239
  inputSchema: {
53240
53240
  workflowId: exports_external.string().describe("The workflow ID")
53241
53241
  },
@@ -53599,7 +53599,7 @@ function registerTools(server, ctx, capabilities) {
53599
53599
  });
53600
53600
  }));
53601
53601
  server.registerTool("list_workflow_runs", {
53602
- 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.",
53602
+ 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.",
53603
53603
  inputSchema: {
53604
53604
  workflowId: exports_external.string().describe("The workflow ID"),
53605
53605
  status: exports_external.enum(["success", "failed", "in_progress"]).optional().describe("Filter runs by outcome"),
@@ -53628,7 +53628,7 @@ function registerTools(server, ctx, capabilities) {
53628
53628
  });
53629
53629
  }));
53630
53630
  server.registerTool("run_workflow", {
53631
- description: "Run a workflow to extract fresh data. 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.",
53631
+ 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.",
53632
53632
  inputSchema: {
53633
53633
  workflowId: exports_external.string().describe("The workflow ID to run"),
53634
53634
  limit: exports_external.preprocess(coerceNumber(), exports_external.number()).optional().describe("Maximum number of records to extract (default: 1000)")
@@ -53651,7 +53651,7 @@ function registerTools(server, ctx, capabilities) {
53651
53651
  const FETCH_DATA_DEFAULT_LIMIT = 50;
53652
53652
  const FETCH_DATA_MAX_LIMIT = 500;
53653
53653
  server.registerTool("fetch_data", {
53654
- 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.",
53654
+ 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.",
53655
53655
  inputSchema: {
53656
53656
  workflowId: exports_external.string().describe("The workflow ID"),
53657
53657
  limit: exports_external.preprocess(coerceNumber(), exports_external.number()).optional().describe(`Maximum number of records to return. Default ${FETCH_DATA_DEFAULT_LIMIT}, max ${FETCH_DATA_MAX_LIMIT}.`),
@@ -53686,7 +53686,7 @@ function registerTools(server, ctx, capabilities) {
53686
53686
  return jsonResult(result);
53687
53687
  }));
53688
53688
  server.registerTool("export_data", {
53689
- 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').",
53689
+ 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').",
53690
53690
  inputSchema: {
53691
53691
  workflowId: exports_external.string().describe("The workflow ID"),
53692
53692
  format: exports_external.enum(["csv", "json"]).optional().describe("Export format. Default 'csv'."),
@@ -53812,7 +53812,7 @@ function registerTools(server, ctx, capabilities) {
53812
53812
  });
53813
53813
  }));
53814
53814
  server.registerTool("pause_workflow", {
53815
- 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.",
53815
+ 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.",
53816
53816
  inputSchema: {
53817
53817
  workflowId: exports_external.string().min(1).describe("The workflow ID to pause")
53818
53818
  },
@@ -53826,17 +53826,17 @@ function registerTools(server, ctx, capabilities) {
53826
53826
  });
53827
53827
  }));
53828
53828
  server.registerTool("update_workflow", {
53829
- description: `Update a workflow's configuration. All fields are optional only provided fields will be updated. Use this to change the name, URLs, extraction schema, entity, prompt, schedule, or other metadata.
53829
+ description: `Update a workflow's configuration. All fields are optional - only provided fields will be updated. Use this to change the name, URLs, extraction schema, entity, prompt, schedule, or other metadata.
53830
53830
 
53831
53831
  ` + `IMPORTANT: You cannot change a workflow's interval to or from REAL_TIME. Julie realtime workflows are architecturally different from scheduled workflows and must be created with create_realtime_monitor from the start. Existing workflows cannot be converted between these modes in place; do not delete and recreate a workflow as an update workaround.
53832
53832
 
53833
- ` + "ASSISTANT-OWNED INTENT: Use request_workflow_updatenot `userPrompt`—for agent-built workflow changes to extraction intent, navigation, pagination, data sourcing, repair, or generated scripts. `userPrompt` may be rejected with `SHELLY_INTENT_REQUIRES_EXTRACTION_SPEC` because those 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 limitationthat changes workflowId, breaks downstream tables/connectors, and discards history.",
53833
+ ` + "ASSISTANT-OWNED INTENT: Use request_workflow_update - not `userPrompt` - for agent-built workflow changes to extraction intent, navigation, pagination, data sourcing, repair, or generated scripts. `userPrompt` may be rejected with `SHELLY_INTENT_REQUIRES_EXTRACTION_SPEC` because those 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.",
53834
53834
  inputSchema: strictSchema({
53835
53835
  workflowId: exports_external.string().describe("The workflow ID to update"),
53836
53836
  name: exports_external.string().optional().describe("New name for the workflow"),
53837
53837
  urls: exports_external.preprocess(coerceArray(true), exports_external.array(exports_external.string()).min(1)).optional().describe("New target URLs for the workflow (array of strings). Also accepts a single URL string."),
53838
53838
  entity: exports_external.string().optional().describe("Entity name for extraction (e.g., 'Product', 'Job Posting')"),
53839
- schema: exports_external.preprocess(coerceArray(), exports_external.array(exports_external.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."),
53839
+ schema: exports_external.preprocess(coerceArray(), exports_external.array(exports_external.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."),
53840
53840
  description: exports_external.string().max(500).optional().describe("Workflow description (max 500 characters)"),
53841
53841
  tags: exports_external.preprocess(coerceArray(true), exports_external.array(exports_external.string())).optional().describe("Tags for organizing workflows"),
53842
53842
  userPrompt: exports_external.string().optional().describe("Navigation prompt for agentic-navigation mode (10-5000 characters)"),
@@ -53858,7 +53858,7 @@ function registerTools(server, ctx, capabilities) {
53858
53858
  "FOUR_WEEKS",
53859
53859
  "MONTHLY",
53860
53860
  "CUSTOM"
53861
- ]).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."),
53861
+ ]).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."),
53862
53862
  schedules: exports_external.preprocess(coerceArray(true), exports_external.array(exports_external.string())).optional().describe("Cron expressions for CUSTOM update interval"),
53863
53863
  timezone: exports_external.string().min(1).optional().describe("IANA timezone for cron schedules, for example America/New_York. A timezone-only update preserves existing cron expressions."),
53864
53864
  location: exports_external.preprocess(coerceJson(), LocationSchema).optional().describe("Scraping location: {type:'auto'} or {type:'manual', isoCode:'US'}"),
@@ -54277,22 +54277,29 @@ function registerTools(server, ctx, capabilities) {
54277
54277
  });
54278
54278
  }));
54279
54279
  server.registerTool("list_templates", {
54280
- description: "List all templates in the current team. Templates define reusable configurations (prompt, schema, notifications).",
54280
+ description: "List all templates in the current team. Templates define reusable configurations (prompt, schema, validation rules, notifications, and frequency). Use this to find a matching template before creating a workflow. To instantiate a template, call create_workflow with its templateId, source URLs, and optional templateVersion - never copy the template prompt, entity, or schema into standalone creation.",
54281
54281
  inputSchema: strictSchema({}),
54282
54282
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true }
54283
54283
  }, withErrorHandling("list_templates", async () => {
54284
54284
  const templates = await ctx.client.template.list();
54285
- return jsonResult({ templates, count: templates.length });
54285
+ return jsonResult({
54286
+ templates,
54287
+ count: templates.length,
54288
+ instructions: "To instantiate a listed template, call create_workflow with the matching templateId, source URLs, and optional templateVersion. Do not copy the template prompt, entity, or schema into standalone creation."
54289
+ });
54286
54290
  }));
54287
54291
  server.registerTool("get_template", {
54288
- description: "Get a template by ID, including all published versions.",
54292
+ 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, and optional templateVersion. Never copy the returned prompt, entity, or schema into standalone creation - those values are inherited when templateId is passed.",
54289
54293
  inputSchema: strictSchema({
54290
54294
  templateId: exports_external.string().describe("The template ID")
54291
54295
  }),
54292
54296
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true }
54293
54297
  }, withErrorHandling("get_template", async (args) => {
54294
54298
  const template = await ctx.client.template.get(args.templateId);
54295
- return jsonResult({ template });
54299
+ return jsonResult({
54300
+ template,
54301
+ instructions: `To instantiate this template, call create_workflow with templateId "${args.templateId}", source URLs, and optional templateVersion. Do not copy this template's prompt, entity, or schema into standalone creation.`
54302
+ });
54296
54303
  }));
54297
54304
  server.registerTool("create_template", {
54298
54305
  description: "Create a new template. After creation, use create_template_version to publish a version with prompt, schema, and notifications.",
@@ -54315,7 +54322,7 @@ function registerTools(server, ctx, capabilities) {
54315
54322
  server.registerTool("update_template", {
54316
54323
  description: `Update a template's name or description ONLY. At least one of the two must be provided.
54317
54324
 
54318
- ` + "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.",
54325
+ ` + "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.",
54319
54326
  inputSchema: strictSchema({
54320
54327
  templateId: exports_external.string().describe("The template ID to update"),
54321
54328
  name: exports_external.preprocess(coerceNull(), exports_external.string().optional()).optional().describe("New template name"),
@@ -54371,7 +54378,7 @@ function registerTools(server, ctx, capabilities) {
54371
54378
  }))).optional().describe("Predefined categories for a CLASSIFICATION field ({title, definition}[]). Required for fieldType=CLASSIFICATION; omitted otherwise.")
54372
54379
  };
54373
54380
  server.registerTool("create_template_version", {
54374
- description: `Publish a new version of a template. Versions capture the full workflow config: prompt, schema, and notifications. All fields are optional include only what this version should set.
54381
+ description: `Publish a new version of a template. Versions capture the full workflow config: prompt, schema, and notifications. All fields are optional - include only what this version should set.
54375
54382
 
54376
54383
  ` + "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.",
54377
54384
  inputSchema: strictSchema({
@@ -54380,7 +54387,7 @@ function registerTools(server, ctx, capabilities) {
54380
54387
  schemaId: exports_external.string().optional().describe("Existing schema ID to reference (mutually exclusive with schemaFields)"),
54381
54388
  schemaFields: exports_external.preprocess(coerceArray(), exports_external.array(exports_external.object(TemplateSchemaFieldShape).strict())).optional().describe("Inline schema fields to create a new schema (mutually exclusive with schemaId)"),
54382
54389
  schemaEntity: exports_external.string().optional().describe("Entity name for the inline schema"),
54383
- schemaValidationRules: exports_external.preprocess(coerceJson(), exports_external.record(exports_external.string(), exports_external.record(exports_external.string(), exports_external.unknown()))).optional().describe("Per-field schema validation rules, keyed by field name. Not inherited from prior versions omitting this on a new version drops any rules the previous version had."),
54390
+ schemaValidationRules: exports_external.preprocess(coerceJson(), 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. Rule metadata requires editedBy (default|llm|agent|ops|user) and an ISO-8601 editedAt timestamp. Example: { "price": { "kind": "NUMBER", "minimum": { "value": 0, "editedBy": "user", "editedAt": "2026-01-01T00:00:00.000Z" } } }. Not inherited from prior versions - omitting this on a new version drops any rules the previous version had.'),
54384
54391
  notifications: exports_external.preprocess(coerceArray(), exports_external.array(exports_external.object({
54385
54392
  eventType: exports_external.string().describe("Notification event type"),
54386
54393
  eventConfiguration: exports_external.preprocess(coerceJson(), exports_external.record(exports_external.string(), exports_external.unknown())).optional(),
@@ -54461,7 +54468,7 @@ function registerTools(server, ctx, capabilities) {
54461
54468
  return jsonResult({ schemas: schemas4, count: schemas4.length });
54462
54469
  }));
54463
54470
  server.registerTool("link_template_to_workflows", {
54464
- 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.",
54471
+ 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.",
54465
54472
  inputSchema: strictSchema({
54466
54473
  templateId: exports_external.string().describe("The template ID to link workflows to"),
54467
54474
  workflowIds: exports_external.preprocess(coerceArray(true), exports_external.array(exports_external.string()).min(1)).describe("Workflow IDs to link to the template (array of strings). Also accepts a single ID string."),
@@ -54478,7 +54485,7 @@ function registerTools(server, ctx, capabilities) {
54478
54485
  return jsonResult({
54479
54486
  success: false,
54480
54487
  conflicts: result.conflicts,
54481
- message: `Cannot link ${result.conflicts.length} workflow(s) are already linked to another template: ${summary}. ` + "Re-run with force: true to move them to this template."
54488
+ message: `Cannot link - ${result.conflicts.length} workflow(s) are already linked to another template: ${summary}. ` + "Re-run with force: true to move them to this template."
54482
54489
  });
54483
54490
  }
54484
54491
  return jsonResult({
@@ -54538,7 +54545,7 @@ function registerTools(server, ctx, capabilities) {
54538
54545
  });
54539
54546
  }));
54540
54547
  }
54541
- var SchemaFieldShape, LocationSchema, MonitoringValueOperators, MonitoringValuelessOperators, MonitoringConditionOperatorSchema, MonitoringConditionSchema, MonitoringSchema, RESUMABLE_ASSISTANT_STATUSES, ACTIVE_ASSISTANT_STATUSES, IDLE_ASSISTANT_STATUSES, CLOSED_ASSISTANT_STATUSES, DASHBOARD_BASE_URL = "https://www.kadoa.com", WORKFLOW_AUDIT_WATCHED_KEYS;
54548
+ var SchemaFieldShape, SchemaValidationAttributionShape, SchemaValidationPresenceRule, SchemaValidationUniquenessRule, SchemaValidationStringLengthRule, SchemaValidationStringFormatRule, SchemaValidationFieldRulesSchema, SchemaValidationRulesSchema, LocationSchema, MonitoringValueOperators, MonitoringValuelessOperators, MonitoringConditionOperatorSchema, MonitoringConditionSchema, MonitoringSchema, RESUMABLE_ASSISTANT_STATUSES, ACTIVE_ASSISTANT_STATUSES, IDLE_ASSISTANT_STATUSES, CLOSED_ASSISTANT_STATUSES, DASHBOARD_BASE_URL = "https://www.kadoa.com", WORKFLOW_AUDIT_WATCHED_KEYS;
54542
54549
  var init_tools = __esm(() => {
54543
54550
  init_dist2();
54544
54551
  init_zod();
@@ -54548,8 +54555,105 @@ var init_tools = __esm(() => {
54548
54555
  description: exports_external.string().optional().describe("What this field contains"),
54549
54556
  example: exports_external.string().describe("Example value"),
54550
54557
  dataType: exports_external.enum(["STRING", "NUMBER", "BOOLEAN", "DATE", "DATETIME", "MONEY", "IMAGE", "LINK", "OBJECT", "ARRAY"]).optional().describe("Data type for the field"),
54551
- isKey: exports_external.preprocess(coerceBoolean(), exports_external.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).")
54558
+ isKey: exports_external.preprocess(coerceBoolean(), exports_external.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).")
54552
54559
  };
54560
+ SchemaValidationAttributionShape = {
54561
+ editedBy: exports_external.enum(["default", "llm", "agent", "ops", "user"]).describe("Who last edited this rule: default, llm, agent, ops, or user"),
54562
+ editedByLabel: exports_external.string().optional().describe("Optional editor identifier"),
54563
+ editedAt: exports_external.string().datetime().describe("ISO-8601 timestamp when this rule was last edited")
54564
+ };
54565
+ SchemaValidationPresenceRule = exports_external.object({
54566
+ target: exports_external.number().int().min(0).max(100).step(20).describe("Expected percentage of rows with a value"),
54567
+ ...SchemaValidationAttributionShape
54568
+ }).strict();
54569
+ SchemaValidationUniquenessRule = exports_external.object({
54570
+ target: exports_external.number().int().min(0).max(100).step(20).describe("Expected percentage of rows with a unique value"),
54571
+ ...SchemaValidationAttributionShape
54572
+ }).strict();
54573
+ SchemaValidationStringLengthRule = exports_external.object({
54574
+ value: exports_external.number().int().min(0).describe("String length bound"),
54575
+ ...SchemaValidationAttributionShape
54576
+ }).strict();
54577
+ SchemaValidationStringFormatRule = exports_external.discriminatedUnion("kind", [
54578
+ exports_external.object({
54579
+ kind: exports_external.literal("FREE_TEXT"),
54580
+ charset: exports_external.discriminatedUnion("kind", [
54581
+ exports_external.object({
54582
+ kind: exports_external.literal("PRESET"),
54583
+ preset: exports_external.enum(["natural_language", "alphanumeric", "alpha"])
54584
+ }).strict()
54585
+ ]),
54586
+ ...SchemaValidationAttributionShape
54587
+ }).strict(),
54588
+ exports_external.object({
54589
+ kind: exports_external.literal("FORMAT"),
54590
+ source: exports_external.discriminatedUnion("kind", [
54591
+ exports_external.object({
54592
+ kind: exports_external.literal("PRESET"),
54593
+ preset: exports_external.enum(["url", "email", "phone", "date", "datetime", "time", "uuid", "slug"])
54594
+ }).strict(),
54595
+ exports_external.object({
54596
+ kind: exports_external.literal("CUSTOM"),
54597
+ pattern: exports_external.string().min(1).describe("Regular expression pattern")
54598
+ }).strict()
54599
+ ]),
54600
+ ...SchemaValidationAttributionShape
54601
+ }).strict(),
54602
+ exports_external.object({
54603
+ kind: exports_external.literal("LIST"),
54604
+ source: exports_external.discriminatedUnion("kind", [
54605
+ exports_external.object({
54606
+ kind: exports_external.literal("PRESET"),
54607
+ preset: exports_external.enum(["language2", "country2", "country3", "currency3", "month3", "usState2"])
54608
+ }).strict(),
54609
+ exports_external.object({
54610
+ kind: exports_external.literal("CUSTOM"),
54611
+ values: exports_external.array(exports_external.string().min(1)).min(1).describe("Allowed string values")
54612
+ }).strict()
54613
+ ]),
54614
+ ...SchemaValidationAttributionShape
54615
+ }).strict()
54616
+ ]);
54617
+ SchemaValidationFieldRulesSchema = exports_external.lazy(() => exports_external.discriminatedUnion("kind", [
54618
+ exports_external.object({
54619
+ kind: exports_external.literal("STRING"),
54620
+ presence: SchemaValidationPresenceRule.optional(),
54621
+ uniqueness: SchemaValidationUniquenessRule.optional(),
54622
+ minLength: SchemaValidationStringLengthRule.optional(),
54623
+ maxLength: SchemaValidationStringLengthRule.optional(),
54624
+ minHtmlElements: SchemaValidationStringLengthRule.optional(),
54625
+ maxHtmlElements: SchemaValidationStringLengthRule.optional(),
54626
+ format: SchemaValidationStringFormatRule.optional()
54627
+ }).strict(),
54628
+ exports_external.object({
54629
+ kind: exports_external.literal("NUMBER"),
54630
+ presence: SchemaValidationPresenceRule.optional(),
54631
+ uniqueness: SchemaValidationUniquenessRule.optional(),
54632
+ minimum: exports_external.object({ value: exports_external.number().describe("Minimum numeric value"), ...SchemaValidationAttributionShape }).strict().optional(),
54633
+ maximum: exports_external.object({ value: exports_external.number().describe("Maximum numeric value"), ...SchemaValidationAttributionShape }).strict().optional(),
54634
+ maxDecimalPlaces: exports_external.object({ value: exports_external.number().int().min(0).max(16), ...SchemaValidationAttributionShape }).strict().optional()
54635
+ }).strict(),
54636
+ exports_external.object({
54637
+ kind: exports_external.literal("OTHER"),
54638
+ presence: SchemaValidationPresenceRule.optional(),
54639
+ uniqueness: SchemaValidationUniquenessRule.optional()
54640
+ }).strict(),
54641
+ exports_external.object({
54642
+ kind: exports_external.literal("OBJECT"),
54643
+ presence: SchemaValidationPresenceRule.optional(),
54644
+ uniqueness: SchemaValidationUniquenessRule.optional(),
54645
+ properties: exports_external.record(exports_external.string(), SchemaValidationFieldRulesSchema)
54646
+ }).strict(),
54647
+ exports_external.object({
54648
+ kind: exports_external.literal("ARRAY"),
54649
+ presence: SchemaValidationPresenceRule.optional(),
54650
+ uniqueness: SchemaValidationUniquenessRule.optional(),
54651
+ minItems: exports_external.object({ value: exports_external.number().int().min(0), ...SchemaValidationAttributionShape }).strict().optional(),
54652
+ maxItems: exports_external.object({ value: exports_external.number().int().min(0), ...SchemaValidationAttributionShape }).strict().optional(),
54653
+ items: SchemaValidationFieldRulesSchema.optional()
54654
+ }).strict()
54655
+ ]));
54656
+ SchemaValidationRulesSchema = exports_external.record(exports_external.string(), SchemaValidationFieldRulesSchema);
54553
54657
  LocationSchema = exports_external.object({
54554
54658
  type: exports_external.enum(["auto", "manual"]),
54555
54659
  isoCode: exports_external.string().trim().min(2).optional()
@@ -54638,7 +54742,7 @@ var package_default;
54638
54742
  var init_package = __esm(() => {
54639
54743
  package_default = {
54640
54744
  name: "@kadoa/mcp",
54641
- version: "0.5.20",
54745
+ version: "0.5.21",
54642
54746
  description: "Kadoa MCP Server — manage workflows from Claude Desktop, Cursor, and other MCP clients",
54643
54747
  type: "module",
54644
54748
  main: "dist/index.js",
@@ -60241,21 +60345,22 @@ async function createServer(auth, options) {
60241
60345
  const server = new McpServer({ name: "kadoa", version: package_default.version }, {
60242
60346
  instructions: [
60243
60347
  "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.",
60244
- "Do NOT create multiple workflows for different data points on the same page \u2014 instead, define one workflow with a multi-field schema covering everything needed.",
60348
+ "Do NOT create multiple workflows for different data points on the same page - instead, define one workflow with a multi-field schema covering everything needed.",
60245
60349
  "",
60246
60350
  "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.",
60247
60351
  "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').",
60248
60352
  "",
60249
- "Workflow lifecycle: create_workflow \u2192 get_workflow (check status) \u2192 fetch_data (get results). Workflows run asynchronously \u2014 never poll or sleep-wait.",
60353
+ "Workflow lifecycle: create_workflow \u2192 get_workflow (check status) \u2192 fetch_data (get results). Workflows run asynchronously - never poll or sleep-wait.",
60250
60354
  "",
60251
60355
  "Use create_realtime_monitor only when the user wants continuous Julie 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.",
60252
60356
  "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.",
60357
+ "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.",
60253
60358
  "Use list_changes and get_change to retrieve detected diffs from realtime monitoring workflows.",
60254
60359
  "",
60255
60360
  "Schema tips: Use descriptive field names and examples. Group related data under one entity.",
60256
- "The AI agent uses the schema + prompt to understand what to extract \u2014 a detailed prompt with a comprehensive schema produces better results than multiple simple workflows.",
60361
+ "The AI agent uses the schema + prompt to understand what to extract - a detailed prompt with a comprehensive schema produces better results than multiple simple workflows.",
60257
60362
  "",
60258
- "Templates enforce their schema: a workflow created from a template (create_workflow with templateId) or linked to one (link_template_to_workflows) produces output whose field names conform to the template's declared schema. Use templates when you need many workflows to return a consistent, canonical set of fields \u2014 do NOT assume the extractor will drift field names away from a template's schema."
60363
+ "Templates enforce their schema: a workflow created from a template (create_workflow with templateId) or linked to one (link_template_to_workflows) produces output whose field names conform to the template's declared schema. Use templates when you need many workflows to return a consistent, canonical set of fields - do NOT assume the extractor will drift field names away from a template's schema."
60259
60364
  ].join(`
60260
60365
  `)
60261
60366
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kadoa/mcp",
3
- "version": "0.5.20",
3
+ "version": "0.5.21",
4
4
  "description": "Kadoa MCP Server — manage workflows from Claude Desktop, Cursor, and other MCP clients",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",