@kolbo/mcp 1.30.2 → 1.30.4

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.30.2",
3
+ "version": "1.30.4",
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
@@ -123,38 +123,60 @@ function uiResult(uri, text, structured) {
123
123
  /* ------------------------------------------------------------------ */
124
124
 
125
125
  const ICON_TTL_MS = 10 * 60 * 1000;
126
- const iconCache = new Map(); // apiBase → { at, byKey: Map<lowername, url> }
126
+ const infoCache = new Map(); // apiBase → { at, byKey: Map<lowername, {icon, eta}> }
127
127
 
128
- async function modelIconMap(client) {
128
+ /**
129
+ * Resolve a Model.avatar value to an absolute URL. Avatars are bare filenames
130
+ * (sometimes with spaces) living in the BACKEND's public assets folder —
131
+ * `<api origin>/assets/<file>`. Do NOT use app.kolbo.ai for this: it's the SPA,
132
+ * whose catch-all returns 200 text/html for any missing file, which renders as
133
+ * a broken image in widgets.
134
+ */
135
+ function resolveAvatarUrl(avatar, apiBase) {
136
+ if (!avatar) return null;
137
+ if (/^https?:\/\//i.test(avatar)) return avatar;
138
+ let origin = 'https://api.kolbo.ai';
139
+ try {
140
+ origin = new URL(apiBase || 'https://api.kolbo.ai/api').origin;
141
+ } catch (_) { /* keep default */ }
142
+ return `${origin}/assets/${encodeURIComponent(avatar)}`;
143
+ }
144
+
145
+ async function modelInfoMap(client) {
129
146
  const cacheKey = client.apiBase || 'default';
130
- const hit = iconCache.get(cacheKey);
147
+ const hit = infoCache.get(cacheKey);
131
148
  if (hit && Date.now() - hit.at < ICON_TTL_MS) return hit.byKey;
132
149
  const byKey = new Map();
133
150
  try {
134
151
  const res = await client.request('GET', '/v1/models');
135
152
  const models = res?.models || res?.data?.models || [];
136
153
  for (const m of models) {
137
- if (!m || !m.avatar) continue;
138
- // The API usually resolves avatars to absolute URLs; bare filenames (older
139
- // deployments / internal calls) resolve against the app's public icon dir.
140
- const url = /^https?:\/\//i.test(m.avatar)
141
- ? m.avatar
142
- : `https://app.kolbo.ai/models_icons/${encodeURIComponent(m.avatar)}`;
143
- if (m.name) byKey.set(String(m.name).toLowerCase(), url);
144
- if (m.identifier) byKey.set(String(m.identifier).toLowerCase(), url);
154
+ if (!m) continue;
155
+ const icon = resolveAvatarUrl(m.avatar, client.apiBase);
156
+ // Real p75 wall-clock estimate mined from production creditUsages
157
+ // the same source the in-app countdowns use. No estimate → no ETA shown.
158
+ const eta = Number(m.estimatedDurationSeconds || m.estimated_duration_seconds) || null;
159
+ const info = { icon, eta };
160
+ if (m.name) byKey.set(String(m.name).toLowerCase(), info);
161
+ if (m.identifier) byKey.set(String(m.identifier).toLowerCase(), info);
145
162
  }
146
163
  } catch (_) {
147
- /* fail open — widgets fall back to monogram chips */
164
+ /* fail open — widgets fall back to monogram chips, no ETA */
148
165
  }
149
- iconCache.set(cacheKey, { at: Date.now(), byKey });
166
+ infoCache.set(cacheKey, { at: Date.now(), byKey });
150
167
  return byKey;
151
168
  }
152
169
 
153
- /** Resolve one model's icon URL; nullwidget renders a monogram. */
170
+ /** Resolve one model's { icon, eta }; missing{ icon: null, eta: null }. */
171
+ async function modelInfo(client, modelName) {
172
+ if (!modelName) return { icon: null, eta: null };
173
+ const map = await modelInfoMap(client);
174
+ return map.get(String(modelName).toLowerCase()) || { icon: null, eta: null };
175
+ }
176
+
177
+ /** Back-compat shim (used by uiCompleted and older call sites). */
154
178
  async function modelIcon(client, modelName) {
155
- if (!modelName) return null;
156
- const map = await modelIconMap(client);
157
- return map.get(String(modelName).toLowerCase()) || null;
179
+ return (await modelInfo(client, modelName)).icon;
158
180
  }
159
181
 
160
182
  /* ------------------------------------------------------------------ */
@@ -219,6 +241,8 @@ module.exports = {
219
241
  uiResult,
220
242
  appsEnabled,
221
243
  modelIcon,
222
- modelIconMap,
244
+ modelInfo,
245
+ modelInfoMap,
246
+ resolveAvatarUrl,
223
247
  widgetHtml, // exported for smoke tests
224
248
  };
@@ -12,7 +12,7 @@ const { widgetPage } = require('../html');
12
12
  * tool: 'generate_image', // originating MCP tool name
13
13
  * generation_id, poll_tool, // when phase === 'generating'
14
14
  * status_args, // extra args for the poll tool (optional)
15
- * estimated_seconds, // optional ETA hint
15
+
16
16
  * model, model_icon, prompt, count,
17
17
  * settings: { duration, resolution, aspect_ratio, audio, voice, mode },
18
18
  * reference_image, // thumbnail URL (optional)
@@ -35,10 +35,6 @@ const BODY = `
35
35
  <div class="k-prompt" id="prompt"></div>
36
36
  <div class="k-chips" id="chips"></div>
37
37
  <div id="stage"></div>
38
- <div class="k-progress" id="progress" style="display:none"><i id="progress-fill"></i></div>
39
- <div class="k-status-line" id="status-line" style="display:none">
40
- <span id="status-text">Generating…</span><span id="eta"></span>
41
- </div>
42
38
  <div class="k-prompt-row" id="prompt-row">
43
39
  <input class="k-input" id="action-input" placeholder="">
44
40
  <button class="k-btn primary" id="action-send">Send</button>
@@ -57,8 +53,6 @@ const SCRIPT = `
57
53
  var state = null; // current structuredContent
58
54
  var selected = 0; // selected result index
59
55
  var pollTimer = null;
60
- var progressTimer = null;
61
- var startedAt = Date.now();
62
56
 
63
57
  el('logo').innerHTML = KOLBO_LOGO + '<span>Kolbo</span>';
64
58
  el('kolbo-link').onclick = function (e) { e.preventDefault(); window.kolbo.openLink('https://app.kolbo.ai'); };
@@ -98,7 +92,7 @@ function renderChips(sc) {
98
92
  if (s.voice) h += chip('🎤 ' + esc(s.voice));
99
93
  if (s.mode) h += chip(esc(s.mode));
100
94
  if (sc.count > 1) h += chip('×' + sc.count);
101
- if (sc.reference_image) h += '<img class="k-ref-thumb" src="' + esc(sc.reference_image) + '" alt="" title="Reference image" onerror="this.style.display=\'none\'">';
95
+ if (sc.reference_image) h += '<img class="k-ref-thumb" src="' + esc(sc.reference_image) + '" alt="" title="Reference image" onerror="this.style.display=\\'none\\'">';
102
96
  el('chips').innerHTML = h;
103
97
  }
104
98
  function chip(inner) { return '<span class="k-chip">' + inner + '</span>'; }
@@ -117,24 +111,9 @@ function renderGenerating(sc) {
117
111
  (i === 0 ? '<span class="k-gen-badge"><span class="k-spin"></span>Generating</span>' : '') + '</div>';
118
112
  }
119
113
  el('stage').innerHTML = '<div class="k-gen-grid n' + n + '">' + cells + '</div>';
120
- el('progress').style.display = '';
121
- el('status-line').style.display = '';
122
114
  el('actions').innerHTML = '';
123
- startProgress(sc.estimated_seconds || defaultEta(sc.kind));
124
115
  schedulePoll(sc);
125
116
  }
126
- function defaultEta(kind) { return { image: 25, video: 120, audio: 45, '3d': 300, scenes: 240 }[kind] || 60; }
127
-
128
- function startProgress(etaSec) {
129
- clearInterval(progressTimer);
130
- progressTimer = setInterval(function () {
131
- var t = (Date.now() - startedAt) / 1000;
132
- var pct = Math.min(92, 100 * (1 - Math.exp(-t / (etaSec * 0.55))));
133
- el('progress-fill').style.width = pct.toFixed(1) + '%';
134
- var remain = Math.max(0, etaSec - t);
135
- el('eta').textContent = remain > 1 ? '~' + fmtDur(remain) + ' left' : 'finishing…';
136
- }, 500);
137
- }
138
117
 
139
118
  function schedulePoll(sc) {
140
119
  clearTimeout(pollTimer);
@@ -147,7 +126,7 @@ function poll(sc) {
147
126
  var stateName = st.state || st.phase || st.status;
148
127
  if (stateName === 'completed') {
149
128
  var r = st.result || st;
150
- finishProgress();
129
+
151
130
  var done = Object.assign({}, sc, r, {
152
131
  phase: 'completed',
153
132
  urls: r.urls || st.urls || [],
@@ -170,16 +149,11 @@ function poll(sc) {
170
149
  }
171
150
  }).catch(function () { schedulePoll(sc); });
172
151
  }
173
- function finishProgress() {
174
- clearInterval(progressTimer);
175
- el('progress-fill').style.width = '100%';
176
- setTimeout(function () { el('progress').style.display = 'none'; el('status-line').style.display = 'none'; }, 450);
177
- }
178
152
 
179
153
  /* ---------- results ---------- */
180
154
  function renderResult(sc) {
181
- clearTimeout(pollTimer); clearInterval(progressTimer);
182
- el('progress').style.display = 'none'; el('status-line').style.display = 'none';
155
+ clearTimeout(pollTimer);
156
+
183
157
  setPhaseChip('', false);
184
158
  if (sc.kind === 'scenes' && sc.scenes && sc.scenes.length) return renderScenes(sc);
185
159
  var urls = sc.urls || [];
@@ -272,8 +246,8 @@ function renderScenes(sc) {
272
246
  }
273
247
 
274
248
  function renderError(msg) {
275
- clearTimeout(pollTimer); clearInterval(progressTimer);
276
- el('progress').style.display = 'none'; el('status-line').style.display = 'none';
249
+ clearTimeout(pollTimer);
250
+
277
251
  setPhaseChip('Failed', false);
278
252
  el('stage').innerHTML = '<div class="k-error">⚠ ' + esc(msg) + '</div>';
279
253
  el('actions').innerHTML = '<button class="k-btn" id="retry-btn">↻ Try Again</button>';
@@ -311,6 +311,7 @@ const { UI, uiResult, appsEnabled, modelIcon } = require('../apps');
311
311
  * status_args args for poll_tool (default { generation_id })
312
312
  */
313
313
  async function uiGenerating(p) {
314
+ // No ETAs anywhere — just a spinner until the poll flips to completed.
314
315
  const icon = await modelIcon(p.client, p.model).catch(() => null);
315
316
  const structured = {
316
317
  phase: 'generating',
@@ -326,7 +327,6 @@ async function uiGenerating(p) {
326
327
  count: p.count || 1,
327
328
  settings: p.settings || {},
328
329
  reference_image: p.reference_image,
329
- estimated_seconds: p.estimated_seconds,
330
330
  };
331
331
  const text = JSON.stringify({
332
332
  status: 'submitted',
@@ -45,7 +45,7 @@ function registerGenerateTools(server, client, options = {}) {
45
45
  if (ui()) return uiGenerating({
46
46
  tool: 'generate_image', kind: 'image', gen, client, model, prompt,
47
47
  count: num_images, settings: { resolution, aspect_ratio },
48
- reference_image: reference_images?.[0], estimated_seconds: 25
48
+ reference_image: reference_images?.[0]
49
49
  });
50
50
 
51
51
  const result = await pollUntilDone(client, gen.generation_id, {
@@ -95,7 +95,7 @@ function registerGenerateTools(server, client, options = {}) {
95
95
  if (ui()) return uiGenerating({
96
96
  tool: 'generate_image_edit', kind: 'image', gen, client, model, prompt,
97
97
  count: num_images, settings: { resolution, aspect_ratio },
98
- reference_image: source_images?.[0], estimated_seconds: 40
98
+ reference_image: source_images?.[0]
99
99
  });
100
100
 
101
101
  // Multi-source compositing or DNA-anchored edits routinely exceed 120s
@@ -211,7 +211,7 @@ function registerGenerateTools(server, client, options = {}) {
211
211
  if (ui()) return uiGenerating({
212
212
  tool: 'generate_video', kind: 'video', gen, client, model, prompt,
213
213
  settings: { duration, resolution, aspect_ratio },
214
- reference_image: reference_images?.[0], estimated_seconds: 120
214
+ reference_image: reference_images?.[0]
215
215
  });
216
216
 
217
217
  const result = await pollUntilDone(client, gen.generation_id, {
@@ -259,7 +259,7 @@ function registerGenerateTools(server, client, options = {}) {
259
259
  if (ui()) return uiGenerating({
260
260
  tool: 'generate_video_from_image', kind: 'video', gen, client, model, prompt,
261
261
  settings: { duration, resolution, aspect_ratio },
262
- reference_image: image_url, estimated_seconds: 120
262
+ reference_image: image_url
263
263
  });
264
264
 
265
265
  const result = await pollUntilDone(client, gen.generation_id, {
@@ -306,7 +306,6 @@ function registerGenerateTools(server, client, options = {}) {
306
306
  if (ui()) return uiGenerating({
307
307
  tool: 'generate_music', kind: 'audio', gen, client, model: model || 'Suno', prompt,
308
308
  settings: { mode: instrumental ? 'instrumental' : (style || undefined) },
309
- estimated_seconds: 90
310
309
  });
311
310
 
312
311
  const result = await pollUntilDone(client, gen.generation_id, {
@@ -347,7 +346,7 @@ function registerGenerateTools(server, client, options = {}) {
347
346
 
348
347
  if (ui()) return uiGenerating({
349
348
  tool: 'generate_speech', kind: 'audio', gen, client, model, prompt: text,
350
- settings: { voice: voice || 'Rachel' }, estimated_seconds: 20
349
+ settings: { voice: voice || 'Rachel' }
351
350
  });
352
351
 
353
352
  const result = await pollUntilDone(client, gen.generation_id, {
@@ -387,7 +386,7 @@ function registerGenerateTools(server, client, options = {}) {
387
386
 
388
387
  if (ui()) return uiGenerating({
389
388
  tool: 'generate_sound', kind: 'audio', gen, client, model, prompt,
390
- settings: { duration }, estimated_seconds: 20
389
+ settings: { duration }
391
390
  });
392
391
 
393
392
  const result = await pollUntilDone(client, gen.generation_id, {
@@ -486,7 +485,7 @@ function registerGenerateTools(server, client, options = {}) {
486
485
  if (ui()) return uiGenerating({
487
486
  tool: 'generate_elements', kind: 'video', gen: startResponse, client, model, prompt,
488
487
  settings: { duration, resolution, aspect_ratio },
489
- reference_image: reference_images?.[0], estimated_seconds: 180
488
+ reference_image: reference_images?.[0]
490
489
  });
491
490
 
492
491
  const result = await pollUntilDone(client, startResponse.generation_id, {
@@ -564,7 +563,7 @@ function registerGenerateTools(server, client, options = {}) {
564
563
  if (ui()) return uiGenerating({
565
564
  tool: 'generate_first_last_frame', kind: 'video', gen: startResponse, client, model, prompt,
566
565
  settings: { duration, resolution, aspect_ratio },
567
- reference_image: first_frame_url || undefined, estimated_seconds: 120
566
+ reference_image: first_frame_url || undefined
568
567
  });
569
568
 
570
569
  const result = await pollUntilDone(client, startResponse.generation_id, {
@@ -674,7 +673,6 @@ function registerGenerateTools(server, client, options = {}) {
674
673
  tool: 'generate_lipsync', kind: 'video', gen: startResponse, client, model,
675
674
  prompt: text_prompt, settings: { mode: 'lipsync' },
676
675
  reference_image: sourceIsUrl && !/\.(mp4|mov|webm|mkv|avi|m4v)(\?|$)/i.test(source) ? source : undefined,
677
- estimated_seconds: 180
678
676
  });
679
677
 
680
678
  const result = await pollUntilDone(client, startResponse.generation_id, {
@@ -773,7 +771,7 @@ function registerGenerateTools(server, client, options = {}) {
773
771
  tool: 'generate_video_from_video', kind: 'video', gen: startResponse, client, model,
774
772
  prompt: prompt || (preset ? `Subtitles preset: ${preset}` : undefined),
775
773
  settings: { duration, resolution, aspect_ratio, mode: preset ? 'subtitles' : 'restyle' },
776
- reference_image: reference_images?.[0], estimated_seconds: 240
774
+ reference_image: reference_images?.[0]
777
775
  });
778
776
 
779
777
  const result = await pollUntilDone(client, startResponse.generation_id, {
@@ -890,7 +888,7 @@ function registerGenerateTools(server, client, options = {}) {
890
888
  if (ui()) return uiGenerating({
891
889
  tool: 'generate_3d', kind: '3d', gen: startResponse, client, model, prompt,
892
890
  settings: { mode: mode || (reference_images?.length > 1 ? 'multi' : reference_images?.length === 1 ? 'single' : 'text') },
893
- reference_image: reference_images?.[0], estimated_seconds: 300
891
+ reference_image: reference_images?.[0]
894
892
  });
895
893
 
896
894
  const result = await pollUntilDone(client, startResponse.generation_id, {
@@ -940,7 +938,7 @@ function registerGenerateTools(server, client, options = {}) {
940
938
  tool: 'edit_image', kind: 'image', gen, client, model,
941
939
  prompt: prompt || operation,
942
940
  settings: { mode: operation, aspect_ratio },
943
- reference_image: image_url, estimated_seconds: 40
941
+ reference_image: image_url
944
942
  });
945
943
 
946
944
  const result = await pollUntilDone(client, gen.generation_id, {
@@ -997,7 +995,7 @@ function registerGenerateTools(server, client, options = {}) {
997
995
  tool: 'edit_video', kind: 'video', gen, client, model,
998
996
  prompt: prompt || operation,
999
997
  settings: { mode: operation, duration, aspect_ratio },
1000
- reference_image: image_url, estimated_seconds: 180
998
+ reference_image: image_url
1001
999
  });
1002
1000
 
1003
1001
  const result = await pollUntilDone(client, gen.generation_id, {
@@ -263,7 +263,6 @@ function registerShortsCreatorTools(server, client, options = {}) {
263
263
  settings: { mode: 'shorts' },
264
264
  poll_tool: 'shorts_status',
265
265
  status_args: { job_id },
266
- estimated_seconds: 240
267
266
  });
268
267
 
269
268
  let job;