@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.
- package/README.md +5 -2
- package/dist/{chunk-WUORO245.js → chunk-DDBQDDOK.js} +3 -3
- package/dist/chunk-DDBQDDOK.js.map +1 -0
- package/dist/{chunk-4CY5EJCR.js → chunk-ET43M6YO.js} +95 -53
- package/dist/chunk-ET43M6YO.js.map +1 -0
- package/dist/{chunk-BBNREY2Q.js → chunk-PNAMVSVO.js} +14 -14
- package/dist/{chunk-BBNREY2Q.js.map → chunk-PNAMVSVO.js.map} +1 -1
- package/dist/{chunk-3MTEM3VC.js → chunk-SI4C55U6.js} +620 -122
- package/dist/chunk-SI4C55U6.js.map +1 -0
- package/dist/index.js +4 -4
- package/dist/prompts.js +2 -2
- package/dist/resources.js +2 -2
- package/dist/server.d.ts +10 -1
- package/dist/server.js +6 -4
- package/package.json +1 -1
- package/dist/chunk-3MTEM3VC.js.map +0 -1
- package/dist/chunk-4CY5EJCR.js.map +0 -1
- package/dist/chunk-WUORO245.js.map +0 -1
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
getWorkflowText,
|
|
7
7
|
objectType,
|
|
8
8
|
registerPrompts
|
|
9
|
-
} from "./chunk-
|
|
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-
|
|
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-
|
|
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
|
|
16992
|
-
|
|
16993
|
-
|
|
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 === "
|
|
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: "
|
|
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
|
|
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
|
|
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,
|
|
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 /
|
|
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 /
|
|
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(
|
|
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-
|
|
17818
|
-
var
|
|
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
|
|
17838
|
+
function registerUploadMediaAssetTool(server) {
|
|
17842
17839
|
server.registerTool(
|
|
17843
|
-
"
|
|
17840
|
+
"clipform_upload_media_asset",
|
|
17844
17841
|
{
|
|
17845
|
-
title: "Upload
|
|
17846
|
-
description: `
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
17857
|
-
|
|
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 ({
|
|
17873
|
-
const
|
|
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} (
|
|
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({
|
|
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({
|
|
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:
|
|
17990
|
+
body: { media_asset_id: item.media_asset_id }
|
|
17911
17991
|
});
|
|
17912
17992
|
if (!attached.ok) {
|
|
17913
|
-
lines.push(`FAILED to attach
|
|
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,
|
|
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
|
-
|
|
17933
|
-
const
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
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)
|
|
18569
|
-
|
|
18570
|
-
|
|
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
|
|
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)
|
|
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
|
|
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
|
|
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
|
-
|
|
18960
|
-
|
|
18961
|
-
|
|
18962
|
-
|
|
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)
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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-
|
|
19920
|
+
//# sourceMappingURL=chunk-SI4C55U6.js.map
|