@kolbo/mcp 1.102.1 → 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.1",
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
  );
@@ -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 });