@kolbo/mcp 1.87.5 → 1.87.6

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.87.5",
3
+ "version": "1.87.6",
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": {
package/src/apps/index.js CHANGED
@@ -71,7 +71,13 @@ const WIDGET_CSP = {
71
71
  // Public hosts owned by Kolbo.
72
72
  'https://api.kolbo.ai',
73
73
  'https://app.kolbo.ai',
74
- 'https://cdn.kolbo.ai',
74
+ 'https://cdn.kolbo.ai',
75
+ // Preset thumbnails and library media live on the media-* hosts (prod
76
+ // presets still reference media-dev); without these every preset tile
77
+ // rendered as a black box. All three are on kolbo-api's download allowlist.
78
+ 'https://media.kolbo.ai',
79
+ 'https://media-staging.kolbo.ai',
80
+ 'https://media-dev.kolbo.ai',
75
81
  ...KOLBO_MEDIA_DOMAINS,
76
82
  'https://kolbo-general-media.fra1.digitaloceanspaces.com',
77
83
  'https://kolbo-general-media.fra1.cdn.digitaloceanspaces.com',
@@ -195,6 +195,7 @@ function isListPayload(sc, toolName) {
195
195
  function boot(sc) {
196
196
  if (!sc) return;
197
197
  if (isListPayload(sc, sc.tool)) return renderList(sc);
198
+ sc = liveFromStatus(sc);
198
199
  state = sc;
199
200
  el('tool-title').textContent = TOOL_TITLES[sc.tool] || 'Generation';
200
201
  setPrompt(sc.prompt ? promptHTML(sc.prompt) : '', sc.prompt);
@@ -486,6 +487,51 @@ function capAt(sc, i) {
486
487
  return sc._caps[i] || sc.prompts[i];
487
488
  }
488
489
 
490
+ // A completed TOOL CALL is not a completed GENERATION. When a generate_* call
491
+ // outlives its poll window on a text host (Claude desktop over stdio), the
492
+ // server answers with a kind:'status' grid whose items are still processing,
493
+ // and that used to render as a dead card of "processing" badges that never
494
+ // polled again: the user watched skeletons that would never fill while the
495
+ // images landed in the library. Turn any such payload back into a LIVE card,
496
+ // the same generating/polling path a fresh submit takes, cells filled per id.
497
+ function isTerminal(s) { return s === 'completed' || s === 'failed' || s === 'cancelled'; }
498
+ function liveFromStatus(sc) {
499
+ if (!sc || sc.phase !== 'completed' || !Array.isArray(sc.items) || !sc.items.length) return sc;
500
+ var ids = sc.items.map(function (it) { return it && it.id; });
501
+ if (ids.some(function (id) { return !id; })) return sc;
502
+ if (!sc.items.some(function (it) { return !isTerminal(it.state); })) return sc;
503
+ var kind = sc.kind === 'status' || !sc.kind ? kindFromTool(sc.tool, {}) : sc.kind;
504
+ var live = Object.assign({}, sc, {
505
+ phase: 'generating', kind: kind, items: undefined,
506
+ count: ids.length, poll_tool: 'get_generation_status', generation_id: ids[0]
507
+ });
508
+ if (ids.length > 1) {
509
+ live.generation_ids = ids;
510
+ live.prompts = sc.items.map(function (it) { return it.title || ''; });
511
+ live.status_args = { generation_ids: ids, wait: true };
512
+ } else {
513
+ live.status_args = { generation_id: ids[0], wait: true };
514
+ }
515
+ return live;
516
+ }
517
+ // Multi-id status responses carry their per-generation results as items[] in
518
+ // structuredContent (the shape the status grid renders) and as generations[]
519
+ // only in the text, and structured() reads structuredContent first, so a live
520
+ // batch card never saw generations[] on a host that ships structuredContent.
521
+ function itemsToGens(items) {
522
+ if (!Array.isArray(items)) return null;
523
+ return items.map(function (it) {
524
+ return {
525
+ generation_id: it.id, state: it.state, credits_used: it.credits_used,
526
+ result: {
527
+ urls: Array.isArray(it.urls) ? it.urls : (it.url ? [it.url] : []),
528
+ model: it.model, model_name: it.model_name, model_icon: it.model_icon,
529
+ reference_images: it.reference_images
530
+ }
531
+ };
532
+ });
533
+ }
534
+
489
535
  function renderGenerating(sc) {
490
536
  setPhaseChip('Generating', true);
491
537
  var n = Math.min(sc.count || 1, isBatch(sc) ? 8 : 4);
@@ -670,9 +716,14 @@ function poll(sc) {
670
716
  if (++pollErrors >= MAX_POLL_ERRORS) return renderTrackingIssue(st.error || 'Tracking paused. The generation may still be running.');
671
717
  return schedulePoll(sc);
672
718
  }
673
- // Batch (prompts[] fan-out): multi-id status shape { all_done, generations[] }.
674
- if (isBatch(sc) && Array.isArray(st.generations)) {
675
- return handleBatchStatus(sc, st);
719
+ // Batch (prompts[] fan-out): multi-id status shape { all_done, generations[] },
720
+ // or the same data as items[] when it came via structuredContent.
721
+ var gens = Array.isArray(st.generations) ? st.generations : itemsToGens(st.items);
722
+ if (isBatch(sc) && gens) {
723
+ return handleBatchStatus(sc, {
724
+ all_done: st.all_done != null ? st.all_done : gens.every(function (g) { return isTerminal(g.state); }),
725
+ generations: gens
726
+ });
676
727
  }
677
728
  if (stateName === 'completed') {
678
729
  pollErrors = 0;
@@ -724,8 +775,13 @@ function poll(sc) {
724
775
  function handleBatchStatus(sc, st) {
725
776
  pollErrors = 0;
726
777
  var gens = st.generations || [];
778
+ // Cells are laid out in submit order; match results by id, not position.
779
+ var cellIndex = function (g, i) {
780
+ var idx = sc.generation_ids ? sc.generation_ids.indexOf(g.generation_id) : -1;
781
+ return idx < 0 ? i : idx;
782
+ };
727
783
  gens.forEach(function (g, i) {
728
- if (g.state === 'completed') fillBatchCell(sc, i, g);
784
+ if (g.state === 'completed') fillBatchCell(sc, cellIndex(g, i), g);
729
785
  });
730
786
  if (!st.all_done) return schedulePoll(sc);
731
787
 
@@ -753,8 +809,8 @@ function handleBatchStatus(sc, st) {
753
809
  resolved.reference_images = r.reference_images;
754
810
  }
755
811
  scenes.push({
756
- scene_number: i + 1,
757
- title: capAt(sc, i),
812
+ scene_number: cellIndex(g, i) + 1,
813
+ title: capAt(sc, cellIndex(g, i)),
758
814
  image_urls: sc.kind === 'video' ? [] : urls,
759
815
  video_urls: sc.kind === 'video' ? urls : []
760
816
  });
@@ -1305,6 +1361,41 @@ function kindFromTool(tool, sc) {
1305
1361
 
1306
1362
  // Recover a successful legacy/text result when a host mounted the iframe from
1307
1363
  // declaration metadata but the server did not recognize its Apps capability.
1364
+ // Every generate_* tool on a text host answers a poll-window timeout with the
1365
+ // same plain shape from pollOrTimedOut(): { state:'processing', generation_id,
1366
+ // _timed_out }. No phase, no widget, no urls, so nothing above recognised it
1367
+ // and the card stayed on its pre-render skeleton forever while the generation
1368
+ // finished in the library. Rebuild the live card from the tool INPUT (same
1369
+ // source completedFromPlain uses) and let it poll to completion.
1370
+ function liveFromTimedOut(sc) {
1371
+ if (!sc || !sc.generation_id || isTerminal(sc.state) || Array.isArray(sc.urls)) return null;
1372
+ var ids = Array.isArray(sc.generation_ids) && sc.generation_ids.length > 1 ? sc.generation_ids : null;
1373
+ var live = {
1374
+ widget: 'generation',
1375
+ phase: 'generating',
1376
+ tool: originTool,
1377
+ kind: kindFromTool(originTool, sc),
1378
+ prompt: originArgs.prompt || originArgs.text || '',
1379
+ model: sc.model || originArgs.model,
1380
+ settings: {
1381
+ duration: originArgs.duration,
1382
+ resolution: originArgs.resolution,
1383
+ aspect_ratio: originArgs.aspect_ratio,
1384
+ quality: originArgs.quality,
1385
+ visual_dna_ids: originArgs.visual_dna_ids,
1386
+ moodboard_id: originArgs.moodboard_id
1387
+ },
1388
+ count: ids ? ids.length : (originArgs.num_images || 1),
1389
+ generation_id: sc.generation_id,
1390
+ poll_tool: 'get_generation_status',
1391
+ status_args: ids ? { generation_ids: ids, wait: true } : { generation_id: sc.generation_id, wait: true },
1392
+ session_id: sc.session_id || originArgs.session_id,
1393
+ project_id: sc.project_id || originArgs.project_id
1394
+ };
1395
+ if (ids) { live.generation_ids = ids; live.prompts = originArgs.prompts || []; }
1396
+ return live;
1397
+ }
1398
+
1308
1399
  function completedFromPlain(sc) {
1309
1400
  if (!sc || (!Array.isArray(sc.urls) && !Array.isArray(sc.scenes))) return null;
1310
1401
  return Object.assign({}, sc, {
@@ -1333,7 +1424,7 @@ window.kolbo.onToolResult(function (result) {
1333
1424
  var list = listPayload(sc);
1334
1425
  if (list) return renderList(list);
1335
1426
  if (sc && (sc.phase || sc.widget)) return boot(sc);
1336
- var recovered = completedFromPlain(sc);
1427
+ var recovered = completedFromPlain(sc) || liveFromTimedOut(sc);
1337
1428
  if (recovered) return boot(recovered);
1338
1429
  // Tool errored (or returned plain text): show it instead of a dead blank card.
1339
1430
  var txt = '';
@@ -88,7 +88,7 @@ async function submitBatch(rawItems, submitOne) {
88
88
  // multi-id branch, same fix: always ship structuredContent via the shared
89
89
  // kind:'status' grid, which already renders any mix of completed/processing
90
90
  // items correctly.
91
- async function pollBatch(client, batch, { interval, timeout }, toolName) {
91
+ async function pollBatch(client, batch, { interval, timeout }, toolName, submittedModel) {
92
92
  const polls = await Promise.all(batch.ids.map((id) => pollOrTimedOut(client, id, { interval, timeout })));
93
93
  const generations = polls.map((p, i) => p.timedOut
94
94
  ? { 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.' }
@@ -103,7 +103,7 @@ async function pollBatch(client, batch, { interval, timeout }, toolName) {
103
103
  const modelsRan = [...new Set(polls.filter((p) => !p.timedOut).map((p) => p.result.result && p.result.result.model).filter(Boolean))];
104
104
  return uiCompleted({
105
105
  tool: toolName, kind: 'status', client,
106
- model: modelsRan.length === 1 ? modelsRan[0] : 'Generations',
106
+ model: modelsRan.length === 1 ? modelsRan[0] : (submittedModel || 'Generations'),
107
107
  gen: { generation_id: batch.ids[0], session_id: batch.ok[0].gen.session_id },
108
108
  settings: {},
109
109
  items: generations.map(g => ({
@@ -111,6 +111,8 @@ async function pollBatch(client, batch, { interval, timeout }, toolName) {
111
111
  state: g.status,
112
112
  title: g.prompt,
113
113
  url: Array.isArray(g.urls) ? g.urls[0] : undefined,
114
+ urls: Array.isArray(g.urls) ? g.urls : undefined,
115
+ credits_used: g.credits_used,
114
116
  })),
115
117
  }, text);
116
118
  }
@@ -292,7 +294,7 @@ function registerGenerateTools(server, client, options = {}) {
292
294
  status_args: { generation_ids: batch.ids, wait: true },
293
295
  reference_images
294
296
  });
295
- return pollBatch(client, batch, { interval: (batch.ok[0].gen.poll_interval_hint || 3) * 1000, timeout: 150000 }, 'generate_image');
297
+ return pollBatch(client, batch, { interval: (batch.ok[0].gen.poll_interval_hint || 3) * 1000, timeout: 150000 }, 'generate_image', model);
296
298
  }
297
299
 
298
300
  const gen = await client.post('/v1/generate/image', { ...shared, prompt, num_images });
@@ -381,7 +383,7 @@ function registerGenerateTools(server, client, options = {}) {
381
383
  status_args: { generation_ids: batch.ids, wait: true },
382
384
  reference_images: [...(source_images || []), ...(reference_images || [])]
383
385
  });
384
- return pollBatch(client, batch, { interval: (batch.ok[0].gen.poll_interval_hint || 3) * 1000, timeout: 150000 }, 'generate_image_edit');
386
+ return pollBatch(client, batch, { interval: (batch.ok[0].gen.poll_interval_hint || 3) * 1000, timeout: 150000 }, 'generate_image_edit', model);
385
387
  }
386
388
 
387
389
  const gen = await client.post('/v1/generate/image-edit', { ...shared, prompt, num_images });
@@ -634,7 +636,7 @@ function registerGenerateTools(server, client, options = {}) {
634
636
  status_args: { generation_ids: batch.ids, wait: true },
635
637
  reference_images
636
638
  });
637
- return pollBatch(client, batch, { interval: (batch.ok[0].gen.poll_interval_hint || 8) * 1000, timeout: 150000 }, 'generate_video');
639
+ return pollBatch(client, batch, { interval: (batch.ok[0].gen.poll_interval_hint || 8) * 1000, timeout: 150000 }, 'generate_video', model);
638
640
  }
639
641
 
640
642
  const gen = await client.post('/v1/generate/video', { ...shared, prompt });
@@ -724,7 +726,7 @@ function registerGenerateTools(server, client, options = {}) {
724
726
  status_args: { generation_ids: batch.ids, wait: true },
725
727
  reference_images: items.map((item) => item.image_url)
726
728
  });
727
- return pollBatch(client, batch, { interval: (batch.ok[0].gen.poll_interval_hint || 8) * 1000, timeout: 900000 }, 'generate_video_from_image');
729
+ return pollBatch(client, batch, { interval: (batch.ok[0].gen.poll_interval_hint || 8) * 1000, timeout: 900000 }, 'generate_video_from_image', model);
728
730
  }
729
731
 
730
732
  const gen = await client.post('/v1/generate/video/from-image', { ...shared, image_url, prompt });
@@ -1249,6 +1251,12 @@ function registerGenerateTools(server, client, options = {}) {
1249
1251
  state: r.state,
1250
1252
  title: res.prompt_used || res.prompt || undefined,
1251
1253
  url: Array.isArray(res.urls) ? res.urls[0] : undefined,
1254
+ // A live batch card reads these from structuredContent (its only
1255
+ // view of this response) to fill each cell and repaint its chips.
1256
+ urls: Array.isArray(res.urls) ? res.urls : undefined,
1257
+ credits_used: creditFields(r).credits_used,
1258
+ model: res.model, model_name: res.model_name, model_icon: res.model_icon,
1259
+ reference_images: res.reference_images,
1252
1260
  };
1253
1261
  }),
1254
1262
  }, text, extraContent);
@@ -46,10 +46,14 @@ function registerPresetTools(server, client, options = {}) {
46
46
 
47
47
  if (lookup) return { content: [{ type: 'text', text }] };
48
48
 
49
- return uiResult(UI.list, text, {
50
- widget: 'list',
49
+ // TOOL_WIDGETS declares mediaGrid for this tool, so hosts that mount from
50
+ // the declaration (Claude desktop) rendered a media grid fed a list-shaped
51
+ // 8-item page with no page_tool: a "Load more" that could never load. Ship
52
+ // the whole catalog (a few hundred small rows) to the grid it actually mounts.
53
+ return uiResult(UI.mediaGrid, text, {
54
+ widget: 'mediaGrid',
51
55
  title: 'Presets' + (type ? ' — ' + type : ''),
52
- items: presets.slice(0, 8).map(p => ({
56
+ items: presets.slice(0, 300).map(p => ({
53
57
  id: p.id,
54
58
  title: p.name,
55
59
  subtitle: p.category,
@@ -60,7 +64,7 @@ function registerPresetTools(server, client, options = {}) {
60
64
  use_hint: 'Use preset "{TITLE}" (preset_id: {ID}) for my next generation — ask me for the prompt.'
61
65
  })),
62
66
  total: result.count || presets.length,
63
- has_more: presets.length > 8
67
+ has_more: presets.length > 300
64
68
  });
65
69
  }
66
70
  );