@kolbo/mcp 1.79.6 → 1.80.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.
@@ -12,55 +12,56 @@ function registerPresetTools(server, client, options = {}) {
12
12
  // ─── list_presets ──────────────────────────────────────────
13
13
  server.tool(
14
14
  'list_presets',
15
- 'List generation presets across image, image-editing, video, music, and text-to-video catalogs. Use this BEFORE generating whenever the user requests a preset, names a preset, or asks to use one of their/Kolbo presets. Resolve the requested name or choose the closest match from the correct `type`, then pass the returned exact `id` as `preset_id` on the generation tool. Never claim a preset was used unless that id is passed. Returns id, name, description, thumbnail, category, and (for music) audio preview URL.',
15
+ 'Resolve a generation preset and pass its exact id as preset_id. ALWAYS pass `search` when you already know the name (headless, bible, character sheet, a user preset) that is a silent id lookup, not a catalog to show the user. Omit search only when they asked to browse. Custom instructions live on the preset, so prefer generate_image + preset_id over generate_character_sheet. Never invent an id.',
16
16
  {
17
- type: z.string().optional().describe('Filter by catalog: "image" | "image_edit" | "video" | "music" | "text_to_video". Omit for all.')
17
+ type: z.string().optional().describe('Filter by catalog: "image" | "image_edit" | "video" | "music" | "text_to_video". Omit for all.'),
18
+ search: z.string().optional().describe('Name lookup, e.g. "headless", "bible", "character sheet". Required when resolving a named sheet/style — do not list the whole catalog.')
18
19
  },
19
- async ({ type }) => {
20
+ async ({ type, search }) => {
20
21
  const params = new URLSearchParams();
21
22
  if (type) params.set('type', type);
23
+ if (search) params.set('search', search);
22
24
  const qs = params.toString();
23
25
  const result = await client.get(`/v1/presets${qs ? '?' + qs : ''}`);
24
26
 
25
- const presets = result.presets || [];
26
- // Full catalog measured 632,919 chars — the single biggest payload in the
27
- // tool surface. The model only needs enough to pick a preset_id.
27
+ let presets = result.presets || [];
28
+ if (search && !result.search) {
29
+ const q = search.toLowerCase();
30
+ presets = presets.filter((p) => `${p.name || ''} ${p.description || ''} ${p.category || ''}`.toLowerCase().includes(q));
31
+ }
32
+ const lookup = Boolean(search);
33
+ // Full catalog measured 632,919 chars. A named lookup should return a handful of rows.
28
34
  const text = compactList(presets, {
29
35
  fields: ['id', 'name', 'category', 'type', 'description'],
30
- cap: 60,
36
+ cap: lookup ? 8 : 12,
31
37
  total: result.count || presets.length,
32
- extra: result.warning ? { warning: result.warning } : undefined,
33
- note: 'Filter with `type` (image | image_edit | video | music | text_to_video) to see a focused set. Pass the chosen exact id as `preset_id` on the next generation call.',
38
+ extra: {
39
+ ...(result.warning ? { warning: result.warning } : {}),
40
+ ...(lookup ? { _lookup: true } : {}),
41
+ },
42
+ note: lookup
43
+ ? 'Pick the closest id and pass it as preset_id. Do not show this list to the user.'
44
+ : 'Pass search next time if you already know the name. Pass the chosen exact id as preset_id.',
34
45
  });
35
46
 
36
- // Ship structuredContent UNCONDITIONALLY. Gating on ui() left every host that
37
- // renders widgets without advertising MCP Apps (Kolbo Code) with text-only rows
38
- // that carry no thumbnail field at all — and its BY_TOOL map still force-mounts
39
- // the media grid on them, so the card rendered one broken-file glyph per cell.
40
- // media.js and listResult() have always done it this way; these five lagged.
41
- {
42
- return uiResult(UI.mediaGrid, text, {
43
- widget: 'media-grid',
44
- title: 'Presets' + (type ? ' — ' + type : ''),
45
- items: presets.slice(0, 24).map(p => ({
46
- id: p.id,
47
- title: p.name,
48
- subtitle: p.category,
49
- // API returns thumbnail_url / audio_url (see sdk listPresets) — NOT
50
- // thumbnail / audio_preview_url. Reading the wrong key rendered every
51
- // preset as a blank tile and dropped music previews entirely.
52
- thumbnail: p.thumbnail_url || p.thumbnail,
53
- media_type: p.audio_url ? 'audio' : 'image',
54
- preview_audio: p.audio_url,
55
- url: p.thumbnail_url || p.thumbnail,
56
- use_hint: 'Use preset "{TITLE}" (preset_id: {ID}) for my next generation — ask me for the prompt.'
57
- })),
58
- total: result.count || presets.length,
59
- has_more: presets.length > 24
60
- });
61
- }
47
+ if (lookup) return { content: [{ type: 'text', text }] };
62
48
 
63
- return { content: [{ type: 'text', text }] };
49
+ return uiResult(UI.list, text, {
50
+ widget: 'list',
51
+ title: 'Presets' + (type ? ' — ' + type : ''),
52
+ items: presets.slice(0, 8).map(p => ({
53
+ id: p.id,
54
+ title: p.name,
55
+ subtitle: p.category,
56
+ thumbnail: p.thumbnail_url || p.thumbnail,
57
+ media_type: p.audio_url ? 'audio' : 'image',
58
+ preview_audio: p.audio_url,
59
+ url: p.thumbnail_url || p.thumbnail,
60
+ use_hint: 'Use preset "{TITLE}" (preset_id: {ID}) for my next generation — ask me for the prompt.'
61
+ })),
62
+ total: result.count || presets.length,
63
+ has_more: presets.length > 8
64
+ });
64
65
  }
65
66
  );
66
67
 
@@ -153,7 +153,8 @@ function registerVisualDnaTools(server, client, options = {}) {
153
153
  id: d.id,
154
154
  title: d.name,
155
155
  subtitle: (d.dna_type || '') + (Array.isArray(d.tags) && d.tags.length ? ' · ' + d.tags.slice(0, 3).join(', ') : ''),
156
- thumbnail: d.thumbnail_url || d.thumbnail,
156
+ thumbnail: d.sheet_url || d.thumbnail_url || d.thumbnail,
157
+ url: d.sheet_url || d.thumbnail_url || d.thumbnail,
157
158
  media_type: 'image',
158
159
  use_hint: 'Use Visual DNA "{TITLE}" (id: {ID}) in my next generation for character/style consistency.'
159
160
  })),
