@kadoa/mcp 0.5.7 → 0.5.8

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 +142 -12
  2. package/dist/index.js +179 -13
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -137,25 +137,90 @@ delete_workflow for each, confirming before proceeding.
137
137
  - Restart your MCP client
138
138
  - Re-authenticate via OAuth if prompted
139
139
 
140
- ## Deploying the Remote Server
140
+ ## Releases
141
141
 
142
- The remote MCP server at `mcp.kadoa.com` runs as a Docker container on GKE, deployed from the `kadoa-backend` monorepo.
142
+ This repo ships two surfaces, and a "release" usually touches both:
143
143
 
144
- **To deploy a new version:**
144
+ - **npm package `@kadoa/mcp`** stdio CLI users install locally (`npx @kadoa/mcp`). Cut by [Release Please](https://github.com/googleapis/release-please) from Conventional Commits on `main`.
145
+ - **Hosted server at `mcp.kadoa.com`** — Docker container on GKE, built and deployed from the [`kadoa-backend`](https://github.com/kadoa-org/kadoa-backend) monorepo, which pins a specific `@kadoa/mcp` version.
145
146
 
146
- 1. Publish a new `@kadoa/mcp` version to npm (`npm publish`)
147
- 2. In `kadoa-backend`, update `infra/docker/mcp/package.json` to the new version and run `bun install` to regenerate the lockfile
148
- 3. Merge to `main` — the CI pipeline (`main-build-deploy.yml`) builds and pushes the Docker image automatically
149
- 4. Trigger the **Deploy to Production** workflow (`deploy-prod.yml`) with:
147
+ Relevant files in `kadoa-backend`:
148
+ - `infra/docker/mcp/Dockerfile.mcp-server` image definition
149
+ - `infra/docker/mcp/package.json` — pinned `@kadoa/mcp` version
150
+ - `infra/cdk8s/mcp/charts/Server.ts` k8s manifest source (prod chart only; the test deployment is provisioned ad hoc — see below)
151
+
152
+ ### Production release
153
+
154
+ 1. Merge PRs to `main` using Conventional Commits (`feat:`, `fix:`, etc.). Release Please opens/maintains a `chore(main): release mcp x.y.z` PR.
155
+ 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).
156
+ 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.
157
+ 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).
158
+ 5. Trigger the **Deploy to Production** workflow ([`deploy-prod.yml`](https://github.com/kadoa-org/kadoa-backend/actions/workflows/deploy-prod.yml)) with:
150
159
  - **Target cluster:** `gcp`
151
160
  - **Deployment scope:** `mcp`
152
- - **Image tag:** the tag from step 3 (shown in the build summary)
161
+ - **Image tag:** the tag from step 4
153
162
  - **Method:** `kubectl`
154
163
 
155
- Infrastructure files in `kadoa-backend`:
156
- - `infra/docker/mcp/Dockerfile.mcp-server` — Docker image definition
157
- - `infra/docker/mcp/package.json` pinned `@kadoa/mcp` version
158
- - `infra/cdk8s/mcp/` — Kubernetes manifests (cdk8s)
164
+ ### RC / test release
165
+
166
+ 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.
167
+
168
+ There are two ways to consume an RC:
169
+
170
+ - **Local stdio**: `npx @kadoa/mcp@rc` — installs from the `rc` dist-tag on npm. Good for quick sanity checks where the bug doesn't depend on the hosted OAuth flow.
171
+ - **Hosted `mcp-server-test`**: a separate k8s deployment alongside prod in the same GKE cluster. Required when you need to validate the OAuth callback, Redis token store, multi-tenant session behavior, etc.
172
+
173
+ #### Publish an RC to npm
174
+
175
+ Manual — Release Please only cuts stable versions today.
176
+
177
+ ```bash
178
+ # from a branch on kadoa-mcp
179
+ bun install && bun run build
180
+ # bump version to a prerelease, e.g. 0.5.7-rc.1
181
+ npm version 0.5.7-rc.1 --no-git-tag-version
182
+ npm publish --tag rc --access public
183
+ ```
184
+
185
+ Verify: `npm view @kadoa/mcp dist-tags`. The `rc` tag should now point to your version. Existing tags today: `latest`, `rc`, `next`.
186
+
187
+ Promote later with: `npm dist-tag add @kadoa/mcp@0.5.7 latest` (run **after** the matching stable version has been published the normal way).
188
+
189
+ #### Deploy the RC to `mcp-server-test`
190
+
191
+ 1. In `kadoa-backend`, set `infra/docker/mcp/package.json` `@kadoa/mcp` to the RC version, `bun install`, push to a branch and merge to `main`. CI builds the `mcp-server:<IMAGE_TAG>` image as usual.
192
+ 2. Update the test deployment to the new image:
193
+
194
+ ```bash
195
+ gcloud container clusters get-credentials kadoa-prod \
196
+ --region=europe-west3 --project=oceanic-base-310208
197
+
198
+ kubectl set image deployment/mcp-server-test \
199
+ mcp-server=europe-west3-docker.pkg.dev/oceanic-base-310208/kadoa-artifacts/mcp-server:<IMAGE_TAG>
200
+ ```
201
+
202
+ 3. Port-forward to reach it (the test deployment has **no public ingress** by design):
203
+
204
+ ```bash
205
+ kubectl port-forward svc/mcp-server-test 3000:3000
206
+ ```
207
+
208
+ 4. Point your MCP client at `http://localhost:3000/mcp` and exercise the change.
209
+
210
+ **Auth gotcha:** `auth.kadoa.com` enforces an `ALLOWED_CALLBACK_ORIGINS` allowlist (`app.kadoa.com`, `mcp.kadoa.com`, `kadoa.com`) plus a loopback bypass for `127.0.0.1` when `NODE_ENV !== production`. `localhost` is **not** treated the same as `127.0.0.1` by that check. The test pod is configured with `MCP_SERVER_URL=http://localhost:3000` so the port-forwarded session resolves correctly. If you change the local port, update the deployment env var too:
211
+
212
+ ```bash
213
+ kubectl set env deployment/mcp-server-test MCP_SERVER_URL=http://localhost:<PORT>
214
+ ```
215
+
216
+ #### Promotion checklist
217
+
218
+ Before flipping `latest` / deploying to prod:
219
+
220
+ - [ ] RC consumed locally (`npx @kadoa/mcp@rc`) — stdio tools work
221
+ - [ ] RC consumed via `mcp-server-test` — OAuth login succeeds end-to-end
222
+ - [ ] No new errors in the test pod's logs (`kubectl logs deployment/mcp-server-test -f`)
223
+ - [ ] Release Please PR open with the stable version
159
224
 
160
225
  ## Development
161
226
 
@@ -180,6 +245,71 @@ KADOA_PUBLIC_API_URI=http://localhost:12380 bun run dev
180
245
 
181
246
  The server starts in HTTP mode. You authenticate via OAuth the same way as with the remote server.
182
247
 
248
+ ## Contributing
249
+
250
+ A typical feature touches three repos — **`kadoa-backend` → `kadoa-sdks` → `kadoa-mcp`**. The same surgery on three layers; same operation, different audience above it.
251
+
252
+ ### Architecture
253
+
254
+ | Layer | Repo / path | Role |
255
+ |---|---|---|
256
+ | Backend endpoint | `kadoa-backend/` | The HTTP API. Source of truth; the OpenAPI spec is generated from here. |
257
+ | SDK low-level | `kadoa-sdks/sdks/node/src/generated/api/*` | Auto-generated axios client. Raw HTTP, typed from the OpenAPI spec. **Do not hand-edit.** |
258
+ | SDK domain (high-level) | `kadoa-sdks/sdks/node/src/domains/<thing>/<thing>.service.ts` | Thin domain wrapper over the generated client. `client.<thing>.<action>(...)`. This is what callers use. |
259
+ | CLI | `kadoa-cli/src/commands/<thing>.ts` | Commander action: flag parsing, spinner, table/json output. Calls SDK domain. |
260
+ | MCP (this repo) | `kadoa-mcp/src/tools.ts` | Zod input schema + tool description tuned for LLMs. Calls SDK domain. |
261
+
262
+ Mental model:
263
+ - **SDK** = pure function over HTTP.
264
+ - **CLI** = SDK + human UX (flags, table output).
265
+ - **MCP** = SDK + LLM UX (zod schema, prose description).
266
+
267
+ CLI and MCP both call **identical** SDK domain methods. A new feature added to the SDK is picked up by both clients for free — as long as we keep the layering honest.
268
+
269
+ ### Adding a new feature
270
+
271
+ 1. **Backend** (`kadoa-backend`)
272
+ - Add or modify the HTTP endpoint.
273
+ - Make sure the OpenAPI spec reflects the change (paths, request/response schemas).
274
+
275
+ 2. **SDK** (`kadoa-sdks`) — always do this **before** MCP/CLI work (the SDK-first rule). No bandaid `axios` calls in MCP.
276
+ - Regenerate the low-level client:
277
+ ```bash
278
+ bun kadoa-codegen fetch-spec -e https://api.kadoa.com/openapi -f
279
+ bun kadoa-codegen generate -e https://api.kadoa.com/openapi --fetch-latest -f
280
+ ```
281
+ (Swap the endpoint for `http://localhost:12380/openapi` when developing against a local backend.)
282
+ - Add or extend the domain service in `sdks/node/src/domains/<thing>/<thing>.service.ts`. Keep it typed, no `any`.
283
+ - Commit `specs/` separately from generated/domain code so the API diff is reviewable.
284
+ - Open a PR. On merge, a new `@kadoa/node-sdk` version is released.
285
+
286
+ 3. **MCP** (this repo)
287
+ - Bump `@kadoa/node-sdk` in `package.json` to the new version, `bun install`.
288
+ - Add the tool in `src/tools.ts`:
289
+ - Zod input schema (use coercion helpers in `src/coercion.ts` where needed — LLM clients sometimes send strings for numbers/bools).
290
+ - Description string written **for the LLM**: when to call it, what inputs mean, what the response shape is.
291
+ - Implementation calls `ctx.client.<thing>.<action>(...)` — never raw HTTP.
292
+ - Add a unit test under `tests/unit/`.
293
+ - Use a Conventional Commit message (`feat(mcp): ...`, `fix(mcp): ...`). Release Please will cut the next version on merge to `main`.
294
+
295
+ 4. **CLI** (`kadoa-cli`), if the feature is user-facing on the command line: mirror the MCP step in `src/commands/<thing>.ts`. Same SDK call, different UX.
296
+
297
+ ### Anti-patterns
298
+
299
+ - ❌ Raw `axios` calls or hand-rolled HTTP in `src/tools.ts`. Always go through `ctx.client.<thing>`.
300
+ - ❌ Putting tool-shaped logic (input validation, response shaping) in the MCP layer when the SDK should expose it. If two clients would need the same logic, it belongs in the SDK.
301
+ - ❌ Editing files under `sdks/node/src/generated/` by hand. They get overwritten by `kadoa-codegen generate`.
302
+ - ❌ Bundling a spec update and an SDK feature in the same commit — keep `specs/` PRs separate from domain-method PRs for clean review.
303
+
304
+ ### Where to find things in this repo
305
+
306
+ - `src/tools.ts` — every MCP tool. Single file by design; search for the tool name.
307
+ - `src/auth.ts` — OAuth flow, JWT verification, team selection.
308
+ - `src/client.ts` — wires `@kadoa/node-sdk` per request, injects bearer token.
309
+ - `src/http.ts` — Express server, transport adapter, session handling.
310
+ - `src/redis-store.ts` — token store for the hosted server.
311
+ - `tests/unit/` — bun tests; run with `bun run test`.
312
+
183
313
  ## License
184
314
 
185
315
  MIT
package/dist/index.js CHANGED
@@ -44451,6 +44451,9 @@ function isNewerVersion(version1, version2) {
44451
44451
  }
44452
44452
  return false;
44453
44453
  }
44454
+ async function resolveBearerToken(provider) {
44455
+ return typeof provider === "function" ? await provider() : provider;
44456
+ }
44454
44457
  function createValidationDomain(core2, rules) {
44455
44458
  return {
44456
44459
  rules,
@@ -48714,7 +48717,7 @@ var import_debug, __require2, ChangeDifferenceType, KadoaErrorCode, _KadoaSdkExc
48714
48717
  }));
