@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.
package/README.md CHANGED
@@ -65,9 +65,11 @@ Your MCP client lists these automatically on connect (via `tools/list`). Full re
65
65
  |------|-------------|
66
66
  | `clipform_create_form` | Create a new Clipform (interactive video-style form). |
67
67
  | `clipform_list_forms` | List forms in your workspace with optional filtering. |
68
- | `clipform_get_form` | Retrieve a form's details including all nodes in sequential order. |
68
+ | `clipform_whoami` | Show the current identity: auth mode (api key, session, or anonymous), active workspace, and plan limits. |
69
+ | `clipform_get_form` | Retrieve a form's details including all nodes in sequential order and their routing. |
69
70
  | `clipform_update_form` | Update a form's title, publish status, settings, or tags. |
70
71
  | `clipform_delete_form` | Move a form and all its nodes to the trash. |
72
+ | `clipform_get_results` | View response counts, choice/action answer breakdowns, and recent open-text answers for a form. |
71
73
  | `clipform_add_node` | Add a new node to an existing form. |
72
74
  | `clipform_update_node` | Update one or more existing nodes' text, type, config, or options. |
73
75
  | `clipform_delete_node` | Delete a node from a form. |
@@ -76,10 +78,9 @@ Your MCP client lists these automatically on connect (via `tools/list`). Full re
76
78
  | `clipform_attach_node_media` | Attach an existing workspace media asset (from clipform_upload_media_asset) to one or more nodes (max 10). |
77
79
  | `clipform_get_node_media` | Get the media attached to a node, including processing status. |
78
80
  | `clipform_delete_node_media` | Remove media from a node. |
79
- | `clipform_set_logic` | Set routing logic on one or more nodes. |
80
- | `clipform_log_generation` | Save an audit trail for a generated form. |
81
+ | `clipform_set_logic` | Set the next node for one or more nodes, wiring a linear route (A to B to C). |
82
+ | `clipform_log_generation` | Internal audit step run by a form-generation workflow. |
81
83
  | `clipform_search_news` | Fallback news lookup for clients without native web search. |
82
- | `clipform_youtube_transcript` | Extract the transcript, title, and channel info from a YouTube video. |
83
84
  | `clipform_generate_tts` | Generate narration audio from text with word-level captions. |
84
85
  | `clipform_generate_video` | Generate a video from images, video clips, or both, synced to an audio track. |
85
86
  | `clipform_search_media` | Search images or stock video clips. |
@@ -14,9 +14,33 @@ interface McpAuthContext {
14
14
  scopes: string[];
15
15
  tool_name?: string;
16
16
  api_key?: string;
17
+ is_anonymous?: boolean;
18
+ mcp_mint_proof?: string;
17
19
  }
20
+ /**
21
+ * Marker header the in-process MCP tool layer sets on its outgoing API
22
+ * requests (api-client.ts) when `is_anonymous` is true, so an anonymous
23
+ * app-chat session can't launder itself into an unrestricted API-key caller
24
+ * by asking the bot to publish/mint credentials on its behalf. apps/api's
25
+ * authMiddleware / workspaceAccessMiddleware / requireWorkspaceWriteAuth
26
+ * read this on the API-key path and set isAnonymous accordingly - trusting
27
+ * it is safe because it can only RESTRICT a caller: a real integration that
28
+ * sets this header on its own API-key requests only denies itself, it can
29
+ * never use it to gain anonymous-JWT privileges it doesn't already have.
30
+ */
31
+ declare const MCP_ANONYMOUS_HEADER = "x-clipform-anonymous";
32
+ /**
33
+ * Carries `mcp_mint_proof` on the same outgoing bot-key request. Unlike
34
+ * MCP_ANONYMOUS_HEADER (a boolean that can only ever RESTRICT), this header's
35
+ * VALUE matters - it names an identity a claim token gets bound to - so its
36
+ * value is HMAC-signed (mcp-mint-proof.ts) rather than trusted raw: a direct
37
+ * external caller reaching `POST /v1/forms` with any valid workspace API key
38
+ * could otherwise hand-set an unsigned "mint uid" header to an arbitrary
39
+ * victim's uid themselves (#3330 review).
40
+ */
41
+ declare const MCP_MINT_PROOF_HEADER = "x-clipform-mcp-mint-proof";
18
42
  declare function runWithMcpAuth<T>(ctx: McpAuthContext, fn: () => Promise<T> | T): Promise<T> | T;
