@kolbo/mcp 1.60.0 → 1.61.0

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.60.0",
3
+ "version": "1.61.0",
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
@@ -170,7 +170,7 @@ function uiResult(uri, text, structured) {
170
170
  /* ------------------------------------------------------------------ */
171
171
 
172
172
  const ICON_TTL_MS = 10 * 60 * 1000;
173
- const infoCache = new Map(); // apiBase → { at, byKey: Map<lowername, {icon, eta}> }
173
+ const infoCache = new Map(); // apiBase → { at, byKey: Map<lowername, info>, all: info[] }
174
174
 
175
175
  /**
176
176
  * Resolve a Model.avatar value to an absolute URL. Avatars are bare filenames
@@ -189,11 +189,12 @@ function resolveAvatarUrl(avatar) {
189
189
  return `${ICON_CDN_BASE}/${encodeURIComponent(avatar)}`;
190
190
  }
191
191
 
192
- async function modelInfoMap(client) {
192
+ async function modelCatalog(client) {
193
193
  const cacheKey = client.apiBase || 'default';
194
194
  const hit = infoCache.get(cacheKey);
195
- if (hit && Date.now() - hit.at < ICON_TTL_MS) return hit.byKey;
195
+ if (hit && Date.now() - hit.at < ICON_TTL_MS) return hit;
196
196
  const byKey = new Map();
197
+ const all = [];
197
198
  try {
198
199
  const res = await client.request('GET', '/v1/models');
199
200
  const models = res?.models || res?.data?.models || [];
@@ -206,10 +207,17 @@ async function modelInfoMap(client) {
206
207
  // `name` is the CLEAN display name ("Google TTS"); it is what widgets show.
207
208
  // Without it the model chip fell back to whatever raw string the caller or
208
209
  // the status endpoint supplied ("google_tts", "fal-ai/bytedance/omnihuman/v1.5").
209
- const info = { icon, eta, id: m.identifier || null, name: m.name || null };
210
+ // `types` is the catalog `type` array ("text_to_video", "img_to_video", …)
211
+ // the ONLY thing that tells two same-named variants apart. See canonicalModelId.
212
+ const raw = m.types !== undefined ? m.types : m.type;
213
+ const types = (Array.isArray(raw) ? raw : [raw]).filter(Boolean).map(String);
214
+ const info = { icon, eta, id: m.identifier || null, name: m.name || null, types };
215
+ all.push(info);
210
216
  // Display names collide across variants ("Nano Banana 2" names both the
211
217
  // t2i model and its editing sibling) — on collision keep the model with
212
- // the SHORTEST identifier (the base model), deterministically.
218
+ // the SHORTEST identifier (the base model), deterministically. This map is
219
+ // for ICONS/ETAs, where the variant doesn't matter; identifier resolution
220
+ // must NOT use it (that is what made "Kling 2.6 Pro" always mean the t2v one).
213
221
  const setName = (k) => {
214
222
  const prev = byKey.get(k);
215
223
  if (!prev || !prev.id || (info.id && info.id.length < prev.id.length)) byKey.set(k, info);
@@ -220,11 +228,16 @@ async function modelInfoMap(client) {
220
228
  } catch (_) {
221
229
  /* fail open — widgets fall back to monogram chips, no ETA */
222
230
  }
231
+ const entry = { at: Date.now(), byKey, all };
223
232
  // Never cache an empty map: the first request in a fresh worker (typical
224
233
  // right after a deploy restart) can fail transiently, and caching that
225
234
  // failure blanks every model icon for the TTL window.
226
- if (byKey.size > 0) infoCache.set(cacheKey, { at: Date.now(), byKey });
227
- return byKey;
235
+ if (byKey.size > 0) infoCache.set(cacheKey, entry);
236
+ return entry;
237
+ }
238
+
239
+ async function modelInfoMap(client) {
240
+ return (await modelCatalog(client)).byKey;
228
241
  }
229
242
 
230
243
  /** Resolve one model's { icon, eta, name }; missing → all null. */
@@ -291,6 +304,22 @@ const AUTO_ALIASES = new Set([
291
304
  'auto', 'autoselect', 'smartselect', 'kolbosmartselectrouter', 'default', 'none',
292
305
  ]);
293
306
 