48715
48718
  return channels;
48716
48719
  }
48717
- }, PUBLIC_API_URI, WSS_API_URI, REALTIME_API_URI, SDK_VERSION = "0.33.0", SDK_NAME = "kadoa-node-sdk", SDK_LANGUAGE = "node", debug6, isDrainControlMessage = (message) => message.type === "control.draining", isRealtimeEvent = (message) => message.type !== "heartbeat" && message.type !== "control.draining", _Realtime = class _Realtime2 {
48720
+ }, PUBLIC_API_URI, WSS_API_URI, REALTIME_API_URI, SDK_VERSION = "0.34.0", SDK_NAME = "kadoa-node-sdk", SDK_LANGUAGE = "node", debug6, isDrainControlMessage = (message) => message.type === "control.draining", isRealtimeEvent = (message) => message.type !== "heartbeat" && message.type !== "control.draining", _Realtime = class _Realtime2 {
48718
48721
  constructor(config2) {
48719
48722
  this.drainingSockets = /* @__PURE__ */ new Set;
48720
48723
  this.lastHeartbeat = Date.now();
@@ -49117,6 +49120,54 @@ var import_debug, __require2, ChangeDifferenceType, KadoaErrorCode, _KadoaSdkExc
49117
49120
  }
49118
49121
  return version2;
49119
49122
  }
