@kolbo/mcp 1.30.5 → 1.30.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.30.5",
3
+ "version": "1.30.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": {
@@ -102,10 +102,28 @@ const BRIDGE_JS = `
102
102
  }
103
103
 
104
104
  var sizeTimer = null;
105
- new MutationObserver(function () {
105
+ function queueSize(delay) {
106
106
  clearTimeout(sizeTimer);
107
- sizeTimer = setTimeout(notifySize, 120);
108
- }).observe(document.documentElement, { childList: true, subtree: true, attributes: true });
107
+ sizeTimer = setTimeout(notifySize, delay || 120);
108
+ }
109
+ new MutationObserver(function () { queueSize(120); })
110
+ .observe(document.documentElement, { childList: true, subtree: true, attributes: true });
111
+ // DOM mutations don't fire when an <img>/<video> finishes loading and reflows
112
+ // the card — without this the host keeps the pre-image height and the card
113
+ // gets an inner scrollbar. ResizeObserver catches every layout change.
114
+ if (window.ResizeObserver) {
115
+ var ro = new ResizeObserver(function () { queueSize(60); });
116
+ ro.observe(document.documentElement);
117
+ ro.observe(document.body);
118
+ var card = document.querySelector('.k-card');
119
+ if (card) ro.observe(card);
120
+ }
121
+ // Belt and suspenders: media load events bubble as capture-phase 'load'.
122
+ document.addEventListener('load', function (e) {
123
+ var t = e.target && e.target.tagName;
124
+ if (t === 'IMG' || t === 'VIDEO') queueSize(60);
125
+ }, true);
126
+ document.addEventListener('loadedmetadata', function () { queueSize(60); }, true);
109
127
 
110
128
  window.kolbo = {
111
129
  ready: function (f) { if (initialized) f(hostContext); else readyFns.push(f); },
package/src/apps/index.js CHANGED
@@ -169,14 +169,24 @@ async function modelInfoMap(client) {
169
169
  // Real p75 wall-clock estimate mined from production creditUsages —
170
170
  // the same source the in-app countdowns use. No estimate → no ETA shown.
171
171
  const eta = Number(m.estimatedDurationSeconds || m.estimated_duration_seconds) || null;
172
- const info = { icon, eta };
173
- if (m.name) byKey.set(String(m.name).toLowerCase(), info);
172
+ const info = { icon, eta, id: m.identifier || null };
173
+ // Display names collide across variants ("Nano Banana 2" names both the
174
+ // t2i model and its editing sibling) — on collision keep the model with
175
+ // the SHORTEST identifier (the base model), deterministically.
176
+ const setName = (k) => {
177
+ const prev = byKey.get(k);
178
+ if (!prev || !prev.id || (info.id && info.id.length < prev.id.length)) byKey.set(k, info);
179
+ };
180
+ if (m.name) setName(String(m.name).toLowerCase());
174
181
  if (m.identifier) byKey.set(String(m.identifier).toLowerCase(), info);
175
182
  }
176
183
  } catch (_) {
177
184
  /* fail open — widgets fall back to monogram chips, no ETA */
178
185
  }
179
- infoCache.set(cacheKey, { at: Date.now(), byKey });
186
+ // Never cache an empty map: the first request in a fresh worker (typical
187
+ // right after a deploy restart) can fail transiently, and caching that
188
+ // failure blanks every model icon for the TTL window.
189
+ if (byKey.size > 0) infoCache.set(cacheKey, { at: Date.now(), byKey });
180
190
  return byKey;
181
191
  }
182
192
 
@@ -192,6 +202,31 @@ async function modelIcon(client, modelName) {
192
202
  return (await modelInfo(client, modelName)).icon;
193
203
  }
194
204
 
205
+ /**
206
+ * Lenient model-identifier resolution for LLM-supplied model args.
207
+ * Users say "z-image"; the real identifier is "z-image/turbo" — the backend
208
+ * has no fuzzy matching on generation routes and fails deep in credit
209
+ * reservation. Resolve here: exact name/identifier hit → its identifier;
210
+ * else a UNIQUE identifier prefix match ("z-image" → "z-image/turbo");
211
+ * ambiguous or unknown → pass through unchanged (API stays source of truth).
212
+ */
213
+ async function canonicalModelId(client, input) {
214
+ if (!input || typeof input !== 'string') return input;
215
+ try {
216
+ const map = await modelInfoMap(client);
217
+ const key = input.toLowerCase().trim();
218
+ const hit = map.get(key) || map.get(key.replace(/\s+/g, '-'));
219
+ if (hit && hit.id) return hit.id;
220
+ const ids = new Set();
221
+ for (const info of map.values()) {
222
+ const id = (info.id || '').toLowerCase();
223
+ if (id && (id.startsWith(key + '/') || id.startsWith(key + '-'))) ids.add(info.id);
224
+ }
225
+ if (ids.size === 1) return [...ids][0];
226
+ } catch (_) { /* fail open */ }
227
+ return input;
228
+ }
229
+
195
230
  /* ------------------------------------------------------------------ */
196
231
  /* Declaration-level widget metadata */
197
232
  /* ------------------------------------------------------------------ */
@@ -256,6 +291,7 @@ module.exports = {
256
291
  modelIcon,
257
292
  modelInfo,
258
293
  modelInfoMap,
294
+ canonicalModelId,
259
295
  resolveAvatarUrl,
260
296
  widgetHtml, // exported for smoke tests
261
297
  };
@@ -7,7 +7,7 @@ const { z } = require('zod');
7
7
  const FormData = require('form-data');
8
8
  const { pollUntilDone } = require('../polling');
9
9
  const { resolveToBuffer, creditFields, projectIdField, inlineImageBlocks, uiGenerating, uiCompleted, appsEnabled } = require('./_shared');
10
- const { UI, uiResult } = require('../apps');
10
+ const { UI, uiResult, canonicalModelId } = require('../apps');
11
11
 
12
12
  function registerGenerateTools(server, client, options = {}) {
13
13
  // Only enabled by hosts that explicitly opt in (the remote HTTP connector).
@@ -37,6 +37,7 @@ function registerGenerateTools(server, client, options = {}) {
37
37
  project_id: projectIdField
38
38
  },
39
39
  async ({ prompt, model, aspect_ratio, enhance_prompt, num_images, reference_images, visual_dna_ids, moodboard_id, enable_web_search, resolution, preset_id, project_id }) => {
40
+ model = await canonicalModelId(client, model); // lenient id resolution ("z-image" → "z-image/turbo")
40
41
  const gen = await client.post('/v1/generate/image', {
41
42
  prompt, model, aspect_ratio, enhance_prompt, num_images,
42
43
  reference_images, visual_dna_ids, moodboard_id, enable_web_search, resolution, preset_id, project_id
@@ -87,6 +88,7 @@ function registerGenerateTools(server, client, options = {}) {
87
88
  project_id: projectIdField
88
89
  },
89
90
  async ({ prompt, model, source_images, aspect_ratio, enhance_prompt, num_images, visual_dna_ids, moodboard_id, enable_web_search, resolution, project_id }) => {
91
+ model = await canonicalModelId(client, model); // lenient id resolution ("z-image" → "z-image/turbo")
90
92
  const gen = await client.post('/v1/generate/image-edit', {
91
93
  prompt, model, source_images, aspect_ratio, enhance_prompt, num_images,
92
94
  visual_dna_ids, moodboard_id, enable_web_search, resolution, project_id
@@ -143,6 +145,7 @@ function registerGenerateTools(server, client, options = {}) {
143
145
  project_id: projectIdField
144
146
  },
145
147
  async ({ prompt, scene_count, model, aspect_ratio, workflow_type, duration, enhance_prompt, reference_images, visual_dna_ids, moodboard_id, moodboard_ids, resolution, project_id }) => {
148
+ model = await canonicalModelId(client, model); // lenient id resolution ("z-image" → "z-image/turbo")
146
149
  const gen = await client.post('/v1/generate/creative-director', {
147
150
  prompt, scene_count, model, aspect_ratio, workflow_type, duration,
148
151
  enhance_prompt, reference_images, visual_dna_ids, moodboard_id, moodboard_ids, resolution, project_id
@@ -204,6 +207,7 @@ function registerGenerateTools(server, client, options = {}) {
204
207
  project_id: projectIdField
205
208
  },
206
209
  async ({ prompt, model, aspect_ratio, duration, enhance_prompt, reference_images, resolution, preset_id, project_id }) => {
210
+ model = await canonicalModelId(client, model); // lenient id resolution ("z-image" → "z-image/turbo")
207
211
  const gen = await client.post('/v1/generate/video', {
208
212
  prompt, model, aspect_ratio, duration, enhance_prompt, reference_images, resolution, preset_id, project_id
209
213
  });
@@ -252,6 +256,7 @@ function registerGenerateTools(server, client, options = {}) {
252
256
  project_id: projectIdField
253
257
  },
254
258
  async ({ image_url, prompt, model, aspect_ratio, duration, enhance_prompt, visual_dna_ids, resolution, project_id }) => {
259
+ model = await canonicalModelId(client, model); // lenient id resolution ("z-image" → "z-image/turbo")
255
260
  const gen = await client.post('/v1/generate/video/from-image', {
256
261
  image_url, prompt, model, aspect_ratio, duration, enhance_prompt, visual_dna_ids, resolution, project_id
257
262
  });
@@ -299,6 +304,7 @@ function registerGenerateTools(server, client, options = {}) {
299
304
  project_id: projectIdField
300
305
  },
301
306
  async ({ prompt, model, style, instrumental, lyrics, vocal_gender, enhance_prompt, preset_id, project_id }) => {
307
+ model = await canonicalModelId(client, model); // lenient id resolution ("z-image" → "z-image/turbo")
302
308
  const gen = await client.post('/v1/generate/music', {
303
309
  prompt, model, style, instrumental, lyrics, vocal_gender, enhance_prompt, preset_id, project_id
304
310
  });
@@ -340,6 +346,7 @@ function registerGenerateTools(server, client, options = {}) {
340
346
  project_id: projectIdField
341
347
  },
342
348
  async ({ text, voice, model, language, project_id }) => {
349
+ model = await canonicalModelId(client, model); // lenient id resolution ("z-image" → "z-image/turbo")
343
350
  const gen = await client.post('/v1/generate/speech', {
344
351
  text, voice, model, language, project_id
345
352
  });
@@ -380,6 +387,7 @@ function registerGenerateTools(server, client, options = {}) {
380
387
  project_id: projectIdField
381
388
  },
382
389
  async ({ prompt, model, duration, prompt_influence, project_id }) => {
390
+ model = await canonicalModelId(client, model); // lenient id resolution ("z-image" → "z-image/turbo")
383
391
  const gen = await client.post('/v1/generate/sound', {
384
392
  prompt, model, duration, prompt_influence, project_id
385
393
  });
@@ -451,6 +459,7 @@ function registerGenerateTools(server, client, options = {}) {
451
459
  project_id: projectIdField
452
460
  },
453
461
  async ({ prompt, model, reference_images, reference_videos, audio_url, files, duration, aspect_ratio, motion, preset_id, enhance_prompt, visual_dna_ids, resolution, project_id }) => {
462
+ model = await canonicalModelId(client, model); // lenient id resolution ("z-image" → "z-image/turbo")
454
463
  if (!prompt) throw new Error('prompt is required');
455
464
 
456
465
  let startResponse;
@@ -527,6 +536,7 @@ function registerGenerateTools(server, client, options = {}) {
527
536
  project_id: projectIdField
528
537
  },
529
538
  async ({ first_frame_url, last_frame_url, first_frame, last_frame, prompt, model, duration, aspect_ratio, enhance_prompt, visual_dna_ids, resolution, project_id }) => {
539
+ model = await canonicalModelId(client, model); // lenient id resolution ("z-image" → "z-image/turbo")
530
540
  const urlMode = first_frame_url && last_frame_url;
531
541
  const fileMode = first_frame && last_frame;
532
542
  if (!urlMode && !fileMode) {
@@ -614,6 +624,7 @@ function registerGenerateTools(server, client, options = {}) {
614
624
  project_id: projectIdField
615
625
  },
616
626
  async ({ source, audio, text_prompt, model, bounding_box_target, sync_mode, model_mode, emotion, temperature, occlusion_detection_enabled, active_speaker_detection, project_id }) => {
627
+ model = await canonicalModelId(client, model); // lenient id resolution ("z-image" → "z-image/turbo")
617
628
  if (!source) throw new Error('source is required (URL or absolute local path to image/video)');
618
629
  if (!audio) throw new Error('audio is required (URL or absolute local path to audio file)');
619
630
 
@@ -732,6 +743,7 @@ function registerGenerateTools(server, client, options = {}) {
732
743
  project_id: projectIdField
733
744
  },
734
745
  async ({ source_video, prompt, model, aspect_ratio, duration, enhance_prompt, visual_dna_ids, resolution, reference_images, reference_videos, elements, preset, source_language, translation_language, srt_content, srt_file_url, vocabulary, customization, project_id }) => {
746
+ model = await canonicalModelId(client, model); // lenient id resolution ("z-image" → "z-image/turbo")
735
747
  if (!source_video) throw new Error('source_video is required');
736
748
 
737
749
  const isUrl = /^https?:\/\//i.test(source_video);
@@ -868,6 +880,7 @@ function registerGenerateTools(server, client, options = {}) {
868
880
  project_id: projectIdField
869
881
  },
870
882
  async ({ prompt, reference_images, mode, texture_prompt, model, topology, target_polycount, enable_tpose, enable_pbr, project_id }) => {
883
+ model = await canonicalModelId(client, model); // lenient id resolution ("z-image" → "z-image/turbo")
871
884
  if (!prompt && !(reference_images && reference_images.length > 0)) {
872
885
  throw new Error('Provide prompt (text mode) or reference_images (single/multi mode)');
873
886
  }
@@ -927,6 +940,7 @@ function registerGenerateTools(server, client, options = {}) {
927
940
  project_id: projectIdField
928
941
  },
929
942
  async ({ image_url, operation, model, scale, aspect_ratio, skin_strength, prompt, project_id }) => {
943
+ model = await canonicalModelId(client, model); // lenient id resolution ("z-image" → "z-image/turbo")
930
944
  if (operation === 'magic_edit' && !prompt) throw new Error('prompt is required for magic_edit operation');
931
945
  if (operation === 'reframe' && !aspect_ratio) throw new Error('aspect_ratio is required for reframe operation');
932
946
 
@@ -979,6 +993,7 @@ function registerGenerateTools(server, client, options = {}) {
979
993
  project_id: projectIdField
980
994
  },
981
995
  async ({ video_url, operation, model, aspect_ratio, scale, prompt, image_url, audio_url, duration, mode, project_id }) => {
996
+ model = await canonicalModelId(client, model); // lenient id resolution ("z-image" → "z-image/turbo")
982
997
  if (operation === 'magic_edit' && !prompt) throw new Error('prompt is required for magic_edit');
983
998
  if (operation === 'generate_audio' && !prompt) throw new Error('prompt is required for generate_audio');
984
999
  if (operation === 'reframe' && !aspect_ratio) throw new Error('aspect_ratio is required for reframe');