@kolbo/mcp 1.14.1 → 1.15.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/package.json CHANGED
@@ -1,56 +1,56 @@
1
- {
2
- "name": "@kolbo/mcp",
3
- "version": "1.14.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-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
- }
1
+ {
2
+ "name": "@kolbo/mcp",
3
+ "version": "1.15.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-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/polling.js CHANGED
@@ -6,9 +6,10 @@ class PollingTimeoutError extends Error {
6
6
  constructor(generationId, timeoutMs) {
7
7
  const seconds = Math.round(timeoutMs / 1000);
8
8
  super(
9
- `Generation timed out after ${seconds}s of polling. The generation may STILL be running on the server — ` +
10
- `call get_generation_status with generation_id="${generationId}" to check its current state. ` +
11
- `Videos, deep-think chat, and large batches can take longer than the default polling window.`
9
+ `Generation timed out after ${seconds}s of polling. The generation is STILL RUNNING on the server — ` +
10
+ `DO NOT re-issue the original generation tool. A retry would burn credits while the original keeps producing the result you already paid for. ` +
11
+ `Instead, IMMEDIATELY call get_generation_status with generation_id="${generationId}" (it block-polls server-side, no need to loop). ` +
12
+ `Videos, deep-think chat, and large batches can legitimately take 5–15+ minutes beyond the default polling window.`
12
13
  );
13
14
  this.name = 'PollingTimeoutError';
14
15
  this.generationId = generationId;
@@ -32,9 +32,14 @@ function registerGenerateTools(server, client) {
32
32
  reference_images, visual_dna_ids, moodboard_id, enable_web_search, resolution, preset_id
33
33
  });
34
34
 
35
+ // 60s here (not 120s) so a stuck generation surfaces faster — the
36
+ // PollingTimeoutError tells the model to switch to get_generation_status,
37
+ // which block-polls for 10min by default. Same total wait, but the
38
+ // first hand-off happens at 60s instead of 120s so the user isn't
39
+ // staring at a frozen spinner.
35
40
  const result = await pollUntilDone(client, gen.generation_id, {
36
41
  interval: (gen.poll_interval_hint || 3) * 1000,
37
- timeout: 120000
42
+ timeout: 60000
38
43
  });
39
44
 
40
45
  return {
@@ -74,13 +79,16 @@ function registerGenerateTools(server, client) {
74
79
  visual_dna_ids, moodboard_id, enable_web_search, resolution
75
80
  });
76
81
 
77
- // Multi-source compositing or DNA-anchored edits routinely exceed 120s
78
- // server-side. Extend the polling window in those cases to avoid forcing
79
- // every call into the timeout-and-recover path via get_generation_status.
82
+ // Flux/Seedance/DNA-anchored edits routinely run 3-5 min. Old 120s
83
+ // timeout forced every call into the timeout-and-recover path via
84
+ // get_generation_status and when the model fired multiple parallel
85
+ // edits, some recovered, some got abandoned with their URLs lost
86
+ // (the user's "lost generations" bug). 360s/480s covers the realistic
87
+ // p99 single-call duration without making the model poll forever.
80
88
  const heavy = (source_images && source_images.length > 1) || (visual_dna_ids && visual_dna_ids.length > 0);
81
89
  const result = await pollUntilDone(client, gen.generation_id, {
82
90
  interval: (gen.poll_interval_hint || 3) * 1000,
83
- timeout: heavy ? 240000 : 120000
91
+ timeout: heavy ? 480000 : 360000
84
92
  });
85
93
 
86
94
  return {
@@ -109,6 +117,7 @@ function registerGenerateTools(server, client) {
109
117
  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
118
  workflow_type: z.string().optional().describe('"image" (default) or "video"'),
111
119
  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.'),
120
+ 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
121
  enhance_prompt: z.boolean().optional().describe('Enhance prompts per scene. Default: true'),
113
122
  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
123
  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 +125,9 @@ function registerGenerateTools(server, client) {
116
125
  moodboard_ids: z.array(z.string()).optional().describe('Multiple moodboard IDs when blending styles. Prefer `moodboard_id` for single moodboards.'),
117
126
  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
127
  },
119
- async ({ prompt, scene_count, model, aspect_ratio, workflow_type, duration, enhance_prompt, reference_images, visual_dna_ids, moodboard_id, moodboard_ids, resolution }) => {
128
+ 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
129
  const gen = await client.post('/v1/generate/creative-director', {
121
- prompt, scene_count, model, aspect_ratio, workflow_type, duration,
130
+ prompt, scene_count, model, aspect_ratio, workflow_type, duration, sound_enabled,
122
131
  enhance_prompt, reference_images, visual_dna_ids, moodboard_id, moodboard_ids, resolution
123
132
  });
124
133
 
@@ -168,11 +177,12 @@ function registerGenerateTools(server, client) {
168
177
  enhance_prompt: z.boolean().optional().describe('Enhance the prompt. Default: true'),
169
178
  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
179
  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.'),
180
+ 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
181
  preset_id: z.string().optional().describe('Preset ID from list_presets type="video" to apply a saved motion/style preset to this generation.')
172
182
  },
173
- async ({ prompt, model, aspect_ratio, duration, enhance_prompt, reference_images, resolution, preset_id }) => {
183
+ async ({ prompt, model, aspect_ratio, duration, enhance_prompt, reference_images, resolution, sound_enabled, preset_id }) => {
174
184
  const gen = await client.post('/v1/generate/video', {
175
- prompt, model, aspect_ratio, duration, enhance_prompt, reference_images, resolution, preset_id
185
+ prompt, model, aspect_ratio, duration, enhance_prompt, reference_images, resolution, sound_enabled, preset_id
176
186
  });
177
187
 
178
188
  const result = await pollUntilDone(client, gen.generation_id, {
@@ -209,11 +219,12 @@ function registerGenerateTools(server, client) {
209
219
  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
220
  enhance_prompt: z.boolean().optional().describe('Enhance the motion prompt. Default: true'),
211
221
  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.')
222
+ 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.'),
223
+ 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
224
  },
214
- async ({ image_url, prompt, model, aspect_ratio, duration, enhance_prompt, visual_dna_ids, resolution }) => {
225
+ async ({ image_url, prompt, model, aspect_ratio, duration, enhance_prompt, visual_dna_ids, resolution, sound_enabled }) => {
215
226
  const gen = await client.post('/v1/generate/video/from-image', {
216
- image_url, prompt, model, aspect_ratio, duration, enhance_prompt, visual_dna_ids, resolution
227
+ image_url, prompt, model, aspect_ratio, duration, enhance_prompt, visual_dna_ids, resolution, sound_enabled
217
228
  });
218
229
 
219
230
  const result = await pollUntilDone(client, gen.generation_id, {
@@ -380,16 +391,80 @@ function registerGenerateTools(server, client) {
380
391
  }
381
392
  );
382
393
 
394
+ // Track how many consecutive get_generation_status polls land on the same
395
+ // id within a short window. Models routinely ignore the "stop polling"
396
+ // hint and re-call, burning minutes of wall-clock and confusing the user.
397
+ // After STATUS_POLL_CAP attempts in a row, we hard-abandon: return a
398
+ // distinct shape (no `still_pending`) so the model can't pattern-match
399
+ // its way back into the loop.
400
+ const STATUS_POLL_CAP = 2;
401
+ const STATUS_POLL_RESET_MS = 10 * 60 * 1000; // a quiet 10min resets the counter
402
+ const statusPollHistory = new Map(); // id → { count, lastAt }
403
+
383
404
  // ─── get_generation_status ─────────────────────────────────
384
405
  server.tool(
385
406
  'get_generation_status',
386
- '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 it returns with `still_pending: true`, the generation is genuinely slow STOP calling this tool, tell the user the generation is still running, and resume on the next user turn. Never call this tool more than ONCE per generation_id consecutively.',
407
+ '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.** A response with `abandoned: true` is TERMINAL — the generation will not be polled again in this turn; report it to the user and move on. A response with `still_pending: true` means the job is genuinely slow — STOP polling THAT id immediately, tell the user, and wait for them to ask again. Never call this tool more than ONCE per generation_id consecutively in the same turn.',
387
408
  {
388
409
  generation_id: z.string().describe('The generation ID to check'),
389
- wait_seconds: z.number().int().min(10).max(1700).optional().describe('How long to block-poll internally before giving up (10–1700 seconds, default 600). Use higher values for video / 3D / large batches that can legitimately take 15+ minutes.'),
410
+ 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.'),
390
411
  },
391
412
  async ({ generation_id, wait_seconds }) => {
392
- const timeoutMs = (wait_seconds ?? 600) * 1000;
413
+ // Clamp the floor: shorter waits caused models to abandon generations
414
+ // that completed seconds later (URLs lost forever, paid-for compute
415
+ // wasted). 300s is the floor that survived real-world cases.
416
+ const requested = wait_seconds ?? 600;
417
+ const timeoutMs = Math.max(requested, 300) * 1000;
418
+
419
+ // Update the consecutive-call counter for this id. We reset if the
420
+ // last attempt was a long time ago (new user turn), so this only
421
+ // catches within-turn looping. Opportunistic sweep keeps the map
422
+ // bounded on long-lived MCP servers (no setInterval — that would
423
+ // keep the Node event loop alive forever).
424
+ const now = Date.now();
425
+ if (statusPollHistory.size > 64) {
426
+ const cutoff = now - STATUS_POLL_RESET_MS;
427
+ for (const [k, v] of statusPollHistory) {
428
+ if (v.lastAt < cutoff) statusPollHistory.delete(k);
429
+ }
430
+ }
431
+ const prior = statusPollHistory.get(generation_id);
432
+ const count = prior && now - prior.lastAt < STATUS_POLL_RESET_MS ? prior.count + 1 : 1;
433
+ statusPollHistory.set(generation_id, { count, lastAt: now });
434
+
435
+ // Hard abandon — the model has hit the cap. Return a non-pending
436
+ // shape so it has nothing left to keep polling against.
437
+ if (count > STATUS_POLL_CAP) {
438
+ const current = await client
439
+ .get(`/v1/generate/${encodeURIComponent(generation_id)}/status`)
440
+ .catch(() => null);
441
+ // If the generation actually finished in the meantime, surface it.
442
+ if (current?.state === 'completed') {
443
+ statusPollHistory.delete(generation_id);
444
+ return {
445
+ content: [{ type: 'text', text: JSON.stringify(current, null, 2) }],
446
+ };
447
+ }
448
+ return {
449
+ content: [
450
+ {
451
+ type: 'text',
452
+ text: JSON.stringify(
453
+ {
454
+ generation_id,
455
+ state: current?.state ?? 'unknown',
456
+ abandoned: true,
457
+ poll_attempts: count,
458
+ _note: `This generation has been polled ${count} consecutive times in this turn and is still not done. ABANDONED — do NOT call get_generation_status for this id again. Tell the user the generation is taking unusually long and ask them to check back later.`,
459
+ },
460
+ null,
461
+ 2,
462
+ ),
463
+ },
464
+ ],
465
+ };
466
+ }
467
+
393
468
  let result;
394
469
  try {
395
470
  result = await pollUntilDone(client, generation_id, {
@@ -408,13 +483,17 @@ function registerGenerateTools(server, client) {
408
483
  state: current?.state ?? 'unknown',
409
484
  still_pending: true,
410
485
  waited_seconds: Math.round(timeoutMs / 1000),
411
- _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.`,
486
+ poll_attempts: count,
487
+ _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 — one more consecutive call for this id will be hard-abandoned.`,
412
488
  }, null, 2),
413
489
  }],
414
490
  };
415
491
  }
416
492
  throw err;
417
493
  }
494
+ // Completed cleanly — clear the counter so a future generation reusing
495
+ // the id (theoretically possible) starts fresh.
496
+ statusPollHistory.delete(generation_id);
418
497
  return {
419
498
  content: [{
420
499
  type: 'text',
@@ -445,9 +524,10 @@ function registerGenerateTools(server, client) {
445
524
  preset_id: z.string().optional().describe('Preset ID from list_presets type="video" (optional)'),
446
525
  enhance_prompt: z.boolean().optional().describe('Enhance the prompt. Default: true'),
447
526
  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.**'),
448
- resolution: z.string().optional().describe('Video resolution tier (vertical pixels): "720p" / "1080p" / "1440p" / "2160p". Model-dependent — call list_models and read supported_resolutions.')
527
+ resolution: z.string().optional().describe('Video resolution tier (vertical pixels): "720p" / "1080p" / "1440p" / "2160p". Model-dependent — call list_models and read supported_resolutions.'),
528
+ 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.')
449
529
  },
450
- async ({ prompt, model, reference_images, reference_videos, audio_url, files, duration, aspect_ratio, motion, preset_id, enhance_prompt, visual_dna_ids, resolution }) => {
530
+ async ({ prompt, model, reference_images, reference_videos, audio_url, files, duration, aspect_ratio, motion, preset_id, enhance_prompt, visual_dna_ids, resolution, sound_enabled }) => {
451
531
  if (!prompt) throw new Error('prompt is required');
452
532
 
453
533
  let startResponse;
@@ -467,6 +547,7 @@ function registerGenerateTools(server, client) {
467
547
  if (reference_videos) form.append('reference_videos', JSON.stringify(reference_videos));
468
548
  if (audio_url) form.append('audio_url', audio_url);
469
549
  if (resolution) form.append('resolution', resolution);
550
+ if (sound_enabled !== undefined) form.append('sound_enabled', String(sound_enabled));
470
551
  for (const f of resolved) {
471
552
  form.append('files', f.buffer, { filename: f.filename, contentType: f.contentType });
472
553
  }
@@ -474,7 +555,7 @@ function registerGenerateTools(server, client) {
474
555
  } else {
475
556
  // URL-only mode: plain JSON.
476
557
  startResponse = await client.post('/v1/generate/elements', {
477
- prompt, model, reference_images, reference_videos, audio_url, duration, aspect_ratio, motion, preset_id, enhance_prompt, visual_dna_ids, resolution
558
+ prompt, model, reference_images, reference_videos, audio_url, duration, aspect_ratio, motion, preset_id, enhance_prompt, visual_dna_ids, resolution, sound_enabled
478
559
  });
479
560
  }
480
561
 
@@ -513,9 +594,10 @@ function registerGenerateTools(server, client) {
513
594
  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"'),
514
595
  enhance_prompt: z.boolean().optional().describe('Enhance the prompt. Default: true'),
515
596
  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.**'),
516
- resolution: z.string().optional().describe('Video resolution tier (vertical pixels): "720p" / "1080p" / "1440p" / "2160p". Model-dependent — call list_models and read supported_resolutions.')
597
+ resolution: z.string().optional().describe('Video resolution tier (vertical pixels): "720p" / "1080p" / "1440p" / "2160p". Model-dependent — call list_models and read supported_resolutions.'),
598
+ 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.')
517
599
  },
518
- async ({ first_frame_url, last_frame_url, first_frame, last_frame, prompt, model, duration, aspect_ratio, enhance_prompt, visual_dna_ids, resolution }) => {
600
+ async ({ first_frame_url, last_frame_url, first_frame, last_frame, prompt, model, duration, aspect_ratio, enhance_prompt, visual_dna_ids, resolution, sound_enabled }) => {
519
601
  const urlMode = first_frame_url && last_frame_url;
520
602
  const fileMode = first_frame && last_frame;
521
603
  if (!urlMode && !fileMode) {
@@ -541,10 +623,11 @@ function registerGenerateTools(server, client) {
541
623
  if (enhance_prompt !== undefined) form.append('enhance_prompt', String(enhance_prompt));
542
624
  if (visual_dna_ids) form.append('visual_dna_ids', JSON.stringify(visual_dna_ids));
543
625
  if (resolution) form.append('resolution', resolution);
626
+ if (sound_enabled !== undefined) form.append('sound_enabled', String(sound_enabled));
544
627
  startResponse = await client.postMultipart('/v1/generate/first-last-frame', form);
545
628
  } else {
546
629
  startResponse = await client.post('/v1/generate/first-last-frame', {
547
- first_frame_url, last_frame_url, prompt, model, duration, aspect_ratio, enhance_prompt, visual_dna_ids, resolution
630
+ first_frame_url, last_frame_url, prompt, model, duration, aspect_ratio, enhance_prompt, visual_dna_ids, resolution, sound_enabled
548
631
  });
549
632
  }
550
633
 
@@ -654,9 +737,10 @@ function registerGenerateTools(server, client) {
654
737
  resolution: z.string().optional().describe('Video resolution tier (vertical pixels): "720p" / "1080p" / "1440p" / "2160p". Model-dependent — call list_models and read supported_resolutions.'),
655
738
  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.'),
656
739
  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.'),
657
- 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.')
740
+ 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.'),
741
+ 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.')
658
742
  },
659
- async ({ source_video, prompt, model, aspect_ratio, duration, enhance_prompt, visual_dna_ids, resolution, reference_images, reference_videos, elements }) => {
743
+ async ({ source_video, prompt, model, aspect_ratio, duration, enhance_prompt, visual_dna_ids, resolution, reference_images, reference_videos, elements, sound_enabled }) => {
660
744
  if (!source_video) throw new Error('source_video is required');
661
745
  if (!prompt) throw new Error('prompt is required');
662
746
 
@@ -665,7 +749,7 @@ function registerGenerateTools(server, client) {
665
749
  if (isUrl) {
666
750
  startResponse = await client.post('/v1/generate/video-from-video', {
667
751
  video_url: source_video, prompt, model, aspect_ratio, duration, enhance_prompt, visual_dna_ids, resolution,
668
- reference_images, reference_videos, elements
752
+ reference_images, reference_videos, elements, sound_enabled
669
753
  });
670
754
  } else {
671
755
  const resolved = await resolveToBuffer(source_video, 'video');
@@ -681,6 +765,7 @@ function registerGenerateTools(server, client) {
681
765
  if (reference_images) form.append('reference_images', JSON.stringify(reference_images));
682
766
  if (reference_videos) form.append('reference_videos', JSON.stringify(reference_videos));
683
767
  if (elements) form.append('elements', JSON.stringify(elements));
768
+ if (sound_enabled !== undefined) form.append('sound_enabled', String(sound_enabled));
684
769
  startResponse = await client.postMultipart('/v1/generate/video-from-video', form);
685
770
  }
686
771