@kolbo/mcp 1.14.1 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kolbo/mcp",
3
- "version": "1.14.1",
3
+ "version": "1.15.0",
4
4
  "description": "Kolbo AI MCP Server - Generate images, videos, music, speech, and sound effects from Claude Code",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -74,13 +74,16 @@ function registerGenerateTools(server, client) {
74
74
  visual_dna_ids, moodboard_id, enable_web_search, resolution
75
75
  });
76
76
 
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.
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 ? 240000 : 120000
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,17 @@ function registerGenerateTools(server, client) {
383
389
  // ─── get_generation_status ─────────────────────────────────
384
390
  server.tool(
385
391
  '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.',
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
394
  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.'),
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.'),
390
396
  },
391
397
  async ({ generation_id, wait_seconds }) => {
392
- const timeoutMs = (wait_seconds ?? 600) * 1000;
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;
393
403
  let result;
394
404
  try {
395
405
  result = await pollUntilDone(client, generation_id, {
@@ -445,9 +455,10 @@ function registerGenerateTools(server, client) {
445
455
  preset_id: z.string().optional().describe('Preset ID from list_presets type="video" (optional)'),
446
456
  enhance_prompt: z.boolean().optional().describe('Enhance the prompt. Default: true'),
447
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.**'),
448
- 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.')
449
460
  },
450
- 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 }) => {
451
462
  if (!prompt) throw new Error('prompt is required');
452
463
 
453
464
  let startResponse;
@@ -467,6 +478,7 @@ function registerGenerateTools(server, client) {
467
478
  if (reference_videos) form.append('reference_videos', JSON.stringify(reference_videos));
468
479
  if (audio_url) form.append('audio_url', audio_url);
469
480
  if (resolution) form.append('resolution', resolution);
481
+ if (sound_enabled !== undefined) form.append('sound_enabled', String(sound_enabled));
470
482
  for (const f of resolved) {
471
483
  form.append('files', f.buffer, { filename: f.filename, contentType: f.contentType });
472
484
  }
@@ -474,7 +486,7 @@ function registerGenerateTools(server, client) {
474
486
  } else {
475
487
  // URL-only mode: plain JSON.
476
488
  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
489
+ prompt, model, reference_images, reference_videos, audio_url, duration, aspect_ratio, motion, preset_id, enhance_prompt, visual_dna_ids, resolution, sound_enabled
478
490
  });
479
491
  }
480
492
 
@@ -513,9 +525,10 @@ function registerGenerateTools(server, client) {
513
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"'),
514
526
  enhance_prompt: z.boolean().optional().describe('Enhance the prompt. Default: true'),
515
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.**'),
516
- 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.')
517
530
  },
518
- 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 }) => {
519
532
  const urlMode = first_frame_url && last_frame_url;
520
533
  const fileMode = first_frame && last_frame;
521
534
  if (!urlMode && !fileMode) {
@@ -541,10 +554,11 @@ function registerGenerateTools(server, client) {
541
554
  if (enhance_prompt !== undefined) form.append('enhance_prompt', String(enhance_prompt));
542
555
  if (visual_dna_ids) form.append('visual_dna_ids', JSON.stringify(visual_dna_ids));
543
556
  if (resolution) form.append('resolution', resolution);
557
+ if (sound_enabled !== undefined) form.append('sound_enabled', String(sound_enabled));
544
558
  startResponse = await client.postMultipart('/v1/generate/first-last-frame', form);
545
559
  } else {
546
560
  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
561
+ first_frame_url, last_frame_url, prompt, model, duration, aspect_ratio, enhance_prompt, visual_dna_ids, resolution, sound_enabled
548
562
  });
549
563
  }
550
564
 
@@ -654,9 +668,10 @@ function registerGenerateTools(server, client) {
654
668
  resolution: z.string().optional().describe('Video resolution tier (vertical pixels): "720p" / "1080p" / "1440p" / "2160p". Model-dependent — call list_models and read supported_resolutions.'),
655
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.'),
656
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.'),
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.')
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.')
658
673
  },
659
- 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 }) => {
660
675
  if (!source_video) throw new Error('source_video is required');
661
676
  if (!prompt) throw new Error('prompt is required');
662
677
 
@@ -665,7 +680,7 @@ function registerGenerateTools(server, client) {
665
680
  if (isUrl) {
666
681
  startResponse = await client.post('/v1/generate/video-from-video', {
667
682
  video_url: source_video, prompt, model, aspect_ratio, duration, enhance_prompt, visual_dna_ids, resolution,
668
- reference_images, reference_videos, elements
683
+ reference_images, reference_videos, elements, sound_enabled
669
684
  });
670
685
  } else {
671
686
  const resolved = await resolveToBuffer(source_video, 'video');
@@ -681,6 +696,7 @@ function registerGenerateTools(server, client) {
681
696
  if (reference_images) form.append('reference_images', JSON.stringify(reference_images));
682
697
  if (reference_videos) form.append('reference_videos', JSON.stringify(reference_videos));
683
698
  if (elements) form.append('elements', JSON.stringify(elements));
699
+ if (sound_enabled !== undefined) form.append('sound_enabled', String(sound_enabled));
684
700
  startResponse = await client.postMultipart('/v1/generate/video-from-video', form);
685
701
  }
686
702