@kolbo/mcp 1.84.0 → 1.84.2

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.84.0",
3
+ "version": "1.84.2",
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
@@ -88,6 +88,17 @@ const WIDGET_CSP = {
88
88
  'https://assets.sketchfab.com',
89
89
  'https://sketchfab-prod-media.s3.amazonaws.com',
90
90
 
91
+ // Voice PREVIEW audio hosts. list_voices ships every voice with a
92
+ // preview_url and the card renders a real <audio> for it, but 150 of the
93
+ // 864 production voices store that preview on a provider host rather than a
94
+ // Kolbo bucket: 138 google voices on storage.googleapis.com and 12 on
95
+ // api.us.elevenlabs.io. media-src blocked both, so every google voice — the
96
+ // entire Hebrew set — rendered a player stuck at 0:00 / 0:00 with no error
97
+ // anywhere. The Spaces-hosted previews come free via HOST_MAP above; these
98
+ // two do not, because they are not ours.
99
+ 'https://storage.googleapis.com',
100
+ 'https://api.us.elevenlabs.io',
101
+
91
102
  // Default SYNCI catalog project. Any production override must be reviewed
92
103
  // and added here as an exact hostname before deployment.
93
104
  'https://gfbpxdkripkbbrcvoyeh.supabase.co',
@@ -48,17 +48,17 @@ function boot(sc) {
48
48
  var audioItems = sc.items.filter(function (i) { return i.media_type === 'audio'; });
49
49
  var visualItems = sc.items.filter(function (i) { return i.media_type !== 'audio'; });
50
50
  var h = '';
51
+ // Render everything currently in state — each PAGE is already capped
52
+ // server-side (GRID_CAP), so this grows one page per Load more instead of
53
+ // being pinned forever. The old hard slices (24 visual / 12 audio) meant an
54
+ // appended page could never actually appear, and the count below tallied all
55
+ // items while only 36 were drawn, so the button also lied about the total.
51
56
  if (visualItems.length) {
52
- h += '<div class="k-grid">' + visualItems.slice(0, 24).map(cellHTML).join('') + '</div>';
57
+ h += '<div class="k-grid">' + visualItems.map(cellHTML).join('') + '</div>';
53
58
  }
54
59
  if (audioItems.length) {
55
- h += audioItems.slice(0, 12).map(audioRowHTML).join('');
60
+ h += audioItems.map(audioRowHTML).join('');
56
61
  }
57
- // Only a fixed page ever renders here (24 visual + 12 audio) — with a library
58
- // in the thousands there was no way to reach the rest short of asking in
59
- // chat. A visible "Load more" turns that into one click; it sends a message
60
- // rather than calling the tool directly, since the widget has no host API
61
- // to invoke a tool itself — same mechanism every other "Use" action here uses.
62
62
  var shown = visualItems.length + audioItems.length;
63
63
  if (sc.total != null && sc.total > shown) {
64
64
  h += '<button class="k-btn" id="load-more" style="width:100%;margin-top:10px">Load more (' +
@@ -145,13 +145,50 @@ function wire() {
145
145
  });
146
146
  var loadMore = el('load-more');
147
147
  if (loadMore) {
148
- loadMore.onclick = function () {
149
- window.kolbo.sendMessage('Show me the next page of this same media search (already saw ' +
150
- (state.items.length) + ' of ' + state.total + ' results).');
151
- };
148
+ loadMore.onclick = function () { fetchNextPage(loadMore); };
152
149
  }
153
150
  }
154
151
 
152
+ // Fetch the next page IN the widget and append it.
153
+ //
154
+ // This used to sendMessage() a request for "the next page of this same media
155
+ // search", on the stated belief that a widget has no way to invoke a tool. It
156
+ // does — window.kolbo.callTool, the same bridge call every generation card
157
+ // polls status with. And the message could not have worked anyway: the payload
158
+ // carried no page number and none of the filters, so the model had nothing to
159
+ // reconstruct the query from and would re-run page 1 or something else. The
160
+ // button appeared to do nothing.
161
+ function fetchNextPage(btn) {
162
+ if (!state || !state.page_tool || btn.disabled) return;
163
+ var next = (state.page || 1) + 1;
164
+ btn.disabled = true;
165
+ var label = btn.textContent;
166
+ btn.innerHTML = '<span class="k-spin"></span> Loading';
167
+ var args = {};
168
+ var q = state.query || {};
169
+ for (var k in q) { if (q[k] !== undefined && q[k] !== null && q[k] !== '') args[k] = q[k]; }
170
+ args.page = next;
171
+ if (state.page_size) args.page_size = state.page_size;
172
+
173
+ window.kolbo.callTool(state.page_tool, args).then(function (res) {
174
+ var sc = structured(res);
175
+ var more = (sc && sc.items) || [];
176
+ if (!more.length) {
177
+ // Nothing came back: say so rather than restoring a button that still
178
+ // looks like it has pages behind it.
179
+ btn.textContent = 'No more results';
180
+ return;
181
+ }
182
+ state.items = (state.items || []).concat(more);
183
+ state.page = (sc && sc.page) || next;
184
+ if (sc && sc.total != null) state.total = sc.total;
185
+ boot(state); // re-renders the grid + a fresh Load more button
186
+ }).catch(function () {
187
+ btn.disabled = false;
188
+ btn.textContent = label;
189
+ });
190
+ }
191
+
155
192
  function useItem(i) {
156
193
  var item = state.items[i];
157
194
  if (!item || !item.id) return;
@@ -851,6 +851,18 @@ async function uiCompleted(p, textPayload, extraContent) {
851
851
  // above which assume everything finished together. Only set when the
852
852
  // caller actually has this shape; every existing caller is unaffected.
853
853
  ...(Array.isArray(p.items) ? { items: p.items } : {}),
854
+ // Transcription payload. get_generation_status is the ONLY way the live
855
+ // transcript widget learns its result, and it reads text/srt_url/txt_url off
856
+ // this object — but uiCompleted is shaped for the generation card and
857
+ // dropped every one, so a finished transcription rendered "(empty
858
+ // transcript)" with no SRT/TXT buttons while the text sat in the status
859
+ // response. structuredContent SHADOWS the text block on widget hosts, so
860
+ // omitting a field here is the same as deleting it.
861
+ ...(typeof p.text === 'string' ? { text: p.text } : {}),
862
+ ...(p.srt_url ? { srt_url: p.srt_url } : {}),
863
+ ...(p.word_by_word_srt_url ? { word_by_word_srt_url: p.word_by_word_srt_url } : {}),
864
+ ...(p.txt_url ? { txt_url: p.txt_url } : {}),
865
+ ...(p.audio_url ? { audio_url: p.audio_url } : {}),
854
866
  // The voice, by name and portrait. uiGenerating has carried this since the
855
867
  // chips were introduced; uiCompleted never did, so it silently dropped a
856
868
  // resolved voice its caller had already looked up — every FINISHED speech
@@ -1118,6 +1118,14 @@ function registerGenerateTools(server, client, options = {}) {
1118
1118
  state: single.state,
1119
1119
  urls: done ? urls : undefined,
1120
1120
  thumbnail_url: res.thumbnail_url,
1121
+ // Transcription results ride the same status tool as media
1122
+ // generations; without these the transcript widget merges a payload
1123
+ // with no transcript in it and renders "(empty transcript)".
1124
+ text: typeof res.text === 'string' ? res.text : undefined,
1125
+ srt_url: res.srt_url || undefined,
1126
+ word_by_word_srt_url: res.word_by_word_srt_url || undefined,
1127
+ txt_url: res.txt_url || undefined,
1128
+ audio_url: res.audio_url || undefined,
1121
1129
  // The refs the server actually conditioned on (reference_details) —
1122
1130
  // the live card merges this payload over its submit-time state, so
1123
1131
  // the finished card shows every reference, including server-side
@@ -303,7 +303,17 @@ function registerMediaTools(server, client, options = {}) {
303
303
  title: 'Media Library',
304
304
  items,
305
305
  total: totalItems != null ? totalItems : media.length,
306
- shown: Math.min(media.length, GRID_CAP)
306
+ shown: Math.min(media.length, GRID_CAP),
307
+ // Everything "Load more" needs to fetch page N+1 ITSELF. The button used
308
+ // to send a chat message asking the model to run the next page, on the
309
+ // belief that a widget cannot invoke a tool — it can
310
+ // (window.kolbo.callTool, the same call every generation card polls
311
+ // with). Worse, the payload carried no page and no filters, so the model
312
+ // could not reconstruct the query either and typically re-ran page 1.
313
+ page_tool: 'list_media',
314
+ page: page || 1,
315
+ page_size: page_size || 50,
316
+ query: { project_id, folder_id, type, category, source_type, sort, search }
307
317
  });
308
318
  }
309
319
  );
@@ -28,6 +28,7 @@ function registerVisualDnaTools(server, client, options = {}) {
28
28
  name: z.string().describe('Name of the Visual DNA profile. **Pick a short, lowercase, no-space single token** (e.g. `maya`, `tokyo_neon`, `brand_red`, `esther_model`) — never names with spaces (`Sarah Johnson` ❌). The user/LLM types this as `@<name>` inside generation prompts, and the @ parser stops at the first space, so `@Sarah Johnson` matches only `Sarah` and the binding silently drops. Multi-word concepts should use underscores or be a single token. Names are case-insensitive on lookup, but **reserved** values rejected on creation: `Image1`, `Image2`, …, `Video1`, …, `Audio1`, … (any-language characters allowed; max 100 chars).'),
29
29
  dna_type: z.string().optional().describe('Type: "character", "style", "product", "scene", "environment". Default: "character"'),
30
30
  prompt_helper: z.string().optional().describe('Optional description/notes to guide DNA extraction'),
31
+ description: z.string().optional().describe('Alias for `prompt_helper`. Accepted because every tool here REPORTS this field as `description`, so callers round-trip that name back in; without the alias zod stripped it and the notes were silently lost. Ignored when `prompt_helper` is also given.'),
31
32
  images: z.array(z.string()).optional().describe('Array of image sources (URLs or absolute local paths). Max 4. All of them can reach the model on later gens — same subject, same vibe only; no extra heroes on character DNAs, no main characters on environment DNAs.'),
32
33
  video: z.string().optional().describe('Optional video source (URL or absolute local path)'),
33
34
  audio: z.string().optional().describe('Optional audio source (URL or absolute local path) — the character\'s voice, 5-30s of clean speech. Stored on the DNA and used two ways: (1) as REFERENCE AUDIO in video generation — attaching this DNA to an image-to-video generation on a model with audio slots (Seedance 2.x, Wan 3.0) auto-attaches the clip and tells the model it is that character\'s voice; (2) as the source for a real speaking voice, but ONLY when you ask for one — see `voice_source`.'),
@@ -35,7 +36,8 @@ function registerVisualDnaTools(server, client, options = {}) {
35
36
  assigned_voice_id: z.string().optional().describe('Voice to attach when voice_source="assign" — a `custom_<id>` from the user\'s clones or a voice_id from `list_voices`.'),
36
37
  character_sheet_url: z.string().optional().describe('URL of a reference sheet (from `generate_character_sheet`, any sheet_type) to set as the DNA\'s primary reference. Works for ALL DNA types — character turnaround, product detail sheet, location sheet, or style board — and is the single biggest consistency booster. Omit only when the user declines.')
37
38
  },
38
- async ({ name, dna_type, prompt_helper, images, video, audio, voice_source, assigned_voice_id, character_sheet_url }) => {
39
+ async ({ name, dna_type, prompt_helper, description, images, video, audio, voice_source, assigned_voice_id, character_sheet_url }) => {
40
+ const helper = prompt_helper !== undefined ? prompt_helper : description;
39
41
  if (!name || !name.trim()) {
40
42
  throw new Error('name is required');
41
43
  }
@@ -58,7 +60,7 @@ function registerVisualDnaTools(server, client, options = {}) {
58
60
  const form = new FormData();
59
61
  form.append('name', name);
60
62
  if (dna_type) form.append('dnaType', dna_type);
61
- if (prompt_helper) form.append('promptHelper', prompt_helper);
63
+ if (helper) form.append('promptHelper', helper);
62
64
  if (character_sheet_url) form.append('characterSheetUrl', character_sheet_url);
63
65
  // Omitted stays omitted: the server infers 'clone' from a present audio clip, which is the
64
66
  // long-standing behaviour older installs depend on. Only an explicit choice is forwarded.
@@ -193,6 +195,7 @@ function registerVisualDnaTools(server, client, options = {}) {
193
195
  name: z.string().optional().describe('New name. Same no-space single-token rule as create_visual_dna — @Name binding stops at the first space.'),
194
196
  dna_type: z.string().optional().describe('Type: "character", "style", "product", "scene", "environment". Changing type re-analyzes the profile.'),
195
197
  prompt_helper: z.string().optional().describe('New description / intent notes. Replaces the old ones and re-synthesizes the DNA analysis when the text actually changes. Pass "" to clear.'),
198
+ description: z.string().optional().describe('Alias for `prompt_helper`. Accepted because every tool here REPORTS this field as `description`, so callers round-trip that name back in; without the alias zod stripped it and the edit silently did nothing. Ignored when `prompt_helper` is also given.'),
196
199
  images: z.array(z.string()).optional().describe('Full replacement still set (URLs or absolute local paths). Max 4. Omit to keep current stills. Same purity rules as create: one identity, one vibe.'),
197
200
  video: z.string().optional().describe('Replacement video source (URL or absolute local path).'),
198
201
  audio: z.string().optional().describe('Replacement audio source (URL or absolute local path).'),
@@ -208,14 +211,15 @@ function registerVisualDnaTools(server, client, options = {}) {
208
211
  specific_age: z.number().optional().describe('Character attribute (character DNAs).')
209
212
  },
210
213
  async ({
211
- visual_dna_id, name, dna_type, prompt_helper, images, video, audio,
214
+ visual_dna_id, name, dna_type, prompt_helper, description, images, video, audio,
212
215
  character_sheet_url, remove_character_sheet,
213
216
  gender, ethnicity, body_type, hair_color, eye_color, skin_tone, age_range, specific_age
214
217
  }) => {
218
+ const helper = prompt_helper !== undefined ? prompt_helper : description;
215
219
  const imageList = Array.isArray(images) ? images.filter(Boolean) : [];
216
220
  if (imageList.length > 4) throw new Error('Maximum 4 images allowed');
217
221
  const hasMedia = imageList.length > 0 || !!video || !!audio;
218
- const hasMeta = name !== undefined || dna_type !== undefined || prompt_helper !== undefined
222
+ const hasMeta = name !== undefined || dna_type !== undefined || helper !== undefined
219
223
  || character_sheet_url !== undefined || remove_character_sheet !== undefined
220
224
  || gender !== undefined || ethnicity !== undefined || body_type !== undefined
221
225
  || hair_color !== undefined || eye_color !== undefined || skin_tone !== undefined
@@ -240,7 +244,7 @@ function registerVisualDnaTools(server, client, options = {}) {
240
244
  const body = { ...attrs };
241
245
  if (name !== undefined) body.name = name;
242
246
  if (dna_type !== undefined) body.dna_type = dna_type;
243
- if (prompt_helper !== undefined) body.prompt_helper = prompt_helper;
247
+ if (helper !== undefined) body.prompt_helper = helper;
244
248
  if (character_sheet_url !== undefined) body.character_sheet_url = character_sheet_url;
245
249
  if (remove_character_sheet !== undefined) body.remove_character_sheet = remove_character_sheet;
246
250
  const result = await client.put(path, body);
@@ -264,7 +268,7 @@ function registerVisualDnaTools(server, client, options = {}) {
264
268
  const form = new FormData();
265
269
  if (name !== undefined) form.append('name', name);
266
270
  if (dna_type) form.append('dnaType', dna_type);
267
- if (prompt_helper !== undefined) form.append('promptHelper', prompt_helper);
271
+ if (helper !== undefined) form.append('promptHelper', helper);
268
272
  if (character_sheet_url) form.append('characterSheetUrl', character_sheet_url);
269
273
  if (remove_character_sheet !== undefined) {
270
274
  form.append('removeCharacterSheet', remove_character_sheet ? 'true' : 'false');