@kolbo/mcp 1.30.1 → 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.1",
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
 
@@ -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) {
@@ -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