@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 +1 -1
- package/src/apps/bridge.js +6 -2
- package/src/apps/index.js +85 -3
- package/src/apps/widgets/catalog.js +11 -9
- package/src/apps/widgets/generation.js +5 -3
- package/src/index.js +4 -1
- package/src/tools/models.js +25 -3
package/package.json
CHANGED
package/src/apps/bridge.js
CHANGED
|
@@ -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
|
-
|
|
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:
|
|
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(
|
|
54
|
-
|
|
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
|
|
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 =
|
|
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:
|
|
42
|
-
'<div style="font-size:
|
|
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:
|
|
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="
|
|
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
|
-
|
|
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
|
}
|
package/src/tools/models.js
CHANGED
|
@@ -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
|
-
|
|
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 >=
|
|
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
|
-
|
|
88
|
+
total_available: models.length,
|
|
89
|
+
groups: [{ name: 'Recommended', models: [smartSelect] }, ...groups],
|
|
68
90
|
};
|
|
69
91
|
}
|
|
70
92
|
|