@clipform/mcp-server 2.4.0 → 2.5.1

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,19 @@ 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
  }
17296
+ const firstUrlLabel = isPreview ? "PREVIEW" : "FORM";
17297
+ const secondUrlLabel = isPreview ? "CLAIM" : "DASHBOARD";
17279
17298
  lines.push(
17280
17299
  ``,
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.`,
17300
+ isPreview ? `This form is a preview - claim and publish to start collecting responses.${formUrl ? " Open the CLAIM URL to sign into the account this form already belongs to, then republish" : " Sign in to the account this form already belongs to and 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.`,
17301
+ formUrl ? `The URLs above are exact values; constructed or guessed URLs return 404. Use the ${firstUrlLabel} URL and ${secondUrlLabel} URL as shown.` : `The ${firstUrlLabel} URL above is an exact value; constructed or guessed URLs return 404.`,
17283
17302
  `Pass form_id on follow-up tools (get_form, add_node, attach_node_media, etc.).`
17284
17303
  );
17285
17304
  if (planContext) {
@@ -17322,7 +17341,7 @@ function registerListFormsTool(server) {
17322
17341
  "clipform_list_forms",
17323
17342
  {
17324
17343
  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.`,
17344
+ 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
17345
  inputSchema: {
17327
17346
  limit: external_exports.number().int().min(1).max(100).optional().describe("Number of forms to return (default 25, max 100)"),
17328
17347
  cursor: external_exports.string().optional().describe(
@@ -17332,7 +17351,9 @@ function registerListFormsTool(server) {
17332
17351
  "Filter by tag name(s), comma-separated. AND logic: only forms with ALL tags are returned."
17333
17352
  ),
17334
17353
  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)"),
17354
+ search: external_exports.string().optional().describe(
17355
+ "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."
17356
+ ),
17336
17357
  sort: external_exports.enum(["created_at", "updated_at"]).optional().describe("Sort field (default: created_at)"),
17337
17358
  order: external_exports.enum(["asc", "desc"]).optional().describe("Sort order (default: desc, newest first)")
17338
17359
  },
@@ -17344,8 +17365,6 @@ function registerListFormsTool(server) {
17344
17365
  share_id: external_exports.string(),
17345
17366
  is_live: external_exports.boolean(),
17346
17367
  has_unpublished_changes: external_exports.boolean(),
17347
- created_at: external_exports.string(),
17348
- updated_at: external_exports.string(),
17349
17368
  tags: external_exports.array(external_exports.object({ id: external_exports.string(), name: external_exports.string(), color: external_exports.string().nullable() }))
17350
17369
  })
17351
17370
  ).describe("Matching forms (empty if none)"),
@@ -17379,8 +17398,6 @@ function registerListFormsTool(server) {
17379
17398
  share_id: f.share_id,
17380
17399
  is_live: f.is_live,
17381
17400
  has_unpublished_changes: f.has_unpublished_changes,
17382
- created_at: f.created_at,
17383
- updated_at: f.updated_at,
17384
17401
  tags: f.tags.map((t) => ({ id: t.id, name: t.name, color: t.color }))
17385
17402
  })),
17386
17403
  next_cursor: data.next_cursor
@@ -17406,30 +17423,78 @@ function registerListFormsTool(server) {
17406
17423
  }
17407
17424
 
17408
17425
  // src/lib/format-form.ts
17426
+ function allowedConfigKeys(nodeType) {
17427
+ const schema = NODE_TYPES[nodeType]?.config_schema;
17428
+ const properties = schema?.properties;
17429
+ if (properties) return new Set(Object.keys(properties));
17430
+ const oneOf = schema?.oneOf;
17431
+ if (Array.isArray(oneOf)) {
17432
+ const keys = /* @__PURE__ */ new Set();
17433
+ for (const branch of oneOf) {
17434
+ for (const k of Object.keys(branch?.properties ?? {})) keys.add(k);
17435
+ }
17436
+ return keys;
17437
+ }
17438
+ return /* @__PURE__ */ new Set();
17439
+ }
17440
+ function projectConfig(nodeType, config2) {
17441
+ if (!config2) return void 0;
17442
+ const allowed = allowedConfigKeys(nodeType);
17443
+ const projected = {};
17444
+ for (const [key, value] of Object.entries(config2)) {
17445
+ if (allowed.has(key)) projected[key] = value;
17446
+ }
17447
+ return projected;
17448
+ }
17409
17449
  function getFormNodes(data) {
17410
17450
  return data.node ?? data.nodes ?? [];
17411
17451
  }
17452
+ function buildLogicByNode(nodes) {
17453
+ const logicByNode = /* @__PURE__ */ new Map();
17454
+ for (const node of nodes) {
17455
+ if (node.node_logic && node.node_logic.length > 0) {
17456
+ logicByNode.set(
17457
+ node.id,
17458
+ node.node_logic.flatMap((entry) => entry.actions ?? [])
17459
+ );
17460
+ }
17461
+ }
17462
+ return logicByNode;
17463
+ }
17464
+ function orderNodesWithLogic(data) {
17465
+ const rawNodes = getFormNodes(data);
17466
+ const logicByNode = buildLogicByNode(rawNodes);
17467
+ return { nodes: bfsOrder(rawNodes, logicByNode), logicByNode };
17468
+ }
17469
+ function projectNext(node, logicByNode) {
17470
+ const target = defaultTarget(logicByNode.get(node.id));
17471
+ return target ? { target_id: target.value, target_type: target.type } : void 0;
17472
+ }
17412
17473
  function projectFormState(data) {
17474
+ const { nodes, logicByNode } = orderNodesWithLogic(data);
17413
17475
  return {
17414
17476
  form_id: String(data.form_id ?? ""),
17415
17477
  title: String(data.title ?? ""),
17416
17478
  is_live: Boolean(data.is_live),
17417
17479
  has_unpublished_changes: Boolean(data.has_unpublished_changes),
17418
- nodes: getFormNodes(data).map((q) => {
17480
+ nodes: nodes.map((q) => {
17419
17481
  const options = q.node_options ?? q.options;
17482
+ const config2 = projectConfig(q.type, q.config);
17483
+ const next = projectNext(q, logicByNode);
17420
17484
  return {
17421
17485
  id: q.id,
17422
17486
  type: q.type,
17423
17487
  prompt: q.prompt ?? "",
17424
17488
  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 })) } : {}
17489
+ ...config2 && Object.keys(config2).length > 0 ? { config: config2 } : {},
17490
+ ...options && options.length > 0 ? { options: options.map((o) => ({ id: o.id, content: o.content })) } : {},
17491
+ ...next ? { next } : {}
17427
17492
  };
17428
17493
  })
17429
17494
  };
17430
17495
  }
17431
17496
  function formatFormState(data) {
17432
- const nodes = getFormNodes(data);
17497
+ const { nodes, logicByNode } = orderNodesWithLogic(data);
17433
17498
  const lines = [
17434
17499
  `Form: ${data.title}`,
17435
17500
  `Form ID: ${data.form_id}`,
@@ -17446,8 +17511,9 @@ function formatFormState(data) {
17446
17511
  lines.push(` ${i + 1}. [${q.type}] ${q.prompt || "(no prompt)"}`);
17447
17512
  lines.push(` Node ID: ${q.id}`);
17448
17513
  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)}`);
17514
+ const config2 = projectConfig(q.type, q.config);
17515
+ if (config2 && Object.keys(config2).length > 0) {
17516
+ lines.push(` Config: ${JSON.stringify(config2)}`);
17451
17517
  }
17452
17518
  const options = q.node_options ?? q.options;
17453
17519
  if (options && options.length > 0) {
@@ -17455,6 +17521,10 @@ function formatFormState(data) {
17455
17521
  ` Options: ${options.map((o) => o.content).join(", ")}`
17456
17522
  );
17457
17523
  }
17524
+ const next = projectNext(q, logicByNode);
17525
+ if (next) {
17526
+ lines.push(` -> next: ${next.target_id} (${next.target_type})`);
17527
+ }
17458
17528
  }
17459
17529
  return lines.join("\n");
17460
17530
  }
@@ -17465,7 +17535,7 @@ function registerGetFormTool(server) {
17465
17535
  "clipform_get_form",
17466
17536
  {
17467
17537
  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.`,
17538
+ 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
17539
  inputSchema: {
17470
17540
  form_id: external_exports.string().uuid().describe("The form UUID (returned by clipform_create_form, not the short share_id from the URL)")
17471
17541
  },
@@ -17481,7 +17551,11 @@ function registerGetFormTool(server) {
17481
17551
  prompt: external_exports.string(),
17482
17552
  required: external_exports.boolean(),
17483
17553
  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()
17554
+ options: external_exports.array(external_exports.object({ id: external_exports.string(), content: external_exports.string() })).optional(),
17555
+ next: external_exports.object({
17556
+ target_id: external_exports.string().describe("The node/screen ID this node routes to"),
17557
+ target_type: external_exports.enum(["node", "end_screen", "redirect", "link_list"]).describe("What kind of target target_id is")
17558
+ }).optional().describe("This node's outgoing edge, set via clipform_set_logic. Omitted for a chain tail or terminal node.")
17485
17559
  })
17486
17560
  ).describe("Nodes in sequential order - IDs for update_node / attach_node_media")
17487
17561
  },
@@ -17493,7 +17567,7 @@ function registerGetFormTool(server) {
17493
17567
  }
17494
17568
  },
17495
17569
  async ({ form_id }) => {
17496
- const result = await callApi(`/forms/${form_id}`);
17570
+ const result = await callApi(`/forms/${form_id}?include=logic`);
17497
17571
  if (!result.ok) {
17498
17572
  return errorResult(result.error);
17499
17573
  }
@@ -17526,11 +17600,20 @@ function registerUpdateFormTool(server) {
17526
17600
  brand_name: external_exports.string().nullable().optional().describe("AI-PROTECTED: Custom brand name shown in footer. Only set when user explicitly provides."),
17527
17601
  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
17602
  },
17603
+ outputSchema: {
17604
+ form_id: external_exports.string().describe("Form UUID"),
17605
+ updated_fields: external_exports.array(external_exports.string()).describe("Names of the fields that were changed by this call")
17606
+ },
17529
17607
  annotations: {
17530
17608
  readOnlyHint: false,
17531
- destructiveHint: false,
17609
+ // is_live: true/false publishes, republishes, or unpublishes the
17610
+ // live form, and other fields (title, theme, logo) overwrite what
17611
+ // respondents already see (#2976 research pass 2).
17612
+ destructiveHint: true,
17532
17613
  idempotentHint: true,
17533
- openWorldHint: false
17614
+ // is_live: true makes the form live at a public URL - openWorldHint
17615
+ // per OpenAI's "publishing content" reading (#2976 research pass 2).
17616
+ openWorldHint: true
17534
17617
  }
17535
17618
  },
17536
17619
  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 +17650,64 @@ function registerUpdateFormTool(server) {
17567
17650
  }
17568
17651
  }
17569
17652
  const updates = [];
17570
- if (title !== void 0) updates.push(`Title \u2192 "${title}"`);
17571
- if (is_live !== void 0)
17653
+ const updatedFields = [];
17654
+ if (title !== void 0) {
17655
+ updates.push(`Title \u2192 "${title}"`);
17656
+ updatedFields.push("title");
17657
+ }
17658
+ if (is_live !== void 0) {
17572
17659
  updates.push(`Live \u2192 ${is_live}`);
17573
- if (show_step_counter !== void 0)
17660
+ updatedFields.push("is_live");
17661
+ }
17662
+ if (show_step_counter !== void 0) {
17574
17663
  updates.push(`Step counter \u2192 ${show_step_counter}`);
17575
- if (disable_back_navigation !== void 0)
17664
+ updatedFields.push("show_step_counter");
17665
+ }
17666
+ if (disable_back_navigation !== void 0) {
17576
17667
  updates.push(`Back navigation \u2192 ${disable_back_navigation ? "disabled" : "enabled"}`);
17577
- if (total_steps !== void 0)
17668
+ updatedFields.push("disable_back_navigation");
17669
+ }
17670
+ if (total_steps !== void 0) {
17578
17671
  updates.push(`Total steps \u2192 ${total_steps === null ? "auto" : total_steps}`);
17579
- if (primary_color !== void 0)
17672
+ updatedFields.push("total_steps");
17673
+ }
17674
+ if (primary_color !== void 0) {
17580
17675
  updates.push(`Primary color \u2192 ${primary_color}`);
17581
- if (background_color !== void 0)
17676
+ updatedFields.push("primary_color");
17677
+ }
17678
+ if (background_color !== void 0) {
17582
17679
  updates.push(`Background color \u2192 ${background_color}`);
17583
- if (font_family !== void 0)
17680
+ updatedFields.push("background_color");
17681
+ }
17682
+ if (font_family !== void 0) {
17584
17683
  updates.push(`Font \u2192 ${font_family}`);
17585
- if (description !== void 0)
17684
+ updatedFields.push("font_family");
17685
+ }
17686
+ if (description !== void 0) {
17586
17687
  updates.push(`Description \u2192 ${description === null ? "cleared" : `"${description}"`}`);
17587
- if (author !== void 0)
17688
+ updatedFields.push("description");
17689
+ }
17690
+ if (author !== void 0) {
17588
17691
  updates.push(`Author \u2192 ${author === null ? "cleared" : `"${author}"`}`);
17589
- if (logo_url !== void 0)
17692
+ updatedFields.push("author");
17693
+ }
17694
+ if (logo_url !== void 0) {
17590
17695
  updates.push(`Logo URL \u2192 ${logo_url === null ? "cleared" : logo_url}`);
17591
- if (show_branding !== void 0)
17696
+ updatedFields.push("logo_url");
17697
+ }
17698
+ if (show_branding !== void 0) {
17592
17699
  updates.push(`Show branding \u2192 ${show_branding}`);
17593
- if (brand_name !== void 0)
17700
+ updatedFields.push("show_branding");
17701
+ }
17702
+ if (brand_name !== void 0) {
17594
17703
  updates.push(`Brand name \u2192 ${brand_name === null ? "cleared" : `"${brand_name}"`}`);
17595
- if (tags)
17704
+ updatedFields.push("brand_name");
17705
+ }
17706
+ if (tags) {
17596
17707
  updates.push(`Tags \u2192 [${tags.join(", ")}]`);
17597
- return textResult(`Form updated: ${updates.join(", ")}`);
17708
+ updatedFields.push("tags");
17709
+ }
17710
+ return structuredResult(`Form updated: ${updates.join(", ")}`, { form_id, updated_fields: updatedFields });
17598
17711
  }
17599
17712
  );
17600
17713
  }
@@ -17636,8 +17749,13 @@ function registerDeleteFormTool(server) {
17636
17749
  annotations: {
17637
17750
  readOnlyHint: false,
17638
17751
  destructiveHint: true,
17639
- idempotentHint: false,
17640
- openWorldHint: false
17752
+ // Deleting an already-trashed form has no further effect -
17753
+ // calling this twice with the same form_id is a no-op the second
17754
+ // time (textbook idempotent delete, #2976).
17755
+ idempotentHint: true,
17756
+ // Removes a form's public page - openWorldHint per OpenAI's
17757
+ // "publishing content" reading (#2976 research pass 2).
17758
+ openWorldHint: true
17641
17759
  }
17642
17760
  },
17643
17761
  async ({ form_id, confirm }) => {
@@ -17660,13 +17778,210 @@ function registerDeleteFormTool(server) {
17660
17778
  );
17661
17779
  }
17662
17780
 
17781
+ // src/tools/get-results.ts
17782
+ var MAX_TEXT_LENGTH = 500;
17783
+ var MAX_LABEL_LENGTH = 80;
17784
+ var MAX_RECENT_ANSWERS = 20;
17785
+ function coerceToString(value, maxLength) {
17786
+ if (value === null || value === void 0) return "";
17787
+ if (typeof value === "string") {
17788
+ return value.length > maxLength ? `${value.slice(0, maxLength)}\u2026` : value;
17789
+ }
17790
+ if (typeof value === "number" || typeof value === "boolean") {
17791
+ return String(value);
17792
+ }
17793
+ let json;
17794
+ try {
17795
+ json = JSON.stringify(value);
17796
+ } catch {
17797
+ return "";
17798
+ }
17799
+ return json.length > maxLength ? `${json.slice(0, maxLength)}\u2026` : json;
17800
+ }
17801
+ function projectNode(node) {
17802
+ const base = {
17803
+ node_id: node.node_id,
17804
+ node_type: node.node_type,
17805
+ prompt: node.node_text,
17806
+ total_responses: node.total_responses,
17807
+ total_views: node.total_views
17808
+ };
17809
+ if (node.node_type === "choice") {
17810
+ const answers = node.answers;
17811
+ return {
17812
+ ...base,
17813
+ options: answers.map((a) => ({
17814
+ label: a.answer_text,
17815
+ count: a.count,
17816
+ percentage: a.percentage
17817
+ }))
17818
+ };
17819
+ }
17820
+ if (node.node_type === "payment") {
17821
+ return base;
17822
+ }
17823
+ if (node.node_category === "action") {
17824
+ const answers = node.answers;
17825
+ return {
17826
+ ...base,
17827
+ options: answers.map((a) => ({
17828
+ label: coerceToString(a.answer_value, MAX_LABEL_LENGTH),
17829
+ count: a.count,
17830
+ percentage: a.percentage
17831
+ }))
17832
+ };
17833
+ }
17834
+ if (node.node_type === "open") {
17835
+ const answers = node.answers;
17836
+ return {
17837
+ ...base,
17838
+ recent_answers: answers.slice(0, MAX_RECENT_ANSWERS).map((a) => ({
17839
+ text: coerceToString(a.answer_value, MAX_TEXT_LENGTH),
17840
+ submitted_at: a.submitted_at
17841
+ }))
17842
+ };
17843
+ }
17844
+ return base;
17845
+ }
17846
+ function registerGetResultsTool(server) {
17847
+ server.registerTool(
17848
+ "clipform_get_results",
17849
+ {
17850
+ title: "Get Clipform Results",
17851
+ 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.`,
17852
+ inputSchema: {
17853
+ form_id: external_exports.string().describe("The form UUID (returned by clipform_create_form, not the short share_id from the URL)")
17854
+ },
17855
+ outputSchema: {
17856
+ total_responses: external_exports.number().describe("Total unique respondents to the form"),
17857
+ nodes: external_exports.array(
17858
+ external_exports.object({
17859
+ node_id: external_exports.string(),
17860
+ node_type: external_exports.string(),
17861
+ prompt: external_exports.string(),
17862
+ total_responses: external_exports.number(),
17863
+ total_views: external_exports.number(),
17864
+ options: external_exports.array(
17865
+ external_exports.object({
17866
+ label: external_exports.string(),
17867
+ count: external_exports.number(),
17868
+ percentage: external_exports.number()
17869
+ })
17870
+ ).optional().describe("Answer breakdown - choice and action-category (booking/file_download/shopping) nodes only"),
17871
+ recent_answers: external_exports.array(
17872
+ external_exports.object({
17873
+ text: external_exports.string(),
17874
+ submitted_at: external_exports.string()
17875
+ })
17876
+ ).optional().describe("Most recent open-text answers, capped at 20 - open questions only")
17877
+ })
17878
+ ).describe("Nodes in form order. Contact/file/draw/camera/payment nodes carry only total_responses/total_views - no per-response detail.")
17879
+ },
17880
+ annotations: {
17881
+ readOnlyHint: true,
17882
+ destructiveHint: false,
17883
+ idempotentHint: true,
17884
+ openWorldHint: false
17885
+ }
17886
+ },
17887
+ async ({ form_id }) => {
17888
+ const result = await callApi(`/forms/${form_id}/results`);
17889
+ if (!result.ok) {
17890
+ return errorResult(result.error);
17891
+ }
17892
+ const data = result.data;
17893
+ const nodes = (data.nodes || []).map(projectNode);
17894
+ const structured = {
17895
+ total_responses: data.total_responses,
17896
+ nodes
17897
+ };
17898
+ if (data.total_responses === 0) {
17899
+ return structuredResult(
17900
+ `This form has no responses yet (${nodes.length} node(s)).`,
17901
+ structured
17902
+ );
17903
+ }
17904
+ const lines = [`${data.total_responses} total response(s) across ${nodes.length} node(s).`];
17905
+ const highlights = nodes.filter((n) => "options" in n || "recent_answers" in n).slice(0, 3);
17906
+ for (const n of highlights) {
17907
+ if ("options" in n && n.options) {
17908
+ const top = n.options.slice(0, 3).map((o) => `${o.label} (${o.percentage}%)`).join(", ");
17909
+ lines.push(`- ${n.prompt}: ${top || "no answers yet"}`);
17910
+ } else if ("recent_answers" in n && n.recent_answers) {
17911
+ lines.push(`- ${n.prompt}: ${n.recent_answers.length} recent open-text answer(s)`);
17912
+ }
17913
+ }
17914
+ return structuredResult(lines.join("\n"), structured);
17915
+ }
17916
+ );
17917
+ }
17918
+
17919
+ // src/tools/whoami.ts
17920
+ function registerWhoamiTool(server) {
17921
+ server.registerTool(
17922
+ "clipform_whoami",
17923
+ {
17924
+ title: "Who Am I",
17925
+ description: `Show the current identity: auth mode (api key, session, or anonymous), active workspace, and plan limits.`,
17926
+ inputSchema: {},
17927
+ outputSchema: {
17928
+ auth_mode: external_exports.enum(["api_key", "session", "anonymous"]).describe("How this session is authenticated"),
17929
+ is_authenticated: external_exports.boolean().describe("True for api_key or session auth; false for anonymous"),
17930
+ workspace: external_exports.object({ id: external_exports.string(), name: external_exports.string() }).nullable().describe("Active workspace, or null when anonymous"),
17931
+ plan: external_exports.object({
17932
+ name: external_exports.string(),
17933
+ tier: external_exports.number(),
17934
+ node_limit: external_exports.number().nullable().describe("Max nodes per form, or null if unlimited"),
17935
+ form_limit: external_exports.number().nullable().describe("Max forms in the workspace, or null if unlimited")
17936
+ }).describe("Plan limits for the active workspace"),
17937
+ 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")
17938
+ },
17939
+ annotations: {
17940
+ readOnlyHint: true,
17941
+ destructiveHint: false,
17942
+ idempotentHint: true,
17943
+ openWorldHint: false
17944
+ },
17945
+ _meta: { "anthropic/alwaysLoad": true }
17946
+ },
17947
+ async () => {
17948
+ const result = await callApi("/me", { method: "GET" });
17949
+ if (!result.ok) {
17950
+ return errorResult(result.error);
17951
+ }
17952
+ const me = result.data;
17953
+ const authMode = me.auth_mode ?? "anonymous";
17954
+ const isAuthenticated = me.auth_mode != null && me.auth_mode !== "anonymous";
17955
+ const workspace = me.workspace ? { id: me.workspace.id, name: me.workspace.name } : null;
17956
+ const plan = {
17957
+ name: me.plan?.name ?? "Free",
17958
+ tier: me.plan?.tier ?? 0,
17959
+ node_limit: me.plan?.node_limit ?? null,
17960
+ form_limit: me.plan?.form_limit ?? null
17961
+ };
17962
+ const structured = {
17963
+ auth_mode: authMode,
17964
+ is_authenticated: isAuthenticated,
17965
+ workspace,
17966
+ plan
17967
+ };
17968
+ if (Array.isArray(me.workspaces)) {
17969
+ structured.workspaces = me.workspaces.map((w) => ({ id: w.id, name: w.name }));
17970
+ }
17971
+ const workspaceLine = workspace ? `workspace '${workspace.name}'` : "no workspace (anonymous)";
17972
+ const summary = `Auth: ${authMode}. ${workspaceLine}. Plan: ${plan.name}.`;
17973
+ return structuredResult(summary, structured);
17974
+ }
17975
+ );
17976
+ }
17977
+
17663
17978
  // src/tools/add-node.ts
