@clipform/mcp-server 1.44.3 → 1.45.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,7 +6,7 @@ import {
6
6
  getWorkflowText,
7
7
  objectType,
8
8
  registerPrompts
9
- } from "./chunk-VTRO7ZC3.js";
9
+ } from "./chunk-EIQ2JGZE.js";
10
10
  import {
11
11
  GUIDE_TYPES,
12
12
  QUIZ_VARIANTS,
@@ -14,7 +14,7 @@ import {
14
14
  getGuideUri,
15
15
  guideFallbackText,
16
16
  registerResources
17
- } from "./chunk-GAAMMQM3.js";
17
+ } from "./chunk-VCUBY3Y4.js";
18
18
  import {
19
19
  BUSINESS,
20
20
  CONTACT_FIELDS,
@@ -33,7 +33,7 @@ import {
33
33
  errorResult,
34
34
  resolveFormType,
35
35
  textResult
36
- } from "./chunk-PCCTN3T7.js";
36
+ } from "./chunk-2DQUET75.js";
37
37
  import {
38
38
  __commonJS,
39
39
  __export,
@@ -17292,7 +17292,11 @@ Example: A form that asks a question, collects contact info, then finishes:
17292
17292
  form_id: formId,
17293
17293
  viewer_url: data.viewer_url,
17294
17294
  ...formUrl ? { dashboard_url: formUrl } : {},
17295
- nodes: createdNodes,
17295
+ // Project to just the fields the agent needs - the API node rows may
17296
+ // carry created_at/config/etc., and a cast doesn't strip them at
17297
+ // runtime. structuredContent should return only relevant data (ChatGPT
17298
+ // App Directory response-hygiene policy, #797).
17299
+ nodes: createdNodes.map((n) => ({ id: n.id, type: n.type, prompt: n.prompt })),
17296
17300
  ...planContext ? { plan: { name: planContext.plan_name, auth_mode: planContext.auth_mode } } : {}
17297
17301
  }
17298
17302
  };
@@ -18295,6 +18299,27 @@ function pruneJobs() {
18295
18299
  }
18296
18300
 
18297
18301
  // src/tools/render-composition.ts
18302
+ var RenderItemSchema = external_exports.object({
18303
+ compositionId: external_exports.string().describe("The composition ID (see clipform_list_compositions)"),
18304
+ outputFormat: external_exports.enum(["mp4", "png"]).default("mp4").describe("Output format (default: mp4)"),
18305
+ inputProps: external_exports.record(external_exports.unknown()).optional().describe("Props matching the composition's schema - validated strictly")
18306
+ });
18307
+ function fireRender(item) {
18308
+ const job = createJob("clipform_render_composition");
18309
+ callApi("/internal/render", {
18310
+ timeoutMs: 3e5,
18311
+ // local renders pay a cold webpack bundle; Lambda is fast but bursty
18312
+ body: {
18313
+ compositionId: item.compositionId,
18314
+ outputFormat: item.outputFormat,
18315
+ inputProps: item.inputProps ?? {}
18316
+ }
18317
+ }).then((r) => {
18318
+ if (r.ok) completeJob(job.id, r.data);
18319
+ else failJob(job.id, r.error);
18320
+ }).catch((err) => failJob(job.id, err instanceof Error ? err.message : String(err)));
18321
+ return job;
18322
+ }
18298
18323
  function registerRenderCompositionTool(server) {
18299
18324
  server.registerTool(
18300
18325
  "clipform_render_composition",
@@ -18304,34 +18329,40 @@ function registerRenderCompositionTool(server) {
18304
18329
 
18305
18330
  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
18331
 
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.`,
18332
+ 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
18333
  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.")
18334
+ 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.`),
18335
+ outputFormat: external_exports.enum(["mp4", "png"]).default("mp4").describe("Single render: output format (default: mp4)"),
18336
+ 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`."),
18337
+ 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."),
18338
+ 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
18339
  },
18314
18340
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }
18315
18341
  },
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.`);
18342
+ async ({ compositionId, outputFormat, inputProps, wait, items }) => {
18343
+ if (items?.length && compositionId) {
18344
+ return errorResult("Pass EITHER items (batch) OR compositionId (single render), not both.");
18345
+ }
18346
+ if (!items?.length && !compositionId) {
18347
+ return errorResult("Pass compositionId for a single render, or items for a batch.");
18348
+ }
18349
+ const unknown2 = (items ?? [{ compositionId, outputFormat, inputProps }]).map((i) => i.compositionId).filter((id) => !EXPOSED_COMPOSITIONS.includes(id));
18350
+ if (unknown2.length) {
18351
+ return errorResult(`Composition${unknown2.length > 1 ? "s" : ""} ${unknown2.map((id) => `"${id}"`).join(", ")} not available. Call clipform_list_compositions to see available options.`);
18352
+ }
18353
+ if (items?.length) {
18354
+ const jobs2 = items.map((item) => ({ item, job: fireRender(item) }));
18355
+ return textResult(
18356
+ [
18357
+ `${jobs2.length} renders started in parallel:`,
18358
+ ...jobs2.map(({ item, job }) => `- ${item.compositionId} (${item.outputFormat}): job ${job.id}`),
18359
+ ``,
18360
+ `Collect with ONE clipform_check_render call (job_ids: [...]). Renders typically take ${RENDER_TIMING.expectedRange}.`
18361
+ ].join("\n")
18362
+ );
18319
18363
  }
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
18364
  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)));
