@kolbo/mcp 1.14.0 → 1.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -19
- package/bin/kolbo-mcp.js +1 -1
- package/package.json +56 -55
- package/src/client.js +116 -9
- package/src/tools/artifacts.js +4 -11
- package/src/tools/generate.js +67 -26
package/README.md
CHANGED
|
@@ -4,20 +4,7 @@ Use [Kolbo AI](https://kolbo.ai) as native tools in Claude Code and Claude Deskt
|
|
|
4
4
|
|
|
5
5
|
Generate images, videos, music, speech, sound effects, multi-scene campaigns, and conversational chat — all from natural language in your coding environment. 100+ AI models behind Smart Select routing, with reusable Visual DNA profiles for character/style consistency.
|
|
6
6
|
|
|
7
|
-
##
|
|
8
|
-
|
|
9
|
-
The easiest way to use this MCP from Claude Code is the official **Kolbo plugin**, which bundles this server + the routing skill + a first-run API-key prompt — no `settings.json` editing required:
|
|
10
|
-
|
|
11
|
-
```bash
|
|
12
|
-
claude plugin marketplace add Zoharvan12/kolbo-claude-plugin
|
|
13
|
-
claude plugin install kolbo@kolbo
|
|
14
|
-
```
|
|
15
|
-
|
|
16
|
-
You'll be prompted for your API key once and it's stored in your OS keychain. The plugin lives in its own tiny repo at [`kolbo-claude-plugin`](https://github.com/Zoharvan12/kolbo-claude-plugin); the routing skill it ships is auto-synced from the canonical source in [`kolbo-code`](https://github.com/Zoharvan12/kolbo-code), so the Claude Code plugin and the Kolbo Code CLI always carry the same skill.
|
|
17
|
-
|
|
18
|
-
Continue below if you'd rather wire the MCP in by hand (Claude Desktop, Cursor, custom setups).
|
|
19
|
-
|
|
20
|
-
## Manual setup
|
|
7
|
+
## Quick Setup
|
|
21
8
|
|
|
22
9
|
### 1. Get an API Key
|
|
23
10
|
|
|
@@ -128,11 +115,6 @@ Every generation tool also accepts an optional `resolution` arg. Images use `"1K
|
|
|
128
115
|
| `move_folder_contents` | Move every item in a folder to a project |
|
|
129
116
|
| `get_media_stats` | Counts + storage bytes per type (optionally per project) |
|
|
130
117
|
|
|
131
|
-
**Artifacts**
|
|
132
|
-
| Tool | Description |
|
|
133
|
-
|------|-------------|
|
|
134
|
-
| `publish_html_artifact` | Publish an HTML page, SVG, or Mermaid diagram and get a public shareable URL on `sites.kolbo.ai`. Pass `share_token` from a prior publish to update the same URL in place (old content kept in version history). |
|
|
135
|
-
|
|
136
118
|
**Discovery & Account**
|
|
137
119
|
| Tool | Description |
|
|
138
120
|
|------|-------------|
|
package/bin/kolbo-mcp.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
1
|
+
#!/usr/bin/env node
|
|
2
2
|
require('../src/index.js');
|
package/package.json
CHANGED
|
@@ -1,55 +1,56 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@kolbo/mcp",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Kolbo AI MCP Server - Generate images, videos, music, speech, and sound effects from Claude Code",
|
|
5
|
-
"main": "src/index.js",
|
|
6
|
-
"bin": {
|
|
7
|
-
"kolbo-mcp": "./bin/kolbo-mcp.js"
|
|
8
|
-
},
|
|
9
|
-
"scripts": {
|
|
10
|
-
"start": "node src/index.js",
|
|
11
|
-
"smoke": "node scripts/smoke.js",
|
|
12
|
-
"check-parity": "node scripts/check-parity.js",
|
|
13
|
-
"prepublishOnly": "node scripts/smoke.js && node scripts/check-parity.js"
|
|
14
|
-
},
|
|
15
|
-
"keywords": [
|
|
16
|
-
"kolbo",
|
|
17
|
-
"mcp",
|
|
18
|
-
"ai",
|
|
19
|
-
"image-generation",
|
|
20
|
-
"video-generation",
|
|
21
|
-
"music-generation",
|
|
22
|
-
"text-to-speech",
|
|
23
|
-
"claude-code",
|
|
24
|
-
"claude-desktop",
|
|
25
|
-
"model-context-protocol"
|
|
26
|
-
],
|
|
27
|
-
"license": "MIT",
|
|
28
|
-
"repository": {
|
|
29
|
-
"type": "git",
|
|
30
|
-
"url": "https://github.com/Zoharvan12/kolbo-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
"
|
|
40
|
-
"
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
"
|
|
45
|
-
"
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@kolbo/mcp",
|
|
3
|
+
"version": "1.15.0",
|
|
4
|
+
"description": "Kolbo AI MCP Server - Generate images, videos, music, speech, and sound effects from Claude Code",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"kolbo-mcp": "./bin/kolbo-mcp.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"start": "node src/index.js",
|
|
11
|
+
"smoke": "node scripts/smoke.js",
|
|
12
|
+
"check-parity": "node scripts/check-parity.js",
|
|
13
|
+
"prepublishOnly": "node scripts/smoke.js && node scripts/check-parity.js"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"kolbo",
|
|
17
|
+
"mcp",
|
|
18
|
+
"ai",
|
|
19
|
+
"image-generation",
|
|
20
|
+
"video-generation",
|
|
21
|
+
"music-generation",
|
|
22
|
+
"text-to-speech",
|
|
23
|
+
"claude-code",
|
|
24
|
+
"claude-desktop",
|
|
25
|
+
"model-context-protocol"
|
|
26
|
+
],
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "https://github.com/Zoharvan12/kolbo-code",
|
|
31
|
+
"directory": "packages/kolbo-mcp"
|
|
32
|
+
},
|
|
33
|
+
"homepage": "https://docs.kolbo.ai/developer-api/claude-code-skill",
|
|
34
|
+
"author": "Kolbo AI <support@kolbo.ai>",
|
|
35
|
+
"publishConfig": {
|
|
36
|
+
"access": "public"
|
|
37
|
+
},
|
|
38
|
+
"files": [
|
|
39
|
+
"src/",
|
|
40
|
+
"bin/",
|
|
41
|
+
"README.md"
|
|
42
|
+
],
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"@modelcontextprotocol/sdk": "1.29.0",
|
|
45
|
+
"form-data": "^4.0.5",
|
|
46
|
+
"zod": "^3.25.0"
|
|
47
|
+
},
|
|
48
|
+
"overrides": {
|
|
49
|
+
"hono": "^4.12.12",
|
|
50
|
+
"@hono/node-server": "^1.19.13",
|
|
51
|
+
"path-to-regexp": "^8.4.2"
|
|
52
|
+
},
|
|
53
|
+
"engines": {
|
|
54
|
+
"node": ">=18.0.0"
|
|
55
|
+
}
|
|
56
|
+
}
|
package/src/client.js
CHANGED
|
@@ -139,6 +139,63 @@ function readCliAuthKey() {
|
|
|
139
139
|
return null;
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
// Burst control
|
|
144
|
+
// ---------------------------------------------------------------------------
|
|
145
|
+
// Caps in-flight requests so multi-step tool calls (file upload + generate +
|
|
146
|
+
// poll status) don't fan out past the kolbo-api global rate limit. The SDK
|
|
147
|
+
// tier on the server is 5000 req/min per API key; 6 in-flight × typical 2s
|
|
148
|
+
// latency keeps us at ~180/min — plenty of headroom.
|
|
149
|
+
const MAX_CONCURRENT = 6;
|
|
150
|
+
let inFlight = 0;
|
|
151
|
+
const waiting = [];
|
|
152
|
+
|
|
153
|
+
function acquireSlot() {
|
|
154
|
+
if (inFlight < MAX_CONCURRENT) {
|
|
155
|
+
inFlight++;
|
|
156
|
+
return Promise.resolve();
|
|
157
|
+
}
|
|
158
|
+
return new Promise((resolve) => waiting.push(resolve));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function releaseSlot() {
|
|
162
|
+
if (waiting.length > 0) {
|
|
163
|
+
waiting.shift()();
|
|
164
|
+
} else {
|
|
165
|
+
inFlight = Math.max(0, inFlight - 1);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Parse retry hint from a 429 response. Honors (in order):
|
|
173
|
+
* - body.retryAfter (kolbo-api shape)
|
|
174
|
+
* - Retry-After header (RFC, seconds)
|
|
175
|
+
* - RateLimit-Reset header (seconds until reset)
|
|
176
|
+
* Falls back to 5s. Caps at 60s so we don't hang the tool call forever.
|
|
177
|
+
*/
|
|
178
|
+
function parseRetryAfter(response, body) {
|
|
179
|
+
let seconds = null;
|
|
180
|
+
if (body && typeof body.retryAfter === 'number') seconds = body.retryAfter;
|
|
181
|
+
if (seconds == null) {
|
|
182
|
+
const ra = response.headers.get('retry-after');
|
|
183
|
+
if (ra) {
|
|
184
|
+
const n = Number(ra);
|
|
185
|
+
if (Number.isFinite(n)) seconds = n;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (seconds == null) {
|
|
189
|
+
const reset = response.headers.get('ratelimit-reset');
|
|
190
|
+
if (reset) {
|
|
191
|
+
const n = Number(reset);
|
|
192
|
+
if (Number.isFinite(n)) seconds = n;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
if (seconds == null || !Number.isFinite(seconds) || seconds <= 0) seconds = 5;
|
|
196
|
+
return Math.min(seconds, 60) * 1000;
|
|
197
|
+
}
|
|
198
|
+
|
|
142
199
|
// ---------------------------------------------------------------------------
|
|
143
200
|
// HTTP client
|
|
144
201
|
// ---------------------------------------------------------------------------
|
|
@@ -189,13 +246,31 @@ class KolboClient {
|
|
|
189
246
|
}
|
|
190
247
|
|
|
191
248
|
async request(method, reqPath, body = null) {
|
|
192
|
-
|
|
249
|
+
await acquireSlot();
|
|
250
|
+
try {
|
|
251
|
+
const result = await this._doRequestWithRetry(method, reqPath, body);
|
|
193
252
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
253
|
+
// On 401, try re-reading auth store and retry once
|
|
254
|
+
if (result._status === 401 && this._tryRefreshKey()) {
|
|
255
|
+
return this._doRequestWithRetry(method, reqPath, body);
|
|
256
|
+
}
|
|
257
|
+
return result;
|
|
258
|
+
} finally {
|
|
259
|
+
releaseSlot();
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
async _doRequestWithRetry(method, reqPath, body = null) {
|
|
264
|
+
try {
|
|
265
|
+
return await this._doRequest(method, reqPath, body);
|
|
266
|
+
} catch (err) {
|
|
267
|
+
if (err instanceof KolboApiError && err.status === 429) {
|
|
268
|
+
const delay = err.data?._kolboRetryAfterMs ?? 5000;
|
|
269
|
+
await sleep(delay);
|
|
270
|
+
return this._doRequest(method, reqPath, body);
|
|
271
|
+
}
|
|
272
|
+
throw err;
|
|
197
273
|
}
|
|
198
|
-
return result;
|
|
199
274
|
}
|
|
200
275
|
|
|
201
276
|
async _doRequest(method, reqPath, body = null) {
|
|
@@ -232,6 +307,13 @@ class KolboClient {
|
|
|
232
307
|
});
|
|
233
308
|
}
|
|
234
309
|
|
|
310
|
+
if (response.status === 429) {
|
|
311
|
+
throw new KolboApiError('Rate limited by Kolbo API. Retrying after backoff.', {
|
|
312
|
+
status: 429,
|
|
313
|
+
data: { ...(data || {}), _kolboRetryAfterMs: parseRetryAfter(response, data) },
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
|
|
235
317
|
if (!response.ok || data.success === false) {
|
|
236
318
|
const message = data.error || data.message || `API error: ${response.status}`;
|
|
237
319
|
const code = data.code || null;
|
|
@@ -278,11 +360,29 @@ class KolboClient {
|
|
|
278
360
|
}
|
|
279
361
|
|
|
280
362
|
async postMultipart(reqPath, formData) {
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
363
|
+
await acquireSlot();
|
|
364
|
+
try {
|
|
365
|
+
const result = await this._doMultipartWithRetry(reqPath, formData);
|
|
366
|
+
if (result._status === 401 && this._tryRefreshKey()) {
|
|
367
|
+
return this._doMultipartWithRetry(reqPath, formData);
|
|
368
|
+
}
|
|
369
|
+
return result;
|
|
370
|
+
} finally {
|
|
371
|
+
releaseSlot();
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
async _doMultipartWithRetry(reqPath, formData) {
|
|
376
|
+
try {
|
|
377
|
+
return await this._doMultipart(reqPath, formData);
|
|
378
|
+
} catch (err) {
|
|
379
|
+
if (err instanceof KolboApiError && err.status === 429) {
|
|
380
|
+
const delay = err.data?._kolboRetryAfterMs ?? 5000;
|
|
381
|
+
await sleep(delay);
|
|
382
|
+
return this._doMultipart(reqPath, formData);
|
|
383
|
+
}
|
|
384
|
+
throw err;
|
|
284
385
|
}
|
|
285
|
-
return result;
|
|
286
386
|
}
|
|
287
387
|
|
|
288
388
|
async _doMultipart(reqPath, formData) {
|
|
@@ -319,6 +419,13 @@ class KolboClient {
|
|
|
319
419
|
});
|
|
320
420
|
}
|
|
321
421
|
|
|
422
|
+
if (response.status === 429) {
|
|
423
|
+
throw new KolboApiError('Rate limited by Kolbo API. Retrying after backoff.', {
|
|
424
|
+
status: 429,
|
|
425
|
+
data: { ...(data || {}), _kolboRetryAfterMs: parseRetryAfter(response, data) },
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
|
|
322
429
|
if (!response.ok || data.success === false) {
|
|
323
430
|
const message = data.error || data.message || `API error: ${response.status}`;
|
|
324
431
|
const code = data.code || null;
|
package/src/tools/artifacts.js
CHANGED
|
@@ -9,29 +9,23 @@ function registerArtifactTools(server, client) {
|
|
|
9
9
|
// ─── publish_html_artifact ─────────────────────────────────────
|
|
10
10
|
server.tool(
|
|
11
11
|
'publish_html_artifact',
|
|
12
|
-
'Publish an HTML page (or SVG / Mermaid diagram) to kolbo.ai and return a public shareable URL. Use this when the user explicitly asks to share, publish, or deploy a built artifact so they can send the URL to someone. The content is hosted at https://sites.kolbo.ai/<slug>; the page is served with restrictive CSP (no fetch/XHR/form-action) so it cannot exfiltrate data. Identical content uploaded twice returns the same URL (server dedup).
|
|
12
|
+
'Publish an HTML page (or SVG / Mermaid diagram) to kolbo.ai and return a public shareable URL. Use this when the user explicitly asks to share, publish, or deploy a built artifact so they can send the URL to someone. The content is hosted at https://sites.kolbo.ai/<slug>; the page is served with restrictive CSP (no fetch/XHR/form-action) so it cannot exfiltrate data. Identical content uploaded twice returns the same URL (server dedup).',
|
|
13
13
|
{
|
|
14
14
|
title: z.string().describe('Human-friendly title for the page (also used to generate the SEO slug). Keep under ~60 chars.'),
|
|
15
15
|
content: z.string().describe('The raw artifact body. For type="html" this is a full HTML document (DOCTYPE + html/head/body). For "svg" it is an <svg> document. For "mermaid" it is the Mermaid source text.'),
|
|
16
16
|
type: z.enum(['html', 'svg', 'mermaid']).optional().describe('Artifact type. Default: "html".'),
|
|
17
17
|
allow_js: z.boolean().optional().describe('Allow inline <script> execution on the published page. Default: false. Required for Tailwind JIT, Chart.js, Three.js, React-from-CDN etc.'),
|
|
18
|
-
share_token: z.string().optional().describe('Optional. Pass the `shareToken` returned from a previous publish to update that artifact in place. The public URL stays the same and the old content is moved into version history. Omit this on the first publish.'),
|
|
19
18
|
},
|
|
20
|
-
async ({ title, content, type, allow_js
|
|
19
|
+
async ({ title, content, type, allow_js }) => {
|
|
21
20
|
if (!title || !title.trim()) throw new Error('title is required');
|
|
22
21
|
if (typeof content !== 'string' || !content.length) throw new Error('content is required');
|
|
23
22
|
|
|
24
|
-
const
|
|
23
|
+
const result = await client.post('/artifact/quick-share', {
|
|
25
24
|
title: title.trim(),
|
|
26
25
|
content,
|
|
27
26
|
type: type || 'html',
|
|
28
27
|
allowJs: allow_js === true,
|
|
29
|
-
};
|
|
30
|
-
if (typeof share_token === 'string' && share_token.trim()) {
|
|
31
|
-
body.shareToken = share_token.trim();
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const result = await client.post('/artifact/quick-share', body);
|
|
28
|
+
});
|
|
35
29
|
|
|
36
30
|
const artifact = result?.data || {};
|
|
37
31
|
const slug = artifact.shareableSlug || artifact.shareToken;
|
|
@@ -55,7 +49,6 @@ function registerArtifactTools(server, client) {
|
|
|
55
49
|
shareToken: artifact.shareToken,
|
|
56
50
|
shareableSlug: slug,
|
|
57
51
|
duplicate: result?.duplicate === true,
|
|
58
|
-
updated: result?.updated === true,
|
|
59
52
|
title: artifact.title,
|
|
60
53
|
}),
|
|
61
54
|
}],
|
package/src/tools/generate.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
const { z } = require('zod');
|
|
7
7
|
const FormData = require('form-data');
|
|
8
|
-
const { pollUntilDone } = require('../polling');
|
|
8
|
+
const { pollUntilDone, PollingTimeoutError } = require('../polling');
|
|
9
9
|
const { resolveToBuffer, creditFields } = require('./_shared');
|
|
10
10
|
|
|
11
11
|
function registerGenerateTools(server, client) {
|
|
@@ -74,13 +74,16 @@ function registerGenerateTools(server, client) {
|
|
|
74
74
|
visual_dna_ids, moodboard_id, enable_web_search, resolution
|
|
75
75
|
});
|
|
76
76
|
|
|
77
|
-
//
|
|
78
|
-
//
|
|
79
|
-
//
|
|
77
|
+
// Flux/Seedance/DNA-anchored edits routinely run 3-5 min. Old 120s
|
|
78
|
+
// timeout forced every call into the timeout-and-recover path via
|
|
79
|
+
// get_generation_status — and when the model fired multiple parallel
|
|
80
|
+
// edits, some recovered, some got abandoned with their URLs lost
|
|
81
|
+
// (the user's "lost generations" bug). 360s/480s covers the realistic
|
|
82
|
+
// p99 single-call duration without making the model poll forever.
|
|
80
83
|
const heavy = (source_images && source_images.length > 1) || (visual_dna_ids && visual_dna_ids.length > 0);
|
|
81
84
|
const result = await pollUntilDone(client, gen.generation_id, {
|
|
82
85
|
interval: (gen.poll_interval_hint || 3) * 1000,
|
|
83
|
-
timeout: heavy ?
|
|
86
|
+
timeout: heavy ? 480000 : 360000
|
|
84
87
|
});
|
|
85
88
|
|
|
86
89
|
return {
|
|
@@ -109,6 +112,7 @@ function registerGenerateTools(server, client) {
|
|
|
109
112
|
aspect_ratio: z.string().optional().describe('Aspect ratio applied to every scene (e.g., "1:1", "16:9", "9:16"). Must be in the chosen model\'s `supported_aspect_ratios` from list_models. Default: "1:1"'),
|
|
110
113
|
workflow_type: z.string().optional().describe('"image" (default) or "video"'),
|
|
111
114
|
duration: z.number().optional().describe('Duration in seconds per scene (video mode only). Must be a value in `supported_durations` from list_models, OR within `min_output_duration`-`max_output_duration`. E.g., 5 or 10.'),
|
|
115
|
+
sound_enabled: z.boolean().optional().describe('Video mode only. Enable (`true`) or disable (`false`) AI-generated synced audio on every scene. Only honored by models with `sound_generation_type: "native"` from list_models (Veo 3.1, Kling V3/2.6/O3, PixVerse V6). Omit to use each model\'s `sound_enabled_by_default`. Pass `false` when the user says no sound / silent / mute / without audio.'),
|
|
112
116
|
enhance_prompt: z.boolean().optional().describe('Enhance prompts per scene. Default: true'),
|
|
113
117
|
reference_images: z.array(z.string()).optional().describe('Array of reference image URLs to guide style/composition of every scene. **Cap: pass at most `max_reference_images` URLs from list_models for the chosen model.**'),
|
|
114
118
|
visual_dna_ids: z.array(z.string()).optional().describe('Array of Visual DNA profile IDs to apply consistently across every scene. **Cap: pass at most `max_visual_dna` IDs from list_models for the chosen model.** This is the ideal way to keep a character or product looking the same in all scenes of a campaign.'),
|
|
@@ -116,9 +120,9 @@ function registerGenerateTools(server, client) {
|
|
|
116
120
|
moodboard_ids: z.array(z.string()).optional().describe('Multiple moodboard IDs when blending styles. Prefer `moodboard_id` for single moodboards.'),
|
|
117
121
|
resolution: z.string().optional().describe('Resolution tier applied to every scene. Images: "1K" / "2K" / "3K" / "4K". Videos: "720p" / "1080p" / "1440p" / "2160p". Values are model-dependent — call list_models and read supported_resolutions on the target model. Multiplied across every scene.')
|
|
118
122
|
},
|
|
119
|
-
async ({ prompt, scene_count, model, aspect_ratio, workflow_type, duration, enhance_prompt, reference_images, visual_dna_ids, moodboard_id, moodboard_ids, resolution }) => {
|
|
123
|
+
async ({ prompt, scene_count, model, aspect_ratio, workflow_type, duration, sound_enabled, enhance_prompt, reference_images, visual_dna_ids, moodboard_id, moodboard_ids, resolution }) => {
|
|
120
124
|
const gen = await client.post('/v1/generate/creative-director', {
|
|
121
|
-
prompt, scene_count, model, aspect_ratio, workflow_type, duration,
|
|
125
|
+
prompt, scene_count, model, aspect_ratio, workflow_type, duration, sound_enabled,
|
|
122
126
|
enhance_prompt, reference_images, visual_dna_ids, moodboard_id, moodboard_ids, resolution
|
|
123
127
|
});
|
|
124
128
|
|
|
@@ -168,11 +172,12 @@ function registerGenerateTools(server, client) {
|
|
|
168
172
|
enhance_prompt: z.boolean().optional().describe('Enhance the prompt. Default: true'),
|
|
169
173
|
reference_images: z.array(z.string()).optional().describe('Array of image URLs used as visual references (style / composition / subject). **Cap: pass at most `max_reference_images` URLs from list_models for the chosen model — exceeding it is a deterministic 400.**'),
|
|
170
174
|
resolution: z.string().optional().describe('Video resolution tier (vertical pixels): "720p" / "1080p" / "1440p" / "2160p". Some models use labels like "512P"/"1024P"/"768P"/"1080P". Model-dependent — call list_models and read supported_resolutions. Read resolution_multipliers to predict cost.'),
|
|
175
|
+
sound_enabled: z.boolean().optional().describe('Enable (`true`) or disable (`false`) AI-generated synced audio on the output video. Only honored by models with `sound_generation_type: "native"` from list_models (e.g. Veo 3.1, Kling V3/2.6, PixVerse V6). On `sound_generation_type: "none"` models the flag has no effect. Omit to use the model\'s `sound_enabled_by_default`. Pass `false` when the user says no sound / silent / mute / without audio. Enabling sound may apply `sound_credit_multiplier` to cost.'),
|
|
171
176
|
preset_id: z.string().optional().describe('Preset ID from list_presets type="video" to apply a saved motion/style preset to this generation.')
|
|
172
177
|
},
|
|
173
|
-
async ({ prompt, model, aspect_ratio, duration, enhance_prompt, reference_images, resolution, preset_id }) => {
|
|
178
|
+
async ({ prompt, model, aspect_ratio, duration, enhance_prompt, reference_images, resolution, sound_enabled, preset_id }) => {
|
|
174
179
|
const gen = await client.post('/v1/generate/video', {
|
|
175
|
-
prompt, model, aspect_ratio, duration, enhance_prompt, reference_images, resolution, preset_id
|
|
180
|
+
prompt, model, aspect_ratio, duration, enhance_prompt, reference_images, resolution, sound_enabled, preset_id
|
|
176
181
|
});
|
|
177
182
|
|
|
178
183
|
const result = await pollUntilDone(client, gen.generation_id, {
|
|
@@ -209,11 +214,12 @@ function registerGenerateTools(server, client) {
|
|
|
209
214
|
duration: z.number().optional().describe('Duration in seconds. Must be in `supported_durations` from list_models, OR within `min_output_duration`-`max_output_duration`. Default: 5'),
|
|
210
215
|
enhance_prompt: z.boolean().optional().describe('Enhance the motion prompt. Default: true'),
|
|
211
216
|
visual_dna_ids: z.array(z.string()).optional().describe('Array of Visual DNA profile IDs to maintain consistency with prior characters / styles. **Cap: pass at most `max_visual_dna` IDs from list_models for the chosen model; if `supports_visual_dna: false` the model ignores DNA entirely.**'),
|
|
212
|
-
resolution: z.string().optional().describe('Video resolution tier (vertical pixels): "720p" / "1080p" / "1440p" / "2160p". Some models use labels like "512P"/"1024P"/"768P"/"1080P". Model-dependent — call list_models and read supported_resolutions. Read resolution_multipliers to predict cost.')
|
|
217
|
+
resolution: z.string().optional().describe('Video resolution tier (vertical pixels): "720p" / "1080p" / "1440p" / "2160p". Some models use labels like "512P"/"1024P"/"768P"/"1080P". Model-dependent — call list_models and read supported_resolutions. Read resolution_multipliers to predict cost.'),
|
|
218
|
+
sound_enabled: z.boolean().optional().describe('Enable (`true`) or disable (`false`) AI-generated synced audio on the output video. Only honored by models with `sound_generation_type: "native"` from list_models (e.g. Veo 3.1 Lite, Kling V3 4K, PixVerse V6, Kling 2.6/v3). On `sound_generation_type: "none"` models the flag has no effect. Omit to use the model\'s `sound_enabled_by_default`. Pass `false` when the user says no sound / silent / mute / without audio. Enabling sound may apply `sound_credit_multiplier` to cost.')
|
|
213
219
|
},
|
|
214
|
-
async ({ image_url, prompt, model, aspect_ratio, duration, enhance_prompt, visual_dna_ids, resolution }) => {
|
|
220
|
+
async ({ image_url, prompt, model, aspect_ratio, duration, enhance_prompt, visual_dna_ids, resolution, sound_enabled }) => {
|
|
215
221
|
const gen = await client.post('/v1/generate/video/from-image', {
|
|
216
|
-
image_url, prompt, model, aspect_ratio, duration, enhance_prompt, visual_dna_ids, resolution
|
|
222
|
+
image_url, prompt, model, aspect_ratio, duration, enhance_prompt, visual_dna_ids, resolution, sound_enabled
|
|
217
223
|
});
|
|
218
224
|
|
|
219
225
|
const result = await pollUntilDone(client, gen.generation_id, {
|
|
@@ -383,13 +389,42 @@ function registerGenerateTools(server, client) {
|
|
|
383
389
|
// ─── get_generation_status ─────────────────────────────────
|
|
384
390
|
server.tool(
|
|
385
391
|
'get_generation_status',
|
|
386
|
-
'
|
|
392
|
+
'Resume polling a generation after a timeout. Pass the generation_id from a prior generation tool that timed out. This call BLOCKS server-side, polling internally — you do NOT need to call it again in a loop. Defaults to a 10-minute internal poll which covers most image edits and short videos; pass `wait_seconds` up to 1700 (~28 min) for long video / 3D / batch generations. **If you fired multiple generations in parallel and they all timed out, you MUST call get_generation_status for EACH generation_id in this same turn — do not give up on any of them, or their URLs will be lost forever.** If a single call returns `still_pending: true`, that specific generation is genuinely slow — STOP calling THAT one, tell the user it is still running, and resume on the next user turn. Never call this tool more than ONCE per generation_id consecutively in the same turn.',
|
|
387
393
|
{
|
|
388
|
-
generation_id: z.string().describe('The generation ID to check')
|
|
394
|
+
generation_id: z.string().describe('The generation ID to check'),
|
|
395
|
+
wait_seconds: z.number().int().min(60).max(1700).optional().describe('How long to block-poll internally before giving up (60–1700 seconds, default 600). Values below 300 are clamped up to 300 server-side because shorter waits cause the model to abandon still-running generations. Use higher values for video / 3D / large batches that can legitimately take 15+ minutes.'),
|
|
389
396
|
},
|
|
390
|
-
async ({ generation_id }) => {
|
|
391
|
-
|
|
392
|
-
|
|
397
|
+
async ({ generation_id, wait_seconds }) => {
|
|
398
|
+
// Clamp the floor: shorter waits caused models to abandon generations
|
|
399
|
+
// that completed seconds later (URLs lost forever, paid-for compute
|
|
400
|
+
// wasted). 300s is the floor that survived real-world cases.
|
|
401
|
+
const requested = wait_seconds ?? 600;
|
|
402
|
+
const timeoutMs = Math.max(requested, 300) * 1000;
|
|
403
|
+
let result;
|
|
404
|
+
try {
|
|
405
|
+
result = await pollUntilDone(client, generation_id, {
|
|
406
|
+
interval: 5000,
|
|
407
|
+
timeout: timeoutMs,
|
|
408
|
+
});
|
|
409
|
+
} catch (err) {
|
|
410
|
+
if (err instanceof PollingTimeoutError) {
|
|
411
|
+
const current = await client.get(`/v1/generate/${encodeURIComponent(generation_id)}/status`).catch(() => null);
|
|
412
|
+
const minutes = Math.round(timeoutMs / 60000);
|
|
413
|
+
return {
|
|
414
|
+
content: [{
|
|
415
|
+
type: 'text',
|
|
416
|
+
text: JSON.stringify({
|
|
417
|
+
generation_id,
|
|
418
|
+
state: current?.state ?? 'unknown',
|
|
419
|
+
still_pending: true,
|
|
420
|
+
waited_seconds: Math.round(timeoutMs / 1000),
|
|
421
|
+
_note: `Polled for ${minutes} minute(s) — generation is taking longer than usual. STOP calling get_generation_status now. Tell the user the generation is still running and ask them to prompt you to check again later. Do NOT loop.`,
|
|
422
|
+
}, null, 2),
|
|
423
|
+
}],
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
throw err;
|
|
427
|
+
}
|
|
393
428
|
return {
|
|
394
429
|
content: [{
|
|
395
430
|
type: 'text',
|
|
@@ -420,9 +455,10 @@ function registerGenerateTools(server, client) {
|
|
|
420
455
|
preset_id: z.string().optional().describe('Preset ID from list_presets type="video" (optional)'),
|
|
421
456
|
enhance_prompt: z.boolean().optional().describe('Enhance the prompt. Default: true'),
|
|
422
457
|
visual_dna_ids: z.array(z.string()).optional().describe('Array of Visual DNA profile IDs to apply for character/style consistency across outputs. **Cap: pass at most `max_visual_dna` IDs from list_models for the chosen model.**'),
|
|
423
|
-
resolution: z.string().optional().describe('Video resolution tier (vertical pixels): "720p" / "1080p" / "1440p" / "2160p". Model-dependent — call list_models and read supported_resolutions.')
|
|
458
|
+
resolution: z.string().optional().describe('Video resolution tier (vertical pixels): "720p" / "1080p" / "1440p" / "2160p". Model-dependent — call list_models and read supported_resolutions.'),
|
|
459
|
+
sound_enabled: z.boolean().optional().describe('Enable (`true`) or disable (`false`) AI-generated synced audio on the output. Only honored by models with `sound_generation_type: "native"` from list_models (e.g. Kling O3 4K, Kling O3 via KIE). On other models the flag has no effect. Omit to use the model\'s `sound_enabled_by_default`. Pass `false` when the user says no sound / silent / mute / without audio. Enabling sound may apply `sound_credit_multiplier` to cost.')
|
|
424
460
|
},
|
|
425
|
-
async ({ prompt, model, reference_images, reference_videos, audio_url, files, duration, aspect_ratio, motion, preset_id, enhance_prompt, visual_dna_ids, resolution }) => {
|
|
461
|
+
async ({ prompt, model, reference_images, reference_videos, audio_url, files, duration, aspect_ratio, motion, preset_id, enhance_prompt, visual_dna_ids, resolution, sound_enabled }) => {
|
|
426
462
|
if (!prompt) throw new Error('prompt is required');
|
|
427
463
|
|
|
428
464
|
let startResponse;
|
|
@@ -442,6 +478,7 @@ function registerGenerateTools(server, client) {
|
|
|
442
478
|
if (reference_videos) form.append('reference_videos', JSON.stringify(reference_videos));
|
|
443
479
|
if (audio_url) form.append('audio_url', audio_url);
|
|
444
480
|
if (resolution) form.append('resolution', resolution);
|
|
481
|
+
if (sound_enabled !== undefined) form.append('sound_enabled', String(sound_enabled));
|
|
445
482
|
for (const f of resolved) {
|
|
446
483
|
form.append('files', f.buffer, { filename: f.filename, contentType: f.contentType });
|
|
447
484
|
}
|
|
@@ -449,7 +486,7 @@ function registerGenerateTools(server, client) {
|
|
|
449
486
|
} else {
|
|
450
487
|
// URL-only mode: plain JSON.
|
|
451
488
|
startResponse = await client.post('/v1/generate/elements', {
|
|
452
|
-
prompt, model, reference_images, reference_videos, audio_url, duration, aspect_ratio, motion, preset_id, enhance_prompt, visual_dna_ids, resolution
|
|
489
|
+
prompt, model, reference_images, reference_videos, audio_url, duration, aspect_ratio, motion, preset_id, enhance_prompt, visual_dna_ids, resolution, sound_enabled
|
|
453
490
|
});
|
|
454
491
|
}
|
|
455
492
|
|
|
@@ -488,9 +525,10 @@ function registerGenerateTools(server, client) {
|
|
|
488
525
|
aspect_ratio: z.string().optional().describe('Aspect ratio (auto-detected from first frame if not provided). Must be in `supported_aspect_ratios` from list_models when set. Default: "16:9"'),
|
|
489
526
|
enhance_prompt: z.boolean().optional().describe('Enhance the prompt. Default: true'),
|
|
490
527
|
visual_dna_ids: z.array(z.string()).optional().describe('Array of Visual DNA profile IDs to apply. **Cap: pass at most `max_visual_dna` IDs from list_models for the chosen model; if `supports_visual_dna: false`, DNA is silently ignored.**'),
|
|
491
|
-
resolution: z.string().optional().describe('Video resolution tier (vertical pixels): "720p" / "1080p" / "1440p" / "2160p". Model-dependent — call list_models and read supported_resolutions.')
|
|
528
|
+
resolution: z.string().optional().describe('Video resolution tier (vertical pixels): "720p" / "1080p" / "1440p" / "2160p". Model-dependent — call list_models and read supported_resolutions.'),
|
|
529
|
+
sound_enabled: z.boolean().optional().describe('Enable (`true`) or disable (`false`) AI-generated synced audio on the output video. Only honored by models with `sound_generation_type: "native"` from list_models (e.g. Veo 3.1 Lite, Kling V3 4K, PixVerse V6). On other models the flag has no effect. Omit to use the model\'s `sound_enabled_by_default`. Pass `false` when the user says no sound / silent / mute / without audio.')
|
|
492
530
|
},
|
|
493
|
-
async ({ first_frame_url, last_frame_url, first_frame, last_frame, prompt, model, duration, aspect_ratio, enhance_prompt, visual_dna_ids, resolution }) => {
|
|
531
|
+
async ({ first_frame_url, last_frame_url, first_frame, last_frame, prompt, model, duration, aspect_ratio, enhance_prompt, visual_dna_ids, resolution, sound_enabled }) => {
|
|
494
532
|
const urlMode = first_frame_url && last_frame_url;
|
|
495
533
|
const fileMode = first_frame && last_frame;
|
|
496
534
|
if (!urlMode && !fileMode) {
|
|
@@ -516,10 +554,11 @@ function registerGenerateTools(server, client) {
|
|
|
516
554
|
if (enhance_prompt !== undefined) form.append('enhance_prompt', String(enhance_prompt));
|
|
517
555
|
if (visual_dna_ids) form.append('visual_dna_ids', JSON.stringify(visual_dna_ids));
|
|
518
556
|
if (resolution) form.append('resolution', resolution);
|
|
557
|
+
if (sound_enabled !== undefined) form.append('sound_enabled', String(sound_enabled));
|
|
519
558
|
startResponse = await client.postMultipart('/v1/generate/first-last-frame', form);
|
|
520
559
|
} else {
|
|
521
560
|
startResponse = await client.post('/v1/generate/first-last-frame', {
|
|
522
|
-
first_frame_url, last_frame_url, prompt, model, duration, aspect_ratio, enhance_prompt, visual_dna_ids, resolution
|
|
561
|
+
first_frame_url, last_frame_url, prompt, model, duration, aspect_ratio, enhance_prompt, visual_dna_ids, resolution, sound_enabled
|
|
523
562
|
});
|
|
524
563
|
}
|
|
525
564
|
|
|
@@ -629,9 +668,10 @@ function registerGenerateTools(server, client) {
|
|
|
629
668
|
resolution: z.string().optional().describe('Video resolution tier (vertical pixels): "720p" / "1080p" / "1440p" / "2160p". Model-dependent — call list_models and read supported_resolutions.'),
|
|
630
669
|
reference_images: z.array(z.string()).optional().describe('Array of reference image URLs for models that support additional image inputs. **Cap: pass at most `max_images` URLs from list_models — if `max_images === 0` the model does not accept image refs.** Examples: character reference images for Kling O1/O3, style reference for Aleph/gen4_aleph, character image for WAN VACE video-edit.'),
|
|
631
670
|
reference_videos: z.array(z.string()).optional().describe('Array of additional reference video URLs for models that support multiple video inputs. **Cap: pass at most `max_videos` URLs from list_models — if `max_videos <= 1` only the source_video is accepted.** Example: WAN 2.6 reference-to-video accepts 1–3 reference videos.'),
|
|
632
|
-
elements: z.array(z.string()).optional().describe('Array of element image URLs. **Cap: pass at most `max_elements` URLs from list_models — if `max_elements === 0` the model does not accept elements.** Elements are style or character reference assets alongside the main video.')
|
|
671
|
+
elements: z.array(z.string()).optional().describe('Array of element image URLs. **Cap: pass at most `max_elements` URLs from list_models — if `max_elements === 0` the model does not accept elements.** Elements are style or character reference assets alongside the main video.'),
|
|
672
|
+
sound_enabled: z.boolean().optional().describe('Enable (`true`) or disable (`false`) AI-generated synced audio on the output video. Only honored by models with `sound_generation_type: "native"` from list_models (e.g. Kling v3 via KIE). On other models the flag has no effect. Omit to use the model\'s `sound_enabled_by_default`. Pass `false` when the user says no sound / silent / mute / without audio.')
|
|
633
673
|
},
|
|
634
|
-
async ({ source_video, prompt, model, aspect_ratio, duration, enhance_prompt, visual_dna_ids, resolution, reference_images, reference_videos, elements }) => {
|
|
674
|
+
async ({ source_video, prompt, model, aspect_ratio, duration, enhance_prompt, visual_dna_ids, resolution, reference_images, reference_videos, elements, sound_enabled }) => {
|
|
635
675
|
if (!source_video) throw new Error('source_video is required');
|
|
636
676
|
if (!prompt) throw new Error('prompt is required');
|
|
637
677
|
|
|
@@ -640,7 +680,7 @@ function registerGenerateTools(server, client) {
|
|
|
640
680
|
if (isUrl) {
|
|
641
681
|
startResponse = await client.post('/v1/generate/video-from-video', {
|
|
642
682
|
video_url: source_video, prompt, model, aspect_ratio, duration, enhance_prompt, visual_dna_ids, resolution,
|
|
643
|
-
reference_images, reference_videos, elements
|
|
683
|
+
reference_images, reference_videos, elements, sound_enabled
|
|
644
684
|
});
|
|
645
685
|
} else {
|
|
646
686
|
const resolved = await resolveToBuffer(source_video, 'video');
|
|
@@ -656,6 +696,7 @@ function registerGenerateTools(server, client) {
|
|
|
656
696
|
if (reference_images) form.append('reference_images', JSON.stringify(reference_images));
|
|
657
697
|
if (reference_videos) form.append('reference_videos', JSON.stringify(reference_videos));
|
|
658
698
|
if (elements) form.append('elements', JSON.stringify(elements));
|
|
699
|
+
if (sound_enabled !== undefined) form.append('sound_enabled', String(sound_enabled));
|
|
659
700
|
startResponse = await client.postMultipart('/v1/generate/video-from-video', form);
|
|
660
701
|
}
|
|
661
702
|
|