@clipform/mcp-server 1.47.1 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -2
- package/dist/{chunk-MV27JVSL.js → chunk-6U3CVTLZ.js} +14 -14
- package/dist/{chunk-MV27JVSL.js.map → chunk-6U3CVTLZ.js.map} +1 -1
- package/dist/{chunk-UH77CDNO.js → chunk-AP6UGWKC.js} +2 -2
- package/dist/{chunk-PJNOCHKR.js → chunk-GDUJLYR4.js} +90 -51
- package/dist/chunk-GDUJLYR4.js.map +1 -0
- package/dist/{chunk-JT7YWEQP.js → chunk-RX7V2UCM.js} +970 -133
- package/dist/chunk-RX7V2UCM.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-JT7YWEQP.js.map +0 -1
- package/dist/chunk-PJNOCHKR.js.map +0 -1
- /package/dist/{chunk-UH77CDNO.js.map → chunk-AP6UGWKC.js.map} +0 -0
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
getWorkflowText,
|
|
7
7
|
objectType,
|
|
8
8
|
registerPrompts
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-6U3CVTLZ.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-AP6UGWKC.js";
|
|
18
18
|
import {
|
|
19
19
|
ACTIVE_NODE_TYPES,
|
|
20
20
|
BUSINESS,
|
|
@@ -23,18 +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,
|
|
34
|
+
structuredResult,
|
|
36
35
|
textResult
|
|
37
|
-
} from "./chunk-
|
|
36
|
+
} from "./chunk-GDUJLYR4.js";
|
|
38
37
|
import {
|
|
39
38
|
__commonJS,
|
|
40
39
|
__export,
|
|
@@ -16987,14 +16986,9 @@ import { fileURLToPath } from "url";
|
|
|
16987
16986
|
var _endScreen = NODE_TYPES.end_screen.config_schema.properties;
|
|
16988
16987
|
var END_SCREEN_ICONS = _endScreen.icon.enum;
|
|
16989
16988
|
var END_SCREEN_CTA_TYPES = _endScreen.cta_type.enum;
|
|
16990
|
-
var
|
|
16991
|
-
|
|
16992
|
-
|
|
16993
|
-
var EXPOSED_COMPOSITIONS = [
|
|
16994
|
-
...PUBLIC_COMPOSITIONS,
|
|
16995
|
-
...process.env.CLIPFORM_LABS === "1" ? LABS_COMPOSITIONS : [],
|
|
16996
|
-
...process.env.CLIPFORM_SOCIAL === "1" ? SOCIAL_COMPOSITIONS : []
|
|
16997
|
-
];
|
|
16989
|
+
var EXPOSED_COMPOSITIONS = exposedCompositionIds(
|
|
16990
|
+
process.env.NODE_ENV !== "production"
|
|
16991
|
+
);
|
|
16998
16992
|
|
|
16999
16993
|
// src/lib/schemas.ts
|
|
17000
16994
|
var ACTIVE_NODE_TYPES2 = ACTIVE_NODE_TYPES;
|
|
@@ -17038,7 +17032,7 @@ function generateConfigSummary(configSchema, typeKey) {
|
|
|
17038
17032
|
parts.push(`${key} ({${childKeys}})`);
|
|
17039
17033
|
}
|
|
17040
17034
|
}
|
|
17041
|
-
if (typeKey === "
|
|
17035
|
+
if (typeKey === "details") {
|
|
17042
17036
|
parts.push(`Available field IDs: ${CONTACT_FIELD_IDS.join(", ")}`);
|
|
17043
17037
|
}
|
|
17044
17038
|
return parts.length > 0 ? `Config: ${parts.join(", ")}` : "";
|
|
@@ -17072,6 +17066,12 @@ var NODE_TYPES_DESCRIPTION = (() => {
|
|
|
17072
17066
|
})();
|
|
17073
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.";
|
|
17074
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
|
+
});
|
|
17075
17075
|
var OptionSchema = external_exports.object({
|
|
17076
17076
|
content: external_exports.string().describe("Option text"),
|
|
17077
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."),
|
|
@@ -17131,7 +17131,7 @@ Example: A form that asks a question, collects contact info, then finishes:
|
|
|
17131
17131
|
title: "Quick Survey",
|
|
17132
17132
|
nodes: [
|
|
17133
17133
|
{ type: "open", prompt: "What's your biggest challenge?" },
|
|
17134
|
-
{ type: "
|
|
17134
|
+
{ type: "details", prompt: "Leave your details", config: { fields: [{ id: "first_name", required: true }, { id: "email", required: true }] } },
|
|
17135
17135
|
{ type: "end_screen", prompt: "Thanks for your response!" }
|
|
17136
17136
|
]
|
|
17137
17137
|
}`,
|
|
@@ -17149,7 +17149,7 @@ Example: A form that asks a question, collects contact info, then finishes:
|
|
|
17149
17149
|
form_id: external_exports.string().describe("Form UUID - pass to follow-up tools"),
|
|
17150
17150
|
viewer_url: external_exports.string().describe("Live form URL for respondents"),
|
|
17151
17151
|
dashboard_url: external_exports.string().optional().describe("Builder URL (sign in to edit)"),
|
|
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
|
|
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"),
|
|
17153
17153
|
plan: external_exports.object({ name: external_exports.string(), auth_mode: external_exports.string() }).optional().describe("Plan and auth context for this session")
|
|
17154
17154
|
},
|
|
17155
17155
|
annotations: {
|
|
@@ -17257,7 +17257,7 @@ Example: A form that asks a question, collects contact info, then finishes:
|
|
|
17257
17257
|
`Title: ${title}`,
|
|
17258
17258
|
`Form ID: ${formId}`,
|
|
17259
17259
|
``,
|
|
17260
|
-
`Node IDs (use directly with
|
|
17260
|
+
`Node IDs (use directly with attach_node_media / update_node - no get_form round-trip needed):`,
|
|
17261
17261
|
...createdNodes.map((n) => `- [${n.type}] "${truncate(n.prompt)}": ${n.id}`),
|
|
17262
17262
|
``,
|
|
17263
17263
|
`FORM URL (live - share this with respondents): ${data.viewer_url}`
|
|
@@ -17269,7 +17269,7 @@ Example: A form that asks a question, collects contact info, then finishes:
|
|
|
17269
17269
|
``,
|
|
17270
17270
|
`The form is live and accessible at the FORM URL above. Continue with media/narration steps if needed - changes appear immediately.`,
|
|
17271
17271
|
`The URLs above are exact values; constructed or guessed URLs return 404. Use the FORM URL and DASHBOARD URL as shown.`,
|
|
17272
|
-
`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.).`
|
|
17273
17273
|
);
|
|
17274
17274
|
if (planContext) {
|
|
17275
17275
|
const nodePart = planContext.node_limit === null ? "unlimited nodes" : `up to ${planContext.node_limit} nodes per form`;
|
|
@@ -17325,6 +17325,21 @@ function registerListFormsTool(server) {
|
|
|
17325
17325
|
sort: external_exports.enum(["created_at", "updated_at"]).optional().describe("Sort field (default: created_at)"),
|
|
17326
17326
|
order: external_exports.enum(["asc", "desc"]).optional().describe("Sort order (default: desc, newest first)")
|
|
17327
17327
|
},
|
|
17328
|
+
outputSchema: {
|
|
17329
|
+
forms: external_exports.array(
|
|
17330
|
+
external_exports.object({
|
|
17331
|
+
id: external_exports.string(),
|
|
17332
|
+
title: external_exports.string(),
|
|
17333
|
+
share_id: external_exports.string(),
|
|
17334
|
+
is_live: external_exports.boolean(),
|
|
17335
|
+
has_unpublished_changes: external_exports.boolean(),
|
|
17336
|
+
created_at: external_exports.string(),
|
|
17337
|
+
updated_at: external_exports.string(),
|
|
17338
|
+
tags: external_exports.array(external_exports.object({ id: external_exports.string(), name: external_exports.string(), color: external_exports.string().nullable() }))
|
|
17339
|
+
})
|
|
17340
|
+
).describe("Matching forms (empty if none)"),
|
|
17341
|
+
next_cursor: external_exports.string().nullable().describe("Pass as cursor to fetch the next page, or null when no more")
|
|
17342
|
+
},
|
|
17328
17343
|
annotations: {
|
|
17329
17344
|
readOnlyHint: true,
|
|
17330
17345
|
destructiveHint: false,
|
|
@@ -17346,8 +17361,21 @@ function registerListFormsTool(server) {
|
|
|
17346
17361
|
return errorResult(result.error);
|
|
17347
17362
|
}
|
|
17348
17363
|
const data = result.data;
|
|
17364
|
+
const structured = {
|
|
17365
|
+
forms: data.forms.map((f) => ({
|
|
17366
|
+
id: f.id,
|
|
17367
|
+
title: f.title,
|
|
17368
|
+
share_id: f.share_id,
|
|
17369
|
+
is_live: f.is_live,
|
|
17370
|
+
has_unpublished_changes: f.has_unpublished_changes,
|
|
17371
|
+
created_at: f.created_at,
|
|
17372
|
+
updated_at: f.updated_at,
|
|
17373
|
+
tags: f.tags.map((t) => ({ id: t.id, name: t.name, color: t.color }))
|
|
17374
|
+
})),
|
|
17375
|
+
next_cursor: data.next_cursor
|
|
17376
|
+
};
|
|
17349
17377
|
if (data.forms.length === 0) {
|
|
17350
|
-
return
|
|
17378
|
+
return structuredResult("No forms found matching the criteria.", structured);
|
|
17351
17379
|
}
|
|
17352
17380
|
const lines = [`Found ${data.forms.length} form(s):
|
|
17353
17381
|
`];
|
|
@@ -17361,14 +17389,36 @@ function registerListFormsTool(server) {
|
|
|
17361
17389
|
`More results available. Pass cursor: "${data.next_cursor}" to get the next page.`
|
|
17362
17390
|
);
|
|
17363
17391
|
}
|
|
17364
|
-
return
|
|
17392
|
+
return structuredResult(lines.join("\n"), structured);
|
|
17365
17393
|
}
|
|
17366
17394
|
);
|
|
17367
17395
|
}
|
|
17368
17396
|
|
|
17369
17397
|
// src/lib/format-form.ts
|
|
17398
|
+
function getFormNodes(data) {
|
|
17399
|
+
return data.node ?? data.nodes ?? [];
|
|
17400
|
+
}
|
|
17401
|
+
function projectFormState(data) {
|
|
17402
|
+
return {
|
|
17403
|
+
form_id: String(data.form_id ?? ""),
|
|
17404
|
+
title: String(data.title ?? ""),
|
|
17405
|
+
is_live: Boolean(data.is_live),
|
|
17406
|
+
has_unpublished_changes: Boolean(data.has_unpublished_changes),
|
|
17407
|
+
nodes: getFormNodes(data).map((q) => {
|
|
17408
|
+
const options = q.node_options ?? q.options;
|
|
17409
|
+
return {
|
|
17410
|
+
id: q.id,
|
|
17411
|
+
type: q.type,
|
|
17412
|
+
prompt: q.prompt ?? "",
|
|
17413
|
+
required: Boolean(q.required),
|
|
17414
|
+
...q.config && Object.keys(q.config).length > 0 ? { config: q.config } : {},
|
|
17415
|
+
...options && options.length > 0 ? { options: options.map((o) => ({ id: o.id, content: o.content })) } : {}
|
|
17416
|
+
};
|
|
17417
|
+
})
|
|
17418
|
+
};
|
|
17419
|
+
}
|
|
17370
17420
|
function formatFormState(data) {
|
|
17371
|
-
const nodes = data
|
|
17421
|
+
const nodes = getFormNodes(data);
|
|
17372
17422
|
const lines = [
|
|
17373
17423
|
`Form: ${data.title}`,
|
|
17374
17424
|
`Form ID: ${data.form_id}`,
|
|
@@ -17408,6 +17458,22 @@ function registerGetFormTool(server) {
|
|
|
17408
17458
|
inputSchema: {
|
|
17409
17459
|
form_id: external_exports.string().uuid().describe("The form UUID (returned by clipform_create_form, not the short share_id from the URL)")
|
|
17410
17460
|
},
|
|
17461
|
+
outputSchema: {
|
|
17462
|
+
form_id: external_exports.string().describe("Form UUID"),
|
|
17463
|
+
title: external_exports.string().describe("Form title"),
|
|
17464
|
+
is_live: external_exports.boolean().describe("Whether the form is published"),
|
|
17465
|
+
has_unpublished_changes: external_exports.boolean().describe("Live form has edits not yet republished"),
|
|
17466
|
+
nodes: external_exports.array(
|
|
17467
|
+
external_exports.object({
|
|
17468
|
+
id: external_exports.string(),
|
|
17469
|
+
type: external_exports.string(),
|
|
17470
|
+
prompt: external_exports.string(),
|
|
17471
|
+
required: external_exports.boolean(),
|
|
17472
|
+
config: external_exports.record(external_exports.unknown()).optional(),
|
|
17473
|
+
options: external_exports.array(external_exports.object({ id: external_exports.string(), content: external_exports.string() })).optional()
|
|
17474
|
+
})
|
|
17475
|
+
).describe("Nodes in sequential order - IDs for update_node / attach_node_media")
|
|
17476
|
+
},
|
|
17411
17477
|
annotations: {
|
|
17412
17478
|
readOnlyHint: true,
|
|
17413
17479
|
destructiveHint: false,
|
|
@@ -17420,7 +17486,7 @@ function registerGetFormTool(server) {
|
|
|
17420
17486
|
if (!result.ok) {
|
|
17421
17487
|
return errorResult(result.error);
|
|
17422
17488
|
}
|
|
17423
|
-
return
|
|
17489
|
+
return structuredResult(formatFormState(result.data), projectFormState(result.data));
|
|
17424
17490
|
}
|
|
17425
17491
|
);
|
|
17426
17492
|
}
|
|
@@ -17597,6 +17663,10 @@ function registerAddNodeTool(server) {
|
|
|
17597
17663
|
"Insert after this node ID. Omit to append before the end screen."
|
|
17598
17664
|
)
|
|
17599
17665
|
},
|
|
17666
|
+
outputSchema: {
|
|
17667
|
+
node_id: external_exports.string().describe("ID of the created node - pass to update_node / attach_node_media"),
|
|
17668
|
+
type: external_exports.string().describe("Node type that was added")
|
|
17669
|
+
},
|
|
17600
17670
|
annotations: {
|
|
17601
17671
|
readOnlyHint: false,
|
|
17602
17672
|
destructiveHint: false,
|
|
@@ -17615,8 +17685,10 @@ function registerAddNodeTool(server) {
|
|
|
17615
17685
|
if (!result.ok) {
|
|
17616
17686
|
return errorResult(result.error);
|
|
17617
17687
|
}
|
|
17618
|
-
|
|
17619
|
-
|
|
17688
|
+
const nodeId = result.data.node_id;
|
|
17689
|
+
return structuredResult(
|
|
17690
|
+
`Node added. ID: ${nodeId} | Type: ${node.type}`,
|
|
17691
|
+
{ node_id: nodeId, type: node.type }
|
|
17620
17692
|
);
|
|
17621
17693
|
}
|
|
17622
17694
|
);
|
|
@@ -17629,7 +17701,9 @@ var NodeUpdateSchema = external_exports.object({
|
|
|
17629
17701
|
label: external_exports.string().optional().describe("Short label for the node (used in logic builder)"),
|
|
17630
17702
|
type: external_exports.enum(ACTIVE_NODE_TYPES2).optional().describe("Change the node type"),
|
|
17631
17703
|
required: external_exports.boolean().optional().describe("Whether an answer is required"),
|
|
17632
|
-
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
|
+
),
|
|
17633
17707
|
options: external_exports.array(OptionSchema).optional().describe(
|
|
17634
17708
|
"Replace all options (for choice nodes). Omit to keep existing options."
|
|
17635
17709
|
)
|
|
@@ -17644,6 +17718,16 @@ function registerUpdateNodeTool(server) {
|
|
|
17644
17718
|
form_id: external_exports.string().uuid().describe("The form UUID (returned by clipform_create_form, not the short share_id from the URL)"),
|
|
17645
17719
|
updates: external_exports.array(NodeUpdateSchema).min(1).max(20).describe("One or more node updates to apply")
|
|
17646
17720
|
},
|
|
17721
|
+
outputSchema: {
|
|
17722
|
+
results: external_exports.array(
|
|
17723
|
+
external_exports.object({
|
|
17724
|
+
node_id: external_exports.string(),
|
|
17725
|
+
ok: external_exports.boolean(),
|
|
17726
|
+
warnings: external_exports.array(external_exports.string()).optional(),
|
|
17727
|
+
error: external_exports.string().optional()
|
|
17728
|
+
})
|
|
17729
|
+
).describe("One result per requested update, in order")
|
|
17730
|
+
},
|
|
17647
17731
|
annotations: {
|
|
17648
17732
|
readOnlyHint: false,
|
|
17649
17733
|
destructiveHint: false,
|
|
@@ -17653,6 +17737,7 @@ function registerUpdateNodeTool(server) {
|
|
|
17653
17737
|
},
|
|
17654
17738
|
async ({ form_id, updates }) => {
|
|
17655
17739
|
const allLines = [];
|
|
17740
|
+
const results = [];
|
|
17656
17741
|
for (const { node_id, prompt, label, type, required: required2, config: config2, options } of updates) {
|
|
17657
17742
|
const body = {};
|
|
17658
17743
|
if (prompt !== void 0) body.prompt = prompt;
|
|
@@ -17667,6 +17752,7 @@ function registerUpdateNodeTool(server) {
|
|
|
17667
17752
|
);
|
|
17668
17753
|
if (!result.ok) {
|
|
17669
17754
|
allLines.push(`Node ${node_id}: FAILED \u2014 ${result.error}`);
|
|
17755
|
+
results.push({ node_id, ok: false, error: result.error });
|
|
17670
17756
|
continue;
|
|
17671
17757
|
}
|
|
17672
17758
|
const changes = [];
|
|
@@ -17682,8 +17768,9 @@ function registerUpdateNodeTool(server) {
|
|
|
17682
17768
|
if (warnings?.length) {
|
|
17683
17769
|
for (const w of warnings) allLines.push(`Node ${node_id} warning: ${w}`);
|
|
17684
17770
|
}
|
|
17771
|
+
results.push({ node_id, ok: true, ...warnings?.length ? { warnings } : {} });
|
|
17685
17772
|
}
|
|
17686
|
-
return
|
|
17773
|
+
return structuredResult(allLines.join("\n"), { results });
|
|
17687
17774
|
}
|
|
17688
17775
|
);
|
|
17689
17776
|
}
|
|
@@ -17728,10 +17815,8 @@ function registerDeleteNodeTool(server) {
|
|
|
17728
17815
|
);
|
|
17729
17816
|
}
|
|
17730
17817
|
|
|
17731
|
-
// src/tools/upload-
|
|
17732
|
-
var
|
|
17733
|
-
var MediaItemSchema = external_exports.object({
|
|
17734
|
-
node_id: external_exports.string().describe("The node ID"),
|
|
17818
|
+
// src/tools/upload-media-asset.ts
|
|
17819
|
+
var MediaAssetItemSchema = external_exports.object({
|
|
17735
17820
|
media_type: external_exports.enum(["video", "still"]).describe("Type of media"),
|
|
17736
17821
|
media_source: external_exports.enum(["uploaded", "recorded"]).default("uploaded").describe("How the media was created"),
|
|
17737
17822
|
url: external_exports.string().url().optional().describe("Public URL of the media file. When provided, the server fetches and stores it directly."),
|
|
@@ -17748,21 +17833,31 @@ var MediaItemSchema = external_exports.object({
|
|
|
17748
17833
|
})
|
|
17749
17834
|
).optional().describe("Per-word timestamps within the segment")
|
|
17750
17835
|
})
|
|
17751
|
-
).optional().describe("Word-level captions from clipform_generate_tts. Required for per-word highlighting - pass the full objects including 'words' arrays.")
|
|
17752
|
-
show_captions: external_exports.boolean().optional().describe("Display captions/subtitles on the node. Defaults to on; pass false to hide them."),
|
|
17753
|
-
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.")
|
|
17754
17837
|
});
|
|
17755
|
-
function
|
|
17838
|
+
function registerUploadMediaAssetTool(server) {
|
|
17756
17839
|
server.registerTool(
|
|
17757
|
-
"
|
|
17840
|
+
"clipform_upload_media_asset",
|
|
17758
17841
|
{
|
|
17759
|
-
title: "Upload
|
|
17760
|
-
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.
|
|
17761
17844
|
|
|
17762
|
-
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.`,
|
|
17763
17846
|
inputSchema: {
|
|
17764
|
-
|
|
17765
|
-
|
|
17847
|
+
items: external_exports.array(MediaAssetItemSchema).min(1).max(10).describe("One or more media items to create as workspace library assets")
|
|
17848
|
+
},
|
|
17849
|
+
outputSchema: {
|
|
17850
|
+
results: external_exports.array(
|
|
17851
|
+
external_exports.object({
|
|
17852
|
+
media_asset_id: external_exports.string().optional(),
|
|
17853
|
+
media_type: external_exports.string(),
|
|
17854
|
+
stored: external_exports.boolean().describe("Bytes already stored (URL-based); false when an upload_url is pending"),
|
|
17855
|
+
upload_url: external_exports.string().optional(),
|
|
17856
|
+
upload_method: external_exports.string().optional(),
|
|
17857
|
+
ok: external_exports.boolean(),
|
|
17858
|
+
error: external_exports.string().optional()
|
|
17859
|
+
})
|
|
17860
|
+
).describe("One result per item, in order")
|
|
17766
17861
|
},
|
|
17767
17862
|
annotations: {
|
|
17768
17863
|
readOnlyHint: false,
|
|
@@ -17771,20 +17866,24 @@ When a public URL is provided, the media is fetched and stored automatically. On
|
|
|
17771
17866
|
openWorldHint: false
|
|
17772
17867
|
}
|
|
17773
17868
|
},
|
|
17774
|
-
async ({
|
|
17775
|
-
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
|
+
}
|
|
17776
17881
|
const lines = [];
|
|
17882
|
+
const results = [];
|
|
17777
17883
|
let successCount = 0;
|
|
17778
17884
|
for (let i = 0; i < items.length; i++) {
|
|
17779
17885
|
const item = items[i];
|
|
17780
|
-
if (items.length > 1) lines.push(`--- Item ${i + 1} (
|
|
17781
|
-
if (!workspaceId) {
|
|
17782
|
-
lines.push(
|
|
17783
|
-
"FAILED: no workspace in context - reconnect the MCP or set CLIPFORM_API_KEY so media can land in your library."
|
|
17784
|
-
);
|
|
17785
|
-
lines.push("");
|
|
17786
|
-
continue;
|
|
17787
|
-
}
|
|
17886
|
+
if (items.length > 1) lines.push(`--- Item ${i + 1} (${item.media_type}) ---`);
|
|
17788
17887
|
const createBody = {
|
|
17789
17888
|
media_type: item.media_type,
|
|
17790
17889
|
media_source: item.media_source
|
|
@@ -17795,21 +17894,105 @@ When a public URL is provided, the media is fetched and stored automatically. On
|
|
|
17795
17894
|
if (!created.ok) {
|
|
17796
17895
|
lines.push(`FAILED: ${created.error}`);
|
|
17797
17896
|
lines.push("");
|
|
17897
|
+
results.push({ media_type: item.media_type, stored: false, ok: false, error: created.error });
|
|
17798
17898
|
continue;
|
|
17799
17899
|
}
|
|
17800
17900
|
const mediaAssetId = created.data.media_asset_id;
|
|
17801
17901
|
if (!mediaAssetId) {
|
|
17802
|
-
|
|
17902
|
+
const error2 = `upload did not return a media asset id (route ${String(created.data.route ?? "unknown")}).`;
|
|
17903
|
+
lines.push(`FAILED: ${error2}`);
|
|
17803
17904
|
lines.push("");
|
|
17905
|
+
results.push({ media_type: item.media_type, stored: false, ok: false, error: error2 });
|
|
17804
17906
|
continue;
|
|
17805
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}) ---`);
|
|
17806
17988
|
const attached = await callApi(`/forms/${form_id}/nodes/${item.node_id}/media/attach`, {
|
|
17807
17989
|
method: "POST",
|
|
17808
|
-
body: { media_asset_id:
|
|
17990
|
+
body: { media_asset_id: item.media_asset_id }
|
|
17809
17991
|
});
|
|
17810
17992
|
if (!attached.ok) {
|
|
17811
|
-
lines.push(`FAILED to attach
|
|
17993
|
+
lines.push(`FAILED to attach media to the node: ${attached.error}`);
|
|
17812
17994
|
lines.push("");
|
|
17995
|
+
results.push({ node_id: item.node_id, ok: false, error: attached.error });
|
|
17813
17996
|
continue;
|
|
17814
17997
|
}
|
|
17815
17998
|
const presentation = {};
|
|
@@ -17826,29 +18009,18 @@ When a public URL is provided, the media is fetched and stored automatically. On
|
|
|
17826
18009
|
}
|
|
17827
18010
|
}
|
|
17828
18011
|
successCount++;
|
|
17829
|
-
|
|
17830
|
-
|
|
17831
|
-
resultLines.push(
|
|
17832
|
-
`Upload URL: ${created.data.upload_url}`,
|
|
17833
|
-
`Upload method: ${created.data.upload_method}`,
|
|
17834
|
-
`Upload the file directly to the upload URL using ${created.data.upload_method === "tus" ? "TUS resumable upload" : "HTTP PUT"}.`
|
|
17835
|
-
);
|
|
17836
|
-
} else {
|
|
17837
|
-
resultLines.push(`Media stored in the workspace library and attached to the node.`);
|
|
17838
|
-
}
|
|
17839
|
-
if (item.captions) {
|
|
17840
|
-
resultLines.push(`Captions: ${item.captions.length} segments saved`);
|
|
17841
|
-
}
|
|
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.`];
|
|
17842
18014
|
if (presentationNote) resultLines.push(presentationNote);
|
|
17843
18015
|
lines.push(resultLines.join("\n"));
|
|
17844
18016
|
lines.push("");
|
|
17845
18017
|
}
|
|
17846
18018
|
if (items.length > 1) {
|
|
17847
|
-
lines.unshift(`Media
|
|
18019
|
+
lines.unshift(`Media attach: ${successCount}/${items.length} succeeded
|
|
17848
18020
|
`);
|
|
17849
18021
|
}
|
|
17850
18022
|
if (successCount === 0) return errorResult(lines.join("\n"));
|
|
17851
|
-
return
|
|
18023
|
+
return structuredResult(lines.join("\n"), { results });
|
|
17852
18024
|
}
|
|
17853
18025
|
);
|
|
17854
18026
|
}
|
|
@@ -17864,6 +18036,14 @@ function registerGetNodeMediaTool(server) {
|
|
|
17864
18036
|
form_id: external_exports.string().uuid().describe("The form UUID (returned by clipform_create_form, not the short share_id from the URL)"),
|
|
17865
18037
|
node_id: external_exports.string().describe("The node ID")
|
|
17866
18038
|
},
|
|
18039
|
+
outputSchema: {
|
|
18040
|
+
media: external_exports.object({
|
|
18041
|
+
media_type: external_exports.string(),
|
|
18042
|
+
status: external_exports.string(),
|
|
18043
|
+
duration: external_exports.number().optional(),
|
|
18044
|
+
transcription_status: external_exports.string().nullable().optional()
|
|
18045
|
+
}).nullable().describe("Attached media (null if the node has none)")
|
|
18046
|
+
},
|
|
17867
18047
|
annotations: {
|
|
17868
18048
|
readOnlyHint: true,
|
|
17869
18049
|
destructiveHint: false,
|
|
@@ -17880,15 +18060,22 @@ function registerGetNodeMediaTool(server) {
|
|
|
17880
18060
|
}
|
|
17881
18061
|
const media = result.data.media;
|
|
17882
18062
|
if (!media) {
|
|
17883
|
-
return
|
|
18063
|
+
return structuredResult("No media attached to this node.", { media: null });
|
|
17884
18064
|
}
|
|
17885
|
-
|
|
18065
|
+
const projected = {
|
|
18066
|
+
media_type: String(media.media_type),
|
|
18067
|
+
status: String(media.status),
|
|
18068
|
+
...typeof media.duration === "number" ? { duration: media.duration } : {},
|
|
18069
|
+
transcription_status: media.transcription_status == null ? null : String(media.transcription_status)
|
|
18070
|
+
};
|
|
18071
|
+
return structuredResult(
|
|
17886
18072
|
[
|
|
17887
18073
|
`Type: ${media.media_type}`,
|
|
17888
18074
|
`Status: ${media.status}`,
|
|
17889
18075
|
media.duration ? `Duration: ${media.duration}s` : "",
|
|
17890
18076
|
`Transcription: ${media.transcription_status}`
|
|
17891
|
-
].filter(Boolean).join("\n")
|
|
18077
|
+
].filter(Boolean).join("\n"),
|
|
18078
|
+
{ media: projected }
|
|
17892
18079
|
);
|
|
17893
18080
|
}
|
|
17894
18081
|
);
|
|
@@ -18042,6 +18229,20 @@ Returns: article title, source, author, date, URL, description, and image URL pe
|
|
|
18042
18229
|
query: external_exports.string().describe("News search query (e.g. 'Iran war 2026', 'Australian Open final', 'UK election')"),
|
|
18043
18230
|
count: external_exports.number().min(1).max(15).default(5).optional().describe("Max results per provider (default 5)")
|
|
18044
18231
|
},
|
|
18232
|
+
outputSchema: {
|
|
18233
|
+
query: external_exports.string(),
|
|
18234
|
+
articles: external_exports.array(
|
|
18235
|
+
external_exports.object({
|
|
18236
|
+
title: external_exports.string().optional(),
|
|
18237
|
+
source: external_exports.string().optional(),
|
|
18238
|
+
description: external_exports.string().optional(),
|
|
18239
|
+
author: external_exports.string().optional(),
|
|
18240
|
+
published_at: external_exports.string().optional(),
|
|
18241
|
+
url: external_exports.string(),
|
|
18242
|
+
image_url: external_exports.string().optional()
|
|
18243
|
+
})
|
|
18244
|
+
).describe("Matching articles (empty if none)")
|
|
18245
|
+
},
|
|
18045
18246
|
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
|
|
18046
18247
|
},
|
|
18047
18248
|
async ({ query, count }) => {
|
|
@@ -18050,7 +18251,16 @@ Returns: article title, source, author, date, URL, description, and image URL pe
|
|
|
18050
18251
|
});
|
|
18051
18252
|
if (!result.ok) return errorResult(result.error);
|
|
18052
18253
|
const results = result.data.results;
|
|
18053
|
-
if (!results.length) return
|
|
18254
|
+
if (!results.length) return structuredResult(`No news articles found for "${query}".`, { query, articles: [] });
|
|
18255
|
+
const articles = results.map((article) => ({
|
|
18256
|
+
...article.title ? { title: article.title } : {},
|
|
18257
|
+
...article.source ? { source: article.source } : {},
|
|
18258
|
+
...article.description ? { description: article.description } : {},
|
|
18259
|
+
...article.author ? { author: article.author } : {},
|
|
18260
|
+
...article.publishedAt ? { published_at: article.publishedAt } : {},
|
|
18261
|
+
url: article.url,
|
|
18262
|
+
...article.imageUrl ? { image_url: article.imageUrl } : {}
|
|
18263
|
+
}));
|
|
18054
18264
|
const lines = [`Found ${results.length} articles for "${query}":
|
|
18055
18265
|
`];
|
|
18056
18266
|
for (const article of results) {
|
|
@@ -18062,7 +18272,7 @@ Returns: article title, source, author, date, URL, description, and image URL pe
|
|
|
18062
18272
|
if (article.imageUrl) lines.push(` Image: ${article.imageUrl}`);
|
|
18063
18273
|
lines.push("");
|
|
18064
18274
|
}
|
|
18065
|
-
return
|
|
18275
|
+
return structuredResult(lines.join("\n"), { query, articles });
|
|
18066
18276
|
}
|
|
18067
18277
|
);
|
|
18068
18278
|
}
|
|
@@ -18118,6 +18328,12 @@ Supports any public YouTube video with captions enabled. Does NOT work for priva
|
|
|
18118
18328
|
}
|
|
18119
18329
|
|
|
18120
18330
|
// src/tools/generate-tts.ts
|
|
18331
|
+
var CaptionSchema2 = external_exports.object({
|
|
18332
|
+
start: external_exports.number(),
|
|
18333
|
+
end: external_exports.number(),
|
|
18334
|
+
text: external_exports.string(),
|
|
18335
|
+
words: external_exports.array(external_exports.object({ word: external_exports.string(), start: external_exports.number(), end: external_exports.number() })).optional()
|
|
18336
|
+
});
|
|
18121
18337
|
var TtsItemSchema = external_exports.object({
|
|
18122
18338
|
text: external_exports.string().min(1).max(5e3).describe("Narration text"),
|
|
18123
18339
|
voice: external_exports.enum(["ryan", "sonia", "andrew", "ava", "guy"]).optional().default("ryan").describe("Voice: ryan (British male, clear/articulate), sonia (British female, warm/bright), andrew (American male, smooth/neutral), ava (American female, vibrant/friendly), guy (American male, deep/authoritative). Pick based on the form's topic and audience."),
|
|
@@ -18138,6 +18354,16 @@ Pass one item or many (max 10) - multiple items run in parallel. Returns audio U
|
|
|
18138
18354
|
inputSchema: {
|
|
18139
18355
|
items: external_exports.array(TtsItemSchema).min(1).max(10).describe("One or more TTS items to generate")
|
|
18140
18356
|
},
|
|
18357
|
+
outputSchema: {
|
|
18358
|
+
results: external_exports.array(
|
|
18359
|
+
external_exports.object({
|
|
18360
|
+
ok: external_exports.boolean(),
|
|
18361
|
+
audio_url: external_exports.string().optional(),
|
|
18362
|
+
captions: external_exports.array(CaptionSchema2).optional().describe("Pass as the captions param to upload_media_asset"),
|
|
18363
|
+
error: external_exports.string().optional()
|
|
18364
|
+
})
|
|
18365
|
+
).describe("One result per item, in order")
|
|
18366
|
+
},
|
|
18141
18367
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }
|
|
18142
18368
|
},
|
|
18143
18369
|
async ({ items }) => {
|
|
@@ -18151,6 +18377,7 @@ Pass one item or many (max 10) - multiple items run in parallel. Returns audio U
|
|
|
18151
18377
|
)
|
|
18152
18378
|
);
|
|
18153
18379
|
const lines = [];
|
|
18380
|
+
const structured = [];
|
|
18154
18381
|
let successCount = 0;
|
|
18155
18382
|
for (let i = 0; i < results.length; i++) {
|
|
18156
18383
|
const r = results[i];
|
|
@@ -18167,11 +18394,17 @@ Pass one item or many (max 10) - multiple items run in parallel. Returns audio U
|
|
|
18167
18394
|
...Array.isArray(c.words) ? { words: c.words.map((w) => ({ word: w.word, start: w.start, end: w.end })) } : {}
|
|
18168
18395
|
}));
|
|
18169
18396
|
lines.push(`Captions: ${JSON.stringify(captions)}`);
|
|
18397
|
+
structured.push({
|
|
18398
|
+
ok: true,
|
|
18399
|
+
...data.audioUrl ? { audio_url: String(data.audioUrl) } : {},
|
|
18400
|
+
captions
|
|
18401
|
+
});
|
|
18170
18402
|
} else {
|
|
18171
18403
|
const error2 = r.status === "rejected" ? r.reason?.message || String(r.reason) : r.value.error;
|
|
18172
18404
|
const status = r.status === "fulfilled" ? r.value.status : 0;
|
|
18173
18405
|
const hint = status === 500 || status === 0 ? " (transient - retrying may help)" : "";
|
|
18174
18406
|
lines.push(`FAILED: ${error2}${hint}`);
|
|
18407
|
+
structured.push({ ok: false, error: String(error2) });
|
|
18175
18408
|
}
|
|
18176
18409
|
lines.push("");
|
|
18177
18410
|
}
|
|
@@ -18181,7 +18414,7 @@ Pass one item or many (max 10) - multiple items run in parallel. Returns audio U
|
|
|
18181
18414
|
}
|
|
18182
18415
|
lines.push(`Pass the complete Captions JSON as the "captions" parameter when uploading.`);
|
|
18183
18416
|
if (successCount === 0) return errorResult(lines.join("\n"));
|
|
18184
|
-
return
|
|
18417
|
+
return structuredResult(lines.join("\n"), { results: structured });
|
|
18185
18418
|
}
|
|
18186
18419
|
);
|
|
18187
18420
|
}
|
|
@@ -18192,7 +18425,7 @@ function registerSearchMediaTool(server) {
|
|
|
18192
18425
|
"clipform_search_media",
|
|
18193
18426
|
{
|
|
18194
18427
|
title: "Search Media",
|
|
18195
|
-
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.
|
|
18196
18429
|
|
|
18197
18430
|
Example: { queries: [{ query: "saturn rings" }, { query: "mars surface", count: 3 }] } returns portrait images for both.`,
|
|
18198
18431
|
inputSchema: {
|
|
@@ -18205,10 +18438,30 @@ Example: { queries: [{ query: "saturn rings" }, { query: "mars surface", count:
|
|
|
18205
18438
|
})
|
|
18206
18439
|
).min(1).max(10).describe("One or more search queries to run")
|
|
18207
18440
|
},
|
|
18441
|
+
outputSchema: {
|
|
18442
|
+
searches: external_exports.array(
|
|
18443
|
+
external_exports.object({
|
|
18444
|
+
query: external_exports.string(),
|
|
18445
|
+
kind: external_exports.enum(["image", "video"]),
|
|
18446
|
+
items: external_exports.array(
|
|
18447
|
+
external_exports.object({
|
|
18448
|
+
title: external_exports.string().optional(),
|
|
18449
|
+
url: external_exports.string(),
|
|
18450
|
+
width: external_exports.number().optional(),
|
|
18451
|
+
height: external_exports.number().optional(),
|
|
18452
|
+
duration: external_exports.number().optional(),
|
|
18453
|
+
source: external_exports.string().optional(),
|
|
18454
|
+
attribution: external_exports.string().optional()
|
|
18455
|
+
})
|
|
18456
|
+
)
|
|
18457
|
+
})
|
|
18458
|
+
).describe("Results grouped by query (items empty if a query found nothing or failed)")
|
|
18459
|
+
},
|
|
18208
18460
|
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
|
|
18209
18461
|
},
|
|
18210
18462
|
async ({ queries }) => {
|
|
18211
18463
|
const allLines = [];
|
|
18464
|
+
const searches = [];
|
|
18212
18465
|
for (const q of queries) {
|
|
18213
18466
|
const { query, count, orientation } = q;
|
|
18214
18467
|
const kind = q.kind ?? "image";
|
|
@@ -18218,16 +18471,32 @@ Example: { queries: [{ query: "saturn rings" }, { query: "mars surface", count:
|
|
|
18218
18471
|
if (queries.length > 1) allLines.push(`--- "${query}" (${kind}) ---`);
|
|
18219
18472
|
if (!result.ok) {
|
|
18220
18473
|
allLines.push(`FAILED: ${result.error}`, "");
|
|
18474
|
+
searches.push({ query, kind, items: [] });
|
|
18221
18475
|
continue;
|
|
18222
18476
|
}
|
|
18223
18477
|
const results = result.data.results;
|
|
18224
18478
|
if (!results.length) {
|
|
18225
18479
|
allLines.push(`No ${kind}s found.`, "");
|
|
18480
|
+
searches.push({ query, kind, items: [] });
|
|
18226
18481
|
continue;
|
|
18227
18482
|
}
|
|
18483
|
+
const shown = results.slice(0, 10);
|
|
18484
|
+
searches.push({
|
|
18485
|
+
query,
|
|
18486
|
+
kind,
|
|
18487
|
+
items: shown.map((item) => ({
|
|
18488
|
+
...item.title ? { title: item.title } : {},
|
|
18489
|
+
url: item.url,
|
|
18490
|
+
...typeof item.width === "number" ? { width: item.width } : {},
|
|
18491
|
+
...typeof item.height === "number" ? { height: item.height } : {},
|
|
18492
|
+
...kind === "video" && typeof item.duration === "number" ? { duration: item.duration } : {},
|
|
18493
|
+
...item.source ? { source: item.source } : {},
|
|
18494
|
+
...item.attribution ? { attribution: item.attribution } : {}
|
|
18495
|
+
}))
|
|
18496
|
+
});
|
|
18228
18497
|
allLines.push(`Found ${results.length} ${kind}s:
|
|
18229
18498
|
`);
|
|
18230
|
-
for (const item of
|
|
18499
|
+
for (const item of shown) {
|
|
18231
18500
|
allLines.push(`- ${item.title}`);
|
|
18232
18501
|
allLines.push(` URL: ${item.url}`);
|
|
18233
18502
|
const meta = [];
|
|
@@ -18242,11 +18511,16 @@ Example: { queries: [{ query: "saturn rings" }, { query: "mars surface", count:
|
|
|
18242
18511
|
allLines.push("");
|
|
18243
18512
|
}
|
|
18244
18513
|
}
|
|
18245
|
-
return
|
|
18514
|
+
return structuredResult(allLines.join("\n"), { searches });
|
|
18246
18515
|
}
|
|
18247
18516
|
);
|
|
18248
18517
|
}
|
|
18249
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
|
+
|
|
18250
18524
|
// ../../node_modules/.pnpm/uuid@11.1.1/node_modules/uuid/dist/esm/stringify.js
|
|
18251
18525
|
var byteToHex = [];
|
|
18252
18526
|
for (let i = 0; i < 256; ++i) {
|
|
@@ -18303,7 +18577,11 @@ var jobs = /* @__PURE__ */ new Map();
|
|
|
18303
18577
|
var MAX_AGE_MS = 30 * 60 * 1e3;
|
|
18304
18578
|
var RENDER_TIMING = {
|
|
18305
18579
|
expectedRange: "15-120 seconds",
|
|
18306
|
-
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"
|
|
18307
18585
|
};
|
|
18308
18586
|
function createJob(tool) {
|
|
18309
18587
|
const job = {
|
|
@@ -18315,11 +18593,12 @@ function createJob(tool) {
|
|
|
18315
18593
|
jobs.set(job.id, job);
|
|
18316
18594
|
return job;
|
|
18317
18595
|
}
|
|
18318
|
-
function completeJob(id, result) {
|
|
18596
|
+
function completeJob(id, result, attach) {
|
|
18319
18597
|
const job = jobs.get(id);
|
|
18320
18598
|
if (job) {
|
|
18321
18599
|
job.status = "complete";
|
|
18322
18600
|
job.result = result;
|
|
18601
|
+
if (attach) job.attach = attach;
|
|
18323
18602
|
}
|
|
18324
18603
|
}
|
|
18325
18604
|
function failJob(id, error2) {
|
|
@@ -18339,14 +18618,133 @@ function pruneJobs() {
|
|
|
18339
18618
|
}
|
|
18340
18619
|
}
|
|
18341
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
|
+
|
|
18342
18736
|
// src/tools/render-composition.ts
|
|
18343
18737
|
var RenderItemSchema = external_exports.object({
|
|
18344
18738
|
compositionId: external_exports.string().describe("The composition ID (see clipform_list_compositions)"),
|
|
18345
18739
|
outputFormat: external_exports.enum(["mp4", "png"]).default("mp4").describe("Output format (default: mp4)"),
|
|
18346
|
-
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.")
|
|
18347
18743
|
});
|
|
18348
|
-
function fireRender(item) {
|
|
18744
|
+
function fireRender(item, formId) {
|
|
18349
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);
|
|
18350
18748
|
callApi("/internal/render", {
|
|
18351
18749
|
timeoutMs: 3e5,
|
|
18352
18750
|
// local renders pay a cold webpack bundle; Lambda is fast but bursty
|
|
@@ -18355,10 +18753,39 @@ function fireRender(item) {
|
|
|
18355
18753
|
outputFormat: item.outputFormat,
|
|
18356
18754
|
inputProps: item.inputProps ?? {}
|
|
18357
18755
|
}
|
|
18358
|
-
}).then((r) => {
|
|
18359
|
-
if (r.ok)
|
|
18360
|
-
|
|
18361
|
-
|
|
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
|
+
});
|
|
18362
18789
|
return job;
|
|
18363
18790
|
}
|
|
18364
18791
|
function registerRenderCompositionTool(server) {
|
|
@@ -18370,59 +18797,130 @@ function registerRenderCompositionTool(server) {
|
|
|
18370
18797
|
|
|
18371
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.
|
|
18372
18799
|
|
|
18373
|
-
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.`,
|
|
18374
18804
|
inputSchema: {
|
|
18375
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.`),
|
|
18376
18806
|
outputFormat: external_exports.enum(["mp4", "png"]).default("mp4").describe("Single render: output format (default: mp4)"),
|
|
18377
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`."),
|
|
18378
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."),
|
|
18379
|
-
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.")
|
|
18813
|
+
},
|
|
18814
|
+
outputSchema: {
|
|
18815
|
+
jobs: external_exports.array(
|
|
18816
|
+
external_exports.object({
|
|
18817
|
+
status: external_exports.enum(["rendering", "complete"]),
|
|
18818
|
+
job_id: external_exports.string().optional().describe("Present when status is 'rendering' - pass to check_render"),
|
|
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)"),
|
|
18820
|
+
composition_id: 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.")
|
|
18826
|
+
})
|
|
18827
|
+
).describe("One entry per render (single or batch)")
|
|
18380
18828
|
},
|
|
18381
18829
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }
|
|
18382
18830
|
},
|
|
18383
|
-
async ({ compositionId, outputFormat, inputProps, wait, items }) => {
|
|
18831
|
+
async ({ compositionId, outputFormat, inputProps, wait, items, node_id, form_id, captions }) => {
|
|
18384
18832
|
if (items?.length && compositionId) {
|
|
18385
18833
|
return errorResult("Pass EITHER items (batch) OR compositionId (single render), not both.");
|
|
18386
18834
|
}
|
|
18387
18835
|
if (!items?.length && !compositionId) {
|
|
18388
18836
|
return errorResult("Pass compositionId for a single render, or items for a batch.");
|
|
18389
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
|
+
}
|
|
18390
18844
|
const unknown2 = (items ?? [{ compositionId, outputFormat, inputProps }]).map((i) => i.compositionId).filter((id) => !EXPOSED_COMPOSITIONS.includes(id));
|
|
18391
18845
|
if (unknown2.length) {
|
|
18392
18846
|
return errorResult(`Composition${unknown2.length > 1 ? "s" : ""} ${unknown2.map((id) => `"${id}"`).join(", ")} not available. Call clipform_list_compositions to see available options.`);
|
|
18393
18847
|
}
|
|
18394
18848
|
if (items?.length) {
|
|
18395
|
-
const jobs2 = items.map((item) => ({ item, job: fireRender(item) }));
|
|
18396
|
-
return
|
|
18849
|
+
const jobs2 = items.map((item) => ({ item, job: fireRender(item, form_id) }));
|
|
18850
|
+
return structuredResult(
|
|
18397
18851
|
[
|
|
18398
18852
|
`${jobs2.length} renders started in parallel:`,
|
|
18399
|
-
...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}` : ""}`),
|
|
18400
18854
|
``,
|
|
18401
18855
|
`Collect with ONE clipform_check_render call (job_ids: [...]). Renders typically take ${RENDER_TIMING.expectedRange}.`
|
|
18402
|
-
].join("\n")
|
|
18856
|
+
].join("\n"),
|
|
18857
|
+
{
|
|
18858
|
+
jobs: jobs2.map(({ item, job }) => ({
|
|
18859
|
+
status: "rendering",
|
|
18860
|
+
job_id: job.id,
|
|
18861
|
+
composition_id: item.compositionId,
|
|
18862
|
+
output_format: item.outputFormat
|
|
18863
|
+
}))
|
|
18864
|
+
}
|
|
18403
18865
|
);
|
|
18404
18866
|
}
|
|
18405
18867
|
if (wait === false) {
|
|
18406
|
-
const job = fireRender({ compositionId, outputFormat, inputProps });
|
|
18407
|
-
return
|
|
18868
|
+
const job = fireRender({ compositionId, outputFormat, inputProps, node_id, captions }, form_id);
|
|
18869
|
+
return structuredResult(
|
|
18408
18870
|
[
|
|
18409
18871
|
`Render started (${compositionId}).`,
|
|
18410
18872
|
`Job ID: ${job.id}`,
|
|
18411
|
-
`Fire any remaining renders now, then poll clipform_check_render. Renders typically take ${RENDER_TIMING.expectedRange}.`
|
|
18412
|
-
].join("\n")
|
|
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}.`
|
|
18874
|
+
].join("\n"),
|
|
18875
|
+
{
|
|
18876
|
+
jobs: [{ status: "rendering", job_id: job.id, composition_id: compositionId, output_format: outputFormat }]
|
|
18877
|
+
}
|
|
18413
18878
|
);
|
|
18414
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);
|
|
18415
18882
|
const result = await callApi("/internal/render", {
|
|
18416
18883
|
timeoutMs: 3e5,
|
|
18417
18884
|
body: { compositionId, outputFormat, inputProps: inputProps ?? {} }
|
|
18418
18885
|
});
|
|
18419
|
-
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
|
+
}
|
|
18420
18891
|
const data = result.data;
|
|
18421
|
-
|
|
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
|
+
}
|
|
18908
|
+
return structuredResult(
|
|
18422
18909
|
[
|
|
18423
18910
|
`Render complete. Public URL: ${data.public_url}`,
|
|
18424
|
-
`Attach via
|
|
18425
|
-
].join("\n")
|
|
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).`
|
|
18912
|
+
].join("\n"),
|
|
18913
|
+
{
|
|
18914
|
+
jobs: [
|
|
18915
|
+
{
|
|
18916
|
+
status: "complete",
|
|
18917
|
+
...data.public_url ? { public_url: data.public_url } : {},
|
|
18918
|
+
composition_id: compositionId,
|
|
18919
|
+
output_format: outputFormat,
|
|
18920
|
+
...attachStructuredFields(attach)
|
|
18921
|
+
}
|
|
18922
|
+
]
|
|
18923
|
+
}
|
|
18426
18924
|
);
|
|
18427
18925
|
}
|
|
18428
18926
|
);
|
|
@@ -18443,6 +18941,19 @@ function registerSearchMusicTool(server) {
|
|
|
18443
18941
|
maxDuration: external_exports.number().optional().describe("Maximum duration in seconds"),
|
|
18444
18942
|
tags: external_exports.array(external_exports.string()).optional().describe("Genre/mood tags to filter by")
|
|
18445
18943
|
},
|
|
18944
|
+
outputSchema: {
|
|
18945
|
+
query: external_exports.string(),
|
|
18946
|
+
tracks: external_exports.array(
|
|
18947
|
+
external_exports.object({
|
|
18948
|
+
title: external_exports.string().optional(),
|
|
18949
|
+
artist: external_exports.string().optional(),
|
|
18950
|
+
url: external_exports.string(),
|
|
18951
|
+
duration: external_exports.number().optional(),
|
|
18952
|
+
source: external_exports.string().optional(),
|
|
18953
|
+
license: external_exports.string().optional()
|
|
18954
|
+
})
|
|
18955
|
+
).describe("Matching tracks (empty if none); pass a url as the audio track to generate_video")
|
|
18956
|
+
},
|
|
18446
18957
|
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
|
|
18447
18958
|
},
|
|
18448
18959
|
async ({ query, count, instrumentalOnly, minDuration, maxDuration, tags }) => {
|
|
@@ -18451,7 +18962,15 @@ function registerSearchMusicTool(server) {
|
|
|
18451
18962
|
});
|
|
18452
18963
|
if (!result.ok) return errorResult(result.error);
|
|
18453
18964
|
const results = result.data.results;
|
|
18454
|
-
if (!results.length) return
|
|
18965
|
+
if (!results.length) return structuredResult(`No music found for "${query}".`, { query, tracks: [] });
|
|
18966
|
+
const tracks = results.map((item) => ({
|
|
18967
|
+
...item.title ? { title: item.title } : {},
|
|
18968
|
+
...item.artist ? { artist: item.artist } : {},
|
|
18969
|
+
url: item.url,
|
|
18970
|
+
...typeof item.duration === "number" ? { duration: item.duration } : {},
|
|
18971
|
+
...item.source ? { source: item.source } : {},
|
|
18972
|
+
...item.license ? { license: item.license } : {}
|
|
18973
|
+
}));
|
|
18455
18974
|
const lines = [`Found ${results.length} tracks for "${query}":
|
|
18456
18975
|
`];
|
|
18457
18976
|
for (const item of results) {
|
|
@@ -18462,7 +18981,7 @@ function registerSearchMusicTool(server) {
|
|
|
18462
18981
|
if (item.license) lines.push(` License: ${item.license}`);
|
|
18463
18982
|
lines.push("");
|
|
18464
18983
|
}
|
|
18465
|
-
return
|
|
18984
|
+
return structuredResult(lines.join("\n"), { query, tracks });
|
|
18466
18985
|
}
|
|
18467
18986
|
);
|
|
18468
18987
|
}
|
|
@@ -18503,6 +19022,19 @@ function registerListCompositionsTool(server) {
|
|
|
18503
19022
|
title: "List Compositions",
|
|
18504
19023
|
description: `Browse available video compositions and their expected props schemas. Call this before using clipform_render_composition to discover visual styles and their input props. For narrated slideshows from images, use clipform_generate_video instead.`,
|
|
18505
19024
|
inputSchema: {},
|
|
19025
|
+
outputSchema: {
|
|
19026
|
+
compositions: external_exports.array(
|
|
19027
|
+
external_exports.object({
|
|
19028
|
+
id: external_exports.string(),
|
|
19029
|
+
width: external_exports.number(),
|
|
19030
|
+
height: external_exports.number(),
|
|
19031
|
+
fps: external_exports.number(),
|
|
19032
|
+
durationInFrames: external_exports.number(),
|
|
19033
|
+
props: external_exports.string().describe("Compact props summary (name*: type)"),
|
|
19034
|
+
propsSchema: external_exports.record(external_exports.unknown()).optional().describe("Full JSON Schema for inputProps")
|
|
19035
|
+
})
|
|
19036
|
+
).describe("Available compositions - pass an id to render_composition")
|
|
19037
|
+
},
|
|
18506
19038
|
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
|
|
18507
19039
|
},
|
|
18508
19040
|
async () => {
|
|
@@ -18517,7 +19049,18 @@ function registerListCompositionsTool(server) {
|
|
|
18517
19049
|
}
|
|
18518
19050
|
const all = result.data.compositions;
|
|
18519
19051
|
const compositions = all.filter((c) => EXPOSED_COMPOSITIONS.includes(c.id));
|
|
18520
|
-
if (!compositions.length) return
|
|
19052
|
+
if (!compositions.length) return structuredResult("No compositions found.", { compositions: [] });
|
|
19053
|
+
const structured = {
|
|
19054
|
+
compositions: compositions.map((comp) => ({
|
|
19055
|
+
id: comp.id,
|
|
19056
|
+
width: comp.width,
|
|
19057
|
+
height: comp.height,
|
|
19058
|
+
fps: comp.fps,
|
|
19059
|
+
durationInFrames: comp.durationInFrames,
|
|
19060
|
+
props: summarizeSchema(comp.propsSchema),
|
|
19061
|
+
...comp.propsSchema ? { propsSchema: comp.propsSchema } : {}
|
|
19062
|
+
}))
|
|
19063
|
+
};
|
|
18521
19064
|
const lines = [`Available compositions (${compositions.length}):
|
|
18522
19065
|
`];
|
|
18523
19066
|
for (const comp of compositions) {
|
|
@@ -18527,7 +19070,183 @@ function registerListCompositionsTool(server) {
|
|
|
18527
19070
|
}
|
|
18528
19071
|
lines.push("");
|
|
18529
19072
|
lines.push("Use clipform_render_composition with a compositionId to render. inputProps are validated strictly against the schema - unknown or missing props fail.");
|
|
18530
|
-
return
|
|
19073
|
+
return structuredResult(lines.join("\n"), structured);
|
|
19074
|
+
}
|
|
19075
|
+
);
|
|
19076
|
+
}
|
|
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
|
+
);
|
|
18531
19250
|
}
|
|
18532
19251
|
);
|
|
18533
19252
|
}
|
|
@@ -18542,6 +19261,11 @@ function registerListAssetsTool(server) {
|
|
|
18542
19261
|
inputSchema: {
|
|
18543
19262
|
type: external_exports.enum(["sfx", "animation", "font", "all"]).default("all").optional().describe("Asset type to list (default: all)")
|
|
18544
19263
|
},
|
|
19264
|
+
outputSchema: {
|
|
19265
|
+
sfx: external_exports.array(external_exports.object({ name: external_exports.string(), description: external_exports.string().nullable().optional(), path: external_exports.string().optional() })),
|
|
19266
|
+
animations: external_exports.array(external_exports.object({ name: external_exports.string(), description: external_exports.string().nullable().optional(), path: external_exports.string().optional() })),
|
|
19267
|
+
fonts: external_exports.array(external_exports.object({ name: external_exports.string(), weights: external_exports.array(external_exports.union([external_exports.string(), external_exports.number()])).optional() }))
|
|
19268
|
+
},
|
|
18545
19269
|
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
|
|
18546
19270
|
},
|
|
18547
19271
|
async ({ type }) => {
|
|
@@ -18552,6 +19276,11 @@ function registerListAssetsTool(server) {
|
|
|
18552
19276
|
if (!result.ok) return errorResult(result.error);
|
|
18553
19277
|
const data = result.data;
|
|
18554
19278
|
const lines = [];
|
|
19279
|
+
const structured = {
|
|
19280
|
+
sfx: (data.sfx ?? []).map((s) => ({ name: s.name, description: s.description ?? null, path: s.path })),
|
|
19281
|
+
animations: (data.animations ?? []).map((a) => ({ name: a.name, description: a.description ?? null, path: a.path })),
|
|
19282
|
+
fonts: (data.fonts ?? []).map((f) => ({ name: f.name, ...Array.isArray(f.weights) ? { weights: f.weights } : {} }))
|
|
19283
|
+
};
|
|
18555
19284
|
if (data.sfx?.length) {
|
|
18556
19285
|
lines.push(`## Sound Effects (${data.sfx.length})
|
|
18557
19286
|
`);
|
|
@@ -18576,7 +19305,7 @@ function registerListAssetsTool(server) {
|
|
|
18576
19305
|
}
|
|
18577
19306
|
lines.push("");
|
|
18578
19307
|
}
|
|
18579
|
-
return
|
|
19308
|
+
return structuredResult(lines.length ? lines.join("\n") : "No assets found.", structured);
|
|
18580
19309
|
}
|
|
18581
19310
|
);
|
|
18582
19311
|
}
|
|
@@ -18591,7 +19320,11 @@ function registerGenerateVideoTool(server) {
|
|
|
18591
19320
|
|
|
18592
19321
|
Items: type "image" (Ken Burns motion) or "video" (cover-cropped, muted by default). Duration matches audio_url or set duration_seconds explicitly.
|
|
18593
19322
|
|
|
18594
|
-
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}`,
|
|
18595
19328
|
inputSchema: {
|
|
18596
19329
|
items: external_exports.array(
|
|
18597
19330
|
external_exports.object({
|
|
@@ -18625,11 +19358,27 @@ For multi-question builds, pass wait: false on every render: each call returns a
|
|
|
18625
19358
|
contrast: external_exports.number().positive().optional().describe("Contrast boost before mapping (default 1.1)")
|
|
18626
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."),
|
|
18627
19360
|
background_color: external_exports.string().optional().describe("Background color (default '#000')"),
|
|
18628
|
-
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.")
|
|
19365
|
+
},
|
|
19366
|
+
outputSchema: {
|
|
19367
|
+
status: external_exports.enum(["rendering", "complete"]).describe("'rendering' when wait:false (poll check_render); 'complete' with a public_url when wait:true"),
|
|
19368
|
+
job_id: external_exports.string().optional().describe("Present when status is 'rendering' - pass to check_render"),
|
|
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.")
|
|
18629
19375
|
},
|
|
18630
19376
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }
|
|
18631
19377
|
},
|
|
18632
|
-
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
|
+
}
|
|
18633
19382
|
const workspace_id = getMcpAuth()?.workspace_id;
|
|
18634
19383
|
const default_style = style_preset || texture || duotone ? {
|
|
18635
19384
|
...style_preset ? { preset: style_preset } : {},
|
|
@@ -18654,34 +19403,95 @@ For multi-question builds, pass wait: false on every render: each call returns a
|
|
|
18654
19403
|
});
|
|
18655
19404
|
if (wait === false) {
|
|
18656
19405
|
const job = createJob("clipform_generate_video");
|
|
18657
|
-
|
|
18658
|
-
|
|
18659
|
-
|
|
18660
|
-
|
|
18661
|
-
|
|
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
|
+
});
|
|
19433
|
+
return structuredResult(
|
|
18662
19434
|
[
|
|
18663
19435
|
`Render started (${items.length} item${items.length > 1 ? "s" : ""}).`,
|
|
18664
19436
|
`Job ID: ${job.id}`,
|
|
18665
|
-
`Fire any remaining renders now, then poll clipform_check_render. Renders typically take ${RENDER_TIMING.expectedRange}.`
|
|
18666
|
-
].join("\n")
|
|
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}.`
|
|
19438
|
+
].join("\n"),
|
|
19439
|
+
{ status: "rendering", job_id: job.id }
|
|
18667
19440
|
);
|
|
18668
19441
|
}
|
|
19442
|
+
const placeholderPromise = node_id && form_id ? createRenderPlaceholder({ formId: form_id, nodeId: node_id, mediaType: "video" }) : Promise.resolve(void 0);
|
|
18669
19443
|
const result = await apiCall();
|
|
18670
|
-
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
|
+
}
|
|
18671
19449
|
const data = result.data;
|
|
18672
|
-
|
|
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
|
+
}
|
|
19459
|
+
return structuredResult(
|
|
18673
19460
|
[
|
|
18674
19461
|
`Video rendered (${items.length} item${items.length > 1 ? "s" : ""}).`,
|
|
18675
19462
|
`Public URL: ${data.public_url}`,
|
|
18676
19463
|
data.duration_seconds ? `Duration: ${data.duration_seconds}s` : "",
|
|
18677
|
-
`Attach via
|
|
18678
|
-
].filter(Boolean).join("\n")
|
|
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).`
|
|
19465
|
+
].filter(Boolean).join("\n"),
|
|
19466
|
+
{
|
|
19467
|
+
status: "complete",
|
|
19468
|
+
...data.public_url ? { public_url: data.public_url } : {},
|
|
19469
|
+
...typeof data.duration_seconds === "number" ? { duration_seconds: data.duration_seconds } : {},
|
|
19470
|
+
...attachStructuredFields(attach)
|
|
19471
|
+
}
|
|
18679
19472
|
);
|
|
18680
19473
|
}
|
|
18681
19474
|
);
|
|
18682
19475
|
}
|
|
18683
19476
|
|
|
18684
19477
|
// src/tools/check-render.ts
|
|
19478
|
+
function projectJob(id, job) {
|
|
19479
|
+
if (!job) return { job_id: id, status: "not_found" };
|
|
19480
|
+
if (job.status === "rendering") {
|
|
19481
|
+
return { job_id: id, status: "rendering", elapsed_seconds: Math.round((Date.now() - job.createdAt) / 1e3) };
|
|
19482
|
+
}
|
|
19483
|
+
if (job.status === "failed") {
|
|
19484
|
+
return { job_id: id, status: "failed", ...job.error ? { error: job.error } : {} };
|
|
19485
|
+
}
|
|
19486
|
+
const data = job.result;
|
|
19487
|
+
return {
|
|
19488
|
+
job_id: id,
|
|
19489
|
+
status: "complete",
|
|
19490
|
+
...data?.public_url ? { public_url: data.public_url } : {},
|
|
19491
|
+
...typeof data?.duration_seconds === "number" ? { duration_seconds: data.duration_seconds } : {},
|
|
19492
|
+
...attachStructuredFields(job.attach)
|
|
19493
|
+
};
|
|
19494
|
+
}
|
|
18685
19495
|
var LOST_JOB_RECOVERY = [
|
|
18686
19496
|
`Jobs expire after 30 minutes and are lost if the server restarts - the render itself may still have completed.`,
|
|
18687
19497
|
`To recover, re-run the original render call with identical arguments:`,
|
|
@@ -18704,20 +19514,37 @@ function describeJob(id, job) {
|
|
|
18704
19514
|
...data.public_url ? [data.public_url] : [],
|
|
18705
19515
|
...data.duration_seconds ? [`${data.duration_seconds}s`] : []
|
|
18706
19516
|
].join(" | ");
|
|
18707
|
-
|
|
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 };
|
|
18708
19519
|
}
|
|
18709
19520
|
function registerCheckRenderTool(server) {
|
|
18710
19521
|
server.registerTool(
|
|
18711
19522
|
"clipform_check_render",
|
|
18712
19523
|
{
|
|
18713
19524
|
title: "Check Render Status",
|
|
18714
|
-
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.
|
|
18715
19526
|
|
|
18716
|
-
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.`,
|
|
18717
19528
|
inputSchema: {
|
|
18718
19529
|
job_id: external_exports.string().uuid().optional().describe("A single job ID returned by the render tool"),
|
|
18719
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")
|
|
18720
19531
|
},
|
|
19532
|
+
outputSchema: {
|
|
19533
|
+
jobs: external_exports.array(
|
|
19534
|
+
external_exports.object({
|
|
19535
|
+
job_id: external_exports.string(),
|
|
19536
|
+
status: external_exports.enum(["rendering", "complete", "failed", "not_found"]),
|
|
19537
|
+
public_url: external_exports.string().optional(),
|
|
19538
|
+
duration_seconds: external_exports.number().optional(),
|
|
19539
|
+
elapsed_seconds: external_exports.number().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.")
|
|
19545
|
+
})
|
|
19546
|
+
).describe("One entry per queried job; attach completed public_urls via upload_media_asset then attach_node_media unless already auto-attached")
|
|
19547
|
+
},
|
|
18721
19548
|
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
|
|
18722
19549
|
},
|
|
18723
19550
|
async ({ job_id, job_ids }) => {
|
|
@@ -18733,38 +19560,44 @@ Pass job_ids to check a whole batch in ONE call - one line of status per job. Pa
|
|
|
18733
19560
|
}
|
|
18734
19561
|
if (job.status === "rendering") {
|
|
18735
19562
|
const elapsed = Math.round((Date.now() - job.createdAt) / 1e3);
|
|
18736
|
-
return
|
|
19563
|
+
return structuredResult(
|
|
18737
19564
|
[
|
|
18738
19565
|
`Status: rendering (${elapsed}s elapsed)`,
|
|
18739
19566
|
`Tool: ${job.tool}`,
|
|
18740
19567
|
``,
|
|
18741
19568
|
`Still in progress. Check again in ${RENDER_TIMING.pollDelay}.`
|
|
18742
|
-
].join("\n")
|
|
19569
|
+
].join("\n"),
|
|
19570
|
+
{ jobs: [projectJob(ids[0], job)] }
|
|
18743
19571
|
);
|
|
18744
19572
|
}
|
|
18745
19573
|
if (job.status === "failed") {
|
|
18746
19574
|
return errorResult(`Render failed: ${job.error}`);
|
|
18747
19575
|
}
|
|
18748
19576
|
const data = job.result;
|
|
18749
|
-
|
|
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).`];
|
|
19580
|
+
return structuredResult(
|
|
18750
19581
|
[
|
|
18751
19582
|
`Render complete.`,
|
|
18752
19583
|
...data.public_url ? [`Public URL: ${data.public_url}`] : [],
|
|
18753
19584
|
...data.duration_seconds ? [`Duration: ${data.duration_seconds}s`] : [],
|
|
18754
|
-
|
|
18755
|
-
].join("\n")
|
|
19585
|
+
...attachLines
|
|
19586
|
+
].join("\n"),
|
|
19587
|
+
{ jobs: [projectJob(ids[0], job)] }
|
|
18756
19588
|
);
|
|
18757
19589
|
}
|
|
18758
19590
|
const results = ids.map((id) => describeJob(id, getJob(id)));
|
|
18759
19591
|
const pending = results.filter((r) => !r.done).length;
|
|
18760
19592
|
const lost = results.some((r) => r.line.includes("NOT FOUND"));
|
|
18761
|
-
return
|
|
19593
|
+
return structuredResult(
|
|
18762
19594
|
[
|
|
18763
19595
|
`Render batch: ${results.length - pending}/${results.length} finished${pending ? ` - check again in ${RENDER_TIMING.pollDelay}` : ""}.`,
|
|
18764
19596
|
...results.map((r) => r.line),
|
|
18765
|
-
...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).`] : [],
|
|
18766
19598
|
...lost ? [``, LOST_JOB_RECOVERY] : []
|
|
18767
|
-
].join("\n")
|
|
19599
|
+
].join("\n"),
|
|
19600
|
+
{ jobs: ids.map((id) => projectJob(id, getJob(id))) }
|
|
18768
19601
|
);
|
|
18769
19602
|
}
|
|
18770
19603
|
);
|
|
@@ -19053,7 +19886,8 @@ function createServer(options) {
|
|
|
19053
19886
|
registerAddNodeTool(server);
|
|
19054
19887
|
registerUpdateNodeTool(server);
|
|
19055
19888
|
registerDeleteNodeTool(server);
|
|
19056
|
-
|
|
19889
|
+
registerUploadMediaAssetTool(server);
|
|
19890
|
+
registerAttachNodeMediaTool(server);
|
|
19057
19891
|
registerGetNodeMediaTool(server);
|
|
19058
19892
|
registerDeleteNodeMediaTool(server);
|
|
19059
19893
|
registerSetNodeLogicTool(server);
|
|
@@ -19066,6 +19900,8 @@ function createServer(options) {
|
|
|
19066
19900
|
registerRenderCompositionTool(server);
|
|
19067
19901
|
registerSearchMusicTool(server);
|
|
19068
19902
|
registerListCompositionsTool(server);
|
|
19903
|
+
registerListVideoTemplatesTool(server);
|
|
19904
|
+
registerRenderVideoTemplateTool(server);
|
|
19069
19905
|
registerListAssetsTool(server);
|
|
19070
19906
|
registerCheckRenderTool(server);
|
|
19071
19907
|
registerFetchBoundaryTool(server);
|
|
@@ -19078,6 +19914,7 @@ function createServer(options) {
|
|
|
19078
19914
|
|
|
19079
19915
|
export {
|
|
19080
19916
|
JSONRPCMessageSchema,
|
|
19917
|
+
RENDER_TIMING,
|
|
19081
19918
|
createServer
|
|
19082
19919
|
};
|
|
19083
|
-
//# sourceMappingURL=chunk-
|
|
19920
|
+
//# sourceMappingURL=chunk-RX7V2UCM.js.map
|