@instavar/mcp-server 0.3.0 → 0.5.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.
Files changed (3) hide show
  1. package/README.md +107 -15
  2. package/dist/index.js +1390 -120
  3. package/package.json +10 -3
package/dist/index.js CHANGED
@@ -4,12 +4,559 @@
4
4
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
5
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
6
 
7
- // src/tools.ts
7
+ // src/creative-guides.ts
8
+ import { z } from "zod";
9
+
10
+ // src/creative-guides.generated.ts
11
+ var CREATIVE_GUIDES = [
12
+ {
13
+ "id": "visual-copy",
14
+ "title": "Concise plain and poetic visual copy",
15
+ "uri": "instavar://creative-guides/visual-copy",
16
+ "text": "# Write concise text for visual stories\n\nStatus: practical guidance, adapted from documented writing methods and bounded human feedback. This is not a measured guarantee of comprehension or engagement.\n\nUse for carousel headlines, supporting text, captions and video scripts. Begin with plain language. Add poetic expression when the brief asks for an experience or mood; keep instructions, factual claims and interface states literal.\n\n## Start with the viewer and the evidence\n\nName the audience, what they already know, the one idea they should understand, and the action or feeling the piece invites. List supplied facts and necessary qualifications separately from imagined details. Inspect the accompanying image before describing it. If it is unavailable, ask for context or mark the visual description as a proposal.\n\nDecide what the image demonstrates and what the words must add. A caption can rely on an accompanying picture only when the intended viewer receives that picture. Provide a self-contained alternative description for accessibility and text-only reuse.\n\n## Expand, select, connect, compress\n\n1. Expand enough to understand the full thought. Find the concrete objects, actions, relationships, evidence and limits.\n2. Select what must survive. In an explanation, preserve the causal link, units, conditions and uncertainty. In poetic copy, preserve the scene, voice and details that carry the experience.\n3. Connect the ideas in the order a new viewer needs them. Each slide or scene should advance the explanation or experience.\n4. Compress by removing repeated meaning, vague labels and redundant explanations. Read it again against the source. Shorter wording must not make the claim stronger.\n\nTreat brevity as revision pressure, not a word-count target. If a condition matters to the decision, retain it close to that decision. A tiny footnote or a later slide may be missed when the current slide is shared alone.\n\n## Use poetic language selectively\n\nLet a concrete action or sensory detail suggest emotion before naming the emotion. Choose details that belong to this scene. Movement, posture, pressure, temperature, hesitation and small corrections can matter as much as sight or sound. Do not force every sense into a passage.\n\nMetaphor, personification and rhythm should serve one governing idea. Remove mixed metaphors and phrases that could describe any product. Leave room for interpretation in expressive copy, but do not leave factual instructions ambiguous. Do not invent an observed event, testimonial, feeling or product result. Sensuality is an optional requested mode, not a default meaning of poetic.\n\n## Draft examples\n\nThese are newly authored teaching examples, not approved campaign copy.\n\n| Purpose | Weak draft | More concrete draft | What must remain |\n| ------------------- | --------------------------------------------- | --------------------------------------------------------------------- | ------------------------------------------------------- |\n| Product instruction | Experience seamless creative transformation. | Choose a clip. Add your caption. Preview the result. | Name only actions the product actually supports. |\n| Quiet atmosphere | Discover the magic of a new morning. | The kettle clicks. Light reaches the table. | Use only supplied or explicitly imagined scene details. |\n| Evidence summary | This format makes everyone understand faster. | In this review, two readers found the revised chart easier to follow. | Counts, population and evidence source must be real. |\n\n## Deliver copy the creator can use\n\nReturn the title and caption separately. For a carousel or video, give one row per slide or scene: purpose, on-screen text, visual evidence, optional narration, and necessary qualification. Keep narration separate from on-screen copy so the screen need not repeat every spoken sentence.\n\nUse line breaks to shape meaningful groups. A single newline can keep short actions together; a blank line can mark a change of scene or thought. Follow the requested channel formatting. Neither spacing rule is universal.\n\nReview the actual render at the intended viewing size. Read the headline sequence alone, then the complete piece. Ask an unfamiliar viewer what happened, why, and what they would do next. A correct schema or short sentence does not establish reader understanding.\n\n## Reusable request\n\n\u201CUsing the supplied facts and visuals, draft concise copy for [audience] in [carousel/video format]. The viewer should understand [idea]. Use [plain/poetic] language. Give each scene a clear job, keep necessary conditions and evidence, and separate on-screen text from narration. Mark missing facts rather than inventing them. Return title, scene copy, caption and a short source-preservation check.\u201D\n\nSee the companion carousel, design and human-review guides. Source provenance and scope are recorded in the repository's September 2026 creative-playbook transfer report.\n"
17
+ },
18
+ {
19
+ "id": "carousel-stories",
20
+ "title": "Carousel story structures and transfer limits",
21
+ "uri": "instavar://creative-guides/carousel-stories",
22
+ "text": "# Choose a carousel story structure\n\nStatus: planning recipes adapted from the Eclat carousel programme. Recipes are not newly ported renderers or proof of audience performance.\n\nChoose a structure for the relationship being explained. A topic is not a template: circular motion, collisions and half-life can share a questioning structure while needing different diagrams, conditions and notation.\n\n## Existing Instavar templates\n\nThe HTTP template MCP currently scaffolds `listicle`, `manifesto`, `before-after` and `value-first-reveal`. Use `list_carousel_templates` and `get_carousel_schema` for the executable contract. The npm connector's video tools are a separate catalogue; these recipes do not imply npm carousel rendering support.\n\n- Listicle: parallel points that belong together.\n- Manifesto: one idea per slide in a connected progression.\n- Before/after: a genuine two-state contrast. This builder uses the first and last point; do not put essential intermediate reasoning into points that will be omitted.\n- Value-first reveal: useful information followed by evidence and a restrained product reveal. Do not invent product proof to fill a slot.\n\n## Newer educational recipes\n\n| Recipe | Reading path | Use when | Preserve |\n| -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------- | ---------------------------------------------------------------- |\n| Question, evidence, explanation, check | Ask a specific question; show the relevant objects; explain the relationship; compare a case; ask a final check with its answer | A plausible misconception can be resolved visually | The conditions that make the explanation true; a complete answer |\n| Observation to conclusion | Show an observation; separate it from inference; compare explanations; state what can be concluded | The viewer must reason from visible evidence | Uncertainty and alternative explanations |\n| Worked comparison | Establish objects and quantities; show the symbolic relationship; substitute values; compare outcomes; conclude | A calculation needs to stay connected to the physical situation | Units, signs, definitions and unchanged variables |\n| Action and correction | Show the setup; show an action; show a small correction; show the result | A practical action or sensory moment carries the story | Safe technique, sequence and a truthful visible result |\n\nThese can be drafted with one idea per slide using the manifesto skeleton, then reviewed and edited. Diagrams, equation blocks, editable placeholders and scientific apparatus require an appropriate construction layer; the text scaffold does not create or verify them. Never flatten a necessary diagram into decorative background artwork.\n\n## Lessons from the newer drafts\n\nKeep the relevant objects beside their equations so the viewer can connect symbols to the situation. State a useful symbolic relationship before numerical substitution when that helps the intended reader. Typeset fractions and symbols rather than using ambiguous slash text.\n\nGroup related words with their diagram. Give each calculation slide one main job. Remove duplicate results when the chart already carries them, but retain necessary assumptions. Make supporting text readable at delivery size. Recheck the whole deck after a local change.\n\nThe September 2026 Eclat batch included photoelectric threshold, circular motion, terminal velocity, sticking collision, wave amplitude, SHM turning points, standing waves, single-slit diffraction, field versus potential and half-life. A later six-deck revision applied earlier feedback to circular motion, SHM, standing waves, diffraction, field/potential and half-life. Those revised drafts were awaiting human review in the inspected record. This topic inventory is provenance, not a claim of ten new Instavar templates.\n\n## Delivery check\n\nReview full masters and every intended crop. A centred 4:5 crop of a 9:16 master was an Eclat production choice, not a universal platform requirement. Keep essential text, diagrams and qualifications inside the actual delivery region; verify current platform constraints separately before publication.\n\nTransfer the story method. Choose branding, type, texture, colour and imagery for the current creator. Eclat logos, source images, specialist answers and approval labels do not transfer automatically.\n"
23
+ },
24
+ {
25
+ "id": "visual-design",
26
+ "title": "UI, graphic design and video principles",
27
+ "uri": "instavar://creative-guides/visual-design",
28
+ "text": "# Make meaning visible across slides, video and interfaces\n\nStatus: practical design and review guidance. Context-specific aesthetic and outcome claims still need audience evidence.\n\n## Give attention a useful path\n\nStart with the audience's task. Identify what should be noticed first, what it means, where attention moves next, and what evidence supports the conclusion. Use hierarchy to express that path. The first meaningful text should usually carry the main idea; a small label above it earns its place only when it changes interpretation.\n\nMatch form to relationship: sequence for steps, side-by-side comparison for alternatives, a diagram for a mechanism, and an explicit state change for an action. Do not force every relationship into the same card layout. Make the heading sequence tell a connected story while the full reading supplies proof and limits.\n\n## Graphic design\n\nUse proximity, alignment, spacing, scale, weight and contrast to show grouping and priority. Keep type and colour roles consistent. Test text against its actual background and at its final size; do not assume a colour token alone guarantees contrast. Keep non-colour cues for important distinctions.\n\nTypography and colour can suggest tone, but their meanings depend on audience and context. A serif does not prove quality and a particular hue does not guarantee trust. Choose an expressive treatment only when the core content remains legible.\n\nReview local details, the whole image and the collection separately. A sharper object or stronger accent may weaken overall harmony. Compare against the verified source image; a file labelled original can still be an edited derivative. Retaining the original is a valid outcome.\n\n## Video and motion\n\nPlan what changes in the viewer's understanding across time. Show the setup before an action that requires it. Keep spatial and causal continuity when the viewer must follow a procedure. Use cuts, movement and sound to reveal a relationship or shift attention, not simply to add activity.\n\nGive viewers enough time to read the actual text and inspect the evidence. Test at normal playback speed. Treat muted review, picture review, narration review and the combined sound mix as separate checks. Time visible contact and its sound together when synchrony matters. Keep necessary information available when audio is absent.\n\nDo not import film pacing into interactive controls blindly. A film controls its timeline; an interface must preserve user agency, orientation and recovery.\n\n## UI and editing workflows\n\nMake the current object, possible action, saved state and consequence clear. Keep frequent controls visible at the intended viewport. Distinguish selection, dragging and text editing. Provide undo and understandable conflict handling for editable work. A preview and its export should reflect the same saved document.\n\nThese are review questions, not claims that the Eclat prototype's editor has been integrated into Instavar. Prototype gestures, exact button layouts and crop choices require fresh implementation and testing in the receiving product.\n\n## Verify the artifact\n\nInspect final-size text, crops, diagram labels, symbols, contrast, grouping and the overall sequence. For interfaces, also exercise keyboard use, focus, narrow viewports, saving, errors and recovery. For video, review timing and sound in playback rather than relying only on contact sheets.\n\nAutomated checks establish their measured properties, such as dimensions, parsing or bounds. They do not establish human preference, comprehension or campaign performance. Ask a new viewer to explain the idea or perform the intended task before making those stronger claims.\n"
29
+ },
30
+ {
31
+ "id": "human-review",
32
+ "title": "Human-guided copy, visual and media review",
33
+ "uri": "instavar://creative-guides/human-review",
34
+ "text": "# Preserve human feedback without inventing approval\n\nStatus: repository adaptation of the shared copy, UI/UX and media recording protocols. This guide is read-only; it does not store feedback or publish media.\n\nRecord a meaningful transition when a person reviews an identifiable draft, gives concrete feedback and a later revision responds. Preserve the exact draft and context, the feedback, the revision and the follow-up judgment. Do not treat every autonomous adjustment as human-guided work.\n\n## Keep one owning record\n\nUse an existing workstream report when available. Otherwise use `reports/copy/<workstream>/` for wording, `reports/ui-ux/` for layout and controls, or `reports/media/<workstream>/` for rendered picture, pacing, narration and sound. Link overlapping records instead of duplicating their histories. Keep old evidence at its existing paths.\n\nFor copy, preserve exact text, punctuation and line breaks. Record title and caption separately. For visual work, identify slide, route, state, viewport and before/after render. For media, also identify seconds or frames and playback conditions, including whether sound was on. If evidence is unavailable or a revision is source-only, say so.\n\n## Record template\n\n- Artifact and audience:\n- Before version, exact text or safe render reference:\n- Reviewed context, slide/range, viewport or playback conditions:\n- Human feedback, exact wording when safe:\n- Perceived problem and requested change:\n- Edit hypothesis:\n- Revision and after version:\n- Kept stable:\n- Follow-up judgment: not_judged / accepted_in_scope / rejected / mixed / superseded\n- Acceptance scope:\n- Publication: not_requested / requested / authorized / sent / verified, with evidence\n- Audience outcome: not_measured, or the actual observation and its limits\n- Source provenance, rights status and safe evidence references:\n- New issue and successor reference, if any:\n\nUse superseded only with a successor and keep the earlier judgment. A new complaint begins a new transition; chronology alone does not prove the previous change caused it.\n\n## What transfers\n\nKeep the problem-solving principle distinct from the particular remedy. \u201CThese calculation slides look cluttered\u201D can motivate grouping and removing repeated results. It does not justify removing assumptions from all science slides. A preference for one background, font size or line break applies to the reviewed context until further evidence supports transfer.\n\nApproval of a local detail is not approval of a whole image. Approval of a silent video does not approve the mix. Delivery is not approval, and approval is not evidence of audience impact. Silence remains not_judged.\n\nUse only public, owned or cleared examples in shared resources. Do not copy private screenshots, raw conversations, source photographs or unrelated account details to complete a record. A file hash identifies bytes; it does not establish rights, accuracy or quality. Recording feedback does not authorize publication or admission to a training or taste-critic corpus.\n"
35
+ }
36
+ ];
37
+
38
+ // src/creative-guides.ts
39
+ var creativeGuideIdSchema = z.enum([
40
+ "visual-copy",
41
+ "carousel-stories",
42
+ "visual-design",
43
+ "human-review"
44
+ ]);
45
+ var CREATIVE_GUIDE_INDEX = CREATIVE_GUIDES.map(
46
+ ({ id: id2, title, uri }) => ({
47
+ id: id2,
48
+ title,
49
+ uri
50
+ })
51
+ );
52
+ function registerCreativeGuides(server) {
53
+ for (const guide of CREATIVE_GUIDES) {
54
+ server.registerResource(
55
+ guide.id,
56
+ guide.uri,
57
+ {
58
+ title: guide.title,
59
+ description: guide.title,
60
+ mimeType: "text/markdown"
61
+ },
62
+ async () => ({
63
+ contents: [
64
+ { uri: guide.uri, mimeType: "text/markdown", text: guide.text }
65
+ ]
66
+ })
67
+ );
68
+ }
69
+ server.registerTool(
70
+ "get_creative_guide",
71
+ {
72
+ title: "Get a visual communication guide",
73
+ description: "Read practical guidance before drafting concise plain or poetic copy, choosing carousel stories, designing visuals or recording human feedback. Returns guidance for the calling assistant to apply, not generated copy or saved feedback. Omit guideId to list guides.",
74
+ inputSchema: { guideId: creativeGuideIdSchema.optional() },
75
+ annotations: {
76
+ readOnlyHint: true,
77
+ destructiveHint: false,
78
+ openWorldHint: false
79
+ },
80
+ _meta: { securitySchemes: [{ type: "noauth" }] }
81
+ },
82
+ async ({ guideId }) => {
83
+ const guide = CREATIVE_GUIDES.find((entry) => entry.id === guideId);
84
+ return {
85
+ structuredContent: guide ? { guide } : { guides: CREATIVE_GUIDE_INDEX },
86
+ content: [
87
+ {
88
+ type: "text",
89
+ text: guide ? guide.text : CREATIVE_GUIDE_INDEX.map(
90
+ (entry) => `${entry.id}: ${entry.title} (${entry.uri})`
91
+ ).join("\n")
92
+ }
93
+ ]
94
+ };
95
+ }
96
+ );
97
+ }
98
+
99
+ // src/public-templates.ts
8
100
  import { z as z2 } from "zod";
