@kolbo/mcp 1.82.2 → 1.82.5

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.82.2",
3
+ "version": "1.82.5",
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": {
@@ -280,8 +280,12 @@ function renderChips(sc) {
280
280
  var s = sc.settings || {};
281
281
  var kind = displayKind(sc);
282
282
  if (kind) h += chip(iconFor(kind) + ' ' + kind);
283
- if (s.duration) h += chip(ICONS.clock + ' ' + fmtDur(s.duration) + (s.shots > 1 ? ' · ' + s.shots + ' shots' : ''));
284
- else if (s.shots > 1) h += chip(s.shots + ' shots');
283
+ // Shot count when we know it, otherwise just say multishot is on the
284
+ // provider picks the count in that case, and showing nothing made an
285
+ // enabled toggle look like it had not applied.
286
+ var shotLabel = s.shots > 1 ? (s.shots + ' shots') : (s.multi_shot ? 'multishot' : '');
287
+ if (s.duration) h += chip(ICONS.clock + ' ' + fmtDur(s.duration) + (shotLabel ? ' · ' + shotLabel : ''));
288
+ else if (shotLabel) h += chip(shotLabel);
285
289
  if (s.resolution) h += chip(esc(s.resolution));
286
290
  if (s.aspect_ratio) h += chip(esc(s.aspect_ratio));
287
291
  if (s.quality) h += chip(esc(s.quality) + ' quality');
@@ -790,7 +794,7 @@ function fillBatchCell(sc, i, g) {
790
794
  // the user is meant to watch, and without it the tile was a muted poster with
791
795
  // no way to play, seek or unmute until the whole batch finished and repainted.
792
796
  cell.innerHTML = (sc.kind === 'video'
793
- ? '<video class="k-cell-fill" src="' + esc(u) + '"' + (r.thumbnail_url ? ' poster="' + esc(r.thumbnail_url) + '"' : '') + ' controls playsinline preload="metadata"></video>'
797
+ ? '<video class="k-cell-fill" src="' + esc(playableAt(r, r.urls || [], 0)) + '"' + (r.thumbnail_url ? ' poster="' + esc(r.thumbnail_url) + '"' : '') + ' controls playsinline preload="metadata"></video>'
794
798
  : '<img class="k-cell-fill" src="' + esc(u) + '" alt="">') + cap;
795
799
  window.kolbo.notifySize();
796
800
  }
@@ -858,10 +862,28 @@ function renderImages(sc, urls) {
858
862
  });
859
863
  }
860
864
 
