@clipform/mcp-server 1.48.0 → 2.0.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.
@@ -6,7 +6,7 @@ import {
6
6
  getWorkflowText,
7
7
  objectType,
8
8
  registerPrompts
9
- } from "./chunk-BBNREY2Q.js";
9
+ } from "./chunk-PNAMVSVO.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-WUORO245.js";
17
+ } from "./chunk-DDBQDDOK.js";
18
18
  import {
19
19
  ACTIVE_NODE_TYPES,
20
20
  BUSINESS,
@@ -23,19 +23,17 @@ import {
23
23
  KEN_BURNS_EFFECTS,
24
24
  KEN_BURNS_FIT_MODES,
25
25
  KEN_BURNS_PRESETS,
26
- LABS_COMPOSITION_IDS,
27
26
  MCP_TOOL_TIERS,
28
27
  NODE_TYPES,
29
28
  NON_COUNTABLE_TYPES,
30
- PUBLIC_COMPOSITION_IDS,
31
29
  SLIDESHOW_TRANSITIONS,
32
- SOCIAL_COMPOSITION_IDS,
33
30
  callApi,
34
31
  errorResult,
32
+ exposedCompositionIds,
35
33
  resolveFormType,
36
34
  structuredResult,
37
35
  textResult
38
- } from "./chunk-4CY5EJCR.js";
36
+ } from "./chunk-ET43M6YO.js";
39
37
  import {
40
38
  __commonJS,
41
39
  __export,
@@ -16988,14 +16986,9 @@ import { fileURLToPath } from "url";
16988
16986
  var _endScreen = NODE_TYPES.end_screen.config_schema.properties;
16989
16987
  var END_SCREEN_ICONS = _endScreen.icon.enum;
16990
16988
  var END_SCREEN_CTA_TYPES = _endScreen.cta_type.enum;
16991
- var PUBLIC_COMPOSITIONS = PUBLIC_COMPOSITION_IDS;
16992
- var LABS_COMPOSITIONS = LABS_COMPOSITION_IDS;
16993
- var SOCIAL_COMPOSITIONS = SOCIAL_COMPOSITION_IDS;
16994
- var EXPOSED_COMPOSITIONS = [
16995
- ...PUBLIC_COMPOSITIONS,
16996
- ...process.env.CLIPFORM_LABS === "1" ? LABS_COMPOSITIONS : [],
16997
- ...process.env.CLIPFORM_SOCIAL === "1" ? SOCIAL_COMPOSITIONS : []
16998
- ];
16989
+ var EXPOSED_COMPOSITIONS = exposedCompositionIds(
16990
+ process.env.NODE_ENV !== "production"
16991
+ );
16999
16992
 
17000
16993
  // src/lib/schemas.ts
17001
16994
  var ACTIVE_NODE_TYPES2 = ACTIVE_NODE_TYPES;
@@ -17039,7 +17032,7 @@ function generateConfigSummary(configSchema, typeKey) {
17039
17032
  parts.push(`${key} ({${childKeys}})`);
17040
17033
  }
17041
17034
  }
