@kolbo/mcp 1.102.0 → 1.103.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kolbo/mcp",
3
- "version": "1.102.0",
3
+ "version": "1.103.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": {
@@ -5,6 +5,7 @@
5
5
 
6
6
  const { z } = require('zod');
7
7
  const { UI, uiResult, appsEnabled } = require('../apps');
8
+ const { projectScopeReadField } = require('./_shared');
8
9
 
9
10
  /** SVG strip of the palette's colors as a data URI — every palette gets a real
10
11
  * thumbnail in the media-grid widget even when it has no source images. */
@@ -25,9 +26,10 @@ function registerColorPaletteTools(server, client, options = {}) {
25
26
  server.tool(
26
27
  'list_color_palettes',
27
28
  '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');
29
+ { project_id: projectScopeReadField },
30
+ async ({ project_id } = {}) => {
31
+ const suffix = project_id ? `?project_id=${encodeURIComponent(project_id)}` : '';
32
+ const result = await client.get(`/v1/color-palettes${suffix}`);
31
33
  const palettes = result.color_palettes || [];
32
34
  const text = JSON.stringify({ color_palettes: palettes, pagination: result.pagination }, null, 2);
33
35
 
@@ -82,10 +84,11 @@ function registerColorPaletteTools(server, client, options = {}) {
82
84
  role: z.enum(['dominant', 'secondary', 'accent', 'background']).optional().describe('Role of this color in the palette. Default: "accent".')
83
85
  })).min(1).max(10).describe('1-10 colors.'),
84
86
  source_image_urls: z.array(z.string()).max(5).optional().describe('Optional reference image URLs this palette was derived from.'),
85
- is_active: z.boolean().optional().describe('Activate immediately on save. Default: true (saving auto-activates and unsets any other active palette).')
87
+ is_active: z.boolean().optional().describe('Activate immediately on save. Default: true (saving auto-activates and unsets any other active palette).'),
88
+ project_id: z.string().optional().describe('Save the palette inside this project so teammates with edit access see it in list_color_palettes (with the same project_id). Requires edit access; ignored otherwise.')
86
89
  },
87
- async ({ name, colors, source_image_urls, is_active }) => {
88
- const result = await client.post('/v1/color-palettes', { name, colors, source_image_urls, is_active });
90
+ async ({ name, colors, source_image_urls, is_active, project_id }) => {
91
+ const result = await client.post('/v1/color-palettes', { name, colors, source_image_urls, is_active, ...(project_id ? { project_id } : {}) });
89
92
  return { content: [{ type: 'text', text: JSON.stringify({ color_palette: result.color_palette }, null, 2) }] };
90
93
  }
91
94
  );