101
+ var TEMPLATE_REPOSITORY = "https://github.com/instavar/remotion-templates";
102
+ var TEMPLATE_ENDPOINT = "https://instavar.com/api/mcp/templates";
103
+ var VOICE_STUDIO_URL = "https://instavar.com/voice";
104
+ var templateIds = [
105
+ "qa-ad",
106
+ "proof-walkthrough",
107
+ "announcement-brief",
108
+ "social-remix",
109
+ "finance-brief"
110
+ ];
111
+ var templateIdSchema = z2.enum(templateIds);
112
+ var aspectSchema = z2.enum(["9:16", "4:5", "1:1"]);
113
+ var templates = [
114
+ {
115
+ id: "qa-ad",
116
+ name: "Question and answer ad",
117
+ description: "A short video with a hook, source-video window, explanation points, and call to action.",
118
+ aspects: ["9:16", "4:5", "1:1"]
119
+ },
120
+ {
121
+ id: "proof-walkthrough",
122
+ name: "Proof walkthrough",
123
+ description: "A paced explanation for equations, worked reasoning, and a final takeaway.",
124
+ aspects: ["9:16", "4:5", "1:1"]
125
+ },
126
+ {
127
+ id: "announcement-brief",
128
+ name: "Announcement brief",
129
+ description: "A concise announcement with a headline, supporting points, optional media, and next action.",
130
+ aspects: ["9:16", "4:5", "1:1"]
131
+ },
132
+ {
133
+ id: "social-remix",
134
+ name: "Social remix",
135
+ description: "A caption-forward treatment that frames an existing clip with a hook, context, and call to action.",
136
+ aspects: ["9:16", "4:5", "1:1"]
137
+ },
138
+ {
139
+ id: "finance-brief",
140
+ name: "Data and finance brief",
141
+ description: "An editorial brief for comparisons, numeric highlights, evidence, caveats, and a recommendation.",
142
+ aspects: ["9:16", "4:5", "1:1"]
143
+ }
144
+ ];
145
+ var videoSpecSchema = {
146
+ $schema: "https://json-schema.org/draft/2020-12/schema",
147
+ $id: "https://instavar.com/schemas/video-spec-1.0.json",
148
+ title: "Instavar VideoSpec",
149
+ type: "object",
150
+ additionalProperties: false,
151
+ required: [
152
+ "schemaVersion",
153
+ "id",
154
+ "templateFamily",
155
+ "meta",
156
+ "target",
157
+ "scenes"
158
+ ],
159
+ properties: {
160
+ schemaVersion: { const: "1.0" },
161
+ id: { type: "string", pattern: "^[a-z0-9][a-z0-9-]{2,80}$" },
162
+ templateFamily: { enum: templateIds },
163
+ meta: {
164
+ type: "object",
165
+ required: ["title"],
166
+ properties: {
167
+ title: { type: "string", minLength: 1 },
168
+ description: { type: "string" },
169
+ tags: { type: "array", items: { type: "string" } }
170
+ }
171
+ },
172
+ target: {
173
+ type: "object",
174
+ required: ["aspect", "fps", "durationMode"],
175
+ properties: {
176
+ aspect: { enum: ["9:16", "4:5", "1:1"] },
177
+ fps: { type: "integer", minimum: 1 },
178
+ durationMode: { enum: ["auto", "fixed"] },
179
+ fixedDurationSec: { type: "number", exclusiveMinimum: 0 }
180
+ }
181
+ },
182
+ audio: {
183
+ type: "object",
184
+ properties: {
185
+ mode: { enum: ["auto", "narration", "music", "silent"] },
186
+ narrationText: { type: "string" },
187
+ narrationSrc: { type: "string" },
188
+ narrationVolume: { type: "number", minimum: 0, maximum: 1 },
189
+ musicSrc: { type: "string" },
190
+ musicVolume: { type: "number", minimum: 0, maximum: 1 },
191
+ ducking: { type: "boolean" },
192
+ narrationTiming: {
193
+ type: "object",
194
+ required: ["schemaVersion", "source", "durationMs", "segments"],
195
+ properties: {
196
+ schemaVersion: { const: "1.0" },
197
+ source: { const: "segmented-synthesis" },
198
+ durationMs: { type: "integer", minimum: 1 },
199
+ segments: {
200
+ type: "array",
201
+ minItems: 1,
202
+ maxItems: 24,
203
+ items: {
204
+ type: "object",
205
+ required: ["sceneId", "startMs", "endMs", "durationMs"],
206
+ properties: {
207
+ sceneId: { type: "string", minLength: 1 },
208
+ startMs: { type: "integer", minimum: 0 },
209
+ endMs: { type: "integer", minimum: 1 },
210
+ durationMs: { type: "integer", minimum: 1 }
211
+ }
212
+ }
213
+ }
214
+ }
215
+ }
216
+ }
217
+ },
218
+ scenes: {
219
+ type: "array",
220
+ minItems: 1,
221
+ items: {
222
+ type: "object",
223
+ required: ["id", "kind", "content"],
224
+ properties: {
225
+ id: { type: "string", minLength: 1 },
226
+ kind: {
227
+ enum: [
228
+ "hero",
229
+ "statement",
230
+ "mechanism",
231
+ "points",
232
+ "equation",
233
+ "video-window",
234
+ "cta",
235
+ "custom"
236
+ ]
237
+ },
238
+ content: { type: "object" },
239
+ timing: { type: "object" }
240
+ }
241
+ }
242
+ },
243
+ style: {
244
+ type: "object",
245
+ properties: {
246
+ theme: {
247
+ enum: ["default", "editorial-light", "editorial-dark", "clinical"]
248
+ },
249
+ variant: { type: "string" },
250
+ safeAreaProfile: { enum: ["baseline", "metaSafe"] },
251
+ showSceneLabels: { type: "boolean", default: false }
252
+ }
253
+ }
254
+ }
255
+ };
256
+ var annotations = {
257
+ readOnlyHint: true,
258
+ openWorldHint: false,
259
+ destructiveHint: false
260
+ };
261
+ function result(value, summary) {
262
+ return {
263
+ structuredContent: value,
264
+ content: [{ type: "text", text: summary }]
265
+ };
266
+ }
267
+ function slugify(value) {
268
+ const slug = value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 72);
269
+ return slug.length >= 3 ? slug : "new-video";
270
+ }
271
+ function scaffoldVideoSpec(args) {
272
+ const scenes = [
273
+ {
274
+ id: "hero",
275
+ kind: "hero",
276
+ content: {
277
+ title: args.title,
278
+ subtitle: "Replace with a concise promise"
279
+ },
280
+ timing: { durationMs: 4e3 }
281
+ }
282
+ ];
283
+ if (args.templateId === "qa-ad" || args.templateId === "social-remix") {
284
+ scenes.push({
285
+ id: "source-video",
286
+ kind: "video-window",
287
+ content: {
288
+ title: "See it in context",
289
+ src: args.mediaSrc ?? "public/replace-with-your-video.mp4"
290
+ },
291
+ timing: { durationMs: 7e3 }
292
+ });
293
+ }
294
+ if (args.templateId === "announcement-brief") {
295
+ scenes.push(
296
+ {
297
+ id: "context",
298
+ kind: "statement",
299
+ content: {
300
+ title: "Name the change",
301
+ subtitle: "Explain why it matters in one sentence"
302
+ },
303
+ timing: { durationMs: 6e3 }
304
+ },
305
+ {
306
+ id: "mechanism",
307
+ kind: "mechanism",
308
+ content: {
309
+ title: "Show how the change happens",
310
+ before: "Before",
311
+ after: "After"
312
+ },
313
+ timing: { durationMs: 7e3 }
314
+ },
315
+ {
316
+ id: "outcomes",
317
+ kind: "points",
318
+ content: {
319
+ title: "What changes",
320
+ points: ["First outcome", "Second outcome", "Third outcome"]
321
+ },
322
+ timing: { durationMs: 7e3 }
323
+ }
324
+ );
325
+ } else {
326
+ scenes.push({
327
+ id: args.templateId === "proof-walkthrough" ? "working" : "key-points",
328
+ kind: args.templateId === "proof-walkthrough" ? "equation" : "points",
329
+ content: args.templateId === "proof-walkthrough" ? {
330
+ title: "Work through the idea",
331
+ latexBlocks: ["x = \\frac{-b \\pm \\sqrt{b^2 - 4ac}}{2a}"]
332
+ } : {
333
+ title: "Key points",
334
+ points: [
335
+ "Replace with evidence",
336
+ "Replace with explanation",
337
+ "Replace with takeaway"
338
+ ]
339
+ },
340
+ timing: { durationMs: 8e3 }
341
+ });
342
+ }
343
+ scenes.push({
344
+ id: "cta",
345
+ kind: "cta",
346
+ content: { line1: "Replace with the next action" },
347
+ timing: {
348
+ durationMs: args.templateId === "announcement-brief" ? 6e3 : 5e3
349
+ }
350
+ });
351
+ return {
352
+ schemaVersion: "1.0",
353
+ id: slugify(args.title),
354
+ templateFamily: args.templateId,
355
+ meta: { title: args.title, tags: [] },
356
+ target: { aspect: args.aspect, fps: 30, durationMode: "auto" },
357
+ style: {
358
+ theme: args.templateId === "announcement-brief" ? "editorial-light" : "default",
359
+ variant: "default",
360
+ safeAreaProfile: "metaSafe",
361
+ showSceneLabels: false
362
+ },
363
+ audio: {
364
+ mode: "silent",
365
+ narrationText: "",
366
+ narrationVolume: 1,
367
+ musicVolume: 0.16,
368
+ ducking: true
369
+ },
370
+ assets: {},
371
+ scenes
372
+ };
373
+ }
374
+ function registerPublicTemplateTools(server) {
375
+ registerCreativeGuides(server);
376
+ server.registerTool(
377
+ "get_capabilities",
378
+ {
379
+ title: "Get video capabilities",
380
+ description: "Call first. Reports free local features and optional account features.",
381
+ inputSchema: {},
382
+ annotations
383
+ },
384
+ async () => result(
385
+ {
386
+ authentication: "optional",
387
+ apiKeyConfigured: Boolean(process.env.INSTAVAR_API_KEY?.trim()),
388
+ repository: TEMPLATE_REPOSITORY,
389
+ endpoint: TEMPLATE_ENDPOINT,
390
+ creativeGuides: CREATIVE_GUIDE_INDEX,
391
+ localFeatures: [
392
+ "template discovery",
393
+ "VideoSpec schema and examples",
394
+ "deterministic scaffolding",
395
+ "local render instructions",
396
+ "render error diagnosis",
397
+ "keyless on-device narration with scene timing",
398
+ "every-scene preview frames",
399
+ "post-render audible-audio verification"
400
+ ],
401
+ narration: {
402
+ url: VOICE_STUDIO_URL,
403
+ cost: "free",
404
+ apiKeyRequired: false,
405
+ guidance: "Write one paragraph per scene. Download the WAV and timing manifest, then apply both before rendering."
406
+ },
407
+ accountFeatures: [
408
+ "managed rendering and storage",
409
+ "workflow review and approval",
410
+ "social publishing and metrics"
411
+ ]
412
+ },
413
+ process.env.INSTAVAR_API_KEY?.trim() ? "Free local tools and authenticated Instavar Studio tools are available." : "Free local tools are available. Add INSTAVAR_API_KEY to unlock hosted Studio tools."
414
+ )
415
+ );
416
+ server.registerTool(
417
+ "list_templates",
418
+ {
419
+ title: "List Remotion templates",
420
+ description: "List public templates, optionally filtered by aspect ratio.",
421
+ inputSchema: { aspect: aspectSchema.optional() },
422
+ annotations
423
+ },
424
+ async ({ aspect }) => {
425
+ const filtered = aspect ? templates.filter((template) => template.aspects.includes(aspect)) : templates;
426
+ return result(
427
+ { templates: filtered },
428
+ filtered.map((template) => `${template.id}: ${template.description}`).join("\n")
429
+ );
430
+ }
431
+ );
432
+ server.registerTool(
433
+ "get_template",
434
+ {
435
+ title: "Get Remotion template",
436
+ description: "Get one public template contract.",
437
+ inputSchema: { templateId: templateIdSchema },
438
+ annotations
439
+ },
440
+ async ({ templateId }) => {
441
+ const template = templates.find((item) => item.id === templateId);
442
+ return result(
443
+ { template },
444
+ `${template?.name ?? templateId}
445
+ ${template?.description ?? ""}`
446
+ );
447
+ }
448
+ );
449
+ server.registerTool(
450
+ "get_video_schema",
451
+ {
452
+ title: "Get VideoSpec schema",
453
+ description: "Return the public VideoSpec JSON Schema.",
454
+ inputSchema: {},
455
+ annotations
456
+ },
457
+ async () => result(
458
+ { schemaVersion: "1.0", jsonSchema: videoSpecSchema },
459
+ "VideoSpec schema version 1.0."
460
+ )
461
+ );
462
+ server.registerTool(
463
+ "get_example",
464
+ {
465
+ title: "Get VideoSpec example",
466
+ description: "Return a starter example for one template.",
467
+ inputSchema: { templateId: templateIdSchema },
468
+ annotations
469
+ },
470
+ async ({ templateId }) => result(
471
+ {
472
+ templateId,
473
+ spec: scaffoldVideoSpec({
474
+ templateId,
475
+ title: `Example ${templateId}`,
476
+ aspect: "9:16"
477
+ })
478
+ },
479
+ `Example VideoSpec for ${templateId}.`
480
+ )
481
+ );
482
+ server.registerTool(
483
+ "scaffold_video_spec",
484
+ {
485
+ title: "Scaffold VideoSpec",
486
+ description: "Return a deterministic VideoSpec skeleton without writing files.",
487
+ inputSchema: {
488
+ templateId: templateIdSchema,
489
+ title: z2.string().min(1).max(160),
490
+ aspect: aspectSchema.default("9:16"),
491
+ mediaSrc: z2.string().min(1).max(500).optional()
492
+ },
493
+ annotations
494
+ },
495
+ async (args) => {
496
+ const spec = scaffoldVideoSpec(args);
497
+ return result(
498
+ { spec, filename: `${String(spec.id)}.video.json` },
499
+ "Starter VideoSpec created. Replace placeholder copy and media."
500
+ );
501
+ }
502
+ );
503
+ server.registerTool(
504
+ "get_render_instructions",
505
+ {
506
+ title: "Get local render instructions",
507
+ description: "Return clone, install, validate, preview, and render commands.",
508
+ inputSchema: {
509
+ client: z2.enum(["codex", "claude-code", "manual"]).default("manual")
510
+ },
511
+ annotations
512
+ },
513
+ async ({ client }) => {
514
+ const commands = [
515
+ `git clone ${TEMPLATE_REPOSITORY}.git`,
516
+ "cd remotion-templates",
517
+ "npm install",
518
+ `# Generate one narration paragraph per scene at ${VOICE_STUDIO_URL}`,
519
+ "npm run video:timing -- path/to/video.video.json --audio public/generated/narration.wav --manifest path/to/narration.timing.json",
520
+ "npm run video:validate -- path/to/video.video.json",
521
+ "npm run video:preview -- path/to/video.video.json",
522
+ "npm run video:render -- path/to/video.video.json",
523
+ "npm run video:qa -- path/to/video.video.json path/to/video.mp4"
524
+ ];
525
+ return result(
526
+ { repository: TEMPLATE_REPOSITORY, client, commands },
527
+ commands.join("\n")
528
+ );
529
+ }
530
+ );
531
+ server.registerTool(
532
+ "diagnose_render_error",
533
+ {
534
+ title: "Diagnose render error",
535
+ description: "Classify a local validation or render error.",
536
+ inputSchema: { errorText: z2.string().min(1).max(1e4) },
537
+ annotations
538
+ },
539
+ async ({ errorText }) => {
540
+ const text = errorText.toLowerCase();
541
+ const code = text.includes("enoent") || text.includes("not found") ? "missing-file" : text.includes("ffmpeg") || text.includes("codec") ? "media-codec" : text.includes("schema") || text.includes("invalid") || text.includes("zod") ? "invalid-video-spec" : text.includes("chrome") || text.includes("browser") ? "browser-runtime" : "unknown-render-error";
542
+ return result(
543
+ {
544
+ code,
545
+ nextStep: "Run validation first, confirm Node 22 or newer, and retry with the smallest included example."
546
+ },
547
+ `Diagnosis: ${code}.`
548
+ );
549
+ }
550
+ );
551
+ }
552
+
553
+ // src/tools.ts
554
+ import { z as z5 } from "zod";
9
555
 
