UNPKG

@kolbo/mcp

Version:

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

132 lines (123 loc) • 8.1 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 } = require('../apps'); const { projectScopeReadField } = require('./_shared'); function registerMoodboardTools(server, client, options = {}) { // ─── list_moodboards ─────────────────────────────────────── server.tool( 'list_moodboards', 'List moodboards. Default scope is "personal" — the user\'s OWN custom moodboards, which is what the card shows. Pass scope "preset"/"global" for the system presets, "organization" for org-shared, or "all" for everything. Returns id, name, master_prompt, thumbnail, and image URLs for each.', { scope: z.enum(['all', 'personal', 'preset', 'global', 'organization']).optional().describe('Filter by scope. Default: "personal" (only the user\'s own custom moodboards). "preset"/"global" = system presets. "organization" = org-shared. "all" = everything accessible.'), project_id: projectScopeReadField }, async ({ scope, project_id } = {}) => { const params = new URLSearchParams(); // Users asked to see only THEIR moodboards on the card, not the system presets. const effectiveScope = scope || 'personal'; if (effectiveScope !== 'all') params.set('scope', effectiveScope); if (project_id) params.set('project_id', project_id); const qs = params.toString(); const result = await client.get(`/v1/moodboards${qs ? '?' + qs : ''}`); const moodboards = result.moodboards || []; const text = JSON.stringify({ moodboards, count: result.count || 0 }, null, 2); // Always ship structuredContent. Kolbo Code does NOT advertise MCP Apps, so // gating the grid payload on appsEnabled() sent it text only; the host then // rebuilt items from the compactList text, whose field names are // `filename`/`url` — not the `title`/`thumbnail` the grid renders — so every // tile came out black and unlabelled. Same reasoning as listResult(). return uiResult(UI.mediaGrid, text, { widget: 'media-grid', title: 'Moodboards', items: moodboards.slice(0, 300).map(mb => ({ id: mb.id, title: mb.name, // API returns thumbnail_url + images[] (sdk listMoodboards) — both // previous keys were wrong, so the fallback never fired either. thumbnail: mb.thumbnail_url || mb.thumbnail || (Array.isArray(mb.images) ? mb.images[0] : undefined), url: mb.thumbnail_url || mb.thumbnail || (Array.isArray(mb.images) ? mb.images[0] : undefined), media_type: 'image', use_hint: 'Apply moodboard "{TITLE}" (moodboard_id: {ID}) to my next generation.' })), total: moodboards.length, has_more: moodboards.length > 300 }); } ); // ─── get_moodboard ───────────────────────────────────────── server.tool( 'get_moodboard', 'Fetch a single moodboard by ID. Returns the full moodboard including master_prompt, style_guide, and all image URLs.', { moodboard_id: z.string().describe('The moodboard ID'), project_id: projectScopeReadField }, async ({ moodboard_id, project_id }) => { const suffix = project_id ? `?project_id=${encodeURIComponent(project_id)}` : ''; const result = await client.get(`/v1/moodboards/${encodeURIComponent(moodboard_id)}${suffix}`); return { content: [{ type: 'text', text: JSON.stringify(result.moodboard || result, null, 2) }] }; } ); // ─── create_moodboard ────────────────────────────────────── server.tool( 'create_moodboard', 'Create a moodboard from 1–15 image URLs. The server analyzes the images and synthesizes a reusable master style prompt — then pass the returned moodboard id as `moodboard_id` on generation tools to apply the style. Use Kolbo URLs (generated images or `upload_media` output) or any public image URL. Typical flow: generate/upload reference images → create_moodboard → generate with moodboard_id.', { name: z.string().describe('Moodboard name (1–100 chars).'), 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.'), 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".'), 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.') }, async ({ name, image_urls, style_guide, project_id }) => { const body = { name, images: image_urls.map(u => ({ type: 'url', url: u })), ...(style_guide ? { style_guide } : {}), ...(project_id ? { project_id } : {}) }; const result = await client.post('/v1/moodboards', body); 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) }] }; } ); // ─── update_moodboard ────────────────────────────────────── server.tool( 'update_moodboard', 'Edit a moodboard in place: name, style guide, and/or images. NEVER delete and recreate a board to rename it or swap stills — existing generations already reference this id as moodboard_id / #Name. Providing `image_urls` REPLACES the whole image set and re-analyzes the style (master prompt regenerates). Owner only.', { moodboard_id: z.string().describe('Moodboard id (from list_moodboards).'), name: z.string().optional().describe('New name.'), style_guide: z.string().optional().describe('New style notes (empty string clears them).'), image_urls: z.array(z.string()).min(1).max(15).optional().describe('Full replacement image set (1–15 URLs). Omit to keep current images.') }, async ({ moodboard_id, name, style_guide, image_urls }) => { const body = {}; if (name !== undefined) body.name = name; if (style_guide !== undefined) body.style_guide = style_guide; if (image_urls) body.images = image_urls.map(u => ({ type: 'url', url: u })); const result = await client.put(`/v1/moodboards/${encodeURIComponent(moodboard_id)}`, body); return { content: [{ type: 'text', text: JSON.stringify(result.moodboard, null, 2) }] }; } ); // ─── delete_moodboard ────────────────────────────────────── server.tool( 'delete_moodboard', 'Permanently delete a moodboard (owner only; system presets cannot be deleted). Do NOT use this to rename or change images — that is `update_moodboard`. The underlying image files stay in storage — only the board is removed. Confirm with the user before deleting boards they did not just create.', { moodboard_id: z.string().describe('Moodboard id to delete.') }, async ({ moodboard_id }) => { const result = await client.delete(`/v1/moodboards/${encodeURIComponent(moodboard_id)}`); return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; } ); } module.exports = { registerMoodboardTools };