@kolbo/mcp 1.30.10 → 1.31.1

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.10",
3
+ "version": "1.31.1",
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": {
@@ -33,7 +33,7 @@ const BRIDGE_JS = `
33
33
  var initialized = false;
34
34
  var queue = []; // deferred host-bound sends until initialized
35
35
  var hostContext = null;
36
- var readyFns = [], toolResultFns = [], themeFns = [];
36
+ var readyFns = [], toolResultFns = [], themeFns = [], toolInputFns = [];
37
37
 
38
38
  function post(msg) { window.parent.postMessage(msg, '*'); }
39
39
 
@@ -66,6 +66,10 @@ const BRIDGE_JS = `
66
66
  }
67
67
  if (m.method === 'ui/notifications/tool-result') {
68
68
  toolResultFns.forEach(function (f) { try { f(m.params || {}); } catch (e) {} });
69
+ } else if (m.method === 'ui/notifications/tool-input' || m.method === 'ui/notifications/tool-input-partial') {
70
+ // Fires while the tool is still RUNNING — lets widgets show a real
71
+ // "preparing" state instead of a blank card until the result lands.
72
+ toolInputFns.forEach(function (f) { try { f((m.params && m.params.arguments) || {}); } catch (e) {} });
69
73
  } else if (m.method === 'ui/notifications/host-context-changed') {
70
74
  hostContext = (m.params && m.params.hostContext) || m.params || hostContext;
71
75
  themeFns.forEach(function (f) { try { f(hostContext); } catch (e) {} });
@@ -132,6 +136,7 @@ const BRIDGE_JS = `
132
136
  window.kolbo = {
133
137
  ready: function (f) { if (initialized) f(hostContext); else readyFns.push(f); },
134
138
  onToolResult: function (f) { toolResultFns.push(f); },
139
+ onToolInput: function (f) { toolInputFns.push(f); },
135
140
  onThemeChange: function (f) { themeFns.push(f); },
136
141
  callTool: function (name, args) { return request('tools/call', { name: name, arguments: args || {} }); },
137
142
  sendMessage: function (text) {
@@ -141,6 +146,10 @@ const BRIDGE_JS = `
141
146
  updateModelContext: function (text) {
142
147
  return request('ui/update-model-context', { content: [{ type: 'text', text: text }] });
143
148
  },
149
+ // Resolves with the mode the host ACTUALLY granted ('inline'|'fullscreen'|'pip').
150
+ requestDisplayMode: function (mode) {
151
+ return request('ui/request-display-mode', { mode: mode });
152
+ },
144
153
  notifySize: notifySize,
145
154
  hostContext: function () { return hostContext; }
146
155
  };