307
+ /**
308
+ * Narrow several models that answer to the same string down to one identifier.
309
+ * The CALLING TOOL's catalog type decides: a display name like "Kling 2.6 Pro"
310
+ * names one model PER MODALITY (…/text-to-video and …/image-to-video), and only
311
+ * the caller knows which it wants. No type match (or no type given) → shortest
312
+ * identifier, the same deterministic tiebreak the icon map uses.
313
+ */
314
+ function pickForType(candidates, types) {
315
+ if (!candidates.length) return null;
316
+ const typed = types.length
317
+ ? candidates.filter((i) => i.types.some((t) => types.includes(t)))
318
+ : [];
319
+ const pool = typed.length ? typed : candidates;
320
+ return pool.reduce((a, b) => (b.id.length < a.id.length ? b : a)).id;
321
+ }
322
+
294
323
  /**
295
324
  * Lenient model-identifier resolution for LLM-supplied model args.
296
325
  * Users say "z-image"; the real identifier is "z-image/turbo" — the backend
@@ -299,6 +328,12 @@ const AUTO_ALIASES = new Set([
299
328
  * else a separator-insensitive hit ("flux-2-flash" → "flux-2/flash"); else a
300
329
  * UNIQUE prefix match ("z-image" → "z-image/turbo").
301
330
  *
331
+ * `type` is the calling tool's catalog type (a string, or an array when the
332
+ * tool spans several — lipsync, 3D). It is what makes resolution MODALITY-AWARE:
333
+ * without it, "Kling 2.6 Pro" from generate_video_from_image resolved to
334
+ * kling-video/v2.6/pro/text-to-video (2026-08-10), so the image-to-video
335
+ * pipeline submitted the TEXT-to-video endpoint and billed against it.
336
+ *
302
337
  * Still unresolved: throw with the near misses named. The API answers a bad
303
338
  * identifier with a bare INVALID_*_MODEL and no hint, which on 2026-08-09 sent
304
339
  * an agent guessing "minimax-hailuo-3" (real id: "minimax-h3") and then
@@ -307,43 +342,46 @@ const AUTO_ALIASES = new Set([
307
342
  * unchanged, so identifiers the catalog does not publish (hidden models) still
308
343
  * reach the API and it stays the source of truth.
309
344
  */
