@kolbo/mcp 1.72.3 → 1.72.4

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.72.3",
3
+ "version": "1.72.4",
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": {
@@ -164,7 +164,8 @@ function renderChips(sc) {
164
164
  var h = modelChipHTML(modelLabel(sc), sc.model_icon);
165
165
  var s = sc.settings || {};
166
166
  if (sc.kind) h += chip(iconFor(sc.kind) + ' ' + sc.kind);
167
- if (s.duration) h += chip(ICONS.clock + ' ' + fmtDur(s.duration));
167
+ if (s.duration) h += chip(ICONS.clock + ' ' + fmtDur(s.duration) + (s.shots > 1 ? ' · ' + s.shots + ' shots' : ''));
168
+ else if (s.shots > 1) h += chip(s.shots + ' shots');
168
169
  if (s.resolution) h += chip(esc(s.resolution));
169
170
  if (s.aspect_ratio) h += chip(esc(s.aspect_ratio));
170
171
  if (s.quality) h += chip(esc(s.quality) + ' quality');
@@ -125,7 +125,13 @@ const imageSettings = (a = {}) => ({
125
125
  // Same block for the video tools, which carried only { duration, resolution,
126
126
  // aspect_ratio } — a DNA-anchored video card showed no sign a DNA was in play.
127
127
  const videoSettings = (a = {}) => ({
128
- duration: a.duration,
128
+ // Multi-shot elements calls usually set shot COUNT and let the model take the
129
+ // default per-shot length, so `duration` came through undefined and the card
130
+ // showed model/resolution/ratio but never how long the video actually is —
131
+ // the one number the user picked and is paying for. Fall back to what the API
132
+ // echoed back on start, then to per-shot × shots.
133
+ duration: a.duration ?? a.reported_duration ?? (a.shot_duration && a.shots ? a.shot_duration * a.shots : undefined),
134
+ ...(a.shots > 1 ? { shots: a.shots } : {}),
129
135
  resolution: a.resolution,
130
136
  aspect_ratio: a.aspect_ratio,
131
137
  ...(a.mode ? { mode: a.mode } : {}),
@@ -1093,7 +1099,12 @@ function registerGenerateTools(server, client, options = {}) {
1093
1099
 
1094
1100
  if (ui()) return uiGenerating({
1095
1101
  tool: 'generate_elements', kind: 'video', gen: startResponse, client, model, prompt,
1096
- settings: videoSettings({ duration, resolution, aspect_ratio, enhance_prompt, visual_dna_ids, preset_id }),
1102
+ settings: videoSettings({
1103
+ duration,
1104
+ reported_duration: startResponse?.duration ?? startResponse?.result?.duration,
1105
+ shots: multi_shot_count ?? (Array.isArray(multi_shots) ? multi_shots.length : undefined),
1106
+ resolution, aspect_ratio, enhance_prompt, visual_dna_ids, preset_id,
1107
+ }),
1097
1108
  reference_images: [
1098
1109
  ...(reference_images || []),
1099
1110
  ...(keyframes || []).map((keyframe) => keyframe.image_url),
@@ -6,7 +6,7 @@
6
6
  const { z } = require('zod');
7
7
  const FormData = require('form-data');
8
8
  const { resolveToBuffer, DEFAULT_MAX_FILE_MB, compactList } = require('./_shared');
9
- const { UI, uiResult, listResult, appsEnabled } = require('../apps');
9
+ const { UI, uiResult, listResult } = require('../apps');
10
10
 
11
11
  // How many tiles the media grid renders. A rendering limit only — the text
12
12
  // payload always carries the full page, and `total` reports the real library
@@ -65,8 +65,6 @@ function uploadTicketPayload(ticket) {
65
65
  }
66
66
 
67
67
  function registerMediaTools(server, client, options = {}) {
68
- const ui = () => appsEnabled(server, options);
69
-
70
68
  // `opts.apps` is set only by kolbo-api's per-request server (see createServer
71
69
  // in ../index.js), which makes it a TRANSPORT signal — deliberately not
72
70
  // `appsEnabled()`, which also returns true for stdio hosts that advertise UI.
@@ -101,24 +99,27 @@ function registerMediaTools(server, client, options = {}) {
101
99
  expires_in_seconds: ticket.expires_in,
102
100
  };
103
101
 
104
- if (ui()) {
105
- // upload_ui_url: top-level page for Claude iOS/Android in-iframe
106
- // <input type=file> selections are dropped by WebKit (see upload widget).
107
- const uploadUiUrl = ticket.upload_ui_url
108
- || String(ticket.upload_url || '').replace(/\/upload\/?$/, '/upload-ui');
109
- return uiResult(UI.upload, JSON.stringify(info, null, 2), {
110
- widget: 'upload',
111
- title: purpose || 'Upload media',
112
- upload_url: ticket.upload_url,
113
- upload_ui_url: uploadUiUrl,
114
- token: ticket.token,
115
- expires_at: Date.now() + (ticket.expires_in || 900) * 1000,
116
- kinds: media_types && media_types.length ? media_types : undefined,
117
- max_files: Math.min(Math.max(Number(max_files) || 10, 1), 20),
118
- max_mb: ticket.max_file_mb || DEFAULT_MAX_FILE_MB,
119
- ...(project_id ? { project_id } : {}),
120
- });
121
- }
102
+ // Always ship structuredContent. Kolbo Code does NOT advertise MCP Apps, so
103
+ // gating the grid payload on appsEnabled() sent it text only; the host then
104
+ // rebuilt items from the compactList text, whose field names are
105
+ // `filename`/`url` not the `title`/`thumbnail` the grid renders — so every
106
+ // tile came out black and unlabelled. Same reasoning as listResult().
107
+ // upload_ui_url: top-level page for Claude iOS/Android — in-iframe
108
+ // <input type=file> selections are dropped by WebKit (see upload widget).
109
+ const uploadUiUrl = ticket.upload_ui_url
110
+ || String(ticket.upload_url || '').replace(/\/upload\/?$/, '/upload-ui');
111
+ return uiResult(UI.upload, JSON.stringify(info, null, 2), {
112
+ widget: 'upload',
113
+ title: purpose || 'Upload media',
114
+ upload_url: ticket.upload_url,
115
+ upload_ui_url: uploadUiUrl,
116
+ token: ticket.token,
117
+ expires_at: Date.now() + (ticket.expires_in || 900) * 1000,
118
+ kinds: media_types && media_types.length ? media_types : undefined,
119
+ max_files: Math.min(Math.max(Number(max_files) || 10, 1), 20),
120
+ max_mb: ticket.max_file_mb || DEFAULT_MAX_FILE_MB,
121
+ ...(project_id ? { project_id } : {}),
122
+ });
122
123
 
123
124
  // Text-only host (Claude Code, Codex CLI, Cursor): no iframe to render —
124
125
  // but these are exactly the hosts that CAN reach a filesystem, so hand
@@ -263,30 +264,33 @@ function registerMediaTools(server, client, options = {}) {
263
264
  note: 'Narrow with `type`, `category`, `project_id`, `folder_id`, or `search`; get_media returns one item in full.',
264
265
  });
265
266
 
266
- if (ui()) {
267
- // The SDK envelope reports `total_items` (see sdk/controller.js listMedia);
268
- // reading `total` always came back undefined, so the grid claimed the page
269
- // size was the whole library. Accept either, then fall back.
270
- const totalItems = pagination
271
- ? (pagination.total_items != null ? pagination.total_items : pagination.total)
272
- : null;
273
- const items = media.slice(0, GRID_CAP).map((m) => ({
274
- id: m.id,
275
- title: m.filename,
276
- subtitle: m.media_type + (m.size ? ' · ' + Math.round(m.size / 1024) + 'KB' : ''),
277
- thumbnail: m.media_type === 'image' ? m.url : (m.thumbnail_url || null),
278
- media_type: m.media_type,
279
- url: m.url,
280
- use_hint: 'Use this media library asset in my next step:\nURL: {URL}\n(id: {ID})'
281
- }));
282
- return uiResult(UI.mediaGrid, text, {
283
- widget: 'media-grid',
284
- title: 'Media Library',
285
- items,
286
- total: totalItems != null ? totalItems : media.length,
287
- shown: Math.min(media.length, GRID_CAP)
288
- });
289
- }
267
+ // Always ship structuredContent. Kolbo Code does NOT advertise MCP Apps, so
268
+ // gating the grid payload on appsEnabled() sent it text only; the host then
269
+ // rebuilt items from the compactList text, whose field names are
270
+ // `filename`/`url` not the `title`/`thumbnail` the grid renders so every
271
+ // tile came out black and unlabelled. Same reasoning as listResult().
272
+ // The SDK envelope reports `total_items` (see sdk/controller.js listMedia);
273
+ // reading `total` always came back undefined, so the grid claimed the page
274
+ // size was the whole library. Accept either, then fall back.
275
+ const totalItems = pagination
276
+ ? (pagination.total_items != null ? pagination.total_items : pagination.total)
277
+ : null;
278
+ const items = media.slice(0, GRID_CAP).map((m) => ({
279
+ id: m.id,
280
+ title: m.filename,
281
+ subtitle: m.media_type + (m.size ? ' · ' + Math.round(m.size / 1024) + 'KB' : ''),
282
+ thumbnail: m.media_type === 'image' ? m.url : (m.thumbnail_url || null),
283
+ media_type: m.media_type,
284
+ url: m.url,
285
+ use_hint: 'Use this media library asset in my next step:\nURL: {URL}\n(id: {ID})'
286
+ }));
287
+ return uiResult(UI.mediaGrid, text, {
288
+ widget: 'media-grid',
289
+ title: 'Media Library',
290
+ items,
291
+ total: totalItems != null ? totalItems : media.length,
292
+ shown: Math.min(media.length, GRID_CAP)
293
+ });
290
294
 
291
295
  return {
292
296
  content: [{
@@ -4,11 +4,10 @@
4
4
  * new OPTIONAL args only. Full rules: ../index.js top-of-file and CLAUDE.md. */
5
5
 
6
6
  const { z } = require('zod');
7
- const { UI, uiResult, appsEnabled } = require('../apps');
7
+ const { UI, uiResult } = require('../apps');
8
8
  const { projectScopeReadField } = require('./_shared');
9
9
 
10
10
  function registerMoodboardTools(server, client, options = {}) {
11
- const ui = () => appsEnabled(server, options);
12
11
  // ─── list_moodboards ───────────────────────────────────────
13
12
  server.tool(
14
13
  'list_moodboards',
@@ -29,25 +28,26 @@ function registerMoodboardTools(server, client, options = {}) {
29
28
  count: result.count || 0
30
29
  }, null, 2);
31
30
 
32
- if (ui()) {
33
- return uiResult(UI.mediaGrid, text, {
34
- widget: 'media-grid',
35
- title: 'Moodboards',
36
- items: moodboards.slice(0, 24).map(mb => ({
37
- id: mb.id,
38
- title: mb.name,
39
- // API returns thumbnail_url + images[] (sdk listMoodboards) — both
40
- // previous keys were wrong, so the fallback never fired either.
41
- thumbnail: mb.thumbnail_url || mb.thumbnail || (Array.isArray(mb.images) ? mb.images[0] : undefined),
42
- media_type: 'image',
43
- use_hint: 'Apply moodboard "{TITLE}" (moodboard_id: {ID}) to my next generation.'
44
- })),
45
- total: result.count || moodboards.length,
46
- has_more: moodboards.length > 24
47
- });
48
- }
49
-
50
- return { content: [{ type: 'text', text }] };
31
+ // Always ship structuredContent. Kolbo Code does NOT advertise MCP Apps, so
32
+ // gating the grid payload on appsEnabled() sent it text only; the host then
33
+ // rebuilt items from the compactList text, whose field names are
34
+ // `filename`/`url` — not the `title`/`thumbnail` the grid renders — so every
35
+ // tile came out black and unlabelled. Same reasoning as listResult().
36
+ return uiResult(UI.mediaGrid, text, {
37
+ widget: 'media-grid',
38
+ title: 'Moodboards',
39
+ items: moodboards.slice(0, 24).map(mb => ({
40
+ id: mb.id,
41
+ title: mb.name,
42
+ // API returns thumbnail_url + images[] (sdk listMoodboards) both
43
+ // previous keys were wrong, so the fallback never fired either.
44
+ thumbnail: mb.thumbnail_url || mb.thumbnail || (Array.isArray(mb.images) ? mb.images[0] : undefined),
45
+ media_type: 'image',
46
+ use_hint: 'Apply moodboard "{TITLE}" (moodboard_id: {ID}) to my next generation.'
47
+ })),
48
+ total: result.count || moodboards.length,
49
+ has_more: moodboards.length > 24
50
+ });
51
51
  }
52
52
  );
53
53
 
@@ -6,7 +6,7 @@
6
6
  const { z } = require('zod');
7
7
  const FormData = require('form-data');
8
8
  const { resolveToBuffer: sharedResolveToBuffer, VISUAL_DNA_MAX_BYTES, projectScopeReadField, compactList } = require('./_shared');
9
- const { UI, uiResult, listResult, appsEnabled } = require('../apps');
9
+ const { UI, uiResult, listResult } = require('../apps');
10
10
 
11
11
  // Reference sheets are a blocking multi-panel render; the 120s client default
12
12
  // aborted them mid-flight while the server finished and charged anyway.
@@ -20,7 +20,6 @@ function resolveToBuffer(source, kind) {
20
20
  }
21
21
 
22
22
  function registerVisualDnaTools(server, client, options = {}) {
23
- const ui = () => appsEnabled(server, options);
24
23
  // ─── create_visual_dna ─────────────────────────────────────
25
24
  server.tool(
26
25
  'create_visual_dna',
@@ -133,24 +132,25 @@ function registerVisualDnaTools(server, client, options = {}) {
133
132
  note: 'Narrow with `search`, `tags`, or `collection`, or pass `page`/`limit` for the rest; get_visual_dna returns one in full.',
134
133
  });
135
134
 
136
- if (ui()) {
137
- return uiResult(UI.mediaGrid, text, {
138
- widget: 'media-grid',
139
- title: 'Visual DNA Profiles',
140
- items: dnas.slice(0, 24).map(d => ({
141
- id: d.id,
142
- title: d.name,
143
- subtitle: (d.dna_type || '') + (Array.isArray(d.tags) && d.tags.length ? ' · ' + d.tags.slice(0, 3).join(', ') : ''),
144
- thumbnail: d.thumbnail_url || d.thumbnail,
145
- media_type: 'image',
146
- use_hint: 'Use Visual DNA "{TITLE}" (id: {ID}) in my next generation for character/style consistency.'
147
- })),
148
- total,
149
- has_more: result.has_more || dnas.length > 24
150
- });
151
- }
152
-
153
- return { content: [{ type: 'text', text }] };
135
+ // Always ship structuredContent. Kolbo Code does NOT advertise MCP Apps, so
136
+ // gating the grid payload on appsEnabled() sent it text only; the host then
137
+ // rebuilt items from the compactList text, whose field names are
138
+ // `filename`/`url` — not the `title`/`thumbnail` the grid renders — so every
139
+ // tile came out black and unlabelled. Same reasoning as listResult().
140
+ return uiResult(UI.mediaGrid, text, {
141
+ widget: 'media-grid',
142
+ title: 'Visual DNA Profiles',
143
+ items: dnas.slice(0, 24).map(d => ({
144
+ id: d.id,
145
+ title: d.name,
146
+ subtitle: (d.dna_type || '') + (Array.isArray(d.tags) && d.tags.length ? ' · ' + d.tags.slice(0, 3).join(', ') : ''),
147
+ thumbnail: d.thumbnail_url || d.thumbnail,
148
+ media_type: 'image',
149
+ use_hint: 'Use Visual DNA "{TITLE}" (id: {ID}) in my next generation for character/style consistency.'
150
+ })),
151
+ total,
152
+ has_more: result.has_more || dnas.length > 24
153
+ });
154
154
  }
155
155
  );
156
156