@clipform/mcp-server 1.31.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-J4KJN4C4.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}'.`
@@ -18211,13 +18212,16 @@ function registerSearchMediaTool(server) {
18211
18212
  "clipform_search_media",
18212
18213
  {
18213
18214
  title: "Search Media",
18214
- description: `Search images or stock video clips. Pass one query or many (max 10) - multiple queries run in one call instead of separate tool calls. Use results to feed into clipform_generate_video for narrated slideshow videos, or upload directly as still images via clipform_upload_node_media. All results are pre-cleared for commercial use.`,
18215
+ description: `Search images or stock video clips. Pass one query or many (max 10) - multiple queries run in one call instead of separate tool calls. Use results to feed into clipform_generate_video for narrated slideshow videos, or upload directly as still images via clipform_upload_node_media. All results are pre-cleared for commercial use. Results include a description (alt text where the provider has it) - use it to pick visually distinct images.
18216
+
18217
+ Example: { queries: [{ query: "saturn rings" }, { query: "mars surface", count: 3 }] } returns portrait images for both.`,
18215
18218
  inputSchema: {
18216
18219
  queries: external_exports.array(
18217
18220
  external_exports.object({
18218
18221
  query: external_exports.string().describe("What to search for (e.g. 'african lion', 'saturn rings')"),
18219
- kind: external_exports.enum(["image", "video"]).describe("image = stock photos, video = stock clips"),
18220
- count: external_exports.number().min(1).max(20).default(6).optional().describe("Max results per provider (default 6 for image, 3 for video)")
18222
+ kind: external_exports.enum(["image", "video"]).default("image").optional().describe("image = stock photos (default), video = stock clips"),
18223
+ count: external_exports.number().min(1).max(20).default(6).optional().describe("Max results per provider (default 6 for image, 3 for video)"),
18224
+ orientation: external_exports.enum(["portrait", "landscape", "any"]).default("portrait").optional().describe("portrait (default) fills the 9:16 frame; landscape gets auto blur-pad framing in Ken Burns videos")
18221
18225
  })
18222
18226
  ).min(1).max(10).describe("One or more search queries to run")
18223
18227
  },
@@ -18225,9 +18229,11 @@ function registerSearchMediaTool(server) {
18225
18229
  },
18226
18230
  async ({ queries }) => {
18227
18231
  const allLines = [];
18228
- for (const { query, kind, count } of queries) {
18232
+ for (const q of queries) {
18233
+ const { query, count, orientation } = q;
18234
+ const kind = q.kind ?? "image";
18229
18235
  const result = await callApi("/internal/search-media", {
18230
- body: { query, kind, count }
18236
+ body: { query, kind, count, orientation }
18231
18237
  });
18232
18238
  if (queries.length > 1) allLines.push(`--- "${query}" (${kind}) ---`);
18233
18239
  if (!result.ok) {
@@ -18244,8 +18250,15 @@ function registerSearchMediaTool(server) {
18244
18250
  for (const item of results.slice(0, 10)) {
18245
18251
  allLines.push(`- ${item.title}`);
18246
18252
  allLines.push(` URL: ${item.url}`);
18247
- if (item.width && item.height) allLines.push(` Size: ${item.width}x${item.height}`);
18248
- if (kind === "video" && item.duration) allLines.push(` Duration: ${item.duration}s`);
18253
+ const meta = [];
18254
+ if (item.width && item.height) {
18255
+ const ar = item.width / item.height;
18256
+ meta.push(`${item.width}x${item.height} (${ar > 1.1 ? "landscape" : "portrait"})`);
18257
+ }
18258
+ if (kind === "video" && item.duration) meta.push(`${item.duration}s`);
18259
+ if (item.source) meta.push(item.source);
18260
+ if (meta.length) allLines.push(` ${meta.join(" | ")}`);
18261
+ if (item.attribution) allLines.push(` Attribution: ${item.attribution}`);
18249
18262
  allLines.push("");
18250
18263
  }
18251
18264
  }
@@ -18436,6 +18449,98 @@ function registerListAssetsTool(server) {
18436
18449
  );
18437
18450
  }
18438
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
+
18439
18544
  // src/tools/generate-video.ts
18440
18545
  function registerGenerateVideoTool(server) {
18441
18546
  server.registerTool(
@@ -18444,7 +18549,9 @@ function registerGenerateVideoTool(server) {
18444
18549
  title: "Generate Video",
18445
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.
18446
18551
 
18447
- 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.`,
18448
18555
  inputSchema: {
18449
18556
  items: external_exports.array(
18450
18557
  external_exports.object({
@@ -18465,13 +18572,14 @@ Items: type "image" (Ken Burns motion) or "video" (cover-cropped, muted by defau
18465
18572
  duration: external_exports.number().optional().default(1).describe("Transition duration in seconds (default: 1)")
18466
18573
  }).optional(),
18467
18574
  style_preset: STYLE_PRESET_ENUM.optional(),
18468
- 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.")
18469
18577
  },
18470
18578
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }
18471
18579
  },
18472
- 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 }) => {
18473
18581
  const workspace_id = getMcpAuth()?.workspace_id;
18474
- const result = await callApi("/internal/generate-video", {
18582
+ const apiCall = () => callApi("/internal/generate-video", {
18475
18583
  timeoutMs: 3e5,
18476
18584
  // local renders pay a cold webpack bundle; Lambda is fast but bursty
18477
18585
  body: {
@@ -18485,6 +18593,21 @@ Items: type "image" (Ken Burns motion) or "video" (cover-cropped, muted by defau
18485
18593
  workspace_id
18486
18594
  }
18487
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();
18488
18611
  if (!result.ok) return errorResult(result.error);
18489
18612
  const data = result.data;
18490
18613
  return textResult(
@@ -18498,23 +18621,6 @@ Items: type "image" (Ken Burns motion) or "video" (cover-cropped, muted by defau
18498
18621
  );
18499
18622
  }
18500
18623
 
18501
- // src/lib/render-jobs.ts
18502
- var jobs = /* @__PURE__ */ new Map();
18503
- var MAX_AGE_MS = 30 * 60 * 1e3;
18504
- var RENDER_TIMING = {
18505
- expectedRange: "15-120 seconds",
18506
- pollDelay: "~15 seconds"
18507
- };
18508
- function getJob(id) {
18509
- return jobs.get(id);
18510
- }
18511
- function pruneJobs() {
18512
- const cutoff = Date.now() - MAX_AGE_MS;
18513
- for (const [id, job] of jobs) {
18514
- if (job.createdAt < cutoff) jobs.delete(id);
18515
- }
18516
- }
18517
-
18518
18624
  // src/tools/check-render.ts
18519
18625
  function registerCheckRenderTool(server) {
18520
18626
  server.registerTool(
@@ -18867,4 +18973,4 @@ export {
18867
18973
  JSONRPCMessageSchema,
18868
18974
  createServer
18869
18975
  };
18870
- //# sourceMappingURL=chunk-46RSQK6L.js.map
18976
+ //# sourceMappingURL=chunk-LSANXDSD.js.map