@instavar/mcp-server 0.2.2 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +87 -15
  2. package/dist/index.js +818 -114
  3. package/package.json +10 -3
package/README.md CHANGED
@@ -4,19 +4,58 @@
4
4
  [![npm](https://img.shields.io/npm/v/%40instavar%2Fmcp-server)](https://www.npmjs.com/package/@instavar/mcp-server)
5
5
  [![Glama score](https://glama.ai/mcp/servers/instavar/mcp-server/badges/score.svg)](https://glama.ai/mcp/servers/instavar/mcp-server)
6
6
 
7
- Drive the full [Instavar Studio](https://instavar.com) video workflow —
8
- **create → render → approve → publish → metrics** — from Claude Code or Codex,
9
- without leaving your terminal.
7
+ Make local videos with Instavar's open-source Remotion templates from Claude
8
+ Code or Codex. The public template, schema, scaffold, and troubleshooting tools
9
+ work without an account or API key.
10
10
 
11
- The server is a thin HTTP client: it authenticates with a per-user API key and
12
- talks to the hosted Instavar API. It holds **no database connection** and stores
13
- no data of its own.
11
+ Add an Instavar API key when you want the hosted workflow:
12
+ **create → render → approve → publish → metrics**. The authenticated tools are
13
+ thin HTTP clients to the hosted Instavar API. The package holds **no database
14
+ connection** and stores no data of its own.
15
+
16
+ ## Which model runs?
17
+
18
+ The MCP host supplies the general-purpose LLM.
19
+
20
+ - In Claude Code, the selected Claude model interprets the request and calls
21
+ Instavar tools.
22
+ - In Codex, the selected OpenAI model interprets the request and calls the same
23
+ tools.
24
+ - Other compatible hosts use their own selected model and authentication.
25
+
26
+ The MCP server does not provide Sonnet, GPT, or another general-purpose model.
27
+ It exposes typed Instavar capabilities to the model already running in the
28
+ user's host. Depending on the host and sign-in method, that model usage may draw
29
+ from a personal or workspace subscription, separate usage credits, or API
30
+ billing. Those provider rules can change.
31
+
32
+ Local Remotion rendering uses your own machine. Instavar meters or charges its
33
+ hosted services, including rendering,
34
+ storage, specialist media generation, QA, publishing, and other declared job
35
+ costs. Using a subscription-backed agent does not make those services free.
36
+
37
+ For the full architecture, see
38
+ [`docs/architecture/studio-agent-workflow-architecture.md`](../../docs/architecture/studio-agent-workflow-architecture.md).
39
+ For dated provider evidence, see
40
+ [`reports/research/subscription-backed-agent-runtime-2026-07-10.md`](../../reports/research/subscription-backed-agent-runtime-2026-07-10.md).
14
41
 
15
42
  ## Setup
16
43
 
17
- 1. Create an API key at **https://instavar.com/studio/settings** (shown once —
18
- copy it).
19
- 2. Add the server to your MCP client config:
44
+ Add the server to your MCP client config:
45
+
46
+ ```json
47
+ {
48
+ "mcpServers": {
49
+ "instavar": {
50
+ "command": "npx",
51
+ "args": ["-y", "@instavar/mcp-server"]
52
+ }
53
+ }
54
+ }
55
+ ```
56
+
57
+ This starts with the free local template tools. To unlock hosted Studio tools,
58
+ create an API key at **https://instavar.com/studio/settings**, then add it:
20
59
 
21
60
  ```json
22
61
  {
@@ -30,17 +69,33 @@ no data of its own.
30
69
  }
31
70
  ```
32
71
 
33
- 3. Restart the client. The `instavar` tools appear.
72
+ Restart the client. The free tools appear in both modes. Hosted tools appear
73
+ when the key is configured.
34
74
 
35
75
  ### Environment
36
76
 
37
- | Var | Required | Default | Purpose |
38
- | ------------------- | -------- | ---------------------- | ---------------------------------------------------------------- |
39
- | `INSTAVAR_API_KEY` | yes | — | Your key from `/studio/settings`. Scoped to your org; revocable. |
40
- | `INSTAVAR_BASE_URL` | no | `https://instavar.com` | Override the API host. |
77
+ | Var | Required | Default | Purpose |
78
+ | ------------------- | -------- | ---------------------- | ----------------------------------------------------------- |
79
+ | `INSTAVAR_API_KEY` | no | — | Unlocks hosted Studio tools. Scoped to your org; revocable. |
80
+ | `INSTAVAR_BASE_URL` | no | `https://instavar.com` | Override the API host. |
41
81
 
42
82
  ## Tools
43
83
 
84
+ Free local tools:
85
+
86
+ | Tool | What it does |
87
+ | ------------------------- | ------------------------------------------------- |
88
+ | `get_capabilities` | Reports free and optional account features. |
89
+ | `list_templates` | Lists published Remotion template families. |
90
+ | `get_template` | Returns one template contract. |
91
+ | `get_video_schema` | Returns the public VideoSpec JSON Schema. |
92
+ | `get_example` | Returns a starter VideoSpec. |
93
+ | `scaffold_video_spec` | Creates a deterministic VideoSpec skeleton. |
94
+ | `get_render_instructions` | Returns the local validation and render workflow. |
95
+ | `diagnose_render_error` | Classifies common local render failures. |
96
+
97
+ Hosted tools, available when `INSTAVAR_API_KEY` is configured:
98
+
44
99
  | Tool | What it does |
45
100
  | ------------------------ | ---------------------------------------------------------------------------------------------- |
46
101
  | `list_jobs` | List recent jobs (newest first). |
@@ -48,14 +103,31 @@ no data of its own.
48
103
  | `get_video_state` | Materialized composition state for a job. |
49
104
  | `get_job_metrics` | Platform engagement snapshots for a published job. |
50
105
  | `get_cost_summary` | Infra cost summary (Lambda / RunPod / R2 / WaveSpeed / PoYo). |
106
+ | `inspect_workflow` | Read the current workflow and its version history. |
51
107
  | `create_video_brief` | Create a job from a structured brief and start rendering. |
52
108
  | `edit_video_brief` | Patch a brief (incl. objective / publishTarget); re-renders unless only publishTarget changed. |
109
+ | `retry_video_qa` | Retry degraded or unavailable QA against the same current frozen video artifact. |
110
+ | `audit_video_qa` | Run an advisory Qwen3.6-Plus second opinion against the same frozen video. |
111
+ | `regenerate_narration` | Regenerate narration and its dependent video from the unchanged durable brief. |
112
+ | `draft_workflow` | Store a complete workflow using the inspected `expectedVersion` (`0` only when none exists). |
113
+ | `patch_workflow` | Create a conflict-aware workflow version from a recursive patch. |
114
+ | `validate_workflow` | Validate and compile the inspected workflow version without rendering. |
115
+ | `preview_workflow` | Read the compiled UniversalVideo brief without rendering. |
116
+ | `compare_workflows` | Compare two durable workflow versions field by field. |
117
+ | `approve_workflow` | Approve the inspected valid version without approving or publishing the video job. |
53
118
  | `approve_job` | Approve a rendered job so it can be published. |
54
119
  | `publish_job` | Publish an approved job to its connected social destination. |
55
120
  | `connect_account` | Start connecting a social account (headless OAuth pairing). |
56
121
  | `connect_account_status` | Poll a pending account connection until it is connected. |
57
122
 
58
- Writes require a key with the `write` scope; `publish_job` requires `publish`.
123
+ Workflow inspection, preview, and comparison require `read`. Workflow draft,
124
+ patch, validation, and approval require `write`. `publish_job` requires
125
+ `publish`.
126
+
127
+ Workflow tools reject unknown arguments instead of silently discarding them.
128
+ API calls time out after 15 seconds. A stale-version `409` tells the caller to
129
+ inspect the latest version, while an unavailable upload or other dependency
130
+ conflict tells the caller to resolve that dependency before retrying.
59
131
 
60
132
  ### Connecting a social account from the terminal
61
133
 
package/dist/index.js CHANGED
@@ -4,12 +4,354 @@
4
4
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
5
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
6
 
7
+ // src/public-templates.ts
8
+ import { z } from "zod";
9
+ var TEMPLATE_REPOSITORY = "https://github.com/instavar/remotion-templates";
10
+ var TEMPLATE_ENDPOINT = "https://instavar.com/api/mcp/templates";
11
+ var templateIds = [
12
+ "qa-ad",
13
+ "proof-walkthrough",
14
+ "announcement-brief",
15
+ "social-remix",
16
+ "finance-brief"
17
+ ];
18
+ var templateIdSchema = z.enum(templateIds);
19
+ var aspectSchema = z.enum(["9:16", "4:5", "1:1"]);
20
+ var templates = [
21
+ {
22
+ id: "qa-ad",
23
+ name: "Question and answer ad",
24
+ description: "A short video with a hook, source-video window, explanation points, and call to action.",
25
+ aspects: ["9:16", "4:5", "1:1"]
26
+ },
27
+ {
28
+ id: "proof-walkthrough",
29
+ name: "Proof walkthrough",
30
+ description: "A paced explanation for equations, worked reasoning, and a final takeaway.",
31
+ aspects: ["9:16", "4:5", "1:1"]
32
+ },
33
+ {
34
+ id: "announcement-brief",
35
+ name: "Announcement brief",
36
+ description: "A concise announcement with a headline, supporting points, optional media, and next action.",
37
+ aspects: ["9:16", "4:5", "1:1"]
38
+ },
39
+ {
40
+ id: "social-remix",
41
+ name: "Social remix",
42
+ description: "A caption-forward treatment that frames an existing clip with a hook, context, and call to action.",
43
+ aspects: ["9:16", "4:5", "1:1"]
44
+ },
45
+ {
46
+ id: "finance-brief",
47
+ name: "Data and finance brief",
48
+ description: "An editorial brief for comparisons, numeric highlights, evidence, caveats, and a recommendation.",
49
+ aspects: ["9:16", "4:5", "1:1"]
50
+ }
51
+ ];
52
+ var videoSpecSchema = {
53
+ $schema: "https://json-schema.org/draft/2020-12/schema",
54
+ $id: "https://instavar.com/schemas/video-spec-1.0.json",
55
+ title: "Instavar VideoSpec",
56
+ type: "object",
57
+ additionalProperties: false,
58
+ required: [
59
+ "schemaVersion",
60
+ "id",
61
+ "templateFamily",
62
+ "meta",
63
+ "target",
64
+ "scenes"
65
+ ],
66
+ properties: {
67
+ schemaVersion: { const: "1.0" },
68
+ id: { type: "string", pattern: "^[a-z0-9][a-z0-9-]{2,80}$" },
69
+ templateFamily: { enum: templateIds },
70
+ meta: {
71
+ type: "object",
72
+ required: ["title"],
73
+ properties: {
74
+ title: { type: "string", minLength: 1 },
75
+ description: { type: "string" },
76
+ tags: { type: "array", items: { type: "string" } }
77
+ }
78
+ },
79
+ target: {
80
+ type: "object",
81
+ required: ["aspect", "fps", "durationMode"],
82
+ properties: {
83
+ aspect: { enum: ["9:16", "4:5", "1:1"] },
84
+ fps: { type: "integer", minimum: 1 },
85
+ durationMode: { enum: ["auto", "fixed"] },
86
+ fixedDurationSec: { type: "number", exclusiveMinimum: 0 }
87
+ }
88
+ },
89
+ scenes: {
90
+ type: "array",
91
+ minItems: 1,
92
+ items: {
93
+ type: "object",
94
+ required: ["id", "kind", "content"],
95
+ properties: {
96
+ id: { type: "string", minLength: 1 },
97
+ kind: {
98
+ enum: [
99
+ "hero",
100
+ "points",
101
+ "equation",
102
+ "video-window",
103
+ "cta",
104
+ "custom"
105
+ ]
106
+ },
107
+ content: { type: "object" },
108
+ timing: { type: "object" }
109
+ }
110
+ }
111
+ }
112
+ }
113
+ };
114
+ var annotations = {
115
+ readOnlyHint: true,
116
+ openWorldHint: false,
117
+ destructiveHint: false
118
+ };
119
+ function result(value, summary) {
120
+ return {
121
+ structuredContent: value,
122
+ content: [{ type: "text", text: summary }]
123
+ };
124
+ }
125
+ function slugify(value) {
126
+ const slug = value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 72);
127
+ return slug.length >= 3 ? slug : "new-video";
128
+ }
129
+ function scaffoldVideoSpec(args) {
130
+ const scenes = [
131
+ {
132
+ id: "hero",
133
+ kind: "hero",
134
+ content: {
135
+ title: args.title,
136
+ subtitle: "Replace with a concise promise"
137
+ }
138
+ }
139
+ ];
140
+ if (args.templateId === "qa-ad" || args.templateId === "social-remix") {
141
+ scenes.push({
142
+ id: "source-video",
143
+ kind: "video-window",
144
+ content: { src: args.mediaSrc ?? "public/replace-with-your-video.mp4" }
145
+ });
146
+ }
147
+ scenes.push({
148
+ id: args.templateId === "proof-walkthrough" ? "working" : "key-points",
149
+ kind: args.templateId === "proof-walkthrough" ? "equation" : "points",
150
+ content: args.templateId === "proof-walkthrough" ? {
151
+ title: "Work through the idea",
152
+ latexBlocks: ["x = \\frac{-b \\pm \\sqrt{b^2 - 4ac}}{2a}"]
153
+ } : {
154
+ title: "Key points",
155
+ points: [
156
+ "Replace with evidence",
157
+ "Replace with explanation",
158
+ "Replace with takeaway"
159
+ ]
160
+ }
161
+ });
162
+ scenes.push({
163
+ id: "cta",
164
+ kind: "cta",
165
+ content: { line1: "Replace with the next action" }
166
+ });
167
+ return {
168
+ schemaVersion: "1.0",
169
+ id: slugify(args.title),
170
+ templateFamily: args.templateId,
171
+ meta: { title: args.title, tags: [] },
172
+ target: { aspect: args.aspect, fps: 30, durationMode: "auto" },
173
+ style: {
174
+ theme: "default",
175
+ variant: "default",
176
+ safeAreaProfile: "metaSafe"
177
+ },
178
+ audio: { ducking: true },
179
+ assets: {},
180
+ scenes
181
+ };
182
+ }
183
+ function registerPublicTemplateTools(server) {
184
+ server.registerTool(
185
+ "get_capabilities",
186
+ {
187
+ title: "Get video capabilities",
188
+ description: "Call first. Reports free local features and optional account features.",
189
+ inputSchema: {},
190
+ annotations
191
+ },
192
+ async () => result(
193
+ {
194
+ authentication: "optional",
195
+ apiKeyConfigured: Boolean(process.env.INSTAVAR_API_KEY?.trim()),
196
+ repository: TEMPLATE_REPOSITORY,
197
+ endpoint: TEMPLATE_ENDPOINT,
198
+ localFeatures: [
199
+ "template discovery",
200
+ "VideoSpec schema and examples",
201
+ "deterministic scaffolding",
202
+ "local render instructions",
203
+ "render error diagnosis"
204
+ ],
205
+ accountFeatures: [
206
+ "managed rendering and storage",
207
+ "workflow review and approval",
208
+ "social publishing and metrics"
209
+ ]
210
+ },
211
+ 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."
212
+ )
213
+ );
214
+ server.registerTool(
215
+ "list_templates",
216
+ {
217
+ title: "List Remotion templates",
218
+ description: "List public templates, optionally filtered by aspect ratio.",
219
+ inputSchema: { aspect: aspectSchema.optional() },
220
+ annotations
221
+ },
222
+ async ({ aspect }) => {
223
+ const filtered = aspect ? templates.filter((template) => template.aspects.includes(aspect)) : templates;
224
+ return result(
225
+ { templates: filtered },
226
+ filtered.map((template) => `${template.id}: ${template.description}`).join("\n")
227
+ );
228
+ }
229
+ );
230
+ server.registerTool(
231
+ "get_template",
232
+ {
233
+ title: "Get Remotion template",
234
+ description: "Get one public template contract.",
235
+ inputSchema: { templateId: templateIdSchema },
236
+ annotations
237
+ },
238
+ async ({ templateId }) => {
239
+ const template = templates.find((item) => item.id === templateId);
240
+ return result(
241
+ { template },
242
+ `${template?.name ?? templateId}
243
+ ${template?.description ?? ""}`
244
+ );
245
+ }
246
+ );
247
+ server.registerTool(
248
+ "get_video_schema",
249
+ {
250
+ title: "Get VideoSpec schema",
251
+ description: "Return the public VideoSpec JSON Schema.",
252
+ inputSchema: {},
253
+ annotations
254
+ },
255
+ async () => result(
256
+ { schemaVersion: "1.0", jsonSchema: videoSpecSchema },
257
+ "VideoSpec schema version 1.0."
258
+ )
259
+ );
260
+ server.registerTool(
261
+ "get_example",
262
+ {
263
+ title: "Get VideoSpec example",
264
+ description: "Return a starter example for one template.",
265
+ inputSchema: { templateId: templateIdSchema },
266
+ annotations
267
+ },
268
+ async ({ templateId }) => result(
269
+ {
270
+ templateId,
271
+ spec: scaffoldVideoSpec({
272
+ templateId,
273
+ title: `Example ${templateId}`,
274
+ aspect: "9:16"
275
+ })
276
+ },
277
+ `Example VideoSpec for ${templateId}.`
278
+ )
279
+ );
280
+ server.registerTool(
281
+ "scaffold_video_spec",
282
+ {
283
+ title: "Scaffold VideoSpec",
284
+ description: "Return a deterministic VideoSpec skeleton without writing files.",
285
+ inputSchema: {
286
+ templateId: templateIdSchema,
287
+ title: z.string().min(1).max(160),
288
+ aspect: aspectSchema.default("9:16"),
289
+ mediaSrc: z.string().min(1).max(500).optional()
290
+ },
291
+ annotations
292
+ },
293
+ async (args) => {
294
+ const spec = scaffoldVideoSpec(args);
295
+ return result(
296
+ { spec, filename: `${String(spec.id)}.video.json` },
297
+ "Starter VideoSpec created. Replace placeholder copy and media."
298
+ );
299
+ }
300
+ );
301
+ server.registerTool(
302
+ "get_render_instructions",
303
+ {
304
+ title: "Get local render instructions",
305
+ description: "Return clone, install, validate, preview, and render commands.",
306
+ inputSchema: {
307
+ client: z.enum(["codex", "claude-code", "manual"]).default("manual")
308
+ },
309
+ annotations
310
+ },
311
+ async ({ client }) => {
312
+ const commands = [
313
+ `git clone ${TEMPLATE_REPOSITORY}.git`,
314
+ "cd remotion-templates",
315
+ "npm install",
316
+ "npm run video:validate -- path/to/video.video.json",
317
+ "npm run video:preview -- path/to/video.video.json",
318
+ "npm run video:render -- path/to/video.video.json"
319
+ ];
320
+ return result(
321
+ { repository: TEMPLATE_REPOSITORY, client, commands },
322
+ commands.join("\n")
323
+ );
324
+ }
325
+ );
326
+ server.registerTool(
327
+ "diagnose_render_error",
328
+ {
329
+ title: "Diagnose render error",
330
+ description: "Classify a local validation or render error.",
331
+ inputSchema: { errorText: z.string().min(1).max(1e4) },
332
+ annotations
333
+ },
334
+ async ({ errorText }) => {
335
+ const text = errorText.toLowerCase();
336
+ 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";
337
+ return result(
338
+ {
339
+ code,
340
+ nextStep: "Run validation first, confirm Node 22 or newer, and retry with the smallest included example."
341
+ },
342
+ `Diagnosis: ${code}.`
343
+ );
344
+ }
345
+ );
346
+ }
347
+
7
348
  // src/tools.ts
8
- import { z as z2 } from "zod";
349
+ import { z as z3 } from "zod";
9
350
 
10
351
  // src/http.ts
11
352
  import { randomUUID } from "crypto";
12
353
  var API_KEY_HEADER = "x-api-key";
354
+ var API_REQUEST_TIMEOUT_MS = 15e3;
13
355
  function getApiKey() {
14
356
  const key = process.env.INSTAVAR_API_KEY?.trim();
15
357
  if (!key) {
@@ -33,8 +375,22 @@ function explain(status, body) {
33
375
  const reason = body && typeof body === "object" && "reason" in body ? String(body.reason) : "insufficient scope";
34
376
  return `Forbidden (403): ${reason}. The key lacks the required scope for this action.`;
35
377
  }
378
+ if (status === 409) {
379
+ const reason = body && typeof body === "object" && "error" in body ? String(body.error) : "The requested change conflicts with the current server state";
380
+ const recovery = /workflow changed/i.test(reason) ? "Inspect the latest workflow state before retrying." : "Resolve the reported conflict before retrying.";
381
+ return `Conflict (409): ${reason}. ${recovery}`;
382
+ }
36
383
  return "";
37
384
  }
385
+ function requestFailure(error) {
386
+ if (error instanceof DOMException && (error.name === "AbortError" || error.name === "TimeoutError")) {
387
+ return "The Instavar API request timed out. Retry the tool call.";
388
+ }
389
+ if (error instanceof TypeError) {
390
+ return "The Instavar API request failed before a response arrived. Check connectivity and retry the tool call.";
391
+ }
392
+ return error instanceof Error ? error.message : String(error);
393
+ }
38
394
  async function parse(res) {
39
395
  const text = await res.text();
40
396
  try {
@@ -45,11 +401,16 @@ async function parse(res) {
45
401
  }
46
402
  async function apiGet(path) {
47
403
  const res = await fetch(`${baseUrl()}${path}`, {
48
- headers: { [API_KEY_HEADER]: getApiKey() }
404
+ headers: { [API_KEY_HEADER]: getApiKey() },
405
+ signal: AbortSignal.timeout(API_REQUEST_TIMEOUT_MS)
49
406
  });
50
407
  const body = await parse(res);
51
408
  const hint = explain(res.status, body);
52
- return { ok: res.ok, status: res.status, body: hint ? { error: hint, body } : body };
409
+ return {
410
+ ok: res.ok,
411
+ status: res.status,
412
+ body: hint ? { error: hint, body } : body
413
+ };
53
414
  }
54
415
  async function apiPost(path, payload) {
55
416
  const clean = Object.fromEntries(
@@ -61,11 +422,16 @@ async function apiPost(path, payload) {
61
422
  [API_KEY_HEADER]: getApiKey(),
62
423
  "content-type": "application/json"
63
424
  },
64
- body: JSON.stringify(clean)
425
+ body: JSON.stringify(clean),
426
+ signal: AbortSignal.timeout(API_REQUEST_TIMEOUT_MS)
65
427
  });
66
428
  const body = await parse(res);
67
429
  const hint = explain(res.status, body);
68
- return { ok: res.ok, status: res.status, body: hint ? { error: hint, body } : body };
430
+ return {
431
+ ok: res.ok,
432
+ status: res.status,
433
+ body: hint ? { error: hint, body } : body
434
+ };
69
435
  }
70
436
  function fenceForModel(json) {
71
437
  const nonce = randomUUID();
@@ -76,15 +442,15 @@ ${open}
76
442
  ${json}
77
443
  ${close}`;
78
444
  }
79
- function toToolResult(result) {
445
+ function toToolResult(result2) {
80
446
  return {
81
447
  content: [
82
448
  {
83
449
  type: "text",
84
- text: fenceForModel(JSON.stringify(result.body, null, 2))
450
+ text: fenceForModel(JSON.stringify(result2.body, null, 2))
85
451
  }
86
452
  ],
87
- isError: !result.ok
453
+ isError: !result2.ok
88
454
  };
89
455
  }
90
456
  async function runTool(fn) {
@@ -95,7 +461,7 @@ async function runTool(fn) {
95
461
  content: [
96
462
  {
97
463
  type: "text",
98
- text: error instanceof Error ? error.message : String(error)
464
+ text: requestFailure(error)
99
465
  }
100
466
  ],
101
467
  isError: true
@@ -104,7 +470,7 @@ async function runTool(fn) {
104
470
  }
105
471
 
106
472
  // src/brief-schema.ts
107
- import { z } from "zod";
473
+ import { z as z2 } from "zod";
108
474
  var PUBLISH_TARGETS = [
109
475
  "linkedin",
110
476
  "x",
@@ -127,13 +493,108 @@ var BRIEF_OBJECTIVES = [
127
493
  "other"
128
494
  ];
129
495
  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()
496
+ var REFERENCE_ROLES = [
497
+ "identity",
498
+ "appearance",
499
+ "motion",
500
+ "composition",
501
+ "camera",
502
+ "environment",
503
+ "style"
504
+ ];
505
+ var optionalDirectionText = z2.string().trim().min(1).max(500).optional();
506
+ var visualDirectionSchema = z2.object({
507
+ communicationJob: optionalDirectionText,
508
+ emotionalTarget: optionalDirectionText,
509
+ attentionPath: z2.object({
510
+ first: optionalDirectionText,
511
+ next: optionalDirectionText,
512
+ proof: optionalDirectionText
513
+ }).optional(),
514
+ visualLanguage: z2.object({
515
+ space: optionalDirectionText,
516
+ line: optionalDirectionText,
517
+ shape: optionalDirectionText,
518
+ tone: optionalDirectionText,
519
+ color: optionalDirectionText,
520
+ movement: optionalDirectionText,
521
+ rhythm: optionalDirectionText
522
+ }).optional(),
523
+ camera: z2.object({
524
+ shotSize: optionalDirectionText,
525
+ angle: optionalDirectionText,
526
+ movement: optionalDirectionText,
527
+ lens: optionalDirectionText
528
+ }).optional()
529
+ });
530
+ var generationContractSchema = z2.object({
531
+ globalInvariants: z2.array(z2.string().trim().min(1).max(300)).max(20).optional(),
532
+ shotDelta: z2.string().trim().min(1).max(500).optional(),
533
+ referenceBindings: z2.array(
534
+ z2.object({
535
+ assetId: z2.string().trim().min(1).max(200),
536
+ roles: z2.array(z2.enum(REFERENCE_ROLES)).min(1).max(7)
537
+ })
538
+ ).max(20).optional(),
539
+ prohibitedChanges: z2.array(z2.string().trim().min(1).max(300)).max(20).optional(),
540
+ reviewCriteria: z2.array(z2.string().trim().min(1).max(300)).max(20).optional()
541
+ });
542
+ var REGION_NAMES = [
543
+ "background",
544
+ "left-half",
545
+ "right-half",
546
+ "figure-center",
547
+ "corner-inset",
548
+ "lower-third"
549
+ ];
550
+ var ASSET_ROLES = [
551
+ "background",
552
+ "figure",
553
+ "inset",
554
+ "lower-third",
555
+ "diagram-node"
556
+ ];
557
+ var assetSlotStyleSchema = z2.object({
558
+ opacity: z2.number().min(0).max(1).optional(),
559
+ fit: z2.enum(["cover", "contain"]).optional(),
560
+ z: z2.number().optional()
561
+ }).strict();
562
+ var assetSlotSchema = z2.object({
563
+ id: z2.string().min(1),
564
+ sceneId: z2.string().min(1).optional(),
565
+ region: z2.enum(REGION_NAMES),
566
+ role: z2.enum(ASSET_ROLES),
567
+ kind: z2.enum(["image", "video"]),
568
+ source: z2.enum(["auto", "stock", "generate"]),
569
+ intent: z2.string(),
570
+ style: assetSlotStyleSchema.optional(),
571
+ timing: z2.object({
572
+ fromPct: z2.number().min(0).max(1).optional(),
573
+ toPct: z2.number().min(0).max(1).optional()
574
+ }).strict().optional(),
575
+ url: z2.string().url().optional()
576
+ }).strict();
577
+ var briefStepSchema = z2.object({
578
+ label: z2.string(),
579
+ latex: z2.string().optional(),
580
+ explanation: z2.string().optional()
581
+ });
582
+ var briefAudioLayerSchema = z2.object({
583
+ id: z2.string().min(1),
584
+ src: z2.string().url(),
585
+ startFrame: z2.number().int().nonnegative().default(0),
586
+ durationInFrames: z2.number().int().positive().optional(),
587
+ volume: z2.number().min(0).max(1).default(1),
588
+ fadeFrames: z2.number().int().nonnegative().default(0),
589
+ role: z2.enum(["music", "foley", "transition", "effect"])
134
590
  });
135
- var briefSceneSchema = z.object({
136
- type: z.enum([
591
+ var beatVisualRequestSchema = z2.object({
592
+ intent: z2.string().trim().min(1).optional(),
593
+ media: z2.enum(["still", "motion"]).default("still"),
594
+ source: z2.enum(["auto", "stock", "generate"]).default("auto")
595
+ }).strict();
596
+ var briefSceneSchema = z2.object({
597
+ type: z2.enum([
137
598
  "hero",
138
599
  "points",
139
600
  "equation",
@@ -149,103 +610,160 @@ var briefSceneSchema = z.object({
149
610
  "callout",
150
611
  "step-pill",
151
612
  "cta-pulse",
613
+ "source-clip",
614
+ "source-image",
615
+ "route-card",
152
616
  "video-window",
617
+ "image-window",
153
618
  "custom"
154
619
  ]),
155
- durationInFrames: z.number().optional(),
156
- title: z.string().optional(),
157
- subtitle: z.string().optional(),
158
- text: z.string().optional(),
159
- points: z.array(z.string()).optional(),
160
- latex: z.string().optional(),
161
- value: z.union([z.string(), z.number()]).optional(),
162
- max: z.number().optional(),
163
- label: z.string().optional(),
164
- unit: z.string().optional(),
165
- prefix: z.string().optional(),
166
- suffix: z.string().optional(),
167
- decimals: z.number().optional(),
168
- color: z.string().optional(),
169
- tone: z.string().optional(),
170
- iconKey: z.string().optional(),
171
- stepNumber: z.number().optional(),
172
- totalSteps: z.number().optional(),
173
- items: z.array(z.object({ label: z.string(), value: z.number() })).optional(),
174
- src: z.string().optional(),
175
- caption: z.string().optional()
620
+ durationInFrames: z2.number().optional(),
621
+ narrationText: z2.string().optional(),
622
+ visualResolution: beatVisualRequestSchema.optional(),
623
+ title: z2.string().optional(),
624
+ subtitle: z2.string().optional(),
625
+ text: z2.string().optional(),
626
+ points: z2.array(z2.string()).optional(),
627
+ latex: z2.string().optional(),
628
+ value: z2.union([z2.string(), z2.number()]).optional(),
629
+ max: z2.number().optional(),
630
+ label: z2.string().optional(),
631
+ unit: z2.string().optional(),
632
+ prefix: z2.string().optional(),
633
+ suffix: z2.string().optional(),
634
+ decimals: z2.number().optional(),
635
+ color: z2.string().optional(),
636
+ tone: z2.string().optional(),
637
+ iconKey: z2.string().optional(),
638
+ stepNumber: z2.number().optional(),
639
+ totalSteps: z2.number().optional(),
640
+ items: z2.array(z2.object({ label: z2.string(), value: z2.number() })).optional(),
641
+ src: z2.string().optional(),
642
+ caption: z2.string().optional(),
643
+ // A-roll generation (video-window scenes): source "generate" triggers
644
+ // flag-gated WaveSpeed image/video gen server-side from generatePrompt;
645
+ // "url" (default) uses src as-is. Kept in sync with the app's
646
+ // briefSceneSchema - see the parity test in the app suite.
647
+ source: z2.enum(["url", "generate"]).optional(),
648
+ generatePrompt: z2.string().optional(),
649
+ generationContract: generationContractSchema.optional(),
650
+ trimStartSeconds: z2.number().nonnegative().optional(),
651
+ trimEndSeconds: z2.number().positive().optional(),
652
+ playbackRate: z2.number().min(0.25).max(8).optional(),
653
+ objectFit: z2.enum(["cover", "contain"]).optional(),
654
+ objectPosition: z2.string().optional(),
655
+ scaleFrom: z2.number().min(0.5).max(2).optional(),
656
+ scaleTo: z2.number().min(0.5).max(2).optional(),
657
+ panFromX: z2.number().min(-50).max(50).optional(),
658
+ panToX: z2.number().min(-50).max(50).optional(),
659
+ panFromY: z2.number().min(-50).max(50).optional(),
660
+ panToY: z2.number().min(-50).max(50).optional(),
661
+ routeFrom: z2.string().optional(),
662
+ routeTo: z2.string().optional(),
663
+ routeLabel: z2.string().optional(),
664
+ overlayKind: z2.enum(["none", "hook", "stage", "end-card"]).optional(),
665
+ overlayPosition: z2.enum(["center", "top", "bottom"]).optional(),
666
+ accentColor: z2.string().optional(),
667
+ overlayBackgroundColor: z2.string().optional(),
668
+ cutFlash: z2.boolean().optional(),
669
+ muted: z2.boolean().optional()
176
670
  });
177
- var directionsBriefSchema = z.object({
178
- subtitle: z.string().optional(),
179
- unitLabel: z.string().optional(),
180
- distanceTimeChip: z.string().optional(),
181
- clips: z.array(
182
- z.object({
183
- src: z.string(),
184
- trimStartSeconds: z.number().optional(),
185
- trimEndSeconds: z.number(),
186
- speed: z.number().optional(),
187
- mirrorFlip: z.boolean().optional()
671
+ var directionsBriefSchema = z2.object({
672
+ subtitle: z2.string().optional(),
673
+ unitLabel: z2.string().optional(),
674
+ distanceTimeChip: z2.string().optional(),
675
+ clips: z2.array(
676
+ z2.object({
677
+ src: z2.string(),
678
+ trimStartSeconds: z2.number().optional(),
679
+ trimEndSeconds: z2.number(),
680
+ speed: z2.number().optional(),
681
+ mirrorFlip: z2.boolean().optional()
188
682
  })
189
683
  ).min(1),
190
- cues: z.array(
191
- z.object({
192
- atSeconds: z.number(),
193
- text: z.string(),
194
- arrow: z.enum(["left", "right", "forward"]).optional(),
195
- step: z.number().optional()
684
+ cues: z2.array(
685
+ z2.object({
686
+ atSeconds: z2.number(),
687
+ text: z2.string(),
688
+ arrow: z2.enum(["left", "right", "forward"]).optional(),
689
+ step: z2.number().optional()
196
690
  })
197
691
  ),
198
- outro: z.object({
199
- heading: z.string().optional(),
200
- body: z.array(z.string()).optional(),
201
- seconds: z.number().optional()
692
+ outro: z2.object({
693
+ heading: z2.string().optional(),
694
+ body: z2.array(z2.string()).optional(),
695
+ seconds: z2.number().optional()
202
696
  }).optional(),
203
- music: z.object({
204
- perSegment: z.array(
205
- z.object({
206
- src: z.string(),
207
- clipIndexStart: z.number().optional(),
208
- hotOffsetSeconds: z.number().optional(),
209
- volume: z.number().optional()
697
+ music: z2.object({
698
+ perSegment: z2.array(
699
+ z2.object({
700
+ src: z2.string(),
701
+ clipIndexStart: z2.number().optional(),
702
+ hotOffsetSeconds: z2.number().optional(),
703
+ volume: z2.number().optional()
210
704
  })
211
705
  ).optional(),
212
- crossfadeSeconds: z.number().optional()
706
+ crossfadeSeconds: z2.number().optional()
213
707
  }).optional()
214
708
  });
215
709
  var createBriefShape = {
216
- title: z.string().min(1).max(120).describe("Short title (max 120 chars)"),
217
- script: z.string().min(1).describe("Full narration script (sent to TTS)"),
218
- caption: z.string().min(1).describe("Social caption with hooks, hashtags, CTA"),
219
- publishTarget: z.enum(PUBLISH_TARGETS).describe("Target social platform"),
220
- objective: z.enum(BRIEF_OBJECTIVES).describe("Content objective (drives composition routing)"),
221
- lessonTitle: z.string().optional().describe("Display title rendered inside the video"),
222
- steps: z.array(briefStepSchema).optional().describe("Structured steps for educational content (LaTeX)"),
223
- scenes: z.array(briefSceneSchema).optional().describe("Scene graph for the UniversalVideo composition"),
224
- aspectRatio: z.enum(ASPECT_RATIOS).default("9:16").describe("Aspect ratio (default 9:16)"),
225
- compositionId: z.string().optional().describe("Remotion composition ID override (advanced)"),
710
+ title: z2.string().min(1).max(120).describe("Short title (max 120 chars)"),
711
+ script: z2.string().min(1).describe("Full narration script (sent to TTS)"),
712
+ caption: z2.string().min(1).describe("Social caption with hooks, hashtags, CTA"),
713
+ publishTarget: z2.enum(PUBLISH_TARGETS).describe("Target social platform"),
714
+ objective: z2.enum(BRIEF_OBJECTIVES).describe("Content objective (drives composition routing)"),
715
+ lessonTitle: z2.string().optional().describe("Display title rendered inside the video"),
716
+ visualDirection: visualDirectionSchema.optional().describe(
717
+ "Whole-video communication, attention, visual-language, and camera direction."
718
+ ),
719
+ steps: z2.array(briefStepSchema).optional().describe("Structured steps for educational content (LaTeX)"),
720
+ scenes: z2.array(briefSceneSchema).optional().describe("Scene graph for the UniversalVideo composition"),
721
+ // No zod .default() here: a default would materialize into the parsed tool
722
+ // arguments and relay to /api/v1/jobs as if the caller stated it, corrupting
723
+ // the resolution provenance the API echoes back. Unset must stay absent;
724
+ // the API re-applies 9:16 server-side.
725
+ aspectRatio: z2.enum(ASPECT_RATIOS).optional().describe("Aspect ratio (defaults to 9:16 when omitted)"),
726
+ compositionId: z2.string().optional().describe("Remotion composition ID override (advanced)"),
727
+ voiceId: z2.string().optional().describe(
728
+ "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."
729
+ ),
226
730
  directions: directionsBriefSchema.optional().describe(
227
731
  "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')."
228
- )
732
+ ),
733
+ assetSlots: z2.array(assetSlotSchema).optional().describe(
734
+ "Optional storyboard asset slots: region-placed image/video assets or placeholders attached to the whole video or a specific scene."
735
+ ),
736
+ audioLayers: z2.array(briefAudioLayerSchema).optional().describe("Optional typed music, Foley, transition, and effect layers.")
229
737
  };
230
738
  var editBriefShape = {
231
- jobId: z.string().uuid().describe("The job to edit"),
232
- title: z.string().optional().describe("Updated title"),
233
- script: z.string().optional().describe("Updated narration script"),
234
- caption: z.string().optional().describe("Updated social caption"),
235
- lessonTitle: z.string().optional().describe("Updated lesson title"),
236
- steps: z.array(briefStepSchema).optional().describe("Updated steps (replaces the array)"),
237
- scenes: z.array(briefSceneSchema).optional().describe("Updated scenes (replaces the array)"),
238
- aspectRatio: z.enum(ASPECT_RATIOS).optional().describe("Updated aspect ratio"),
239
- objective: z.enum(BRIEF_OBJECTIVES).optional().describe(
739
+ jobId: z2.string().uuid().describe("The job to edit"),
740
+ title: z2.string().optional().describe("Updated title"),
741
+ script: z2.string().optional().describe("Updated narration script"),
742
+ caption: z2.string().optional().describe("Updated social caption"),
743
+ lessonTitle: z2.string().optional().describe("Updated lesson title"),
744
+ visualDirection: visualDirectionSchema.optional().describe("Updated whole-video visual direction"),
745
+ steps: z2.array(briefStepSchema).optional().describe("Updated steps (replaces the array)"),
746
+ scenes: z2.array(briefSceneSchema).optional().describe("Updated scenes (replaces the array)"),
747
+ aspectRatio: z2.enum(ASPECT_RATIOS).optional().describe("Updated aspect ratio"),
748
+ objective: z2.enum(BRIEF_OBJECTIVES).optional().describe(
240
749
  "Updated content objective. Re-routes the composition and triggers a re-render."
241
750
  ),
242
- publishTarget: z.enum(PUBLISH_TARGETS).optional().describe(
751
+ publishTarget: z2.enum(PUBLISH_TARGETS).optional().describe(
243
752
  "Updated target social platform. Distribution metadata only - does not trigger a re-render."
753
+ ),
754
+ voiceId: z2.string().optional().describe(
755
+ "Updated TTS narrator voice ID. Triggers a re-render with the new voice."
756
+ ),
757
+ assetSlots: z2.array(assetSlotSchema).optional().describe(
758
+ "Replacement storyboard asset slots. Omit to preserve existing slots; pass [] to clear them."
759
+ ),
760
+ audioLayers: z2.array(briefAudioLayerSchema).optional().describe(
761
+ "Replacement typed audio layers. Omit to preserve existing layers; pass [] to clear them."
244
762
  )
245
763
  };
246
764
 
247
765
  // src/tools.ts
248
- var uuid = (desc) => z2.string().uuid().describe(desc);
766
+ var uuid = (desc) => z3.string().uuid().describe(desc);
249
767
  var READ_ONLY = { readOnlyHint: true, openWorldHint: false };
250
768
  var SAFE_WRITE = {
251
769
  readOnlyHint: false,
@@ -260,8 +778,8 @@ function registerTools(server) {
260
778
  title: "List jobs",
261
779
  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.",
262
780
  inputSchema: {
263
- limit: z2.number().int().min(1).max(50).default(10).describe("Number of jobs to return (default 10, max 50)"),
264
- status: z2.enum([
781
+ limit: z3.number().int().min(1).max(50).default(10).describe("Number of jobs to return (default 10, max 50)"),
782
+ status: z3.enum([
265
783
  "queued",
266
784
  "rendering",
267
785
  "draft_ready",
@@ -309,7 +827,7 @@ function registerTools(server) {
309
827
  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.",
310
828
  inputSchema: {
311
829
  jobId: uuid("The job ID"),
312
- limit: z2.number().int().min(1).max(20).default(10).describe("Max metric snapshots (default 10, max 20)")
830
+ limit: z3.number().int().min(1).max(20).default(10).describe("Max metric snapshots (default 10, max 20)")
313
831
  },
314
832
  annotations: READ_ONLY
315
833
  },
@@ -325,10 +843,10 @@ function registerTools(server) {
325
843
  title: "Get cost summary",
326
844
  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.",
327
845
  inputSchema: {
328
- since: z2.string().optional().describe("ISO date \u2014 only include events after this date"),
329
- provider: z2.enum(["runpod", "lambda", "r2", "wavespeed", "poyo", "all"]).default("all").describe("Filter by provider"),
330
- groupBy: z2.enum(["provider", "job", "day"]).default("provider").describe("How to aggregate the summary"),
331
- includeReconciliation: z2.boolean().default(false).describe("Include account-wide reconciliation events")
846
+ since: z3.string().optional().describe("ISO date \u2014 only include events after this date"),
847
+ provider: z3.enum(["runpod", "lambda", "r2", "wavespeed", "poyo", "all"]).default("all").describe("Filter by provider"),
848
+ groupBy: z3.enum(["provider", "job", "day"]).default("provider").describe("How to aggregate the summary"),
849
+ includeReconciliation: z3.boolean().default(false).describe("Include account-wide reconciliation events")
332
850
  },
333
851
  annotations: READ_ONLY
334
852
  },
@@ -339,6 +857,18 @@ function registerTools(server) {
339
857
  return runTool(() => apiGet(`/api/v1/cost-summary?${q.toString()}`));
340
858
  }
341
859
  );
860
+ server.registerTool(
861
+ "inspect_workflow",
862
+ {
863
+ title: "Inspect workflow",
864
+ description: "Inspect the current Studio workflow and its version history for a job.",
865
+ inputSchema: z3.object({ jobId: uuid("The job ID") }).strict(),
866
+ annotations: READ_ONLY
867
+ },
868
+ async ({ jobId }) => runTool(
869
+ () => apiGet(`/api/v1/jobs/${encodeURIComponent(jobId)}/workflow`)
870
+ )
871
+ );
342
872
  server.registerTool(
343
873
  "create_video_brief",
344
874
  {
@@ -353,7 +883,7 @@ function registerTools(server) {
353
883
  "edit_video_brief",
354
884
  {
355
885
  title: "Edit video brief",
356
- description: "Edit an existing job's brief (only provided fields change). Changing the script, caption, title, lessonTitle, objective, steps, scenes or aspectRatio triggers a re-render and returns the new run ID; objective also re-routes the composition. Changing only publishTarget updates the job's target platform without re-rendering. For a brand-new video use create_video_brief.",
886
+ description: "Edit an existing job's brief (only provided fields change). Changing the script, caption, title, lessonTitle, objective, steps, scenes, aspectRatio or voiceId triggers a re-render and returns the new run ID; objective also re-routes the composition. Changing only publishTarget updates the job's target platform without re-rendering. For a brand-new video use create_video_brief.",
357
887
  inputSchema: editBriefShape,
358
888
  annotations: SAFE_WRITE
359
889
  },
@@ -364,6 +894,169 @@ function registerTools(server) {
364
894
  );
365
895
  }
366
896
  );
897
+ const workflowWrite = (operation, jobId, payload = {}) => runTool(
898
+ () => apiPost(`/api/v1/jobs/${encodeURIComponent(jobId)}/workflow`, {
899
+ operation,
900
+ ...payload
901
+ })
902
+ );
903
+ server.registerTool(
904
+ "draft_workflow",
905
+ {
906
+ title: "Draft workflow",
907
+ 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.",
908
+ inputSchema: z3.object({
909
+ jobId: uuid("The job ID"),
910
+ expectedVersion: z3.number().int().nonnegative().describe("Use 0 when no workflow exists yet"),
911
+ document: z3.unknown().describe("The complete Studio workflow document")
912
+ }).strict(),
913
+ annotations: SAFE_WRITE
914
+ },
915
+ async ({ jobId, expectedVersion, document }) => workflowWrite("draft", jobId, { expectedVersion, document })
916
+ );
917
+ server.registerTool(
918
+ "patch_workflow",
919
+ {
920
+ title: "Patch workflow",
921
+ description: "Create a new workflow version by recursively merging a patch into the current version.",
922
+ inputSchema: z3.object({
923
+ jobId: uuid("The job ID"),
924
+ expectedVersion: z3.number().int().positive(),
925
+ patch: z3.record(z3.string(), z3.unknown())
926
+ }).strict(),
927
+ annotations: SAFE_WRITE
928
+ },
929
+ async ({ jobId, expectedVersion, patch }) => workflowWrite("patch", jobId, { expectedVersion, patch })
930
+ );
931
+ server.registerTool(
932
+ "validate_workflow",
933
+ {
934
+ title: "Validate workflow",
935
+ description: "Validate and compile the inspected workflow version without rendering or publishing it.",
936
+ inputSchema: z3.object({
937
+ jobId: uuid("The job ID"),
938
+ expectedVersion: z3.number().int().positive()
939
+ }).strict(),
940
+ annotations: SAFE_WRITE
941
+ },
942
+ async ({ jobId, expectedVersion }) => workflowWrite("validate", jobId, { expectedVersion })
943
+ );
944
+ server.registerTool(
945
+ "preview_workflow",
946
+ {
947
+ title: "Preview workflow",
948
+ description: "Return the current workflow's compiled UniversalVideo brief for a safe pre-render preview.",
949
+ inputSchema: z3.object({ jobId: uuid("The job ID") }).strict(),
950
+ annotations: READ_ONLY
951
+ },
952
+ async ({ jobId }) => runTool(
953
+ () => apiGet(
954
+ `/api/v1/jobs/${encodeURIComponent(jobId)}/workflow?operation=preview`
955
+ )
956
+ )
957
+ );
958
+ server.registerTool(
959
+ "compare_workflows",
960
+ {
961
+ title: "Compare workflow versions",
962
+ description: "Compare two durable workflow versions field by field.",
963
+ inputSchema: z3.object({
964
+ jobId: uuid("The job ID"),
965
+ fromVersion: z3.number().int().positive(),
966
+ toVersion: z3.number().int().positive()
967
+ }).strict(),
968
+ annotations: READ_ONLY
969
+ },
970
+ async ({ jobId, fromVersion, toVersion }) => {
971
+ const query = new URLSearchParams({
972
+ operation: "compare",
973
+ fromVersion: String(fromVersion),
974
+ toVersion: String(toVersion)
975
+ });
976
+ return runTool(
977
+ () => apiGet(
978
+ `/api/v1/jobs/${encodeURIComponent(
979
+ jobId
980
+ )}/workflow?${query.toString()}`
981
+ )
982
+ );
983
+ }
984
+ );
985
+ server.registerTool(
986
+ "approve_workflow",
987
+ {
988
+ title: "Approve workflow",
989
+ description: "Validate and approve the inspected workflow version. This does not approve or publish the video job.",
990
+ inputSchema: z3.object({
991
+ jobId: uuid("The job ID"),
992
+ expectedVersion: z3.number().int().positive()
993
+ }).strict(),
994
+ annotations: {
995
+ readOnlyHint: false,
996
+ destructiveHint: false,
997
+ idempotentHint: true,
998
+ openWorldHint: false
999
+ }
1000
+ },
1001
+ async ({ jobId, expectedVersion }) => workflowWrite("approve", jobId, { expectedVersion })
1002
+ );
1003
+ server.registerTool(
1004
+ "retry_video_qa",
1005
+ {
1006
+ title: "Retry video QA",
1007
+ 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.",
1008
+ inputSchema: {
1009
+ jobId: uuid("The current job ID"),
1010
+ runId: uuid("The current render run ID"),
1011
+ videoArtifactId: uuid("The current frozen video artifact ID")
1012
+ },
1013
+ annotations: SAFE_WRITE
1014
+ },
1015
+ async ({ jobId, runId, videoArtifactId }) => runTool(
1016
+ () => apiPost(`/api/v1/jobs/${encodeURIComponent(jobId)}/qa/retry`, {
1017
+ runId,
1018
+ videoArtifactId
1019
+ })
1020
+ )
1021
+ );
1022
+ server.registerTool(
1023
+ "audit_video_qa",
1024
+ {
1025
+ title: "Audit video QA",
1026
+ 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.",
1027
+ inputSchema: {
1028
+ jobId: uuid("The current job ID"),
1029
+ runId: uuid("The current render run ID"),
1030
+ videoArtifactId: uuid("The current frozen video artifact ID")
1031
+ },
1032
+ annotations: SAFE_WRITE
1033
+ },
1034
+ async ({ jobId, runId, videoArtifactId }) => runTool(
1035
+ () => apiPost(`/api/v1/jobs/${encodeURIComponent(jobId)}/qa/audit`, {
1036
+ runId,
1037
+ videoArtifactId
1038
+ })
1039
+ )
1040
+ );
1041
+ server.registerTool(
1042
+ "regenerate_narration",
1043
+ {
1044
+ title: "Regenerate narration",
1045
+ 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.",
1046
+ inputSchema: {
1047
+ jobId: uuid("The current job ID"),
1048
+ sourceRunId: uuid("The current run ID"),
1049
+ trigger: z3.enum(["user_requested", "verifier_recommendation"])
1050
+ },
1051
+ annotations: SAFE_WRITE
1052
+ },
1053
+ async ({ jobId, sourceRunId, trigger }) => runTool(
1054
+ () => apiPost(
1055
+ `/api/v1/jobs/${encodeURIComponent(jobId)}/narration/regenerate`,
1056
+ { sourceRunId, trigger }
1057
+ )
1058
+ )
1059
+ );
367
1060
  server.registerTool(
368
1061
  "approve_job",
369
1062
  {
@@ -389,15 +1082,15 @@ function registerTools(server) {
389
1082
  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.",
390
1083
  inputSchema: {
391
1084
  jobId: uuid("The approved job to publish"),
392
- youtubePrivacyStatus: z2.enum(["private", "unlisted", "public"]).optional().describe("YouTube only. Use 'private' for safe dogfood runs."),
393
- tiktokPostMode: z2.enum(["direct_publish", "draft_upload"]).optional().describe("TikTok only. Use 'draft_upload' for safe dogfood runs."),
394
- connectedAccountDestinationId: z2.string().uuid().optional().describe(
1085
+ youtubePrivacyStatus: z3.enum(["private", "unlisted", "public"]).optional().describe("YouTube only. Use 'private' for safe dogfood runs."),
1086
+ tiktokPostMode: z3.enum(["direct_publish", "draft_upload"]).optional().describe("TikTok only. Use 'draft_upload' for safe dogfood runs."),
1087
+ connectedAccountDestinationId: z3.string().uuid().optional().describe(
395
1088
  "Specific destination when the org has multiple accounts for the provider."
396
1089
  ),
397
- confirmSyntheticDisclosure: z2.boolean().optional().describe(
1090
+ confirmSyntheticDisclosure: z3.boolean().optional().describe(
398
1091
  "Set true when the job requires an AI synthetic-media disclosure."
399
1092
  ),
400
- confirmQaOverride: z2.boolean().optional().describe("Set true to override a non-passing QA gate.")
1093
+ confirmQaOverride: z3.boolean().optional().describe("Set true to override a non-passing QA gate.")
401
1094
  },
402
1095
  annotations: {
403
1096
  readOnlyHint: false,
@@ -420,7 +1113,7 @@ function registerTools(server) {
420
1113
  title: "Connect social account",
421
1114
  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'.",
422
1115
  inputSchema: {
423
- provider: z2.enum([
1116
+ provider: z3.enum([
424
1117
  "youtube",
425
1118
  "x",
426
1119
  "tiktok",
@@ -458,12 +1151,20 @@ function registerTools(server) {
458
1151
  );
459
1152
  }
460
1153
 
1154
+ // src/configured-tools.ts
1155
+ function registerConfiguredTools(server, apiKey = process.env.INSTAVAR_API_KEY) {
1156
+ registerPublicTemplateTools(server);
1157
+ if (apiKey?.trim()) {
1158
+ registerTools(server);
1159
+ }
1160
+ }
1161
+
461
1162
  // package.json
462
1163
  var package_default = {
463
1164
  name: "@instavar/mcp-server",
464
- version: "0.2.2",
1165
+ version: "0.4.1",
465
1166
  mcpName: "com.instavar/mcp-server",
466
- 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.",
1167
+ description: "Make local videos with Instavar Remotion templates for free, then add an API key for hosted Studio workflows.",
467
1168
  type: "module",
468
1169
  license: "MIT",
469
1170
  homepage: "https://instavar.com",
@@ -474,7 +1175,14 @@ var package_default = {
474
1175
  bugs: {
475
1176
  url: "https://github.com/instavar/mcp-server/issues"
476
1177
  },
477
- keywords: ["mcp", "model-context-protocol", "instavar", "video", "claude", "codex"],
1178
+ keywords: [
1179
+ "mcp",
1180
+ "model-context-protocol",
1181
+ "instavar",
1182
+ "video",
1183
+ "claude",
1184
+ "codex"
1185
+ ],
478
1186
  bin: {
479
1187
  "instavar-mcp": "dist/index.js"
480
1188
  },
@@ -508,8 +1216,10 @@ var VERSION = package_default.version;
508
1216
  var HELP = [
509
1217
  "instavar-mcp \u2014 Instavar Studio MCP server",
510
1218
  "",
511
- "Runs over stdio (launched by Claude Code / Codex). Configure via env:",
512
- " INSTAVAR_API_KEY (required) your key from https://instavar.com/studio/settings",
1219
+ "Runs over stdio (launched by Claude Code / Codex).",
1220
+ "Public template tools work without configuration.",
1221
+ "Optional env:",
1222
+ " INSTAVAR_API_KEY unlocks hosted Studio tools",
513
1223
  " INSTAVAR_BASE_URL (optional) defaults to https://instavar.com",
514
1224
  "",
515
1225
  "Flags: --version, --help",
@@ -526,14 +1236,8 @@ async function main() {
526
1236
  process.stdout.write(HELP);
527
1237
  return;
528
1238
  }
529
- if (!process.env.INSTAVAR_API_KEY?.trim()) {
530
- process.stderr.write(
531
- "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"
532
- );
533
- process.exit(1);
534
- }
535
1239
  const server = new McpServer({ name: "instavar", version: VERSION });
536
- registerTools(server);
1240
+ registerConfiguredTools(server);
537
1241
  await server.connect(new StdioServerTransport());
538
1242
  }
539
1243
  main().catch((error) => {
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@instavar/mcp-server",
3
- "version": "0.2.2",
3
+ "version": "0.4.1",
4
4
  "mcpName": "com.instavar/mcp-server",
5
- "description": "Instavar Studio MCP server — drive the full video create→render→approve→publish→metrics workflow from Claude Code or Codex with an API key.",
5
+ "description": "Make local videos with Instavar Remotion templates for free, then add an API key for hosted Studio workflows.",
6
6
  "type": "module",
7
7
  "license": "MIT",
8
8
  "homepage": "https://instavar.com",
@@ -13,7 +13,14 @@
13
13
  "bugs": {
14
14
  "url": "https://github.com/instavar/mcp-server/issues"
15
15
  },
16
- "keywords": ["mcp", "model-context-protocol", "instavar", "video", "claude", "codex"],
16
+ "keywords": [
17
+ "mcp",
18
+ "model-context-protocol",
19
+ "instavar",
20
+ "video",
21
+ "claude",
22
+ "codex"
23
+ ],
17
24
  "bin": {
18
25
  "instavar-mcp": "dist/index.js"
19
26
  },