@kolbo/mcp 1.87.5 → 1.87.7

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.7",
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": {
@@ -20,7 +20,8 @@
20
20
  "test:blender": "node --test test/blender-tools.test.js",
21
21
  "generate-chatgpt-submission": "node scripts/generate-chatgpt-submission.js",
22
22
  "check-install": "node scripts/check-install.js",
23
- "check-doctrine-parity": "node scripts/check-doctrine-parity.js"
23
+ "check-doctrine-parity": "node scripts/check-doctrine-parity.js",
24
+ "gallery": "node scripts/widget-gallery.js --serve"
24
25
  },
25
26
  "keywords": [
26
27
  "kolbo",
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);
@@ -265,7 +266,13 @@ function displayKind(sc) {
265
266
  if (/\\.(mp3|wav|m4a|aac|ogg|flac)$/.test(first)) return 'audio';
266
267
  var kind = sc.kind;
267
268
  if (kind === 'video' || kind === 'scenes' || kind === 'audio' || kind === '3d' || kind === 'model3d') {
268
- return kind === 'scenes' ? 'video' : kind;
269
+ if (kind !== 'scenes') return kind;
270
+ // A finished prompts[] batch is kind:'scenes' whatever it generated, so an
271
+ // image batch wore a "video" chip. Read the media the scenes actually hold.
272
+ var s0 = sc.scenes && sc.scenes[0];
273
+ if (s0 && s0.video_urls && s0.video_urls.length) return 'video';
274
+ if (s0 && s0.image_urls && s0.image_urls.length) return 'image';
275
+ return 'video';
269
276
  }
270
277
  var tool = sc.tool || '';
271
278
  if (/video|elements|lipsync|first_last_frame/.test(tool)) return 'video';
@@ -486,6 +493,51 @@ function capAt(sc, i) {
486
493
  return sc._caps[i] || sc.prompts[i];
487
494
  }
488
495
 
496
+ // A completed TOOL CALL is not a completed GENERATION. When a generate_* call
497
+ // outlives its poll window on a text host (Claude desktop over stdio), the
498
+ // server answers with a kind:'status' grid whose items are still processing,
499
+ // and that used to render as a dead card of "processing" badges that never
500
+ // polled again: the user watched skeletons that would never fill while the
501
+ // images landed in the library. Turn any such payload back into a LIVE card,
502
+ // the same generating/polling path a fresh submit takes, cells filled per id.
503
+ function isTerminal(s) { return s === 'completed' || s === 'failed' || s === 'cancelled'; }
504
+ function liveFromStatus(sc) {
505
+ if (!sc || sc.phase !== 'completed' || !Array.isArray(sc.items) || !sc.items.length) return sc;
506
+ var ids = sc.items.map(function (it) { return it && it.id; });
507
+ if (ids.some(function (id) { return !id; })) return sc;
508
+ if (!sc.items.some(function (it) { return !isTerminal(it.state); })) return sc;
509
+ var kind = sc.kind === 'status' || !sc.kind ? kindFromTool(sc.tool, {}) : sc.kind;
510
+ var live = Object.assign({}, sc, {
511
+ phase: 'generating', kind: kind, items: undefined,
512
+ count: ids.length, poll_tool: 'get_generation_status', generation_id: ids[0]
513
+ });
514
+ if (ids.length > 1) {
515
+ live.generation_ids = ids;
516
+ live.prompts = sc.items.map(function (it) { return it.title || ''; });
517
+ live.status_args = { generation_ids: ids, wait: true };
518
+ } else {
519
+ live.status_args = { generation_id: ids[0], wait: true };
520
+ }
521
+ return live;
522
+ }
523
+ // Multi-id status responses carry their per-generation results as items[] in
524
+ // structuredContent (the shape the status grid renders) and as generations[]
525
+ // only in the text, and structured() reads structuredContent first, so a live
526
+ // batch card never saw generations[] on a host that ships structuredContent.
527
+ function itemsToGens(items) {
528
+ if (!Array.isArray(items)) return null;
529
+ return items.map(function (it) {
530
+ return {
531
+ generation_id: it.id, state: it.state, credits_used: it.credits_used,
532
+ result: {
533
+ urls: Array.isArray(it.urls) ? it.urls : (it.url ? [it.url] : []),
534
+ model: it.model, model_name: it.model_name, model_icon: it.model_icon,
535
+ reference_images: it.reference_images
536
+ }
537
+ };
538
+ });
539
+ }
540
+
489
541
  function renderGenerating(sc) {
490
542
  setPhaseChip('Generating', true);
491
543
  var n = Math.min(sc.count || 1, isBatch(sc) ? 8 : 4);
@@ -670,9 +722,14 @@ function poll(sc) {
670
722
  if (++pollErrors >= MAX_POLL_ERRORS) return renderTrackingIssue(st.error || 'Tracking paused. The generation may still be running.');
671
723
  return schedulePoll(sc);
672
724
  }
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);
725
+ // Batch (prompts[] fan-out): multi-id status shape { all_done, generations[] },
726
+ // or the same data as items[] when it came via structuredContent.
727
+ var gens = Array.isArray(st.generations) ? st.generations : itemsToGens(st.items);
728
+ if (isBatch(sc) && gens) {
729
+ return handleBatchStatus(sc, {
730
+ all_done: st.all_done != null ? st.all_done : gens.every(function (g) { return isTerminal(g.state); }),
731
+ generations: gens
732
+ });
676
733
  }
677
734
  if (stateName === 'completed') {
678
735
  pollErrors = 0;
@@ -724,8 +781,13 @@ function poll(sc) {
724
781
  function handleBatchStatus(sc, st) {
725
782
  pollErrors = 0;
726
783
  var gens = st.generations || [];
784
+ // Cells are laid out in submit order; match results by id, not position.
785
+ var cellIndex = function (g, i) {
786
+ var idx = sc.generation_ids ? sc.generation_ids.indexOf(g.generation_id) : -1;
787
+ return idx < 0 ? i : idx;
788
+ };
727
789
  gens.forEach(function (g, i) {
728
- if (g.state === 'completed') fillBatchCell(sc, i, g);
790
+ if (g.state === 'completed') fillBatchCell(sc, cellIndex(g, i), g);
729
791
  });
730
792
  if (!st.all_done) return schedulePoll(sc);
731
793
 
@@ -753,8 +815,8 @@ function handleBatchStatus(sc, st) {
753
815
  resolved.reference_images = r.reference_images;
754
816
  }
755
817
  scenes.push({
756
- scene_number: i + 1,
757
- title: capAt(sc, i),
818
+ scene_number: cellIndex(g, i) + 1,
819
+ title: capAt(sc, cellIndex(g, i)),
758
820
  image_urls: sc.kind === 'video' ? [] : urls,
759
821
  video_urls: sc.kind === 'video' ? urls : []
760
822
  });
@@ -1305,6 +1367,41 @@ function kindFromTool(tool, sc) {
1305
1367
 
1306
1368
  // Recover a successful legacy/text result when a host mounted the iframe from
1307
1369
  // declaration metadata but the server did not recognize its Apps capability.
1370
+ // Every generate_* tool on a text host answers a poll-window timeout with the
1371
+ // same plain shape from pollOrTimedOut(): { state:'processing', generation_id,
1372
+ // _timed_out }. No phase, no widget, no urls, so nothing above recognised it
1373
+ // and the card stayed on its pre-render skeleton forever while the generation
1374
+ // finished in the library. Rebuild the live card from the tool INPUT (same
1375
+ // source completedFromPlain uses) and let it poll to completion.
1376
+ function liveFromTimedOut(sc) {
1377
+ if (!sc || !sc.generation_id || isTerminal(sc.state) || Array.isArray(sc.urls)) return null;
1378
+ var ids = Array.isArray(sc.generation_ids) && sc.generation_ids.length > 1 ? sc.generation_ids : null;
1379
+ var live = {
1380
+ widget: 'generation',
1381
+ phase: 'generating',
1382
+ tool: originTool,
1383
+ kind: kindFromTool(originTool, sc),
1384
+ prompt: originArgs.prompt || originArgs.text || '',
1385
+ model: sc.model || originArgs.model,
1386
+ settings: {
1387
+ duration: originArgs.duration,
1388
+ resolution: originArgs.resolution,
1389
+ aspect_ratio: originArgs.aspect_ratio,
1390
+ quality: originArgs.quality,
1391
+ visual_dna_ids: originArgs.visual_dna_ids,
1392
+ moodboard_id: originArgs.moodboard_id
1393
+ },
1394
+ count: ids ? ids.length : (originArgs.num_images || 1),
1395
+ generation_id: sc.generation_id,
1396
+ poll_tool: 'get_generation_status',
1397
+ status_args: ids ? { generation_ids: ids, wait: true } : { generation_id: sc.generation_id, wait: true },
1398
+ session_id: sc.session_id || originArgs.session_id,
1399
+ project_id: sc.project_id || originArgs.project_id
1400
+ };
1401
+ if (ids) { live.generation_ids = ids; live.prompts = originArgs.prompts || []; }
1402
+ return live;
1403
+ }
1404
+
1308
1405
  function completedFromPlain(sc) {
1309
1406
  if (!sc || (!Array.isArray(sc.urls) && !Array.isArray(sc.scenes))) return null;
1310
1407
  return Object.assign({}, sc, {
@@ -1333,7 +1430,7 @@ window.kolbo.onToolResult(function (result) {
1333
1430
  var list = listPayload(sc);
1334
1431
  if (list) return renderList(list);
1335
1432
  if (sc && (sc.phase || sc.widget)) return boot(sc);
1336
- var recovered = completedFromPlain(sc);
1433
+ var recovered = completedFromPlain(sc) || liveFromTimedOut(sc);
1337
1434
  if (recovered) return boot(recovered);
1338
1435
  // Tool errored (or returned plain text): show it instead of a dead blank card.
1339
1436
  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
  );