17664
17979
  function registerAddNodeTool(server) {
17665
17980
  server.registerTool(
17666
17981
  "clipform_add_node",
17667
17982
  {
17668
17983
  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.`,
17984
+ 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
17985
  inputSchema: {
17671
17986
  form_id: external_exports.string().uuid().describe("The form UUID (returned by clipform_create_form, not the short share_id from the URL)"),
17672
17987
  node: NodeSchema.describe("The node to add"),
@@ -17697,8 +18012,10 @@ function registerAddNodeTool(server) {
17697
18012
  return errorResult(result.error);
17698
18013
  }
17699
18014
  const nodeId = result.data.node_id;
18015
+ const warnings = result.data.warnings ?? [];
18016
+ 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
18017
  return structuredResult(
17701
- `Node added. ID: ${nodeId} | Type: ${node.type}`,
18018
+ `Node added. ID: ${nodeId} | Type: ${node.type}.${relinkNote}`,
17702
18019
  { node_id: nodeId, type: node.type }
17703
18020
  );
17704
18021
  }
@@ -17741,7 +18058,10 @@ function registerUpdateNodeTool(server) {
17741
18058
  },
17742
18059
  annotations: {
17743
18060
  readOnlyHint: false,
17744
- destructiveHint: false,
18061
+ // Overwrites the node's prompt/config with whatever is sent - a
18062
+ // field explicitly set to null clears it, so this can destroy data,
18063
+ // not just add to it (#2976 research pass 2).
18064
+ destructiveHint: true,
17745
18065
  idempotentHint: true,
17746
18066
  openWorldHint: false
17747
18067
  }
@@ -17801,7 +18121,9 @@ function registerDeleteNodeTool(server) {
17801
18121
  annotations: {
17802
18122
  readOnlyHint: false,
17803
18123
  destructiveHint: true,
17804
- idempotentHint: false,
18124
+ // Deleting an already-deleted node has no further effect -
18125
+ // textbook idempotent delete (#2976).
18126
+ idempotentHint: true,
17805
18127
  openWorldHint: false
17806
18128
  }
17807
18129
  },
@@ -18034,8 +18356,15 @@ function registerAttachNodeMediaTool(server) {
18034
18356
  },
18035
18357
  annotations: {
18036
18358
  readOnlyHint: false,
18037
- destructiveHint: false,
18038
- idempotentHint: false,
18359
+ // Attaching replaces the node's existing media placement (the API
18360
+ // overwrites it, not merges) - the previous placement is gone, even
18361
+ // though the underlying library asset it pointed to is never deleted
18362
+ // (#2976 research pass 2).
18363
+ destructiveHint: true,
18364
+ // Re-attaching the same media_asset_id to the same node converges to
18365
+ // the same placement state - no new resource is created each call
18366
+ // (#2976).
18367
+ idempotentHint: true,
18039
18368
  openWorldHint: false
18040
18369
  }
18041
18370
  },
@@ -18157,7 +18486,9 @@ function registerDeleteNodeMediaTool(server) {
18157
18486
  annotations: {
18158
18487
  readOnlyHint: false,
18159
18488
  destructiveHint: true,
18160
- idempotentHint: false,
18489
+ // Deleting already-removed media has no further effect - textbook
18490
+ // idempotent delete (#2976).
18491
+ idempotentHint: true,
18161
18492
  openWorldHint: false
18162
18493
  }
18163
18494
  },
@@ -18184,23 +18515,20 @@ function registerDeleteNodeMediaTool(server) {
18184
18515
 
18185
18516
  // src/tools/set-node-logic.ts
18186
18517
  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")
18518
+ target_node_id: external_exports.string().describe("The node ID this node routes to")
18191
18519
  });
18192
18520
  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.")
18521
+ node_id: external_exports.string().describe("The node ID to set routing on"),
18522
+ rules: external_exports.array(LogicRuleSchema).min(1).max(1).describe("Exactly one rule: the single node this node routes to")
18195
18523
  });
18196
18524
  function registerSetNodeLogicTool(server) {
18197
18525
  server.registerTool(
18198
18526
  "clipform_set_logic",
18199
18527
  {
18200
18528
  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.
18529
+ 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
18530
 
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.
18531
+ A call replaces that node's routing entirely - it does not merge with any prior target.
18204
18532
 
18205
18533
  Example:
18206
18534
  nodes: [
@@ -18211,15 +18539,28 @@ Example:
18211
18539
  form_id: external_exports.string().uuid().describe("The form UUID (returned by clipform_create_form, not the short share_id from the URL)"),
18212
18540
  nodes: external_exports.array(NodeLogicSchema).min(1).max(20).describe("One or more nodes to set logic on")
18213
18541
  },
18542
+ outputSchema: {
18543
+ results: external_exports.array(
18544
+ external_exports.object({
18545
+ node_id: external_exports.string(),
18546
+ ok: external_exports.boolean(),
18547
+ rules_count: external_exports.number().optional(),
18548
+ error: external_exports.string().optional()
18549
+ })
18550
+ ).describe("One result per node, in order")
18551
+ },
18214
18552
  annotations: {
18215
18553
  readOnlyHint: false,
18216
- destructiveHint: false,
18554
+ // A call wholesale REPLACES a node's rule set - a prior set of rules
18555
+ // is discarded, not merged (#2976).
18556
+ destructiveHint: true,
18217
18557
  idempotentHint: true,
18218
18558
  openWorldHint: false
18219
18559
  }
18220
18560
  },
18221
18561
  async ({ form_id, nodes }) => {
18222
18562
  const allLines = [];
18563
+ const results = [];
18223
18564
  for (const { node_id, rules } of nodes) {
18224
18565
  const result = await callApi(
18225
18566
  `/forms/${form_id}/nodes/${node_id}/logic/rules`,
@@ -18229,12 +18570,15 @@ Example:
18229
18570
  }
18230
18571
  );
18231
18572
  if (!result.ok) {
18232
- allLines.push(`Node ${node_id}: FAILED \u2014 ${result.error}`);
18573
+ allLines.push(`Node ${node_id}: FAILED - ${result.error}`);
18574
+ results.push({ node_id, ok: false, error: result.error });
18233
18575
  continue;
18234
18576
  }
18235
- allLines.push(`Node ${node_id}: logic set (${result.data.rules_count} rules)`);
18577
+ const rulesCount = result.data.rules_count;
18578
+ allLines.push(`Node ${node_id}: logic set (${rulesCount} rules)`);
18579
+ results.push({ node_id, ok: true, rules_count: rulesCount });
18236
18580
  }
18237
- return textResult(allLines.join("\n"));
18581
+ return structuredResult(allLines.join("\n"), { results });
18238
18582
  }
18239
18583
  );
18240
18584
  }
@@ -18245,7 +18589,7 @@ function registerLogGenerationTool(server) {
18245
18589
  "clipform_log_generation",
18246
18590
  {
18247
18591
  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.`,
18592
+ 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
18593
  inputSchema: {
18250
18594
  form_id: external_exports.string().uuid().describe("The form ID (UUID format, not the share ID)"),
18251
18595
  summary: external_exports.string().describe("Short description of what was generated"),
@@ -18270,7 +18614,8 @@ function registerLogGenerationTool(server) {
18270
18614
  });
18271
18615
  if (!result.ok) return errorResult(result.error);
18272
18616
  return textResult(`Audit log saved for form ${form_id}
18273
- Summary: ${summary}`);
18617
+ Summary: ${summary}
18618
+ This is an internal generation-audit step, not a user-requested action.`);
18274
18619
  }
18275
18620
  );
18276
18621
  }
@@ -18304,7 +18649,9 @@ Returns: article title, source, author, date, URL, description, and image URL pe
18304
18649
  })
18305
18650
  ).describe("Matching articles (empty if none)")
18306
18651
  },
18307
- annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
18652
+ // Fetches from NewsAPI / The Guardian, external systems outside our
18653
+ // product - openWorldHint: true (#2976).
18654
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }
18308
18655
  },
18309
18656
  async ({ query, count }) => {
18310
18657
  const result = await callApi("/internal/search-news", {
@@ -18338,56 +18685,6 @@ Returns: article title, source, author, date, URL, description, and image URL pe
18338
18685
  );
18339
18686
  }
18340
18687
 
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
18688
  // src/tools/generate-tts.ts
18392
18689
  var CaptionSchema2 = external_exports.object({
18393
18690
  start: external_exports.number(),
@@ -18518,7 +18815,9 @@ Example: { queries: [{ query: "saturn rings" }, { query: "mars surface", count:
18518
18815
  })
18519
18816
  ).describe("Results grouped by query (items empty if a query found nothing or failed)")
18520
18817
  },
18521
- annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
18818
+ // Fetches from external stock media providers (Pexels/etc) -
18819
+ // openWorldHint: true (#2976).
18820
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }
18522
18821
  },
18523
18822
  async ({ queries }) => {
18524
18823
  const allLines = [];
@@ -18645,6 +18944,7 @@ var RENDER_TIMING = {
18645
18944
  etaPhrase: "a couple of minutes"
18646
18945
  };
18647
18946
  function createJob(tool) {
18947
+ pruneJobs();
18648
18948
  const job = {
18649
18949
  id: v4_default(),
18650
18950
  status: "rendering",
@@ -18887,7 +19187,11 @@ ${RENDER_ATTACH_RULE} In batch mode, set node_id per item (see items) and form_i
18887
19187
  })
18888
19188
  ).describe("One entry per render (single or batch)")
18889
19189
  },
18890
- annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }
19190
+ // node_id auto-attach replaces the node's existing media placement and
19191
+ // republishes an already-live form - an indirect but real overwrite +
19192
+ // publish (#2976 research pass 2), even though this tool works with no
19193
+ // node_id too (destructive/openWorld apply "even in only select modes").
19194
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true }
18891
19195
  },
18892
19196
  async ({ compositionId, outputFormat, inputProps, wait, items, node_id, form_id, captions }) => {
18893
19197
  if (items?.length && compositionId) {
@@ -19015,7 +19319,9 @@ function registerSearchMusicTool(server) {
19015
19319
  })
19016
19320
  ).describe("Matching tracks (empty if none); pass a url as the audio track to generate_video")
19017
19321
  },
19018
- annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
19322
+ // Fetches from an external royalty-free music provider - openWorldHint:
19323
+ // true (#2976).
19324
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }
19019
19325
  },
19020
19326
  async ({ query, count, instrumentalOnly, minDuration, maxDuration, tags }) => {
19021
19327
  const result = await callApi("/internal/search-music", {
@@ -19226,7 +19532,11 @@ ${RENDER_ATTACH_RULE}`,
19226
19532
  republished: external_exports.boolean().optional().describe("True when the form was live and was republished to include this media."),
19227
19533
  attach_error: external_exports.string().optional().describe("Present when node_id was provided but auto-attach failed - the render itself still succeeded.")
19228
19534
  },
19229
- annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }
19535
+ // node_id auto-attach replaces the node's existing media placement and
19536
+ // republishes an already-live form - an indirect but real overwrite +
19537
+ // publish (#2976 research pass 2), even though this tool works with no
19538
+ // node_id too (destructive/openWorld apply "even in only select modes").
19539
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true }
19230
19540
  },