49123
+ async linkWorkflows(templateId, body) {
49124
+ debug7("Linking %d workflow(s) to template: %s", body.workflowIds?.length ?? 0, templateId);
49125
+ try {
49126
+ const response = await this.templatesApi.v4TemplatesTemplateIdLinkPost({
49127
+ templateId,
49128
+ linkWorkflowsBody: body
49129
+ });
49130
+ return response.data;
49131
+ } catch (error48) {
49132
+ if (KadoaHttpException.isInstance(error48)) {
49133
+ const httpError = error48;
49134
+ const conflicts = httpError.httpStatus === 409 ? httpError.responseBody?.conflicts : undefined;
49135
+ if (conflicts) {
49136
+ return {
49137
+ error: true,
49138
+ success: false,
49139
+ linkedCount: 0,
49140
+ workflowIds: [],
49141
+ conflicts
49142
+ };
49143
+ }
49144
+ }
49145
+ throw error48;
49146
+ }
49147
+ }
49148
+ async getLinkedWorkflows(templateId) {
49149
+ debug7("Listing workflows linked to template: %s", templateId);
49150
+ const response = await this.templatesApi.v4TemplatesTemplateIdWorkflowsGet({
49151
+ templateId
49152
+ });
49153
+ return response.data.data ?? [];
49154
+ }
49155
+ async applyVersion(templateId, body) {
49156
+ debug7("Applying template %s version %d to %d workflow(s)", templateId, body.targetVersion, body.workflowIds?.length ?? 0);
49157
+ const response = await this.templatesApi.v4TemplatesTemplateIdApplyPost({
49158
+ templateId,
49159
+ applyTemplateUpdateBody: body
49160
+ });
49161
+ return response.data;
49162
+ }
49163
+ async unlinkWorkflows(templateId, body) {
49164
+ debug7("Unlinking %d workflow(s) from template: %s", body.workflowIds?.length ?? 0, templateId);
49165
+ const response = await this.templatesApi.v4TemplatesTemplateIdUnlinkPost({
49166
+ templateId,
49167
+ unlinkWorkflowsBody: body
49168
+ });
49169
+ return response.data;
49170
+ }
49120
49171
  async listSchemas(templateId) {
49121
49172
  debug7("Listing schemas for template: %s", templateId);
49122
49173
  const response = await this.templatesApi.v4TemplatesTemplateIdSchemasGet({
@@ -49146,7 +49197,6 @@ var import_debug, __require2, ChangeDifferenceType, KadoaErrorCode, _KadoaSdkExc
49146
49197
  const response = await this.client.axiosInstance.get("/v5/user", {
49147
49198
  baseURL: this.client.baseUrl,
49148
49199
  headers: {
49149
- "x-api-key": this.client.apiKey,
49150
49200
  "Content-Type": "application/json"
49151
49201
  }
49152
49202
  });
@@ -49708,12 +49758,15 @@ var import_debug, __require2, ChangeDifferenceType, KadoaErrorCode, _KadoaSdkExc
49708
49758
  const timeout2 = config2.timeout ?? 30000;
49709
49759
  const headers = createSdkHeaders();
49710
49760
  this._axiosInstance = createAxiosInstance({ timeout: timeout2, headers });
49711
- this._axiosInstance.interceptors.request.use((reqConfig) => {
49761
+ this._axiosInstance.interceptors.request.use(async (reqConfig) => {
49712
49762
  if (this._bearerToken) {
49763
+ const token = await resolveBearerToken(this._bearerToken);
49713
49764
  if (!reqConfig.headers["Authorization"]) {
49714
- reqConfig.headers["Authorization"] = `Bearer ${this._bearerToken}`;
49765
+ reqConfig.headers["Authorization"] = `Bearer ${token}`;
49715
49766
  }
49716
49767
  delete reqConfig.headers["x-api-key"];
49768
+ } else if (this._apiKey && !reqConfig.headers["x-api-key"]) {
49769
+ reqConfig.headers["x-api-key"] = this._apiKey;
49717
49770
  }
49718
49771
  return reqConfig;
49719
49772
  });
@@ -49777,7 +49830,9 @@ var import_debug, __require2, ChangeDifferenceType, KadoaErrorCode, _KadoaSdkExc
49777
49830
  };
49778
49831
  }
49779
49832
  async listTeams(opts) {
49780
- const headers = opts?.bearerToken ? { Authorization: `Bearer ${opts.bearerToken}` } : undefined;
49833
+ const headers = opts?.bearerToken ? {
49834
+ Authorization: `Bearer ${await resolveBearerToken(opts.bearerToken)}`
49835
+ } : undefined;
49781
49836
  const response = await this._axiosInstance.get("/v5/user", {
49782
49837
  baseURL: this._baseUrl,
49783
49838
  ...headers && { headers }
@@ -51020,7 +51075,7 @@ function registerTools(server, ctx) {
51020
51075
  inputSchema: strictSchema({
51021
51076
  ...extractionInputShape,
51022
51077
  ...urlInputShape,
51023
- 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. Discover templates via list_templates."),
51078
+ 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."),
51024
51079
  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."),
51025
51080
  description: exports_external.string().max(500).optional().describe("Description of what this workflow does (max 500 characters)"),
51026
51081
  tags: exports_external.preprocess(coerceArray(true), exports_external.array(exports_external.string())).optional().describe("Tags for organizing workflows"),
@@ -51984,10 +52039,14 @@ function registerTools(server, ctx) {
51984
52039
  const TemplateSchemaFieldShape = {
51985
52040
  name: exports_external.string().describe("Field name"),
51986
52041
  description: exports_external.string().optional().describe("Field description"),
51987
- fieldType: exports_external.string().optional().describe("Field type (e.g. SCHEMA)"),
52042
+ fieldType: exports_external.string().optional().describe("Field type (e.g. SCHEMA, CLASSIFICATION)"),
51988
52043
  example: exports_external.string().optional().describe("Example value"),
51989
52044
  dataType: exports_external.string().optional().describe("Data type (STRING, NUMBER, DATE, LINK, etc.)"),
51990
- isKey: exports_external.preprocess(coerceBoolean(), exports_external.boolean()).optional().describe("Whether the field is a key field")
52045
+ isKey: exports_external.preprocess(coerceBoolean(), exports_external.boolean()).optional().describe("Whether the field is a key field"),
52046
+ categories: exports_external.preprocess(coerceArray(), exports_external.array(exports_external.object({
52047
+ title: exports_external.string().describe("Short title/label of the category"),
52048
+ definition: exports_external.string().describe("Full definition/description of the category")
52049
+ }))).optional().describe("Predefined categories for a CLASSIFICATION field ({title, definition}[]). Required for fieldType=CLASSIFICATION; omitted otherwise.")
51991
52050
  };
51992
52051
  server.registerTool("create_template_version", {
51993
52052
  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.",
@@ -51997,13 +52056,41 @@ function registerTools(server, ctx) {
51997
52056
  schemaId: exports_external.string().optional().describe("Existing schema ID to reference (mutually exclusive with schemaFields)"),
51998
52057
  schemaFields: exports_external.preprocess(coerceArray(), exports_external.array(exports_external.object(TemplateSchemaFieldShape))).optional().describe("Inline schema fields to create a new schema (mutually exclusive with schemaId)"),
51999
52058
  schemaEntity: exports_external.string().optional().describe("Entity name for the inline schema"),
52059
+ 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."),
52000
52060
  notifications: exports_external.preprocess(coerceArray(), exports_external.array(exports_external.object({
52001
52061
  eventType: exports_external.string().describe("Notification event type"),
52002
52062
  eventConfiguration: exports_external.preprocess(coerceJson(), exports_external.record(exports_external.string(), exports_external.unknown())).optional(),
52003
52063
  enabled: exports_external.preprocess(coerceBoolean(), exports_external.boolean()).optional(),
52004
52064
  channelIds: exports_external.preprocess(coerceArray(), exports_external.array(exports_external.string())).optional(),
52005
52065
  channels: exports_external.preprocess(coerceArray(), exports_external.array(exports_external.object({ channelId: exports_external.string() }))).optional()
52006
- }))).optional().describe("Notification configuration for this version")
52066
+ }))).optional().describe("Notification configuration for this version"),
52067
+ frequency: exports_external.preprocess(coerceJson(), exports_external.object({
52068
+ interval: exports_external.enum([
52069
+ "ONLY_ONCE",
52070
+ "EVERY_10_MINUTES",
52071
+ "HALF_HOURLY",
52072
+ "HOURLY",
52073
+ "THREE_HOURLY",
52074
+ "SIX_HOURLY",
52075
+ "TWELVE_HOURLY",
52076
+ "EIGHTEEN_HOURLY",
52077
+ "DAILY",
52078
+ "TWO_DAY",
52079
+ "THREE_DAY",
52080
+ "WEEKLY",
52081
+ "BIWEEKLY",
52082
+ "TRIWEEKLY",
52083
+ "FOUR_WEEKS",
52084
+ "MONTHLY",
52085
+ "CUSTOM",
52086
+ "REAL_TIME"
52087
+ ]).optional().describe("Scheduling interval the template controls for linked workflows. Use CUSTOM with 'schedules' for cron."),
52088
+ schedules: exports_external.preprocess(coerceArray(), exports_external.array(exports_external.object({
52089
+ type: exports_external.enum(["repeat", "cron"]).describe("'repeat' (interval token) or 'cron' (cron expressions)"),
52090
+ interval: exports_external.string().optional().describe("Repeat token (e.g. '1h', '1d') when type='repeat'"),
52091
+ expressions: exports_external.preprocess(coerceArray(true), exports_external.array(exports_external.string())).optional().describe("Cron expressions when type='cron' (e.g. '0 8 * * 2')")
52092
+ }))).optional().describe("Schedule specs. Required when interval='CUSTOM'.")
52093
+ })).optional().describe("Template-controlled scheduling for linked workflows ({interval, schedules}). Linked workflows inherit and cannot override this when set.")
52007
52094
  }),
52008
52095
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false }
52009
52096
  }, withErrorHandling("create_template_version", async (args) => {
@@ -52022,7 +52109,7 @@ function registerTools(server, ctx) {
52022
52109
  name: exports_external.string().optional().describe("Name for the new template (required if templateId is not set)"),
52023
52110
  description: exports_external.string().optional().describe("Description for the new template"),
52024
52111
  templateId: exports_external.string().optional().describe("Existing template ID to add a new version to (mutually exclusive with name)"),
52025
- includeParts: exports_external.preprocess(coerceArray(), exports_external.array(exports_external.enum(["prompt", "schema", "notifications"])).optional()).describe("Which parts of the workflow config to include. Omit to include all. At least 1 required if provided.")
52112
+ includeParts: exports_external.preprocess(coerceArray(), exports_external.array(exports_external.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.")
52026
52113
  }),
52027
52114
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false }
52028
52115
  }, withErrorHandling("save_workflow_as_template", async (args) => {
@@ -52049,6 +52136,83 @@ function registerTools(server, ctx) {
52049
52136
  const schemas4 = await ctx.client.template.listSchemas(args.templateId);
52050
52137
  return jsonResult({ schemas: schemas4, count: schemas4.length });
52051
52138
  }));
52139
+ server.registerTool("link_template_to_workflows", {
52140
+ 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.",
52141
+ inputSchema: strictSchema({
52142
+ templateId: exports_external.string().describe("The template ID to link workflows to"),
52143
+ 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."),
52144
+ force: exports_external.preprocess(coerceBoolean(), exports_external.boolean()).optional().describe("If true, relink workflows that are already linked to another template. Defaults to false.")
52145
+ }),
52146
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true }
52147
+ }, withErrorHandling("link_template_to_workflows", async (args) => {
52148
+ const result = await ctx.client.template.linkWorkflows(args.templateId, {
52149
+ workflowIds: args.workflowIds,
52150
+ ...args.force != null && { force: args.force }
52151
+ });
52152
+ if (result.conflicts && result.conflicts.length > 0) {
52153
+ const summary = result.conflicts.map((c) => `${c.workflowName || c.workflowId} (currently on "${c.templateName}")`).join(", ");
52154
+ return jsonResult({
52155
+ success: false,
52156
+ conflicts: result.conflicts,
52157
+ 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."
52158
+ });
52159
+ }
52160
+ return jsonResult({
52161
+ success: true,
52162
+ linkedCount: result.linkedCount,
52163
+ workflowIds: result.workflowIds,
52164
+ message: `Linked ${result.linkedCount} workflow(s) to template ${args.templateId}. Linked workflows now follow the template's schema.`
52165
+ });
52166
+ }));
52167
+ server.registerTool("apply_template_version", {
52168
+ 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.",
52169
+ inputSchema: strictSchema({
52170
+ templateId: exports_external.string().describe("The template ID whose version to apply"),
52171
+ targetVersion: exports_external.preprocess(coerceNumber(), exports_external.number()).describe("The template version number to apply to the workflows"),
52172
+ workflowIds: exports_external.preprocess(coerceArray(true), exports_external.array(exports_external.string()).min(1)).describe("Workflow IDs to update to the target version (array of strings). Also accepts a single ID string.")
52173
+ }),
52174
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true }
52175
+ }, withErrorHandling("apply_template_version", async (args) => {
52176
+ const result = await ctx.client.template.applyVersion(args.templateId, {
52177
+ targetVersion: args.targetVersion,
52178
+ workflowIds: args.workflowIds
52179
+ });
52180
+ return jsonResult({
52181
+ success: true,
52182
+ updatedCount: result.updatedCount,
52183
+ workflowIds: result.workflowIds,
52184
+ controlledParts: result.controlledParts,
52185
+ message: `Applied template ${args.templateId} v${args.targetVersion} to ${result.updatedCount} workflow(s).`
52186
+ });
52187
+ }));
52188
+ server.registerTool("list_template_workflows", {
52189
+ 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.",
52190
+ inputSchema: strictSchema({
52191
+ templateId: exports_external.string().describe("The template ID")
52192
+ }),
52193
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true }
52194
+ }, withErrorHandling("list_template_workflows", async (args) => {
52195
+ const workflows = await ctx.client.template.getLinkedWorkflows(args.templateId);
52196
+ return jsonResult({ workflows, count: workflows.length });
52197
+ }));
52198
+ server.registerTool("unlink_template_from_workflows", {
52199
+ 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).",
52200
+ inputSchema: strictSchema({
52201
+ templateId: exports_external.string().describe("The template ID to unlink workflows from"),
52202
+ workflowIds: exports_external.preprocess(coerceArray(true), exports_external.array(exports_external.string()).min(1)).describe("Workflow IDs to unlink from the template (array of strings). Also accepts a single ID string.")
52203
+ }),
52204
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true }
52205
+ }, withErrorHandling("unlink_template_from_workflows", async (args) => {
52206
+ const result = await ctx.client.template.unlinkWorkflows(args.templateId, {
52207
+ workflowIds: args.workflowIds
52208
+ });
52209
+ return jsonResult({
52210
+ success: true,
52211
+ unlinkedCount: result.unlinkedCount,
52212
+ workflowIds: result.workflowIds,
52213
+ message: `Unlinked ${result.unlinkedCount} workflow(s) from template ${args.templateId}.`
52214
+ });
52215
+ }));
52052
52216
  }