@@ -125,9 +128,12 @@ function registerColorPaletteTools(server, client, options = {}) {
125
128
  server.tool(
126
129
  'activate_color_palette',
127
130
  `Make this the account's sticky active Color DNA palette (unsets any other active palette first). ${STICKY_NOTE}`,
128
- { color_palette_id: z.string().describe('Palette id to activate.') },
129
- async ({ color_palette_id }) => {
130
- const result = await client.post(`/v1/color-palettes/${encodeURIComponent(color_palette_id)}/activate`, {});
131
+ {
132
+ color_palette_id: z.string().describe('Palette id to activate.'),
133
+ project_id: z.string().optional().describe('Shared project the palette belongs to — needed to activate a teammate palette from that project (edit access required).')
134
+ },
135
+ async ({ color_palette_id, project_id }) => {
136
+ const result = await client.post(`/v1/color-palettes/${encodeURIComponent(color_palette_id)}/activate`, project_id ? { project_id } : {});
131
137
  return { content: [{ type: 'text', text: JSON.stringify({ color_palette: result.color_palette }, null, 2) }] };
132
138
  }
133
139
  );
@@ -1391,6 +1391,9 @@ function registerGenerateTools(server, client, options = {}) {
1391
1391
  type: single.type,
1392
1392
  },
1393
1393
  state: single.state,
1394
+ // The failure reason. The SDK always sends it, but this card dropped it, so a
1395
+ // failed generation surfaced as a bare "failed" with no way to act on it.
1396
+ error: single.state === 'failed' ? single.error : undefined,
1394
1397
  urls: done ? urls : undefined,
1395
1398
  thumbnail_url: res.thumbnail_url,
1396
1399
  // Transcription results ride the same status tool as media
@@ -1419,6 +1422,7 @@ function registerGenerateTools(server, client, options = {}) {
1419
1422
  items: [{
1420
1423
  id: single.generation_id,
1421
1424
  state: single.state,
1425
+ error: single.state === 'failed' ? single.error : undefined,
1422
1426
  title: res.prompt_used || res.prompt || undefined,
1423
1427
  url: Array.isArray(res.urls) ? res.urls[0] : undefined,
1424
1428
  }],
@@ -1487,6 +1491,7 @@ function registerGenerateTools(server, client, options = {}) {
1487
1491
  return {
1488
1492
  id: r.generation_id,
1489
1493
  state: r.state,
1494
+ error: r.state === 'failed' ? r.error : undefined,
1490
1495
  title: res.prompt_used || res.prompt || undefined,
1491
1496
  url: Array.isArray(res.urls) ? res.urls[0] : undefined,
1492
1497
  // A live batch card reads these from structuredContent (its only
@@ -59,10 +59,12 @@ function registerMoodboardTools(server, client, options = {}) {
59
59
  'get_moodboard',
60
60
  'Fetch a single moodboard by ID. Returns the full moodboard including master_prompt, style_guide, and all image URLs.',
61
61
  {
62
- moodboard_id: z.string().describe('The moodboard ID')
62
+ moodboard_id: z.string().describe('The moodboard ID'),
63
+ project_id: projectScopeReadField
63
64
  },
64
- async ({ moodboard_id }) => {
65
- const result = await client.get(`/v1/moodboards/${encodeURIComponent(moodboard_id)}`);
65
+ async ({ moodboard_id, project_id }) => {
66
+ const suffix = project_id ? `?project_id=${encodeURIComponent(project_id)}` : '';
67
+ const result = await client.get(`/v1/moodboards/${encodeURIComponent(moodboard_id)}${suffix}`);
66
68
  return {
67
69
  content: [{
68
70
  type: 'text',
@@ -79,13 +81,15 @@ function registerMoodboardTools(server, client, options = {}) {
79
81
  {
80
82
  name: z.string().describe('Moodboard name (1–100 chars).'),
81
83
  image_urls: z.array(z.string()).min(1).max(15).describe('1–15 public image URLs. For local files, get URLs first via the LOCAL FILE route in this tool\'s description.'),
82
- style_guide: z.string().optional().describe('Optional style notes (max 500 chars) that steer the analysis, e.g. "focus on the color grading, not the subjects".')
84
+ style_guide: z.string().optional().describe('Optional style notes (max 500 chars) that steer the analysis, e.g. "focus on the color grading, not the subjects".'),
85
+ project_id: z.string().optional().describe('Create the moodboard inside this project so every teammate with edit access can list, fetch and use it as #Name (pass the same project_id to list_moodboards / get_moodboard). Requires edit access; otherwise it is created private.')
83
86
  },
84
- async ({ name, image_urls, style_guide }) => {
87
+ async ({ name, image_urls, style_guide, project_id }) => {
85
88
  const body = {
86
89
  name,
87
90
  images: image_urls.map(u => ({ type: 'url', url: u })),
88
- ...(style_guide ? { style_guide } : {})
91
+ ...(style_guide ? { style_guide } : {}),
92
+ ...(project_id ? { project_id } : {})
89
93
  };
90
94
  const result = await client.post('/v1/moodboards', body);
91
95
  return { content: [{ type: 'text', text: JSON.stringify({ moodboard: result.moodboard, _hint: 'Pass this id as moodboard_id on generate_image / generate_creative_director to apply the style.' }, null, 2) }] };
@@ -37,9 +37,10 @@ function registerVisualDnaTools(server, client, options = {}) {
37
37
  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`.'),
38
38
  voice_source: z.enum(['none', 'clone', 'assign', 'design']).optional().describe('What to do about a SPEAKING voice. **Pass "none" when the audio is just a reference clip** (the usual case for video work) — the clip is stored and usable as video reference audio, and nothing else happens. "clone" mints an ElevenLabs voice from the uploaded audio, which consumes a voice slot and may EVICT another of the user\'s voices to free one; it also makes the DNA addressable as `dna_<id>` in text-to-speech. "assign" points at an existing voice (pass `assigned_voice_id`). "design" generates a voice from the character\'s look. ⚠️ Omitting this while passing `audio` keeps the legacy behaviour and CLONES — pass "none" explicitly unless the user asked for a voice.'),
39
39
  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`.'),
40
- 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.')
40
+ 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.'),
41
+ project_id: z.string().optional().describe('Create the DNA inside this project so every teammate with edit access to it can list, fetch and generate with it (pass the same project_id to list_visual_dnas / get_visual_dna / generations). Requires edit access to the project. Omit for a private DNA.')
41
42
  },
42
- async ({ name, dna_type, prompt_helper, description, images, video, audio, voice_source, assigned_voice_id, character_sheet_url }) => {
43
+ async ({ name, dna_type, prompt_helper, description, images, video, audio, voice_source, assigned_voice_id, character_sheet_url, project_id }) => {
43
44
  const helper = prompt_helper !== undefined ? prompt_helper : description;
44
45
  if (!name || !name.trim()) {
45
46
  throw new Error('name is required');
@@ -71,6 +72,7 @@ function registerVisualDnaTools(server, client, options = {}) {
71
72
  // long-standing behaviour older installs depend on. Only an explicit choice is forwarded.
72
73
  if (voice_source) form.append('voiceSource', voice_source);
73
74
  if (assigned_voice_id) form.append('assignedVoiceId', assigned_voice_id);
75
+ if (project_id) form.append('project_id', project_id);
74
76
 
75
77
  for (const f of imageFiles) {
76
78
  form.append('images', f.buffer, { filename: f.filename, contentType: f.contentType });