package/src/apps/theme.js CHANGED
@@ -161,11 +161,21 @@ body {
161
161
 
162
162
  /* Keep the whole completed card under the host's iframe height cap (~800px):
163
163
  header + prompt + chips + viewer + thumbs + actions + footer must all fit,
164
- or claude.ai adds an inner scrollbar. Click the image to open it full-size. */
164
+ or claude.ai adds an inner scrollbar. Click the image to expand in-Claude. */
165
165
  .k-viewer { margin-bottom: 10px; }
166
166
  .k-viewer img, .k-viewer video { display: block; width: 100%; max-height: 340px; object-fit: contain;
167
167
  border-radius: 12px; background: #000; border: 1px solid var(--border); cursor: zoom-in; }
168
168
  .k-viewer video { cursor: default; }
169
+
170
+ /* ---- Fullscreen (ui/request-display-mode granted) ---- */
171
+ html.k-fullscreen, html.k-fullscreen body { height: 100%; }
172
+ html.k-fullscreen .k-card { height: 100%; display: flex; flex-direction: column; border-radius: 0; }
173
+ html.k-fullscreen .k-body { flex: 1; min-height: 0; display: flex; flex-direction: column; }
174
+ html.k-fullscreen .k-viewer { flex: 1; min-height: 0; display: flex; align-items: center; justify-content: center; }
175
+ html.k-fullscreen .k-viewer img, html.k-fullscreen .k-viewer video {
176
+ max-height: 100%; max-width: 100%; width: auto; margin: 0 auto; cursor: zoom-out; }
177
+ html.k-fullscreen .k-thumbs .k-thumb { width: 64px; height: 64px; }
178
+ .k-expand-hint { display: none; }
169
179
  .k-thumbs { display: flex; gap: 6px; margin: 10px 0 2px; }
170
180
  .k-thumbs .k-thumb { width: 48px; height: 48px; border-radius: 8px; overflow: hidden; cursor: pointer;
171
181
  border: 2px solid transparent; opacity: 0.75; transition: all 150ms var(--smooth); flex: none; }
@@ -64,7 +64,12 @@ function boot(sc) {
64
64
 
65
65
  window.kolbo.onToolResult(function (result) {
66
66
  var sc = result.structuredContent || structured(result);
67
- if (sc) boot(sc);
67
+ if (sc && sc.groups) return boot(sc);
68
+ // Internal lookup (Claude checking constraints mid-task) — no catalog data
69
+ // was attached on purpose. Collapse instead of showing a dead card.
70
+ var card = document.querySelector('.k-card');
71
+ if (card) card.style.display = 'none';
72
+ window.kolbo.notifySize();
68
73
  });
69
74
  `;
70
75
 
@@ -173,10 +173,12 @@ function renderImages(sc, urls) {
173
173
  // instead of a broken empty viewer.
174
174
  var viewer = '<div class="k-viewer"><img id="main-img" src="' + esc(urls[selected]) + '" alt="" onerror="window.__imgFail && window.__imgFail()"></div>';
175
175
  window.__imgFail = function () { renderLinks(urls); window.kolbo.notifySize(); };
176
- // Constrained viewer click opens the original full-size.
176
+ // Clickexpand into an in-Claude fullscreen viewer (all actions stay
177
+ // available); click again (or Exit) collapses back. Hosts that refuse
178
+ // fullscreen fall back to opening the original in a new tab.
177
179
  setTimeout(function () {
178
180
  var img = el('main-img');
179
- if (img) img.onclick = function () { window.kolbo.openLink(state.urls[selected]); };
181
+ if (img) img.onclick = toggleFullscreen;
180
182
  }, 0);
181
183
  var thumbs = '';
182
184
  if (urls.length > 1) {
@@ -257,11 +259,44 @@ function renderError(msg) {
257
259
  el('stage').innerHTML = '<div class="k-error">⚠ ' + esc(msg) + '</div>';
258
260
  el('actions').innerHTML = '<button class="k-btn" id="retry-btn">↻ Try Again</button>';
259
261
  el('retry-btn').onclick = function () {
260
- window.kolbo.sendMessage('Please retry that ' + (TOOL_TITLES[state.tool] || 'generation').toLowerCase() + ' — it failed with: ' + msg);
262
+ var what = (state && TOOL_TITLES[state.tool]) || 'generation';
263
+ window.kolbo.sendMessage('Please retry that ' + what.toLowerCase() + ' — it failed with: ' + msg);
261
264
  };
262
265
  window.kolbo.notifySize();
263
266
  }
264
267
 
268
+ /* ---------- fullscreen viewer ---------- */
269
+ var isFullscreen = false;
270
+ function toggleFullscreen() {
271
+ var want = isFullscreen ? 'inline' : 'fullscreen';
272
+ window.kolbo.requestDisplayMode(want).then(function (res) {
273
+ var granted = res && res.mode;
274
+ if (granted === 'fullscreen') { isFullscreen = true; applyFullscreen(true); }
275
+ else if (granted === 'inline' || isFullscreen) { isFullscreen = false; applyFullscreen(false); }
276
+ else if (!isFullscreen) {
277
+ // Host refused fullscreen — degrade to opening the original file.
278
+ window.kolbo.openLink(state.urls && state.urls[selected]);
279
+ }
280
+ }).catch(function () {
281
+ if (!isFullscreen) window.kolbo.openLink(state.urls && state.urls[selected]);
282
+ });
283
+ }
284
+ function applyFullscreen(on) {
285
+ document.documentElement.classList.toggle('k-fullscreen', on);
286
+ var c = el('phase-chip');
287
+ if (on) {
288
+ c.style.display = '';
289
+ c.innerHTML = '✕ ' + esc('Exit');
290
+ c.style.cursor = 'pointer';
291
+ c.onclick = toggleFullscreen;
292
+ } else {
293
+ c.style.display = 'none';
294
+ c.onclick = null;
295
+ c.style.cursor = '';
296
+ }
297
+ window.kolbo.notifySize();
298
+ }
299
+
265
300
  function setPhaseChip(text, spinning) {
266
301
  var c = el('phase-chip');
267
302
  if (!text) { c.style.display = 'none'; return; }
@@ -333,17 +368,41 @@ function openPromptRow(placeholder, onSend) {
333
368
  window.kolbo.notifySize();
334
369
  }
335
370
 
371
+ /* ---------- pre-result "Preparing" state ----------
372
+ The host mounts this iframe as soon as the tool is CALLED; the result can
373
+ take many seconds (model resolution, file upload, submit). Show a live
374
+ shell immediately instead of a blank card. */
375
+ function bootPre(toolName, args) {
376
+ if (state) return; // real data already arrived
377
+ el('tool-title').textContent = TOOL_TITLES[toolName] || 'Generation';
378
+ if (args && (args.prompt || args.text)) {
379
+ el('prompt').textContent = args.prompt || args.text;
380
+ el('prompt').style.display = '';
381
+ }
382
+ setPhaseChip('Preparing', true);
383
+ if (!el('stage').innerHTML) {
384
+ el('stage').innerHTML = '<div class="k-gen-grid n1"><div class="k-skel video" style="min-height:100px;max-height:140px"></div></div>';
385
+ }
386
+ window.kolbo.notifySize();
387
+ }
388
+
336
389
  /* ---------- wire host events ---------- */
337
390
  window.kolbo.onToolResult(function (result) {
338
391
  var sc = result.structuredContent || structured(result);
339
- if (sc) boot(sc);
392
+ if (sc && (sc.phase || sc.widget)) return boot(sc);
393
+ // Tool errored (or returned plain text): show it instead of a dead blank card.
394
+ var txt = '';
395
+ try { txt = (result.content || []).filter(function (c) { return c.type === 'text'; }).map(function (c) { return c.text; }).join(' '); } catch (e) {}
396
+ if (result.isError || /error|failed/i.test(txt)) {
397
+ renderError((txt || 'The request failed.').slice(0, 300));
398
+ }
340
399
  });
400
+ window.kolbo.onToolInput(function (args) { bootPre(null, args); });
341
401
  window.kolbo.ready(function (ctx) {
342
- // Some hosts deliver the initial result via hostContext.toolInfo; the
343
- // tool-result notification is the primary path.
344
- if (!state && ctx && ctx.toolInfo && ctx.toolInfo.result) {
345
- var sc = ctx.toolInfo.result.structuredContent;
346
- if (sc) boot(sc);
402
+ var info = ctx && ctx.toolInfo;
403
+ if (!state && info) {
404
+ if (info.result && info.result.structuredContent) return boot(info.result.structuredContent);
405
+ bootPre(info.tool && info.tool.name, null);
347
406
  }
348
407
  });
349
408
  `;
@@ -96,9 +96,13 @@ function registerModelTools(server, client, options = {}) {
96
96
  'List available AI models on Kolbo. Filter by `type` to narrow to a generation type, and pass `format: "json"` to get the raw model documents (every constraint field, useful for programmatic comparison / cap validation before submitting a generation). Default `format: "text"` returns the human-readable summary.',
97
97
  {
98
98
  type: z.string().optional().describe('Filter by DB type name: "text_to_img", "image_editing", "text_to_video", "img_to_video", "draw_to_video", "video_to_video", "elements", "firstlastgenerations", "lipsync-image", "lipsync-video", "music_gen", "text_to_speech", "text_to_sound", "stt", "text". Legacy aliases also accepted: "image", "image_edit", "video", "video_from_image", "video_from_video", "music", "speech", "sound", "chat", "lipsync" (both lipsync types), "three_d" (all 3D types), "first_last_frame", "transcription". Omit for all models.'),
99
- format: z.enum(['text', 'json']).optional().describe('Output format. "text" (default) returns a human-readable summary with the most-used caps. "json" returns the raw model documents from the API — use this when you need to programmatically verify caps (max_reference_images, max_visual_dna, max_video_duration, supported_aspect_ratios, etc.) before passing an array/value that might exceed a model-specific limit. The JSON form is the source of truth; the text form is a convenience preview.')
99
+ format: z.enum(['text', 'json']).optional().describe('Output format. "text" (default) returns a human-readable summary with the most-used caps. "json" returns the raw model documents from the API — use this when you need to programmatically verify caps (max_reference_images, max_visual_dna, max_video_duration, supported_aspect_ratios, etc.) before passing an array/value that might exceed a model-specific limit. The JSON form is the source of truth; the text form is a convenience preview.'),
100
+ display_catalog: z.boolean().optional().describe('Set true ONLY when the USER explicitly asked to see/browse the available models — it renders a visual model catalog to them. Leave unset for internal lookups (verifying a model name, checking caps before a generation): those should stay invisible to the user.')
100
101
  },
101
- async ({ type, format }) => {
102
+ async ({ type, format, display_catalog }) => {
103
+ // Widget only when the user asked to browse — internal validation
104
+ // lookups were spamming the chat with a full catalog card.
105
+ const showCatalog = display_catalog === true;
102
106
  const path = type ? `/v1/models?type=${encodeURIComponent(type)}` : '/v1/models';
103
107
  const result = await client.get(path);
104
108
 
@@ -108,7 +112,7 @@ function registerModelTools(server, client, options = {}) {
108
112
  // resolution multipliers, supports_* flags, prompt-length limits, etc.).
109
113
  if (format === 'json') {
110
114
  const text = JSON.stringify({ count: result.count, models: result.models }, null, 2);
111
- if (ui()) return uiResult(UI.catalog, text, buildCatalogStructured(result.models, type));
115
+ if (ui() && showCatalog) return uiResult(UI.catalog, text, buildCatalogStructured(result.models, type));
112
116
  return { content: [{ type: 'text', text }] };
113
117
  }
114
118
 
@@ -279,7 +283,7 @@ function registerModelTools(server, client, options = {}) {
279
283
  }
280
284
 
281
285
  const text = `Available models (${result.count}):\n\n${sections.join('\n\n')}\n\nUse the "identifier" value as the "model" parameter in generate tools. For programmatic cap validation, re-call with format: "json".`;
282
- if (ui()) return uiResult(UI.catalog, text, buildCatalogStructured(result.models, type));
286
+ if (ui() && showCatalog) return uiResult(UI.catalog, text, buildCatalogStructured(result.models, type));
283
287
  return { content: [{ type: 'text', text }] };
284
288
  }
285
289
  );