18365
+ const job = fireRender({ compositionId, outputFormat, inputProps });
18335
18366
  return textResult(
18336
18367
  [
18337
18368
  `Render started (${compositionId}).`,
@@ -18340,7 +18371,10 @@ For multi-render builds (e.g. composition quizzes with a clue + reveal clip per
18340
18371
  ].join("\n")
18341
18372
  );
18342
18373
  }
18343
- const result = await apiCall();
18374
+ const result = await callApi("/internal/render", {
18375
+ timeoutMs: 3e5,
18376
+ body: { compositionId, outputFormat, inputProps: inputProps ?? {} }
18377
+ });
18344
18378
  if (!result.ok) return errorResult(result.error);
18345
18379
  const data = result.data;
18346
18380
  return textResult(
@@ -18607,53 +18641,88 @@ For multi-question builds, pass wait: false on every render: each call returns a
18607
18641
  }
18608
18642
 
18609
18643
  // src/tools/check-render.ts
18644
+ var LOST_JOB_RECOVERY = [
18645
+ `Jobs expire after 30 minutes and are lost if the server restarts - the render itself may still have completed.`,
18646
+ `To recover, re-run the original render call with identical arguments:`,
18647
+ `- clipform_render_composition results are content-cached: a finished render returns its URL immediately without re-rendering.`,
18648
+ `- clipform_generate_video is not cached and will re-render.`
18649
+ ].join("\n");
18650
+ function describeJob(id, job) {
18651
+ if (!job) {
18652
+ return { line: `- ${id}: NOT FOUND`, done: true, failed: true };
18653
+ }
18654
+ if (job.status === "rendering") {
18655
+ const elapsed = Math.round((Date.now() - job.createdAt) / 1e3);
18656
+ return { line: `- ${id}: rendering (${elapsed}s elapsed)`, done: false, failed: false };
18657
+ }
18658
+ if (job.status === "failed") {
18659
+ return { line: `- ${id}: FAILED - ${job.error}`, done: true, failed: true };
18660
+ }
18661
+ const data = job.result;
18662
+ const extras = [
18663
+ ...data.public_url ? [data.public_url] : [],
18664
+ ...data.duration_seconds ? [`${data.duration_seconds}s`] : []
18665
+ ].join(" | ");
18666
+ return { line: `- ${id}: complete${extras ? ` - ${extras}` : ""}`, done: true, failed: false };
18667
+ }
18610
18668
  function registerCheckRenderTool(server) {
18611
18669
  server.registerTool(
18612
18670
  "clipform_check_render",
18613
18671
  {
18614
18672
  title: "Check Render Status",
18615
- description: `Check the status of a render job started by clipform_generate_video or clipform_render_composition.
18673
+ description: `Check the status of render jobs started by clipform_generate_video or clipform_render_composition.
18616
18674
 
18617
- Returns the current status and, when complete, the output URL. Typical render time: 10-60 seconds.`,
18675
+ 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
18676
  inputSchema: {
18619
- job_id: external_exports.string().uuid().describe("The job ID returned by the render tool")
18677
+ job_id: external_exports.string().uuid().optional().describe("A single job ID returned by the render tool"),
18678
+ 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
18679
  },
18621
18680
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
18622
18681
  },
18623
- async ({ job_id }) => {
18682
+ async ({ job_id, job_ids }) => {
18624
18683
  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);
18684
+ const ids = job_ids?.length ? job_ids : job_id ? [job_id] : [];
18685
+ if (!ids.length) {
18686
+ return errorResult("Pass job_id (single) or job_ids (batch).");
18687
+ }
18688
+ if (ids.length === 1) {
18689
+ const job = getJob(ids[0]);
18690
+ if (!job) {
18691
+ return errorResult(`No render job found with ID ${ids[0]}. ${LOST_JOB_RECOVERY}`);
18692
+ }
18693
+ if (job.status === "rendering") {
18694
+ const elapsed = Math.round((Date.now() - job.createdAt) / 1e3);
18695
+ return textResult(
18696
+ [
18697
+ `Status: rendering (${elapsed}s elapsed)`,
18698
+ `Tool: ${job.tool}`,
18699
+ ``,
18700
+ `Still in progress. Check again in ${RENDER_TIMING.pollDelay}.`
18701
+ ].join("\n")
18702
+ );
18703
+ }
18704
+ if (job.status === "failed") {
18705
+ return errorResult(`Render failed: ${job.error}`);
18706
+ }
18707
+ const data = job.result;
18638
18708
  return textResult(
18639
18709
  [
18640
- `Status: rendering (${elapsed}s elapsed)`,
18641
- `Tool: ${job.tool}`,
18642
- ``,
18643
- `Still in progress. Check again in ${RENDER_TIMING.pollDelay}.`
18710
+ `Render complete.`,
18711
+ ...data.public_url ? [`Public URL: ${data.public_url}`] : [],
18712
+ ...data.duration_seconds ? [`Duration: ${data.duration_seconds}s`] : [],
18713
+ `Attach via clipform_upload_node_media with fit_media: true (composed 9:16 frame - contain, never crop).`
18644
18714
  ].join("\n")
18645
18715
  );
18646
18716
  }
18647
- if (job.status === "failed") {
18648
- return errorResult(`Render failed: ${job.error}`);
18649
- }
18650
- const data = job.result;
18717
+ const results = ids.map((id) => describeJob(id, getJob(id)));
18718
+ const pending = results.filter((r) => !r.done).length;
18719
+ const lost = results.some((r) => r.line.includes("NOT FOUND"));
18651
18720
  return textResult(
18652
18721
  [
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).`
18722
+ `Render batch: ${results.length - pending}/${results.length} finished${pending ? ` - check again in ${RENDER_TIMING.pollDelay}` : ""}.`,
18723
+ ...results.map((r) => r.line),
18724
+ ...pending === 0 ? [``, `Attach completed renders via clipform_upload_node_media with fit_media: true (composed 9:16 frames - contain, never crop).`] : [],
18725
+ ...lost ? [``, LOST_JOB_RECOVERY] : []
18657
18726
  ].join("\n")
18658
18727
  );
18659
18728
  }
@@ -18970,4 +19039,4 @@ export {
18970
19039
  JSONRPCMessageSchema,
18971
19040
  createServer
18972
19041
  };
18973
- //# sourceMappingURL=chunk-MIH452WS.js.map
19042
+ //# sourceMappingURL=chunk-AGX5BFWD.js.map