52053
52217
  var SchemaFieldShape, DASHBOARD_BASE_URL = "https://www.kadoa.com", WORKFLOW_AUDIT_WATCHED_KEYS;
52054
52218
  var init_tools = __esm(() => {
@@ -52082,7 +52246,7 @@ var package_default;
52082
52246
  var init_package = __esm(() => {
52083
52247
  package_default = {
52084
52248
  name: "@kadoa/mcp",
52085
- version: "0.5.7",
52249
+ version: "0.5.8",
52086
52250
  description: "Kadoa MCP Server — manage workflows from Claude Desktop, Cursor, and other MCP clients",
52087
52251
  type: "module",
52088
52252
  main: "dist/index.js",
@@ -52106,7 +52270,7 @@ var init_package = __esm(() => {
52106
52270
  prepublishOnly: "bun run check-types && bun run test:unit && bun run build"
52107
52271
  },
52108
52272
  dependencies: {
52109
- "@kadoa/node-sdk": "^0.33.0",
52273
+ "@kadoa/node-sdk": "^0.34.0",
52110
52274
  "@modelcontextprotocol/sdk": "^1.26.0",
52111
52275
  express: "^5.2.1",
52112
52276
  ioredis: "^5.6.1",
@@ -57336,7 +57500,9 @@ function createServer(auth) {
57336
57500
  "Use list_changes and get_change to retrieve detected diffs from real-time monitoring workflows.",
57337
57501
  "",
57338
57502
  "Schema tips: Use descriptive field names and examples. Group related data under one entity.",
57339
- "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."
57503
+ "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.",
57504
+ "",
57505
+ "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."
57340
57506
  ].join(`
57341
57507
  `)
57342
57508
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kadoa/mcp",
3
- "version": "0.5.7",
3
+ "version": "0.5.8",
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",
@@ -24,7 +24,7 @@
24
24
  "prepublishOnly": "bun run check-types && bun run test:unit && bun run build"
25
25
  },
26
26
  "dependencies": {
27
- "@kadoa/node-sdk": "^0.33.0",
27
+ "@kadoa/node-sdk": "^0.34.0",
28
28
  "@modelcontextprotocol/sdk": "^1.26.0",
29
29
  "express": "^5.2.1",
30
30
  "ioredis": "^5.6.1",