@clipform/mcp-server 2.4.0 → 2.5.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.
@@ -6,7 +6,7 @@ import {
6
6
  getWorkflowText,
7
7
  objectType,
8
8
  registerPrompts
9
- } from "./chunk-4G4MF3ME.js";
9
+ } from "./chunk-NYMC63ZK.js";
10
10
  import {
11
11
  GUIDE_TYPES,
12
12
  QUIZ_VARIANTS,
@@ -14,30 +14,32 @@ import {
14
14
  getGuideUri,
15
15
  guideFallbackText,
16
16
  registerResources
17
- } from "./chunk-IDOG3ZTF.js";
17
+ } from "./chunk-6EHOUAPE.js";
18
18
  import {
19
19
  ACTIVE_NODE_TYPES,
20
- BUSINESS,
21
20
  CONTACT_FIELDS,
22
21
  FORM_TYPE_ALIASES,
23
22
  KEN_BURNS_EFFECTS,
24
23
  KEN_BURNS_FIT_MODES,
25
24
  KEN_BURNS_PRESETS,
26
25
  MCP_TOOL_TIERS,
26
+ MCP_TOOL_TOOLSETS,
27
27
  NODE_TYPES,
28
28
  NON_COUNTABLE_TYPES,
29
29
  SLIDESHOW_TRANSITIONS,
30
+ bfsOrder,
30
31
  callApi,
32
+ defaultTarget,
31
33
  errorResult,
32
34
  exposedCompositionIds,
33
35
  resolveFormType,
34
36
  structuredResult,
35
37
  textResult
36
- } from "./chunk-5LV55T7F.js";
38
+ } from "./chunk-3BYQLPTT.js";
37
39
  import {
38
40
  getMcpAuth,
39
41
  runWithMcpTool
40
- } from "./chunk-V4L5RQWK.js";
42
+ } from "./chunk-ENHVQDSI.js";
41
43
  import {
42
44
  __commonJS,
43
45
  __export,
@@ -10749,7 +10751,7 @@ function object(shape, params) {
10749
10751
  return new ZodMiniObject(def);
10750
10752
  }
10751
10753
 
10752
- // ../../node_modules/.pnpm/@modelcontextprotocol+sdk@1.29.0_zod@3.25.76/node_modules/@modelcontextprotocol/sdk/dist/esm/server/zod-compat.js
10754
+ // ../../node_modules/.pnpm/@modelcontextprotocol+sdk@1.30.0_zod@3.25.76/node_modules/@modelcontextprotocol/sdk/dist/esm/server/zod-compat.js
10753
10755
  function isZ4Schema(s) {
10754
10756
  const schema = s;
10755
10757
  return !!schema._zod;
@@ -10833,17 +10835,33 @@ function normalizeObjectSchema(schema) {
10833
10835
  }
10834
10836
  return void 0;
10835
10837
  }
10838
+ function getDotPath(path) {
10839
+ if (path.length === 0) {
10840
+ return "object root";
10841
+ }
10842
+ return path.reduce((acc, seg, index) => {
10843
+ if (index === 0) {
10844
+ return String(seg);
10845
+ }
10846
+ if (typeof seg === "number") {
10847
+ return `${acc}[${seg}]`;
10848
+ }
10849
+ return `${acc}.${seg}`;
10850
+ }, "");
10851
+ }
10836
10852
  function getParseErrorMessage(error2) {
10837
10853
  if (error2 && typeof error2 === "object") {
10854
+ if ("issues" in error2 && Array.isArray(error2.issues) && error2.issues.length > 0) {
10855
+ return error2.issues.map((i) => {
10856
+ if (!i.path?.length) {
10857
+ return i.message;
10858
+ }
10859
+ return `${i.message} at ${getDotPath(i.path)}`;
10860
+ }).join("\n");
10861
+ }
10838
10862
  if ("message" in error2 && typeof error2.message === "string") {
10839
10863
  return error2.message;
10840
10864
  }
10841
- if ("issues" in error2 && Array.isArray(error2.issues) && error2.issues.length > 0) {
10842
- const firstIssue = error2.issues[0];
10843
- if (firstIssue && typeof firstIssue === "object" && "message" in firstIssue) {
10844
- return String(firstIssue.message);
10845
- }
10846
- }
10847
10865
  try {
10848
10866
  return JSON.stringify(error2);
10849
10867
  } catch {
@@ -11588,7 +11606,7 @@ function preprocess(fn, schema) {
11588
11606
  // ../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v4/classic/external.js
11589
11607
  config(en_default());
11590
11608
 
11591
- // ../../node_modules/.pnpm/@modelcontextprotocol+sdk@1.29.0_zod@3.25.76/node_modules/@modelcontextprotocol/sdk/dist/esm/types.js
11609
+ // ../../node_modules/.pnpm/@modelcontextprotocol+sdk@1.30.0_zod@3.25.76/node_modules/@modelcontextprotocol/sdk/dist/esm/types.js
11592
11610
  var LATEST_PROTOCOL_VERSION = "2025-11-25";
11593
11611
  var SUPPORTED_PROTOCOL_VERSIONS = [LATEST_PROTOCOL_VERSION, "2025-06-18", "2025-03-26", "2024-11-05", "2024-10-07"];
11594
11612
  var RELATED_TASK_META_KEY = "io.modelcontextprotocol/related-task";
@@ -13119,7 +13137,7 @@ var UrlElicitationRequiredError = class extends McpError {
13119
13137
  }
13120
13138
  };
13121
13139
 
13122
- // ../../node_modules/.pnpm/@modelcontextprotocol+sdk@1.29.0_zod@3.25.76/node_modules/@modelcontextprotocol/sdk/dist/esm/experimental/tasks/interfaces.js
13140
+ // ../../node_modules/.pnpm/@modelcontextprotocol+sdk@1.30.0_zod@3.25.76/node_modules/@modelcontextprotocol/sdk/dist/esm/experimental/tasks/interfaces.js
13123
13141
  function isTerminal(status) {
13124
13142
  return status === "completed" || status === "failed" || status === "cancelled";
13125
13143
  }
@@ -14408,7 +14426,7 @@ var zodToJsonSchema = (schema, options) => {
14408
14426
  return combined;
14409
14427
  };
14410
14428
 
14411
- // ../../node_modules/.pnpm/@modelcontextprotocol+sdk@1.29.0_zod@3.25.76/node_modules/@modelcontextprotocol/sdk/dist/esm/server/zod-json-schema-compat.js
14429
+ // ../../node_modules/.pnpm/@modelcontextprotocol+sdk@1.30.0_zod@3.25.76/node_modules/@modelcontextprotocol/sdk/dist/esm/server/zod-json-schema-compat.js
14412
14430
  function mapMiniTarget(t) {
14413
14431
  if (!t)
14414
14432
  return "draft-7";
@@ -14450,7 +14468,7 @@ function parseWithCompat(schema, data) {
14450
14468
  return result.data;
14451
14469
  }
14452
14470
 
14453
- // ../../node_modules/.pnpm/@modelcontextprotocol+sdk@1.29.0_zod@3.25.76/node_modules/@modelcontextprotocol/sdk/dist/esm/shared/protocol.js
14471
+ // ../../node_modules/.pnpm/@modelcontextprotocol+sdk@1.30.0_zod@3.25.76/node_modules/@modelcontextprotocol/sdk/dist/esm/shared/protocol.js
14454
14472
  var DEFAULT_REQUEST_TIMEOUT_MSEC = 6e4;
14455
14473
  var Protocol = class {
14456
14474
  constructor(_options) {
@@ -15404,7 +15422,7 @@ function mergeCapabilities(base, additional) {
15404
15422
  return result;
15405
15423
  }
15406
15424
 
15407
- // ../../node_modules/.pnpm/@modelcontextprotocol+sdk@1.29.0_zod@3.25.76/node_modules/@modelcontextprotocol/sdk/dist/esm/validation/ajv-provider.js
15425
+ // ../../node_modules/.pnpm/@modelcontextprotocol+sdk@1.30.0_zod@3.25.76/node_modules/@modelcontextprotocol/sdk/dist/esm/validation/ajv-provider.js
15408
15426
  var import_ajv = __toESM(require_ajv(), 1);
15409
15427
  var import_ajv_formats = __toESM(require_dist(), 1);
15410
15428
  function createDefaultAjvInstance() {
@@ -15472,7 +15490,7 @@ var AjvJsonSchemaValidator = class {
15472
15490
  }
15473
15491
  };
15474
15492
 
15475
- // ../../node_modules/.pnpm/@modelcontextprotocol+sdk@1.29.0_zod@3.25.76/node_modules/@modelcontextprotocol/sdk/dist/esm/experimental/tasks/server.js
15493
+ // ../../node_modules/.pnpm/@modelcontextprotocol+sdk@1.30.0_zod@3.25.76/node_modules/@modelcontextprotocol/sdk/dist/esm/experimental/tasks/server.js
15476
15494
  var ExperimentalServerTasks = class {
15477
15495
  constructor(_server) {
15478
15496
  this._server = _server;
@@ -15685,7 +15703,7 @@ var ExperimentalServerTasks = class {
15685
15703
  }
15686
15704
  };
15687
15705
 
15688
- // ../../node_modules/.pnpm/@modelcontextprotocol+sdk@1.29.0_zod@3.25.76/node_modules/@modelcontextprotocol/sdk/dist/esm/experimental/tasks/helpers.js
15706
+ // ../../node_modules/.pnpm/@modelcontextprotocol+sdk@1.30.0_zod@3.25.76/node_modules/@modelcontextprotocol/sdk/dist/esm/experimental/tasks/helpers.js
15689
15707
  function assertToolsCallTaskCapability(requests, method, entityName) {
15690
15708
  if (!requests) {
15691
15709
  throw new Error(`${entityName} does not support task creation (required for ${method})`);
@@ -15720,7 +15738,7 @@ function assertClientRequestTaskCapability(requests, method, entityName) {
15720
15738
  }
15721
15739
  }
15722
15740
 
15723
- // ../../node_modules/.pnpm/@modelcontextprotocol+sdk@1.29.0_zod@3.25.76/node_modules/@modelcontextprotocol/sdk/dist/esm/server/index.js
15741
+ // ../../node_modules/.pnpm/@modelcontextprotocol+sdk@1.30.0_zod@3.25.76/node_modules/@modelcontextprotocol/sdk/dist/esm/server/index.js
15724
15742
  var Server = class extends Protocol {
15725
15743
  /**
15726
15744
  * Initializes this server with the given name and version information.
@@ -15786,16 +15804,7 @@ var Server = class extends Protocol {
15786
15804
  if (!methodSchema) {
15787
15805
  throw new Error("Schema is missing a method literal");
15788
15806
  }
15789
- let methodValue;
15790
- if (isZ4Schema(methodSchema)) {
15791
- const v4Schema = methodSchema;
15792
- const v4Def = v4Schema._zod?.def;
15793
- methodValue = v4Def?.value ?? v4Schema.value;
15794
- } else {
15795
- const v3Schema = methodSchema;
15796
- const legacyDef = v3Schema._def;
15797
- methodValue = legacyDef?.value ?? v3Schema.value;
15798
- }
15807
+ const methodValue = getLiteralValue(methodSchema);
15799
15808
  if (typeof methodValue !== "string") {
15800
15809
  throw new Error("Schema method literal must be a string");
15801
15810
  }
@@ -16100,7 +16109,7 @@ var Server = class extends Protocol {
16100
16109
  }
16101
16110
  };
16102
16111
 
16103
- // ../../node_modules/.pnpm/@modelcontextprotocol+sdk@1.29.0_zod@3.25.76/node_modules/@modelcontextprotocol/sdk/dist/esm/server/completable.js
16112
+ // ../../node_modules/.pnpm/@modelcontextprotocol+sdk@1.30.0_zod@3.25.76/node_modules/@modelcontextprotocol/sdk/dist/esm/server/completable.js
16104
16113
  var COMPLETABLE_SYMBOL = /* @__PURE__ */ Symbol.for("mcp.completable");
16105
16114
  function isCompletable(schema) {
16106
16115
  return !!schema && typeof schema === "object" && COMPLETABLE_SYMBOL in schema;
@@ -16114,7 +16123,7 @@ var McpZodTypeKind;
16114
16123
  McpZodTypeKind2["Completable"] = "McpCompletable";
16115
16124
  })(McpZodTypeKind || (McpZodTypeKind = {}));
16116
16125
 
16117
- // ../../node_modules/.pnpm/@modelcontextprotocol+sdk@1.29.0_zod@3.25.76/node_modules/@modelcontextprotocol/sdk/dist/esm/shared/toolNameValidation.js
16126
+ // ../../node_modules/.pnpm/@modelcontextprotocol+sdk@1.30.0_zod@3.25.76/node_modules/@modelcontextprotocol/sdk/dist/esm/shared/toolNameValidation.js
16118
16127
  var TOOL_NAME_REGEX = /^[A-Za-z0-9._-]{1,128}$/;
16119
16128
  function validateToolName(name) {
16120
16129
  const warnings = [];
@@ -16172,7 +16181,7 @@ function validateAndWarnToolName(name) {
16172
16181
  return result.isValid;
16173
16182
  }
16174
16183
 
16175
- // ../../node_modules/.pnpm/@modelcontextprotocol+sdk@1.29.0_zod@3.25.76/node_modules/@modelcontextprotocol/sdk/dist/esm/experimental/tasks/mcp-server.js
16184
+ // ../../node_modules/.pnpm/@modelcontextprotocol+sdk@1.30.0_zod@3.25.76/node_modules/@modelcontextprotocol/sdk/dist/esm/experimental/tasks/mcp-server.js
16176
16185
  var ExperimentalMcpServerTasks = class {
16177
16186
  constructor(_mcpServer) {
16178
16187
  this._mcpServer = _mcpServer;
@@ -16187,7 +16196,7 @@ var ExperimentalMcpServerTasks = class {
16187
16196
  }
16188
16197
  };
16189
16198
 
16190
- // ../../node_modules/.pnpm/@modelcontextprotocol+sdk@1.29.0_zod@3.25.76/node_modules/@modelcontextprotocol/sdk/dist/esm/server/mcp.js
16199
+ // ../../node_modules/.pnpm/@modelcontextprotocol+sdk@1.30.0_zod@3.25.76/node_modules/@modelcontextprotocol/sdk/dist/esm/server/mcp.js
16191
16200
  var McpServer = class {
16192
16201
  constructor(serverInfo, options) {
16193
16202
  this._registeredResources = {};
@@ -17158,7 +17167,11 @@ Example: A form that asks a question, collects contact info, then finishes:
17158
17167
  },
17159
17168
  outputSchema: {
17160
17169
  form_id: external_exports.string().describe("Form UUID - pass to follow-up tools"),
17161
- viewer_url: external_exports.string().describe("Live form URL for respondents"),
17170
+ // #3330: an anonymous session's viewer_url is a scoped preview link
17171
+ // (only the caller can open it) until the account is claimed and the
17172
+ // form is published - only an authenticated/claimed workspace's
17173
+ // create returns a genuinely live URL here.
17174
+ viewer_url: external_exports.string().describe("Form URL for respondents - a live public URL once authenticated/published, or a scoped preview-only link for an anonymous session"),
17162
17175
  dashboard_url: external_exports.string().optional().describe("Builder URL (sign in to edit)"),
17163
17176
  nodes: external_exports.array(external_exports.object({ id: external_exports.string(), type: external_exports.string(), prompt: external_exports.string() })).describe("Created nodes in order - IDs for attach_node_media / update_node"),
17164
17177
  plan: external_exports.object({ name: external_exports.string(), auth_mode: external_exports.string() }).optional().describe("Plan and auth context for this session")
@@ -17167,7 +17180,11 @@ Example: A form that asks a question, collects contact info, then finishes:
17167
17180
  readOnlyHint: false,
17168
17181
  destructiveHint: false,
17169
17182
  idempotentHint: false,
17170
- openWorldHint: false
17183
+ // The form is live at a public (unguessable-share-link) URL from
17184
+ // creation - OpenAI's openWorldHint rubric reads "publishing
17185
+ // content" as true regardless of link guessability (#2976 research
17186
+ // pass 2).
17187
+ openWorldHint: true
17171
17188
  },
17172
17189
  _meta: { "anthropic/alwaysLoad": true }
17173
17190
  },
@@ -17195,8 +17212,7 @@ Example: A form that asks a question, collects contact info, then finishes:
17195
17212
  };
17196
17213
  const contentCount = nodes.filter((q) => !NON_COUNTABLE_TYPES.includes(q.type)).length;
17197
17214
  if (planContext.node_limit !== null && contentCount > planContext.node_limit) {
17198
- const upgradeUrl = `${BUSINESS.urls.dashboard}/billing`;
17199
- const message = planContext.is_authenticated ? `Your '${planContext.workspace_name}' workspace is on the ${planContext.plan_name} plan, capped at ${planContext.node_limit} nodes per form. You asked for ${contentCount}. Either rerun with ${planContext.node_limit} nodes or upgrade at ${upgradeUrl}.` : `Anonymous sessions are capped at ${planContext.node_limit} nodes per form (${planContext.plan_name} plan). You asked for ${contentCount}. Either rerun with ${planContext.node_limit} nodes, or sign in to your Clipform account so forms land in your workspace with your real plan limits.`;
17215
+ const message = planContext.is_authenticated ? `Your '${planContext.workspace_name}' workspace is on the ${planContext.plan_name} plan, capped at ${planContext.node_limit} nodes per form. You asked for ${contentCount}. Rerun with ${planContext.node_limit} nodes or fewer.` : `Anonymous sessions are capped at ${planContext.node_limit} nodes per form (${planContext.plan_name} plan). You asked for ${contentCount}. Either rerun with ${planContext.node_limit} nodes, or sign in to your Clipform account so forms land in your workspace with your real plan limits.`;
17200
17216
  return errorResult(message);
17201
17217
  }
17202
17218
  if (planContext.form_limit !== null) {
@@ -17204,10 +17220,8 @@ Example: A form that asks a question, collects contact info, then finishes:
17204
17220
  if (formsResult.ok) {
17205
17221
  const forms = formsResult.data.forms ?? [];
17206
17222
  if (forms.length >= planContext.form_limit) {
17207
- const upgradeUrl = `${BUSINESS.urls.dashboard}/billing`;
17208
17223
  const formList = forms.map((f, i) => ` ${i + 1}. "${f.title || "Untitled"}" (id: ${f.id})`).join("\n");
17209
- const options = planContext.is_authenticated ? ` - Upgrade for unlimited forms: ${upgradeUrl}
17210
- - Or delete one of the forms below to free a slot.` : ` - Delete one of the forms below to free a slot.
17224
+ const options = planContext.is_authenticated ? ` - Delete one of the forms below to free a slot.` : ` - Delete one of the forms below to free a slot.
17211
17225
  - Or sign in to a Clipform account to keep these forms and get higher limits.`;
17212
17226
  return errorResult(
17213
17227
  [
@@ -17219,7 +17233,7 @@ Example: A form that asks a question, collects contact info, then finishes:
17219
17233
  `Current forms:`,
17220
17234
  formList,
17221
17235
  ``,
17222
- `Share this list with the user, explain they've hit the ${planContext.form_limit}-form limit, and let them choose: upgrade, or pick a form to delete. clipform_delete_form asks the user to confirm before removing anything.`
17236
+ `Share this list with the user, explain they've hit the ${planContext.form_limit}-form limit, and let them pick a form to delete. clipform_delete_form asks the user to confirm before removing anything.`
17223
17237
  ].join("\n")
17224
17238
  );
17225
17239
  }
@@ -17249,6 +17263,7 @@ Example: A form that asks a question, collects contact info, then finishes:
17249
17263
  const formId = data.form_id;
17250
17264
  const formUrl = data.form_url ?? void 0;
17251
17265
  const createdNodes = data.nodes ?? [];
17266
+ const isPreview = String(data.viewer_url ?? "").includes("mode=preview");
17252
17267
  if (font_family !== void 0) {
17253
17268
  await callApi(`/forms/${formId}`, {
17254
17269
  method: "PATCH",
@@ -17263,7 +17278,7 @@ Example: A form that asks a question, collects contact info, then finishes:
17263
17278
  }
17264
17279
  const truncate = (s, n = 60) => s.length > n ? `${s.slice(0, n - 1)}\u2026` : s;
17265
17280
  const lines = [
17266
- `Form created and live.`,
17281
+ isPreview ? `Form created as a preview draft.` : `Form created and live.`,
17267
17282
  ``,
17268
17283
  `Title: ${title}`,
17269
17284
  `Form ID: ${formId}`,
@@ -17271,15 +17286,17 @@ Example: A form that asks a question, collects contact info, then finishes:
17271
17286
  `Node IDs (use directly with attach_node_media / update_node - no get_form round-trip needed):`,
17272
17287
  ...createdNodes.map((n) => `- [${n.type}] "${truncate(n.prompt)}": ${n.id}`),
17273
17288
  ``,
17274
- `FORM URL (live - share this with respondents): ${data.viewer_url}`
17289
+ isPreview ? `PREVIEW URL (draft - only you can open this, not yet visible to respondents): ${data.viewer_url}` : `FORM URL (live - share this with respondents): ${data.viewer_url}`
17275
17290
  ];
17276
17291
  if (formUrl) {
17277
- lines.push(`DASHBOARD URL (sign in to edit and manage): ${formUrl}`);
17292
+ lines.push(
17293
+ isPreview ? `CLAIM URL (one-time - claim this account to publish): ${formUrl}` : `DASHBOARD URL (sign in to edit and manage): ${formUrl}`
17294
+ );
17278
17295
  }
17279
17296
  lines.push(
17280
17297
  ``,
17281
- `The form is live and accessible at the FORM URL above. Continue with media/narration steps if needed - changes appear immediately.`,
17282
- `The URLs above are exact values; constructed or guessed URLs return 404. Use the FORM URL and DASHBOARD URL as shown.`,
17298
+ isPreview ? `This form is a preview - claim and publish to start collecting responses. Open the CLAIM URL to sign into the account this form already belongs to, then republish with clipform_update_form (is_live: true). The PREVIEW URL never becomes public on its own.` : `The form is live and accessible at the FORM URL above. Edits and media attached AFTER this point are NOT visible to respondents until you re-send is_live: true via clipform_update_form - republishing is the expected final step of a build, never skip it after attaching media/narration.`,
17299
+ `The URLs above are exact values; constructed or guessed URLs return 404. Use the ${isPreview ? "PREVIEW" : "FORM"} URL and ${isPreview ? "CLAIM" : "DASHBOARD"} URL as shown.`,
17283
17300
  `Pass form_id on follow-up tools (get_form, add_node, attach_node_media, etc.).`
17284
17301
  );
17285
17302
  if (planContext) {
@@ -17322,7 +17339,7 @@ function registerListFormsTool(server) {
17322
17339
  "clipform_list_forms",
17323
17340
  {
17324
17341
  title: "List Clipforms",
17325
- description: `List forms in your workspace with optional filtering. Returns paginated results (cursor-based). next_cursor is included when more results exist.`,
17342
+ description: `List forms in your workspace with optional filtering. Returns paginated results (cursor-based). next_cursor is included when more results exist. To find or open a form by name, pass search with part of its title instead of paging through results.`,
17326
17343
  inputSchema: {
17327
17344
  limit: external_exports.number().int().min(1).max(100).optional().describe("Number of forms to return (default 25, max 100)"),
17328
17345
  cursor: external_exports.string().optional().describe(
@@ -17332,7 +17349,9 @@ function registerListFormsTool(server) {
17332
17349
  "Filter by tag name(s), comma-separated. AND logic: only forms with ALL tags are returned."
17333
17350
  ),
17334
17351
  published: external_exports.enum(["true", "false"]).optional().describe("Filter by publish status"),
17335
- search: external_exports.string().optional().describe("Search forms by title (case-insensitive substring match)"),
17352
+ search: external_exports.string().optional().describe(
17353
+ "Filter to forms whose title contains this text (case-insensitive substring). Use this to resolve a form by name in one call instead of paginating."
17354
+ ),
17336
17355
  sort: external_exports.enum(["created_at", "updated_at"]).optional().describe("Sort field (default: created_at)"),
17337
17356
  order: external_exports.enum(["asc", "desc"]).optional().describe("Sort order (default: desc, newest first)")
17338
17357
  },
@@ -17344,8 +17363,6 @@ function registerListFormsTool(server) {
17344
17363
  share_id: external_exports.string(),
17345
17364
  is_live: external_exports.boolean(),
17346
17365
  has_unpublished_changes: external_exports.boolean(),
17347
- created_at: external_exports.string(),
17348
- updated_at: external_exports.string(),
17349
17366
  tags: external_exports.array(external_exports.object({ id: external_exports.string(), name: external_exports.string(), color: external_exports.string().nullable() }))
17350
17367
  })
17351
17368
  ).describe("Matching forms (empty if none)"),
@@ -17379,8 +17396,6 @@ function registerListFormsTool(server) {
17379
17396
  share_id: f.share_id,
17380
17397
  is_live: f.is_live,
17381
17398
  has_unpublished_changes: f.has_unpublished_changes,
17382
- created_at: f.created_at,
17383
- updated_at: f.updated_at,
17384
17399
  tags: f.tags.map((t) => ({ id: t.id, name: t.name, color: t.color }))
17385
17400
  })),
17386
17401
  next_cursor: data.next_cursor
@@ -17406,30 +17421,78 @@ function registerListFormsTool(server) {
17406
17421
  }
17407
17422
 
17408
17423
  // src/lib/format-form.ts
17424
+ function allowedConfigKeys(nodeType) {
17425
+ const schema = NODE_TYPES[nodeType]?.config_schema;
17426
+ const properties = schema?.properties;
17427
+ if (properties) return new Set(Object.keys(properties));
17428
+ const oneOf = schema?.oneOf;
17429
+ if (Array.isArray(oneOf)) {
17430
+ const keys = /* @__PURE__ */ new Set();
17431
+ for (const branch of oneOf) {
17432
+ for (const k of Object.keys(branch?.properties ?? {})) keys.add(k);
17433
+ }
17434
+ return keys;
17435
+ }
17436
+ return /* @__PURE__ */ new Set();
17437
+ }
17438
+ function projectConfig(nodeType, config2) {
17439
+ if (!config2) return void 0;
17440
+ const allowed = allowedConfigKeys(nodeType);
17441
+ const projected = {};
17442
+ for (const [key, value] of Object.entries(config2)) {
17443
+ if (allowed.has(key)) projected[key] = value;
17444
+ }
17445
+ return projected;
17446
+ }
17409
17447
  function getFormNodes(data) {
17410
17448
  return data.node ?? data.nodes ?? [];
17411
17449
  }
17450
+ function buildLogicByNode(nodes) {
17451
+ const logicByNode = /* @__PURE__ */ new Map();
17452
+ for (const node of nodes) {
17453
+ if (node.node_logic && node.node_logic.length > 0) {
17454
+ logicByNode.set(
17455
+ node.id,
17456
+ node.node_logic.flatMap((entry) => entry.actions ?? [])
17457
+ );
17458
+ }
17459
+ }
17460
+ return logicByNode;
17461
+ }
17462
+ function orderNodesWithLogic(data) {
17463
+ const rawNodes = getFormNodes(data);
17464
+ const logicByNode = buildLogicByNode(rawNodes);
17465
+ return { nodes: bfsOrder(rawNodes, logicByNode), logicByNode };
17466
+ }
17467
+ function projectNext(node, logicByNode) {
17468
+ const target = defaultTarget(logicByNode.get(node.id));
17469
+ return target ? { target_id: target.value, target_type: target.type } : void 0;
17470
+ }
17412
17471
  function projectFormState(data) {
17472
+ const { nodes, logicByNode } = orderNodesWithLogic(data);
17413
17473
  return {
17414
17474
  form_id: String(data.form_id ?? ""),
17415
17475
  title: String(data.title ?? ""),
17416
17476
  is_live: Boolean(data.is_live),
17417
17477
  has_unpublished_changes: Boolean(data.has_unpublished_changes),
17418
- nodes: getFormNodes(data).map((q) => {
17478
+ nodes: nodes.map((q) => {
17419
17479
  const options = q.node_options ?? q.options;
17480
+ const config2 = projectConfig(q.type, q.config);
17481
+ const next = projectNext(q, logicByNode);
17420
17482
  return {
17421
17483
  id: q.id,
17422
17484
  type: q.type,
17423
17485
  prompt: q.prompt ?? "",
17424
17486
  required: Boolean(q.required),
17425
- ...q.config && Object.keys(q.config).length > 0 ? { config: q.config } : {},
17426
- ...options && options.length > 0 ? { options: options.map((o) => ({ id: o.id, content: o.content })) } : {}
17487
+ ...config2 && Object.keys(config2).length > 0 ? { config: config2 } : {},
17488
+ ...options && options.length > 0 ? { options: options.map((o) => ({ id: o.id, content: o.content })) } : {},
17489
+ ...next ? { next } : {}
17427
17490
  };
17428
17491
  })
17429
17492
  };
17430
17493
  }
17431
17494
  function formatFormState(data) {
17432
- const nodes = getFormNodes(data);
17495
+ const { nodes, logicByNode } = orderNodesWithLogic(data);
17433
17496
  const lines = [
17434
17497
  `Form: ${data.title}`,
17435
17498
  `Form ID: ${data.form_id}`,
@@ -17446,8 +17509,9 @@ function formatFormState(data) {
17446
17509
  lines.push(` ${i + 1}. [${q.type}] ${q.prompt || "(no prompt)"}`);
17447
17510
  lines.push(` Node ID: ${q.id}`);
17448
17511
  if (q.required) lines.push(` Required: yes`);
17449
- if (q.config && Object.keys(q.config).length > 0) {
17450
- lines.push(` Config: ${JSON.stringify(q.config)}`);
17512
+ const config2 = projectConfig(q.type, q.config);
17513
+ if (config2 && Object.keys(config2).length > 0) {
17514
+ lines.push(` Config: ${JSON.stringify(config2)}`);
17451
17515
  }
17452
17516
  const options = q.node_options ?? q.options;
17453
17517
  if (options && options.length > 0) {
@@ -17455,6 +17519,10 @@ function formatFormState(data) {
17455
17519
  ` Options: ${options.map((o) => o.content).join(", ")}`
17456
17520
  );
17457
17521
  }
17522
+ const next = projectNext(q, logicByNode);
17523
+ if (next) {
17524
+ lines.push(` -> next: ${next.target_id} (${next.target_type})`);
17525
+ }
17458
17526
  }
17459
17527
  return lines.join("\n");
17460
17528
  }
@@ -17465,7 +17533,7 @@ function registerGetFormTool(server) {
17465
17533
  "clipform_get_form",
17466
17534
  {
17467
17535
  title: "Get Clipform",
17468
- description: `Retrieve a form's details including all nodes in sequential order. Returns title, settings, and every node with its options, config, and media status.`,
17536
+ description: `Retrieve a form's details including all nodes in sequential order and their routing. Returns title, settings, and every node with its options, config, media status, and next node (from clipform_set_logic).`,
17469
17537
  inputSchema: {
17470
17538
  form_id: external_exports.string().uuid().describe("The form UUID (returned by clipform_create_form, not the short share_id from the URL)")
17471
17539
  },
@@ -17481,7 +17549,11 @@ function registerGetFormTool(server) {
17481
17549
  prompt: external_exports.string(),
17482
17550
  required: external_exports.boolean(),
17483
17551
  config: external_exports.record(external_exports.unknown()).optional(),
17484
- options: external_exports.array(external_exports.object({ id: external_exports.string(), content: external_exports.string() })).optional()
17552
+ options: external_exports.array(external_exports.object({ id: external_exports.string(), content: external_exports.string() })).optional(),
17553
+ next: external_exports.object({
17554
+ target_id: external_exports.string().describe("The node/screen ID this node routes to"),
17555
+ target_type: external_exports.enum(["node", "end_screen", "redirect", "link_list"]).describe("What kind of target target_id is")
17556
+ }).optional().describe("This node's outgoing edge, set via clipform_set_logic. Omitted for a chain tail or terminal node.")
17485
17557
  })
17486
17558
  ).describe("Nodes in sequential order - IDs for update_node / attach_node_media")
17487
17559
  },
@@ -17493,7 +17565,7 @@ function registerGetFormTool(server) {
17493
17565
  }
17494
17566
  },
17495
17567
  async ({ form_id }) => {
17496
- const result = await callApi(`/forms/${form_id}`);
17568
+ const result = await callApi(`/forms/${form_id}?include=logic`);
17497
17569
  if (!result.ok) {
17498
17570
  return errorResult(result.error);
17499
17571
  }
@@ -17526,11 +17598,20 @@ function registerUpdateFormTool(server) {
17526
17598
  brand_name: external_exports.string().nullable().optional().describe("AI-PROTECTED: Custom brand name shown in footer. Only set when user explicitly provides."),
17527
17599
  tags: external_exports.array(external_exports.string()).optional().describe("Replace all tags on this form. Pass the full desired set (e.g. ['quiz', 'trivia', 'slug:elephants']). Omit to leave tags unchanged.")
17528
17600
  },
17601
+ outputSchema: {
17602
+ form_id: external_exports.string().describe("Form UUID"),
17603
+ updated_fields: external_exports.array(external_exports.string()).describe("Names of the fields that were changed by this call")
17604
+ },
17529
17605
  annotations: {
17530
17606
  readOnlyHint: false,
17531
- destructiveHint: false,
17607
+ // is_live: true/false publishes, republishes, or unpublishes the
17608
+ // live form, and other fields (title, theme, logo) overwrite what
17609
+ // respondents already see (#2976 research pass 2).
17610
+ destructiveHint: true,
17532
17611
  idempotentHint: true,
17533
- openWorldHint: false
17612
+ // is_live: true makes the form live at a public URL - openWorldHint
17613
+ // per OpenAI's "publishing content" reading (#2976 research pass 2).
17614
+ openWorldHint: true
17534
17615
  }
17535
17616
  },
17536
17617
  async ({ form_id, title, is_live, show_step_counter, disable_back_navigation, total_steps, primary_color, background_color, font_family, description, author, logo_url, show_branding, brand_name, tags }) => {
@@ -17567,34 +17648,64 @@ function registerUpdateFormTool(server) {
17567
17648
  }
17568
17649
  }
17569
17650
  const updates = [];
17570
- if (title !== void 0) updates.push(`Title \u2192 "${title}"`);
17571
- if (is_live !== void 0)
17651
+ const updatedFields = [];
17652
+ if (title !== void 0) {
17653
+ updates.push(`Title \u2192 "${title}"`);
17654
+ updatedFields.push("title");
17655
+ }
17656
+ if (is_live !== void 0) {
17572
17657
  updates.push(`Live \u2192 ${is_live}`);
17573
- if (show_step_counter !== void 0)
17658
+ updatedFields.push("is_live");
17659
+ }
17660
+ if (show_step_counter !== void 0) {
17574
17661
  updates.push(`Step counter \u2192 ${show_step_counter}`);
17575
- if (disable_back_navigation !== void 0)
17662
+ updatedFields.push("show_step_counter");
17663
+ }
17664
+ if (disable_back_navigation !== void 0) {
17576
17665
  updates.push(`Back navigation \u2192 ${disable_back_navigation ? "disabled" : "enabled"}`);
17577
- if (total_steps !== void 0)
17666
+ updatedFields.push("disable_back_navigation");
17667
+ }
17668
+ if (total_steps !== void 0) {
17578
17669
  updates.push(`Total steps \u2192 ${total_steps === null ? "auto" : total_steps}`);
17579
- if (primary_color !== void 0)
17670
+ updatedFields.push("total_steps");
17671
+ }
17672
+ if (primary_color !== void 0) {
17580
17673
  updates.push(`Primary color \u2192 ${primary_color}`);
17581
- if (background_color !== void 0)
17674
+ updatedFields.push("primary_color");
17675
+ }
17676
+ if (background_color !== void 0) {
17582
17677
  updates.push(`Background color \u2192 ${background_color}`);
17583
- if (font_family !== void 0)
17678
+ updatedFields.push("background_color");
17679
+ }
17680
+ if (font_family !== void 0) {
17584
17681
  updates.push(`Font \u2192 ${font_family}`);
17585
- if (description !== void 0)
17682
+ updatedFields.push("font_family");
17683
+ }
17684
+ if (description !== void 0) {
17586
17685
  updates.push(`Description \u2192 ${description === null ? "cleared" : `"${description}"`}`);
17587
- if (author !== void 0)
17686
+ updatedFields.push("description");
17687
+ }
17688
+ if (author !== void 0) {
17588
17689
  updates.push(`Author \u2192 ${author === null ? "cleared" : `"${author}"`}`);
17589
- if (logo_url !== void 0)
17690
+ updatedFields.push("author");
17691
+ }
17692
+ if (logo_url !== void 0) {
17590
17693
  updates.push(`Logo URL \u2192 ${logo_url === null ? "cleared" : logo_url}`);
17591
- if (show_branding !== void 0)
17694
+ updatedFields.push("logo_url");
17695
+ }
17696
+ if (show_branding !== void 0) {
17592
17697
  updates.push(`Show branding \u2192 ${show_branding}`);
17593
- if (brand_name !== void 0)
17698
+ updatedFields.push("show_branding");
17699
+ }
17700
+ if (brand_name !== void 0) {
17594
17701
  updates.push(`Brand name \u2192 ${brand_name === null ? "cleared" : `"${brand_name}"`}`);
17595
- if (tags)
17702
+ updatedFields.push("brand_name");
17703
+ }
17704
+ if (tags) {
17596
17705
  updates.push(`Tags \u2192 [${tags.join(", ")}]`);
17597
- return textResult(`Form updated: ${updates.join(", ")}`);
17706
+ updatedFields.push("tags");
17707
+ }
17708
+ return structuredResult(`Form updated: ${updates.join(", ")}`, { form_id, updated_fields: updatedFields });
17598
17709
  }
17599
17710
  );
17600
17711
  }
@@ -17636,8 +17747,13 @@ function registerDeleteFormTool(server) {
17636
17747
  annotations: {
17637
17748
  readOnlyHint: false,
17638
17749
  destructiveHint: true,
17639
- idempotentHint: false,
17640
- openWorldHint: false
17750
+ // Deleting an already-trashed form has no further effect -
17751
+ // calling this twice with the same form_id is a no-op the second
17752
+ // time (textbook idempotent delete, #2976).
17753
+ idempotentHint: true,
17754
+ // Removes a form's public page - openWorldHint per OpenAI's
17755
+ // "publishing content" reading (#2976 research pass 2).
17756
+ openWorldHint: true
17641
17757
  }
17642
17758
  },
17643
17759
  async ({ form_id, confirm }) => {
@@ -17660,13 +17776,210 @@ function registerDeleteFormTool(server) {
17660
17776
  );
17661
17777
  }
17662
17778
 
17779
+ // src/tools/get-results.ts
17780
+ var MAX_TEXT_LENGTH = 500;
17781
+ var MAX_LABEL_LENGTH = 80;
17782
+ var MAX_RECENT_ANSWERS = 20;
17783
+ function coerceToString(value, maxLength) {
17784
+ if (value === null || value === void 0) return "";
17785
+ if (typeof value === "string") {
17786
+ return value.length > maxLength ? `${value.slice(0, maxLength)}\u2026` : value;
17787
+ }
17788
+ if (typeof value === "number" || typeof value === "boolean") {
17789
+ return String(value);
17790
+ }
17791
+ let json;
17792
+ try {
17793
+ json = JSON.stringify(value);
17794
+ } catch {
17795
+ return "";
17796
+ }
17797
+ return json.length > maxLength ? `${json.slice(0, maxLength)}\u2026` : json;
17798
+ }
17799
+ function projectNode(node) {
17800
+ const base = {
17801
+ node_id: node.node_id,
17802
+ node_type: node.node_type,
17803
+ prompt: node.node_text,
17804
+ total_responses: node.total_responses,
17805
+ total_views: node.total_views
17806
+ };
17807
+ if (node.node_type === "choice") {
17808
+ const answers = node.answers;
17809
+ return {
17810
+ ...base,
17811
+ options: answers.map((a) => ({
17812
+ label: a.answer_text,
17813
+ count: a.count,
17814
+ percentage: a.percentage
17815
+ }))
17816
+ };
17817
+ }
17818
+ if (node.node_type === "payment") {
17819
+ return base;
17820
+ }
17821
+ if (node.node_category === "action") {
17822
+ const answers = node.answers;
17823
+ return {
17824
+ ...base,
17825
+ options: answers.map((a) => ({
17826
+ label: coerceToString(a.answer_value, MAX_LABEL_LENGTH),
17827
+ count: a.count,
17828
+ percentage: a.percentage
17829
+ }))
17830
+ };
17831
+ }
17832
+ if (node.node_type === "open") {
17833
+ const answers = node.answers;
17834
+ return {
17835
+ ...base,
17836
+ recent_answers: answers.slice(0, MAX_RECENT_ANSWERS).map((a) => ({
17837
+ text: coerceToString(a.answer_value, MAX_TEXT_LENGTH),
17838
+ submitted_at: a.submitted_at
17839
+ }))
17840
+ };
17841
+ }
17842
+ return base;
17843
+ }
17844
+ function registerGetResultsTool(server) {
17845
+ server.registerTool(
17846
+ "clipform_get_results",
17847
+ {
17848
+ title: "Get Clipform Results",
17849
+ description: `View response counts, choice/action answer breakdowns, and recent open-text answers for a form. Contact, file, draw, camera, and payment answers are counts only - use the dashboard for per-response detail.`,
17850
+ inputSchema: {
17851
+ form_id: external_exports.string().describe("The form UUID (returned by clipform_create_form, not the short share_id from the URL)")
17852
+ },
17853
+ outputSchema: {
17854
+ total_responses: external_exports.number().describe("Total unique respondents to the form"),
17855
+ nodes: external_exports.array(
17856
+ external_exports.object({
17857
+ node_id: external_exports.string(),
17858
+ node_type: external_exports.string(),
17859
+ prompt: external_exports.string(),
17860
+ total_responses: external_exports.number(),
17861
+ total_views: external_exports.number(),
17862
+ options: external_exports.array(
17863
+ external_exports.object({
17864
+ label: external_exports.string(),
17865
+ count: external_exports.number(),
17866
+ percentage: external_exports.number()
17867
+ })
17868
+ ).optional().describe("Answer breakdown - choice and action-category (booking/file_download/shopping) nodes only"),
17869
+ recent_answers: external_exports.array(
17870
+ external_exports.object({
17871
+ text: external_exports.string(),
17872
+ submitted_at: external_exports.string()
17873
+ })
17874
+ ).optional().describe("Most recent open-text answers, capped at 20 - open questions only")
17875
+ })
17876
+ ).describe("Nodes in form order. Contact/file/draw/camera/payment nodes carry only total_responses/total_views - no per-response detail.")
17877
+ },
17878
+ annotations: {
17879
+ readOnlyHint: true,
17880
+ destructiveHint: false,
17881
+ idempotentHint: true,
17882
+ openWorldHint: false
17883
+ }
17884
+ },
17885
+ async ({ form_id }) => {
17886
+ const result = await callApi(`/forms/${form_id}/results`);
17887
+ if (!result.ok) {
17888
+ return errorResult(result.error);
17889
+ }
17890
+ const data = result.data;
17891
+ const nodes = (data.nodes || []).map(projectNode);
17892
+ const structured = {
17893
+ total_responses: data.total_responses,
17894
+ nodes
17895
+ };
17896
+ if (data.total_responses === 0) {
17897
+ return structuredResult(
17898
+ `This form has no responses yet (${nodes.length} node(s)).`,
17899
+ structured
17900
+ );
17901
+ }
17902
+ const lines = [`${data.total_responses} total response(s) across ${nodes.length} node(s).`];
17903
+ const highlights = nodes.filter((n) => "options" in n || "recent_answers" in n).slice(0, 3);
17904
+ for (const n of highlights) {
17905
+ if ("options" in n && n.options) {
17906
+ const top = n.options.slice(0, 3).map((o) => `${o.label} (${o.percentage}%)`).join(", ");
17907
+ lines.push(`- ${n.prompt}: ${top || "no answers yet"}`);
17908
+ } else if ("recent_answers" in n && n.recent_answers) {
17909
+ lines.push(`- ${n.prompt}: ${n.recent_answers.length} recent open-text answer(s)`);
17910
+ }
17911
+ }
17912
+ return structuredResult(lines.join("\n"), structured);
17913
+ }
17914
+ );
17915
+ }
17916
+
17917
+ // src/tools/whoami.ts
17918
+ function registerWhoamiTool(server) {
17919
+ server.registerTool(
17920
+ "clipform_whoami",
17921
+ {
17922
+ title: "Who Am I",
17923
+ description: `Show the current identity: auth mode (api key, session, or anonymous), active workspace, and plan limits.`,
17924
+ inputSchema: {},
17925
+ outputSchema: {
17926
+ auth_mode: external_exports.enum(["api_key", "session", "anonymous"]).describe("How this session is authenticated"),
17927
+ is_authenticated: external_exports.boolean().describe("True for api_key or session auth; false for anonymous"),
17928
+ workspace: external_exports.object({ id: external_exports.string(), name: external_exports.string() }).nullable().describe("Active workspace, or null when anonymous"),
17929
+ plan: external_exports.object({
17930
+ name: external_exports.string(),
17931
+ tier: external_exports.number(),
17932
+ node_limit: external_exports.number().nullable().describe("Max nodes per form, or null if unlimited"),
17933
+ form_limit: external_exports.number().nullable().describe("Max forms in the workspace, or null if unlimited")
17934
+ }).describe("Plan limits for the active workspace"),
17935
+ workspaces: external_exports.array(external_exports.object({ id: external_exports.string(), name: external_exports.string() })).optional().describe("All workspaces in the caller's company - only present for session (dashboard) auth")
17936
+ },
17937
+ annotations: {
17938
+ readOnlyHint: true,
17939
+ destructiveHint: false,
17940
+ idempotentHint: true,
17941
+ openWorldHint: false
17942
+ },
17943
+ _meta: { "anthropic/alwaysLoad": true }
17944
+ },
17945
+ async () => {
17946
+ const result = await callApi("/me", { method: "GET" });
17947
+ if (!result.ok) {
17948
+ return errorResult(result.error);
17949
+ }
17950
+ const me = result.data;
17951
+ const authMode = me.auth_mode ?? "anonymous";
17952
+ const isAuthenticated = me.auth_mode != null && me.auth_mode !== "anonymous";
17953
+ const workspace = me.workspace ? { id: me.workspace.id, name: me.workspace.name } : null;
17954
+ const plan = {
17955
+ name: me.plan?.name ?? "Free",
17956
+ tier: me.plan?.tier ?? 0,
17957
+ node_limit: me.plan?.node_limit ?? null,
17958
+ form_limit: me.plan?.form_limit ?? null
17959
+ };
17960
+ const structured = {
17961
+ auth_mode: authMode,
17962
+ is_authenticated: isAuthenticated,
17963
+ workspace,
17964
+ plan
17965
+ };
17966
+ if (Array.isArray(me.workspaces)) {
17967
+ structured.workspaces = me.workspaces.map((w) => ({ id: w.id, name: w.name }));
17968
+ }
17969
+ const workspaceLine = workspace ? `workspace '${workspace.name}'` : "no workspace (anonymous)";
17970
+ const summary = `Auth: ${authMode}. ${workspaceLine}. Plan: ${plan.name}.`;
17971
+ return structuredResult(summary, structured);
17972
+ }
17973
+ );
17974
+ }
17975
+
17663
17976
  // src/tools/add-node.ts
17664
17977
  function registerAddNodeTool(server) {
17665
17978
  server.registerTool(
17666
17979
  "clipform_add_node",
17667
17980
  {
17668
17981
  title: "Add Node",
17669
- description: `Add a new node to an existing form. Inserted before the end screen by default; after_node_id controls insertion position. Node types and config schemas match clipform_create_form.`,
17982
+ description: `Add a new node to an existing form. Inserted before the end screen by default; after_node_id controls insertion position. Inserting into a linear flow automatically re-links the chain (the node before the insertion point points to the new node, which points to what followed) - you do not need clipform_set_logic for a simple insert. Branching still needs an explicit logic write. Node types and config schemas match clipform_create_form.`,
17670
17983
  inputSchema: {
17671
17984
  form_id: external_exports.string().uuid().describe("The form UUID (returned by clipform_create_form, not the short share_id from the URL)"),
17672
17985
  node: NodeSchema.describe("The node to add"),
@@ -17697,8 +18010,10 @@ function registerAddNodeTool(server) {
17697
18010
  return errorResult(result.error);
17698
18011
  }
17699
18012
  const nodeId = result.data.node_id;
18013
+ const warnings = result.data.warnings ?? [];
18014
+ const relinkNote = warnings.length > 0 ? ` ${warnings.join(" ")}` : " The logic chain was automatically re-linked - no clipform_set_logic call is needed for this insert.";
17700
18015
  return structuredResult(
17701
- `Node added. ID: ${nodeId} | Type: ${node.type}`,
18016
+ `Node added. ID: ${nodeId} | Type: ${node.type}.${relinkNote}`,
17702
18017
  { node_id: nodeId, type: node.type }
17703
18018
  );
17704
18019
  }
@@ -17741,7 +18056,10 @@ function registerUpdateNodeTool(server) {
17741
18056
  },
17742
18057
  annotations: {
17743
18058
  readOnlyHint: false,
17744
- destructiveHint: false,
18059
+ // Overwrites the node's prompt/config with whatever is sent - a
18060
+ // field explicitly set to null clears it, so this can destroy data,
18061
+ // not just add to it (#2976 research pass 2).
18062
+ destructiveHint: true,
17745
18063
  idempotentHint: true,
17746
18064
  openWorldHint: false
17747
18065
  }
@@ -17801,7 +18119,9 @@ function registerDeleteNodeTool(server) {
17801
18119
  annotations: {
17802
18120
  readOnlyHint: false,
17803
18121
  destructiveHint: true,
17804
- idempotentHint: false,
18122
+ // Deleting an already-deleted node has no further effect -
18123
+ // textbook idempotent delete (#2976).
18124
+ idempotentHint: true,
17805
18125
  openWorldHint: false
17806
18126
  }
17807
18127
  },
@@ -18034,8 +18354,15 @@ function registerAttachNodeMediaTool(server) {
18034
18354
  },
18035
18355
  annotations: {
18036
18356
  readOnlyHint: false,
18037
- destructiveHint: false,
18038
- idempotentHint: false,
18357
+ // Attaching replaces the node's existing media placement (the API
18358
+ // overwrites it, not merges) - the previous placement is gone, even
18359
+ // though the underlying library asset it pointed to is never deleted
18360
+ // (#2976 research pass 2).
18361
+ destructiveHint: true,
18362
+ // Re-attaching the same media_asset_id to the same node converges to
18363
+ // the same placement state - no new resource is created each call
18364
+ // (#2976).
18365
+ idempotentHint: true,
18039
18366
  openWorldHint: false
18040
18367
  }
18041
18368
  },
@@ -18157,7 +18484,9 @@ function registerDeleteNodeMediaTool(server) {
18157
18484
  annotations: {
18158
18485
  readOnlyHint: false,
18159
18486
  destructiveHint: true,
18160
- idempotentHint: false,
18487
+ // Deleting already-removed media has no further effect - textbook
18488
+ // idempotent delete (#2976).
18489
+ idempotentHint: true,
18161
18490
  openWorldHint: false
18162
18491
  }
18163
18492
  },
@@ -18184,23 +18513,20 @@ function registerDeleteNodeMediaTool(server) {
18184
18513
 
18185
18514
  // src/tools/set-node-logic.ts
18186
18515
  var LogicRuleSchema = external_exports.object({
18187
- option_content: external_exports.string().optional().describe(
18188
- "The option text to match. Omit for a default rule (applies to all unmatched options)."
18189
- ),
18190
- target_node_id: external_exports.string().describe("The node ID to jump to when this rule matches")
18516
+ target_node_id: external_exports.string().describe("The node ID this node routes to")
18191
18517
  });
18192
18518
  var NodeLogicSchema = external_exports.object({
18193
- node_id: external_exports.string().describe("The node ID to set logic on"),
18194
- rules: external_exports.array(LogicRuleSchema).min(1).describe("Branching rules. Each maps an option or default to a target node.")
18519
+ node_id: external_exports.string().describe("The node ID to set routing on"),
18520
+ rules: external_exports.array(LogicRuleSchema).min(1).max(1).describe("Exactly one rule: the single node this node routes to")
18195
18521
  });
18196
18522
  function registerSetNodeLogicTool(server) {
18197
18523
  server.registerTool(
18198
18524
  "clipform_set_logic",
18199
18525
  {
18200
18526
  title: "Set Node Logic",
18201
- description: `Set routing logic on one or more nodes. Pass multiple nodes in one call instead of separate tool calls. Linear routing (A to B to C) is supported. NOTE: Conditional branching (different paths based on which option is picked) is a planned feature - do not create conditional branching paths.
18527
+ description: `Set the next node for one or more nodes, wiring a linear route (A to B to C). Pass multiple nodes in one call instead of separate tool calls. Each node routes to exactly one target node; conditional branching by which option was picked is not yet supported.
18202
18528
 
18203
- Each rule maps an option (by its text content) to a target node. Rules without option_content are "default" rules applied to all unmatched options.
18529
+ A call replaces that node's routing entirely - it does not merge with any prior target.
18204
18530
 
18205
18531
  Example:
18206
18532
  nodes: [
@@ -18211,15 +18537,28 @@ Example:
18211
18537
  form_id: external_exports.string().uuid().describe("The form UUID (returned by clipform_create_form, not the short share_id from the URL)"),
18212
18538
  nodes: external_exports.array(NodeLogicSchema).min(1).max(20).describe("One or more nodes to set logic on")
18213
18539
  },
18540
+ outputSchema: {
18541
+ results: external_exports.array(
18542
+ external_exports.object({
18543
+ node_id: external_exports.string(),
18544
+ ok: external_exports.boolean(),
18545
+ rules_count: external_exports.number().optional(),
18546
+ error: external_exports.string().optional()
18547
+ })
18548
+ ).describe("One result per node, in order")
18549
+ },
18214
18550
  annotations: {
18215
18551
  readOnlyHint: false,
18216
- destructiveHint: false,
18552
+ // A call wholesale REPLACES a node's rule set - a prior set of rules
18553
+ // is discarded, not merged (#2976).
18554
+ destructiveHint: true,
18217
18555
  idempotentHint: true,
18218
18556
  openWorldHint: false
18219
18557
  }
18220
18558
  },
18221
18559
  async ({ form_id, nodes }) => {
18222
18560
  const allLines = [];
18561
+ const results = [];
18223
18562
  for (const { node_id, rules } of nodes) {
18224
18563
  const result = await callApi(
18225
18564
  `/forms/${form_id}/nodes/${node_id}/logic/rules`,
@@ -18229,12 +18568,15 @@ Example:
18229
18568
  }
18230
18569
  );
18231
18570
  if (!result.ok) {
18232
- allLines.push(`Node ${node_id}: FAILED \u2014 ${result.error}`);
18571
+ allLines.push(`Node ${node_id}: FAILED - ${result.error}`);
18572
+ results.push({ node_id, ok: false, error: result.error });
18233
18573
  continue;
18234
18574
  }
18235
- allLines.push(`Node ${node_id}: logic set (${result.data.rules_count} rules)`);
18575
+ const rulesCount = result.data.rules_count;
18576
+ allLines.push(`Node ${node_id}: logic set (${rulesCount} rules)`);
18577
+ results.push({ node_id, ok: true, rules_count: rulesCount });
18236
18578
  }
18237
- return textResult(allLines.join("\n"));
18579
+ return structuredResult(allLines.join("\n"), { results });
18238
18580
  }
18239
18581
  );
18240
18582
  }
@@ -18245,7 +18587,7 @@ function registerLogGenerationTool(server) {
18245
18587
  "clipform_log_generation",
18246
18588
  {
18247
18589
  title: "Log Form Generation Audit",
18248
- description: `Save an audit trail for a generated form. Records where content came from - the sources and image attributions.`,
18590
+ description: `Internal audit step run by a form-generation workflow. Records a completed generation's content provenance (sources, image attributions) for the flow that just built the form. Not a user action - only call it as the final step of a generation workflow you are running, never in response to a plain create or edit request.`,
18249
18591
  inputSchema: {
18250
18592
  form_id: external_exports.string().uuid().describe("The form ID (UUID format, not the share ID)"),
18251
18593
  summary: external_exports.string().describe("Short description of what was generated"),
@@ -18270,7 +18612,8 @@ function registerLogGenerationTool(server) {
18270
18612
  });
18271
18613
  if (!result.ok) return errorResult(result.error);
18272
18614
  return textResult(`Audit log saved for form ${form_id}
18273
- Summary: ${summary}`);
18615
+ Summary: ${summary}
18616
+ This is an internal generation-audit step, not a user-requested action.`);
18274
18617
  }
18275
18618
  );
18276
18619
  }
@@ -18304,7 +18647,9 @@ Returns: article title, source, author, date, URL, description, and image URL pe
18304
18647
  })
18305
18648
  ).describe("Matching articles (empty if none)")
18306
18649
  },
18307
- annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
18650
+ // Fetches from NewsAPI / The Guardian, external systems outside our
18651
+ // product - openWorldHint: true (#2976).
18652
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }
18308
18653
  },
18309
18654
  async ({ query, count }) => {
18310
18655
  const result = await callApi("/internal/search-news", {
@@ -18338,56 +18683,6 @@ Returns: article title, source, author, date, URL, description, and image URL pe
18338
18683
  );
18339
18684
  }
18340
18685
 
18341
- // src/tools/youtube-transcript.ts
18342
- function registerYouTubeTranscriptTool(server) {
18343
- server.registerTool(
18344
- "clipform_youtube_transcript",
18345
- {
18346
- title: "Get YouTube Transcript",
18347
- description: `Extract the transcript, title, and channel info from a YouTube video. Returns the full transcript text plus metadata (title, channel, duration).
18348
-
18349
- Supports any public YouTube video with captions enabled. Does NOT work for private/age-restricted videos, videos with captions disabled, or non-YouTube URLs.`,
18350
- inputSchema: {
18351
- url: external_exports.string().url().describe(
18352
- "YouTube video URL (any format: watch?v=, youtu.be/, shorts/)"
18353
- ),
18354
- lang: external_exports.string().optional().default("en").describe(
18355
- "Transcript language code (default: 'en'). Try 'en' first, omit for auto-detect if English unavailable."
18356
- ),
18357
- max_chars: external_exports.number().optional().default(15e3).describe(
18358
- "Maximum transcript characters to return (default: 15000). Increase for longer videos where full context matters."
18359
- )
18360
- },
18361
- annotations: {
18362
- readOnlyHint: true,
18363
- destructiveHint: false,
18364
- idempotentHint: true,
18365
- openWorldHint: false
18366
- }
18367
- },
18368
- async ({ url, lang, max_chars }) => {
18369
- const result = await callApi("/internal/youtube-transcript", {
18370
- body: { url, lang, max_chars }
18371
- });
18372
- if (!result.ok) return errorResult(result.error);
18373
- const data = result.data;
18374
- const meta = data.metadata;
18375
- const lines = [
18376
- `## Video: ${meta.title}`,
18377
- `Channel: ${meta.author}`,
18378
- `Duration: ~${Math.round(data.durationSeconds / 60)} minutes`,
18379
- `Language: ${data.language}`,
18380
- `Transcript length: ${data.transcript.length} characters`,
18381
- ``,
18382
- `## Transcript`,
18383
- ``,
18384
- data.transcript
18385
- ];
18386
- return textResult(lines.join("\n"));
18387
- }
18388
- );
18389
- }
18390
-
18391
18686
  // src/tools/generate-tts.ts
18392
18687
  var CaptionSchema2 = external_exports.object({
18393
18688
  start: external_exports.number(),
@@ -18518,7 +18813,9 @@ Example: { queries: [{ query: "saturn rings" }, { query: "mars surface", count:
18518
18813
  })
18519
18814
  ).describe("Results grouped by query (items empty if a query found nothing or failed)")
18520
18815
  },
18521
- annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
18816
+ // Fetches from external stock media providers (Pexels/etc) -
18817
+ // openWorldHint: true (#2976).
18818
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }
18522
18819
  },
18523
18820
  async ({ queries }) => {
18524
18821
  const allLines = [];
@@ -18645,6 +18942,7 @@ var RENDER_TIMING = {
18645
18942
  etaPhrase: "a couple of minutes"
18646
18943
  };
18647
18944
  function createJob(tool) {
18945
+ pruneJobs();
18648
18946
  const job = {
18649
18947
  id: v4_default(),
18650
18948
  status: "rendering",
@@ -18887,7 +19185,11 @@ ${RENDER_ATTACH_RULE} In batch mode, set node_id per item (see items) and form_i
18887
19185
  })
18888
19186
  ).describe("One entry per render (single or batch)")
18889
19187
  },
18890
- annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }
19188
+ // node_id auto-attach replaces the node's existing media placement and
19189
+ // republishes an already-live form - an indirect but real overwrite +
19190
+ // publish (#2976 research pass 2), even though this tool works with no
19191
+ // node_id too (destructive/openWorld apply "even in only select modes").
19192
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true }
18891
19193
  },
18892
19194
  async ({ compositionId, outputFormat, inputProps, wait, items, node_id, form_id, captions }) => {
18893
19195
  if (items?.length && compositionId) {
@@ -19015,7 +19317,9 @@ function registerSearchMusicTool(server) {
19015
19317
  })
19016
19318
  ).describe("Matching tracks (empty if none); pass a url as the audio track to generate_video")
19017
19319
  },
19018
- annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
19320
+ // Fetches from an external royalty-free music provider - openWorldHint:
19321
+ // true (#2976).
19322
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }
19019
19323
  },
19020
19324
  async ({ query, count, instrumentalOnly, minDuration, maxDuration, tags }) => {
19021
19325
  const result = await callApi("/internal/search-music", {
@@ -19226,7 +19530,11 @@ ${RENDER_ATTACH_RULE}`,
19226
19530
  republished: external_exports.boolean().optional().describe("True when the form was live and was republished to include this media."),
19227
19531
  attach_error: external_exports.string().optional().describe("Present when node_id was provided but auto-attach failed - the render itself still succeeded.")
19228
19532
  },
19229
- annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }
19533
+ // node_id auto-attach replaces the node's existing media placement and
19534
+ // republishes an already-live form - an indirect but real overwrite +
19535
+ // publish (#2976 research pass 2), even though this tool works with no
19536
+ // node_id too (destructive/openWorld apply "even in only select modes").
19537
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true }
19230
19538
  },
19231
19539
  async ({ template, controls, outputFormat, wait, node_id, form_id, captions }) => {
19232
19540
  if (node_id && !form_id) {
@@ -19434,7 +19742,11 @@ ${RENDER_ATTACH_RULE}`,
19434
19742
  republished: external_exports.boolean().optional().describe("True when the form was live and was republished to include this media."),
19435
19743
  attach_error: external_exports.string().optional().describe("Present when node_id was provided but auto-attach failed - the render itself still succeeded.")
19436
19744
  },
19437
- annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }
19745
+ // node_id auto-attach replaces the node's existing media placement and
19746
+ // republishes an already-live form - an indirect but real overwrite +
19747
+ // publish (#2976 research pass 2), even though this tool works with no
19748
+ // node_id too (destructive/openWorld apply "even in only select modes").
19749
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true }
19438
19750
  },
19439
19751
  async ({ items, audio_url, background_audio_url, background_audio_volume, duration_seconds, random_effects, transition, style_preset, texture, duotone, background_color, wait, node_id, form_id, captions }) => {
19440
19752
  if (node_id && !form_id) {
@@ -19609,7 +19921,6 @@ Pass job_ids to check a whole batch in ONE call - one line of status per job. Pa
19609
19921
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
19610
19922
  },
19611
19923
  async ({ job_id, job_ids }) => {
19612
- pruneJobs();
19613
19924
  const ids = job_ids?.length ? job_ids : job_id ? [job_id] : [];
19614
19925
  if (!ids.length) {
19615
19926
  return errorResult("Pass job_id (single) or job_ids (batch).");
@@ -19699,7 +20010,16 @@ mainlandOnly excludes small islands and overseas territories (e.g. Corsica for F
19699
20010
  mainlandOnly: external_exports.boolean().default(true).describe("Keep only the largest landmass, excluding small islands and overseas territories"),
19700
20011
  maxPoints: external_exports.number().default(500).describe("Maximum points per polygon ring for simplification (default 500, lower = smaller payload)")
19701
20012
  },
19702
- annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
20013
+ outputSchema: {
20014
+ name: external_exports.string().describe("Resolved place name"),
20015
+ geometry_type: external_exports.enum(["Polygon", "MultiPolygon"]),
20016
+ size_kb: external_exports.number().describe("Approximate size of the returned GeoJSON in KB"),
20017
+ mainland_only: external_exports.boolean(),
20018
+ geojson: external_exports.string().describe("The Feature GeoJSON as a JSON string - use as the boundary prop in the Map composition")
20019
+ },
20020
+ // Fetches from Nominatim (OpenStreetMap), an external system -
20021
+ // openWorldHint: true (#2976).
20022
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }
19703
20023
  },
19704
20024
  async ({ query, type, mainlandOnly, maxPoints }) => {
19705
20025
  const params = new URLSearchParams({
@@ -19716,11 +20036,16 @@ mainlandOnly excludes small islands and overseas territories (e.g. Corsica for F
19716
20036
  params.set("q", query);
19717
20037
  }
19718
20038
  const url = `${NOMINATIM_BASE}?${params}`;
19719
- const response = await fetch(url, {
19720
- headers: { "User-Agent": USER_AGENT }
19721
- });
20039
+ let response;
20040
+ try {
20041
+ response = await fetch(url, {
20042
+ headers: { "User-Agent": USER_AGENT }
20043
+ });
20044
+ } catch {
20045
+ return errorResult(`Boundary lookup failed for "${query}". Try again, or try a different name or type.`);
20046
+ }
19722
20047
  if (!response.ok) {
19723
- return errorResult(`Nominatim API error: ${response.status} ${response.statusText}`);
20048
+ return errorResult(`Boundary lookup failed for "${query}". Try again, or try a different name or type.`);
19724
20049
  }
19725
20050
  const data = await response.json();
19726
20051
  if (!data.features?.length) {
@@ -19757,8 +20082,8 @@ mainlandOnly excludes small islands and overseas territories (e.g. Corsica for F
19757
20082
  geometry
19758
20083
  };
19759
20084
  const geojsonStr = JSON.stringify(result);
19760
- const sizeKb = (geojsonStr.length / 1024).toFixed(1);
19761
- return textResult(
20085
+ const sizeKb = Number((geojsonStr.length / 1024).toFixed(1));
20086
+ return structuredResult(
19762
20087
  [
19763
20088
  `Boundary fetched for: ${feature.properties.display_name}`,
19764
20089
  `Geometry: ${geometry.type}`,
@@ -19770,7 +20095,14 @@ mainlandOnly excludes small islands and overseas territories (e.g. Corsica for F
19770
20095
  "```json",
19771
20096
  geojsonStr,
19772
20097
  "```"
19773
- ].join("\n")
20098
+ ].join("\n"),
20099
+ {
20100
+ name: feature.properties.display_name,
20101
+ geometry_type: geometry.type,
20102
+ size_kb: sizeKb,
20103
+ mainland_only: mainlandOnly,
20104
+ geojson: geojsonStr
20105
+ }
19774
20106
  );
19775
20107
  }
19776
20108
  );
@@ -19915,6 +20247,7 @@ function createServer(options) {
19915
20247
  "Before building, derive a register from the topic and audience (e.g. hype, playful, warm, considered, elegant, sombre); if the user states a mood explicitly, that wins. State the register once, then apply it to every rendering choice - narration tone, motion preset, music, pacing. Never mix registers in one form.",
19916
20248
  "At the start of a build, call clipform_get_workflow to load the recipe for the form type, and clipform_get_guide for craft detail.",
19917
20249
  "Every *_id argument is a UUID returned by the tools - not the short share_id from a form URL.",
20250
+ "For an account-scoped request, call clipform_whoami first to confirm the active workspace and plan.",
19918
20251
  "Deleting a form, node, or its media is permanent; those tools ask the user to confirm before running."
19919
20252
  ].join(" ")
19920
20253
  }
@@ -19925,6 +20258,12 @@ function createServer(options) {
19925
20258
  if (options?.planTier !== void 0 && requiredTier > options.planTier) {
19926
20259
  return void 0;
19927
20260
  }
20261
+ if (options?.toolsets !== void 0) {
20262
+ const toolset = MCP_TOOL_TOOLSETS[name];
20263
+ if (!toolset || !options.toolsets.includes(toolset)) {
20264
+ return void 0;
20265
+ }
20266
+ }
19928
20267
  return _registerTool(
19929
20268
  name,
19930
20269
  config2,
@@ -19941,9 +20280,11 @@ function createServer(options) {
19941
20280
  };
19942
20281
  registerCreateFormTool(server);
19943
20282
  registerListFormsTool(server);
20283
+ registerWhoamiTool(server);
19944
20284
  registerGetFormTool(server);
19945
20285
  registerUpdateFormTool(server);
19946
20286
  registerDeleteFormTool(server);
20287
+ registerGetResultsTool(server);
19947
20288
  registerAddNodeTool(server);
19948
20289
  registerUpdateNodeTool(server);
19949
20290
  registerDeleteNodeTool(server);
@@ -19955,7 +20296,6 @@ function createServer(options) {
19955
20296
  registerSetNodeLogicTool(server);
19956
20297
  registerLogGenerationTool(server);
19957
20298
  registerSearchNewsTool(server);
19958
- registerYouTubeTranscriptTool(server);
19959
20299
  registerGenerateTtsTool(server);
19960
20300
  registerGenerateVideoTool(server);
19961
20301
  registerSearchMediaTool(server);
@@ -19979,4 +20319,4 @@ export {
19979
20319
  RENDER_TIMING,
19980
20320
  createServer
19981
20321
  };
19982
- //# sourceMappingURL=chunk-NMIUCFJT.js.map
20322
+ //# sourceMappingURL=chunk-3OX54HSJ.js.map