@clipform/mcp-server 4.2.0 → 4.2.2

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.
@@ -29,7 +29,7 @@ import {
29
29
  union,
30
30
  unknown,
31
31
  url
32
- } from "./chunk-5RUWQOYD.js";
32
+ } from "./chunk-APCG2RRD.js";
33
33
  import {
34
34
  GUIDE_TYPES,
35
35
  QUIZ_VARIANTS,
@@ -38,13 +38,18 @@ import {
38
38
  getGuideUri,
39
39
  guideFallbackText,
40
40
  registerResources
41
- } from "./chunk-TXUWAYBZ.js";
41
+ } from "./chunk-5AAKB4S5.js";
42
42
  import {
43
+ ANON_PREVIEW_LINK_LABEL,
43
44
  ANON_PREVIEW_NOTICE,
45
+ ANON_PREVIEW_ONLY_NOTICE,
46
+ CLAIM_URL_DESCRIPTION,
44
47
  FULL_CAPS,
48
+ PREVIEW_URL_DESCRIPTION,
45
49
  anonPreviewNotice,
46
- capsFromToolsets
47
- } from "./chunk-PDI6VJYQ.js";
50
+ capsFromToolsets,
51
+ isPreviewViewerUrl
52
+ } from "./chunk-FGTU764J.js";
48
53
  import {
49
54
  ACTIVE_NODE_TYPES,
50
55
  CONTACT_FIELDS,
@@ -66,7 +71,7 @@ import {
66
71
  resolveFormType,
67
72
  structuredResult,
68
73
  textResult
69
- } from "./chunk-4IAURGG7.js";
74
+ } from "./chunk-KZNBYHHR.js";
70
75
  import {
71
76
  getMcpAuth,
72
77
  runWithMcpAuth,
@@ -4774,6 +4779,37 @@ var inputRequired = Object.assign(buildInputRequired, {
4774
4779
  return { method: "roots/list" };
4775
4780
  }
4776
4781
  });
4782
+ function acceptedContent(responses, key, schema) {
4783
+ const view = inputResponse(responses, key);
4784
+ if (view.kind !== "elicit" || view.action !== "accept" || view.content === void 0) return void 0;
4785
+ if (schema === void 0) return view.content;
4786
+ const outcome = schema["~standard"].validate(view.content);
4787
+ if (outcome instanceof Promise) throw new TypeError("acceptedContent(responses, key, schema) requires a synchronously-validating schema");
4788
+ return outcome.issues === void 0 ? outcome.value : void 0;
4789
+ }
4790
+ function inputResponse(responses, key) {
4791
+ if (responses === void 0 || typeof responses !== "object" || responses === null) return { kind: "missing" };
4792
+ const entry = responses[key];
4793
+ if (entry === null || typeof entry !== "object" || Array.isArray(entry)) return { kind: "missing" };
4794
+ const candidate = entry;
4795
+ if (candidate["action"] === "accept" || candidate["action"] === "decline" || candidate["action"] === "cancel") {
4796
+ const content = candidate["content"];
4797
+ return {
4798
+ kind: "elicit",
4799
+ action: candidate["action"],
4800
+ ...content !== null && typeof content === "object" && !Array.isArray(content) && { content }
4801
+ };
4802
+ }
4803
+ if (Array.isArray(candidate["roots"])) return {
4804
+ kind: "roots",
4805
+ roots: candidate["roots"]
4806
+ };
4807
+ if (typeof candidate["role"] === "string" && candidate["content"] !== void 0) return {
4808
+ kind: "sampling",
4809
+ result: candidate
4810
+ };
4811
+ return { kind: "missing" };
4812
+ }
4777
4813
  var REQUEST_STATE_ONLY_LEG_PACING_MS = 250;
