@kolbo/mcp 1.107.2 → 1.108.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/README.md CHANGED
@@ -317,6 +317,7 @@ Requires DaVinci Resolve Studio with the Kolbo plugin open (Workspace → Workfl
317
317
  | `generate_character_sheet` | Generate a multi-angle character sheet from reference images (credits) → pass URL to create_visual_dna or update_visual_dna |
318
318
  | `list_visual_dna_folders` / `create_visual_dna_folder` / `update_visual_dna_folder` / `delete_visual_dna_folder` / `move_visual_dna_to_folder` | Organize Visual DNA characters into user folders (create/rename/recolor/delete + move DNAs in/out) |
319
319
  | `create_project` / `update_project` / `archive_project` / `unarchive_project` | Project lifecycle (create/rename/describe/archive; deletion stays in-app) |
320
+ | `transfer_project` / `list_project_transfers` / `respond_project_transfer` | Hand a project you own to another Kolbo account (they accept; same ids; cast assets copied; payer history unchanged) |
320
321
  | `list_skills` / `create_skill` / `update_skill` / `delete_skill` | Skills — reusable named personas for the chat tool; `description` is the system instruction |
321
322
  | `list_agents` / `create_agent` / `update_agent` / `delete_agent` | Same four tools under their original names. Still supported and unchanged; new integrations should use the `*_skill` names |
322
323
  | `create_video_editor_session` / `export_video_editor_session` | Build an editable timeline from Kolbo-hosted clips, audio and text, then export it to MP4. No new media is generated; identical snapshots reuse the same export job |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kolbo/mcp",
3
- "version": "1.107.2",
3
+ "version": "1.108.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": {
@@ -26,6 +26,7 @@ const READ_ONLY = [
26
26
  'list_presets', 'list_cinematic_presets',
27
27
  'list_projects', 'get_project', 'list_sessions', 'list_project_context', 'get_project_profile',
28
28
  'list_project_assets',
29
+ 'list_project_transfers',
29
30
  'list_session_generations',
30
31
  'list_agents', 'list_skills', 'list_docs', 'get_doc',
31
32
  'get_review_storage_usage', 'list_review_assets', 'get_review_asset',
@@ -111,6 +112,8 @@ const DESTRUCTIVE_WRITE = [
111
112
  'delete_session',
112
113
  'delete_project_context', 'regenerate_project_profile',
113
114
  'update_project_asset',
115
+ // Ownership hand-off: the sender loses the project (and maybe all access) on accept.
116
+ 'transfer_project', 'respond_project_transfer',
114
117
  'update_agent', 'delete_agent', 'update_skill', 'delete_skill', 'update_doc', 'delete_doc',
115
118
  'delete_review_asset', 'delete_review_collection',
116
119
  'edit_review_comment', 'delete_review_comment',
@@ -315,6 +315,57 @@ function registerProjectTools(server, client) {
315
315
  }
316
316
  );
317
317
 
318
+ // ─── project transfer (hand a project to another account) ──
319
+ server.tool(
320
+ 'transfer_project',
321
+ 'Ask another Kolbo account (by email) to take over a project you OWN. Nothing moves until they accept (in the app, or via `respond_project_transfer`) within 7 days. On accept EVERYTHING moves to them with the same ids — sessions, generations, media, chats, docs, agents; your Visual DNA / moodboards / palette in the project cast are COPIED to them (you keep yours). Credit history keeps you as the payer. Set `keep_sender_as_editor` false to lose access entirely (default: you stay as an editor). Team/organization projects cannot be transferred. ALWAYS confirm the recipient email and the keep-editor choice with the user before calling.',
322
+ {
323
+ project_id: z.string().describe('Project ObjectId you own (from list_projects).'),
324
+ email: z.string().describe('Email of the EXISTING Kolbo account that should become the owner.'),
325
+ keep_sender_as_editor: z.boolean().optional().describe('Stay on the project as an editor after the transfer. Default: true.'),
326
+ },
327
+ async ({ project_id, email, keep_sender_as_editor }) => {
328
+ const result = await client.post(`/v1/projects/${encodeURIComponent(project_id)}/transfer`, {
329
+ email,
330
+ ...(keep_sender_as_editor === undefined ? {} : { keep_sender_as_editor }),
331
+ });
332
+ return { content: [{ type: 'text', text: JSON.stringify(result.transfer, null, 2) }] };
333
+ }
334
+ );
335
+ server.tool(
336
+ 'list_project_transfers',
337
+ 'List project transfer requests. `direction: incoming` (default) = projects other people want to give you; `outgoing` = ones you sent. Defaults to pending only. Use the `id` with `respond_project_transfer`.',
338
+ {
339
+ direction: z.enum(['incoming', 'outgoing']).optional().describe('Default: incoming.'),
340
+ status: z.enum(['pending', 'accepted', 'declined', 'cancelled', 'expired', 'failed']).optional().describe('Default: pending.'),
341
+ },
342
+ async ({ direction, status }) => {
343
+ const params = new URLSearchParams();
344
+ if (direction) params.set('direction', direction);
345
+ if (status) params.set('status', status);
346
+ const qs = params.toString();
347
+ const result = await client.get(`/v1/project-transfers${qs ? '?' + qs : ''}`);
348
+ return { content: [{ type: 'text', text: JSON.stringify(result.transfers || [], null, 2) }] };
349
+ }
350
+ );
351
+ server.tool(
352
+ 'respond_project_transfer',
353
+ 'Act on a project transfer request. `accept` / `decline` = you are the recipient; `cancel` = you sent it. Accepting makes you the owner of the whole project immediately. If the project still has generations running, accept returns TRANSFER_BLOCKED_IN_FLIGHT — wait and retry. Confirm with the user before accepting.',
354
+ {
355
+ transfer_id: z.string().describe('Transfer id from list_project_transfers.'),
356
+ action: z.enum(['accept', 'decline', 'cancel']),
357
+ },
358
+ async ({ transfer_id, action }) => {
359
+ const id = encodeURIComponent(transfer_id);
360
+ // Explicit paths (not `/${action}`) so check-parity can see each route.
361
+ let result;
362
+ if (action === 'cancel') result = await client.delete(`/v1/project-transfers/${id}`);
363
+ else if (action === 'accept') result = await client.post(`/v1/project-transfers/${id}/accept`, {});
364
+ else result = await client.post(`/v1/project-transfers/${id}/decline`, {});
365
+ return { content: [{ type: 'text', text: JSON.stringify(result.transfer, null, 2) }] };
366
+ }
367
+ );
368
+
318
369
  // ─── list_sessions ─────────────────────────────────────────
319
370
  server.tool(
320
371
  'list_sessions',
@@ -14,8 +14,12 @@ const { UI, uiResult } = require('../apps');
14
14
 
15
15
  const slugField = z.string().min(1).max(200).regex(/^[\w-]+(\/[\w-]+)?$/)
16
16
  .describe('Trend slug from list_trends, e.g. "vanish" or "motion/kolbo-urban-dance-glide".');
17
- const inputsField = z.record(z.string(), z.string().max(2048))
18
- .describe('Values for the trend\'s declared inputs, keyed by input `key` from get_trend. Media inputs take an https URL (upload local files with upload_media / create_upload_ticket first). Text inputs take plain text.');
17
+ const inputsField = z.record(z.string(), z.union([z.string().max(2048), z.array(z.string().max(2048)).min(1).max(3)]))
18
+ .describe('Values for the trend\'s declared inputs, keyed by input `key` from get_trend. Media inputs take an https URL (upload local files with upload_media / create_upload_ticket first); a photo input with max_images > 1 also takes an ARRAY of up to that many photos of the same subject (more angles = better likeness). Text inputs take plain text.');
19
+ const dnaField = z.record(z.string(), z.string().regex(/^[a-f0-9]{24}$/i)).optional()
20
+ .describe('Use a saved Visual DNA for a photo input: { input key: visual DNA id } (ids from list_visual_dnas). The DNA\'s own character sheet and photos are used for that input, so leave that key out of inputs.');
21
+ const previewField = z.boolean().optional()
22
+ .describe('Run a short preview (~4 s cut of the trend, priced at its own length) instead of the full video. Only trends whose get_trend shows `preview`.');
19
23
  const resolutionField = z.enum(['480p', '720p', '1080p']).optional()
20
24
  .describe('Render resolution for video trends (see resolutions in get_trend). Default: the trend\'s own. Higher costs more; quote it with estimate_trend_run.');
21
25
  const projectField = z.string().min(1).max(128).optional()
@@ -95,7 +99,8 @@ function registerTrendTools(server, client) {
95
99
  const summary = trends.map(t => ({
96
100
  slug: t.slug, name: t.name, description: t.description, family: t.family, output_type: t.output_type,
97
101
  estimated_credits: t.estimated_credits, free_trial: t.free_trial, badges: t.badges,
98
- inputs: (t.inputs || []).map(i => ({ key: i.key, kind: i.kind, role: i.role, label: i.label, required: i.required })),
102
+ inputs: (t.inputs || []).map(i => ({ key: i.key, kind: i.kind, role: i.role, label: i.label, required: i.required, ...(i.max_images && { max_images: i.max_images }) })),
103
+ ...(t.preview && { preview: t.preview }),
99
104
  }));
100
105
  return uiResult(UI.mediaGrid, JSON.stringify({ trends: summary, count: summary.length, free_trials_left: baseData.free_trials_left ?? null }, null, 2), {
101
106
  widget: 'media-grid',
@@ -117,8 +122,8 @@ function registerTrendTools(server, client) {
117
122
  server.tool(
118
123
  'estimate_trend_run',
119
124
  'Quote a trend run before spending: returns total_credits for these exact inputs (video trends are priced by the source clip length). Free — no credits spent.',
120
- { slug: slugField, inputs: inputsField, project_id: projectField, resolution: resolutionField },
121
- async ({ slug, inputs, project_id, resolution }) => text(unwrap(await client.post(`/v1/trends/${slugPath(slug)}/estimate`, { inputs, ...(project_id && { project_id }), ...(resolution && { resolution }) })))
125
+ { slug: slugField, inputs: inputsField, dna: dnaField, preview: previewField, project_id: projectField, resolution: resolutionField },
126
+ async ({ slug, inputs, dna, preview, project_id, resolution }) => text(unwrap(await client.post(`/v1/trends/${slugPath(slug)}/estimate`, { inputs, ...(dna && { dna }), ...(preview && { preview: true }), ...(project_id && { project_id }), ...(resolution && { resolution }) })))
122
127
  );
123
128
 
124
129
  server.tool(
@@ -127,6 +132,8 @@ function registerTrendTools(server, client) {
127
132
  {
128
133
  slug: slugField,
129
134
  inputs: inputsField,
135
+ dna: dnaField,
136
+ preview: previewField,
130
137
  project_id: projectField,
131
138
  resolution: resolutionField,
132
139
  max_credits: z.number().finite().min(0).optional().describe('Credit ceiling for this run. Default: the trend\'s own ceiling.'),
@@ -135,8 +142,8 @@ function registerTrendTools(server, client) {
135
142
  wait: z.boolean().optional().describe('Wait for the result. Default true.'),
136
143
  wait_seconds: z.number().int().min(0).max(170).optional().describe('How long to wait before returning. Default 150.'),
137
144
  },
138
- async ({ slug, inputs, project_id, resolution, max_credits, free_trial, idempotency_key, wait, wait_seconds }) => {
139
- const body = { inputs, idempotency_key: idempotency_key || randomUUID(), ...(project_id && { project_id }), ...(resolution && { resolution }), ...(max_credits !== undefined && { max_credits }), ...(free_trial && { free_trial: true }) };
145
+ async ({ slug, inputs, dna, preview, project_id, resolution, max_credits, free_trial, idempotency_key, wait, wait_seconds }) => {
146
+ const body = { inputs, ...(dna && { dna }), ...(preview && { preview: true }), idempotency_key: idempotency_key || randomUUID(), ...(project_id && { project_id }), ...(resolution && { resolution }), ...(max_credits !== undefined && { max_credits }), ...(free_trial && { free_trial: true }) };
140
147
  const started = unwrap(await client.post(`/v1/trends/${slugPath(slug)}/runs`, body));
141
148
  started.idempotency_key = body.idempotency_key;
142
149
  if (wait === false || TERMINAL.has(started.status)) return runResult(started);