@clipform/mcp-server 1.32.0 → 1.33.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,14 +6,14 @@ import {
6
6
  getWorkflowText,
7
7
  objectType,
8
8
  registerPrompts
9
- } from "./chunk-ELO7KCMZ.js";
9
+ } from "./chunk-SY7YZPSC.js";
10
10
  import {
11
11
  GUIDE_TYPES,
12
12
  QUIZ_VARIANTS,
13
13
  getGuideContent,
14
14
  getGuideUri,
15
15
  registerResources
16
- } from "./chunk-B32WK2NQ.js";
16
+ } from "./chunk-JID43EDM.js";
17
17
  import {
18
18
  BUSINESS,
19
19
  CONTACT_FIELDS,
@@ -29,7 +29,7 @@ import {
29
29
  errorResult,
30
30
  resolveFormType,
31
31
  textResult
32
- } from "./chunk-NHFRYNJ3.js";
32
+ } from "./chunk-KWEBCAPF.js";
33
33
  import {
34
34
  __commonJS,
35
35
  __export,
@@ -17196,7 +17196,8 @@ Example: A form that asks a question, collects contact info, then finishes:
17196
17196
  );
17197
17197
  }
17198
17198
  planContext = {
17199
- auth_mode: me.auth_mode,
17199
+ auth_mode: me.auth_mode ?? "anonymous",
17200
+ is_authenticated: me.auth_mode != null && me.auth_mode !== "anonymous",
17200
17201
  workspace_id: workspaceId,
17201
17202
  workspace_name: me.workspace?.name ?? null,
17202
17203
  plan_name: me.plan?.name ?? "Free",
@@ -17206,7 +17207,7 @@ Example: A form that asks a question, collects contact info, then finishes:
17206
17207
  const contentCount = nodes.filter((q) => !NON_COUNTABLE_TYPES.includes(q.type)).length;
17207
17208
  if (planContext.node_limit !== null && contentCount > planContext.node_limit) {
17208
17209
  const upgradeUrl = `${BUSINESS.urls.dashboard}/billing`;
17209
- const message = planContext.auth_mode === "oauth" ? `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.`;
17210
+ 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.`;
17210
17211
  return errorResult(message);
17211
17212
  }
17212
17213
  if (planContext.form_limit !== null) {
@@ -17216,14 +17217,14 @@ Example: A form that asks a question, collects contact info, then finishes:
17216
17217
  if (forms.length >= planContext.form_limit) {
17217
17218
  const upgradeUrl = `${BUSINESS.urls.dashboard}/billing`;
17218
17219
  const formList = forms.map((f, i) => ` ${i + 1}. "${f.title || "Untitled"}" (id: ${f.id})`).join("\n");
17219
- const options = planContext.auth_mode === "oauth" ? ` - Upgrade for unlimited forms: ${upgradeUrl}
17220
+ const options = planContext.is_authenticated ? ` - Upgrade for unlimited forms: ${upgradeUrl}
17220
17221
  - Or delete one of the forms below to free a slot.` : ` - Delete one of the forms below to free a slot.
17221
17222
  - Or sign in to a Clipform account to keep these forms and get higher limits.`;
17222
17223
  return errorResult(
17223
17224
  [
17224
17225
  `LIMIT REACHED - the form was NOT created. The plan's form limit is full.`,
17225
17226
  ``,
17226
- `This ${planContext.auth_mode === "oauth" ? `workspace ('${planContext.workspace_name}')` : "anonymous session"} is on the ${planContext.plan_name} plan, which allows ${planContext.form_limit} forms, and it's already full. To make a new one:`,
17227
+ `This ${planContext.is_authenticated ? `workspace ('${planContext.workspace_name}')` : "anonymous session"} is on the ${planContext.plan_name} plan, which allows ${planContext.form_limit} forms, and it's already full. To make a new one:`,
17227
17228
  options,
17228
17229
  ``,
17229
17230
  `Current forms:`,
@@ -17304,7 +17305,7 @@ Example: A form that asks a question, collects contact info, then finishes:
17304
17305
  const nodePart = planContext.node_limit === null ? "unlimited nodes" : `up to ${planContext.node_limit} nodes per form`;
17305
17306
  const formPart = planContext.form_limit === null ? "unlimited forms" : `up to ${planContext.form_limit} forms`;
17306
17307
  const limitNote = `${formPart}, ${nodePart}`;
17307
- if (planContext.auth_mode === "oauth") {
17308
+ if (planContext.is_authenticated) {
17308
17309
  lines.push(
17309
17310
  ``,
17310
17311
  `Plan: ${planContext.plan_name} (${limitNote}) in workspace '${planContext.workspace_name}'.`
@@ -18448,6 +18449,98 @@ function registerListAssetsTool(server) {
18448
18449
  );
18449
18450
  }
18450
18451
 
18452
+ // ../../node_modules/.pnpm/uuid@11.1.1/node_modules/uuid/dist/esm/stringify.js
18453
+ var byteToHex = [];
18454
+ for (let i = 0; i < 256; ++i) {
18455
+ byteToHex.push((i + 256).toString(16).slice(1));
18456
+ }
18457
+ function unsafeStringify(arr, offset = 0) {
18458
+ return (byteToHex[arr[offset + 0]] + byteToHex[arr[offset + 1]] + byteToHex[arr[offset + 2]] + byteToHex[arr[offset + 3]] + "-" + byteToHex[arr[offset + 4]] + byteToHex[arr[offset + 5]] + "-" + byteToHex[arr[offset + 6]] + byteToHex[arr[offset + 7]] + "-" + byteToHex[arr[offset + 8]] + byteToHex[arr[offset + 9]] + "-" + byteToHex[arr[offset + 10]] + byteToHex[arr[offset + 11]] + byteToHex[arr[offset + 12]] + byteToHex[arr[offset + 13]] + byteToHex[arr[offset + 14]] + byteToHex[arr[offset + 15]]).toLowerCase();
18459
+ }
18460
+
18461
+ // ../../node_modules/.pnpm/uuid@11.1.1/node_modules/uuid/dist/esm/rng.js
18462
+ import { randomFillSync } from "crypto";
18463
+ var rnds8Pool = new Uint8Array(256);
18464
+ var poolPtr = rnds8Pool.length;
18465
+ function rng() {
18466
+ if (poolPtr > rnds8Pool.length - 16) {
18467
+ randomFillSync(rnds8Pool);
18468
+ poolPtr = 0;
18469
+ }
18470
+ return rnds8Pool.slice(poolPtr, poolPtr += 16);
18471
+ }
18472
+
18473
+ // ../../node_modules/.pnpm/uuid@11.1.1/node_modules/uuid/dist/esm/native.js
18474
+ import { randomUUID } from "crypto";
18475
+ var native_default = { randomUUID };
18476
+
18477
+ // ../../node_modules/.pnpm/uuid@11.1.1/node_modules/uuid/dist/esm/v4.js
18478
+ function v4(options, buf, offset) {
18479
+ if (native_default.randomUUID && !buf && !options) {
18480
+ return native_default.randomUUID();
18481
+ }
18482
+ options = options || {};
18483
+ const rnds = options.random ?? options.rng?.() ?? rng();
18484
+ if (rnds.length < 16) {
18485
+ throw new Error("Random bytes length must be >= 16");
18486
+ }
18487
+ rnds[6] = rnds[6] & 15 | 64;
18488
+ rnds[8] = rnds[8] & 63 | 128;
18489
+ if (buf) {
18490
+ offset = offset || 0;
18491
+ if (offset < 0 || offset + 16 > buf.length) {
18492
+ throw new RangeError(`UUID byte range ${offset}:${offset + 15} is out of buffer bounds`);
18493
+ }
18494
+ for (let i = 0; i < 16; ++i) {
18495
+ buf[offset + i] = rnds[i];
18496
+ }
18497
+ return buf;
18498
+ }
18499
+ return unsafeStringify(rnds);
18500
+ }
18501
+ var v4_default = v4;
18502
+
18503
+ // src/lib/render-jobs.ts
18504
+ var jobs = /* @__PURE__ */ new Map();
18505
+ var MAX_AGE_MS = 30 * 60 * 1e3;
18506
+ var RENDER_TIMING = {
18507
+ expectedRange: "15-120 seconds",
18508
+ pollDelay: "~15 seconds"
18509
+ };
18510
+ function createJob(tool) {
18511
+ const job = {
18512
+ id: v4_default(),
18513
+ status: "rendering",
18514
+ tool,
18515
+ createdAt: Date.now()
18516
+ };
18517
+ jobs.set(job.id, job);
18518
+ return job;
18519
+ }
18520
+ function completeJob(id, result) {
18521
+ const job = jobs.get(id);
18522
+ if (job) {
18523
+ job.status = "complete";
18524
+ job.result = result;
18525
+ }
18526
+ }
18527
+ function failJob(id, error2) {
18528
+ const job = jobs.get(id);
18529
+ if (job) {
18530
+ job.status = "failed";
18531
+ job.error = error2;
18532
+ }
18533
+ }
18534
+ function getJob(id) {
18535
+ return jobs.get(id);
18536
+ }
18537
+ function pruneJobs() {
18538
+ const cutoff = Date.now() - MAX_AGE_MS;
18539
+ for (const [id, job] of jobs) {
18540
+ if (job.createdAt < cutoff) jobs.delete(id);
18541
+ }
18542
+ }
18543
+
18451
18544
  // src/tools/generate-video.ts
18452
18545
  function registerGenerateVideoTool(server) {
18453
18546
  server.registerTool(
@@ -18456,7 +18549,9 @@ function registerGenerateVideoTool(server) {
18456
18549
  title: "Generate Video",
18457
18550
  description: `Generate a video from images, video clips, or both, synced to an audio track. Use this for narrated question backgrounds, topic visualisations, or any form node that benefits from video. Combine with clipform_generate_tts for narrated audio and clipform_search_media for royalty-free images. Creates 9:16 (720x1280) with Ken Burns pan/zoom effects and transitions. Returns a public URL when complete.
18458
18551
 
18459
- Items: type "image" (Ken Burns motion) or "video" (cover-cropped, muted by default). Duration matches audio_url or set duration_seconds explicitly.`,
18552
+ Items: type "image" (Ken Burns motion) or "video" (cover-cropped, muted by default). Duration matches audio_url or set duration_seconds explicitly.
18553
+
18554
+ For multi-question builds, pass wait: false on every render: each call returns a job ID immediately, so all renders run in parallel - then collect URLs with clipform_check_render. Sequential waiting renders take ${RENDER_TIMING.expectedRange} EACH.`,
18460
18555
  inputSchema: {
18461
18556
  items: external_exports.array(
18462
18557
  external_exports.object({
@@ -18477,13 +18572,14 @@ Items: type "image" (Ken Burns motion) or "video" (cover-cropped, muted by defau
18477
18572
  duration: external_exports.number().optional().default(1).describe("Transition duration in seconds (default: 1)")
18478
18573
  }).optional(),
18479
18574
  style_preset: STYLE_PRESET_ENUM.optional(),
18480
- background_color: external_exports.string().optional().describe("Background color (default '#000')")
18575
+ background_color: external_exports.string().optional().describe("Background color (default '#000')"),
18576
+ wait: external_exports.boolean().optional().default(true).describe("true (default) blocks until the video is ready and returns its URL. false returns a job ID immediately - fire all renders first, then poll clipform_check_render. Use false whenever rendering more than one video.")
18481
18577
  },
18482
18578
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }
18483
18579
  },
18484
- async ({ items, audio_url, duration_seconds, random_effects, transition, style_preset, background_color }) => {
18580
+ async ({ items, audio_url, duration_seconds, random_effects, transition, style_preset, background_color, wait }) => {
18485
18581
  const workspace_id = getMcpAuth()?.workspace_id;
18486
- const result = await callApi("/internal/generate-video", {
18582
+ const apiCall = () => callApi("/internal/generate-video", {
18487
18583
  timeoutMs: 3e5,
18488
18584
  // local renders pay a cold webpack bundle; Lambda is fast but bursty
18489
18585
  body: {
@@ -18497,6 +18593,21 @@ Items: type "image" (Ken Burns motion) or "video" (cover-cropped, muted by defau
18497
18593
  workspace_id
18498
18594
  }
18499
18595
  });
18596
+ if (wait === false) {
18597
+ const job = createJob("clipform_generate_video");
18598
+ apiCall().then((r) => {
18599
+ if (r.ok) completeJob(job.id, r.data);
18600
+ else failJob(job.id, r.error);
18601
+ }).catch((err) => failJob(job.id, err instanceof Error ? err.message : String(err)));
18602
+ return textResult(
18603
+ [
18604
+ `Render started (${items.length} item${items.length > 1 ? "s" : ""}).`,
18605
+ `Job ID: ${job.id}`,
18606
+ `Fire any remaining renders now, then poll clipform_check_render. Renders typically take ${RENDER_TIMING.expectedRange}.`
18607
+ ].join("\n")
18608
+ );
18609
+ }
18610
+ const result = await apiCall();
18500
18611
  if (!result.ok) return errorResult(result.error);
18501
18612
  const data = result.data;
18502
18613
  return textResult(
@@ -18510,23 +18621,6 @@ Items: type "image" (Ken Burns motion) or "video" (cover-cropped, muted by defau
18510
18621
  );
18511
18622
  }
18512
18623
 
18513
- // src/lib/render-jobs.ts
18514
- var jobs = /* @__PURE__ */ new Map();
18515
- var MAX_AGE_MS = 30 * 60 * 1e3;
18516
- var RENDER_TIMING = {
18517
- expectedRange: "15-120 seconds",
18518
- pollDelay: "~15 seconds"
18519
- };
18520
- function getJob(id) {
18521
- return jobs.get(id);
18522
- }
18523
- function pruneJobs() {
18524
- const cutoff = Date.now() - MAX_AGE_MS;
18525
- for (const [id, job] of jobs) {
18526
- if (job.createdAt < cutoff) jobs.delete(id);
18527
- }
18528
- }
18529
-
18530
18624
  // src/tools/check-render.ts
18531
18625
  function registerCheckRenderTool(server) {
18532
18626
  server.registerTool(
@@ -18879,4 +18973,4 @@ export {
18879
18973
  JSONRPCMessageSchema,
18880
18974
  createServer
18881
18975
  };
18882
- //# sourceMappingURL=chunk-HQGYGAE4.js.map
18976
+ //# sourceMappingURL=chunk-LSANXDSD.js.map