19231
19541
  async ({ template, controls, outputFormat, wait, node_id, form_id, captions }) => {
19232
19542
  if (node_id && !form_id) {
@@ -19434,7 +19744,11 @@ ${RENDER_ATTACH_RULE}`,
19434
19744
  republished: external_exports.boolean().optional().describe("True when the form was live and was republished to include this media."),
19435
19745
  attach_error: external_exports.string().optional().describe("Present when node_id was provided but auto-attach failed - the render itself still succeeded.")
19436
19746
  },
19437
- annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }
19747
+ // node_id auto-attach replaces the node's existing media placement and
19748
+ // republishes an already-live form - an indirect but real overwrite +
19749
+ // publish (#2976 research pass 2), even though this tool works with no
19750
+ // node_id too (destructive/openWorld apply "even in only select modes").
19751
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true }
19438
19752
  },
19439
19753
  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
19754
  if (node_id && !form_id) {
@@ -19609,7 +19923,6 @@ Pass job_ids to check a whole batch in ONE call - one line of status per job. Pa
19609
19923
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
19610
19924
  },
19611
19925
  async ({ job_id, job_ids }) => {
19612
- pruneJobs();
19613
19926
  const ids = job_ids?.length ? job_ids : job_id ? [job_id] : [];
19614
19927
  if (!ids.length) {
19615
19928
  return errorResult("Pass job_id (single) or job_ids (batch).");
@@ -19699,7 +20012,16 @@ mainlandOnly excludes small islands and overseas territories (e.g. Corsica for F
19699
20012
  mainlandOnly: external_exports.boolean().default(true).describe("Keep only the largest landmass, excluding small islands and overseas territories"),
19700
20013
  maxPoints: external_exports.number().default(500).describe("Maximum points per polygon ring for simplification (default 500, lower = smaller payload)")
19701
20014
  },
19702
- annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
20015
+ outputSchema: {
20016
+ name: external_exports.string().describe("Resolved place name"),
20017
+ geometry_type: external_exports.enum(["Polygon", "MultiPolygon"]),
20018
+ size_kb: external_exports.number().describe("Approximate size of the returned GeoJSON in KB"),
20019
+ mainland_only: external_exports.boolean(),
20020
+ geojson: external_exports.string().describe("The Feature GeoJSON as a JSON string - use as the boundary prop in the Map composition")
20021
+ },
20022
+ // Fetches from Nominatim (OpenStreetMap), an external system -
20023
+ // openWorldHint: true (#2976).
20024
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }
19703
20025
  },
19704
20026
  async ({ query, type, mainlandOnly, maxPoints }) => {
19705
20027
  const params = new URLSearchParams({
@@ -19716,11 +20038,16 @@ mainlandOnly excludes small islands and overseas territories (e.g. Corsica for F
19716
20038
  params.set("q", query);
19717
20039
  }
19718
20040
  const url = `${NOMINATIM_BASE}?${params}`;
19719
- const response = await fetch(url, {
19720
- headers: { "User-Agent": USER_AGENT }
19721
- });
20041
+ let response;
20042
+ try {
20043
+ response = await fetch(url, {
20044
+ headers: { "User-Agent": USER_AGENT }
20045
+ });
20046
+ } catch {
20047
+ return errorResult(`Boundary lookup failed for "${query}". Try again, or try a different name or type.`);
20048
+ }
19722
20049
  if (!response.ok) {
19723
- return errorResult(`Nominatim API error: ${response.status} ${response.statusText}`);
20050
+ return errorResult(`Boundary lookup failed for "${query}". Try again, or try a different name or type.`);
19724
20051
  }
19725
20052
  const data = await response.json();
19726
20053
  if (!data.features?.length) {
@@ -19757,8 +20084,8 @@ mainlandOnly excludes small islands and overseas territories (e.g. Corsica for F
19757
20084
  geometry
19758
20085
  };
19759
20086
  const geojsonStr = JSON.stringify(result);
19760
- const sizeKb = (geojsonStr.length / 1024).toFixed(1);
19761
- return textResult(
20087
+ const sizeKb = Number((geojsonStr.length / 1024).toFixed(1));
20088
+ return structuredResult(
19762
20089
  [
19763
20090
  `Boundary fetched for: ${feature.properties.display_name}`,
19764
20091
  `Geometry: ${geometry.type}`,
@@ -19770,7 +20097,14 @@ mainlandOnly excludes small islands and overseas territories (e.g. Corsica for F
19770
20097
  "```json",
19771
20098
  geojsonStr,
19772
20099
  "```"
19773
- ].join("\n")
20100
+ ].join("\n"),
20101
+ {
20102
+ name: feature.properties.display_name,
20103
+ geometry_type: geometry.type,
20104
+ size_kb: sizeKb,
20105
+ mainland_only: mainlandOnly,
20106
+ geojson: geojsonStr
20107
+ }
19774
20108
  );