865
+ /**
866
+ * The url to PLAY, which is not always the url to DOWNLOAD.
867
+ *
868
+ * Seedance 2.5 at 1080p returns HEVC Main 10 (10-bit). Chrome and Edge decode HEVC in
869
+ * hardware only — with no software fallback — and Firefox on Windows not at all, so the
870
+ * widget showed a black frame with sound. The API therefore returns playback_urls
871
+ * alongside urls whenever it had to build an H.264 proxy.
872
+ *
873
+ * urls stays the MASTER and is what every download button must keep using: handing
874
+ * someone the 8-bit proxy for a file they paid the 1080p multiplier for is a silent
875
+ * quality downgrade. Backend: kolbo-api/src/services/webPlaybackProxy.js
876
+ */
877
+ function playableAt(src, urls, i) {
878
+ var proxies = (src && src.playback_urls) || [];
879
+ return proxies[i] || (urls || [])[i];
880
+ }
881
+
861
882
  function renderVideo(sc, urls) {
862
883
  // preload="none" behind a poster: the card shows the still until the user
863
884
  // hits play, instead of pulling the video header on mount.
864
- el('stage').innerHTML = '<div class="k-viewer"><video id="main-video" src="' + esc(urls[0]) + '"' +
885
+ // PLAY the proxy when there is one; the download button below keeps urls[0] (master).
886
+ el('stage').innerHTML = '<div class="k-viewer"><video id="main-video" src="' + esc(playableAt(sc, urls, 0)) + '"' +
865
887
  (sc.thumbnail_url ? ' poster="' + esc(sc.thumbnail_url) + '" preload="none"' : ' preload="metadata"') + ' controls playsinline></video>' +
866
888
  dlBtnHTML(urls[0]) + '</div>';
867
889
  wireDlButtons(el('stage'));
@@ -89,7 +89,7 @@ async function pollBatch(client, batch, { interval, timeout }, toolName) {
89
89
  const polls = await Promise.all(batch.ids.map((id) => pollOrTimedOut(client, id, { interval, timeout })));
90
90
  const generations = polls.map((p, i) => p.timedOut
91
91
  ? { prompt: batch.ok[i].prompt, generation_id: batch.ids[i], status: 'processing', note: 'Still running — call get_generation_status with wait=true to collect it.' }
92
- : { prompt: batch.ok[i].prompt, generation_id: batch.ids[i], status: 'completed', ...creditFields(polls[i].result), urls: p.result.result.urls });
92
+ : { prompt: batch.ok[i].prompt, generation_id: batch.ids[i], status: 'completed', ...creditFields(polls[i].result), urls: p.result.result.urls, playback_urls: p.result.result.playback_urls });
93
93
  const text = JSON.stringify({
94
94
  batch: true,
95
95
  session_id: batch.ok[0].gen.session_id,
@@ -186,6 +186,9 @@ const videoSettings = (a = {}) => ({
186
186
  // echoed back on start, then to per-shot × shots.
187
187
  duration: a.duration ?? a.reported_duration ?? (a.shot_duration && a.shots ? a.shot_duration * a.shots : undefined),
188
188
  ...(a.shots > 1 ? { shots: a.shots } : {}),
189
+ // Multishot with no explicit count: the provider decides how many, so there
190
+ // is no number to show — but the card still has to say the toggle applied.
191
+ ...(a.multi_shot ? { multi_shot: true } : {}),
189
192
  resolution: a.resolution,
190
193
  aspect_ratio: a.aspect_ratio,
191
194
  ...(a.mode ? { mode: a.mode } : {}),
@@ -287,11 +290,13 @@ function registerGenerateTools(server, client, options = {}) {
287
290
  // input arg is only what the caller happened to name.
288
291
  reference_images: result.result.reference_images || reference_images,
289
292
  urls: result.result.urls,
293
+ playback_urls: result.result.playback_urls,
290
294
  credits_used: creditFields(result).credits_used,
291
295
  }, JSON.stringify({
292
296
  ...creditFields(result),
293
297
  session_id: gen.session_id,
294
298
  urls: result.result.urls,
299
+ playback_urls: result.result.playback_urls,
295
300
  model: result.result.model,
296
301
  prompt_used: result.result.prompt_used,
297
302
  _followup_hint: 'If the user asks to edit/change/modify this image next (scene, lighting, objects, style, color — any content edit), pass urls[0] to generate_image_edit. Use edit_image ONLY for mechanical ops (upscale/reframe/removebg/enhance_skin). Do NOT call generate_image again.'
@@ -377,11 +382,13 @@ function registerGenerateTools(server, client, options = {}) {
377
382
  reference_images: result.result.reference_images
378
383
  || [...(source_images || []), ...(reference_images || [])],
379
384
  urls: result.result.urls,
385
+ playback_urls: result.result.playback_urls,
380
386
  credits_used: creditFields(result).credits_used,
381
387
  }, JSON.stringify({
382
388
  ...creditFields(result),
383
389
  session_id: gen.session_id,
384
390
  urls: result.result.urls,
391
+ playback_urls: result.result.playback_urls,
385
392
  model: result.result.model,
386
393
  prompt_used: result.result.prompt_used,
387
394
  _followup_hint: 'If the user asks for another edit on this output, pass urls[0] back into generate_image_edit as source_images. For targeted ops (upscale/reframe/removebg/enhance_skin) use edit_image instead. Do NOT call generate_image from scratch.'
@@ -624,6 +631,7 @@ function registerGenerateTools(server, client, options = {}) {
624
631
  settings: videoSettings(shared),
625
632
  reference_images,
626
633
  urls: result.result.urls,
634
+ playback_urls: result.result.playback_urls,
627
635
  thumbnail_url: result.result.thumbnail_url,
628
636
  duration: result.result.duration,
629
637
  credits_used: creditFields(result).credits_used,
@@ -631,6 +639,7 @@ function registerGenerateTools(server, client, options = {}) {
631
639
  ...creditFields(result),
632
640
  session_id: gen.session_id,
633
641
  urls: result.result.urls,
642
+ playback_urls: result.result.playback_urls,
634
643
  model: result.result.model,
635
644
  duration: result.result.duration,
636
645
  thumbnail_url: result.result.thumbnail_url,
@@ -711,6 +720,7 @@ function registerGenerateTools(server, client, options = {}) {
711
720
  settings: videoSettings({ duration, resolution, aspect_ratio, enhance_prompt, visual_dna_ids }),
712
721
  reference_images: [image_url],
713
722
  urls: result.result.urls,
723
+ playback_urls: result.result.playback_urls,
714
724
  thumbnail_url: result.result.thumbnail_url,
715
725
  duration: result.result.duration,
716
726
  credits_used: creditFields(result).credits_used,
@@ -718,6 +728,7 @@ function registerGenerateTools(server, client, options = {}) {
718
728
  ...creditFields(result),
719
729
  session_id: gen.session_id,
720
730
  urls: result.result.urls,
731
+ playback_urls: result.result.playback_urls,
721
732
  model: result.result.model,
722
733
  duration: result.result.duration,
723
734
  thumbnail_url: result.result.thumbnail_url,
@@ -778,6 +789,7 @@ function registerGenerateTools(server, client, options = {}) {
778
789
  tool: 'generate_music', kind: 'audio', gen, client, model: model || 'Suno', prompt,
779
790
  settings: { mode: instrumental ? 'instrumental' : (style || undefined), preset_id },
780
791
  urls: result.result.urls,
792
+ playback_urls: result.result.playback_urls,
781
793
  title: result.result.title,
782
794
  duration: result.result.duration,
783
795
  credits_used: creditFields(result).credits_used,
@@ -785,6 +797,7 @@ function registerGenerateTools(server, client, options = {}) {
785
797
  ...creditFields(result),
786
798
  session_id: gen.session_id,
787
799
  urls: result.result.urls,
800
+ playback_urls: result.result.playback_urls,
788
801
  title: result.result.title,
789
802
  duration: result.result.duration,
790
803
  lyrics: result.result.lyrics
@@ -896,12 +909,14 @@ function registerGenerateTools(server, client, options = {}) {
896
909
  language,
897
910
  },
898
911
  urls: result.result.urls,
912
+ playback_urls: result.result.playback_urls,
899
913
  duration: result.result.duration,
900
914
  credits_used: creditFields(result).credits_used,
901
915
  }, JSON.stringify({
902
916
  ...creditFields(result),
903
917
  session_id: gen.session_id,
904
918
  urls: result.result.urls,
919
+ playback_urls: result.result.playback_urls,
905
920
  voice: result.result.voice,
906
921
  duration: result.result.duration,
907
922
  ...(unknownVoice ? { _warning: unknownVoice } : {})
@@ -961,12 +976,14 @@ function registerGenerateTools(server, client, options = {}) {
961
976
  settings: { duration },
962
977
  reference_images: seed_reference_image_url ? [seed_reference_image_url] : [],
963
978
  urls: result.result.urls,
979
+ playback_urls: result.result.playback_urls,
964
980
  duration: result.result.duration,
965
981
  credits_used: creditFields(result).credits_used,
966
982
  }, JSON.stringify({
967
983
  ...creditFields(result),
968
984
  session_id: gen.session_id,
969
985
  urls: result.result.urls,
986
+ playback_urls: result.result.playback_urls,
970
987
  duration: result.result.duration
971
988
  }, null, 2));
972
989
  }
@@ -1374,7 +1391,14 @@ function registerGenerateTools(server, client, options = {}) {
1374
1391
  settings: videoSettings({
1375
1392
  duration,
1376
1393
  reported_duration: startResponse?.duration ?? startResponse?.result?.duration,
1377
- shots: multi_shot_count ?? (Array.isArray(multi_shots) ? multi_shots.length : undefined),
1394
+ // `multi_shots` is a boolean (z.boolean()); the old
1395
+ // `Array.isArray(multi_shots) ? multi_shots.length : undefined`
1396
+ // branch could never be true, so enabling multishot WITHOUT an
1397
+ // explicit count left shots undefined and the card silently showed
1398
+ // no multishot indicator at all. Carry the flag so the chip can
1399
+ // render even when the shot count is the provider's to decide.
1400
+ shots: multi_shot_count,
1401
+ multi_shot: multi_shots === true || undefined,
1378
1402
  resolution, aspect_ratio, enhance_prompt, visual_dna_ids, preset_id,
1379
1403
  }),
1380
1404
  // Already bucketed and deduped above, so a URL handed to `files`
@@ -1396,7 +1420,14 @@ function registerGenerateTools(server, client, options = {}) {
1396
1420
  settings: videoSettings({
1397
1421
  duration,
1398
1422
  reported_duration: result.result?.duration,
1399
- shots: multi_shot_count ?? (Array.isArray(multi_shots) ? multi_shots.length : undefined),
1423
+ // `multi_shots` is a boolean (z.boolean()); the old
1424
+ // `Array.isArray(multi_shots) ? multi_shots.length : undefined`
1425
+ // branch could never be true, so enabling multishot WITHOUT an
1426
+ // explicit count left shots undefined and the card silently showed
1427
+ // no multishot indicator at all. Carry the flag so the chip can
1428
+ // render even when the shot count is the provider's to decide.
1429
+ shots: multi_shot_count,
1430
+ multi_shot: multi_shots === true || undefined,
1400
1431
  resolution, aspect_ratio, enhance_prompt, visual_dna_ids, preset_id,
1401
1432
  }),
1402
1433
  reference_images: [...urlsOf('image'), ...(keyframes || []).map((keyframe) => keyframe.image_url)],
@@ -102,7 +102,7 @@ function registerVisualDnaTools(server, client, options = {}) {
102
102
  {
103
103
  scope: z.enum(['all', 'personal', 'global', 'organization']).optional().describe('Default: "personal" — the user\'s own DNAs (plus a shared project\'s when project_id is set). "global" = the ~1000 system cast/preset DNAs, for browsing when the user needs a character and has none of their own. "organization" = org-shared. "all" = everything, rarely wanted.'),
104
104
  search: z.string().optional().describe('Search by name, tags, or description (case-insensitive). Matches at WORD STARTS, so "man" finds "Man"/"Manager" but not "woman" or "romantic". Name matches are ranked first.'),
105
- collection: z.string().optional().describe('Filter global presets by collection: cast, influencers, props, locations, styles, glamour, street'),
105
+ collection: z.string().optional().describe('Filter global presets by collection: cast, influencers, props, locations, styles, street'),
106
106
  tags: z.string().optional().describe('Comma-separated tags to filter by (OR logic)'),
107
107
  page: z.number().optional().describe('Page number, 1-indexed. Default: 1. Needed to reach the global cast beyond the first page.'),
108
108
  limit: z.number().optional().describe('Results per page, max 100. Default: 50'),