4778
4814
  function inputRequiredRoundsExceededMessage(method, maxRounds) {
4779
4815
  return `Multi-round-trip request '${method}' still required input after ${maxRounds} rounds (inputRequired.maxRounds)`;
@@ -16283,7 +16319,7 @@ function registerCreateFormTool(server) {
16283
16319
  "clipform_create_form",
16284
16320
  {
16285
16321
  title: "Create Clipform",
16286
- description: `Create a new Clipform (interactive video-style form). Returns a viewer URL and form ID. When connected via an authenticated MCP client (e.g. claude.ai), the form lands directly in the user's workspace. Anonymous sessions build into a temporary account of their own: forms stay drafts with a private preview link. Some results also include a one-time claim URL that signs the user into that account; relay it when present.
16322
+ description: `Create a new Clipform (interactive video-style form). Returns a viewer URL and form ID (an anonymous build's link is a private preview where answers are not saved). When connected via an authenticated MCP client (e.g. claude.ai), the form lands directly in the user's workspace. Anonymous sessions build into a temporary account of their own: forms stay drafts with a private preview link. Some results also include a one-time claim URL that signs the user into that account; relay it when present.
16287
16323
 
16288
16324
  ${ONBOARDING_OFFER_TOOL_CUE}
16289
16325
 
@@ -16313,12 +16349,20 @@ Example: A form that asks a question, collects contact info, then finishes:
16313
16349
  }),
16314
16350
  outputSchema: external_exports.object({
16315
16351
  form_id: external_exports.string().describe("Form UUID - pass to follow-up tools"),
16316
- // #3330: an anonymous session's viewer_url is a scoped preview link
16317
- // (only the caller can open it) until the account is claimed and the
16318
- // form is published - only an authenticated/claimed workspace's
16319
- // create returns a genuinely live URL here.
16320
- 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"),
16321
- dashboard_url: external_exports.string().optional().describe("Builder URL (sign in to edit)"),
16352
+ // #4845 expand/contract. The deployed ChatGPT schema is
16353
+ // additionalProperties:false, so emitting a field it does not declare
16354
+ // is invalid until the Rescan approves a schema that declares it.
16355
+ // Order: (a) deploy the schema declaring preview_url / claim_url /
16356
+ // is_preview as optional, emitting NOTHING new (this step);
16357
+ // (b) Rescan; (c) once approved, emit them; (d) a later Rescan-approved
16358
+ // CONTRACT step drops the anonymous viewer_url / dashboard_url
16359
+ // duplicates. See SUBMISSION.md. Until (c), structuredContent stays
16360
+ // key-for-key what origin/main emitted (approved-schema.test.ts).
16361
+ viewer_url: external_exports.string().describe("Form URL for respondents - the live public URL. For an anonymous build this is a private preview link: answers are not saved, do not share it with respondents."),
16362
+ dashboard_url: external_exports.string().optional().describe("Builder URL (sign in to edit). For an anonymous build this is a one-time claim link, not an editor."),
16363
+ is_preview: external_exports.boolean().optional().describe("True when this is an anonymous preview build (answers are not saved), false otherwise."),
16364
+ preview_url: external_exports.string().optional().describe(`Anonymous builds only. ${PREVIEW_URL_DESCRIPTION}`),
16365
+ claim_url: external_exports.string().optional().describe(`Anonymous builds only. ${CLAIM_URL_DESCRIPTION}`),
16322
16366
  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"),
16323
16367
  plan: external_exports.object({ name: external_exports.string(), auth_mode: external_exports.string() }).optional().describe("Plan and auth context for this session"),
16324
16368
  workspace_name: external_exports.string().optional().describe("Name of the workspace the form was created in")
@@ -16411,7 +16455,7 @@ Example: A form that asks a question, collects contact info, then finishes:
16411
16455
  });
16412
16456
  if (!createResult.ok) {
16413
16457
  const message = `Form NOT created (nothing was saved): ${createResult.error}`;
16414
- return isAnonymousAccountRequired(createResult) ? errorResult(message, authChallengeMeta("invalid_token", createResult.apiMessage ?? message)) : errorResult(message);
16458
+ return isAnonymousAccountRequired(createResult) ? errorResult(message, authChallengeMeta("insufficient_scope", createResult.apiMessage ?? message)) : errorResult(message);
16415
16459
  }
16416
16460
  const { data } = createResult;
16417
16461
  const formId = data.form_id;
@@ -16433,6 +16477,8 @@ Example: A form that asks a question, collects contact info, then finishes:
16433
16477
  const truncate2 = (s, n = 60) => s.length > n ? `${s.slice(0, n - 1)}\u2026` : s;
16434
16478
  const lines = [
16435
16479
  isPreview ? `Form created as a preview draft.` : `Form created and live.`,
16480
+ // #4845: lead with the in-chat path (wording defined once in lib/anon-notice.ts).
16481
+ ...isPreview ? [``, ANON_PREVIEW_ONLY_NOTICE] : [],
16436
16482
  ``,
16437
16483
  `Title: ${title}`,
16438
16484
  ...planContext?.is_authenticated && planContext.workspace_name ? [`Workspace: ${planContext.workspace_name}`] : [],
@@ -16441,18 +16487,18 @@ Example: A form that asks a question, collects contact info, then finishes:
16441
16487
  `Node IDs (use directly with attach_node_media / update_node - no get_form round-trip needed):`,
16442
16488
  ...createdNodes.map((n) => `- [${n.type}] "${truncate2(n.prompt)}": ${n.id}`),
16443
16489
  ``,
16444
- 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}`
16490
+ isPreview ? `PREVIEW URL (draft - answers are not saved, only you can open this, not for respondents): ${data.viewer_url}` : `FORM URL (live - share this with respondents): ${data.viewer_url}`
16445
16491
  ];
16446
16492
  if (formUrl) {
16447
16493
  lines.push(
16448
- isPreview ? `CLAIM URL (one-time - sign up to claim and publish): ${formUrl}` : `DASHBOARD URL (sign in to edit and manage): ${formUrl}`
16494
+ isPreview ? `CLAIM URL (fallback - or open this one-time link to sign up in the browser): ${formUrl}` : `DASHBOARD URL (sign in to edit and manage): ${formUrl}`
16449
16495
  );
16450
16496
  }
16451
16497
  const firstUrlLabel = isPreview ? "PREVIEW" : "FORM";
16452
16498
  const secondUrlLabel = isPreview ? "CLAIM" : "DASHBOARD";
16453
16499
  lines.push(
16454
16500
  ``,
16455
- isPreview ? `This form is a preview - claim and publish to start collecting responses.${formUrl ? " Open the CLAIM URL to sign up and claim the account this form already belongs to, then republish" : " Sign up to claim 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.`,
16501
+ isPreview ? `This form is a preview, not live. When the user says "publish it", call clipform_update_form (is_live: true); it prompts them to sign in. 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.`,
16456
16502
  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.`,
16457
16503
  `Pass form_id on follow-up tools (get_form, add_node, attach_node_media, etc.).`
16458
16504
  );
@@ -16923,7 +16969,11 @@ function formatFormState(data) {
16923
16969
  const urls = projectUrls(data);
16924
16970
  if (urls) {
16925
16971
  lines.push(``, `Links (exact values - constructed or guessed URLs return 404):`);
16926
- if (urls.viewer) lines.push(` Viewer (respondent view): ${urls.viewer}`);
16972
+ if (urls.viewer) {
16973
+ lines.push(
16974
+ isPreviewViewerUrl(urls.viewer) ? ` ${ANON_PREVIEW_LINK_LABEL}: ${urls.viewer}` : ` Viewer (respondent view): ${urls.viewer}`
16975
+ );
16976
+ }
16927
16977
  if (urls.builder) lines.push(` Builder: ${urls.builder}`);
16928
16978
  if (urls.summary) lines.push(` Summary: ${urls.summary}`);
16929
16979
  if (urls.responses) lines.push(` Responses: ${urls.responses}`);
@@ -16932,6 +16982,7 @@ function formatFormState(data) {
16932
16982
  if (urls.embed) lines.push(` Embed: ${urls.embed}`);
16933
16983
  if (urls.dynamic_link) lines.push(` Dynamic link: ${urls.dynamic_link}`);
16934
16984
  }
16985
+ if (isPreviewViewerUrl(urls?.viewer)) lines.push(``, ANON_PREVIEW_ONLY_NOTICE);
16935
16986
  lines.push(``, `Nodes (in order):`);
16936
16987
  for (let i = 0; i < nodes.length; i++) {
16937
16988
  const q = nodes[i];
@@ -16972,7 +17023,7 @@ function registerGetFormTool(server) {
16972
17023
  is_live: external_exports.boolean().describe("Whether the form is published"),
16973
17024
  has_unpublished_changes: external_exports.boolean().describe("Live form has edits not yet republished"),
16974
17025
  urls: external_exports.object({
16975
- viewer: external_exports.string().optional().describe("Live respondent link, or a scoped preview link for an anonymous caller's draft. Omitted for a non-anonymous caller's unpublished draft."),
17026
+ viewer: external_exports.string().optional().describe("Live respondent link, or - for an anonymous caller's draft - a private preview link where answers are not saved (do not share it with respondents). Omitted for a non-anonymous caller's unpublished draft."),
16976
17027
  builder: external_exports.string().optional().describe("Build tab - the node/canvas editor. Not present for an anonymous caller."),
16977
17028
  summary: external_exports.string().optional().describe("Summary tab - form overview and key stats. Not present for an anonymous caller."),
16978
17029
  responses: external_exports.string().optional().describe("Responses tab - individual submissions. Not present for an anonymous caller."),
@@ -17081,7 +17132,7 @@ function registerUpdateFormTool(server) {
17081
17132
  if (!result.ok) {
17082
17133
  return isAnonymousAccountRequired(result) ? errorResult(
17083
17134
  result.error,
17084
- authChallengeMeta("invalid_token", result.apiMessage ?? result.error)
17135
+ authChallengeMeta("insufficient_scope", result.apiMessage ?? result.error)
17085
17136
  ) : errorResult(result.error);
17086
17137
  }
17087
17138
  }
@@ -17093,7 +17144,7 @@ function registerUpdateFormTool(server) {
17093
17144
  if (!tagResult.ok) {
17094
17145
  return isAnonymousAccountRequired(tagResult) ? errorResult(
17095
17146
  tagResult.error,
17096
- authChallengeMeta("invalid_token", tagResult.apiMessage ?? tagResult.error)
17147
+ authChallengeMeta("insufficient_scope", tagResult.apiMessage ?? tagResult.error)
17097
17148
  ) : errorResult(tagResult.error);
17098
17149
  }
17099
17150
  }
@@ -17179,23 +17230,42 @@ ${anonNotice}`;
17179
17230
 
17180
17231
  // src/lib/confirm.ts
17181
17232
  var CONFIRM_FIELD_DESCRIPTION = "Set to true ONLY after the user has explicitly confirmed this destructive action. Leave unset otherwise - hosts that support confirmation dialogs will prompt the user automatically.";
17182
- async function confirmDestructive(server, opts) {
17233
+ var CONFIRM_RESPONSE_SCHEMA = external_exports.object({ confirm: external_exports.boolean() });
17234
+ var CANCELLED = "Cancelled - nothing was changed.";
17235
+ async function confirmDestructive(server, ctx, opts) {
17183
17236
  if (opts.alreadyConfirmed === true) return { proceed: true };
17237
+ const mcpReq = ctx?.mcpReq;
17238
+ const responses = mcpReq?.inputResponses;
17239
+ if (responses && responses.confirm !== void 0) {
17240
+ const answer = acceptedContent(responses, "confirm", CONFIRM_RESPONSE_SCHEMA);
17241
+ if (answer?.confirm === true) return { proceed: true };
17242
+ return { proceed: false, message: CANCELLED };
17243
+ }
17244
+ const requestedSchema = {
17245
+ type: "object",
17246
+ properties: {
17247
+ confirm: { type: "boolean", title: "Confirm", description: opts.message }
17248
+ },
17249
+ required: ["confirm"]
17250
+ };
17251
+ const envelope = mcpReq?.envelope;
17252
+ if (envelope) {
17253
+ if (envelope["io.modelcontextprotocol/clientCapabilities"]?.elicitation) {
17254
+ return {
17255
+ proceed: false,
17256
+ result: inputRequired({
17257
+ inputRequests: { confirm: inputRequired.elicit({ message: opts.message, requestedSchema }) }
17258
+ })
17259
+ };
17260
+ }
17261
+ return { proceed: false, message: opts.relay };
17262
+ }
17184
17263
  const lowLevel = server.server;
17185
17264
  const caps = lowLevel?.getClientCapabilities?.();
17186
17265
  if (caps?.elicitation) {
17187
- const r = await lowLevel.elicitInput({
17188
- message: opts.message,
17189
- requestedSchema: {
17190
- type: "object",
17191
- properties: {
17192
- confirm: { type: "boolean", title: "Confirm", description: opts.message }
17193
- },
17194
- required: ["confirm"]
17195
- }
17196
- });
17266
+ const r = await lowLevel.elicitInput({ message: opts.message, requestedSchema });
17197
17267
  if (r.action === "accept" && r.content?.confirm === true) return { proceed: true };
17198
- return { proceed: false, message: "Cancelled - nothing was changed." };
17268
+ return { proceed: false, message: CANCELLED };
17199
17269
  }
17200
17270
  return { proceed: false, message: opts.relay };
17201
17271
  }
@@ -17223,14 +17293,14 @@ function registerDeleteFormTool(server) {
17223
17293
  openWorldHint: true
17224
17294
  }
17225
17295
  },
17226
- async ({ form_id, confirm }) => {
17227
- const decision = await confirmDestructive(server, {
17296
+ async ({ form_id, confirm }, ctx) => {
17297
+ const decision = await confirmDestructive(server, ctx, {
17228
17298
  alreadyConfirmed: confirm,
17229
17299
  message: `Move form ${form_id} to the trash? It stops accepting responses immediately and can be restored later.`,
17230
17300
  relay: `Confirmation required: moving form ${form_id} to the trash stops it accepting responses immediately. It can be restored later. Ask the user to confirm, then call clipform_delete_form again with confirm: true.`
17231
17301
  });
17232
17302
  if (!decision.proceed) {
17233
- return textResult(decision.message);
17303
+ return decision.result ?? textResult(decision.message);
17234
17304
  }
17235
17305
  const result = await callApi(`/forms/${form_id}`, {
17236
17306
  method: "DELETE"
@@ -17797,14 +17867,14 @@ function registerDeleteNodeTool(server) {
17797
17867
  openWorldHint: false
17798
17868
  }
17799
17869
  },
17800
- async ({ form_id, node_id, confirm }) => {
17801
- const decision = await confirmDestructive(server, {
17870
+ async ({ form_id, node_id, confirm }, ctx) => {
17871
+ const decision = await confirmDestructive(server, ctx, {
17802
17872
  alreadyConfirmed: confirm,
17803
17873
  message: `Delete node ${node_id} from this form? This cannot be undone.`,
17804
17874
  relay: `Confirmation required: deleting node ${node_id} cannot be undone. Ask the user to confirm, then call clipform_delete_node again with confirm: true.`
17805
17875
  });
17806
17876
  if (!decision.proceed) {
17807
- return textResult(decision.message);
17877
+ return decision.result ?? textResult(decision.message);
17808
17878
  }
17809
17879
  const result = await callApi(
17810
17880
  `/forms/${form_id}/nodes/${node_id}`,
@@ -18290,14 +18360,14 @@ function registerDeleteNodeMediaTool(server) {
18290
18360
  openWorldHint: true
18291
18361
  }
18292
18362
  },
18293
- async ({ form_id, node_id, confirm }) => {
18294
- const decision = await confirmDestructive(server, {
18363
+ async ({ form_id, node_id, confirm }, ctx) => {
18364
+ const decision = await confirmDestructive(server, ctx, {
18295
18365
  alreadyConfirmed: confirm,
18296
18366
  message: `Remove the media attached to node ${node_id}? This deletes the underlying video/file and cannot be undone.`,
18297
18367
  relay: `Confirmation required: removing media from node ${node_id} deletes the underlying video/file and cannot be undone. Ask the user to confirm, then call clipform_delete_node_media again with confirm: true.`
18298
18368
  });
18299
18369
  if (!decision.proceed) {
18300
- return textResult(decision.message);
18370
+ return decision.result ?? textResult(decision.message);
18301
18371
  }
18302
18372
  const result = await callApi(
18303
18373
  `/forms/${form_id}/nodes/${node_id}/media`,
@@ -20097,4 +20167,4 @@ export {
20097
20167
  * MIT Licensed
20098
20168
  *)
20099
20169
  */
20100
- //# sourceMappingURL=chunk-YKJV5LZQ.js.map
20170
+ //# sourceMappingURL=chunk-XOV5ILOJ.js.map