@kolbo/mcp 1.30.0 → 1.30.2

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.30.0",
3
+ "version": "1.30.2",
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": {
@@ -91,9 +91,13 @@ const BRIDGE_JS = `
91
91
  }).catch(function () { /* host without apps support — widget stays static */ });
92
92
 
93
93
  function notifySize() {
94
- var el = document.documentElement;
94
+ // Measure the widget card itself — documentElement.scrollHeight over-reports
95
+ // in some hosts and leaves a huge empty iframe below the card.
96
+ var card = document.querySelector('.k-card');
97
+ var rect = card ? card.getBoundingClientRect() : null;
98
+ var height = rect ? Math.ceil(rect.bottom + 8) : document.documentElement.scrollHeight;
95
99
  notify('ui/notifications/size-changed', {
96
- width: el.scrollWidth, height: el.scrollHeight
100
+ width: document.documentElement.scrollWidth, height: height
97
101
  });
98
102
  }
99
103
 
package/src/apps/index.js CHANGED
@@ -42,6 +42,26 @@ function widgetHtml(uri) {
42
42
  return htmlCache.get(uri);
43
43
  }
44
44
 
45
+ // Hosts apply a deny-by-default CSP to widget iframes — without this
46
+ // declaration EVERY external asset (generated images/videos on the CDN, model
47
+ // icons, Google Fonts) is silently blocked. resourceDomains maps to
48
+ // img/script/style/font/media-src; connectDomains to connect-src.
49
+ const WIDGET_CSP = {
50
+ resourceDomains: [
51
+ 'https://*.kolbo.ai', // media.kolbo.ai CDN + app.kolbo.ai model icons
52
+ 'https://*.digitaloceanspaces.com', // DO Spaces buckets (all envs)
53
+ 'https://*.cdn.digitaloceanspaces.com', // DO Spaces CDN endpoints
54
+ 'https://fonts.googleapis.com', // Inter / JetBrains Mono stylesheet
55
+ 'https://fonts.gstatic.com', // font files
56
+ 'https://images.pexels.com', // stock thumbnails
57
+ 'https://*.pexels.com',
58
+ 'https://*.pixabay.com',
59
+ 'https://*.sketchfab.com',
60
+ 'https://*.cloudfront.net', // provider-hosted previews
61
+ ],
62
+ connectDomains: [],
63
+ };
64
+
45
65
  /** Register all Kolbo widget resources on an McpServer. */
46
66
  function registerApps(server) {
47
67
  for (const [uri, name] of [
@@ -50,9 +70,16 @@ function registerApps(server) {
50
70
  [UI.catalog, 'Kolbo Model Catalog Widget'],
51
71
  [UI.transcript, 'Kolbo Transcription Widget'],
52
72
  ]) {
53
- registerAppResource(server, name, uri, { mimeType: RESOURCE_MIME_TYPE }, async () => ({
54
- contents: [{ uri, mimeType: RESOURCE_MIME_TYPE, text: widgetHtml(uri) }],
55
- }));
73
+ registerAppResource(
74
+ server, name, uri,
75
+ { mimeType: RESOURCE_MIME_TYPE, _meta: { csp: WIDGET_CSP, ui: { csp: WIDGET_CSP } } },
76
+ async () => ({
77
+ contents: [{
78
+ uri, mimeType: RESOURCE_MIME_TYPE, text: widgetHtml(uri),
79
+ _meta: { csp: WIDGET_CSP, ui: { csp: WIDGET_CSP } },
80
+ }],
81
+ })
82
+ );
56
83
  }
57
84
  }
58
85
 
@@ -130,9 +157,64 @@ async function modelIcon(client, modelName) {
130
157
  return map.get(String(modelName).toLowerCase()) || null;
131
158
  }
132
159
 
160
+ /* ------------------------------------------------------------------ */
161
+ /* Declaration-level widget metadata */
162
+ /* ------------------------------------------------------------------ */
163
+
164
+ // Hosts (claude.ai) decide whether to prepare a widget iframe from the TOOL
165
+ // DECLARATION in tools/list — result-level `_meta` alone is not enough. The
166
+ // legacy server.tool() registration API has no _meta parameter, so we attach
167
+ // it post-registration via the SDK's registered-tool objects (tools/list
168
+ // serves `tool._meta` verbatim; verified against SDK 1.29.0).
169
+ const TOOL_WIDGETS = {
170
+ // generation card
171
+ generate_image: UI.generation,
172
+ generate_image_edit: UI.generation,
173
+ generate_creative_director: UI.generation,
174
+ generate_video: UI.generation,
175
+ generate_video_from_image: UI.generation,
176
+ generate_video_from_video: UI.generation,
177
+ generate_elements: UI.generation,
178
+ generate_first_last_frame: UI.generation,
179
+ generate_lipsync: UI.generation,
180
+ generate_music: UI.generation,
181
+ generate_speech: UI.generation,
182
+ generate_sound: UI.generation,
183
+ generate_3d: UI.generation,
184
+ edit_image: UI.generation,
185
+ edit_video: UI.generation,
186
+ shorts_render: UI.generation,
187
+ // transcript viewer
188
+ transcribe_audio: UI.transcript,
189
+ // model catalog
190
+ list_models: UI.catalog,
191
+ // media grid
192
+ list_media: UI.mediaGrid,
193
+ search_stock_media: UI.mediaGrid,
194
+ get_stock_collections: UI.mediaGrid,
195
+ search_music_library: UI.mediaGrid,
196
+ browse_music_library: UI.mediaGrid,
197
+ list_presets: UI.mediaGrid,
198
+ list_voices: UI.mediaGrid,
199
+ list_visual_dnas: UI.mediaGrid,
200
+ list_moodboards: UI.mediaGrid,
201
+ shorts_analyze: UI.mediaGrid,
202
+ };
203
+
204
+ function attachToolWidgetMeta(server) {
205
+ const registered = server && server._registeredTools;
206
+ if (!registered) return;
207
+ for (const [name, uri] of Object.entries(TOOL_WIDGETS)) {
208
+ const tool = registered[name];
209
+ if (!tool) continue;
210
+ tool._meta = { ...(tool._meta || {}), ...uiMeta(uri) };
211
+ }
212
+ }
213
+
133
214
  module.exports = {
134
215
  UI,
135
216
  registerApps,
217
+ attachToolWidgetMeta,
136
218
  uiMeta,
137
219
  uiResult,
138
220
  appsEnabled,
@@ -34,20 +34,22 @@ function boot(sc) {
34
34
  if (!sc || !sc.groups) return;
35
35
  state = sc;
36
36
  el('title').textContent = sc.title || 'AI Models';
37
- var total = sc.groups.reduce(function (n, g) { return n + g.models.length; }, 0);
37
+ var shown = sc.groups.reduce(function (n, g) { return n + g.models.length; }, 0);
38
38
  el('count-chip').style.display = '';
39
- el('count-chip').textContent = total + ' models';
39
+ el('count-chip').textContent = sc.total_available && sc.total_available > shown
40
+ ? 'top picks · ' + sc.total_available + ' total'
41
+ : shown + ' models';
40
42
  el('stage').innerHTML = sc.groups.map(function (g, gi) {
41
- return '<div style="margin-bottom:14px">' +
42
- '<div style="font-size:12px;font-weight:600;color:var(--text-muted);margin-bottom:8px">' + esc(g.name) + '</div>' +
43
+ return '<div style="margin-bottom:10px">' +
44
+ '<div style="font-size:11px;font-weight:600;letter-spacing:.04em;text-transform:uppercase;color:var(--text-faint);margin-bottom:6px">' + esc(g.name) + '</div>' +
43
45
  g.models.map(function (m, mi) {
44
- return '<div class="k-audio-row" data-g="' + gi + '" data-m="' + mi + '" style="cursor:pointer">' +
45
- (m.icon ? '<img class="k-audio-art" style="width:32px;height:32px" src="' + esc(m.icon) + '" onerror="this.outerHTML=monogram(\\'' + esc(m.name).replace(/'/g, '') + '\\')">'
46
+ return '<div class="k-audio-row" data-g="' + gi + '" data-m="' + mi + '" style="cursor:pointer;padding:6px 8px;margin-bottom:4px">' +
47
+ (m.icon ? '<img class="k-audio-art" style="width:24px;height:24px;border-radius:6px" src="' + esc(m.icon) + '" onerror="this.outerHTML=monogram(\\'' + esc(m.name).replace(/'/g, '') + '\\')">'
46
48
  : '<span style="flex:none">' + monogram(m.name) + '</span>') +
47
- '<div class="k-audio-meta"><div class="k-audio-title">' + esc(m.name) + '</div>' +
48
- (m.description ? '<div class="k-audio-sub">' + esc(m.description) + '</div>' : '') + '</div>' +
49
+ '<div class="k-audio-meta"><div class="k-audio-title" style="font-size:12px">' + esc(m.name) + '</div>' +
50
+ (m.description ? '<div class="k-audio-sub" style="font-size:10.5px">' + esc(m.description) + '</div>' : '') + '</div>' +
49
51
  '<div style="display:flex;gap:4px;flex:none">' + (m.chips || []).slice(0, 3).map(function (c) {
50
- return '<span class="k-chip">' + esc(c) + '</span>';
52
+ return '<span class="k-chip" style="padding:2px 7px;font-size:10px">' + esc(c) + '</span>';
51
53
  }).join('') + '</div></div>';
52
54
  }).join('') + '</div>';
53
55
  }).join('');
@@ -98,7 +98,7 @@ function renderChips(sc) {
98
98
  if (s.voice) h += chip('🎤 ' + esc(s.voice));
99
99
  if (s.mode) h += chip(esc(s.mode));
100
100
  if (sc.count > 1) h += chip('×' + sc.count);
101
- if (sc.reference_image) h += '<img class="k-ref-thumb" src="' + esc(sc.reference_image) + '" alt="ref" title="Reference image">';
101
+ if (sc.reference_image) h += '<img class="k-ref-thumb" src="' + esc(sc.reference_image) + '" alt="" title="Reference image" onerror="this.style.display=\'none\'">';
102
102
  el('chips').innerHTML = h;
103
103
  }
104
104
  function chip(inner) { return '<span class="k-chip">' + inner + '</span>'; }
@@ -166,7 +166,6 @@ function poll(sc) {
166
166
  } else if (stateName === 'failed' || stateName === 'error' || stateName === 'cancelled') {
167
167
  renderError(st.error || 'Generation ' + stateName);
168
168
  } else {
169
- if (st.progress != null) el('status-text').textContent = 'Generating… ' + Math.round(st.progress) + '%';
170
169
  schedulePoll(sc);
171
170
  }
172
171
  }).catch(function () { schedulePoll(sc); });
@@ -196,7 +195,10 @@ function renderResult(sc) {
196
195
 
197
196
  function renderImages(sc, urls) {
198
197
  selected = Math.min(selected, urls.length - 1);
199
- var viewer = '<div class="k-viewer"><img id="main-img" src="' + esc(urls[selected]) + '" alt=""></div>';
198
+ // If the host CSP still blocks the image, degrade to open-in-browser rows
199
+ // instead of a broken empty viewer.
200
+ var viewer = '<div class="k-viewer"><img id="main-img" src="' + esc(urls[selected]) + '" alt="" onerror="window.__imgFail && window.__imgFail()"></div>';
201
+ window.__imgFail = function () { renderLinks(urls); window.kolbo.notifySize(); };
200
202
  var thumbs = '';
201
203
  if (urls.length > 1) {
202
204
  thumbs = '<div class="k-thumbs">' + urls.map(function (u, i) {
package/src/index.js CHANGED
@@ -73,7 +73,7 @@ const { registerVoiceTools } = require('./tools/voices');
73
73
  const { registerMusicLibraryTools } = require('./tools/music_library');
74
74
  const { registerStockLibraryTools } = require('./tools/stock_library');
75
75
  const { registerShortsCreatorTools } = require('./tools/shorts_creator');
76
- const { registerApps } = require('./apps');
76
+ const { registerApps, attachToolWidgetMeta } = require('./apps');
77
77
 
78
78
  /**
79
79
  * Build a fully-configured Kolbo MCP server (all tool groups registered)
@@ -123,6 +123,9 @@ function createServer(opts = {}) {
123
123
  // MCP Apps widget resources (ui://kolbo/*). Registering resources is inert
124
124
  // for text-only hosts — they never fetch them.
125
125
  registerApps(server);
126
+ // Declaration-level `_meta['ui/resourceUri']` on every widget-carrying tool —
127
+ // claude.ai prepares the widget iframe from tools/list, not from the result.
128
+ attachToolWidgetMeta(server);
126
129
 
127
130
  return server;
128
131
  }
@@ -43,14 +43,34 @@ function modelChips(m) {
43
43
  }
44
44
 
45
45
  // structuredContent for ui://kolbo/catalog.html — see src/apps/widgets/catalog.js
46
+ // Deliberately CURATED, not exhaustive: the widget is a picker, not a database.
47
+ // Smart Select is pinned first, each group shows the recommended/new models
48
+ // (max 6), and the total count chip tells the user how many exist overall.
46
49
  function buildCatalogStructured(models, type) {
47
50
  const groups = [];
48
51
  const byName = new Map();
49
- for (const m of models) {
52
+ const isAuto = (m) => /^auto$|smart.select/i.test(String(m.name || '')) || /smart-select|k_auto/i.test(String(m.identifier || ''));
53
+
54
+ // Pinned Smart Select entry (replaces the confusing "Other / Auto" row).
55
+ const smartSelect = {
56
+ name: 'Smart Select',
57
+ icon: null,
58
+ description: 'Recommended — automatically routes to the best model for your prompt, quality and cost.',
59
+ chips: ['AUTO'],
60
+ use_hint: 'Generate with Smart Select (omit the model) — ask me what I want to create first.',
61
+ };
62
+
63
+ // Recommended + new models float to the top of each group.
64
+ const ranked = [...models].filter((m) => !isAuto(m)).sort((a, b) => {
65
+ const score = (m) => (m.recommended ? 2 : 0) + (m.new_model || m.newModel ? 1 : 0);
66
+ return score(b) - score(a);
67
+ });
68
+
69
+ for (const m of ranked) {
50
70
  const name = groupNameFor(m);
51
71
  let g = byName.get(name);
52
72
  if (!g) { g = { name, models: [] }; byName.set(name, g); groups.push(g); }
53
- if (g.models.length >= 12) continue; // cap per group (≤60 total across groups)
73
+ if (g.models.length >= 6) continue; // curated cap full list lives in the text payload
54
74
  g.models.push({
55
75
  name: m.name,
56
76
  icon: m.avatar
@@ -61,10 +81,12 @@ function buildCatalogStructured(models, type) {
61
81
  use_hint: `Generate with the "${m.name}" model — ask me what I want to create first.`,
62
82
  });
63
83
  }
84
+ groups.sort((a, b) => (a.name === 'Other' ? 1 : b.name === 'Other' ? -1 : 0));
64
85
  return {
65
86
  widget: 'catalog',
66
87
  title: 'Kolbo AI Models' + (type ? ' — ' + type : ''),
67
- groups,
88
+ total_available: models.length,
89
+ groups: [{ name: 'Recommended', models: [smartSelect] }, ...groups],
68
90
  };
69
91
  }
70
92