@@ -210,15 +211,22 @@ function registerVisualDnaTools(server, client, options = {}) {
210
211
  'STANDARD FIRST STEP OF THE ASSET PASS for any film/ad/scene: inventory the characters, locations and props the script needs, generate a sheet for each, create its Visual DNA from that sheet, confirm the whole set with the user, and only THEN generate video. Sheets for cinematic environments and invented characters run well on `mirage-film-2` (3cr); use `nano-banana-2` (10cr) or `gpt-image-2` (12cr) when reference fidelity or legible text matters. Generate a reference sheet for a Visual DNA from 1+ reference image URLs — the same step the in-app Visual DNA wizard offers, for EVERY DNA type via `sheet_type`: character = multi-angle turnaround, product = angles + branding/material/construction close-ups, environment = location angles + one signature detail, style = a style board (the same look applied to six varied subjects). The sheet is the single strongest consistency booster for a DNA, and it always preserves the reference\'s original art style (2D stays 2D, photo stays photo). CHARGES CREDITS, so when the user is about to create a DNA, OFFER this first ("want me to generate a reference sheet for stronger consistency? it costs a few credits") and only run it on a yes. Returns `character_sheet_url` — pass it as `character_sheet_url` to `create_visual_dna` with the matching `dna_type`.',
211
212
  {
212
213
  image_urls: z.array(z.string()).min(1).describe('Reference image URLs of the subject (for characters: front/side/varied angles work best). Use generated-image URLs or upload_media output.'),
213
- sheet_type: z.enum(['character', 'character_headless', 'character_bible', 'product', 'environment', 'style']).optional().describe('Sheet layout. character = front/back/face turnaround. character_headless = wardrobe/body refs with a headless front panel (use when clothing must change without fighting the face sheet). character_bible = denser production model-sheet (turnaround + faces + wardrobe + color swatches). product / environment / style = matching DNA types. Defaults to character.')
214
+ sheet_type: z.enum(['character', 'character_headless', 'character_bible', 'product', 'environment', 'style']).optional().describe('Sheet layout. character = front/back/face turnaround. character_headless = wardrobe/body refs with a headless front panel (use when clothing must change without fighting the face sheet). character_bible = denser production model-sheet (turnaround + faces + wardrobe + color swatches). product / environment / style = matching DNA types. Defaults to character. This IS the Character Sheet / Headless / Bible preset — do not call list_presets for those names.'),
215
+ model: z.string().optional().describe('Image model for the sheet. Default nano-banana-2. Pass gpt-image-2 when the user names it.'),
216
+ resolution: z.enum(['2K', '4K']).optional().describe('Sheet resolution. 2K or 4K only — never 1K. Default 2K. Use 4K for character_bible, high-detail leads, or when the user asks.')
214
217
  },
215
- async ({ image_urls, sheet_type }) => {
218
+ async ({ image_urls, sheet_type, model, resolution }) => {
216
219
  // The endpoint is blocking and a 2K multi-panel sheet routinely runs past the
217
220
  // 120s default: the MCP aborted while kolbo-api kept going, finished, and
218
221
  // billed — the user saw "Failed" for a sheet they had already paid for.
219
222
  const result = await client.post(
220
223
  '/v1/visual-dna/character-sheet',
221
- { image_urls, ...(sheet_type ? { sheet_type } : {}) },
224
+ {
225
+ image_urls,
226
+ ...(sheet_type ? { sheet_type } : {}),
227
+ ...(model ? { model } : {}),
228
+ ...(resolution ? { resolution } : {}),
229
+ },
222
230
  { timeoutMs: CHARACTER_SHEET_TIMEOUT_MS },
223
231
  );
224
232
  // `urls` is NOT redundant with character_sheet_url. Every generation-card