@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
JavaScript
/* ⛔ 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 };