@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.
- package/README.md +87 -15
- package/dist/index.js +818 -114
- 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()
|
|
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
|
|
136
|
-
|
|
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:
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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 =
|
|
178
|
-
subtitle:
|
|
179
|
-
unitLabel:
|
|
180
|
-
distanceTimeChip:
|
|
181
|
-
clips:
|
|
182
|
-
|
|
183
|
-
src:
|
|
184
|
-
trimStartSeconds:
|
|
185
|
-
trimEndSeconds:
|
|
186
|
-
speed:
|
|
187
|
-
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()
|
|
188
682
|
})
|
|
189
683
|
).min(1),
|
|
190
|
-
cues:
|
|
191
|
-
|
|
192
|
-
atSeconds:
|
|
193
|
-
text:
|
|
194
|
-
arrow:
|
|
195
|
-
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()
|
|
196
690
|
})
|
|
197
691
|
),
|
|
198
|
-
outro:
|
|
199
|
-
heading:
|
|
200
|
-
body:
|
|
201
|
-
seconds:
|
|
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:
|
|
204
|
-
perSegment:
|
|
205
|
-
|
|
206
|
-
src:
|
|
207
|
-
clipIndexStart:
|
|
208
|
-
hotOffsetSeconds:
|
|
209
|
-
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()
|
|
210
704
|
})
|
|
211
705
|
).optional(),
|
|
212
|
-
crossfadeSeconds:
|
|
706
|
+
crossfadeSeconds: z2.number().optional()
|
|
213
707
|
}).optional()
|
|
214
708
|
});
|
|
215
709
|
var createBriefShape = {
|
|
216
|
-
title:
|
|
217
|
-
script:
|
|
218
|
-
caption:
|
|
219
|
-
publishTarget:
|
|
220
|
-
objective:
|
|
221
|
-
lessonTitle:
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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:
|
|
232
|
-
title:
|
|
233
|
-
script:
|
|
234
|
-
caption:
|
|
235
|
-
lessonTitle:
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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:
|
|
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) =>
|
|
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:
|
|
264
|
-
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([
|
|
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:
|
|
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:
|
|
329
|
-
provider:
|
|
330
|
-
groupBy:
|
|
331
|
-
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")
|
|
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
|
|
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:
|
|
393
|
-
tiktokPostMode:
|
|
394
|
-
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(
|
|
395
1088
|
"Specific destination when the org has multiple accounts for the provider."
|
|
396
1089
|
),
|
|
397
|
-
confirmSyntheticDisclosure:
|
|
1090
|
+
confirmSyntheticDisclosure: z3.boolean().optional().describe(
|
|
398
1091
|
"Set true when the job requires an AI synthetic-media disclosure."
|
|
399
1092
|
),
|
|
400
|
-
confirmQaOverride:
|
|
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:
|
|
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.
|
|
1165
|
+
version: "0.4.1",
|
|
465
1166
|
mcpName: "com.instavar/mcp-server",
|
|
466
|
-
description: "
|
|
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: [
|
|
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).
|
|
512
|
-
"
|
|
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
|
-
|
|
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.
|
|
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
|
},
|