@clipform/mcp-server 1.44.2 → 1.44.4

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-VTRO7ZC3.js";
9
+ } from "./chunk-6IRFVCIG.js";
10
10
  import {
11
11
  GUIDE_TYPES,
12
12
  QUIZ_VARIANTS,
@@ -18295,6 +18295,27 @@ function pruneJobs() {
18295
18295
  }
18296
18296
 
18297
18297
  // src/tools/render-composition.ts
18298
+ var RenderItemSchema = external_exports.object({
18299
+ compositionId: external_exports.string().describe("The composition ID (see clipform_list_compositions)"),
18300
+ outputFormat: external_exports.enum(["mp4", "png"]).default("mp4").describe("Output format (default: mp4)"),
18301
+ inputProps: external_exports.record(external_exports.unknown()).optional().describe("Props matching the composition's schema - validated strictly")
18302
+ });
18303
+ function fireRender(item) {
18304
+ const job = createJob("clipform_render_composition");
18305
+ callApi("/internal/render", {
18306
+ timeoutMs: 3e5,
18307
+ // local renders pay a cold webpack bundle; Lambda is fast but bursty
18308
+ body: {
18309
+ compositionId: item.compositionId,
18310
+ outputFormat: item.outputFormat,
18311
+ inputProps: item.inputProps ?? {}
18312
+ }
18313
+ }).then((r) => {
18314
+ if (r.ok) completeJob(job.id, r.data);
18315
+ else failJob(job.id, r.error);
18316
+ }).catch((err) => failJob(job.id, err instanceof Error ? err.message : String(err)));
18317
+ return job;
18318
+ }
18298
18319
  function registerRenderCompositionTool(server) {
18299
18320
  server.registerTool(
18300
18321
  "clipform_render_composition",
@@ -18304,34 +18325,40 @@ function registerRenderCompositionTool(server) {
18304
18325
 
18305
18326
  For narrated Ken Burns slideshows from images, use clipform_generate_video instead. Output formats: mp4 (H.264, best for social media) or png (single frame). Returns a public URL when complete.
18306
18327
 
18307
- For multi-render builds (e.g. composition quizzes with a clue + reveal clip per question), pass wait: false on every call: each returns a job ID immediately so renders run in parallel - then collect URLs with clipform_check_render.`,
18328
+ For multi-render builds (e.g. one clip per quiz question), pass everything in ONE call via items (max 10): all renders fire in parallel and you get one job ID each - then collect the URLs in a single clipform_check_render call with job_ids. Single render: pass compositionId/inputProps at the top level (wait: true blocks and returns the URL; wait: false returns a job ID).`,
18308
18329
  inputSchema: {
18309
- compositionId: external_exports.string().describe(`The composition ID. Call clipform_list_compositions to see available options (e.g. ${EXPOSED_COMPOSITIONS.map((id) => `'${id}'`).join(", ")})`),
18310
- outputFormat: external_exports.enum(["mp4", "png"]).default("mp4").describe("Output format (default: mp4)"),
18311
- inputProps: external_exports.record(external_exports.unknown()).optional().describe("Props object matching the composition's schema from clipform_list_compositions. Validated STRICTLY - unknown or missing props fail with the schema in the error, nothing renders silently with defaults. For map compositions (Map), wide shots (camera.zoom < 9) may round lat/lng to 1 decimal place to improve cache hit rates; close shots and pin drops should keep full precision \u2014 the pin lands exactly on `target`."),
18312
- wait: external_exports.boolean().optional().default(true).describe("true (default) blocks until the render 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 clip.")
18330
+ compositionId: external_exports.string().optional().describe(`Single render: the composition ID. Call clipform_list_compositions to see available options (e.g. ${EXPOSED_COMPOSITIONS.map((id) => `'${id}'`).join(", ")}). Use items instead for multiple renders.`),
18331
+ outputFormat: external_exports.enum(["mp4", "png"]).default("mp4").describe("Single render: output format (default: mp4)"),
18332
+ inputProps: external_exports.record(external_exports.unknown()).optional().describe("Single render: props object matching the composition's schema from clipform_list_compositions. Validated STRICTLY - unknown or missing props fail with the schema in the error, nothing renders silently with defaults. For map compositions (Map), wide shots (camera.zoom < 9) may round lat/lng to 1 decimal place to improve cache hit rates; close shots and pin drops should keep full precision \u2014 the pin lands exactly on `target`."),
18333
+ wait: external_exports.boolean().optional().default(true).describe("Single render only: true (default) blocks until the render is ready and returns its URL; false returns a job ID for clipform_check_render. Batch items always run fire-and-poll."),
18334
+ items: external_exports.array(RenderItemSchema).min(1).max(10).optional().describe("Batch mode: multiple renders in one call. All fire in parallel; returns one job ID per item - collect with clipform_check_render (job_ids). Use this whenever rendering more than one clip.")
18313
18335
  },
18314
18336
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }
18315
18337
  },
18316
- async ({ compositionId, outputFormat, inputProps, wait }) => {
18317
- if (!EXPOSED_COMPOSITIONS.includes(compositionId)) {
18318
- return errorResult(`Composition "${compositionId}" is not available. Call clipform_list_compositions to see available options.`);
18338
+ async ({ compositionId, outputFormat, inputProps, wait, items }) => {
18339
+ if (items?.length && compositionId) {
18340
+ return errorResult("Pass EITHER items (batch) OR compositionId (single render), not both.");
18341
+ }
18342
+ if (!items?.length && !compositionId) {
18343
+ return errorResult("Pass compositionId for a single render, or items for a batch.");
18344
+ }
18345
+ const unknown2 = (items ?? [{ compositionId, outputFormat, inputProps }]).map((i) => i.compositionId).filter((id) => !EXPOSED_COMPOSITIONS.includes(id));
18346
+ if (unknown2.length) {
18347
+ return errorResult(`Composition${unknown2.length > 1 ? "s" : ""} ${unknown2.map((id) => `"${id}"`).join(", ")} not available. Call clipform_list_compositions to see available options.`);
18348
+ }
18349
+ if (items?.length) {
18350
+ const jobs2 = items.map((item) => ({ item, job: fireRender(item) }));
18351
+ return textResult(
18352
+ [
18353
+ `${jobs2.length} renders started in parallel:`,
18354
+ ...jobs2.map(({ item, job }) => `- ${item.compositionId} (${item.outputFormat}): job ${job.id}`),
18355
+ ``,
18356
+ `Collect with ONE clipform_check_render call (job_ids: [...]). Renders typically take ${RENDER_TIMING.expectedRange}.`
18357
+ ].join("\n")
18358
+ );
18319
18359
  }
18320
- const apiCall = () => callApi("/internal/render", {
18321
- timeoutMs: 3e5,
18322
- // local renders pay a cold webpack bundle; Lambda is fast but bursty
18323
- body: {
18324
- compositionId,
18325
- outputFormat,
18326
- inputProps: inputProps ?? {}
18327
- }
18328
- });
18329
18360
  if (wait === false) {
18330
- const job = createJob("clipform_render_composition");
18331
- apiCall().then((r) => {
18332
- if (r.ok) completeJob(job.id, r.data);
18333
- else failJob(job.id, r.error);
18334
- }).catch((err) => failJob(job.id, err instanceof Error ? err.message : String(err)));
18361
+ const job = fireRender({ compositionId, outputFormat, inputProps });
18335
18362
  return textResult(
18336
18363
  [
18337
18364
  `Render started (${compositionId}).`,
@@ -18340,7 +18367,10 @@ For multi-render builds (e.g. composition quizzes with a clue + reveal clip per
18340
18367
  ].join("\n")
18341
18368
  );
18342
18369
  }
18343
- const result = await apiCall();
18370
+ const result = await callApi("/internal/render", {
18371
+ timeoutMs: 3e5,
18372
+ body: { compositionId, outputFormat, inputProps: inputProps ?? {} }
18373
+ });
18344
18374
  if (!result.ok) return errorResult(result.error);
18345
18375
  const data = result.data;
18346
18376
  return textResult(
@@ -18607,53 +18637,88 @@ For multi-question builds, pass wait: false on every render: each call returns a
18607
18637
  }
18608
18638
 
18609
18639
  // src/tools/check-render.ts
18640
+ var LOST_JOB_RECOVERY = [
18641
+ `Jobs expire after 30 minutes and are lost if the server restarts - the render itself may still have completed.`,
18642
+ `To recover, re-run the original render call with identical arguments:`,
18643
+ `- clipform_render_composition results are content-cached: a finished render returns its URL immediately without re-rendering.`,
18644
+ `- clipform_generate_video is not cached and will re-render.`
18645
+ ].join("\n");
18646
+ function describeJob(id, job) {
18647
+ if (!job) {
18648
+ return { line: `- ${id}: NOT FOUND`, done: true, failed: true };
18649
+ }
18650
+ if (job.status === "rendering") {
18651
+ const elapsed = Math.round((Date.now() - job.createdAt) / 1e3);
18652
+ return { line: `- ${id}: rendering (${elapsed}s elapsed)`, done: false, failed: false };
18653
+ }
18654
+ if (job.status === "failed") {
18655
+ return { line: `- ${id}: FAILED - ${job.error}`, done: true, failed: true };
18656
+ }
18657
+ const data = job.result;
18658
+ const extras = [
18659
+ ...data.public_url ? [data.public_url] : [],
18660
+ ...data.duration_seconds ? [`${data.duration_seconds}s`] : []
18661
+ ].join(" | ");
18662
+ return { line: `- ${id}: complete${extras ? ` - ${extras}` : ""}`, done: true, failed: false };
18663
+ }
18610
18664
  function registerCheckRenderTool(server) {
18611
18665
  server.registerTool(
18612
18666
  "clipform_check_render",
18613
18667
  {
18614
18668
  title: "Check Render Status",
18615
- description: `Check the status of a render job started by clipform_generate_video or clipform_render_composition.
18669
+ description: `Check the status of render jobs started by clipform_generate_video or clipform_render_composition.
18616
18670
 
18617
- Returns the current status and, when complete, the output URL. Typical render time: 10-60 seconds.`,
18671
+ Pass job_ids to check a whole batch in ONE call - one line of status per job. Pass job_id for a single job. Returns the output URL for each completed render. Typical render time: 10-60 seconds.`,
18618
18672
  inputSchema: {
18619
- job_id: external_exports.string().uuid().describe("The job ID returned by the render tool")
18673
+ job_id: external_exports.string().uuid().optional().describe("A single job ID returned by the render tool"),
18674
+ job_ids: external_exports.array(external_exports.string().uuid()).min(1).max(20).optional().describe("Multiple job IDs - check the whole batch in one call instead of one call per job")
18620
18675
  },
18621
18676
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
18622
18677
  },
18623
- async ({ job_id }) => {
18678
+ async ({ job_id, job_ids }) => {
18624
18679
  pruneJobs();
18625
- const job = getJob(job_id);
18626
- if (!job) {
18627
- return errorResult(
18628
- [
18629
- `No render job found with ID ${job_id}. Jobs expire after 30 minutes and are lost if the server restarts - the render itself may still have completed.`,
18630
- `To recover, re-run the original render call with identical arguments:`,
18631
- `- clipform_render_composition results are content-cached: a finished render returns its URL immediately without re-rendering.`,
18632
- `- clipform_generate_video is not cached and will re-render.`
18633
- ].join("\n")
18634
- );
18635
- }
18636
- if (job.status === "rendering") {
18637
- const elapsed = Math.round((Date.now() - job.createdAt) / 1e3);
18680
+ const ids = job_ids?.length ? job_ids : job_id ? [job_id] : [];
18681
+ if (!ids.length) {
18682
+ return errorResult("Pass job_id (single) or job_ids (batch).");
18683
+ }
18684
+ if (ids.length === 1) {
18685
+ const job = getJob(ids[0]);
18686
+ if (!job) {
18687
+ return errorResult(`No render job found with ID ${ids[0]}. ${LOST_JOB_RECOVERY}`);
18688
+ }
18689
+ if (job.status === "rendering") {
18690
+ const elapsed = Math.round((Date.now() - job.createdAt) / 1e3);
18691
+ return textResult(
18692
+ [
18693
+ `Status: rendering (${elapsed}s elapsed)`,
18694
+ `Tool: ${job.tool}`,
18695
+ ``,
18696
+ `Still in progress. Check again in ${RENDER_TIMING.pollDelay}.`
18697
+ ].join("\n")
18698
+ );
18699
+ }
18700
+ if (job.status === "failed") {
18701
+ return errorResult(`Render failed: ${job.error}`);
18702
+ }
18703
+ const data = job.result;
18638
18704
  return textResult(
18639
18705
  [
18640
- `Status: rendering (${elapsed}s elapsed)`,
18641
- `Tool: ${job.tool}`,
18642
- ``,
18643
- `Still in progress. Check again in ${RENDER_TIMING.pollDelay}.`
18706
+ `Render complete.`,
18707
+ ...data.public_url ? [`Public URL: ${data.public_url}`] : [],
18708
+ ...data.duration_seconds ? [`Duration: ${data.duration_seconds}s`] : [],
18709
+ `Attach via clipform_upload_node_media with fit_media: true (composed 9:16 frame - contain, never crop).`
18644
18710
  ].join("\n")
18645
18711
  );
18646
18712
  }
18647
- if (job.status === "failed") {
18648
- return errorResult(`Render failed: ${job.error}`);
18649
- }
18650
- const data = job.result;
18713
+ const results = ids.map((id) => describeJob(id, getJob(id)));
18714
+ const pending = results.filter((r) => !r.done).length;
18715
+ const lost = results.some((r) => r.line.includes("NOT FOUND"));
18651
18716
  return textResult(
18652
18717
  [
18653
- `Render complete.`,
18654
- ...data.public_url ? [`Public URL: ${data.public_url}`] : [],
18655
- ...data.duration_seconds ? [`Duration: ${data.duration_seconds}s`] : [],
18656
- `Attach via clipform_upload_node_media with fit_media: true (composed 9:16 frame - contain, never crop).`
18718
+ `Render batch: ${results.length - pending}/${results.length} finished${pending ? ` - check again in ${RENDER_TIMING.pollDelay}` : ""}.`,
18719
+ ...results.map((r) => r.line),
18720
+ ...pending === 0 ? [``, `Attach completed renders via clipform_upload_node_media with fit_media: true (composed 9:16 frames - contain, never crop).`] : [],
18721
+ ...lost ? [``, LOST_JOB_RECOVERY] : []
18657
18722
  ].join("\n")
18658
18723
  );
18659
18724
  }
@@ -18970,4 +19035,4 @@ export {
18970
19035
  JSONRPCMessageSchema,
18971
19036
  createServer
18972
19037
  };
18973
- //# sourceMappingURL=chunk-MIH452WS.js.map
19038
+ //# sourceMappingURL=chunk-MSXDS22S.js.map