UNPKG

@kolbo/mcp

Version:

Kolbo AI MCP Server - Generate images, videos, music, speech, and sound effects from Claude Code

154 lines (140 loc) • 9.55 kB
/* ⛔ BACKWARD COMPATIBILITY: Tool names and arg names below are a PUBLIC * CONTRACT. Never rename, remove, or break an existing tool/arg — old cached * `npx @kolbo/mcp` installs in the wild will break silently. Add new tools or * new OPTIONAL args only. Full rules: ../index.js top-of-file and CLAUDE.md. */ const { z } = require('zod'); const { UI, uiResult, appsEnabled } = require('../apps'); const { projectScopeReadField } = require('./_shared'); /** SVG strip of the palette's colors as a data URI — every palette gets a real * thumbnail in the media-grid widget even when it has no source images. */ function paletteStripThumbnail(colors = []) { const hexes = colors.map(c => c && c.hex).filter(Boolean).slice(0, 10); if (hexes.length === 0) return undefined; const w = 320, h = 180, seg = w / hexes.length; const rects = hexes.map((hex, i) => `<rect x="${(i * seg).toFixed(1)}" y="0" width="${seg.toFixed(1)}" height="${h}" fill="${hex}"/>`).join(''); 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>`)}`; } 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.'; function registerColorPaletteTools(server, client, options = {}) { const ui = () => appsEnabled(server, options); // ─── list_color_palettes ─────────────────────────────────── server.tool( 'list_color_palettes', '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.', { project_id: projectScopeReadField }, async ({ project_id } = {}) => { const suffix = project_id ? `?project_id=${encodeURIComponent(project_id)}` : ''; const result = await client.get(`/v1/color-palettes${suffix}`); const palettes = result.color_palettes || []; const text = JSON.stringify({ color_palettes: palettes, pagination: result.pagination }, null, 2); // Ship structuredContent UNCONDITIONALLY. Gating on ui() left every host that // renders widgets without advertising MCP Apps (Kolbo Code) with text-only rows // that carry no thumbnail field at all — and its BY_TOOL map still force-mounts // the media grid on them, so the card rendered one broken-file glyph per cell. // media.js and listResult() have always done it this way; these five lagged. { return uiResult(UI.mediaGrid, text, { widget: 'media-grid', title: 'Color DNA Palettes', items: palettes.slice(0, 300).map(p => ({ id: p.id, title: p.is_active ? `${p.name} (active)` : p.name, // First source image when available, else an SVG strip of the palette itself. thumbnail: (Array.isArray(p.source_image_urls) && p.source_image_urls[0]) || paletteStripThumbnail(p.colors), media_type: 'image', use_hint: 'Activate Color DNA palette "{TITLE}" (color_palette_id: {ID}) so it grades my generations.' })), total: palettes.length, has_more: palettes.length > 300 }); } return { content: [{ type: 'text', text }] }; } ); // ─── analyze_color_palette ────────────────────────────────── server.tool( 'analyze_color_palette', '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.', { image_urls: z.array(z.string()).min(1).max(5).describe('1-5 public image URLs to extract dominant colors from.') }, async ({ image_urls }) => { const result = await client.post('/v1/color-palettes/analyze', { image_urls }); 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) }] }; } ); // ─── create_color_palette ─────────────────────────────────── server.tool( 'create_color_palette', `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.`, { name: z.string().describe('Palette name (1-100 chars).'), colors: z.array(z.object({ hex: z.string().describe('#RRGGBB hex code.'), name: z.string().optional().describe('Color name, e.g. "terracotta".'), role: z.enum(['dominant', 'secondary', 'accent', 'background']).optional().describe('Role of this color in the palette. Default: "accent".') })).min(1).max(10).describe('1-10 colors.'), source_image_urls: z.array(z.string()).max(5).optional().describe('Optional reference image URLs this palette was derived from.'), is_active: z.boolean().optional().describe('Activate immediately on save. Default: true (saving auto-activates and unsets any other active palette).'), 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.') }, async ({ name, colors, source_image_urls, is_active, project_id }) => { const result = await client.post('/v1/color-palettes', { name, colors, source_image_urls, is_active, ...(project_id ? { project_id } : {}) }); return { content: [{ type: 'text', text: JSON.stringify({ color_palette: result.color_palette }, null, 2) }] }; } ); // ─── update_color_palette ─────────────────────────────────── server.tool( 'update_color_palette', '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.', { color_palette_id: z.string().describe('Palette id (from list_color_palettes).'), name: z.string().optional().describe('New name.'), colors: z.array(z.object({ hex: z.string(), name: z.string().optional(), role: z.enum(['dominant', 'secondary', 'accent', 'background']).optional() })).min(1).max(10).optional().describe('Full replacement color set (1-10). Omit to keep current colors.'), source_image_urls: z.array(z.string()).max(5).optional() }, async ({ color_palette_id, name, colors, source_image_urls }) => { const result = await client.put(`/v1/color-palettes/${encodeURIComponent(color_palette_id)}`, { name, colors, source_image_urls }); return { content: [{ type: 'text', text: JSON.stringify({ color_palette: result.color_palette }, null, 2) }] }; } ); // ─── delete_color_palette ─────────────────────────────────── server.tool( 'delete_color_palette', 'Permanently delete a Color DNA palette. Owner only. If it was the active palette, generations stop being color-graded.', { color_palette_id: z.string().describe('Palette id to delete.') }, async ({ color_palette_id }) => { const result = await client.delete(`/v1/color-palettes/${encodeURIComponent(color_palette_id)}`); return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; } ); // ─── activate_color_palette ───────────────────────────────── server.tool( 'activate_color_palette', `Make this the account's sticky active Color DNA palette (unsets any other active palette first). ${STICKY_NOTE}`, { color_palette_id: z.string().describe('Palette id to activate.'), project_id: z.string().optional().describe('Shared project the palette belongs to — needed to activate a teammate palette from that project (edit access required).') }, async ({ color_palette_id, project_id }) => { const result = await client.post(`/v1/color-palettes/${encodeURIComponent(color_palette_id)}/activate`, project_id ? { project_id } : {}); return { content: [{ type: 'text', text: JSON.stringify({ color_palette: result.color_palette }, null, 2) }] }; } ); // ─── deactivate_color_palette ─────────────────────────────── server.tool( 'deactivate_color_palette', 'Clear whichever Color DNA palette is currently active for the account. Generations stop being auto color-graded until another palette is activated.', {}, async () => { const result = await client.post('/v1/color-palettes/deactivate', {}); return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; } ); } module.exports = { registerColorPaletteTools };