17042
- if (typeKey === "contact") {
17035
+ if (typeKey === "details") {
17043
17036
  parts.push(`Available field IDs: ${CONTACT_FIELD_IDS.join(", ")}`);
17044
17037
  }
17045
17038
  return parts.length > 0 ? `Config: ${parts.join(", ")}` : "";
@@ -17073,6 +17066,12 @@ var NODE_TYPES_DESCRIPTION = (() => {
17073
17066
  })();
17074
17067
  var CONFIG_FIELD_DESCRIPTION = "Type-specific configuration keyed by node type. Omit to use defaults. Per-type keys, shapes, and defaults are listed in the clipform_create_form description; craft guidance via clipform_get_guide.";
17075
17068
  var STYLE_PRESET_ENUM = external_exports.enum(KEN_BURNS_PRESETS).describe(`Ken Burns style preset: ${KEN_BURNS_PRESETS.join(", ")}`);
17069
+ var CaptionSchema = external_exports.object({
17070
+ start: external_exports.number().describe("Segment start time in seconds"),
17071
+ end: external_exports.number().describe("Segment end time in seconds"),
17072
+ text: external_exports.string().describe("Full segment text"),
17073
+ words: external_exports.array(external_exports.object({ word: external_exports.string(), start: external_exports.number(), end: external_exports.number() })).optional().describe("Per-word timestamps within the segment")
17074
+ });
17076
17075
  var OptionSchema = external_exports.object({
17077
17076
  content: external_exports.string().describe("Option text"),
17078
17077
  score: external_exports.number().optional().describe("Score value for this option (for scored quizzes). Use 1 for correct, 0 for wrong. Scored options automatically enable correct-answer marking (record_scores) on the node."),
@@ -17132,7 +17131,7 @@ Example: A form that asks a question, collects contact info, then finishes:
17132
17131
  title: "Quick Survey",
17133
17132
  nodes: [
17134
17133
  { type: "open", prompt: "What's your biggest challenge?" },
17135
- { type: "contact", prompt: "Leave your details", config: { fields: [{ id: "first_name", required: true }, { id: "email", required: true }] } },
17134
+ { type: "details", prompt: "Leave your details", config: { fields: [{ id: "first_name", required: true }, { id: "email", required: true }] } },
17136
17135
  { type: "end_screen", prompt: "Thanks for your response!" }
17137
17136
  ]
17138
17137
  }`,
@@ -17150,7 +17149,7 @@ Example: A form that asks a question, collects contact info, then finishes:
17150
17149
  form_id: external_exports.string().describe("Form UUID - pass to follow-up tools"),
17151
17150
  viewer_url: external_exports.string().describe("Live form URL for respondents"),
17152
17151
  dashboard_url: external_exports.string().optional().describe("Builder URL (sign in to edit)"),
17153
- nodes: external_exports.array(external_exports.object({ id: external_exports.string(), type: external_exports.string(), prompt: external_exports.string() })).describe("Created nodes in order - IDs for upload_node_media / update_node"),
17152
+ nodes: external_exports.array(external_exports.object({ id: external_exports.string(), type: external_exports.string(), prompt: external_exports.string() })).describe("Created nodes in order - IDs for attach_node_media / update_node"),
17154
17153
  plan: external_exports.object({ name: external_exports.string(), auth_mode: external_exports.string() }).optional().describe("Plan and auth context for this session")
17155
17154
  },
17156
17155
  annotations: {
@@ -17258,7 +17257,7 @@ Example: A form that asks a question, collects contact info, then finishes:
17258
17257
  `Title: ${title}`,
17259
17258
  `Form ID: ${formId}`,
17260
17259
  ``,
17261
- `Node IDs (use directly with upload_node_media / update_node - no get_form round-trip needed):`,
17260
+ `Node IDs (use directly with attach_node_media / update_node - no get_form round-trip needed):`,
17262
17261
  ...createdNodes.map((n) => `- [${n.type}] "${truncate(n.prompt)}": ${n.id}`),
17263
17262
  ``,
17264
17263
  `FORM URL (live - share this with respondents): ${data.viewer_url}`
@@ -17270,7 +17269,7 @@ Example: A form that asks a question, collects contact info, then finishes:
17270
17269
  ``,
17271
17270
  `The form is live and accessible at the FORM URL above. Continue with media/narration steps if needed - changes appear immediately.`,
17272
17271
  `The URLs above are exact values; constructed or guessed URLs return 404. Use the FORM URL and DASHBOARD URL as shown.`,
17273
- `Pass form_id on follow-up tools (get_form, add_node, upload_node_media, etc.).`
17272
+ `Pass form_id on follow-up tools (get_form, add_node, attach_node_media, etc.).`
17274
17273
  );
17275
17274
  if (planContext) {
17276
17275
  const nodePart = planContext.node_limit === null ? "unlimited nodes" : `up to ${planContext.node_limit} nodes per form`;
@@ -17473,7 +17472,7 @@ function registerGetFormTool(server) {
17473
17472
  config: external_exports.record(external_exports.unknown()).optional(),
17474
17473
  options: external_exports.array(external_exports.object({ id: external_exports.string(), content: external_exports.string() })).optional()
17475
17474
  })
17476
- ).describe("Nodes in sequential order - IDs for update_node / upload_node_media")
17475
+ ).describe("Nodes in sequential order - IDs for update_node / attach_node_media")
17477
17476
  },
17478
17477
  annotations: {
17479
17478
  readOnlyHint: true,
@@ -17665,7 +17664,7 @@ function registerAddNodeTool(server) {
17665
17664
  )
17666
17665
  },
17667
17666
  outputSchema: {
17668
- node_id: external_exports.string().describe("ID of the created node - pass to update_node / upload_node_media"),
17667
+ node_id: external_exports.string().describe("ID of the created node - pass to update_node / attach_node_media"),
17669
17668
  type: external_exports.string().describe("Node type that was added")
17670
17669
  },
17671
17670
  annotations: {
@@ -17702,7 +17701,9 @@ var NodeUpdateSchema = external_exports.object({
17702
17701
  label: external_exports.string().optional().describe("Short label for the node (used in logic builder)"),
17703
17702
  type: external_exports.enum(ACTIVE_NODE_TYPES2).optional().describe("Change the node type"),
17704
17703
  required: external_exports.boolean().optional().describe("Whether an answer is required"),
17705
- config: external_exports.record(external_exports.unknown()).optional().describe(CONFIG_FIELD_DESCRIPTION),
17704
+ config: external_exports.record(external_exports.unknown()).optional().describe(
17705
+ `${CONFIG_FIELD_DESCRIPTION} Merged shallowly into the stored config - only the keys you pass are changed. Set a key to null to delete it; nested objects are replaced wholesale. When type is changed in the same call, config replaces the stored configuration instead of merging.`
17706
+ ),
17706
17707
  options: external_exports.array(OptionSchema).optional().describe(
17707
17708
  "Replace all options (for choice nodes). Omit to keep existing options."
17708
17709
  )
@@ -17814,10 +17815,8 @@ function registerDeleteNodeTool(server) {
17814
17815
  );
17815
17816
  }
17816
17817
 
17817
- // src/tools/upload-node-media.ts
17818
- var MEDIA_SUPPORTED_TYPES = Object.entries(NODE_TYPES).filter(([, def]) => def.supports_media).map(([type]) => type);
17819
- var MediaItemSchema = external_exports.object({
17820
- node_id: external_exports.string().describe("The node ID"),
17818
+ // src/tools/upload-media-asset.ts
17819
+ var MediaAssetItemSchema = external_exports.object({
17821
17820
  media_type: external_exports.enum(["video", "still"]).describe("Type of media"),
17822
17821
  media_source: external_exports.enum(["uploaded", "recorded"]).default("uploaded").describe("How the media was created"),
17823
17822
  url: external_exports.string().url().optional().describe("Public URL of the media file. When provided, the server fetches and stores it directly."),
@@ -17834,30 +17833,28 @@ var MediaItemSchema = external_exports.object({
17834
17833
  })
17835
17834
  ).optional().describe("Per-word timestamps within the segment")
17836
17835
  })
17837
- ).optional().describe("Word-level captions from clipform_generate_tts. Required for per-word highlighting - pass the full objects including 'words' arrays."),
17838
- show_captions: external_exports.boolean().optional().describe("Display captions/subtitles on the node. Defaults to on; pass false to hide them."),
17839
- fit_media: external_exports.boolean().optional().describe("Show the whole frame (contain) instead of cover-cropping. ALWAYS set true when attaching renders from clipform_render_composition or clipform_generate_video - they are composed 9:16 frames that must never be cropped. Leave unset for user-supplied media (arbitrary aspect ratios want the cover default).")
17836
+ ).optional().describe("Word-level captions from clipform_generate_tts. Required for per-word highlighting - pass the full objects including 'words' arrays.")
17840
17837
  });
17841
- function registerUploadNodeMediaTool(server) {
17838
+ function registerUploadMediaAssetTool(server) {
17842
17839
  server.registerTool(
17843
- "clipform_upload_node_media",
17840
+ "clipform_upload_media_asset",
17844
17841
  {
17845
- title: "Upload Node Media",
17846
- description: `Upload media for one or more nodes. Pass one item or many (max 10). Multiple items upload sequentially.
17842
+ title: "Upload Media Asset",
17843
+ description: `Put one or more media files into your workspace media library (max 10, uploaded sequentially). This is step one of attaching media to a node - follow up with clipform_attach_node_media to place the returned media_asset_id on a node.
17847
17844
 
17848
- When a public URL is provided, the media is fetched and stored automatically. Only works on node types that support media (${MEDIA_SUPPORTED_TYPES.join(", ")}). For video: ingested via Mux. For image: stored in Supabase. Each upload lands in your workspace media library and is then attached to the node (so it is reusable across nodes), matching the builder. Captions from clipform_generate_tts enable per-word highlighting in the viewer. When attaching renders from clipform_render_composition or clipform_generate_video, set fit_media: true on each item.`,
17845
+ When a public URL is provided, the media is fetched and stored automatically. For video: ingested via Mux. For image: stored in Supabase. Captions from clipform_generate_tts enable per-word highlighting in the viewer once attached.`,
17849
17846
  inputSchema: {
17850
- form_id: external_exports.string().uuid().describe("The form UUID (returned by clipform_create_form, not the short share_id from the URL)"),
17851
- items: external_exports.array(MediaItemSchema).min(1).max(10).describe("One or more media items to upload")
17847
+ items: external_exports.array(MediaAssetItemSchema).min(1).max(10).describe("One or more media items to create as workspace library assets")
17852
17848
  },
17853
17849
  outputSchema: {
17854
17850
  results: external_exports.array(
17855
17851
  external_exports.object({
17856
- node_id: external_exports.string(),
17857
- ok: external_exports.boolean(),
17852
+ media_asset_id: external_exports.string().optional(),
17853
+ media_type: external_exports.string(),
17858
17854
  stored: external_exports.boolean().describe("Bytes already stored (URL-based); false when an upload_url is pending"),
17859
17855
  upload_url: external_exports.string().optional(),
17860
17856
  upload_method: external_exports.string().optional(),
17857
+ ok: external_exports.boolean(),
17861
17858
  error: external_exports.string().optional()
17862
17859
  })
17863
17860
  ).describe("One result per item, in order")
@@ -17869,21 +17866,24 @@ When a public URL is provided, the media is fetched and stored automatically. On
17869
17866
  openWorldHint: false
17870
17867
  }
17871
17868
  },
17872
- async ({ form_id, items }) => {
17873
- const workspaceId = getMcpAuth()?.workspace_id;
17869
+ async ({ items }) => {
17870
+ const meResult = await callApi("/me", { method: "GET" });
17871
+ if (!meResult.ok) {
17872
+ return errorResult(`Unable to determine workspace: ${meResult.error}`);
17873
+ }
17874
+ const me = meResult.data;
17875
+ const workspaceId = me.workspace?.id ?? null;
17876
+ if (!workspaceId) {
17877
+ return errorResult(
17878
+ "No workspace available. This usually means authentication is misconfigured - check your API key or OAuth connection."
17879
+ );
17880
+ }
17874
17881
  const lines = [];
17875
17882
  const results = [];
17876
17883
  let successCount = 0;
17877
17884
  for (let i = 0; i < items.length; i++) {
17878
17885
  const item = items[i];
17879
- if (items.length > 1) lines.push(`--- Item ${i + 1} (node ${item.node_id}) ---`);
17880
- if (!workspaceId) {
17881
- const error2 = "no workspace in context - reconnect the MCP or set CLIPFORM_API_KEY so media can land in your library.";
17882
- lines.push(`FAILED: ${error2}`);
17883
- lines.push("");
17884
- results.push({ node_id: item.node_id, ok: false, stored: false, error: error2 });
17885
- continue;
17886
- }
17886
+ if (items.length > 1) lines.push(`--- Item ${i + 1} (${item.media_type}) ---`);
17887
17887
  const createBody = {
17888
17888
  media_type: item.media_type,
17889
17889
  media_source: item.media_source
@@ -17894,7 +17894,7 @@ When a public URL is provided, the media is fetched and stored automatically. On
17894
17894
  if (!created.ok) {
17895
17895
  lines.push(`FAILED: ${created.error}`);
17896
17896
  lines.push("");
17897
- results.push({ node_id: item.node_id, ok: false, stored: false, error: created.error });
17897
+ results.push({ media_type: item.media_type, stored: false, ok: false, error: created.error });
17898
17898
  continue;
17899
17899
  }
17900
17900
  const mediaAssetId = created.data.media_asset_id;
@@ -17902,17 +17902,97 @@ When a public URL is provided, the media is fetched and stored automatically. On
17902
17902
  const error2 = `upload did not return a media asset id (route ${String(created.data.route ?? "unknown")}).`;
17903
17903
  lines.push(`FAILED: ${error2}`);
17904
17904
  lines.push("");
17905
- results.push({ node_id: item.node_id, ok: false, stored: false, error: error2 });
17905
+ results.push({ media_type: item.media_type, stored: false, ok: false, error: error2 });
17906
17906
  continue;
17907
17907
  }
17908
+ successCount++;
17909
+ const uploadUrl = created.data.upload_url;
17910
+ const uploadMethod = created.data.upload_method;
17911
+ results.push({
17912
+ media_asset_id: mediaAssetId,
17913
+ media_type: item.media_type,
17914
+ ok: true,
17915
+ stored: !uploadUrl,
17916
+ ...uploadUrl ? { upload_url: uploadUrl } : {},
17917
+ ...uploadUrl && uploadMethod ? { upload_method: uploadMethod } : {}
17918
+ });
17919
+ const resultLines = [`Media asset ID: ${mediaAssetId}`];
17920
+ if (uploadUrl) {
17921
+ resultLines.push(
17922
+ `Upload URL: ${uploadUrl}`,
17923
+ `Upload method: ${uploadMethod}`,
17924
+ `Upload the file directly to the upload URL using ${uploadMethod === "tus" ? "TUS resumable upload" : "HTTP PUT"}.`
17925
+ );
17926
+ } else {
17927
+ resultLines.push(`Media stored in the workspace library.`);
17928
+ }
17929
+ if (item.captions) {
17930
+ resultLines.push(`Captions: ${item.captions.length} segments saved`);
17931
+ }
17932
+ lines.push(resultLines.join("\n"));
17933
+ lines.push("");
17934
+ }
17935
+ if (items.length > 1) {
17936
+ lines.unshift(`Media asset upload: ${successCount}/${items.length} succeeded
17937
+ `);
17938
+ }
17939
+ lines.push(`Attach these to nodes with clipform_attach_node_media.`);
17940
+ if (successCount === 0) return errorResult(lines.join("\n"));
17941
+ return structuredResult(lines.join("\n"), { results });
17942
+ }
17943
+ );
17944
+ }
17945
+
17946
+ // src/tools/attach-node-media.ts
17947
+ var MEDIA_SUPPORTED_TYPES = Object.entries(NODE_TYPES).filter(([, def]) => def.supports_media).map(([type]) => type);
17948
+ var AttachMediaItemSchema = external_exports.object({
17949
+ node_id: external_exports.string().describe("The node ID"),
17950
+ media_asset_id: external_exports.string().describe("A media asset ID returned by clipform_upload_media_asset"),
17951
+ fit_media: external_exports.boolean().optional().describe("Show the whole frame (contain) instead of cover-cropping. ALWAYS set true when attaching renders from clipform_render_composition or clipform_generate_video - they are composed 9:16 frames that must never be cropped. Leave unset for user-supplied media (arbitrary aspect ratios want the cover default)."),
17952
+ show_captions: external_exports.boolean().optional().describe("Display captions/subtitles on the node. Defaults to on; pass false to hide them.")
17953
+ });
17954
+ function registerAttachNodeMediaTool(server) {
17955
+ server.registerTool(
17956
+ "clipform_attach_node_media",
17957
+ {
17958
+ title: "Attach Node Media",
17959
+ description: `Attach an existing workspace media asset (from clipform_upload_media_asset) to one or more nodes (max 10). Pass one item or many; multiple items attach sequentially. Only works on node types that support media (${MEDIA_SUPPORTED_TYPES.join(", ")}). A media asset is reusable - attach the same media_asset_id to several nodes.`,
17960
+ inputSchema: {
17961
+ form_id: external_exports.string().uuid().describe("The form UUID (returned by clipform_create_form, not the short share_id from the URL)"),
17962
+ items: external_exports.array(AttachMediaItemSchema).min(1).max(10).describe("One or more attachments to make")
17963
+ },
17964
+ outputSchema: {
17965
+ results: external_exports.array(
17966
+ external_exports.object({
17967
+ node_id: external_exports.string(),
17968
+ ok: external_exports.boolean(),
17969
+ media_asset_id: external_exports.string().optional(),
17970
+ error: external_exports.string().optional()
17971
+ })
17972
+ ).describe("One result per item, in order")
17973
+ },
17974
+ annotations: {
17975
+ readOnlyHint: false,
17976
+ destructiveHint: false,
17977
+ idempotentHint: false,
17978
+ openWorldHint: false
17979
+ }
17980
+ },
17981
+ async ({ form_id, items }) => {
17982
+ const lines = [];
17983
+ const results = [];
17984
+ let successCount = 0;
17985
+ for (let i = 0; i < items.length; i++) {
17986
+ const item = items[i];
17987
+ if (items.length > 1) lines.push(`--- Item ${i + 1} (node ${item.node_id}) ---`);
17908
17988
  const attached = await callApi(`/forms/${form_id}/nodes/${item.node_id}/media/attach`, {
17909
17989
  method: "POST",
17910
- body: { media_asset_id: mediaAssetId }
17990
+ body: { media_asset_id: item.media_asset_id }
17911
17991
  });
17912
17992
  if (!attached.ok) {
17913
- lines.push(`FAILED to attach the uploaded media to the node: ${attached.error}`);
17993
+ lines.push(`FAILED to attach media to the node: ${attached.error}`);
17914
17994
  lines.push("");
17915
- results.push({ node_id: item.node_id, ok: false, stored: false, error: attached.error });
17995
+ results.push({ node_id: item.node_id, ok: false, error: attached.error });
17916
17996
  continue;
17917
17997
  }
17918
17998
  const presentation = {};
@@ -17929,34 +18009,14 @@ When a public URL is provided, the media is fetched and stored automatically. On
17929
18009
  }
17930
18010
  }
17931
18011
  successCount++;
17932
- const uploadUrl = created.data.upload_url;
17933
- const uploadMethod = created.data.upload_method;
17934
- results.push({
17935
- node_id: item.node_id,
17936
- ok: true,
17937
- stored: !uploadUrl,
17938
- ...uploadUrl ? { upload_url: uploadUrl } : {},
17939
- ...uploadUrl && uploadMethod ? { upload_method: uploadMethod } : {}
17940
- });
17941
- const resultLines = [];
17942
- if (created.data.upload_url) {
17943
- resultLines.push(
17944
- `Upload URL: ${created.data.upload_url}`,
17945
- `Upload method: ${created.data.upload_method}`,
17946
- `Upload the file directly to the upload URL using ${created.data.upload_method === "tus" ? "TUS resumable upload" : "HTTP PUT"}.`
17947
- );
17948
- } else {
17949
- resultLines.push(`Media stored in the workspace library and attached to the node.`);
17950
- }
17951
- if (item.captions) {
17952
- resultLines.push(`Captions: ${item.captions.length} segments saved`);
17953
- }
18012
+ results.push({ node_id: item.node_id, ok: true, media_asset_id: item.media_asset_id });
18013
+ const resultLines = [`Media attached to the node.`];
17954
18014
  if (presentationNote) resultLines.push(presentationNote);
17955
18015
  lines.push(resultLines.join("\n"));
17956
18016
  lines.push("");
17957
18017
  }
17958
18018
  if (items.length > 1) {
17959
- lines.unshift(`Media upload: ${successCount}/${items.length} succeeded
18019
+ lines.unshift(`Media attach: ${successCount}/${items.length} succeeded
17960
18020
  `);
17961
18021
  }
17962
18022
  if (successCount === 0) return errorResult(lines.join("\n"));
@@ -18268,7 +18328,7 @@ Supports any public YouTube video with captions enabled. Does NOT work for priva
18268
18328
  }