19
43
  declare function getMcpAuth(): McpAuthContext | undefined;
20
44
  declare function runWithMcpTool<T>(toolName: string, fn: () => Promise<T> | T): Promise<T> | T;
21
45
 
22
- export { type McpAuthContext, getMcpAuth, runWithMcpAuth, runWithMcpTool };
46
+ export { MCP_ANONYMOUS_HEADER, MCP_MINT_PROOF_HEADER, type McpAuthContext, getMcpAuth, runWithMcpAuth, runWithMcpTool };
@@ -1,10 +1,14 @@
1
1
  import {
2
+ MCP_ANONYMOUS_HEADER,
3
+ MCP_MINT_PROOF_HEADER,
2
4
  getMcpAuth,
3
5
  runWithMcpAuth,
4
6
  runWithMcpTool
5
- } from "./chunk-V4L5RQWK.js";
7
+ } from "./chunk-ENHVQDSI.js";
6
8
  import "./chunk-G3PMV62Z.js";
7
9
  export {
10
+ MCP_ANONYMOUS_HEADER,
11
+ MCP_MINT_PROOF_HEADER,
8
12
  getMcpAuth,
9
13
  runWithMcpAuth,
10
14
  runWithMcpTool
@@ -1,6 +1,8 @@
1
1
  import {
2
+ MCP_ANONYMOUS_HEADER,
3
+ MCP_MINT_PROOF_HEADER,
2
4
  getMcpAuth
3
- } from "./chunk-V4L5RQWK.js";
5
+ } from "./chunk-ENHVQDSI.js";
4
6
  import {
5
7
  isAuthFault,
6
8
  isServerFault,
@@ -95,6 +97,17 @@ var FEATURES = {
95
97
  status: "live",
96
98
  category: "builder"
97
99
  },
100
+ anonymous_builder: {
101
+ name: "Anonymous builder",
102
+ description: "Try the builder without an account via /try - an anonymous Supabase session, converted to a real account at publish time",
103
+ // Desktop v1 (#2979) shipped flag-dark: the entry route, publish wall, and
104
+ // conversion modal all land behind this flag. Enabled at launch rollout -
105
+ // the prod Supabase Anonymous Sign-Ins toggle must stay on for /try guest
106
+ // sessions to mint (dev first, then prod after #3016).
107
+ enabled: true,
108
+ status: "beta",
109
+ category: "builder"
110
+ },
98
111
  // -- Node types --
99
112
  node_start: {
100
113
  name: "Start node",
@@ -339,8 +352,30 @@ var FEATURES = {
339
352
  dynamic_link: {
340
353
  name: "Dynamic links",
341
354
  description: "A named link that points at a form, so the target can be swapped from the dashboard without changing the embed code or printed link",
355
+ enabled: true,
356
+ status: "live",
357
+ category: "sharing",
358
+ wentLiveAt: "2026-08-27"
359
+ },
360
+ custom_domains: {
361
+ name: "Custom domains",
362
+ description: "Serve your forms on your own subdomain (forms.yourbrand.com) with vanity paths, instead of a clipform.io link",
342
363
  enabled: false,
343
- status: "beta",
364
+ status: "planned",
365
+ category: "sharing"
366
+ },
367
+ branded_qr: {
368
+ name: "Branded QR codes",
369
+ description: "Render a form's share QR in its theme color with the logo centered, instead of plain black",
370
+ enabled: false,
371
+ status: "planned",
372
+ category: "sharing"
373
+ },
374
+ email_share: {
375
+ name: "Add to an email",
376
+ description: "Copy-paste image + link snippet for pasting a Clipform into an email campaign",
377
+ enabled: false,
378
+ status: "planned",
344
379
  category: "sharing"
345
380
  },
346
381
  remove_branding: {
@@ -2145,8 +2180,10 @@ var HELP_TOPICS = {
2145
2180
  liveAndDraft: { path: "/help/managing-forms", anchor: "live-and-draft" },
2146
2181
  publishingChanges: { path: "/help/managing-forms", anchor: "publishing-changes" },
2147
2182
  embedding: { path: "/help/embed", anchor: null },
2183
+ emailShare: { path: "/help/sharing", anchor: "share-by-email" },
2148
2184
  publicResults: { path: "/help/public-results", anchor: null },
2149
- trash: { path: "/help/managing-forms", anchor: "trash" }
2185
+ trash: { path: "/help/managing-forms", anchor: "trash" },
2186
+ agreementVersions: { path: "/help/responses", anchor: "agreement-versions" }
2150
2187
  };
2151
2188
  function buildHelpLinks(docsBaseUrl) {
2152
2189
  const links = {};
@@ -2156,6 +2193,58 @@ function buildHelpLinks(docsBaseUrl) {
2156
2193
  return links;
2157
2194
  }
2158
2195
 
2196
+ // ../config/logic-types.js
2197
+ function isBranchingActive(node) {
2198
+ if (!node) return true;
2199
+ return node.type === "choice" || node.type === "binary" ? node.config?.choice?.enable_branching === true : true;
2200
+ }
2201
+ function isOptionEdge(action) {
2202
+ return !!action?.condition?.vars?.some((v) => v.type === "option");
2203
+ }
2204
+ function isInactiveOptionEdge(node, action) {
2205
+ return isOptionEdge(action) && !isBranchingActive(node);
2206
+ }
2207
+
2208
+ // ../config/logic-chain.js
2209
+ function outgoingTargets(actions, node) {
2210
+ const out = [];
2211
+ for (const a of actions ?? []) {
2212
+ if (node && isInactiveOptionEdge(node, a)) continue;
2213
+ const v = a?.details?.to?.value;
2214
+ if (v && !out.includes(v)) out.push(v);
2215
+ }
2216
+ return out;
2217
+ }
2218
+ function defaultTarget(actions) {
2219
+ if (!actions || actions.length === 0) return null;
2220
+ const nodeLevel = actions.find((a) => {
2221
+ const vars = a.condition?.vars;
2222
+ return !!vars && vars.length > 0 && vars.every((v) => v.type === "node");
2223
+ });
2224
+ return (nodeLevel ?? actions[0])?.details?.to ?? null;
2225
+ }
2226
+ function bfsOrder(nodes, logicByNode) {
2227
+ const byId = new Map(nodes.map((n) => [n.id, n]));
2228
+ const ordered = [];
2229
+ const seen = /* @__PURE__ */ new Set();
2230
+ const start = nodes.find((n) => n.type === "start");
2231
+ if (start) {
2232
+ const queue = [start.id];
2233
+ while (queue.length) {
2234
+ const id = queue.shift();
2235
+ if (seen.has(id)) continue;
2236
+ seen.add(id);
2237
+ const node = byId.get(id);
2238
+ if (node) ordered.push(node);
2239
+ for (const target of outgoingTargets(logicByNode.get(id), node)) {
2240
+ if (!seen.has(target) && byId.has(target)) queue.push(target);
2241
+ }
2242
+ }
2243
+ }
2244
+ for (const node of nodes) if (!seen.has(node.id)) ordered.push(node);
2245
+ return ordered;
2246
+ }
2247
+
2159
2248
  // ../config/index.js
2160
2249
  var isServer = typeof window === "undefined";
2161
2250
  var isBrowser = typeof window !== "undefined";
@@ -2254,6 +2343,8 @@ var PLAN_LIMITS = {
2254
2343
  workspace_limit: 1,
2255
2344
  show_branding: true,
2256
2345
  custom_theme: false,
2346
+ dynamic_links: false,
2347
+ custom_domain: false,
2257
2348
  response_limit: 100,
2258
2349
  features: [
2259
2350
  "Up to 5 forms",
@@ -2281,13 +2372,16 @@ var PLAN_LIMITS = {
2281
2372
  workspace_limit: null,
2282
2373
  show_branding: false,
2283
2374
  custom_theme: true,
2375
+ dynamic_links: true,
2376
+ custom_domain: true,
2284
2377
  response_limit: null,
2285
2378
  features: [
2286
2379
  "Unlimited forms",
2287
2380
  "Unlimited questions",
2288
2381
  "Unlimited responses",
2289
2382
  "Custom themes & colors",
2290
- "Remove Clipform branding"
2383
+ "Remove Clipform branding",
2384
+ "Dynamic links - repoint a printed URL to a new form"
2291
2385
  ]
2292
2386
  },
2293
2387
  // Banded Pro (#1937). pro_v2_1k IS the current tier-1 version (#2671) - new
@@ -2328,13 +2422,16 @@ var PLAN_LIMITS = {
2328
2422
  workspace_limit: null,
2329
2423
  show_branding: false,
2330
2424
  custom_theme: true,
2425
+ dynamic_links: true,
2426
+ custom_domain: true,
2331
2427
  response_limit: 1e3,
2332
2428
  features: [
2333
2429
  "Unlimited forms",
2334
2430
  "Unlimited questions",
2335
2431
  "1,000 completed responses per month",
2336
2432
  "Custom themes & colors",
2337
- "Remove Clipform branding"
2433
+ "Remove Clipform branding",
2434
+ "Dynamic links - repoint a printed URL to a new form"
2338
2435
  ]
2339
2436
  },
2340
2437
  pro_v2_5k: {
@@ -2350,13 +2447,16 @@ var PLAN_LIMITS = {
2350
2447
  workspace_limit: null,
2351
2448
  show_branding: false,
2352
2449
  custom_theme: true,
2450
+ dynamic_links: true,
2451
+ custom_domain: true,
2353
2452
  response_limit: 5e3,
2354
2453
  features: [
2355
2454
  "Unlimited forms",
2356
2455
  "Unlimited questions",
2357
2456
  "5,000 completed responses per month",
2358
2457
  "Custom themes & colors",
2359
- "Remove Clipform branding"
2458
+ "Remove Clipform branding",
2459
+ "Dynamic links - repoint a printed URL to a new form"
2360
2460
  ]
2361
2461
  },
2362
2462
  pro_v2_20k: {
@@ -2372,13 +2472,16 @@ var PLAN_LIMITS = {
2372
2472
  workspace_limit: null,
2373
2473
  show_branding: false,
2374
2474
  custom_theme: true,
2475
+ dynamic_links: true,
2476
+ custom_domain: true,
2375
2477
  response_limit: 2e4,
2376
2478
  features: [
2377
2479
  "Unlimited forms",
2378
2480
  "Unlimited questions",
2379
2481
  "20,000 completed responses per month",
2380
2482
  "Custom themes & colors",
2381
- "Remove Clipform branding"
2483
+ "Remove Clipform branding",
2484
+ "Dynamic links - repoint a printed URL to a new form"
2382
2485
  ]
2383
2486
  }
2384
2487
  };
@@ -2466,6 +2569,42 @@ function exposedCompositionIds(includeNonPublic) {
2466
2569
  }
2467
2570
  var SCENE_LAYER_COMPOSITION_IDS = COMPOSITION_REGISTRY.filter((c) => c.scene_layer).map((c) => c.id);
2468
2571
  var MCP_TOOL_TIERS = {};
2572
+ var MCP_TOOL_TOOLSETS = {
2573
+ // account: workspace/account-scoped read + admin tools.
2574
+ clipform_list_forms: "account",
2575
+ clipform_get_form: "account",
2576
+ clipform_delete_form: "account",
2577
+ clipform_whoami: "account",
2578
+ clipform_get_results: "account",
2579
+ // forms: form/node structure + research tools used while building.
2580
+ clipform_create_form: "forms",
2581
+ clipform_add_node: "forms",
2582
+ clipform_update_node: "forms",
2583
+ clipform_delete_node: "forms",
2584
+ clipform_update_form: "forms",
2585
+ clipform_set_logic: "forms",
2586
+ clipform_get_guide: "forms",
2587
+ clipform_get_workflow: "forms",
2588
+ clipform_search_news: "forms",
2589
+ clipform_fetch_boundary: "forms",
2590
+ clipform_log_generation: "forms",
2591
+ // media: creative/render/asset tools.
2592
+ clipform_generate_tts: "media",
2593
+ clipform_generate_video: "media",
2594
+ clipform_render_composition: "media",
2595
+ clipform_render_video_template: "media",
2596
+ clipform_check_render: "media",
2597
+ clipform_upload_media_asset: "media",
2598
+ clipform_complete_media_upload: "media",
2599
+ clipform_attach_node_media: "media",
2600
+ clipform_get_node_media: "media",
2601
+ clipform_delete_node_media: "media",
2602
+ clipform_search_media: "media",
2603
+ clipform_search_music: "media",
2604
+ clipform_list_compositions: "media",
2605
+ clipform_list_assets: "media",
2606
+ clipform_list_video_templates: "media"
2607
+ };
2469
2608
 
2470
2609
  // src/lib/api-client.ts
2471
2610
  function getApiBaseUrl() {
@@ -2493,7 +2632,14 @@ function getAuthHeaders() {
2493
2632
  });
2494
2633
  throw new Error(message);
2495
2634
  }
2496
- return { "Authorization": `Bearer ${apiKey}` };
2635
+ const headers = { "Authorization": `Bearer ${apiKey}` };
2636
+ if (authCtx?.is_anonymous) {
2637
+ headers[MCP_ANONYMOUS_HEADER] = "1";
2638
+ }
2639
+ if (authCtx?.mcp_mint_proof) {
2640
+ headers[MCP_MINT_PROOF_HEADER] = authCtx.mcp_mint_proof;
2641
+ }
2642
+ return headers;
2497
2643
  }
2498
2644
  async function callApi(path, options = {}) {
2499
2645
  const { body, params, timeoutMs = 3e4 } = options;
@@ -2531,9 +2677,7 @@ async function callApi(path, options = {}) {
2531
2677
  return {
2532
2678
  ok: false,
2533
2679
  status: 0,
2534
- error: `Failed to connect to API at ${url}.
2535
-
2536
- Error: ${message}`
2680
+ error: "Unable to reach the Clipform API right now. Please try again in a moment."
2537
2681
  };
2538
2682
  }
2539
2683
  if (response.status === 204) {
@@ -2613,13 +2757,11 @@ async function getSessionContextWithAuth() {
2613
2757
  lines.push(
2614
2758
  apiAuthMode === "api_key" ? "Auth: API key (workspace-scoped)" : "Auth: signed in (connected to a Clipform account)"
2615
2759
  );
2616
- lines.push(`Workspace: ${me.workspace?.name ?? "Unknown"} (${me.workspace?.id ?? "?"})`);
2617
- if (me.user_id) lines.push(`User: ${me.user_id}`);
2618
- if (me.company_id) lines.push(`Company: ${me.company_id}`);
2760
+ lines.push(`Workspace: ${me.workspace?.name ?? "Unknown"}`);
2619
2761
  } else {
2620
2762
  lines.push("Auth: anonymous (not connected to a Clipform account)");
2621
2763
  if (me.workspace) {
2622
- lines.push(`Workspace: ${me.workspace.name} (${me.workspace.id})`);
2764
+ lines.push(`Workspace: ${me.workspace.name}`);
2623
2765
  }
2624
2766
  }
2625
2767
  lines.push(`Plan: ${plan.name} (tier ${plan.tier})`);
@@ -2638,7 +2780,7 @@ async function getSessionContextWithAuth() {
2638
2780
  }
2639
2781
  if (authMode === "anonymous") {
2640
2782
  lines.push(`
2641
- To unlock your full plan: sign in to your Clipform account.`);
2783
+ Anonymous session: forms are not tied to a Clipform account until you sign in.`);
2642
2784
  }
2643
2785
  return { text: lines.join("\n"), authMode };
2644
2786
  }
@@ -2658,7 +2800,8 @@ export {
2658
2800
  ALL_VARIANTS,
2659
2801
  resolveFormType,
2660
2802
  getDiscoveryParams,
2661
- BUSINESS,
2803
+ defaultTarget,
2804
+ bfsOrder,
2662
2805
  CONTACT_FIELDS,
2663
2806
  KEN_BURNS_EFFECTS,
2664
2807
  KEN_BURNS_PRESETS,
@@ -2666,6 +2809,7 @@ export {
2666
2809
  SLIDESHOW_TRANSITIONS,
2667
2810
  exposedCompositionIds,
2668
2811
  MCP_TOOL_TIERS,
2812
+ MCP_TOOL_TOOLSETS,
2669
2813
  callApi,
2670
2814
  errorResult,
2671
2815
  textResult,
@@ -2673,4 +2817,4 @@ export {
2673
2817
  getSessionContextWithAuth,
2674
2818
  getSessionContext
2675
2819
  };
2676
- //# sourceMappingURL=chunk-5LV55T7F.js.map
2820
+ //# sourceMappingURL=chunk-3BYQLPTT.js.map