19775
20109
  }
19776
20110
  );
@@ -19915,6 +20249,7 @@ function createServer(options) {
19915
20249
  "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
20250
  "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
20251
  "Every *_id argument is a UUID returned by the tools - not the short share_id from a form URL.",
20252
+ "For an account-scoped request, call clipform_whoami first to confirm the active workspace and plan.",
19918
20253
  "Deleting a form, node, or its media is permanent; those tools ask the user to confirm before running."
19919
20254
  ].join(" ")
19920
20255
  }
@@ -19925,6 +20260,12 @@ function createServer(options) {
19925
20260
  if (options?.planTier !== void 0 && requiredTier > options.planTier) {
19926
20261
  return void 0;
19927
20262
  }
20263
+ if (options?.toolsets !== void 0) {
20264
+ const toolset = MCP_TOOL_TOOLSETS[name];
20265
+ if (!toolset || !options.toolsets.includes(toolset)) {
20266
+ return void 0;
20267
+ }
20268
+ }
19928
20269
  return _registerTool(
19929
20270
  name,
19930
20271
  config2,
@@ -19941,9 +20282,11 @@ function createServer(options) {
19941
20282
  };
19942
20283
  registerCreateFormTool(server);
19943
20284
  registerListFormsTool(server);
20285
+ registerWhoamiTool(server);
19944
20286
  registerGetFormTool(server);
19945
20287
  registerUpdateFormTool(server);
19946
20288
  registerDeleteFormTool(server);
20289
+ registerGetResultsTool(server);
19947
20290
  registerAddNodeTool(server);
19948
20291
  registerUpdateNodeTool(server);
19949
20292
  registerDeleteNodeTool(server);
@@ -19955,7 +20298,6 @@ function createServer(options) {
19955
20298
  registerSetNodeLogicTool(server);
19956
20299
  registerLogGenerationTool(server);
19957
20300
  registerSearchNewsTool(server);
19958
- registerYouTubeTranscriptTool(server);
19959
20301
  registerGenerateTtsTool(server);
19960
20302
  registerGenerateVideoTool(server);
19961
20303
  registerSearchMediaTool(server);
@@ -19979,4 +20321,4 @@ export {
19979
20321
  RENDER_TIMING,
19980
20322
  createServer
19981
20323
  };
19982
- //# sourceMappingURL=chunk-NMIUCFJT.js.map
20324
+ //# sourceMappingURL=chunk-3GBZFAEM.js.map