18269
18329
 
18270
18330
  // src/tools/generate-tts.ts
18271
- var CaptionSchema = external_exports.object({
18331
+ var CaptionSchema2 = external_exports.object({
18272
18332
  start: external_exports.number(),
18273
18333
  end: external_exports.number(),
18274
18334
  text: external_exports.string(),
@@ -18299,7 +18359,7 @@ Pass one item or many (max 10) - multiple items run in parallel. Returns audio U
18299
18359
  external_exports.object({
18300
18360
  ok: external_exports.boolean(),
18301
18361
  audio_url: external_exports.string().optional(),
18302
- captions: external_exports.array(CaptionSchema).optional().describe("Pass as the captions param to upload_node_media"),
18362
+ captions: external_exports.array(CaptionSchema2).optional().describe("Pass as the captions param to upload_media_asset"),
18303
18363
  error: external_exports.string().optional()
18304
18364
  })
18305
18365
  ).describe("One result per item, in order")
@@ -18365,7 +18425,7 @@ function registerSearchMediaTool(server) {
18365
18425
  "clipform_search_media",
18366
18426
  {
18367
18427
  title: "Search Media",
18368
- 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.
18428
+ 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_media_asset then clipform_attach_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.
18369
18429
 
18370
18430
  Example: { queries: [{ query: "saturn rings" }, { query: "mars surface", count: 3 }] } returns portrait images for both.`,
18371
18431
  inputSchema: {
@@ -18456,6 +18516,11 @@ Example: { queries: [{ query: "saturn rings" }, { query: "mars surface", count:
18456
18516
  );
18457
18517
  }
18458
18518
 
18519
+ // src/lib/render-routing.ts
18520
+ var RENDER_ROUTING_RULE = `Choosing a render tool: for a recognisable form/quiz beat (guess-the-city, this-or-that, mystery reveal, multiple choice, photo montage...) reach for a video template first (clipform_list_video_templates + clipform_render_video_template) - it is a one-call recipe. Use clipform_generate_video for a narrated or audio-synced media montage (images/clips timed to a voice track). Use clipform_render_composition only when neither fits and you need a custom layer stack.`;
18521
+ var MONTAGE_NARRATION_RULE = `Montage disambiguation: choose clipform_generate_video when the montage is narrated or synced to an audio track; choose the slideshow video template when it is silent (motion + transitions only, no voice-over).`;
18522
+ var RENDER_ATTACH_RULE = `A render for a form node is not done until it is attached to that node. Pass node_id (and form_id) so the completed render attaches itself automatically - do not poll clipform_check_render to completion or manually chain clipform_upload_media_asset + clipform_attach_node_media; fire the render and move on.`;
18523
+
18459
18524
  // ../../node_modules/.pnpm/uuid@11.1.1/node_modules/uuid/dist/esm/stringify.js
18460
18525
  var byteToHex = [];
18461
18526
  for (let i = 0; i < 256; ++i) {
@@ -18512,7 +18577,11 @@ var jobs = /* @__PURE__ */ new Map();
18512
18577
  var MAX_AGE_MS = 30 * 60 * 1e3;
18513
18578
  var RENDER_TIMING = {
18514
18579
  expectedRange: "15-120 seconds",
18515
- pollDelay: "~15 seconds"
18580
+ pollDelay: "~15 seconds",
18581
+ /** Rounder phrasing for chat/email ETA acks (#1964) - not exact enough for
18582
+ * a poll instruction like the two above, but right for "still rendering,
18583
+ * you'll hear back in..." copy. Never hardcode minutes in api code. */
18584
+ etaPhrase: "a couple of minutes"
18516
18585
  };
18517
18586
  function createJob(tool) {
18518
18587
  const job = {
@@ -18524,11 +18593,12 @@ function createJob(tool) {
18524
18593
  jobs.set(job.id, job);
18525
18594
  return job;
18526
18595
  }
18527
- function completeJob(id, result) {
18596
+ function completeJob(id, result, attach) {
18528
18597
  const job = jobs.get(id);
18529
18598
  if (job) {
18530
18599
  job.status = "complete";
18531
18600
  job.result = result;
18601
+ if (attach) job.attach = attach;
18532
18602
  }
18533
18603
  }
18534
18604
  function failJob(id, error2) {
@@ -18548,14 +18618,133 @@ function pruneJobs() {
18548
18618
  }
18549
18619
  }
18550
18620
 
18621
+ // src/lib/auto-attach.ts
18622
+ async function createRenderPlaceholder(input) {
18623
+ try {
18624
+ const me = await callApi("/me", { method: "GET" });
18625
+ if (!me.ok) return void 0;
18626
+ const workspaceId = me.data?.workspace?.id;
18627
+ if (!workspaceId) return void 0;
18628
+ const created = await callApi(`/workspaces/${workspaceId}/media`, {
18629
+ method: "POST",
18630
+ body: { media_type: input.mediaType, media_source: "uploaded", pending: true }
18631
+ });
18632
+ if (!created.ok) return void 0;
18633
+ const assetId = created.data?.media_asset_id;
18634
+ if (!assetId) return void 0;
18635
+ const attached = await callApi(`/forms/${input.formId}/nodes/${input.nodeId}/media/attach`, {
18636
+ method: "POST",
18637
+ body: { media_asset_id: assetId }
18638
+ });
18639
+ if (!attached.ok) return void 0;
18640
+ await callApi(`/forms/${input.formId}/nodes/${input.nodeId}/media`, {
18641
+ method: "PATCH",
18642
+ body: { fit_media: true, show_captions: true }
18643
+ }).catch(() => void 0);
18644
+ return { workspaceId, assetId };
18645
+ } catch {
18646
+ return void 0;
18647
+ }
18648
+ }
18649
+ async function failPlaceholder(placeholder, error2) {
18650
+ try {
18651
+ await callApi(`/workspaces/${placeholder.workspaceId}/media/${placeholder.assetId}/complete`, {
18652
+ method: "POST",
18653
+ body: { error: error2 }
18654
+ });
18655
+ } catch {
18656
+ }
18657
+ }
18658
+ async function autoAttachRender(input) {
18659
+ if (input.placeholder) {
18660
+ const completed = await completeViaPlaceholder(input.placeholder, input);
18661
+ if (completed) return completed;
18662
+ }
18663
+ return legacyAutoAttach(input);
18664
+ }
18665
+ async function completeViaPlaceholder(placeholder, input) {
18666
+ try {
18667
+ const body = { url: input.publicUrl };
18668
+ if (input.captions?.length) body.captions = input.captions;
18669
+ const completed = await callApi(`/workspaces/${placeholder.workspaceId}/media/${placeholder.assetId}/complete`, {
18670
+ method: "POST",
18671
+ body
18672
+ });
18673
+ if (!completed.ok) return void 0;
18674
+ let republished = false;
18675
+ const form = await callApi(`/forms/${input.formId}`, { method: "GET" });
18676
+ if (form.ok && form.data?.is_live === true) {
18677
+ const republish = await callApi(`/forms/${input.formId}`, { method: "PATCH", body: { is_live: true } });
18678
+ republished = republish.ok;
18679
+ }
18680
+ return { attached: true, media_asset_id: placeholder.assetId, republished };
18681
+ } catch {
18682
+ return void 0;
18683
+ }
18684
+ }
18685
+ async function legacyAutoAttach(input) {
18686
+ try {
18687
+ const me = await callApi("/me", { method: "GET" });
18688
+ if (!me.ok) return { attached: false, error: `Unable to determine workspace: ${me.error}` };
18689
+ const workspaceId = me.data?.workspace?.id;
18690
+ if (!workspaceId) return { attached: false, error: "No workspace available for auto-attach." };
18691
+ const createBody = {
18692
+ media_type: input.mediaType,
18693
+ media_source: "uploaded",
18694
+ url: input.publicUrl
18695
+ };
18696
+ if (input.captions?.length) createBody.captions = input.captions;
18697
+ const created = await callApi(`/workspaces/${workspaceId}/media`, { method: "POST", body: createBody });
18698
+ if (!created.ok) return { attached: false, error: `Failed to create media asset: ${created.error}` };
18699
+ const mediaAssetId = created.data?.media_asset_id;
18700
+ if (!mediaAssetId) return { attached: false, error: "Media asset creation did not return an id." };
18701
+ const attached = await callApi(`/forms/${input.formId}/nodes/${input.nodeId}/media/attach`, {
18702
+ method: "POST",
18703
+ body: { media_asset_id: mediaAssetId }
18704
+ });
18705
+ if (!attached.ok) {
18706
+ return { attached: false, media_asset_id: mediaAssetId, error: `Failed to attach to node: ${attached.error}` };
18707
+ }
18708
+ await callApi(`/forms/${input.formId}/nodes/${input.nodeId}/media`, {
18709
+ method: "PATCH",
18710
+ body: { fit_media: true, show_captions: true }
18711
+ }).catch(() => void 0);
18712
+ let republished = false;
18713
+ const form = await callApi(`/forms/${input.formId}`, { method: "GET" });
18714
+ if (form.ok && form.data?.is_live === true) {
18715
+ const republish = await callApi(`/forms/${input.formId}`, { method: "PATCH", body: { is_live: true } });
18716
+ republished = republish.ok;
18717
+ }
18718
+ return { attached: true, media_asset_id: mediaAssetId, republished };
18719
+ } catch (err) {
18720
+ return { attached: false, error: err instanceof Error ? err.message : String(err) };
18721
+ }
18722
+ }
18723
+ function attachStructuredFields(attach) {
18724
+ if (!attach) return {};
18725
+ return {
18726
+ attached: attach.attached,
18727
+ ...attach.media_asset_id ? { media_asset_id: attach.media_asset_id } : {},
18728
+ ...attach.republished !== void 0 ? { republished: attach.republished } : {},
18729
+ ...attach.error ? { attach_error: attach.error } : {}
18730
+ };
18731
+ }
18732
+ function attachSummaryLine(nodeId, attach) {
18733
+ return attach.attached ? `Attached to node ${nodeId}${attach.republished ? " and republished." : "."}` : `Auto-attach to node ${nodeId} failed: ${attach.error}. Attach manually via clipform_upload_media_asset then clipform_attach_node_media.`;
18734
+ }
18735
+
18551
18736
  // src/tools/render-composition.ts
18552
18737
  var RenderItemSchema = external_exports.object({
18553
18738
  compositionId: external_exports.string().describe("The composition ID (see clipform_list_compositions)"),
18554
18739
  outputFormat: external_exports.enum(["mp4", "png"]).default("mp4").describe("Output format (default: mp4)"),
18555
- inputProps: external_exports.record(external_exports.unknown()).optional().describe("Props matching the composition's schema - validated strictly")
18740
+ inputProps: external_exports.record(external_exports.unknown()).optional().describe("Props matching the composition's schema - validated strictly"),
18741
+ node_id: external_exports.string().optional().describe("Form node to attach this item's render to automatically once it completes. Requires the batch's top-level form_id."),
18742
+ captions: external_exports.array(CaptionSchema).optional().describe("Word-level captions from clipform_generate_tts - carried onto the attached media asset. Only used when node_id is set.")
18556
18743
  });
18557
- function fireRender(item) {
18744
+ function fireRender(item, formId) {
18558
18745
  const job = createJob("clipform_render_composition");
18746
+ const mediaType = item.outputFormat === "png" ? "still" : "video";
18747
+ const placeholderPromise = item.node_id && formId ? createRenderPlaceholder({ formId, nodeId: item.node_id, mediaType }) : Promise.resolve(void 0);
18559
18748
  callApi("/internal/render", {
18560
18749
  timeoutMs: 3e5,
18561
18750
  // local renders pay a cold webpack bundle; Lambda is fast but bursty
@@ -18564,10 +18753,39 @@ function fireRender(item) {
18564
18753
  outputFormat: item.outputFormat,
18565
18754
  inputProps: item.inputProps ?? {}
18566
18755
  }
18567
- }).then((r) => {
18568
- if (r.ok) completeJob(job.id, r.data);
18569
- else failJob(job.id, r.error);
18570
- }).catch((err) => failJob(job.id, err instanceof Error ? err.message : String(err)));
18756
+ }).then(async (r) => {
18757
+ if (!r.ok) {
18758
+ failJob(job.id, r.error);
18759
+ const placeholder = await placeholderPromise;
18760
+ if (placeholder) await failPlaceholder(placeholder, r.error);
18761
+ return;
18762
+ }
18763
+ let attach;
18764
+ if (item.node_id && formId) {
18765
+ const publicUrl = r.data.public_url;
18766
+ const placeholder = await placeholderPromise;
18767
+ if (publicUrl) {
18768
+ attach = await autoAttachRender({
18769
+ formId,
18770
+ nodeId: item.node_id,
18771
+ publicUrl,
18772
+ mediaType,
18773
+ captions: item.captions,
18774
+ placeholder
18775
+ });
18776
+ } else {
18777
+ const message = "Render completed without a public_url; nothing to attach.";
18778
+ attach = { attached: false, error: message };
18779
+ if (placeholder) await failPlaceholder(placeholder, message);
18780
+ }
18781
+ }
18782
+ completeJob(job.id, r.data, attach);
18783
+ }).catch(async (err) => {
18784
+ const message = err instanceof Error ? err.message : String(err);
18785
+ failJob(job.id, message);
18786
+ const placeholder = await placeholderPromise;
18787
+ if (placeholder) await failPlaceholder(placeholder, message);
18788
+ });
18571
18789
  return job;
18572
18790
  }
18573
18791
  function registerRenderCompositionTool(server) {
@@ -18579,44 +18797,60 @@ function registerRenderCompositionTool(server) {
18579
18797
 
18580
18798
  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.
18581
18799
 
18582
- 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).`,
18800
+ 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).
18801
+
18802
+ ${RENDER_ROUTING_RULE}
18803
+ ${RENDER_ATTACH_RULE} In batch mode, set node_id per item (see items) and form_id once at the top level.`,
18583
18804
  inputSchema: {
18584
18805
  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.`),
18585
18806
  outputFormat: external_exports.enum(["mp4", "png"]).default("mp4").describe("Single render: output format (default: mp4)"),
18586
18807
  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`."),
18587
18808
  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."),
18588
- 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.")
18809
+ 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."),
18810
+ node_id: external_exports.string().optional().describe("Single render only: form node to attach this render to automatically once it completes. Requires form_id. For batch mode, set node_id per item instead."),
18811
+ form_id: external_exports.string().uuid().optional().describe("The form UUID - required when node_id is set (single render) or any item sets node_id (batch)."),
18812
+ captions: external_exports.array(CaptionSchema).optional().describe("Single render only: word-level captions from clipform_generate_tts, carried onto the attached media asset. Only used when node_id is set. For batch mode, set captions per item instead.")
18589
18813
  },
18590
18814
  outputSchema: {
18591
18815
  jobs: external_exports.array(
18592
18816
  external_exports.object({
18593
18817
  status: external_exports.enum(["rendering", "complete"]),
18594
18818
  job_id: external_exports.string().optional().describe("Present when status is 'rendering' - pass to check_render"),
18595
- public_url: external_exports.string().optional().describe("Present when status is 'complete' - attach via upload_node_media (fit_media: true)"),
18819
+ public_url: external_exports.string().optional().describe("Present when status is 'complete' - attach via upload_media_asset then attach_node_media (fit_media: true), unless node_id was set (auto-attached)"),
18596
18820
  composition_id: external_exports.string().optional(),
18597
- output_format: external_exports.string().optional()
18821
+ output_format: external_exports.string().optional(),
18822
+ attached: external_exports.boolean().optional().describe("True when node_id was provided and the render was attached to the node automatically (present once the attach outcome is known)."),
18823
+ media_asset_id: external_exports.string().optional().describe("The workspace media asset created from this render, when attached."),
18824
+ republished: external_exports.boolean().optional().describe("True when the form was live and was republished to include this media."),
18825
+ attach_error: external_exports.string().optional().describe("Present when node_id was provided but auto-attach failed - the render itself still succeeded.")
18598
18826
  })
18599
18827
  ).describe("One entry per render (single or batch)")
18600
18828
  },
18601
18829
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }
18602
18830
  },
18603
- async ({ compositionId, outputFormat, inputProps, wait, items }) => {
18831
+ async ({ compositionId, outputFormat, inputProps, wait, items, node_id, form_id, captions }) => {
18604
18832
  if (items?.length && compositionId) {
18605
18833
  return errorResult("Pass EITHER items (batch) OR compositionId (single render), not both.");
18606
18834
  }
18607
18835
  if (!items?.length && !compositionId) {
18608
18836
  return errorResult("Pass compositionId for a single render, or items for a batch.");
18609
18837
  }
18838
+ if (node_id && !form_id) {
18839
+ return errorResult("form_id is required when node_id is set.");
18840
+ }
18841
+ if (items?.some((i) => i.node_id) && !form_id) {
18842
+ return errorResult("form_id is required when any item sets node_id.");
18843
+ }
18610
18844
  const unknown2 = (items ?? [{ compositionId, outputFormat, inputProps }]).map((i) => i.compositionId).filter((id) => !EXPOSED_COMPOSITIONS.includes(id));
18611
18845
  if (unknown2.length) {
18612
18846
  return errorResult(`Composition${unknown2.length > 1 ? "s" : ""} ${unknown2.map((id) => `"${id}"`).join(", ")} not available. Call clipform_list_compositions to see available options.`);
18613
18847
  }
18614
18848
  if (items?.length) {
18615
- const jobs2 = items.map((item) => ({ item, job: fireRender(item) }));
18849
+ const jobs2 = items.map((item) => ({ item, job: fireRender(item, form_id) }));
18616
18850
  return structuredResult(
18617
18851
  [
18618
18852
  `${jobs2.length} renders started in parallel:`,
18619
- ...jobs2.map(({ item, job }) => `- ${item.compositionId} (${item.outputFormat}): job ${job.id}`),
18853
+ ...jobs2.map(({ item, job }) => `- ${item.compositionId} (${item.outputFormat}): job ${job.id}${item.node_id ? ` -> attaches to node ${item.node_id}` : ""}`),
18620
18854
  ``,
18621
18855
  `Collect with ONE clipform_check_render call (job_ids: [...]). Renders typically take ${RENDER_TIMING.expectedRange}.`
18622
18856
  ].join("\n"),
@@ -18631,28 +18865,50 @@ For multi-render builds (e.g. one clip per quiz question), pass everything in ON
18631
18865
  );
18632
18866
  }
18633
18867
  if (wait === false) {
18634
- const job = fireRender({ compositionId, outputFormat, inputProps });
18868
+ const job = fireRender({ compositionId, outputFormat, inputProps, node_id, captions }, form_id);
18635
18869
  return structuredResult(
18636
18870
  [
18637
18871
  `Render started (${compositionId}).`,
18638
18872
  `Job ID: ${job.id}`,
18639
- `Fire any remaining renders now, then poll clipform_check_render. Renders typically take ${RENDER_TIMING.expectedRange}.`
18873
+ node_id ? `Will attach to node ${node_id} automatically once it completes - no need to poll to completion.` : `Fire any remaining renders now, then poll clipform_check_render. Renders typically take ${RENDER_TIMING.expectedRange}.`
18640
18874
  ].join("\n"),
18641
18875
  {
18642
18876
  jobs: [{ status: "rendering", job_id: job.id, composition_id: compositionId, output_format: outputFormat }]
18643
18877
  }
18644
18878
  );
18645
18879
  }
18880
+ const mediaTypeSingle = outputFormat === "png" ? "still" : "video";
18881
+ const placeholderPromise = node_id && form_id ? createRenderPlaceholder({ formId: form_id, nodeId: node_id, mediaType: mediaTypeSingle }) : Promise.resolve(void 0);
18646
18882
  const result = await callApi("/internal/render", {
18647
18883
  timeoutMs: 3e5,
18648
18884
  body: { compositionId, outputFormat, inputProps: inputProps ?? {} }
18649
18885
  });
18650
- if (!result.ok) return errorResult(result.error);
18886
+ if (!result.ok) {
18887
+ const placeholder = await placeholderPromise;
18888
+ if (placeholder) await failPlaceholder(placeholder, result.error);
18889
+ return errorResult(result.error);
18890
+ }
18651
18891
  const data = result.data;
18892
+ let attach;
18893
+ if (node_id && form_id) {
18894
+ const placeholder = await placeholderPromise;
18895
+ if (data.public_url) {
18896
+ attach = await autoAttachRender({
18897
+ formId: form_id,
18898
+ nodeId: node_id,
18899
+ publicUrl: data.public_url,
18900
+ mediaType: mediaTypeSingle,
18901
+ captions,
18902
+ placeholder
18903
+ });
18904
+ } else if (placeholder) {
18905
+ await failPlaceholder(placeholder, "Render completed without a public_url; nothing to attach.");
18906
+ }
18907
+ }
18652
18908
  return structuredResult(
18653
18909
  [
18654
18910
  `Render complete. Public URL: ${data.public_url}`,
18655
- `Attach via clipform_upload_node_media with fit_media: true (composed 9:16 frame - contain, never crop).`
18911
+ attach ? attachSummaryLine(node_id, attach) : `Attach via clipform_upload_media_asset then clipform_attach_node_media with fit_media: true (composed 9:16 frame - contain, never crop).`
18656
18912
  ].join("\n"),
18657
18913
  {
18658
18914
  jobs: [
@@ -18660,7 +18916,8 @@ For multi-render builds (e.g. one clip per quiz question), pass everything in ON
18660
18916
  status: "complete",
18661
18917
  ...data.public_url ? { public_url: data.public_url } : {},
18662
18918
  composition_id: compositionId,
18663
- output_format: outputFormat
18919
+ output_format: outputFormat,
18920
+ ...attachStructuredFields(attach)
18664
18921
  }
18665
18922
  ]
18666
18923
  }
@@ -18818,6 +19075,182 @@ function registerListCompositionsTool(server) {
18818
19075
  );
18819
19076
  }
18820
19077
 
19078
+ // src/tools/list-video-templates.ts
19079
+ function registerListVideoTemplatesTool(server) {
19080
+ server.registerTool(
19081
+ "clipform_list_video_templates",
19082
+ {
19083
+ title: "List Video Templates",
19084
+ description: `Browse available video templates - curated Scene arrangements (bed + overlay + sane defaults) that render through the Scene composition from a small controls object. Call this before clipform_render_video_template to see template names and their exposed controls. For a fully custom layer stack, assemble Scene layers directly via clipform_render_composition instead.`,
19085
+ inputSchema: {},
19086
+ outputSchema: {
19087
+ templates: external_exports.array(
19088
+ external_exports.object({
19089
+ name: external_exports.string(),
19090
+ tier: external_exports.string(),
19091
+ description: external_exports.string(),
19092
+ keywords: external_exports.array(external_exports.string()).describe("Natural-language phrases a user's intent might match against this template"),
19093
+ controls: external_exports.array(external_exports.string()).describe("Control names, '?'-suffixed when optional"),
19094
+ exampleControls: external_exports.record(external_exports.unknown()).optional().describe("A worked, ready-to-render controls object for this template - copy and adapt it")
19095
+ })
19096
+ ).describe("Available video templates - pass a name + controls to clipform_render_video_template")
19097
+ },
19098
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
19099
+ },
19100
+ async () => {
19101
+ const result = await callApi("/internal/video-templates", { method: "GET" });
19102
+ if (!result.ok) {
19103
+ if (result.status === 0 || result.status === 404) {
19104
+ return errorResult("Video templates are not available (API server may not be running). Start the API with 'npm run dev --workspace=apps/api'.");
19105
+ }
19106
+ return errorResult(result.error);
19107
+ }
19108
+ const templates = result.data.templates ?? [];
19109
+ if (!templates.length) {
19110
+ return structuredResult("No video templates found.", { templates: [] });
19111
+ }
19112
+ const lines = [`Available video templates (${templates.length}):
19113
+ `];
19114
+ for (const t of templates) {
19115
+ lines.push(`- **${t.name}** - ${t.description}`);
19116
+ lines.push(` use when: ${t.keywords.join(", ")}`);
19117
+ lines.push(` controls: ${t.controls.join(", ")}`);
19118
+ if (t.exampleControls) {
19119
+ lines.push(` example: ${JSON.stringify(t.exampleControls)}`);
19120
+ }
19121
+ }
19122
+ lines.push("");
19123
+ lines.push("Match the user's intent against each template's description/use-when keywords before picking one. Use clipform_render_video_template with a template name + controls to render (renders through Scene).");
19124
+ return structuredResult(lines.join("\n"), {
19125
+ templates: templates.map((t) => ({
19126
+ name: t.name,
19127
+ tier: t.tier,
19128
+ description: t.description,
19129
+ keywords: t.keywords,
19130
+ controls: t.controls,
19131
+ ...t.exampleControls ? { exampleControls: t.exampleControls } : {}
19132
+ }))
19133
+ });
19134
+ }
19135
+ );
19136
+ }
19137
+
19138
+ // src/tools/render-video-template.ts
19139
+ function registerRenderVideoTemplateTool(server) {
19140
+ server.registerTool(
19141
+ "clipform_render_video_template",
19142
+ {
19143
+ title: "Render Video Template",
19144
+ description: `Render a curated video template (a pre-arranged Scene: bed + overlay + sane defaults) to MP4 or PNG from a small controls object, instead of hand-assembling Scene layers. Call clipform_list_video_templates first for available template names and their controls. Renders through the Scene composition under the hood.
19145
+
19146
+ ${RENDER_ROUTING_RULE}
19147
+ ${RENDER_ATTACH_RULE}`,
19148
+ inputSchema: {
19149
+ template: external_exports.string().describe("Video template name (see clipform_list_video_templates)."),
19150
+ controls: external_exports.record(external_exports.unknown()).describe("Template-specific controls (see clipform_list_video_templates for the exposed control surface)."),
19151
+ outputFormat: external_exports.enum(["mp4", "png"]).default("mp4").describe("Output format (default: mp4)"),
19152
+ 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 for clipform_check_render."),
19153
+ node_id: external_exports.string().optional().describe("Form node to attach this render to automatically once it completes - skips the manual clipform_upload_media_asset + clipform_attach_node_media steps and republishes the form if it's currently live. Requires form_id."),
19154
+ form_id: external_exports.string().uuid().optional().describe("The form UUID (required when node_id is set)."),
19155
+ captions: external_exports.array(CaptionSchema).optional().describe("Word-level captions from clipform_generate_tts - carried onto the attached media asset. Only used when node_id is set.")
19156
+ },
19157
+ outputSchema: {
19158
+ status: external_exports.enum(["rendering", "complete"]),
19159
+ job_id: external_exports.string().optional().describe("Present when status is 'rendering' - pass to clipform_check_render"),
19160
+ public_url: external_exports.string().optional().describe("Present when status is 'complete' - attach via upload_media_asset then attach_node_media (fit_media: true), unless node_id was set (auto-attached)"),
19161
+ template: external_exports.string().optional(),
19162
+ output_format: external_exports.string().optional(),
19163
+ attached: external_exports.boolean().optional().describe("True when node_id was provided and the render was attached to the node automatically (present once the attach outcome is known)."),
19164
+ media_asset_id: external_exports.string().optional().describe("The workspace media asset created from this render, when attached."),
19165
+ republished: external_exports.boolean().optional().describe("True when the form was live and was republished to include this media."),
19166
+ attach_error: external_exports.string().optional().describe("Present when node_id was provided but auto-attach failed - the render itself still succeeded.")
19167
+ },
19168
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }
19169
+ },
19170
+ async ({ template, controls, outputFormat, wait, node_id, form_id, captions }) => {
19171
+ if (node_id && !form_id) {
19172
+ return errorResult("form_id is required when node_id is set.");
19173
+ }
19174
+ const renderBody = { template, controls: controls ?? {}, outputFormat };
19175
+ const mediaType = outputFormat === "png" ? "still" : "video";
19176
+ if (wait === false) {
19177
+ const job = createJob("clipform_render_video_template");
19178
+ const placeholderPromise2 = node_id && form_id ? createRenderPlaceholder({ formId: form_id, nodeId: node_id, mediaType }) : Promise.resolve(void 0);
19179
+ callApi("/internal/render-template", {
19180
+ timeoutMs: 3e5,
19181
+ body: renderBody
19182
+ }).then(async (r) => {
19183
+ if (!r.ok) {
19184
+ failJob(job.id, r.error);
19185
+ const placeholder = await placeholderPromise2;
19186
+ if (placeholder) await failPlaceholder(placeholder, r.error);
19187
+ return;
19188
+ }
19189
+ let attach2;
19190
+ if (node_id && form_id) {
19191
+ const publicUrl = r.data.public_url;
19192
+ const placeholder = await placeholderPromise2;
19193
+ if (publicUrl) {
19194
+ attach2 = await autoAttachRender({ formId: form_id, nodeId: node_id, publicUrl, mediaType, captions, placeholder });
19195
+ } else {
19196
+ const message = "Render completed without a public_url; nothing to attach.";
19197
+ attach2 = { attached: false, error: message };
19198
+ if (placeholder) await failPlaceholder(placeholder, message);
19199
+ }
19200
+ }
19201
+ completeJob(job.id, r.data, attach2);
19202
+ }).catch(async (err) => {
19203
+ const message = err instanceof Error ? err.message : String(err);
19204
+ failJob(job.id, message);
19205
+ const placeholder = await placeholderPromise2;
19206
+ if (placeholder) await failPlaceholder(placeholder, message);
19207
+ });
19208
+ return structuredResult(
19209
+ [
19210
+ `Render started (template: ${template}).`,
19211
+ `Job ID: ${job.id}`,
19212
+ node_id ? `Will attach to node ${node_id} automatically once it completes - no need to poll to completion.` : `Poll clipform_check_render. Renders typically take ${RENDER_TIMING.expectedRange}.`
19213
+ ].join("\n"),
19214
+ { status: "rendering", job_id: job.id, template, output_format: outputFormat }
19215
+ );
19216
+ }
19217
+ const placeholderPromise = node_id && form_id ? createRenderPlaceholder({ formId: form_id, nodeId: node_id, mediaType }) : Promise.resolve(void 0);
19218
+ const result = await callApi("/internal/render-template", {
19219
+ timeoutMs: 3e5,
19220
+ body: renderBody
19221
+ });
19222
+ if (!result.ok) {
19223
+ const placeholder = await placeholderPromise;
19224
+ if (placeholder) await failPlaceholder(placeholder, result.error);
19225
+ return errorResult(result.error);
19226
+ }
19227
+ const data = result.data;
19228
+ let attach;
19229
+ if (node_id && form_id) {
19230
+ const placeholder = await placeholderPromise;
19231
+ if (data.public_url) {
19232
+ attach = await autoAttachRender({ formId: form_id, nodeId: node_id, publicUrl: data.public_url, mediaType, captions, placeholder });
19233
+ } else if (placeholder) {
19234
+ await failPlaceholder(placeholder, "Render completed without a public_url; nothing to attach.");
19235
+ }
19236
+ }
19237
+ return structuredResult(
19238
+ [
19239
+ `Render complete. Public URL: ${data.public_url}`,
19240
+ attach ? attachSummaryLine(node_id, attach) : `Attach via clipform_upload_media_asset then clipform_attach_node_media with fit_media: true (composed 9:16 frame - contain, never crop).`
19241
+ ].join("\n"),
19242
+ {
19243
+ status: "complete",
19244
+ ...data.public_url ? { public_url: data.public_url } : {},
19245
+ template,
19246
+ output_format: outputFormat,
19247
+ ...attachStructuredFields(attach)
19248
+ }
19249
+ );
19250
+ }
19251
+ );
19252
+ }
19253
+
18821
19254
  // src/tools/list-assets.ts
18822
19255
  function registerListAssetsTool(server) {
18823
19256
  server.registerTool(
@@ -18887,7 +19320,11 @@ function registerGenerateVideoTool(server) {
18887
19320
 
18888
19321
  Items: type "image" (Ken Burns motion) or "video" (cover-cropped, muted by default). Duration matches audio_url or set duration_seconds explicitly.
18889
19322
 
18890
- 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.`,
19323
+ 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.
19324
+
19325
+ ${RENDER_ROUTING_RULE}
19326
+ ${MONTAGE_NARRATION_RULE}
19327
+ ${RENDER_ATTACH_RULE}`,
18891
19328
  inputSchema: {
18892
19329
  items: external_exports.array(
18893
19330
  external_exports.object({
@@ -18921,17 +19358,27 @@ For multi-question builds, pass wait: false on every render: each call returns a
18921
19358
  contrast: external_exports.number().positive().optional().describe("Contrast boost before mapping (default 1.1)")
18922
19359
  }).optional().describe("Two-tone editorial recolour on image items - desaturates then maps to a shadow->highlight palette. Pair with a halftone texture for a screen-print poster look."),
18923
19360
  background_color: external_exports.string().optional().describe("Background color (default '#000')"),
18924
- 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.")
19361
+ 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."),
19362
+ node_id: external_exports.string().optional().describe("Form node to attach this render to automatically once it completes - skips the manual clipform_upload_media_asset + clipform_attach_node_media steps and republishes the form if it's currently live. Requires form_id."),
19363
+ form_id: external_exports.string().uuid().optional().describe("The form UUID (required when node_id is set)."),
19364
+ captions: external_exports.array(CaptionSchema).optional().describe("Word-level captions from clipform_generate_tts - carried onto the attached media asset. Only used when node_id is set.")
18925
19365
  },
18926
19366
  outputSchema: {
18927
19367
  status: external_exports.enum(["rendering", "complete"]).describe("'rendering' when wait:false (poll check_render); 'complete' with a public_url when wait:true"),
18928
19368
  job_id: external_exports.string().optional().describe("Present when status is 'rendering' - pass to check_render"),
18929
- public_url: external_exports.string().optional().describe("Present when status is 'complete' - attach via upload_node_media (fit_media: true)"),
18930
- duration_seconds: external_exports.number().optional()
19369
+ public_url: external_exports.string().optional().describe("Present when status is 'complete' - attach via upload_media_asset then attach_node_media (fit_media: true), unless node_id was set (auto-attached)"),
19370
+ duration_seconds: external_exports.number().optional(),
19371
+ attached: external_exports.boolean().optional().describe("True when node_id was provided and the render was attached to the node automatically (present once the attach outcome is known)."),
19372
+ media_asset_id: external_exports.string().optional().describe("The workspace media asset created from this render, when attached."),
19373
+ republished: external_exports.boolean().optional().describe("True when the form was live and was republished to include this media."),
19374
+ attach_error: external_exports.string().optional().describe("Present when node_id was provided but auto-attach failed - the render itself still succeeded.")
18931
19375
  },
18932
19376
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }
18933
19377
  },
18934
- async ({ items, audio_url, background_audio_url, background_audio_volume, duration_seconds, random_effects, transition, style_preset, texture, duotone, background_color, wait }) => {
19378
+ async ({ items, audio_url, background_audio_url, background_audio_volume, duration_seconds, random_effects, transition, style_preset, texture, duotone, background_color, wait, node_id, form_id, captions }) => {
19379
+ if (node_id && !form_id) {
19380
+ return errorResult("form_id is required when node_id is set.");
19381
+ }
18935
19382
  const workspace_id = getMcpAuth()?.workspace_id;
18936
19383
  const default_style = style_preset || texture || duotone ? {
18937
19384
  ...style_preset ? { preset: style_preset } : {},
@@ -18956,33 +19403,71 @@ For multi-question builds, pass wait: false on every render: each call returns a
18956
19403
  });
18957
19404
  if (wait === false) {
18958
19405
  const job = createJob("clipform_generate_video");
18959
- apiCall().then((r) => {
18960
- if (r.ok) completeJob(job.id, r.data);
18961
- else failJob(job.id, r.error);
18962
- }).catch((err) => failJob(job.id, err instanceof Error ? err.message : String(err)));
19406
+ const placeholderPromise2 = node_id && form_id ? createRenderPlaceholder({ formId: form_id, nodeId: node_id, mediaType: "video" }) : Promise.resolve(void 0);
19407
+ apiCall().then(async (r) => {
19408
+ if (!r.ok) {
19409
+ failJob(job.id, r.error);
19410
+ const placeholder = await placeholderPromise2;
19411
+ if (placeholder) await failPlaceholder(placeholder, r.error);
19412
+ return;
19413
+ }
19414
+ let attach2;
19415
+ if (node_id && form_id) {
19416
+ const publicUrl = r.data.public_url;
19417
+ const placeholder = await placeholderPromise2;
19418
+ if (publicUrl) {
19419
+ attach2 = await autoAttachRender({ formId: form_id, nodeId: node_id, publicUrl, mediaType: "video", captions, placeholder });
19420
+ } else {
19421
+ const message = "Render completed without a public_url; nothing to attach.";
19422
+ attach2 = { attached: false, error: message };
19423
+ if (placeholder) await failPlaceholder(placeholder, message);
19424
+ }
19425
+ }
19426
+ completeJob(job.id, r.data, attach2);
19427
+ }).catch(async (err) => {
19428
+ const message = err instanceof Error ? err.message : String(err);
19429
+ failJob(job.id, message);
19430
+ const placeholder = await placeholderPromise2;
19431
+ if (placeholder) await failPlaceholder(placeholder, message);
19432
+ });
18963
19433
  return structuredResult(
18964
19434
  [
18965
19435
  `Render started (${items.length} item${items.length > 1 ? "s" : ""}).`,
18966
19436
  `Job ID: ${job.id}`,
18967
- `Fire any remaining renders now, then poll clipform_check_render. Renders typically take ${RENDER_TIMING.expectedRange}.`
19437
+ node_id ? `Will attach to node ${node_id} automatically once it completes - no need to poll to completion.` : `Fire any remaining renders now, then poll clipform_check_render. Renders typically take ${RENDER_TIMING.expectedRange}.`
18968
19438
  ].join("\n"),
18969
19439
  { status: "rendering", job_id: job.id }
18970
19440
  );
18971
19441
  }
19442
+ const placeholderPromise = node_id && form_id ? createRenderPlaceholder({ formId: form_id, nodeId: node_id, mediaType: "video" }) : Promise.resolve(void 0);
18972
19443
  const result = await apiCall();
18973
- if (!result.ok) return errorResult(result.error);
19444
+ if (!result.ok) {
19445
+ const placeholder = await placeholderPromise;
19446
+ if (placeholder) await failPlaceholder(placeholder, result.error);
19447
+ return errorResult(result.error);
19448
+ }
18974
19449
  const data = result.data;
19450
+ let attach;
19451
+ if (node_id && form_id) {
19452
+ const placeholder = await placeholderPromise;
19453
+ if (data.public_url) {
19454
+ attach = await autoAttachRender({ formId: form_id, nodeId: node_id, publicUrl: data.public_url, mediaType: "video", captions, placeholder });
19455
+ } else if (placeholder) {
19456
+ await failPlaceholder(placeholder, "Render completed without a public_url; nothing to attach.");
19457
+ }
19458
+ }
18975
19459
  return structuredResult(
18976
19460
  [
18977
19461
  `Video rendered (${items.length} item${items.length > 1 ? "s" : ""}).`,
18978
19462
  `Public URL: ${data.public_url}`,
18979
19463
  data.duration_seconds ? `Duration: ${data.duration_seconds}s` : "",
18980
- `Attach via clipform_upload_node_media with fit_media: true (composed 9:16 frame - contain, never crop).`
19464
+ attach ? attachSummaryLine(node_id, attach) : `Attach via clipform_upload_media_asset then clipform_attach_node_media with fit_media: true (composed 9:16 frame - contain, never crop).`
18981
19465
  ].filter(Boolean).join("\n"),
18982
19466
  {
18983
19467
  status: "complete",
18984
19468
  ...data.public_url ? { public_url: data.public_url } : {},
18985
- ...typeof data.duration_seconds === "number" ? { duration_seconds: data.duration_seconds } : {}
19469
+ ...typeof data.duration_seconds === "number" ? { duration_seconds: data.duration_seconds } : {},
19470
+ ...attachStructuredFields(attach)
18986
19471
  }
18987
19472
  );
18988
19473
  }
@@ -19003,7 +19488,8 @@ function projectJob(id, job) {
19003
19488
  job_id: id,
19004
19489
  status: "complete",
19005
19490
  ...data?.public_url ? { public_url: data.public_url } : {},
19006
- ...typeof data?.duration_seconds === "number" ? { duration_seconds: data.duration_seconds } : {}
19491
+ ...typeof data?.duration_seconds === "number" ? { duration_seconds: data.duration_seconds } : {},
19492
+ ...attachStructuredFields(job.attach)
19007
19493
  };
19008
19494
  }
19009
19495
  var LOST_JOB_RECOVERY = [
@@ -19028,16 +19514,17 @@ function describeJob(id, job) {
19028
19514
  ...data.public_url ? [data.public_url] : [],
19029
19515
  ...data.duration_seconds ? [`${data.duration_seconds}s`] : []
19030
19516
  ].join(" | ");
19031
- return { line: `- ${id}: complete${extras ? ` - ${extras}` : ""}`, done: true, failed: false };
19517
+ const attachNote = job.attach ? job.attach.attached ? ` (attached${job.attach.republished ? " + republished" : ""})` : ` (attach FAILED: ${job.attach.error})` : "";
19518
+ return { line: `- ${id}: complete${extras ? ` - ${extras}` : ""}${attachNote}`, done: true, failed: false };
19032
19519
  }
19033
19520
  function registerCheckRenderTool(server) {
19034
19521
  server.registerTool(
19035
19522
  "clipform_check_render",
19036
19523
  {
19037
19524
  title: "Check Render Status",
19038
- description: `Check the status of render jobs started by clipform_generate_video or clipform_render_composition.
19525
+ description: `Check the status of render jobs started by clipform_generate_video, clipform_render_video_template, or clipform_render_composition.
19039
19526
 
19040
- 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.`,
19527
+ 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. Attach is automatic when node_id was provided to the render tool - no need to poll to completion, and the attach outcome is reported here once known.`,
19041
19528
  inputSchema: {
19042
19529
  job_id: external_exports.string().uuid().optional().describe("A single job ID returned by the render tool"),
19043
19530
  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")
@@ -19050,9 +19537,13 @@ Pass job_ids to check a whole batch in ONE call - one line of status per job. Pa
19050
19537
  public_url: external_exports.string().optional(),
19051
19538
  duration_seconds: external_exports.number().optional(),
19052
19539
  elapsed_seconds: external_exports.number().optional(),
19053
- error: external_exports.string().optional()
19540
+ error: external_exports.string().optional(),
19541
+ attached: external_exports.boolean().optional().describe("True when the render's node_id was set and it attached to the node automatically."),
19542
+ media_asset_id: external_exports.string().optional().describe("The workspace media asset created from this render, when attached."),
19543
+ republished: external_exports.boolean().optional().describe("True when the form was live and was republished to include this media."),
19544
+ attach_error: external_exports.string().optional().describe("Present when node_id was set but auto-attach failed - the render itself still succeeded.")
19054
19545
  })
19055
- ).describe("One entry per queried job; attach completed public_urls via upload_node_media")
19546
+ ).describe("One entry per queried job; attach completed public_urls via upload_media_asset then attach_node_media unless already auto-attached")
19056
19547
  },
19057
19548
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
19058
19549
  },
@@ -19083,12 +19574,15 @@ Pass job_ids to check a whole batch in ONE call - one line of status per job. Pa
19083
19574
  return errorResult(`Render failed: ${job.error}`);
19084
19575
  }
19085
19576
  const data = job.result;
19577
+ const attachLines = job.attach ? [
19578
+ job.attach.attached ? `Attached to node automatically${job.attach.republished ? " and republished." : "."}` : `Auto-attach failed: ${job.attach.error}. Attach manually via clipform_upload_media_asset then clipform_attach_node_media.`
19579
+ ] : [`Attach via clipform_upload_media_asset then clipform_attach_node_media with fit_media: true (composed 9:16 frame - contain, never crop).`];
19086
19580
  return structuredResult(
19087
19581
  [
19088
19582
  `Render complete.`,
19089
19583
  ...data.public_url ? [`Public URL: ${data.public_url}`] : [],
19090
19584
  ...data.duration_seconds ? [`Duration: ${data.duration_seconds}s`] : [],
19091
- `Attach via clipform_upload_node_media with fit_media: true (composed 9:16 frame - contain, never crop).`
19585
+ ...attachLines
19092
19586
  ].join("\n"),
19093
19587
  { jobs: [projectJob(ids[0], job)] }
19094
19588
  );
@@ -19100,7 +19594,7 @@ Pass job_ids to check a whole batch in ONE call - one line of status per job. Pa
19100
19594
  [
19101
19595
  `Render batch: ${results.length - pending}/${results.length} finished${pending ? ` - check again in ${RENDER_TIMING.pollDelay}` : ""}.`,
19102
19596
  ...results.map((r) => r.line),
19103
- ...pending === 0 ? [``, `Attach completed renders via clipform_upload_node_media with fit_media: true (composed 9:16 frames - contain, never crop).`] : [],
19597
+ ...pending === 0 ? [``, `Attach any renders not already auto-attached (see above) via clipform_upload_media_asset then clipform_attach_node_media with fit_media: true (composed 9:16 frames - contain, never crop).`] : [],
19104
19598
  ...lost ? [``, LOST_JOB_RECOVERY] : []
19105
19599
  ].join("\n"),
19106
19600
  { jobs: ids.map((id) => projectJob(id, getJob(id))) }
@@ -19392,7 +19886,8 @@ function createServer(options) {
19392
19886
  registerAddNodeTool(server);
19393
19887
  registerUpdateNodeTool(server);
19394
19888
  registerDeleteNodeTool(server);
19395
- registerUploadNodeMediaTool(server);
19889
+ registerUploadMediaAssetTool(server);
19890
+ registerAttachNodeMediaTool(server);
19396
19891
  registerGetNodeMediaTool(server);
19397
19892
  registerDeleteNodeMediaTool(server);
19398
19893
  registerSetNodeLogicTool(server);
@@ -19405,6 +19900,8 @@ function createServer(options) {
19405
19900
  registerRenderCompositionTool(server);
19406
19901
  registerSearchMusicTool(server);
19407
19902
  registerListCompositionsTool(server);
19903
+ registerListVideoTemplatesTool(server);
19904
+ registerRenderVideoTemplateTool(server);
19408
19905
  registerListAssetsTool(server);
19409
19906
  registerCheckRenderTool(server);
19410
19907
  registerFetchBoundaryTool(server);
@@ -19417,6 +19914,7 @@ function createServer(options) {
19417
19914
 
19418
19915
  export {
19419
19916
  JSONRPCMessageSchema,
19917
+ RENDER_TIMING,
19420
19918
  createServer
19421
19919
  };
19422
- //# sourceMappingURL=chunk-3MTEM3VC.js.map
19920
+ //# sourceMappingURL=chunk-SI4C55U6.js.map