@kolbo/mcp 1.50.1 → 1.51.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/README.md CHANGED
@@ -113,7 +113,7 @@ Just ask your agent naturally:
113
113
 
114
114
  Without the optional skill, the config block alone already exposes every tool — you just describe what you want. With the skill installed, each of these is also routed to the right MCP tool with the right defaults — UGC mode picks 9:16 + sound-off + no-captions, marketplace mode enforces compliance (pure white bg, no text, no props), product photoshoot mode uses the right aspect for the platform (2:3 Pinterest, 16:9 hero banner, 1:1 IG feed), etc. The routing logic is shared with [Kolbo Code](https://github.com/Zoharvan12/kolbo-code), so the behavior is identical however you connect.
115
115
 
116
- ## Available Tools (95)
116
+ ## Available Tools (101)
117
117
 
118
118
  **Generation**
119
119
  | Tool | Description |
@@ -160,6 +160,17 @@ Every generation tool also accepts an optional `project_id` arg that routes the
160
160
  | `list_moodboards` | Browse presets + your moodboards |
161
161
  | `get_moodboard` | Fetch one moodboard with all image URLs |
162
162
 
163
+ **Color DNA** — sticky, account-wide: the ACTIVE palette strict-grades every generation until deactivated. Opt a single generation out with `skip_color_palette`.
164
+ | Tool | Description |
165
+ |------|-------------|
166
+ | `list_color_palettes` | List your palettes (+ org) |
167
+ | `analyze_color_palette` | Extract colors from 1-5 image URLs (free, does not save) |
168
+ | `create_color_palette` | Save a palette (colors from analyze or manual); auto-activates by default |
169
+ | `update_color_palette` | Rename / replace colors |
170
+ | `delete_color_palette` | Delete a palette |
171
+ | `activate_color_palette` | Make a palette the sticky active one |
172
+ | `deactivate_color_palette` | Clear the active palette |
173
+
163
174
  **Media Library**
164
175
  | Tool | Description |
165
176
  |------|-------------|
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kolbo/mcp",
3
- "version": "1.50.1",
3
+ "version": "1.51.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": {
@@ -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"
13
+ "prepublishOnly": "node scripts/smoke.js && node scripts/check-parity.js && node scripts/check-widget-fields.js",
14
+ "check-widget-fields": "node scripts/check-widget-fields.js"
14
15
  },
15
16
  "keywords": [
16
17
  "kolbo",
@@ -121,18 +121,84 @@ function renderGenerating(sc) {
121
121
  (i === 0 ? '<span class="k-gen-badge"><span class="k-spin"></span>Generating</span>' : '') + '</div>';
122
122
  }
123
123
  el('stage').innerHTML = '<div class="k-gen-grid n' + n + '">' + cells + '</div>';
124
- el('actions').innerHTML = '';
124
+ renderStopButton(sc);
125
125
  schedulePoll(sc);
126
126
  }
127
127
 
128
+ /* ---------- cancel ----------
129
+ Shorts run on their own job collection and have their own cancel tool;
130
+ everything else is SdkGeneration-tracked and goes through cancel_generation.
131
+ Returns null when the card has no id to cancel (nothing to offer). */
132
+ function cancelSpec(sc) {
133
+ if (sc.poll_tool === 'shorts_status') {
134
+ var jobId = (sc.status_args && sc.status_args.job_id) || sc.generation_id;
135
+ return jobId ? { tool: 'shorts_cancel', args: { job_id: jobId } } : null;
136
+ }
137
+ if (!sc.generation_id) return null;
138
+ return { tool: 'cancel_generation', args: { generation_id: sc.generation_id } };
139
+ }
140
+
141
+ function renderStopButton(sc) {
142
+ var spec = cancelSpec(sc);
143
+ if (!spec) { el('actions').innerHTML = ''; return; }
144
+ el('actions').innerHTML = '<button class="k-btn ghost" id="stop-btn">' + ICONS.x + ' Stop</button>';
145
+ el('stop-btn').onclick = function () {
146
+ var btn = el('stop-btn');
147
+ btn.disabled = true;
148
+ btn.innerHTML = '<span class="k-spin"></span> Stopping';
149
+ // Stop polling immediately so a long-wait status call that lands mid-cancel
150
+ // cannot repaint the card back into "Generating".
151
+ cancelRequested = true;
152
+ clearTimeout(pollTimer);
153
+ window.kolbo.callTool(spec.tool, spec.args).then(function (res) {
154
+ var st = structured(res) || {};
155
+ if (st.cancelled === false) {
156
+ // Already terminal — let the normal poll path report the real outcome
157
+ // instead of claiming a cancel that did not happen.
158
+ cancelRequested = false;
159
+ btn.disabled = false;
160
+ btn.innerHTML = ICONS.x + ' Stop';
161
+ return schedulePoll(sc);
162
+ }
163
+ renderCancelled(st.credits_refunded);
164
+ }).catch(function (e) {
165
+ cancelRequested = false;
166
+ btn.disabled = false;
167
+ btn.innerHTML = ICONS.x + ' Stop';
168
+ schedulePoll(sc);
169
+ });
170
+ };
171
+ }
172
+
173
+ function renderCancelled(creditsRefunded) {
174
+ clearTimeout(pollTimer);
175
+ setPhaseChip('Cancelled', false);
176
+ var note = creditsRefunded ? ' ' + creditsRefunded + ' credits refunded.' : '';
177
+ el('stage').innerHTML = '<div class="k-error">' + ICONS.x + ' Generation cancelled.' + esc(note) + '</div>';
178
+ el('actions').innerHTML = '<button class="k-btn" id="retry-btn">' + ICONS.retry + ' Try Again</button>';
179
+ el('retry-btn').onclick = function () {
180
+ var what = (state && TOOL_TITLES[state.tool]) || 'generation';
181
+ window.kolbo.sendMessage('Run that ' + what.toLowerCase() + ' again — I cancelled the previous one.');
182
+ };
183
+ // Tell the model, so it does not go on to report the generation as running.
184
+ try {
185
+ window.kolbo.updateModelContext('The user cancelled generation ' +
186
+ ((state && state.generation_id) || '') + '.' + note +
187
+ ' Do not poll it or report it as in progress.');
188
+ } catch (e) {}
189
+ window.kolbo.notifySize();
190
+ }
191
+
128
192
  // Poll ceilings so the card never spins forever. Losing widget-side tracking
129
193
  // is not a generation failure: keep retries disabled because the paid server
130
194
  // job may still complete.
131
195
  var MAX_POLL_MS = 35 * 60 * 1000;
132
196
  var MAX_POLL_ERRORS = 30;
133
197
  var pollStart = 0, pollErrors = 0;
198
+ var cancelRequested = false; // set by the Stop button; freezes the poll loop
134
199
 
135
200
  function schedulePoll(sc) {
201
+ if (cancelRequested) return;
136
202
  if (!pollStart) pollStart = Date.now();
137
203
  clearTimeout(pollTimer);
138
204
  // The call itself long-waits server-side (normally up to three minutes).
@@ -141,6 +207,7 @@ function schedulePoll(sc) {
141
207
  pollTimer = setTimeout(function () { poll(sc); }, delay);
142
208
  }
143
209
  function poll(sc) {
210
+ if (cancelRequested) return;
144
211
  if (pollStart && (Date.now() - pollStart) > MAX_POLL_MS) {
145
212
  return renderTrackingIssue('This is still running longer than the tracking window. Do not retry it — any completed result will appear in your Kolbo library.');
146
213
  }
package/src/index.js CHANGED
@@ -64,6 +64,7 @@ const { registerModelTools } = require('./tools/models');
64
64
  const { registerChatTools } = require('./tools/chat');
65
65
  const { registerVisualDnaTools } = require('./tools/visual_dna');
66
66
  const { registerMoodboardTools } = require('./tools/moodboards');
67
+ const { registerColorPaletteTools } = require('./tools/color_palettes');
67
68
  const { registerMediaTools } = require('./tools/media');
68
69
  const { registerPresetTools } = require('./tools/presets');
69
70
  const { registerArtifactTools } = require('./tools/artifacts');
@@ -134,6 +135,7 @@ function createServer(opts = {}) {
134
135
  registerChatTools(server, client, toolOptions);
135
136
  registerVisualDnaTools(server, client, toolOptions);
136
137
  registerMoodboardTools(server, client, toolOptions);
138
+ registerColorPaletteTools(server, client, toolOptions);
137
139
  registerMediaTools(server, client, toolOptions);
138
140
  registerPresetTools(server, client, toolOptions);
139
141
  registerArtifactTools(server, client, toolOptions);
@@ -0,0 +1,142 @@
1
+ /* ⛔ BACKWARD COMPATIBILITY: Tool names and arg names below are a PUBLIC
2
+ * CONTRACT. Never rename, remove, or break an existing tool/arg — old cached
3
+ * `npx @kolbo/mcp` installs in the wild will break silently. Add new tools or
4
+ * new OPTIONAL args only. Full rules: ../index.js top-of-file and CLAUDE.md. */
5
+
6
+ const { z } = require('zod');
7
+ const { UI, uiResult, appsEnabled } = require('../apps');
8
+
9
+ /** SVG strip of the palette's colors as a data URI — every palette gets a real
10
+ * thumbnail in the media-grid widget even when it has no source images. */
11
+ function paletteStripThumbnail(colors = []) {
12
+ const hexes = colors.map(c => c && c.hex).filter(Boolean).slice(0, 10);
13
+ if (hexes.length === 0) return undefined;
14
+ const w = 320, h = 180, seg = w / hexes.length;
15
+ const rects = hexes.map((hex, i) =>
16
+ `<rect x="${(i * seg).toFixed(1)}" y="0" width="${seg.toFixed(1)}" height="${h}" fill="${hex}"/>`).join('');
17
+ return `data:image/svg+xml,${encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}">${rects}</svg>`)}`;
18
+ }
19
+
20
+ const STICKY_NOTE ='IMPORTANT: activating a palette is STICKY and ACCOUNT-WIDE — once active, it strict-grades EVERY generation (generate_image, generate_image_edit, generate_video, generate_video_from_image) automatically until deactivated, not just the next one. Always tell the user this before activating, and mention deactivate_color_palette / skip_color_palette (per-call opt-out) as the way out.';
21
+
22
+ function registerColorPaletteTools(server, client, options = {}) {
23
+ const ui = () => appsEnabled(server, options);
24
+ // ─── list_color_palettes ───────────────────────────────────
25
+ server.tool(
26
+ 'list_color_palettes',
27
+ 'List the user\'s Color DNA palettes (personal + org). Each has a name, 1-10 colors, and an is_active flag — at most one palette is active per account at a time, and the active one auto-applies to every generation.',
28
+ {},
29
+ async () => {
30
+ const result = await client.get('/v1/color-palettes');
31
+ const palettes = result.color_palettes || [];
32
+ const text = JSON.stringify({ color_palettes: palettes, pagination: result.pagination }, null, 2);
33
+
34
+ if (ui()) {
35
+ return uiResult(UI.mediaGrid, text, {
36
+ widget: 'media-grid',
37
+ title: 'Color DNA Palettes',
38
+ items: palettes.slice(0, 24).map(p => ({
39
+ id: p.id,
40
+ title: p.is_active ? `${p.name} (active)` : p.name,
41
+ // First source image when available, else an SVG strip of the palette itself.
42
+ thumbnail: (Array.isArray(p.source_image_urls) && p.source_image_urls[0]) || paletteStripThumbnail(p.colors),
43
+ media_type: 'image',
44
+ use_hint: 'Activate Color DNA palette "{TITLE}" (color_palette_id: {ID}) so it grades my generations.'
45
+ })),
46
+ total: result.pagination?.total || palettes.length,
47
+ has_more: palettes.length > 24
48
+ });
49
+ }
50
+
51
+ return { content: [{ type: 'text', text }] };
52
+ }
53
+ );
54
+
55
+ // ─── analyze_color_palette ──────────────────────────────────
56
+ server.tool(
57
+ 'analyze_color_palette',
58
+ 'Extract a color palette from 1-5 reference image URLs using fast local pixel analysis (no LLM, free). Does NOT save anything — pass the returned colors to create_color_palette to save + optionally activate it.',
59
+ {
60
+ image_urls: z.array(z.string()).min(1).max(5).describe('1-5 public image URLs to extract dominant colors from.')
61
+ },
62
+ async ({ image_urls }) => {
63
+ const result = await client.post('/v1/color-palettes/analyze', { image_urls });
64
+ return { content: [{ type: 'text', text: JSON.stringify({ name: result.name, colors: result.colors, _hint: 'Pass these colors (and optionally the name) to create_color_palette to save this palette.' }, null, 2) }] };
65
+ }
66
+ );
67
+
68
+ // ─── create_color_palette ───────────────────────────────────
69
+ server.tool(
70
+ 'create_color_palette',
71
+ `Save a new Color DNA palette from manual colors or from analyze_color_palette output. ${STICKY_NOTE} Pass is_active: false to save without activating.`,
72
+ {
73
+ name: z.string().describe('Palette name (1-100 chars).'),
74
+ colors: z.array(z.object({
75
+ hex: z.string().describe('#RRGGBB hex code.'),
76
+ name: z.string().optional().describe('Color name, e.g. "terracotta".'),
77
+ role: z.enum(['dominant', 'secondary', 'accent', 'background']).optional().describe('Role of this color in the palette. Default: "accent".')
78
+ })).min(1).max(10).describe('1-10 colors.'),
79
+ source_image_urls: z.array(z.string()).max(5).optional().describe('Optional reference image URLs this palette was derived from.'),
80
+ is_active: z.boolean().optional().describe('Activate immediately on save. Default: true (saving auto-activates and unsets any other active palette).')
81
+ },
82
+ async ({ name, colors, source_image_urls, is_active }) => {
83
+ const result = await client.post('/v1/color-palettes', { name, colors, source_image_urls, is_active });
84
+ return { content: [{ type: 'text', text: JSON.stringify({ color_palette: result.color_palette }, null, 2) }] };
85
+ }
86
+ );
87
+
88
+ // ─── update_color_palette ───────────────────────────────────
89
+ server.tool(
90
+ 'update_color_palette',
91
+ 'Rename a Color DNA palette and/or replace its colors. Owner only. Does not change is_active — use activate_color_palette / deactivate_color_palette for that.',
92
+ {
93
+ color_palette_id: z.string().describe('Palette id (from list_color_palettes).'),
94
+ name: z.string().optional().describe('New name.'),
95
+ colors: z.array(z.object({
96
+ hex: z.string(),
97
+ name: z.string().optional(),
98
+ role: z.enum(['dominant', 'secondary', 'accent', 'background']).optional()
99
+ })).min(1).max(10).optional().describe('Full replacement color set (1-10). Omit to keep current colors.'),
100
+ source_image_urls: z.array(z.string()).max(5).optional()
101
+ },
102
+ async ({ color_palette_id, name, colors, source_image_urls }) => {
103
+ const result = await client.put(`/v1/color-palettes/${encodeURIComponent(color_palette_id)}`, { name, colors, source_image_urls });
104
+ return { content: [{ type: 'text', text: JSON.stringify({ color_palette: result.color_palette }, null, 2) }] };
105
+ }
106
+ );
107
+
108
+ // ─── delete_color_palette ───────────────────────────────────
109
+ server.tool(
110
+ 'delete_color_palette',
111
+ 'Permanently delete a Color DNA palette. Owner only. If it was the active palette, generations stop being color-graded.',
112
+ { color_palette_id: z.string().describe('Palette id to delete.') },
113
+ async ({ color_palette_id }) => {
114
+ const result = await client.delete(`/v1/color-palettes/${encodeURIComponent(color_palette_id)}`);
115
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
116
+ }
117
+ );
118
+
119
+ // ─── activate_color_palette ─────────────────────────────────
120
+ server.tool(
121
+ 'activate_color_palette',
122
+ `Make this the account's sticky active Color DNA palette (unsets any other active palette first). ${STICKY_NOTE}`,
123
+ { color_palette_id: z.string().describe('Palette id to activate.') },
124
+ async ({ color_palette_id }) => {
125
+ const result = await client.post(`/v1/color-palettes/${encodeURIComponent(color_palette_id)}/activate`, {});
126
+ return { content: [{ type: 'text', text: JSON.stringify({ color_palette: result.color_palette }, null, 2) }] };
127
+ }
128
+ );
129
+
130
+ // ─── deactivate_color_palette ───────────────────────────────
131
+ server.tool(
132
+ 'deactivate_color_palette',
133
+ 'Clear whichever Color DNA palette is currently active for the account. Generations stop being auto color-graded until another palette is activated.',
134
+ {},
135
+ async () => {
136
+ const result = await client.post('/v1/color-palettes/deactivate', {});
137
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
138
+ }
139
+ );
140
+ }
141
+
142
+ module.exports = { registerColorPaletteTools };
@@ -64,13 +64,14 @@ function registerGenerateTools(server, client, options = {}) {
64
64
  quality: z.string().optional().describe('Quality tier for models that support it (e.g. "low", "medium", "high", "auto"). Check list_models → supported_qualities on the chosen model. "auto" is normalised to "medium" on gpt-image-2. Omit to use the model default.'),
65
65
  preset_id: z.string().optional().describe('Preset ID from list_presets type="image" to apply a saved style preset to this generation.'),
66
66
  cinematic: CINEMATIC_SCHEMA,
67
+ skip_color_palette: z.boolean().optional().describe('Opt this single call OUT of the account\'s active Color DNA palette (see list_color_palettes / activate_color_palette). By default, if the user has an active palette it strict-grades every generation automatically — pass true only when the user explicitly wants this one image ungraded.'),
67
68
  project_id: projectIdField
68
69
  },
69
- async ({ prompt, model, aspect_ratio, enhance_prompt, num_images, reference_images, visual_dna_ids, moodboard_id, enable_web_search, resolution, quality, preset_id, cinematic, project_id }) => {
70
+ async ({ prompt, model, aspect_ratio, enhance_prompt, num_images, reference_images, visual_dna_ids, moodboard_id, enable_web_search, resolution, quality, preset_id, cinematic, skip_color_palette, project_id }) => {
70
71
  model = await canonicalModelId(client, model); // lenient id resolution ("z-image" → "z-image/turbo")
71
72
  const gen = await client.post('/v1/generate/image', {
72
73
  prompt, model, aspect_ratio, enhance_prompt, num_images,
73
- reference_images, visual_dna_ids, moodboard_id, enable_web_search, resolution, quality, preset_id, cinematic, project_id
74
+ reference_images, visual_dna_ids, moodboard_id, enable_web_search, resolution, quality, preset_id, cinematic, skip_color_palette, project_id
74
75
  });
75
76
 
76
77
  if (ui()) return uiGenerating({
@@ -118,13 +119,14 @@ function registerGenerateTools(server, client, options = {}) {
118
119
  enable_web_search: z.boolean().optional().describe('Enable web-search grounding. Default: false'),
119
120
  resolution: z.string().optional().describe('Image resolution tier: "1K" / "2K" / "3K" / "4K". Model-dependent — call list_models and read supported_resolutions. Default: "1K" for most edit models.'),
120
121
  cinematic: CINEMATIC_SCHEMA,
122
+ skip_color_palette: z.boolean().optional().describe('Opt this single call OUT of the account\'s active Color DNA palette (see list_color_palettes / activate_color_palette). By default, if the user has an active palette it strict-grades every generation automatically — pass true only when the user explicitly wants this one edit ungraded.'),
121
123
  project_id: projectIdField
122
124
  },
123
- async ({ prompt, model, source_images, aspect_ratio, enhance_prompt, num_images, visual_dna_ids, moodboard_id, enable_web_search, resolution, cinematic, project_id }) => {
125
+ async ({ prompt, model, source_images, aspect_ratio, enhance_prompt, num_images, visual_dna_ids, moodboard_id, enable_web_search, resolution, cinematic, skip_color_palette, project_id }) => {
124
126
  model = await canonicalModelId(client, model); // lenient id resolution ("z-image" → "z-image/turbo")
125
127
  const gen = await client.post('/v1/generate/image-edit', {
126
128
  prompt, model, source_images, aspect_ratio, enhance_prompt, num_images,
127
- visual_dna_ids, moodboard_id, enable_web_search, resolution, cinematic, project_id
129
+ visual_dna_ids, moodboard_id, enable_web_search, resolution, cinematic, skip_color_palette, project_id
128
130
  });
129
131
 
130
132
  if (ui()) return uiGenerating({
@@ -262,7 +264,7 @@ function registerGenerateTools(server, client, options = {}) {
262
264
  // blocking poll window) needs this tool to be re-checked until done.
263
265
  server.tool(
264
266
  'get_creative_director_status',
265
- 'Check the status of a Creative Director batch (from generate_creative_director) by its generation_id. Returns overall state ("processing" until EVERY scene is terminal, then "completed"/"failed") plus each scene\'s number, title, per-scene status, and image_urls/video_urls. Set wait=true to block for up to ~3 minutes instead of repeatedly calling this tool. Do NOT use the generic get_generation_status for Creative Director ids; it points at the wrong endpoint.',
267
+ 'Check the status of a Creative Director batch (from generate_creative_director) by its generation_id. Returns overall state ("processing" until EVERY scene is terminal, then "completed"/"failed") plus each scene\'s number, title, per-scene status, and image_urls/video_urls. Set wait=true to block for up to ~3 minutes instead of repeatedly calling this tool. Prefer this over the generic get_generation_status for Creative Director ids — the generic tool now returns the same scene data (it delegates here), but this one is the direct route.',
266
268
  {
267
269
  generation_id: z.string().describe('The Creative Director generation_id returned by generate_creative_director.'),
268
270
  wait: z.boolean().optional().describe('If true, block until the batch is terminal, up to ~3 minutes. Use this instead of repeatedly checking in a loop.')
@@ -327,12 +329,13 @@ function registerGenerateTools(server, client, options = {}) {
327
329
  resolution: z.string().optional().describe('Video resolution tier (vertical pixels): "720p" / "1080p" / "1440p" / "2160p". Some models use labels like "512P"/"1024P"/"768P"/"1080P". Model-dependent — call list_models and read supported_resolutions. Read resolution_multipliers to predict cost.'),
328
330
  preset_id: z.string().optional().describe('Preset ID from list_presets type="video" to apply a saved motion/style preset to this generation.'),
329
331
  sound_enabled: z.boolean().optional().describe('Enable (`true`) or disable (`false`) AI-generated synced audio on the output video. Only honored by models with `sound_generation_type: "native"` from list_models (e.g. Veo 3.1, Kling V3/2.6, PixVerse V6). On `sound_generation_type: "none"` models the flag has no effect. Omit to use the model\'s `sound_enabled_by_default`. Pass `false` when the user says no sound / silent / mute / without audio. Enabling sound may apply `sound_credit_multiplier` to cost.'),
332
+ skip_color_palette: z.boolean().optional().describe('Opt this single call OUT of the account\'s active Color DNA palette (see list_color_palettes / activate_color_palette). By default, if the user has an active palette it strict-grades every generation automatically — pass true only when the user explicitly wants this one video ungraded.'),
330
333
  project_id: projectIdField
331
334
  },
332
- async ({ prompt, model, aspect_ratio, duration, enhance_prompt, reference_images, resolution, preset_id, sound_enabled, project_id }) => {
335
+ async ({ prompt, model, aspect_ratio, duration, enhance_prompt, reference_images, resolution, preset_id, sound_enabled, skip_color_palette, project_id }) => {
333
336
  model = await canonicalModelId(client, model); // lenient id resolution ("z-image" → "z-image/turbo")
334
337
  const gen = await client.post('/v1/generate/video', {
335
- prompt, model, aspect_ratio, duration, enhance_prompt, reference_images, resolution, preset_id, sound_enabled, project_id
338
+ prompt, model, aspect_ratio, duration, enhance_prompt, reference_images, resolution, preset_id, sound_enabled, skip_color_palette, project_id
336
339
  });
337
340
 
338
341
  if (ui()) return uiGenerating({
@@ -382,12 +385,13 @@ function registerGenerateTools(server, client, options = {}) {
382
385
  visual_dna_ids: z.array(z.string()).optional().describe('Array of Visual DNA profile IDs to maintain consistency with prior characters / styles. **Cap: pass at most `max_visual_dna` IDs from list_models for the chosen model; if `supports_visual_dna: false` the model ignores DNA entirely.**'),
383
386
  resolution: z.string().optional().describe('Video resolution tier (vertical pixels): "720p" / "1080p" / "1440p" / "2160p". Some models use labels like "512P"/"1024P"/"768P"/"1080P". Model-dependent — call list_models and read supported_resolutions.'),
384
387
  sound_enabled: z.boolean().optional().describe('Enable (`true`) or disable (`false`) AI-generated synced audio on the output video. Only honored by models with `sound_generation_type: "native"` from list_models (e.g. Veo 3.1 Lite, Kling V3 4K, PixVerse V6, Kling 2.6/v3). On `sound_generation_type: "none"` models the flag has no effect. Omit to use the model\'s `sound_enabled_by_default`. Pass `false` when the user says no sound / silent / mute / without audio. Enabling sound may apply `sound_credit_multiplier` to cost.'),
388
+ skip_color_palette: z.boolean().optional().describe('Opt this single call OUT of the account\'s active Color DNA palette (see list_color_palettes / activate_color_palette). By default, if the user has an active palette it strict-grades every generation automatically — pass true only when the user explicitly wants this one video ungraded.'),
385
389
  project_id: projectIdField
386
390
  },
387
- async ({ image_url, prompt, model, aspect_ratio, duration, enhance_prompt, visual_dna_ids, resolution, sound_enabled, project_id }) => {
391
+ async ({ image_url, prompt, model, aspect_ratio, duration, enhance_prompt, visual_dna_ids, resolution, sound_enabled, skip_color_palette, project_id }) => {
388
392
  model = await canonicalModelId(client, model); // lenient id resolution ("z-image" → "z-image/turbo")
389
393
  const gen = await client.post('/v1/generate/video/from-image', {
390
- image_url, prompt, model, aspect_ratio, duration, enhance_prompt, visual_dna_ids, resolution, sound_enabled, project_id
394
+ image_url, prompt, model, aspect_ratio, duration, enhance_prompt, visual_dna_ids, resolution, sound_enabled, skip_color_palette, project_id
391
395
  });
392
396
 
393
397
  if (ui()) return uiGenerating({
@@ -690,6 +694,56 @@ function registerGenerateTools(server, client, options = {}) {
690
694
  }
691
695
  );
692
696
 
697
+ // ═════════════════════════════════════════════════════════════
698
+ // ─── cancel_generation ─────────────────────────────────────
699
+ server.tool(
700
+ 'cancel_generation',
701
+ 'Cancel a running generation and release/refund its credits. Works for every ' +
702
+ 'generation type the SDK tracks (image, image edit, video, image-to-video, ' +
703
+ 'video-to-video, elements, first/last frame, lipsync, music, speech, sound, 3D, ' +
704
+ 'motion graphic, global image/video edit, creative director). Use when the user says ' +
705
+ '"stop", "cancel that", "never mind" while something is still running, or to clear a ' +
706
+ 'stuck generation. Returns how many credits came back. A generation that already ' +
707
+ 'finished or failed cannot be cancelled — you get a 409 with its current status, which ' +
708
+ 'is not an error worth retrying. For Shorts Creator jobs use shorts_cancel instead.',
709
+ {
710
+ generation_id: z.string().describe('The generation id to cancel, as returned by any generate_* tool or get_generation_status.')
711
+ },
712
+ async ({ generation_id }) => {
713
+ try {
714
+ const result = await client.post(`/v1/generate/${encodeURIComponent(generation_id)}/cancel`, {});
715
+ return {
716
+ content: [{
717
+ type: 'text',
718
+ text: JSON.stringify({
719
+ generation_id,
720
+ cancelled: true,
721
+ ...(result && typeof result === 'object' ? result : {}),
722
+ _hint: 'Generation cancelled. Do NOT call get_generation_status on it — the state is final.'
723
+ }, null, 2)
724
+ }]
725
+ };
726
+ } catch (err) {
727
+ // 409 = already terminal. That is an answer, not a failure — report it
728
+ // plainly so the model does not retry or apologise for a non-problem.
729
+ const conflict = err.status === 409 || /cannot cancel/i.test(err.message || '');
730
+ return {
731
+ content: [{
732
+ type: 'text',
733
+ text: JSON.stringify({
734
+ generation_id,
735
+ cancelled: false,
736
+ reason: err.message,
737
+ _hint: conflict
738
+ ? 'This generation already reached a final state, so there was nothing to cancel. Tell the user it had already finished — do not retry.'
739
+ : 'Cancel failed. Check the generation_id is correct via get_generation_status.'
740
+ }, null, 2)
741
+ }]
742
+ };
743
+ }
744
+ }
745
+ );
746
+
693
747
  // ═════════════════════════════════════════════════════════════
694
748
  // ─── 2026-04 SDK Expansion Batch ─────────────────────────────
695
749
  // ═════════════════════════════════════════════════════════════
@@ -36,7 +36,9 @@ function registerMoodboardTools(server, client, options = {}) {
36
36
  items: moodboards.slice(0, 24).map(mb => ({
37
37
  id: mb.id,
38
38
  title: mb.name,
39
- thumbnail: mb.thumbnail || (Array.isArray(mb.image_urls) ? mb.image_urls[0] : undefined),
39
+ // API returns thumbnail_url + images[] (sdk listMoodboards) — both
40
+ // previous keys were wrong, so the fallback never fired either.
41
+ thumbnail: mb.thumbnail_url || mb.thumbnail || (Array.isArray(mb.images) ? mb.images[0] : undefined),
40
42
  media_type: 'image',
41
43
  use_hint: 'Apply moodboard "{TITLE}" (moodboard_id: {ID}) to my next generation.'
42
44
  })),
@@ -66,7 +66,14 @@ function registerMusicLibraryTools(server, client, options = {}) {
66
66
 
67
67
  server.tool(
68
68
  'search_music_library',
69
- 'Search the licensed SYNCI catalog. Results contain watermarked preview audio only. For any download or timeline use, call acquire_clean_music_track, which consumes one SYNCI vendor credit.',
69
+ 'Search the licensed SYNCI catalog the PAID third-party option. ' +
70
+ '⚠️ NOT the default for music. Kolbo has its OWN large AI music library that is FREE and ' +
71
+ 'costs no vendor credit: call search_stock_media with source="kolbo-ai" and mediaType="music" ' +
72
+ '(it also supports natural-language vibe search, e.g. "uplifting hopeful corporate background"). ' +
73
+ 'Reach for SYNCI only when the user explicitly asks for the licensed/SYNCI catalog, names a real ' +
74
+ 'artist or commercial track, or needs stems / a specific licensed cue. ' +
75
+ 'Results here contain watermarked preview audio only; any download or timeline use requires ' +
76
+ 'acquire_clean_music_track, which consumes one SYNCI vendor credit.',
70
77
  {
71
78
  query: z.string().max(200).optional(),
72
79
  mood: z.string().optional(),
@@ -36,10 +36,13 @@ function registerPresetTools(server, client, options = {}) {
36
36
  id: p.id,
37
37
  title: p.name,
38
38
  subtitle: p.category,
39
- thumbnail: p.thumbnail,
40
- media_type: p.audio_preview_url ? 'audio' : 'image',
41
- preview_audio: p.audio_preview_url,
42
- url: p.thumbnail,
39
+ // API returns thumbnail_url / audio_url (see sdk listPresets) — NOT
40
+ // thumbnail / audio_preview_url. Reading the wrong key rendered every
41
+ // preset as a blank tile and dropped music previews entirely.
42
+ thumbnail: p.thumbnail_url || p.thumbnail,
43
+ media_type: p.audio_url ? 'audio' : 'image',
44
+ preview_audio: p.audio_url,
45
+ url: p.thumbnail_url || p.thumbnail,
43
46
  use_hint: 'Use preset "{TITLE}" (preset_id: {ID}) for my next generation — ask me for the prompt.'
44
47
  })),
45
48
  total: result.count || presets.length,
@@ -35,7 +35,12 @@ function registerStockLibraryTools(server, client, options = {}) {
35
35
  // ─── search_stock_media ───────────────────────────────────────
36
36
  server.tool(
37
37
  'search_stock_media',
38
- 'Search the Kolbo unified stock media library and return matching assets. Covers external providers (Pexels photos/videos, Pixabay, Sketchfab 3D, licensed Music) AND Kolbo\'s OWN AI-generated library: thousands of SOUND EFFECTS (mediaType="sfx") and MUSIC tracks (source="kolbo-ai"). Use this to FIND ready-made photos, videos, 3D models, music, or sound effects as b-roll/references/project assets — distinct from generate_* tools which create new content.\n\nFor SOUND EFFECTS or MUSIC, Kolbo supports SEMANTIC "VIBE" SEARCH: pass a natural-language description of the feeling/use ("tense ominous build-up for a horror reveal", "uplifting hopeful corporate background", "retro arcade coin pickup") with source="kolbo-ai" and mediaType="sfx" (or "music") — it matches by meaning, not just keywords. For external visual providers, use concrete keywords.\n\nsource="all" interleaves providers for the requested media type; or pick a single source. Returns assets with source, sourceId, mediaType, dimensions, author, attribution, thumbnail, and downloadable variants. To turn a script into queries first, call analyze_script_for_stock.',
38
+ 'Search the Kolbo unified stock media library and return matching assets. ' +
39
+ '★ THE DEFAULT TOOL FOR MUSIC: Kolbo\'s own AI music library is FREE (no vendor credit) — ' +
40
+ 'use source="kolbo-ai" with mediaType="music" for background/score/bed requests. Prefer it over ' +
41
+ 'search_music_library (SYNCI), which is the paid licensed catalog and should only be used when the ' +
42
+ 'user explicitly asks for it. ' +
43
+ 'Covers external providers (Pexels photos/videos, Pixabay, Sketchfab 3D, licensed Music) AND Kolbo\'s OWN AI-generated library: thousands of SOUND EFFECTS (mediaType="sfx") and MUSIC tracks (source="kolbo-ai"). Use this to FIND ready-made photos, videos, 3D models, music, or sound effects as b-roll/references/project assets — distinct from generate_* tools which create new content.\n\nFor SOUND EFFECTS or MUSIC, Kolbo supports SEMANTIC "VIBE" SEARCH: pass a natural-language description of the feeling/use ("tense ominous build-up for a horror reveal", "uplifting hopeful corporate background", "retro arcade coin pickup") with source="kolbo-ai" and mediaType="sfx" (or "music") — it matches by meaning, not just keywords. For external visual providers, use concrete keywords.\n\nsource="all" interleaves providers for the requested media type; or pick a single source. Returns assets with source, sourceId, mediaType, dimensions, author, attribution, thumbnail, and downloadable variants. To turn a script into queries first, call analyze_script_for_stock.',
39
44
  {
40
45
  query: z.string().max(200).optional().describe('For visual providers: concrete keywords ("city skyline sunset"). For Kolbo SFX/music (source="kolbo-ai"): a natural-language VIBE works great ("eerie suspenseful drone", "emotional sad piano"). Omit to browse.'),
41
46
  source: z.enum(['all', 'kolbo-ai', 'pexels', 'pixabay', 'sketchfab', 'music', 'freesound']).optional().describe('Provider. "all" (default) interleaves. "kolbo-ai" = Kolbo\'s own AI SFX + music (best for vibe search). "freesound" = external CC sound effects.'),
@@ -80,9 +80,16 @@ function registerVisualDnaTools(server, client, options = {}) {
80
80
  // ─── list_visual_dnas ──────────────────────────────────────
81
81
  server.tool(
82
82
  'list_visual_dnas',
83
- 'List Visual DNA profiles. By default returns ALL (personal + global cast presets + organization). Use "scope" to filter: "personal" (user\'s own), "global" (system cast/presets), or "organization" (org-shared). Use "search" to filter by name/tags/description. Use "collection" to filter global presets by collection (cast, influencers, props, locations, styles, glamour, street).',
83
+ 'List the user\'s OWN Visual DNA profiles. This is almost always what you want "my characters", '
84
+ + '"which DNAs do I have", picking a DNA for a generation. Pass `project_id` to also include a shared '
85
+ + 'project\'s DNAs (teammates\' assets on that project). '
86
+ + 'Kolbo ALSO ships a large library of ~1000 global cast/preset DNAs — those are NOT returned by default '
87
+ + 'because dumping them buries the user\'s own handful. Only pass scope="global" (optionally with '
88
+ + '`collection` and `search`) when the user explicitly wants to BROWSE the preset cast — e.g. "find me a '
89
+ + 'character", "show me street style models", "I need a location DNA" — and they have not named one of '
90
+ + 'their own. Use scope="all" only if the user genuinely wants both at once.',
84
91
  {
85
- scope: z.enum(['all', 'personal', 'global', 'organization']).optional().describe('Filter by scope. Default: "all" (everything accessible). "personal" = only your own. "global" = system presets/cast. "organization" = org-shared.'),
92
+ scope: z.enum(['all', 'personal', 'global', 'organization']).optional().describe('Default: "personal" — the user\'s own DNAs (plus a shared project\'s when project_id is set). "global" = the ~1000 system cast/preset DNAs, for browsing when the user needs a character and has none of their own. "organization" = org-shared. "all" = everything, rarely wanted.'),
86
93
  search: z.string().optional().describe('Search by name, tags, or description (case-insensitive)'),
87
94
  collection: z.string().optional().describe('Filter global presets by collection: cast, influencers, props, locations, styles, glamour, street'),
88
95
  tags: z.string().optional().describe('Comma-separated tags to filter by (OR logic)'),
@@ -90,7 +97,12 @@ function registerVisualDnaTools(server, client, options = {}) {
90
97
  },
91
98
  async ({ scope, search, collection, tags, project_id } = {}) => {
92
99
  const params = new URLSearchParams();
93
- if (scope && scope !== 'all') params.set('scope', scope);
100
+ // Default to the user's OWN DNAs. The API defaults to "all", which pulls
101
+ // in ~1000 global cast presets and buries the handful the user actually
102
+ // made. Global is opt-in via scope="global" (browse the preset cast).
103
+ // Shared-project assets still come through project_id, independently.
104
+ const effectiveScope = scope || 'personal';
105
+ if (effectiveScope !== 'all') params.set('scope', effectiveScope);
94
106
  if (search) params.set('search', search);
95
107
  if (collection) params.set('collection', collection);
96
108
  if (tags) params.set('tags', tags);
@@ -111,7 +123,7 @@ function registerVisualDnaTools(server, client, options = {}) {
111
123
  id: d.id,
112
124
  title: d.name,
113
125
  subtitle: (d.dna_type || '') + (Array.isArray(d.tags) && d.tags.length ? ' · ' + d.tags.slice(0, 3).join(', ') : ''),
114
- thumbnail: d.thumbnail,
126
+ thumbnail: d.thumbnail_url || d.thumbnail,
115
127
  media_type: 'image',
116
128
  use_hint: 'Use Visual DNA "{TITLE}" (id: {ID}) in my next generation for character/style consistency.'
117
129
  })),
@@ -51,6 +51,7 @@ function registerVoiceTools(server, client, options = {}) {
51
51
  title: v.name,
52
52
  subtitle: [v.provider, v.language, v.gender, v.accent].filter(Boolean).join(' · '),
53
53
  media_type: 'audio',
54
+ thumbnail: v.thumbnail || null,
54
55
  preview_audio: v.preview_url,
55
56
  use_hint: 'Use voice "{TITLE}" (voice_id: {ID}) for text-to-speech — ask me what text to speak.'
56
57
  })),