310
- async function canonicalModelId(client, input) {
345
+ async function canonicalModelId(client, input, type) {
311
346
  if (!input || typeof input !== 'string') return input;
312
347
  const key = input.toLowerCase().trim();
313
348
  const want = normId(key);
314
349
  if (!want || AUTO_ALIASES.has(want)) return input;
315
350
 
316
- let map;
351
+ let all;
317
352
  try {
318
- map = await modelInfoMap(client);
353
+ all = (await modelCatalog(client)).all;
319
354
  } catch (_) {
320
355
  return input; // fail open — never block a generation on a catalog hiccup
321
356
  }
322
- if (!map || map.size === 0) return input;
357
+ const models = (all || []).filter((i) => i.id);
358
+ if (!models.length) return input;
359
+
360
+ const types = (Array.isArray(type) ? type : [type]).filter(Boolean);
361
+ const dashed = key.replace(/\s+/g, '-');
323
362
 
324
363
  // 1. exact name / identifier hit
325
- const hit = map.get(key) || map.get(key.replace(/\s+/g, '-'));
326
- if (hit && hit.id) return hit.id;
327
-
328
- // 2 + 3. separator-insensitive exact, then unique prefix
329
- const byNorm = new Map();
330
- for (const info of map.values()) {
331
- if (!info.id) continue;
332
- for (const k of [info.id, info.name]) {
333
- const n = normId(k);
334
- if (n && !byNorm.has(n)) byNorm.set(n, info.id);
335
- }
336
- }
337
- if (byNorm.has(want)) return byNorm.get(want);
338
- const prefixed = new Set();
339
- for (const [n, id] of byNorm) if (n.startsWith(want)) prefixed.add(id);
340
- if (prefixed.size === 1) return [...prefixed][0];
364
+ const exact = pickForType(models.filter((i) => [i.id, i.name].some(
365
+ (k) => k && (k.toLowerCase() === key || k.toLowerCase() === dashed)
366
+ )), types);
367
+ if (exact) return exact;
368
+
369
+ // 2. separator-insensitive exact ("flux-2-flash" → "flux-2/flash")
370
+ const loose = pickForType(models.filter((i) => normId(i.id) === want || normId(i.name) === want), types);
371
+ if (loose) return loose;
372
+
373
+ // 3. unique prefix ("z-image" "z-image/turbo") the modality filter runs
374
+ // FIRST, so a stem shared by a t2v/i2v pair is no longer ambiguous.
375
+ const prefixed = models.filter((i) => normId(i.id).startsWith(want) || normId(i.name).startsWith(want));
376
+ const narrowed = types.length ? prefixed.filter((i) => i.types.some((t) => types.includes(t))) : [];
377
+ const ids = new Set((narrowed.length ? narrowed : prefixed).map((i) => i.id));
378
+ if (ids.size === 1) return [...ids][0];
341
379
 
342
380
  // 4. unknown — name the near misses instead of dead-ending at the API.
343
381
  const stem = normId(key.split(/[\s._/-]+/).filter(Boolean)[0] || key);
344
382
  const near = [...new Set(
345
- [...map.values()]
346
- .filter((i) => i.id && stem && (normId(i.id).startsWith(stem) || normId(i.name).startsWith(stem)))
383
+ models
384
+ .filter((i) => stem && (normId(i.id).startsWith(stem) || normId(i.name).startsWith(stem)))
347
385
  .map((i) => (i.name ? `${i.id} (${i.name})` : i.id))
348
386
  )].sort().slice(0, 12);
349
387
  if (!near.length) return input;
@@ -139,7 +139,7 @@ function registerGenerateTools(server, client, options = {}) {
139
139
  },
140
140
  async ({ prompt, prompts, model, aspect_ratio, enhance_prompt = false, num_images, reference_images, visual_dna_ids, moodboard_id, enable_web_search, resolution, quality, preset_id, cinematic, skip_color_palette, project_id }) => {
141
141
  if (!prompt && !(prompts && prompts.length)) throw new Error('Provide prompt or prompts');
142
- model = await canonicalModelId(client, model); // lenient id resolution ("z-image" → "z-image/turbo")
142
+ model = await canonicalModelId(client, model, 'text_to_img'); // lenient id resolution ("z-image" → "z-image/turbo")
143
143
  const shared = {
144
144
  model, aspect_ratio, enhance_prompt,
145
145
  reference_images, visual_dna_ids, moodboard_id, enable_web_search, resolution, quality, preset_id, cinematic, skip_color_palette, project_id
@@ -210,7 +210,7 @@ function registerGenerateTools(server, client, options = {}) {
210
210
  project_id: projectIdField
211
211
  },
212
212
  async ({ prompt, model, source_images, aspect_ratio, enhance_prompt = false, num_images, visual_dna_ids, moodboard_id, enable_web_search, resolution, cinematic, skip_color_palette, project_id }) => {
213
- model = await canonicalModelId(client, model); // lenient id resolution ("z-image" → "z-image/turbo")
213
+ model = await canonicalModelId(client, model, 'image_editing'); // lenient id resolution ("z-image" → "z-image/turbo")
214
214
  const gen = await client.post('/v1/generate/image-edit', {
215
215
  prompt, model, source_images, aspect_ratio, enhance_prompt, num_images,
216
216
  visual_dna_ids, moodboard_id, enable_web_search, resolution, cinematic, skip_color_palette, project_id
@@ -270,7 +270,7 @@ function registerGenerateTools(server, client, options = {}) {
270
270
  project_id: projectIdField
271
271
  },
272
272
  async ({ prompt, scene_count, model, aspect_ratio, workflow_type, duration, enhance_prompt = false, reference_images, visual_dna_ids, moodboard_id, moodboard_ids, resolution, project_id }) => {
273
- model = await canonicalModelId(client, model); // lenient id resolution ("z-image" → "z-image/turbo")
273
+ model = await canonicalModelId(client, model, workflow_type === 'video' ? 'text_to_video' : 'text_to_img'); // lenient id resolution ("z-image" → "z-image/turbo")
274
274
  const gen = await client.post('/v1/generate/creative-director', {
275
275
  prompt, scene_count, model, aspect_ratio, workflow_type, duration,
276
276
  enhance_prompt, reference_images, visual_dna_ids, moodboard_id, moodboard_ids, resolution, project_id
@@ -423,7 +423,7 @@ function registerGenerateTools(server, client, options = {}) {
423
423
  },
424
424
  async ({ prompt, prompts, model, aspect_ratio, duration, enhance_prompt = false, reference_images, resolution, preset_id, sound_enabled, skip_color_palette, project_id }) => {
425
425
  if (!prompt && !(prompts && prompts.length)) throw new Error('Provide prompt or prompts');
426
- model = await canonicalModelId(client, model); // lenient id resolution ("z-image" → "z-image/turbo")
426
+ model = await canonicalModelId(client, model, 'text_to_video'); // lenient id resolution ("z-image" → "z-image/turbo")
427
427
  const shared = {
428
428
  model, aspect_ratio, duration, enhance_prompt, reference_images, resolution, preset_id, sound_enabled, skip_color_palette, project_id
429
429
  };
@@ -495,7 +495,7 @@ function registerGenerateTools(server, client, options = {}) {
495
495
  project_id: projectIdField
496
496
  },
497
497
  async ({ image_url, prompt, model, aspect_ratio, duration, enhance_prompt = false, visual_dna_ids, resolution, sound_enabled, skip_color_palette, project_id }) => {
498
- model = await canonicalModelId(client, model); // lenient id resolution ("z-image" → "z-image/turbo")
498
+ model = await canonicalModelId(client, model, 'img_to_video'); // lenient id resolution ("z-image" → "z-image/turbo")
499
499
  const gen = await client.post('/v1/generate/video/from-image', {
500
500
  image_url, prompt, model, aspect_ratio, duration, enhance_prompt, visual_dna_ids, resolution, sound_enabled, skip_color_palette, project_id
501
501
  });
@@ -558,7 +558,7 @@ function registerGenerateTools(server, client, options = {}) {
558
558
  project_id: projectIdField
559
559
  },
560
560
  async ({ prompt, model, style, title, instrumental, lyrics, vocal_gender, negative_tags, duration_seconds, enhance_prompt = false, preset_id, style_weight, weirdness, audio_weight, persona_id, use_composition_plan, singing_dna_id, singing_voice_id, project_id }) => {
561
- model = await canonicalModelId(client, model); // lenient id resolution ("z-image" → "z-image/turbo")
561
+ model = await canonicalModelId(client, model, 'music_gen'); // lenient id resolution ("z-image" → "z-image/turbo")
562
562
  const gen = await client.post('/v1/generate/music', {
563
563
  prompt, model, style, title, instrumental, lyrics, vocal_gender, negative_tags,
564
564
  duration_seconds, enhance_prompt, preset_id,
@@ -630,7 +630,7 @@ function registerGenerateTools(server, client, options = {}) {
630
630
  project_id: projectIdField
631
631
  },
632
632
  async ({ text, voice, model, language, style_instructions, selected_style, emotion, speaking_speed, similarity_boost, style, use_speaker_boost, variance, tempo, promptBoost, seed, accentControl, voiceTitle, minimax_pitch, minimax_vol, minimax_intensity, minimax_timbre, project_id }) => {
633
- model = await canonicalModelId(client, model); // lenient id resolution ("z-image" → "z-image/turbo")
633
+ model = await canonicalModelId(client, model, 'text_to_speech'); // lenient id resolution ("z-image" → "z-image/turbo")
634
634
  // Resolve the requested voice against the REAL catalog (cached) so the card
635
635
  // can show its display name + portrait instead of a raw id, and so an id
636
636
  // that does not exist is reported instead of rendering silently: Google
@@ -704,7 +704,7 @@ function registerGenerateTools(server, client, options = {}) {
704
704
  project_id: projectIdField
705
705
  },
706
706
  async ({ prompt, model, duration, prompt_influence, cfg_strength, sound_loop, sound_tempo, sound_key, seed_voice, seed_speed, seed_volume, seed_pitch, seed_reference_audio_urls, seed_reference_image_url, project_id }) => {
707
- model = await canonicalModelId(client, model); // lenient id resolution ("z-image" → "z-image/turbo")
707
+ model = await canonicalModelId(client, model, 'text_to_sound'); // lenient id resolution ("z-image" → "z-image/turbo")
708
708
  const gen = await client.post('/v1/generate/sound', {
709
709
  prompt, model, duration, prompt_influence,
710
710
  cfg_strength, sound_loop, sound_tempo, sound_key,
@@ -920,7 +920,7 @@ function registerGenerateTools(server, client, options = {}) {
920
920
  project_id: projectIdField
921
921
  },
922
922
  async ({ prompt, model, reference_images, reference_videos, reference_audio_urls, audio_url, files, duration, aspect_ratio, motion, preset_id, enhance_prompt = false, visual_dna_ids, resolution, keyframes, project_id }) => {
923
- model = await canonicalModelId(client, model); // lenient id resolution ("z-image" → "z-image/turbo")
923
+ model = await canonicalModelId(client, model, 'elements'); // lenient id resolution ("z-image" → "z-image/turbo")
924
924
  if (!prompt) throw new Error('prompt is required');
925
925
 
926
926
  let startResponse;
@@ -1001,7 +1001,7 @@ function registerGenerateTools(server, client, options = {}) {
1001
1001
  project_id: projectIdField
1002
1002
  },
1003
1003
  async ({ first_frame_url, last_frame_url, first_frame, last_frame, prompt, model, duration, aspect_ratio, enhance_prompt = false, visual_dna_ids, resolution, project_id }) => {
1004
- model = await canonicalModelId(client, model); // lenient id resolution ("z-image" → "z-image/turbo")
1004
+ model = await canonicalModelId(client, model, 'firstlastgenerations'); // lenient id resolution ("z-image" → "z-image/turbo")
1005
1005
  const urlMode = first_frame_url && last_frame_url;
1006
1006
  const fileMode = first_frame && last_frame;
1007
1007
  if (!urlMode && !fileMode) {
@@ -1091,7 +1091,7 @@ function registerGenerateTools(server, client, options = {}) {
1091
1091
  project_id: projectIdField
1092
1092
  },
1093
1093
  async ({ source, audio, text_prompt, model, bounding_box_target, sync_mode, model_mode, emotion, temperature, occlusion_detection_enabled, active_speaker_detection, project_id }) => {
1094
- model = await canonicalModelId(client, model); // lenient id resolution ("z-image" → "z-image/turbo")
1094
+ model = await canonicalModelId(client, model, ['lipsync-image', 'lipsync-video']); // lenient id resolution ("z-image" → "z-image/turbo")
1095
1095
  if (!source) throw new Error('source is required (URL or absolute local path to image/video)');
1096
1096
  if (!audio) throw new Error('audio is required (URL or absolute local path to audio file)');
1097
1097
 
@@ -1212,7 +1212,7 @@ function registerGenerateTools(server, client, options = {}) {
1212
1212
  project_id: projectIdField
1213
1213
  },
1214
1214
  async ({ source_video, prompt, model, aspect_ratio, duration, enhance_prompt = false, visual_dna_ids, resolution, reference_images, reference_videos, elements, preset, source_language, translation_language, srt_content, srt_file_url, vocabulary, customization, project_id }) => {
1215
- model = await canonicalModelId(client, model); // lenient id resolution ("z-image" → "z-image/turbo")
1215
+ model = await canonicalModelId(client, model, 'video_to_video'); // lenient id resolution ("z-image" → "z-image/turbo")
1216
1216
  if (!source_video) throw new Error('source_video is required');
1217
1217
 
1218
1218
  const isUrl = /^https?:\/\//i.test(source_video);
@@ -1371,7 +1371,7 @@ function registerGenerateTools(server, client, options = {}) {
1371
1371
  project_id: projectIdField
1372
1372
  },
1373
1373
  async ({ prompt, reference_images, mode, texture_prompt, model, topology, target_polycount, enable_tpose, enable_pbr, project_id }) => {
1374
- model = await canonicalModelId(client, model); // lenient id resolution ("z-image" → "z-image/turbo")
1374
+ model = await canonicalModelId(client, model, ['3d_text_to_model', '3d_image_to_model', '3d_multi_image_to_model', '3d_world']); // lenient id resolution ("z-image" → "z-image/turbo")
1375
1375
  if (!prompt && !(reference_images && reference_images.length > 0)) {
1376
1376
  throw new Error('Provide prompt (text mode) or reference_images (single/multi mode)');
1377
1377
  }
@@ -1514,6 +1514,9 @@ function registerGenerateTools(server, client, options = {}) {
1514
1514
  zoom_out_percentage, expand_left, expand_right, expand_top, expand_bottom,
1515
1515
  project_id
1516
1516
  }) => {
1517
+ // No `type` argument: these are operation-routed tools (upscale / reframe /
1518
+ // removebg / …), each operation with its own model family — there is no single
1519
+ // catalog type to disambiguate against.
1517
1520
  model = await canonicalModelId(client, model);
1518
1521
 
1519
1522
  // Basic validation
@@ -1667,6 +1670,9 @@ function registerGenerateTools(server, client, options = {}) {
1667
1670
  start_time,
1668
1671
  project_id
1669
1672
  }) => {
1673
+ // No `type` argument: these are operation-routed tools (upscale / reframe /
1674
+ // removebg / …), each operation with its own model family — there is no single
1675
+ // catalog type to disambiguate against.
1670
1676
  model = await canonicalModelId(client, model);
1671
1677
 
1672
1678
  // Validation