@kolbo/mcp 1.59.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 +3 -2
- package/src/apps/index.js +110 -22
- package/src/tools/generate.js +33 -16
- package/src/tools/models.js +52 -15
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kolbo/mcp",
|
|
3
|
-
"version": "1.
|
|
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": {
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
"start": "node src/index.js",
|
|
11
11
|
"smoke": "node scripts/smoke.js",
|
|
12
12
|
"check-parity": "node scripts/check-parity.js",
|
|
13
|
-
"prepublishOnly": "node scripts/smoke.js && node scripts/check-parity.js && node scripts/check-widget-fields.js && node scripts/check-widget-render.js && node scripts/check-skill-tools.js && node scripts/check-install.js",
|
|
13
|
+
"prepublishOnly": "node scripts/smoke.js && node scripts/check-parity.js && node scripts/check-widget-fields.js && node scripts/check-widget-render.js && node scripts/check-model-catalog.js && node scripts/check-skill-tools.js && node scripts/check-install.js",
|
|
14
|
+
"check-model-catalog": "node scripts/check-model-catalog.js",
|
|
14
15
|
"check-widget-fields": "node scripts/check-widget-fields.js",
|
|
15
16
|
"check-widget-render": "node scripts/check-widget-render.js",
|
|
16
17
|
"check-skill-tools": "node scripts/check-skill-tools.js",
|
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,
|
|
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
|
|
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
|
|
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
|
-
|
|
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,
|
|
227
|
-
return
|
|
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. */
|
|
@@ -279,29 +292,104 @@ async function modelIcon(client, modelName) {
|
|
|
279
292
|
return (await modelInfo(client, modelName)).icon;
|
|
280
293
|
}
|
|
281
294
|
|
|
295
|
+
// Separator-insensitive key. Catalog keys carry their own punctuation — the
|
|
296
|
+
// NAME is keyed "minimax h3", the IDENTIFIER "flux-2/flash" — so both sides
|
|
297
|
+
// must be flattened before comparing. Normalising only the input (the old
|
|
298
|
+
// `key.replace(/\s+/g, '-')`) is why "flux-2-flash" never found "flux-2/flash".
|
|
299
|
+
const normId = (s) => String(s || '').toLowerCase().replace(/[\s._/-]+/g, '');
|
|
300
|
+
|
|
301
|
+
// The API maps these to Smart Select itself. They are never typos, so they must
|
|
302
|
+
// never be "corrected" or reported as unknown.
|
|
303
|
+
const AUTO_ALIASES = new Set([
|
|
304
|
+
'auto', 'autoselect', 'smartselect', 'kolbosmartselectrouter', 'default', 'none',
|
|
305
|
+
]);
|
|
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
|
+
|
|
282
323
|
/**
|
|
283
324
|
* Lenient model-identifier resolution for LLM-supplied model args.
|
|
284
325
|
* Users say "z-image"; the real identifier is "z-image/turbo" — the backend
|
|
285
326
|
* has no fuzzy matching on generation routes and fails deep in credit
|
|
286
327
|
* reservation. Resolve here: exact name/identifier hit → its identifier;
|
|
287
|
-
* else a
|
|
288
|
-
*
|
|
328
|
+
* else a separator-insensitive hit ("flux-2-flash" → "flux-2/flash"); else a
|
|
329
|
+
* UNIQUE prefix match ("z-image" → "z-image/turbo").
|
|
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
|
+
*
|
|
337
|
+
* Still unresolved: throw with the near misses named. The API answers a bad
|
|
338
|
+
* identifier with a bare INVALID_*_MODEL and no hint, which on 2026-08-09 sent
|
|
339
|
+
* an agent guessing "minimax-hailuo-3" (real id: "minimax-h3") and then
|
|
340
|
+
* substituting a far more expensive model. Only throws when the catalog is
|
|
341
|
+
* healthy AND actually offers candidates — otherwise it passes through
|
|
342
|
+
* unchanged, so identifiers the catalog does not publish (hidden models) still
|
|
343
|
+
* reach the API and it stays the source of truth.
|
|
289
344
|
*/
|
|
290
|
-
async function canonicalModelId(client, input) {
|
|
345
|
+
async function canonicalModelId(client, input, type) {
|
|
291
346
|
if (!input || typeof input !== 'string') return input;
|
|
347
|
+
const key = input.toLowerCase().trim();
|
|
348
|
+
const want = normId(key);
|
|
349
|
+
if (!want || AUTO_ALIASES.has(want)) return input;
|
|
350
|
+
|
|
351
|
+
let all;
|
|
292
352
|
try {
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
353
|
+
all = (await modelCatalog(client)).all;
|
|
354
|
+
} catch (_) {
|
|
355
|
+
return input; // fail open — never block a generation on a catalog hiccup
|
|
356
|
+
}
|
|
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, '-');
|
|
362
|
+
|
|
363
|
+
// 1. exact name / identifier hit
|
|
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];
|
|
379
|
+
|
|
380
|
+
// 4. unknown — name the near misses instead of dead-ending at the API.
|
|
381
|
+
const stem = normId(key.split(/[\s._/-]+/).filter(Boolean)[0] || key);
|
|
382
|
+
const near = [...new Set(
|
|
383
|
+
models
|
|
384
|
+
.filter((i) => stem && (normId(i.id).startsWith(stem) || normId(i.name).startsWith(stem)))
|
|
385
|
+
.map((i) => (i.name ? `${i.id} (${i.name})` : i.id))
|
|
386
|
+
)].sort().slice(0, 12);
|
|
387
|
+
if (!near.length) return input;
|
|
388
|
+
throw new Error(
|
|
389
|
+
`Unknown model identifier "${input}". Did you mean: ${near.join(', ')}? `
|
|
390
|
+
+ 'Never guess an identifier — call list_models with the matching `type` and `format: "json"` '
|
|
391
|
+
+ 'to get the exact identifiers and caps.'
|
|
392
|
+
);
|
|
305
393
|
}
|
|
306
394
|
|
|
307
395
|
/* ------------------------------------------------------------------ */
|
package/src/tools/generate.js
CHANGED
|
@@ -42,9 +42,20 @@ const CINEMATIC_SCHEMA = z.object({
|
|
|
42
42
|
// The manual-control twin of generate_creative_director: no orchestration pass,
|
|
43
43
|
// the user's exact prompts verbatim. Submit failures never sink the batch —
|
|
44
44
|
// successful ids proceed, failed prompts are reported alongside.
|
|
45
|
+
// Over the cap is a hard rejection, never a truncation: this used to
|
|
46
|
+
// `.slice(0, MAX_BATCH_PROMPTS)`, so a 9-prompt call silently generated 8 and
|
|
47
|
+
// the caller had no way to know which prompt vanished. `promptsField` also caps
|
|
48
|
+
// the array in the schema (so hosts see `maxItems` before calling); the guard
|
|
49
|
+
// below is the choke point EVERY batch tool routes through, and names the count.
|
|
45
50
|
const MAX_BATCH_PROMPTS = 8;
|
|
46
51
|
async function submitBatch(rawPrompts, submitOne) {
|
|
47
|
-
|
|
52
|
+
if (rawPrompts.length > MAX_BATCH_PROMPTS) {
|
|
53
|
+
throw new Error(
|
|
54
|
+
`Too many prompts: ${rawPrompts.length} received, max ${MAX_BATCH_PROMPTS} per call. ` +
|
|
55
|
+
`Split them across ${Math.ceil(rawPrompts.length / MAX_BATCH_PROMPTS)} calls of at most ${MAX_BATCH_PROMPTS}.`
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
const prompts = rawPrompts.map((s) => String(s).trim()).filter(Boolean);
|
|
48
59
|
const settled = await Promise.allSettled(prompts.map((p) => submitOne(p)));
|
|
49
60
|
const ok = [], failed = [];
|
|
50
61
|
settled.forEach((s, i) => {
|
|
@@ -91,8 +102,8 @@ const imageSettings = (a = {}) => ({
|
|
|
91
102
|
cinematic: a.cinematic ? true : undefined,
|
|
92
103
|
});
|
|
93
104
|
|
|
94
|
-
const promptsField = (what) => z.array(z.string()).optional().describe(
|
|
95
|
-
`BATCH MODE — several DIFFERENT prompts (2–${MAX_BATCH_PROMPTS}) generated concurrently in ONE call and rendered together in ONE combined widget. Whenever the user wants multiple distinct ${what} with their own prompts, ALWAYS pass them all here instead of making several separate calls — separate calls clutter the chat with stacked widgets. All prompts share the same model/settings. When set, \`prompt\` is ignored. For N variations of a SINGLE prompt use num_images (image tools); for an AI-planned coherent scene set use generate_creative_director.`
|
|
105
|
+
const promptsField = (what) => z.array(z.string()).max(MAX_BATCH_PROMPTS).optional().describe(
|
|
106
|
+
`BATCH MODE — several DIFFERENT prompts (2–${MAX_BATCH_PROMPTS}) generated concurrently in ONE call and rendered together in ONE combined widget. **Hard cap: ${MAX_BATCH_PROMPTS} prompts per call — more than that is REJECTED with an error (never silently truncated), so split a longer list across several calls of at most ${MAX_BATCH_PROMPTS}.** Whenever the user wants multiple distinct ${what} with their own prompts, ALWAYS pass them all here instead of making several separate calls — separate calls clutter the chat with stacked widgets. All prompts share the same model/settings. When set, \`prompt\` is ignored. For N variations of a SINGLE prompt use num_images (image tools); for an AI-planned coherent scene set use generate_creative_director.`
|
|
96
107
|
);
|
|
97
108
|
|
|
98
109
|
function registerGenerateTools(server, client, options = {}) {
|
|
@@ -128,7 +139,7 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
128
139
|
},
|
|
129
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 }) => {
|
|
130
141
|
if (!prompt && !(prompts && prompts.length)) throw new Error('Provide prompt or prompts');
|
|
131
|
-
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")
|
|
132
143
|
const shared = {
|
|
133
144
|
model, aspect_ratio, enhance_prompt,
|
|
134
145
|
reference_images, visual_dna_ids, moodboard_id, enable_web_search, resolution, quality, preset_id, cinematic, skip_color_palette, project_id
|
|
@@ -199,7 +210,7 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
199
210
|
project_id: projectIdField
|
|
200
211
|
},
|
|
201
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 }) => {
|
|
202
|
-
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")
|
|
203
214
|
const gen = await client.post('/v1/generate/image-edit', {
|
|
204
215
|
prompt, model, source_images, aspect_ratio, enhance_prompt, num_images,
|
|
205
216
|
visual_dna_ids, moodboard_id, enable_web_search, resolution, cinematic, skip_color_palette, project_id
|
|
@@ -259,7 +270,7 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
259
270
|
project_id: projectIdField
|
|
260
271
|
},
|
|
261
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 }) => {
|
|
262
|
-
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")
|
|
263
274
|
const gen = await client.post('/v1/generate/creative-director', {
|
|
264
275
|
prompt, scene_count, model, aspect_ratio, workflow_type, duration,
|
|
265
276
|
enhance_prompt, reference_images, visual_dna_ids, moodboard_id, moodboard_ids, resolution, project_id
|
|
@@ -412,7 +423,7 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
412
423
|
},
|
|
413
424
|
async ({ prompt, prompts, model, aspect_ratio, duration, enhance_prompt = false, reference_images, resolution, preset_id, sound_enabled, skip_color_palette, project_id }) => {
|
|
414
425
|
if (!prompt && !(prompts && prompts.length)) throw new Error('Provide prompt or prompts');
|
|
415
|
-
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")
|
|
416
427
|
const shared = {
|
|
417
428
|
model, aspect_ratio, duration, enhance_prompt, reference_images, resolution, preset_id, sound_enabled, skip_color_palette, project_id
|
|
418
429
|
};
|
|
@@ -484,7 +495,7 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
484
495
|
project_id: projectIdField
|
|
485
496
|
},
|
|
486
497
|
async ({ image_url, prompt, model, aspect_ratio, duration, enhance_prompt = false, visual_dna_ids, resolution, sound_enabled, skip_color_palette, project_id }) => {
|
|
487
|
-
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")
|
|
488
499
|
const gen = await client.post('/v1/generate/video/from-image', {
|
|
489
500
|
image_url, prompt, model, aspect_ratio, duration, enhance_prompt, visual_dna_ids, resolution, sound_enabled, skip_color_palette, project_id
|
|
490
501
|
});
|
|
@@ -547,7 +558,7 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
547
558
|
project_id: projectIdField
|
|
548
559
|
},
|
|
549
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 }) => {
|
|
550
|
-
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")
|
|
551
562
|
const gen = await client.post('/v1/generate/music', {
|
|
552
563
|
prompt, model, style, title, instrumental, lyrics, vocal_gender, negative_tags,
|
|
553
564
|
duration_seconds, enhance_prompt, preset_id,
|
|
@@ -619,7 +630,7 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
619
630
|
project_id: projectIdField
|
|
620
631
|
},
|
|
621
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 }) => {
|
|
622
|
-
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")
|
|
623
634
|
// Resolve the requested voice against the REAL catalog (cached) so the card
|
|
624
635
|
// can show its display name + portrait instead of a raw id, and so an id
|
|
625
636
|
// that does not exist is reported instead of rendering silently: Google
|
|
@@ -693,7 +704,7 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
693
704
|
project_id: projectIdField
|
|
694
705
|
},
|
|
695
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 }) => {
|
|
696
|
-
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")
|
|
697
708
|
const gen = await client.post('/v1/generate/sound', {
|
|
698
709
|
prompt, model, duration, prompt_influence,
|
|
699
710
|
cfg_strength, sound_loop, sound_tempo, sound_key,
|
|
@@ -909,7 +920,7 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
909
920
|
project_id: projectIdField
|
|
910
921
|
},
|
|
911
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 }) => {
|
|
912
|
-
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")
|
|
913
924
|
if (!prompt) throw new Error('prompt is required');
|
|
914
925
|
|
|
915
926
|
let startResponse;
|
|
@@ -990,7 +1001,7 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
990
1001
|
project_id: projectIdField
|
|
991
1002
|
},
|
|
992
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 }) => {
|
|
993
|
-
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")
|
|
994
1005
|
const urlMode = first_frame_url && last_frame_url;
|
|
995
1006
|
const fileMode = first_frame && last_frame;
|
|
996
1007
|
if (!urlMode && !fileMode) {
|
|
@@ -1080,7 +1091,7 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
1080
1091
|
project_id: projectIdField
|
|
1081
1092
|
},
|
|
1082
1093
|
async ({ source, audio, text_prompt, model, bounding_box_target, sync_mode, model_mode, emotion, temperature, occlusion_detection_enabled, active_speaker_detection, project_id }) => {
|
|
1083
|
-
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")
|
|
1084
1095
|
if (!source) throw new Error('source is required (URL or absolute local path to image/video)');
|
|
1085
1096
|
if (!audio) throw new Error('audio is required (URL or absolute local path to audio file)');
|
|
1086
1097
|
|
|
@@ -1201,7 +1212,7 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
1201
1212
|
project_id: projectIdField
|
|
1202
1213
|
},
|
|
1203
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 }) => {
|
|
1204
|
-
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")
|
|
1205
1216
|
if (!source_video) throw new Error('source_video is required');
|
|
1206
1217
|
|
|
1207
1218
|
const isUrl = /^https?:\/\//i.test(source_video);
|
|
@@ -1360,7 +1371,7 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
1360
1371
|
project_id: projectIdField
|
|
1361
1372
|
},
|
|
1362
1373
|
async ({ prompt, reference_images, mode, texture_prompt, model, topology, target_polycount, enable_tpose, enable_pbr, project_id }) => {
|
|
1363
|
-
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")
|
|
1364
1375
|
if (!prompt && !(reference_images && reference_images.length > 0)) {
|
|
1365
1376
|
throw new Error('Provide prompt (text mode) or reference_images (single/multi mode)');
|
|
1366
1377
|
}
|
|
@@ -1503,6 +1514,9 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
1503
1514
|
zoom_out_percentage, expand_left, expand_right, expand_top, expand_bottom,
|
|
1504
1515
|
project_id
|
|
1505
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.
|
|
1506
1520
|
model = await canonicalModelId(client, model);
|
|
1507
1521
|
|
|
1508
1522
|
// Basic validation
|
|
@@ -1656,6 +1670,9 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
1656
1670
|
start_time,
|
|
1657
1671
|
project_id
|
|
1658
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.
|
|
1659
1676
|
model = await canonicalModelId(client, model);
|
|
1660
1677
|
|
|
1661
1678
|
// Validation
|
package/src/tools/models.js
CHANGED
|
@@ -83,15 +83,26 @@ function buildCatalogStructured(models, type, compact) {
|
|
|
83
83
|
};
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
// One row per model — every identifier, nothing else. ~90 bytes/model, so the
|
|
87
|
+
// whole 400+ model catalog fits in a payload an agent can actually read.
|
|
88
|
+
const identifierRow = (m) => ({
|
|
89
|
+
identifier: m.identifier,
|
|
90
|
+
name: m.name,
|
|
91
|
+
types: m.types,
|
|
92
|
+
credit: m.credit,
|
|
93
|
+
...(m.recommended ? { recommended: true } : {}),
|
|
94
|
+
...(m.new_model ? { new_model: true } : {}),
|
|
95
|
+
});
|
|
96
|
+
|
|
86
97
|
function registerModelTools(server, client, options = {}) {
|
|
87
98
|
const ui = () => appsEnabled(server, options);
|
|
88
99
|
// ─── list_models ───────────────────────────────────────────
|
|
89
100
|
server.tool(
|
|
90
101
|
'list_models',
|
|
91
|
-
'List available AI models on Kolbo. Filter by `type` to narrow to a generation type, and pass `format: "json"` to
|
|
102
|
+
'List available AI models on Kolbo. Filter by `type` to narrow to a generation type, and pass `format: "json"` to enumerate the catalog with exact identifiers — `format: "json"` + `type` returns the full raw model documents (every constraint field, for programmatic comparison / cap validation before submitting a generation); `format: "json"` alone returns a compact index of EVERY model and its identifier. Default `format: "text"` returns the human-readable summary. NEVER guess a model identifier: call this tool.',
|
|
92
103
|
{
|
|
93
104
|
type: z.string().optional().describe('Filter by DB type name: "text_to_img", "image_editing", "text_to_video", "img_to_video", "draw_to_video", "video_to_video", "elements", "firstlastgenerations", "lipsync-image", "lipsync-video", "music_gen", "text_to_speech", "text_to_sound", "stt", "text". Legacy aliases also accepted: "image", "image_edit", "video", "video_from_image", "video_from_video", "music", "speech", "sound", "chat", "lipsync" (both lipsync types), "three_d" (all 3D types), "first_last_frame", "transcription". Omit for all models.'),
|
|
94
|
-
format: z.enum(['text', 'json']).optional().describe('Output format. "text" (default) returns a human-readable summary with the most-used caps. "json" returns the raw model documents from the API
|
|
105
|
+
format: z.enum(['text', 'json']).optional().describe('Output format. "text" (default) returns a human-readable summary with the most-used caps. "json" is the source of truth for identifiers and caps: with `type` it returns the raw model documents from the API (identifier, credit, supported_durations, supported_resolutions, supported_aspect_ratios, max_reference_images, max_visual_dna, max_video_duration, …) for EVERY model of that type; without `type` it returns a compact index of every model in the catalog and its exact identifier. Use it whenever you need an identifier you have not seen listed, or must verify a cap before passing a value that might exceed a model-specific limit.'),
|
|
95
106
|
display_catalog: z.boolean().optional().describe('Set true when the USER explicitly asked to see/browse the available models — the visual catalog opens expanded. Leave unset for internal lookups (verifying a model name, checking caps before a generation): the catalog stays collapsed to a single row the user can tap to browse.')
|
|
96
107
|
},
|
|
97
108
|
async ({ type, format, display_catalog }) => {
|
|
@@ -106,14 +117,41 @@ function registerModelTools(server, client, options = {}) {
|
|
|
106
117
|
const path = type ? `/v1/models?type=${encodeURIComponent(type)}` : '/v1/models';
|
|
107
118
|
const result = await client.get(path);
|
|
108
119
|
|
|
109
|
-
//
|
|
110
|
-
//
|
|
111
|
-
//
|
|
112
|
-
//
|
|
120
|
+
// ⚠️ Hosts that mount this widget (claude.ai, Claude Code desktop) hand the
|
|
121
|
+
// MODEL `structuredContent` and DROP `content[].text`. So every payload the
|
|
122
|
+
// agent needs has to ride in structuredContent — shipping it as text only
|
|
123
|
+
// makes it invisible. That is exactly how `format: "json"` came to return
|
|
124
|
+
// the curated 6-per-group picker instead of the raw documents: v1.53.1
|
|
125
|
+
// (406a51e) flipped `if (ui() && showCatalog)` → `if (ui())` on all three
|
|
126
|
+
// return paths, so the widget payload started shadowing the real answer and
|
|
127
|
+
// the other 43 text_to_video identifiers became undiscoverable by any MCP
|
|
128
|
+
// call. On 2026-08-09 that cost a wrong-model generation (minimax-h3).
|
|
129
|
+
// `extra` (json mode) carries the data as structured fields; without it the
|
|
130
|
+
// full text payload is attached verbatim. The widget ignores both.
|
|
131
|
+
const respond = (text, extra) => (ui()
|
|
132
|
+
? uiResult(UI.catalog, text, {
|
|
133
|
+
...buildCatalogStructured(result.models, type, !showCatalog),
|
|
134
|
+
...(extra || { text }),
|
|
135
|
+
})
|
|
136
|
+
: { content: [{ type: 'text', text }] });
|
|
137
|
+
|
|
138
|
+
// JSON mode — the authoritative shape; every constraint the agent might
|
|
139
|
+
// need to validate a request lives here (durations, reference caps,
|
|
140
|
+
// audio/video min/max, resolution multipliers, supports_* flags,
|
|
141
|
+
// prompt-length limits, etc.).
|
|
113
142
|
if (format === 'json') {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
143
|
+
// Raw documents once `type` narrows the set (~49 docs for a video type).
|
|
144
|
+
// Unfiltered that is 400+ documents / hundreds of KB, so return the
|
|
145
|
+
// complete IDENTIFIER INDEX instead: every model stays enumerable and
|
|
146
|
+
// the full caps are one `type` away.
|
|
147
|
+
const payload = type
|
|
148
|
+
? { count: result.count, models: result.models }
|
|
149
|
+
: {
|
|
150
|
+
count: result.count,
|
|
151
|
+
models: result.models.map(identifierRow),
|
|
152
|
+
note: 'Compact index — every model in the catalog and its exact identifier. Re-call with `type` for the full documents (all caps, credit costs, supported_* fields).',
|
|
153
|
+
};
|
|
154
|
+
return respond(JSON.stringify(payload, null, 2), payload);
|
|
117
155
|
}
|
|
118
156
|
|
|
119
157
|
// Split into auto-selectable (has summary) and named-only (no summary)
|
|
@@ -306,9 +344,9 @@ function registerModelTools(server, client, options = {}) {
|
|
|
306
344
|
+ ' first_last_frame · elements · lipsync · music_gen · text_to_speech ·\n'
|
|
307
345
|
+ ' text_to_sound · stt · three_d · text\n\n'
|
|
308
346
|
+ 'Use the "identifier" value as the "model" parameter in generate tools. '
|
|
309
|
-
+ 'For
|
|
310
|
-
|
|
311
|
-
return
|
|
347
|
+
+ 'For EVERY model + its exact identifier, re-call with format: "json" (compact index of the '
|
|
348
|
+
+ 'whole catalog). Add `type` to that call for the full raw documents with all caps.';
|
|
349
|
+
return respond(text);
|
|
312
350
|
}
|
|
313
351
|
|
|
314
352
|
if (withSummary.length > 0) {
|
|
@@ -318,9 +356,8 @@ function registerModelTools(server, client, options = {}) {
|
|
|
318
356
|
sections.push(`Named-only models (${withoutSummary.length}) — only use if the user explicitly requests by name:\n${withoutSummary.map(formatModel).join('\n')}`);
|
|
319
357
|
}
|
|
320
358
|
|
|
321
|
-
const text = `Available ${type} models (${result.count}):\n\n${sections.join('\n\n')}\n\
|
|
322
|
-
|
|
323
|
-
return { content: [{ type: 'text', text }] };
|
|
359
|
+
const text = `Available ${type} models (${result.count}):\n\n${sections.join('\n\n')}\n\nEvery ${type} model in the catalog is listed above — both sections together are the complete set. Use the "identifier" value as the "model" parameter in generate tools. For the raw documents (programmatic cap validation), re-call with format: "json".`;
|
|
360
|
+
return respond(text);
|
|
324
361
|
}
|
|
325
362
|
);
|
|
326
363
|
|