10
556
  // src/http.ts
11
557
  import { randomUUID } from "crypto";
12
558
  var API_KEY_HEADER = "x-api-key";
559
+ var API_REQUEST_TIMEOUT_MS = 15e3;
13
560
  function getApiKey() {
14
561
  const key = process.env.INSTAVAR_API_KEY?.trim();
15
562
  if (!key) {
@@ -33,8 +580,22 @@ function explain(status, body) {
33
580
  const reason = body && typeof body === "object" && "reason" in body ? String(body.reason) : "insufficient scope";
34
581
  return `Forbidden (403): ${reason}. The key lacks the required scope for this action.`;
35
582
  }
583
+ if (status === 409) {
584
+ const reason = body && typeof body === "object" && "error" in body ? String(body.error) : "The requested change conflicts with the current server state";
585
+ const recovery = /workflow changed/i.test(reason) ? "Inspect the latest workflow state before retrying." : "Resolve the reported conflict before retrying.";
586
+ return `Conflict (409): ${reason}. ${recovery}`;
587
+ }
36
588
  return "";
37
589
  }
590
+ function requestFailure(error) {
591
+ if (error instanceof DOMException && (error.name === "AbortError" || error.name === "TimeoutError")) {
592
+ return "The Instavar API request timed out. Retry the tool call.";
593
+ }
594
+ if (error instanceof TypeError) {
595
+ return "The Instavar API request failed before a response arrived. Check connectivity and retry the tool call.";
596
+ }
597
+ return error instanceof Error ? error.message : String(error);
598
+ }
38
599
  async function parse(res) {
39
600
  const text = await res.text();
40
601
  try {
@@ -45,11 +606,16 @@ async function parse(res) {
45
606
  }
46
607
  async function apiGet(path) {
47
608
  const res = await fetch(`${baseUrl()}${path}`, {
48
- headers: { [API_KEY_HEADER]: getApiKey() }
609
+ headers: { [API_KEY_HEADER]: getApiKey() },
610
+ signal: AbortSignal.timeout(API_REQUEST_TIMEOUT_MS)
49
611
  });
50
612
  const body = await parse(res);
51
613
  const hint = explain(res.status, body);
52
- return { ok: res.ok, status: res.status, body: hint ? { error: hint, body } : body };
614
+ return {
615
+ ok: res.ok,
616
+ status: res.status,
617
+ body: hint ? { error: hint, body } : body
618
+ };
53
619
  }
54
620
  async function apiPost(path, payload) {
55
621
  const clean = Object.fromEntries(
@@ -61,11 +627,16 @@ async function apiPost(path, payload) {
61
627
  [API_KEY_HEADER]: getApiKey(),
62
628
  "content-type": "application/json"
63
629
  },
64
- body: JSON.stringify(clean)
630
+ body: JSON.stringify(clean),
631
+ signal: AbortSignal.timeout(API_REQUEST_TIMEOUT_MS)
65
632
  });
66
633
  const body = await parse(res);
67
634
  const hint = explain(res.status, body);
68
- return { ok: res.ok, status: res.status, body: hint ? { error: hint, body } : body };
635
+ return {
636
+ ok: res.ok,
637
+ status: res.status,
638
+ body: hint ? { error: hint, body } : body
639
+ };
69
640
  }
70
641
  function fenceForModel(json) {
71
642
  const nonce = randomUUID();
@@ -76,15 +647,15 @@ ${open}
76
647
  ${json}
77
648
  ${close}`;
78
649
  }
79
- function toToolResult(result) {
650
+ function toToolResult(result2) {
80
651
  return {
81
652
  content: [
82
653
  {
83
654
  type: "text",
84
- text: fenceForModel(JSON.stringify(result.body, null, 2))
655
+ text: fenceForModel(JSON.stringify(result2.body, null, 2))
85
656
  }
86
657
  ],
87
- isError: !result.ok
658
+ isError: !result2.ok
88
659
  };
89
660
  }
90
661
  async function runTool(fn) {
@@ -95,7 +666,7 @@ async function runTool(fn) {
95
666
  content: [
96
667
  {
97
668
  type: "text",
98
- text: error instanceof Error ? error.message : String(error)
669
+ text: requestFailure(error)
99
670
  }
100
671
  ],
101
672
  isError: true
@@ -104,7 +675,7 @@ async function runTool(fn) {
104
675
  }
105
676
 
106
677
  // src/brief-schema.ts
107
- import { z } from "zod";
678
+ import { z as z3 } from "zod";
108
679
  var PUBLISH_TARGETS = [
109
680
  "linkedin",
110
681
  "x",
@@ -127,13 +698,153 @@ var BRIEF_OBJECTIVES = [
127
698
  "other"
128
699
  ];
129
700
  var ASPECT_RATIOS = ["9:16", "4:5", "1:1", "16:9"];
130
- var briefStepSchema = z.object({
131
- label: z.string(),
132
- latex: z.string().optional(),
133
- explanation: z.string().optional()
701
+ var REFERENCE_ROLES = [
702
+ "identity",
703
+ "appearance",
704
+ "motion",
705
+ "composition",
706
+ "camera",
707
+ "environment",
708
+ "style"
709
+ ];
710
+ var optionalDirectionText = z3.string().trim().min(1).max(500).optional();
711
+ var visualDirectionSchema = z3.object({
712
+ communicationJob: optionalDirectionText,
713
+ emotionalTarget: optionalDirectionText,
714
+ attentionPath: z3.object({
715
+ first: optionalDirectionText,
716
+ next: optionalDirectionText,
717
+ proof: optionalDirectionText
718
+ }).optional(),
719
+ visualLanguage: z3.object({
720
+ space: optionalDirectionText,
721
+ line: optionalDirectionText,
722
+ shape: optionalDirectionText,
723
+ tone: optionalDirectionText,
724
+ color: optionalDirectionText,
725
+ movement: optionalDirectionText,
726
+ rhythm: optionalDirectionText
727
+ }).optional(),
728
+ camera: z3.object({
729
+ shotSize: optionalDirectionText,
730
+ angle: optionalDirectionText,
731
+ movement: optionalDirectionText,
732
+ lens: optionalDirectionText
733
+ }).optional()
134
734
  });
135
- var briefSceneSchema = z.object({
136
- type: z.enum([
735
+ var generationContractSchema = z3.object({
736
+ globalInvariants: z3.array(z3.string().trim().min(1).max(300)).max(20).optional(),
737
+ shotDelta: z3.string().trim().min(1).max(500).optional(),
738
+ referenceBindings: z3.array(
739
+ z3.object({
740
+ assetId: z3.string().trim().min(1).max(200),
741
+ roles: z3.array(z3.enum(REFERENCE_ROLES)).min(1).max(7)
742
+ })
743
+ ).max(20).optional(),
744
+ prohibitedChanges: z3.array(z3.string().trim().min(1).max(300)).max(20).optional(),
745
+ reviewCriteria: z3.array(z3.string().trim().min(1).max(300)).max(20).optional()
746
+ });
747
+ var REGION_NAMES = [
748
+ "background",
749
+ "left-half",
750
+ "right-half",
751
+ "figure-center",
752
+ "corner-inset",
753
+ "lower-third"
754
+ ];
755
+ var ASSET_ROLES = [
756
+ "background",
757
+ "figure",
758
+ "inset",
759
+ "lower-third",
760
+ "diagram-node"
761
+ ];
762
+ var assetSlotStyleSchema = z3.object({
763
+ opacity: z3.number().min(0).max(1).optional(),
764
+ fit: z3.enum(["cover", "contain"]).optional(),
765
+ z: z3.number().optional()
766
+ }).strict();
767
+ var assetSlotSchema = z3.object({
768
+ id: z3.string().min(1),
769
+ sceneId: z3.string().min(1).optional(),
770
+ region: z3.enum(REGION_NAMES),
771
+ role: z3.enum(ASSET_ROLES),
772
+ kind: z3.enum(["image", "video"]),
773
+ source: z3.enum(["auto", "stock", "generate"]),
774
+ intent: z3.string(),
775
+ style: assetSlotStyleSchema.optional(),
776
+ timing: z3.object({
777
+ fromPct: z3.number().min(0).max(1).optional(),
778
+ toPct: z3.number().min(0).max(1).optional()
779
+ }).strict().optional(),
780
+ url: z3.string().url().optional()
781
+ }).strict();
782
+ var briefStepSchema = z3.object({
783
+ label: z3.string(),
784
+ latex: z3.string().optional(),
785
+ explanation: z3.string().optional()
786
+ });
787
+ var briefAudioLayerSchema = z3.object({
788
+ id: z3.string().min(1),
789
+ src: z3.string().url(),
790
+ startFrame: z3.number().int().nonnegative().default(0),
791
+ durationInFrames: z3.number().int().positive().optional(),
792
+ volume: z3.number().min(0).max(1).default(1),
793
+ fadeFrames: z3.number().int().nonnegative().default(0),
794
+ role: z3.enum(["music", "foley", "transition", "effect"])
795
+ });
796
+ var protectedRegionEffectSchema = z3.object({
797
+ id: z3.string().min(1).max(80),
798
+ kind: z3.literal("border-glint"),
799
+ coordinateSpace: z3.literal("output-frame-v1").default("output-frame-v1"),
800
+ region: z3.object({
801
+ leftPercent: z3.number().min(0).max(100),
802
+ topPercent: z3.number().min(0).max(100),
803
+ widthPercent: z3.number().positive().max(100),
804
+ heightPercent: z3.number().positive().max(100)
805
+ }),
806
+ startFrame: z3.number().int().nonnegative().default(0),
807
+ endFrame: z3.number().int().positive(),
808
+ speedUpFrame: z3.number().int().nonnegative().optional(),
809
+ color: z3.string().max(64).default("#ffffff"),
810
+ thicknessPercent: z3.number().positive().max(20).default(3)
811
+ }).superRefine((effect2, context) => {
812
+ if (effect2.region.leftPercent + effect2.region.widthPercent > 100) {
813
+ context.addIssue({
814
+ code: "custom",
815
+ message: "Protected region exceeds the output frame width.",
816
+ path: ["region", "widthPercent"]
817
+ });
818
+ }
819
+ if (effect2.region.topPercent + effect2.region.heightPercent > 100) {
820
+ context.addIssue({
821
+ code: "custom",
822
+ message: "Protected region exceeds the output frame height.",
823
+ path: ["region", "heightPercent"]
824
+ });
825
+ }
826
+ if (effect2.endFrame <= effect2.startFrame) {
827
+ context.addIssue({
828
+ code: "custom",
829
+ message: "Effect endFrame must be after startFrame.",
830
+ path: ["endFrame"]
831
+ });
832
+ }
833
+ if (effect2.speedUpFrame !== void 0 && (effect2.speedUpFrame < effect2.startFrame || effect2.speedUpFrame >= effect2.endFrame)) {
834
+ context.addIssue({
835
+ code: "custom",
836
+ message: "speedUpFrame must fall inside the effect window.",
837
+ path: ["speedUpFrame"]
838
+ });
839
+ }
840
+ });
841
+ var beatVisualRequestSchema = z3.object({
842
+ intent: z3.string().trim().min(1).optional(),
843
+ media: z3.enum(["still", "motion"]).default("still"),
844
+ source: z3.enum(["auto", "stock", "generate"]).default("auto")
845
+ }).strict();
846
+ var briefSceneSchema = z3.object({
847
+ type: z3.enum([
137
848
  "hero",
138
849
  "points",
139
850
  "equation",
@@ -149,121 +860,435 @@ var briefSceneSchema = z.object({
149
860
  "callout",
150
861
  "step-pill",
151
862
  "cta-pulse",
863
+ "source-clip",
864
+ "source-image",
865
+ "route-card",
152
866
  "video-window",
867
+ "image-window",
153
868
  "custom"
154
869
  ]),
155
- durationInFrames: z.number().optional(),
156
- narrationText: z.string().optional(),
157
- title: z.string().optional(),
158
- subtitle: z.string().optional(),
159
- text: z.string().optional(),
160
- points: z.array(z.string()).optional(),
161
- latex: z.string().optional(),
162
- value: z.union([z.string(), z.number()]).optional(),
163
- max: z.number().optional(),
164
- label: z.string().optional(),
165
- unit: z.string().optional(),
166
- prefix: z.string().optional(),
167
- suffix: z.string().optional(),
168
- decimals: z.number().optional(),
169
- color: z.string().optional(),
170
- tone: z.string().optional(),
171
- iconKey: z.string().optional(),
172
- stepNumber: z.number().optional(),
173
- totalSteps: z.number().optional(),
174
- items: z.array(z.object({ label: z.string(), value: z.number() })).optional(),
175
- src: z.string().optional(),
176
- caption: z.string().optional(),
870
+ durationInFrames: z3.number().optional(),
871
+ narrationText: z3.string().optional(),
872
+ visualResolution: beatVisualRequestSchema.optional(),
873
+ title: z3.string().optional(),
874
+ subtitle: z3.string().optional(),
875
+ text: z3.string().optional(),
876
+ points: z3.array(z3.string()).optional(),
877
+ latex: z3.string().optional(),
878
+ value: z3.union([z3.string(), z3.number()]).optional(),
879
+ max: z3.number().optional(),
880
+ label: z3.string().optional(),
881
+ unit: z3.string().optional(),
882
+ prefix: z3.string().optional(),
883
+ suffix: z3.string().optional(),
884
+ decimals: z3.number().optional(),
885
+ color: z3.string().optional(),
886
+ tone: z3.string().optional(),
887
+ iconKey: z3.string().optional(),
888
+ stepNumber: z3.number().optional(),
889
+ totalSteps: z3.number().optional(),
890
+ items: z3.array(z3.object({ label: z3.string(), value: z3.number() })).optional(),
891
+ src: z3.string().optional(),
892
+ caption: z3.string().optional(),
177
893
  // A-roll generation (video-window scenes): source "generate" triggers
178
894
  // flag-gated WaveSpeed image/video gen server-side from generatePrompt;
179
895
  // "url" (default) uses src as-is. Kept in sync with the app's
180
896
  // briefSceneSchema - see the parity test in the app suite.
181
- source: z.enum(["url", "generate"]).optional(),
182
- generatePrompt: z.string().optional()
897
+ source: z3.enum(["url", "generate"]).optional(),
898
+ generatePrompt: z3.string().optional(),
899
+ generationContract: generationContractSchema.optional(),
900
+ trimStartSeconds: z3.number().nonnegative().optional(),
901
+ trimEndSeconds: z3.number().positive().optional(),
902
+ playbackRate: z3.number().min(0.25).max(8).optional(),
903
+ objectFit: z3.enum(["cover", "contain"]).optional(),
904
+ objectPosition: z3.string().optional(),
905
+ scaleFrom: z3.number().min(0.5).max(2).optional(),
906
+ scaleTo: z3.number().min(0.5).max(2).optional(),
907
+ panFromX: z3.number().min(-50).max(50).optional(),
908
+ panToX: z3.number().min(-50).max(50).optional(),
909
+ panFromY: z3.number().min(-50).max(50).optional(),
910
+ panToY: z3.number().min(-50).max(50).optional(),
911
+ routeFrom: z3.string().optional(),
912
+ routeTo: z3.string().optional(),
913
+ routeLabel: z3.string().optional(),
914
+ overlayKind: z3.enum(["none", "hook", "stage", "end-card"]).optional(),
915
+ overlayPosition: z3.enum(["center", "top", "bottom"]).optional(),
916
+ accentColor: z3.string().optional(),
917
+ overlayBackgroundColor: z3.string().optional(),
918
+ cutFlash: z3.boolean().optional(),
919
+ muted: z3.boolean().optional(),
920
+ protectedRegionEffects: z3.array(protectedRegionEffectSchema).max(4).optional(),
921
+ flashAtFrame: z3.number().int().nonnegative().optional(),
922
+ flashDurationFrames: z3.number().int().positive().max(15).optional()
183
923
  });
184
- var directionsBriefSchema = z.object({
185
- subtitle: z.string().optional(),
186
- unitLabel: z.string().optional(),
187
- distanceTimeChip: z.string().optional(),
188
- clips: z.array(
189
- z.object({
190
- src: z.string(),
191
- trimStartSeconds: z.number().optional(),
192
- trimEndSeconds: z.number(),
193
- speed: z.number().optional(),
194
- mirrorFlip: z.boolean().optional()
924
+ var directionsBriefSchema = z3.object({
925
+ subtitle: z3.string().optional(),
926
+ unitLabel: z3.string().optional(),
927
+ distanceTimeChip: z3.string().optional(),
928
+ clips: z3.array(
929
+ z3.object({
930
+ src: z3.string(),
931
+ trimStartSeconds: z3.number().optional(),
932
+ trimEndSeconds: z3.number(),
933
+ speed: z3.number().optional(),
934
+ mirrorFlip: z3.boolean().optional()
195
935
  })
196
936
  ).min(1),
197
- cues: z.array(
198
- z.object({
199
- atSeconds: z.number(),
200
- text: z.string(),
201
- arrow: z.enum(["left", "right", "forward"]).optional(),
202
- step: z.number().optional()
937
+ cues: z3.array(
938
+ z3.object({
939
+ atSeconds: z3.number(),
940
+ text: z3.string(),
941
+ arrow: z3.enum(["left", "right", "forward"]).optional(),
942
+ step: z3.number().optional()
203
943
  })
204
944
  ),
205
- outro: z.object({
206
- heading: z.string().optional(),
207
- body: z.array(z.string()).optional(),
208
- seconds: z.number().optional()
945
+ outro: z3.object({
946
+ heading: z3.string().optional(),
947
+ body: z3.array(z3.string()).optional(),
948
+ seconds: z3.number().optional()
209
949
  }).optional(),
210
- music: z.object({
211
- perSegment: z.array(
212
- z.object({
213
- src: z.string(),
214
- clipIndexStart: z.number().optional(),
215
- hotOffsetSeconds: z.number().optional(),
216
- volume: z.number().optional()
950
+ music: z3.object({
951
+ perSegment: z3.array(
952
+ z3.object({
953
+ src: z3.string(),
954
+ clipIndexStart: z3.number().optional(),
955
+ hotOffsetSeconds: z3.number().optional(),
956
+ volume: z3.number().optional()
217
957
  })
218
958
  ).optional(),
219
- crossfadeSeconds: z.number().optional()
959
+ crossfadeSeconds: z3.number().optional()
220
960
  }).optional()
221
961
  });
222
962
  var createBriefShape = {
223
- title: z.string().min(1).max(120).describe("Short title (max 120 chars)"),
224
- script: z.string().min(1).describe("Full narration script (sent to TTS)"),
225
- caption: z.string().min(1).describe("Social caption with hooks, hashtags, CTA"),
226
- publishTarget: z.enum(PUBLISH_TARGETS).describe("Target social platform"),
227
- objective: z.enum(BRIEF_OBJECTIVES).describe("Content objective (drives composition routing)"),
228
- lessonTitle: z.string().optional().describe("Display title rendered inside the video"),
229
- steps: z.array(briefStepSchema).optional().describe("Structured steps for educational content (LaTeX)"),
230
- scenes: z.array(briefSceneSchema).optional().describe("Scene graph for the UniversalVideo composition"),
963
+ title: z3.string().min(1).max(120).describe("Short title (max 120 chars)"),
964
+ script: z3.string().min(1).describe("Full narration script (sent to TTS)"),
965
+ caption: z3.string().min(1).describe("Social caption with hooks, hashtags, CTA"),
966
+ publishTarget: z3.enum(PUBLISH_TARGETS).describe("Target social platform"),
967
+ publishTargets: z3.array(z3.enum(PUBLISH_TARGETS)).max(9).optional().describe("All allowed social destinations for this video"),
968
+ objective: z3.enum(BRIEF_OBJECTIVES).describe("Content objective (drives composition routing)"),
969
+ targetDurationSeconds: z3.number().int().min(5).max(600).optional().describe("Requested final video duration in seconds"),
970
+ narrationMode: z3.enum(["tts", "off"]).optional().describe("Disable synthesized narration for source-audio or silent work"),
971
+ lessonTitle: z3.string().optional().describe("Display title rendered inside the video"),
972
+ visualDirection: visualDirectionSchema.optional().describe(
973
+ "Whole-video communication, attention, visual-language, and camera direction."
974
+ ),
975
+ steps: z3.array(briefStepSchema).optional().describe("Structured steps for educational content (LaTeX)"),
976
+ scenes: z3.array(briefSceneSchema).optional().describe("Scene graph for the UniversalVideo composition"),
231
977
  // No zod .default() here: a default would materialize into the parsed tool
232
978
  // arguments and relay to /api/v1/jobs as if the caller stated it, corrupting
233
979
  // the resolution provenance the API echoes back. Unset must stay absent;
234
980
  // the API re-applies 9:16 server-side.
235
- aspectRatio: z.enum(ASPECT_RATIOS).optional().describe("Aspect ratio (defaults to 9:16 when omitted)"),
236
- compositionId: z.string().optional().describe("Remotion composition ID override (advanced)"),
237
- voiceId: z.string().optional().describe(
981
+ aspectRatio: z3.enum(ASPECT_RATIOS).optional().describe("Aspect ratio (defaults to 9:16 when omitted)"),
982
+ compositionId: z3.string().optional().describe("Remotion composition ID override (advanced)"),
983
+ voiceId: z3.string().optional().describe(
238
984
  "TTS narrator voice ID. Unknown IDs are rejected before rendering; when omitted, the org's saved voice preference or the system default is used and reported in the response's resolution block."
239
985
  ),
240
986
  directions: directionsBriefSchema.optional().describe(
241
987
  "DirectionsWalkthrough 'find-us' payload: source clips (legs), decision-point cues on the realtime walk timeline, outro, and optional music. Set compositionId 'DirectionsWalkthrough' (or objective 'directions')."
242
- )
988
+ ),
989
+ assetSlots: z3.array(assetSlotSchema).optional().describe(
990
+ "Optional storyboard asset slots: region-placed image/video assets or placeholders attached to the whole video or a specific scene."
991
+ ),
992
+ audioLayers: z3.array(briefAudioLayerSchema).optional().describe("Optional typed music, Foley, transition, and effect layers.")
243
993
  };
244
994
  var editBriefShape = {
245
- jobId: z.string().uuid().describe("The job to edit"),
246
- title: z.string().optional().describe("Updated title"),
247
- script: z.string().optional().describe("Updated narration script"),
248
- caption: z.string().optional().describe("Updated social caption"),
249
- lessonTitle: z.string().optional().describe("Updated lesson title"),
250
- steps: z.array(briefStepSchema).optional().describe("Updated steps (replaces the array)"),
251
- scenes: z.array(briefSceneSchema).optional().describe("Updated scenes (replaces the array)"),
252
- aspectRatio: z.enum(ASPECT_RATIOS).optional().describe("Updated aspect ratio"),
253
- objective: z.enum(BRIEF_OBJECTIVES).optional().describe(
995
+ jobId: z3.string().uuid().describe("The job to edit"),
996
+ title: z3.string().optional().describe("Updated title"),
997
+ script: z3.string().optional().describe("Updated narration script"),
998
+ caption: z3.string().optional().describe("Updated social caption"),
999
+ lessonTitle: z3.string().optional().describe("Updated lesson title"),
1000
+ narrationMode: z3.enum(["tts", "off"]).optional().describe("Updated synthesized narration mode"),
1001
+ visualDirection: visualDirectionSchema.optional().describe("Updated whole-video visual direction"),
1002
+ steps: z3.array(briefStepSchema).optional().describe("Updated steps (replaces the array)"),
1003
+ scenes: z3.array(briefSceneSchema).optional().describe("Updated scenes (replaces the array)"),
1004
+ aspectRatio: z3.enum(ASPECT_RATIOS).optional().describe("Updated aspect ratio"),
1005
+ objective: z3.enum(BRIEF_OBJECTIVES).optional().describe(
254
1006
  "Updated content objective. Re-routes the composition and triggers a re-render."
255
1007
  ),
256
- publishTarget: z.enum(PUBLISH_TARGETS).optional().describe(
1008
+ publishTarget: z3.enum(PUBLISH_TARGETS).optional().describe(
257
1009
  "Updated target social platform. Distribution metadata only - does not trigger a re-render."
258
1010
  ),
259
- voiceId: z.string().optional().describe(
1011
+ publishTargets: z3.array(z3.enum(PUBLISH_TARGETS)).max(9).optional().describe("Replacement allowed social destination list"),
1012
+ targetDurationSeconds: z3.number().int().min(5).max(600).optional().describe("Updated final video duration in seconds"),
1013
+ voiceId: z3.string().optional().describe(
260
1014
  "Updated TTS narrator voice ID. Triggers a re-render with the new voice."
1015
+ ),
1016
+ assetSlots: z3.array(assetSlotSchema).optional().describe(
1017
+ "Replacement storyboard asset slots. Omit to preserve existing slots; pass [] to clear them."
1018
+ ),
1019
+ audioLayers: z3.array(briefAudioLayerSchema).optional().describe(
1020
+ "Replacement typed audio layers. Omit to preserve existing layers; pass [] to clear them."
261
1021
  )
262
1022
  };
263
1023
 
1024
+ // src/render-diagnostics.ts
1025
+ function record(value) {
1026
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
1027
+ }
1028
+ function rows(value) {
1029
+ return Array.isArray(value) ? value.map(record).filter((row) => !!row) : [];
1030
+ }
1031
+ function string(value) {
1032
+ return typeof value === "string" && value.trim() ? value.trim() : null;
1033
+ }
1034
+ function number(value) {
1035
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
1036
+ }
1037
+ function currentVideoArtifact(status) {
1038
+ return rows(status?.artifacts).find(
1039
+ (artifact) => artifact.artifactType === "video" && artifact.isCurrent === true
1040
+ ) ?? null;
1041
+ }
1042
+ function verificationMatches(verifications, pattern) {
1043
+ return verifications.filter(
1044
+ (verification) => pattern.test(
1045
+ [
1046
+ verification.phase,
1047
+ verification.checkKey,
1048
+ verification.verifier,
1049
+ JSON.stringify(verification.detailsJson ?? null)
1050
+ ].map((value) => String(value ?? "")).join(" ")
1051
+ )
1052
+ );
1053
+ }
1054
+ function buildRenderDiagnosticSnapshot(params) {
1055
+ const failed = [params.jobStatus, params.videoState].find(
1056
+ (result2) => !result2.ok
1057
+ );
1058
+ if (failed) return failed;
1059
+ const status = record(params.jobStatus.body);
1060
+ const stateResponse = record(params.videoState.body);
1061
+ const state = record(stateResponse?.state);
1062
+ const runs = rows(status?.runs);
1063
+ const latestRunId = string(status?.latestRunId);
1064
+ const latestRun = runs.find((run) => string(run.id) === latestRunId) ?? runs[0] ?? null;
1065
+ const videoArtifact = currentVideoArtifact(status);
1066
+ const artifactMetadata = record(videoArtifact?.metadataJson);
1067
+ const verifications = rows(status?.verificationTimeline);
1068
+ const blankChecks = verificationMatches(verifications, /blank|black frame/i);
1069
+ const placeholderChecks = verificationMatches(
1070
+ verifications,
1071
+ /placeholder|source clip|source asset/i
1072
+ );
1073
+ const rightsChecks = verificationMatches(
1074
+ verifications,
1075
+ /rights|licen[cs]|clearance/i
1076
+ );
1077
+ const currentVideoArtifactId = string(videoArtifact?.id);
1078
+ const stateVideoArtifactId = string(state?.currentVideoArtifactId);
1079
+ const expectedDurationSeconds = number(state?.durationSeconds) ?? number(status?.durationSec);
1080
+ const actualDurationSeconds = number(videoArtifact?.durationSeconds) ?? number(artifactMetadata?.durationSeconds) ?? number(artifactMetadata?.durationSec) ?? number(artifactMetadata?.duration);
1081
+ return {
1082
+ ok: true,
1083
+ status: 200,
1084
+ body: {
1085
+ job: {
1086
+ id: string(status?.id),
1087
+ title: string(status?.title),
1088
+ status: string(status?.status),
1089
+ latestRunId
1090
+ },
1091
+ latestRun: latestRun ? {
1092
+ id: string(latestRun.id),
1093
+ runType: string(latestRun.runType),
1094
+ status: string(latestRun.status),
1095
+ errorCode: string(latestRun.errorCode),
1096
+ errorMessage: string(latestRun.errorMessage)
1097
+ } : null,
1098
+ artifactBinding: {
1099
+ jobCurrentVideoArtifactId: currentVideoArtifactId,
1100
+ videoStateCurrentArtifactId: stateVideoArtifactId,
1101
+ matches: currentVideoArtifactId && stateVideoArtifactId ? currentVideoArtifactId === stateVideoArtifactId : null,
1102
+ currentVideoUrl: string(status?.currentVideoUrl) ?? string(state?.currentVideoUrl)
1103
+ },
1104
+ duration: {
1105
+ expectedSeconds: expectedDurationSeconds,
1106
+ actualSeconds: actualDurationSeconds,
1107
+ deltaSeconds: expectedDurationSeconds !== null && actualDurationSeconds !== null ? Math.round(
1108
+ (actualDurationSeconds - expectedDurationSeconds) * 1e3
1109
+ ) / 1e3 : null
1110
+ },
1111
+ retryRecommendations: Array.isArray(status?.retryRecommendations) ? status?.retryRecommendations : [],
1112
+ evidence: {
1113
+ rights: rightsChecks,
1114
+ blankFrames: blankChecks,
1115
+ placeholders: placeholderChecks,
1116
+ videoState: state
1117
+ },
1118
+ evidenceLimits: {
1119
+ visualContentInspection: blankChecks.length || placeholderChecks.length ? "verification_rows_available" : "not_evaluated",
1120
+ note: "No blank or placeholder verdict is inferred when the current verification timeline contains no matching check. Inspect the video artifact visually before deciding on an edit."
1121
+ }
1122
+ }
1123
+ };
1124
+ }
1125
+
1126
+ // src/design-action-schema.ts
1127
+ import { z as z4 } from "zod";
1128
+ var id = z4.string().min(1).max(120);
1129
+ var point = z4.object({ x: z4.number().min(0).max(1), y: z4.number().min(0).max(1) }).strict();
1130
+ var polygonShape = z4.object({
1131
+ kind: z4.literal("polygon"),
1132
+ points: z4.array(point).min(3).max(512)
1133
+ }).strict();
1134
+ var rasterShape = z4.object({
1135
+ kind: z4.literal("raster"),
1136
+ source: z4.object({
1137
+ assetId: id,
1138
+ expectedSha256: z4.string().regex(/^[a-f0-9]{64}$/),
1139
+ sourceRange: z4.object({
1140
+ startFrame: z4.number().int().nonnegative(),
1141
+ endFrame: z4.number().int().positive()
1142
+ }).strict().optional()
1143
+ }).strict(),
1144
+ widthPx: z4.number().int().positive().max(8192),
1145
+ heightPx: z4.number().int().positive().max(8192)
1146
+ }).strict();
1147
+ var shape = z4.discriminatedUnion("kind", [polygonShape, rasterShape]);
1148
+ var mask = z4.object({
1149
+ id,
1150
+ shape,
1151
+ operation: z4.enum(["add", "subtract"]),
1152
+ inverted: z4.boolean().default(false),
1153
+ feather: z4.number().finite().nonnegative().max(0.25).default(0),
1154
+ allowedSupportMargin: z4.number().finite().nonnegative().max(0.25).default(0),
1155
+ reviewState: z4.enum(["draft", "approved", "rejected"]),
1156
+ provenance: z4.object({
1157
+ kind: z4.enum(["manual", "imported", "model-proposed"]),
1158
+ sourceAssetId: id.optional(),
1159
+ modelId: z4.string().min(1).max(160).optional()
1160
+ }).strict()
1161
+ }).strict();
1162
+ var transform = z4.object({
1163
+ x: z4.number().min(0).max(1),
1164
+ y: z4.number().min(0).max(1),
1165
+ width: z4.number().positive().max(1),
1166
+ height: z4.number().positive().max(1),
1167
+ rotationDegrees: z4.number().min(-360).max(360),
1168
+ opacity: z4.number().min(0).max(1)
1169
+ }).strict();
1170
+ var colourParameters = {
1171
+ exposure: z4.number().finite().min(-2).max(2).default(0),
1172
+ contrast: z4.number().finite().min(0.5).max(2).default(1),
1173
+ saturation: z4.number().finite().min(0).max(2).default(1),
1174
+ temperature: z4.number().finite().min(-1).max(1).default(0),
1175
+ tint: z4.number().finite().min(-1).max(1).default(0)
1176
+ };
1177
+ var effect = z4.object({
1178
+ id,
1179
+ kind: z4.literal("local-colour"),
1180
+ version: z4.literal(1),
1181
+ maskId: id,
1182
+ ...colourParameters
1183
+ }).strict();
1184
+ var motionPath = z4.object({
1185
+ id,
1186
+ kind: z4.literal("cubic-bezier"),
1187
+ startFrame: z4.number().int().nonnegative(),
1188
+ endFrame: z4.number().int().positive(),
1189
+ start: point,
1190
+ control1: point,
1191
+ control2: point,
1192
+ end: point,
1193
+ easing: z4.enum(["linear", "ease-in", "ease-out", "ease-in-out"]),
1194
+ orientToPath: z4.boolean().default(false)
1195
+ }).strict();
1196
+ var region = z4.object({ id, shape, label: z4.string().min(1).max(160) }).strict();
1197
+ var base = { actionId: id, sceneId: id };
1198
+ var vendoredDesignActionSchema = z4.discriminatedUnion("kind", [
1199
+ z4.object({
1200
+ ...base,
1201
+ kind: z4.literal("set-layer-transform"),
1202
+ layerId: id,
1203
+ transform
1204
+ }).strict(),
1205
+ z4.object({
1206
+ ...base,
1207
+ kind: z4.literal("set-local-colour-effect"),
1208
+ effectId: id,
1209
+ parameters: z4.object(colourParameters).strict()
1210
+ }).strict(),
1211
+ z4.object({ ...base, kind: z4.literal("create-mask"), mask }).strict(),
1212
+ z4.object({ ...base, kind: z4.literal("set-mask"), maskId: id, mask }).strict(),
1213
+ z4.object({
1214
+ ...base,
1215
+ kind: z4.literal("set-mask-review-state"),
1216
+ maskId: id,
1217
+ reviewState: z4.enum(["draft", "approved", "rejected"])
1218
+ }).strict(),
1219
+ z4.object({ ...base, kind: z4.literal("delete-mask"), maskId: id }).strict(),
1220
+ z4.object({
1221
+ ...base,
1222
+ kind: z4.literal("attach-effect"),
1223
+ effect,
1224
+ layerIds: z4.array(id).min(1).max(50)
1225
+ }).strict(),
1226
+ z4.object({ ...base, kind: z4.literal("remove-effect"), effectId: id }).strict(),
1227
+ z4.object({
1228
+ ...base,
1229
+ kind: z4.literal("set-layer-text"),
1230
+ layerId: id,
1231
+ text: z4.string().max(1e4)
1232
+ }).strict(),
1233
+ z4.object({
1234
+ ...base,
1235
+ kind: z4.literal("set-layer-visibility"),
1236
+ layerId: id,
1237
+ visible: z4.boolean()
1238
+ }).strict(),
1239
+ z4.object({
1240
+ ...base,
1241
+ kind: z4.literal("set-layer-locked"),
1242
+ layerId: id,
1243
+ locked: z4.boolean()
1244
+ }).strict(),
1245
+ z4.object({
1246
+ ...base,
1247
+ kind: z4.literal("set-layer-mask-ids"),
1248
+ layerId: id,
1249
+ maskIds: z4.array(id).max(16)
1250
+ }).strict(),
1251
+ z4.object({
1252
+ ...base,
1253
+ kind: z4.literal("set-layer-order"),
1254
+ orderedLayerIds: z4.array(id).min(1).max(500)
1255
+ }).strict(),
1256
+ z4.object({
1257
+ ...base,
1258
+ kind: z4.literal("set-motion-path"),
1259
+ path: motionPath,
1260
+ layerIds: z4.array(id).max(50).default([])
1261
+ }).strict(),
1262
+ z4.object({ ...base, kind: z4.literal("remove-motion-path"), pathId: id }).strict(),
1263
+ z4.object({ ...base, kind: z4.literal("set-protected-region"), region }).strict(),
1264
+ z4.object({
1265
+ ...base,
1266
+ kind: z4.literal("remove-protected-region"),
1267
+ regionId: id
1268
+ }).strict()
1269
+ ]);
1270
+ var inspectDesignDocumentToolSchema = z4.object({
1271
+ projectId: z4.string().uuid(),
1272
+ documentId: z4.string().uuid(),
1273
+ includeVersions: z4.boolean().default(true)
1274
+ }).strict();
1275
+ var applyDesignActionsToolSchema = z4.object({
1276
+ projectId: z4.string().uuid(),
1277
+ documentId: z4.string().uuid(),
1278
+ actionGroupId: z4.string().min(1).max(120),
1279
+ expectedVersion: z4.number().int().positive(),
1280
+ actions: z4.array(vendoredDesignActionSchema).min(1).max(50),
1281
+ clientCreatedAt: z4.string().datetime()
1282
+ }).strict();
1283
+
264
1284
  // src/tools.ts
265
- var uuid = (desc) => z2.string().uuid().describe(desc);
266
- var READ_ONLY = { readOnlyHint: true, openWorldHint: false };
1285
+ var uuid = (desc) => z5.string().uuid().describe(desc);
1286
+ var READ_ONLY = {
1287
+ readOnlyHint: true,
1288
+ destructiveHint: false,
1289
+ idempotentHint: true,
1290
+ openWorldHint: false
1291
+ };
267
1292
  var SAFE_WRITE = {
268
1293
  readOnlyHint: false,
269
1294
  destructiveHint: false,
@@ -277,8 +1302,8 @@ function registerTools(server) {
277
1302
  title: "List jobs",
278
1303
  description: "List recent video jobs for the current organization (newest first). Start here to discover job IDs; for one job's full detail use get_job_status.",
279
1304
  inputSchema: {
280
- limit: z2.number().int().min(1).max(50).default(10).describe("Number of jobs to return (default 10, max 50)"),
281
- status: z2.enum([
1305
+ limit: z5.number().int().min(1).max(50).default(10).describe("Number of jobs to return (default 10, max 50)"),
1306
+ status: z5.enum([
282
1307
  "queued",
283
1308
  "rendering",
284
1309
  "draft_ready",
@@ -319,6 +1344,23 @@ function registerTools(server) {
319
1344
  () => apiGet(`/api/v1/jobs/${encodeURIComponent(jobId)}/video-state`)
320
1345
  )
321
1346
  );
1347
+ server.registerTool(
1348
+ "diagnose_render",
1349
+ {
1350
+ title: "Diagnose render",
1351
+ description: "Build one read-only troubleshooting snapshot from the current job status and video state. Reports run failures, artifact binding, duration evidence, retry recommendations, rights checks, and whether blank-frame or placeholder verification evidence exists. It does not visually inspect the video.",
1352
+ inputSchema: { jobId: uuid("The job ID to diagnose") },
1353
+ annotations: READ_ONLY
1354
+ },
1355
+ async ({ jobId }) => runTool(async () => {
1356
+ const encodedJobId = encodeURIComponent(jobId);
1357
+ const [jobStatus, videoState] = await Promise.all([
1358
+ apiGet(`/api/v1/jobs/${encodedJobId}`),
1359
+ apiGet(`/api/v1/jobs/${encodedJobId}/video-state`)
1360
+ ]);
1361
+ return buildRenderDiagnosticSnapshot({ jobStatus, videoState });
1362
+ })
1363
+ );
322
1364
  server.registerTool(
323
1365
  "get_job_metrics",
324
1366
  {
@@ -326,7 +1368,7 @@ function registerTools(server) {
326
1368
  description: "Get the latest platform engagement metric snapshots for a published job (newest first). Read-only. Only meaningful after publish_job; for render progress use get_job_status.",
327
1369
  inputSchema: {
328
1370
  jobId: uuid("The job ID"),
329
- limit: z2.number().int().min(1).max(20).default(10).describe("Max metric snapshots (default 10, max 20)")
1371
+ limit: z5.number().int().min(1).max(20).default(10).describe("Max metric snapshots (default 10, max 20)")
330
1372
  },
331
1373
  annotations: READ_ONLY
332
1374
  },
@@ -342,10 +1384,10 @@ function registerTools(server) {
342
1384
  title: "Get cost summary",
343
1385
  description: "Query production infrastructure costs (Lambda, RunPod, R2, WaveSpeed, PoYo), optionally with reconciliation drift. This is spend data, not engagement \u2014 for a published job's audience numbers use get_job_metrics.",
344
1386
  inputSchema: {
345
- since: z2.string().optional().describe("ISO date \u2014 only include events after this date"),
346
- provider: z2.enum(["runpod", "lambda", "r2", "wavespeed", "poyo", "all"]).default("all").describe("Filter by provider"),
347
- groupBy: z2.enum(["provider", "job", "day"]).default("provider").describe("How to aggregate the summary"),
348
- includeReconciliation: z2.boolean().default(false).describe("Include account-wide reconciliation events")
1387
+ since: z5.string().optional().describe("ISO date \u2014 only include events after this date"),
1388
+ provider: z5.enum(["runpod", "lambda", "r2", "wavespeed", "poyo", "all"]).default("all").describe("Filter by provider"),
1389
+ groupBy: z5.enum(["provider", "job", "day"]).default("provider").describe("How to aggregate the summary"),
1390
+ includeReconciliation: z5.boolean().default(false).describe("Include account-wide reconciliation events")
349
1391
  },
350
1392
  annotations: READ_ONLY
351
1393
  },
@@ -356,6 +1398,55 @@ function registerTools(server) {
356
1398
  return runTool(() => apiGet(`/api/v1/cost-summary?${q.toString()}`));
357
1399
  }
358
1400
  );
1401
+ server.registerTool(
1402
+ "inspect_workflow",
1403
+ {
1404
+ title: "Inspect workflow",
1405
+ description: "Inspect the current Studio workflow and its version history for a job.",
1406
+ inputSchema: z5.object({ jobId: uuid("The job ID") }).strict(),
1407
+ annotations: READ_ONLY
1408
+ },
1409
+ async ({ jobId }) => runTool(
1410
+ () => apiGet(`/api/v1/jobs/${encodeURIComponent(jobId)}/workflow`)
1411
+ )
1412
+ );
1413
+ server.registerTool(
1414
+ "inspect_design_document",
1415
+ {
1416
+ title: "Inspect design document",
1417
+ description: "Inspect one versioned design document and its recent immutable version history. This tool never changes the document.",
1418
+ inputSchema: inspectDesignDocumentToolSchema,
1419
+ annotations: READ_ONLY
1420
+ },
1421
+ async ({ projectId, documentId, includeVersions }) => {
1422
+ const query = new URLSearchParams({
1423
+ documentId,
1424
+ includeVersions: includeVersions ? "1" : "0"
1425
+ });
1426
+ return runTool(
1427
+ () => apiGet(
1428
+ `/api/v1/projects/${encodeURIComponent(
1429
+ projectId
1430
+ )}/design?${query.toString()}`
1431
+ )
1432
+ );
1433
+ }
1434
+ );
1435
+ server.registerTool(
1436
+ "apply_design_actions",
1437
+ {
1438
+ title: "Apply design actions",
1439
+ description: "Apply a reviewed group of closed, invertible design actions to an expected document version. Inspect first. A stale version returns a readable conflict and does not overwrite customer work.",
1440
+ inputSchema: applyDesignActionsToolSchema,
1441
+ annotations: SAFE_WRITE
1442
+ },
1443
+ async (input) => runTool(
1444
+ () => apiPost(
1445
+ `/api/v1/projects/${encodeURIComponent(input.projectId)}/design`,
1446
+ input
1447
+ )
1448
+ )
1449
+ );
359
1450
  server.registerTool(
360
1451
  "create_video_brief",
361
1452
  {
@@ -381,6 +1472,169 @@ function registerTools(server) {
381
1472
  );
382
1473
  }
383
1474
  );
1475
+ const workflowWrite = (operation, jobId, payload = {}) => runTool(
1476
+ () => apiPost(`/api/v1/jobs/${encodeURIComponent(jobId)}/workflow`, {
1477
+ operation,
1478
+ ...payload
1479
+ })
1480
+ );
1481
+ server.registerTool(
1482
+ "draft_workflow",
1483
+ {
1484
+ title: "Draft workflow",
1485
+ description: "Create a durable Documentary Journey or Transformation Montage workflow version. Inspect first and pass its current version, or 0 only when no workflow exists.",
1486
+ inputSchema: z5.object({
1487
+ jobId: uuid("The job ID"),
1488
+ expectedVersion: z5.number().int().nonnegative().describe("Use 0 when no workflow exists yet"),
1489
+ document: z5.unknown().describe("The complete Studio workflow document")
1490
+ }).strict(),
1491
+ annotations: SAFE_WRITE
1492
+ },
1493
+ async ({ jobId, expectedVersion, document }) => workflowWrite("draft", jobId, { expectedVersion, document })
1494
+ );
1495
+ server.registerTool(
1496
+ "patch_workflow",
1497
+ {
1498
+ title: "Patch workflow",
1499
+ description: "Create a new workflow version by recursively merging a patch into the current version.",
1500
+ inputSchema: z5.object({
1501
+ jobId: uuid("The job ID"),
1502
+ expectedVersion: z5.number().int().positive(),
1503
+ patch: z5.record(z5.string(), z5.unknown())
1504
+ }).strict(),
1505
+ annotations: SAFE_WRITE
1506
+ },
1507
+ async ({ jobId, expectedVersion, patch }) => workflowWrite("patch", jobId, { expectedVersion, patch })
1508
+ );
1509
+ server.registerTool(
1510
+ "validate_workflow",
1511
+ {
1512
+ title: "Validate workflow",
1513
+ description: "Validate and compile the inspected workflow version without rendering or publishing it.",
1514
+ inputSchema: z5.object({
1515
+ jobId: uuid("The job ID"),
1516
+ expectedVersion: z5.number().int().positive()
1517
+ }).strict(),
1518
+ annotations: SAFE_WRITE
1519
+ },
1520
+ async ({ jobId, expectedVersion }) => workflowWrite("validate", jobId, { expectedVersion })
1521
+ );
1522
+ server.registerTool(
1523
+ "preview_workflow",
1524
+ {
1525
+ title: "Preview workflow",
1526
+ description: "Return the current workflow's compiled UniversalVideo brief for a safe pre-render preview.",
1527
+ inputSchema: z5.object({ jobId: uuid("The job ID") }).strict(),
1528
+ annotations: READ_ONLY
1529
+ },
1530
+ async ({ jobId }) => runTool(
1531
+ () => apiGet(
1532
+ `/api/v1/jobs/${encodeURIComponent(jobId)}/workflow?operation=preview`
1533
+ )
1534
+ )
1535
+ );
1536
+ server.registerTool(
1537
+ "compare_workflows",
1538
+ {
1539
+ title: "Compare workflow versions",
1540
+ description: "Compare two durable workflow versions field by field.",
1541
+ inputSchema: z5.object({
1542
+ jobId: uuid("The job ID"),
1543
+ fromVersion: z5.number().int().positive(),
1544
+ toVersion: z5.number().int().positive()
1545
+ }).strict(),
1546
+ annotations: READ_ONLY
1547
+ },
1548
+ async ({ jobId, fromVersion, toVersion }) => {
1549
+ const query = new URLSearchParams({
1550
+ operation: "compare",
1551
+ fromVersion: String(fromVersion),
1552
+ toVersion: String(toVersion)
1553
+ });
1554
+ return runTool(
1555
+ () => apiGet(
1556
+ `/api/v1/jobs/${encodeURIComponent(
1557
+ jobId
1558
+ )}/workflow?${query.toString()}`
1559
+ )
1560
+ );
1561
+ }
1562
+ );
1563
+ server.registerTool(
1564
+ "approve_workflow",
1565
+ {
1566
+ title: "Approve workflow",
1567
+ description: "Validate and approve the inspected workflow version. This does not approve or publish the video job.",
1568
+ inputSchema: z5.object({
1569
+ jobId: uuid("The job ID"),
1570
+ expectedVersion: z5.number().int().positive()
1571
+ }).strict(),
1572
+ annotations: {
1573
+ readOnlyHint: false,
1574
+ destructiveHint: false,
1575
+ idempotentHint: true,
1576
+ openWorldHint: false
1577
+ }
1578
+ },
1579
+ async ({ jobId, expectedVersion }) => workflowWrite("approve", jobId, { expectedVersion })
1580
+ );
1581
+ server.registerTool(
1582
+ "retry_video_qa",
1583
+ {
1584
+ title: "Retry video QA",
1585
+ description: "Retry the default QA evaluator against the same current frozen video. Use only when get_job_status recommends retryTarget qa_only, or when QA is missing, unavailable, or parse-degraded. This does not regenerate media and cannot clear publish gates by itself.",
1586
+ inputSchema: {
1587
+ jobId: uuid("The current job ID"),
1588
+ runId: uuid("The current render run ID"),
1589
+ videoArtifactId: uuid("The current frozen video artifact ID")
1590
+ },
1591
+ annotations: SAFE_WRITE
1592
+ },
1593
+ async ({ jobId, runId, videoArtifactId }) => runTool(
1594
+ () => apiPost(`/api/v1/jobs/${encodeURIComponent(jobId)}/qa/retry`, {
1595
+ runId,
1596
+ videoArtifactId
1597
+ })
1598
+ )
1599
+ );
1600
+ server.registerTool(
1601
+ "audit_video_qa",
1602
+ {
1603
+ title: "Audit video QA",
1604
+ description: "Run Qwen3.6-Plus as an advisory second evaluator against the same current frozen video. Use only on an explicit request for another model, second opinion, or evaluator escalation. The result is separate from default QA and has no publish-gate effect.",
1605
+ inputSchema: {
1606
+ jobId: uuid("The current job ID"),
1607
+ runId: uuid("The current render run ID"),
1608
+ videoArtifactId: uuid("The current frozen video artifact ID")
1609
+ },
1610
+ annotations: SAFE_WRITE
1611
+ },
1612
+ async ({ jobId, runId, videoArtifactId }) => runTool(
1613
+ () => apiPost(`/api/v1/jobs/${encodeURIComponent(jobId)}/qa/audit`, {
1614
+ runId,
1615
+ videoArtifactId
1616
+ })
1617
+ )
1618
+ );
1619
+ server.registerTool(
1620
+ "regenerate_narration",
1621
+ {
1622
+ title: "Regenerate narration",
1623
+ description: "Regenerate narration from the unchanged durable brief and then regenerate its dependent video in a paid adaptation run. Use only on an explicit user request or when get_job_status recommends retryTarget tts. This is an audio_and_video cascade, consumes rerender quota, and requires fresh QA.",
1624
+ inputSchema: {
1625
+ jobId: uuid("The current job ID"),
1626
+ sourceRunId: uuid("The current run ID"),
1627
+ trigger: z5.enum(["user_requested", "verifier_recommendation"])
1628
+ },
1629
+ annotations: SAFE_WRITE
1630
+ },
1631
+ async ({ jobId, sourceRunId, trigger }) => runTool(
1632
+ () => apiPost(
1633
+ `/api/v1/jobs/${encodeURIComponent(jobId)}/narration/regenerate`,
1634
+ { sourceRunId, trigger }
1635
+ )
1636
+ )
1637
+ );
384
1638
  server.registerTool(
385
1639
  "approve_job",
386
1640
  {
@@ -406,15 +1660,15 @@ function registerTools(server) {
406
1660
  description: "Publish an APPROVED job to its connected social destination via the gated publish API. All production gates run (paid plan, ownership, abuse, QA, disclosure, destination, rights, moderation). Requires a key with publish scope; the post goes live on the external platform and cannot be recalled by the API. For YouTube pass youtubePrivacyStatus:'private' for safe runs.",
407
1661
  inputSchema: {
408
1662
  jobId: uuid("The approved job to publish"),
409
- youtubePrivacyStatus: z2.enum(["private", "unlisted", "public"]).optional().describe("YouTube only. Use 'private' for safe dogfood runs."),
410
- tiktokPostMode: z2.enum(["direct_publish", "draft_upload"]).optional().describe("TikTok only. Use 'draft_upload' for safe dogfood runs."),
411
- connectedAccountDestinationId: z2.string().uuid().optional().describe(
1663
+ youtubePrivacyStatus: z5.enum(["private", "unlisted", "public"]).optional().describe("YouTube only. Use 'private' for safe dogfood runs."),
1664
+ tiktokPostMode: z5.enum(["direct_publish", "draft_upload"]).optional().describe("TikTok only. Use 'draft_upload' for safe dogfood runs."),
1665
+ connectedAccountDestinationId: z5.string().uuid().optional().describe(
412
1666
  "Specific destination when the org has multiple accounts for the provider."
413
1667
  ),
414
- confirmSyntheticDisclosure: z2.boolean().optional().describe(
1668
+ confirmSyntheticDisclosure: z5.boolean().optional().describe(
415
1669
  "Set true when the job requires an AI synthetic-media disclosure."
416
1670
  ),
417
- confirmQaOverride: z2.boolean().optional().describe("Set true to override a non-passing QA gate.")
1671
+ confirmQaOverride: z5.boolean().optional().describe("Set true to override a non-passing QA gate.")
418
1672
  },
419
1673
  annotations: {
420
1674
  readOnlyHint: false,
@@ -437,7 +1691,7 @@ function registerTools(server) {
437
1691
  title: "Connect social account",
438
1692
  description: "Start connecting a social account headlessly (no local browser needed). Returns an approve URL + a short confirm code: open the URL in a browser signed in to Instavar, check the code matches THIS terminal, and approve. Then poll connect_account_status with the returned pairingId until it reports 'connected'.",
439
1693
  inputSchema: {
440
- provider: z2.enum([
1694
+ provider: z5.enum([
441
1695
  "youtube",
442
1696
  "x",
443
1697
  "tiktok",
@@ -475,12 +1729,20 @@ function registerTools(server) {
475
1729
  );
476
1730
  }
477
1731
 
1732
+ // src/configured-tools.ts
1733
+ function registerConfiguredTools(server, apiKey = process.env.INSTAVAR_API_KEY) {
1734
+ registerPublicTemplateTools(server);
1735
+ if (apiKey?.trim()) {
1736
+ registerTools(server);
1737
+ }
1738
+ }
1739
+
478
1740
  // package.json
479
1741
  var package_default = {
480
1742
  name: "@instavar/mcp-server",
481
- version: "0.3.0",
1743
+ version: "0.5.0",
482
1744
  mcpName: "com.instavar/mcp-server",
483
- description: "Instavar Studio MCP server \u2014 drive the full video create\u2192render\u2192approve\u2192publish\u2192metrics workflow from Claude Code or Codex with an API key.",
1745
+ description: "Make local videos with Instavar Remotion templates for free, then add an API key for hosted Studio workflows.",
484
1746
  type: "module",
485
1747
  license: "MIT",
486
1748
  homepage: "https://instavar.com",
@@ -491,7 +1753,14 @@ var package_default = {
491
1753
  bugs: {
492
1754
  url: "https://github.com/instavar/mcp-server/issues"
493
1755
  },
494
- keywords: ["mcp", "model-context-protocol", "instavar", "video", "claude", "codex"],
1756
+ keywords: [
1757
+ "mcp",
1758
+ "model-context-protocol",
1759
+ "instavar",
1760
+ "video",
1761
+ "claude",
1762
+ "codex"
1763
+ ],
495
1764
  bin: {
496
1765
  "instavar-mcp": "dist/index.js"
497
1766
  },
@@ -525,8 +1794,10 @@ var VERSION = package_default.version;
525
1794
  var HELP = [
526
1795
  "instavar-mcp \u2014 Instavar Studio MCP server",
527
1796
  "",
528
- "Runs over stdio (launched by Claude Code / Codex). Configure via env:",
529
- " INSTAVAR_API_KEY (required) your key from https://instavar.com/studio/settings",
1797
+ "Runs over stdio (launched by Claude Code / Codex).",
1798
+ "Public template tools work without configuration.",
1799
+ "Optional env:",
1800
+ " INSTAVAR_API_KEY unlocks hosted Studio tools",
530
1801
  " INSTAVAR_BASE_URL (optional) defaults to https://instavar.com",
531
1802
  "",
532
1803
  "Flags: --version, --help",
@@ -543,14 +1814,13 @@ async function main() {
543
1814
  process.stdout.write(HELP);
544
1815
  return;
545
1816
  }
546
- if (!process.env.INSTAVAR_API_KEY?.trim()) {
547
- process.stderr.write(
548
- "instavar-mcp: INSTAVAR_API_KEY is not set. Create a key at https://instavar.com/studio/settings and add it to the MCP server env block (env.INSTAVAR_API_KEY).\n"
549
- );
550
- process.exit(1);
551
- }
552
- const server = new McpServer({ name: "instavar", version: VERSION });
553
- registerTools(server);
1817
+ const server = new McpServer(
1818
+ { name: "instavar", version: VERSION },
1819
+ {
1820
+ instructions: "Before drafting concise carousel or video copy, call get_creative_guide for visual-copy. Use the carousel-stories, visual-design and human-review guides as relevant. Apply them to the user brief; the guides do not generate, render, save or publish media."
1821
+ }
1822
+ );
1823
+ registerConfiguredTools(server);
554
1824
  await server.connect(new StdioServerTransport());
555
1825
  }
556
1826
  main().catch((error) => {