@instavar/mcp-server 0.3.0 → 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.
- package/README.md +87 -15
- package/dist/index.js +805 -118
- package/package.json +10 -3
package/README.md
CHANGED
|
@@ -4,19 +4,58 @@
|
|
|
4
4
|
[](https://www.npmjs.com/package/@instavar/mcp-server)
|
|
5
5
|
[](https://glama.ai/mcp/servers/instavar/mcp-server)
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
without
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
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` |
|
|
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
|
-
|
|
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
|
|
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 {
|
|
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 {
|
|
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(
|
|
445
|
+
function toToolResult(result2) {
|
|
80
446
|
return {
|
|
81
447
|
content: [
|
|
82
448
|
{
|
|
83
449
|
type: "text",
|
|
84
|
-
text: fenceForModel(JSON.stringify(
|
|
450
|
+
text: fenceForModel(JSON.stringify(result2.body, null, 2))
|
|
85
451
|
}
|
|
86
452
|
],
|
|
87
|
-
isError: !
|
|
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:
|
|
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
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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()
|
|
134
581
|
});
|
|
135
|
-
var
|
|
136
|
-
|
|
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"])
|
|
590
|
+
});
|
|
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,120 +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:
|
|
156
|
-
narrationText:
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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(),
|
|
177
643
|
// A-roll generation (video-window scenes): source "generate" triggers
|
|
178
644
|
// flag-gated WaveSpeed image/video gen server-side from generatePrompt;
|
|
179
645
|
// "url" (default) uses src as-is. Kept in sync with the app's
|
|
180
646
|
// briefSceneSchema - see the parity test in the app suite.
|
|
181
|
-
source:
|
|
182
|
-
generatePrompt:
|
|
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()
|
|
183
670
|
});
|
|
184
|
-
var directionsBriefSchema =
|
|
185
|
-
subtitle:
|
|
186
|
-
unitLabel:
|
|
187
|
-
distanceTimeChip:
|
|
188
|
-
clips:
|
|
189
|
-
|
|
190
|
-
src:
|
|
191
|
-
trimStartSeconds:
|
|
192
|
-
trimEndSeconds:
|
|
193
|
-
speed:
|
|
194
|
-
mirrorFlip:
|
|
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()
|
|
195
682
|
})
|
|
196
683
|
).min(1),
|
|
197
|
-
cues:
|
|
198
|
-
|
|
199
|
-
atSeconds:
|
|
200
|
-
text:
|
|
201
|
-
arrow:
|
|
202
|
-
step:
|
|
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()
|
|
203
690
|
})
|
|
204
691
|
),
|
|
205
|
-
outro:
|
|
206
|
-
heading:
|
|
207
|
-
body:
|
|
208
|
-
seconds:
|
|
692
|
+
outro: z2.object({
|
|
693
|
+
heading: z2.string().optional(),
|
|
694
|
+
body: z2.array(z2.string()).optional(),
|
|
695
|
+
seconds: z2.number().optional()
|
|
209
696
|
}).optional(),
|
|
210
|
-
music:
|
|
211
|
-
perSegment:
|
|
212
|
-
|
|
213
|
-
src:
|
|
214
|
-
clipIndexStart:
|
|
215
|
-
hotOffsetSeconds:
|
|
216
|
-
volume:
|
|
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()
|
|
217
704
|
})
|
|
218
705
|
).optional(),
|
|
219
|
-
crossfadeSeconds:
|
|
706
|
+
crossfadeSeconds: z2.number().optional()
|
|
220
707
|
}).optional()
|
|
221
708
|
});
|
|
222
709
|
var createBriefShape = {
|
|
223
|
-
title:
|
|
224
|
-
script:
|
|
225
|
-
caption:
|
|
226
|
-
publishTarget:
|
|
227
|
-
objective:
|
|
228
|
-
lessonTitle:
|
|
229
|
-
|
|
230
|
-
|
|
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"),
|
|
231
721
|
// No zod .default() here: a default would materialize into the parsed tool
|
|
232
722
|
// arguments and relay to /api/v1/jobs as if the caller stated it, corrupting
|
|
233
723
|
// the resolution provenance the API echoes back. Unset must stay absent;
|
|
234
724
|
// the API re-applies 9:16 server-side.
|
|
235
|
-
aspectRatio:
|
|
236
|
-
compositionId:
|
|
237
|
-
voiceId:
|
|
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(
|
|
238
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."
|
|
239
729
|
),
|
|
240
730
|
directions: directionsBriefSchema.optional().describe(
|
|
241
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')."
|
|
242
|
-
)
|
|
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.")
|
|
243
737
|
};
|
|
244
738
|
var editBriefShape = {
|
|
245
|
-
jobId:
|
|
246
|
-
title:
|
|
247
|
-
script:
|
|
248
|
-
caption:
|
|
249
|
-
lessonTitle:
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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(
|
|
254
749
|
"Updated content objective. Re-routes the composition and triggers a re-render."
|
|
255
750
|
),
|
|
256
|
-
publishTarget:
|
|
751
|
+
publishTarget: z2.enum(PUBLISH_TARGETS).optional().describe(
|
|
257
752
|
"Updated target social platform. Distribution metadata only - does not trigger a re-render."
|
|
258
753
|
),
|
|
259
|
-
voiceId:
|
|
754
|
+
voiceId: z2.string().optional().describe(
|
|
260
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."
|
|
261
762
|
)
|
|
262
763
|
};
|
|
263
764
|
|
|
264
765
|
// src/tools.ts
|
|
265
|
-
var uuid = (desc) =>
|
|
766
|
+
var uuid = (desc) => z3.string().uuid().describe(desc);
|
|
266
767
|
var READ_ONLY = { readOnlyHint: true, openWorldHint: false };
|
|
267
768
|
var SAFE_WRITE = {
|
|
268
769
|
readOnlyHint: false,
|
|
@@ -277,8 +778,8 @@ function registerTools(server) {
|
|
|
277
778
|
title: "List jobs",
|
|
278
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.",
|
|
279
780
|
inputSchema: {
|
|
280
|
-
limit:
|
|
281
|
-
status:
|
|
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([
|
|
282
783
|
"queued",
|
|
283
784
|
"rendering",
|
|
284
785
|
"draft_ready",
|
|
@@ -326,7 +827,7 @@ function registerTools(server) {
|
|
|
326
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.",
|
|
327
828
|
inputSchema: {
|
|
328
829
|
jobId: uuid("The job ID"),
|
|
329
|
-
limit:
|
|
830
|
+
limit: z3.number().int().min(1).max(20).default(10).describe("Max metric snapshots (default 10, max 20)")
|
|
330
831
|
},
|
|
331
832
|
annotations: READ_ONLY
|
|
332
833
|
},
|
|
@@ -342,10 +843,10 @@ function registerTools(server) {
|
|
|
342
843
|
title: "Get cost summary",
|
|
343
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.",
|
|
344
845
|
inputSchema: {
|
|
345
|
-
since:
|
|
346
|
-
provider:
|
|
347
|
-
groupBy:
|
|
348
|
-
includeReconciliation:
|
|
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")
|
|
349
850
|
},
|
|
350
851
|
annotations: READ_ONLY
|
|
351
852
|
},
|
|
@@ -356,6 +857,18 @@ function registerTools(server) {
|
|
|
356
857
|
return runTool(() => apiGet(`/api/v1/cost-summary?${q.toString()}`));
|
|
357
858
|
}
|
|
358
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
|
+
);
|
|
359
872
|
server.registerTool(
|
|
360
873
|
"create_video_brief",
|
|
361
874
|
{
|
|
@@ -381,6 +894,169 @@ function registerTools(server) {
|
|
|
381
894
|
);
|
|
382
895
|
}
|
|
383
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
|
+
);
|
|
384
1060
|
server.registerTool(
|
|
385
1061
|
"approve_job",
|
|
386
1062
|
{
|
|
@@ -406,15 +1082,15 @@ function registerTools(server) {
|
|
|
406
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.",
|
|
407
1083
|
inputSchema: {
|
|
408
1084
|
jobId: uuid("The approved job to publish"),
|
|
409
|
-
youtubePrivacyStatus:
|
|
410
|
-
tiktokPostMode:
|
|
411
|
-
connectedAccountDestinationId:
|
|
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(
|
|
412
1088
|
"Specific destination when the org has multiple accounts for the provider."
|
|
413
1089
|
),
|
|
414
|
-
confirmSyntheticDisclosure:
|
|
1090
|
+
confirmSyntheticDisclosure: z3.boolean().optional().describe(
|
|
415
1091
|
"Set true when the job requires an AI synthetic-media disclosure."
|
|
416
1092
|
),
|
|
417
|
-
confirmQaOverride:
|
|
1093
|
+
confirmQaOverride: z3.boolean().optional().describe("Set true to override a non-passing QA gate.")
|
|
418
1094
|
},
|
|
419
1095
|
annotations: {
|
|
420
1096
|
readOnlyHint: false,
|
|
@@ -437,7 +1113,7 @@ function registerTools(server) {
|
|
|
437
1113
|
title: "Connect social account",
|
|
438
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'.",
|
|
439
1115
|
inputSchema: {
|
|
440
|
-
provider:
|
|
1116
|
+
provider: z3.enum([
|
|
441
1117
|
"youtube",
|
|
442
1118
|
"x",
|
|
443
1119
|
"tiktok",
|
|
@@ -475,12 +1151,20 @@ function registerTools(server) {
|
|
|
475
1151
|
);
|
|
476
1152
|
}
|
|
477
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
|
+
|
|
478
1162
|
// package.json
|
|
479
1163
|
var package_default = {
|
|
480
1164
|
name: "@instavar/mcp-server",
|
|
481
|
-
version: "0.
|
|
1165
|
+
version: "0.4.1",
|
|
482
1166
|
mcpName: "com.instavar/mcp-server",
|
|
483
|
-
description: "
|
|
1167
|
+
description: "Make local videos with Instavar Remotion templates for free, then add an API key for hosted Studio workflows.",
|
|
484
1168
|
type: "module",
|
|
485
1169
|
license: "MIT",
|
|
486
1170
|
homepage: "https://instavar.com",
|
|
@@ -491,7 +1175,14 @@ var package_default = {
|
|
|
491
1175
|
bugs: {
|
|
492
1176
|
url: "https://github.com/instavar/mcp-server/issues"
|
|
493
1177
|
},
|
|
494
|
-
keywords: [
|
|
1178
|
+
keywords: [
|
|
1179
|
+
"mcp",
|
|
1180
|
+
"model-context-protocol",
|
|
1181
|
+
"instavar",
|
|
1182
|
+
"video",
|
|
1183
|
+
"claude",
|
|
1184
|
+
"codex"
|
|
1185
|
+
],
|
|
495
1186
|
bin: {
|
|
496
1187
|
"instavar-mcp": "dist/index.js"
|
|
497
1188
|
},
|
|
@@ -525,8 +1216,10 @@ var VERSION = package_default.version;
|
|
|
525
1216
|
var HELP = [
|
|
526
1217
|
"instavar-mcp \u2014 Instavar Studio MCP server",
|
|
527
1218
|
"",
|
|
528
|
-
"Runs over stdio (launched by Claude Code / Codex).
|
|
529
|
-
"
|
|
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",
|
|
530
1223
|
" INSTAVAR_BASE_URL (optional) defaults to https://instavar.com",
|
|
531
1224
|
"",
|
|
532
1225
|
"Flags: --version, --help",
|
|
@@ -543,14 +1236,8 @@ async function main() {
|
|
|
543
1236
|
process.stdout.write(HELP);
|
|
544
1237
|
return;
|
|
545
1238
|
}
|
|
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
1239
|
const server = new McpServer({ name: "instavar", version: VERSION });
|
|
553
|
-
|
|
1240
|
+
registerConfiguredTools(server);
|
|
554
1241
|
await server.connect(new StdioServerTransport());
|
|
555
1242
|
}
|
|
556
1243
|
main().catch((error) => {
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@instavar/mcp-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"mcpName": "com.instavar/mcp-server",
|
|
5
|
-
"description": "
|
|
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": [
|
|
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
|
},
|