@kolbo/mcp 1.71.1 → 1.71.3

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.71.1",
3
+ "version": "1.71.3",
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": {
@@ -1,6 +1,6 @@
1
1
  # AUTO-GENERATED — do not edit
2
2
 
3
- This tree is mirrored from kolbo-code@c5b2132, the single source of truth.
3
+ This tree is mirrored from kolbo-code@ff2a2e2, the single source of truth.
4
4
  Canonical source: packages/opencode/skills/kolbo/
5
5
  Distribution: .github/workflows/sync-skill-to-plugin.yml
6
6
 
package/src/apps/html.js CHANGED
@@ -109,11 +109,63 @@ function monogram(name) {
109
109
  function structured(res) {
110
110
  if (!res) return null;
111
111
  if (res.structuredContent) return res.structuredContent;
112
+ if (res.result) return structured(res.result);
112
113
  try {
113
114
  var t = (res.content || []).filter(function (c) { return c.type === 'text'; })[0];
114
115
  return t ? JSON.parse(t.text) : null;
115
116
  } catch (e) { return null; }
116
117
  }
118
+ // Hosts that do not advertise MCP Apps (Kolbo Code) get plain JSON
119
+ // ({ sessions }, { generations }, { projects }, …) with no items[].
120
+ // Normalize those shapes so list.html and the generation fallback can
121
+ // leave Loading on first paint without waiting for a newer host.
122
+ function listPayload(sc) {
123
+ if (!sc || typeof sc !== 'object') return null;
124
+ if (sc.widget && sc.widget !== 'list') return null;
125
+ if (sc.phase || sc.generation_id || sc.generation_ids) return null;
126
+ if (Array.isArray(sc.items)) {
127
+ return {
128
+ widget: 'list',
129
+ title: sc.title || 'List',
130
+ items: sc.items,
131
+ total: sc.total != null ? sc.total : sc.items.length
132
+ };
133
+ }
134
+ var key = sc.sessions ? 'sessions' : sc.projects ? 'projects' : sc.generations ? 'generations'
135
+ : sc.agents ? 'agents' : sc.docs ? 'docs' : sc.folders ? 'folders' : sc.sources ? 'sources' : '';
136
+ var rows = key ? sc[key] : null;
137
+ if (!Array.isArray(rows)) return null;
138
+ var titles = {
139
+ sessions: 'Sessions', projects: 'Projects', generations: 'Generations',
140
+ agents: 'Agents', docs: 'Docs', folders: 'Folders', sources: 'Knowledge Base'
141
+ };
142
+ return {
143
+ widget: 'list',
144
+ title: sc.title || titles[key] || 'List',
145
+ items: rows.map(function (row) {
146
+ var types = Array.isArray(row.types)
147
+ ? row.types.filter(function (x) { return typeof x === 'string'; })
148
+ : String(row.type || '').split('|').filter(Boolean);
149
+ var id = row.session_id || row.id || row.generation_id || row.file_key;
150
+ var name = row.name || row.title || (row.prompt ? String(row.prompt).slice(0, 80) : '') || types[0] || 'Item';
151
+ return {
152
+ id: id,
153
+ title: name,
154
+ subtitle: [
155
+ types.join(', ') || row.role || row.status || row.description,
156
+ id,
157
+ row.project_id ? 'project ' + row.project_id : null,
158
+ row.updated_at ? String(row.updated_at).slice(0, 10) : null,
159
+ row.output_count ? row.output_count + ' outputs' : null
160
+ ].filter(Boolean).join(' · '),
161
+ badge: types[0] || row.role || row.status,
162
+ open_url: row.open_url || row.share_url || null,
163
+ meta: row.item_count != null ? String(row.item_count) : undefined
164
+ };
165
+ }),
166
+ total: sc.total != null ? sc.total : (sc.count != null ? sc.count : rows.length)
167
+ };
168
+ }
117
169
  </script>
118
170
  <script>${script}</script>
119
171
  </body>
package/src/apps/index.js CHANGED
@@ -166,6 +166,13 @@ function uiResult(uri, text, structured) {
166
166
  };
167
167
  }
168
168
 
169
+ /** List tools always ship structuredContent — Kolbo Code does not advertise
170
+ * MCP Apps, so gating on appsEnabled() left list.html / the generation
171
+ * fallback stuck on Loading with only `{ sessions }` text. */
172
+ function listResult(text, structured) {
173
+ return uiResult(UI.list, text, structured);
174
+ }
175
+
169
176
  /* ------------------------------------------------------------------ */
170
177
  /* Model icon lookup (name/identifier → absolute avatar URL) */
171
178
  /* ------------------------------------------------------------------ */
@@ -494,6 +501,7 @@ module.exports = {
494
501
  attachToolWidgetMeta,
495
502
  uiMeta,
496
503
  uiResult,
504
+ listResult,
497
505
  appsEnabled,
498
506
  modelIcon,
499
507
  modelInfo,
@@ -889,6 +889,8 @@ function completedFromPlain(sc) {
889
889
  /* ---------- wire host events ---------- */
890
890
  window.kolbo.onToolResult(function (result) {
891
891
  var sc = result.structuredContent || structured(result);
892
+ var list = listPayload(sc);
893
+ if (list) return renderList(list);
892
894
  if (sc && (sc.phase || sc.widget)) return boot(sc);
893
895
  var recovered = completedFromPlain(sc);
894
896
  if (recovered) return boot(recovered);
@@ -907,7 +909,10 @@ window.kolbo.ready(function (ctx) {
907
909
  var info = ctx && ctx.toolInfo;
908
910
  if (!state && info) {
909
911
  originTool = (info.tool && info.tool.name) || originTool;
910
- if (info.result && info.result.structuredContent) return boot(info.result.structuredContent);
912
+ var raw = info.result && (info.result.structuredContent || structured(info.result));
913
+ var list = listPayload(raw);
914
+ if (list) return renderList(list);
915
+ if (raw) return boot(raw);
911
916
  bootPre(originTool, null);
912
917
  }
913
918
  });
@@ -43,17 +43,30 @@ el('kolbo-link').onclick = function (e) { e.preventDefault(); window.kolbo.openL
43
43
  var state = null;
44
44
 
45
45
  function boot(sc) {
46
- if (!sc || !sc.items) return;
47
- state = sc;
48
- el('title').textContent = sc.title || 'List';
49
- var total = sc.total != null ? sc.total : sc.items.length;
46
+ var list = listPayload(sc);
47
+ if (!list) return false;
48
+ state = list;
49
+ el('title').textContent = list.title || 'List';
50
+ var total = list.total != null ? list.total : list.items.length;
50
51
  el('count-chip').style.display = '';
51
52
  el('count-chip').textContent = total + (total === 1 ? ' item' : ' items');
52
- if (!sc.items.length) { el('stage').innerHTML = '<div class="k-empty">Nothing here yet</div>'; return; }
53
- el('stage').innerHTML = sc.items.slice(0, 40).map(itemHTML).join('');
53
+ if (!list.items.length) {
54
+ el('stage').classList.add('k-empty');
55
+ el('stage').innerHTML = 'Nothing here yet';
56
+ window.kolbo.notifySize();
57
+ return true;
58
+ }
59
+ el('stage').innerHTML = list.items.slice(0, 40).map(itemHTML).join('');
54
60
  el('stage').classList.remove('k-empty');
55
61
  wire();
56
62
  window.kolbo.notifySize();
63
+ return true;
64
+ }
65
+
66
+ function apply(result) {
67
+ if (!result) return false;
68
+ var inner = result.result || result;
69
+ return boot(inner.structuredContent || structured(inner) || inner);
57
70
  }
58
71
 
59
72
  function itemHTML(item, i) {
@@ -86,11 +99,11 @@ function wire() {
86
99
  }
87
100
 
88
101
  window.kolbo.onToolResult(function (result) {
89
- var sc = result.structuredContent || structured(result);
90
- if (sc && sc.items) return boot(sc);
91
- var card = document.querySelector('.k-card');
92
- if (card) card.style.display = 'none';
93
- window.kolbo.notifySize();
102
+ apply(result);
103
+ });
104
+ window.kolbo.ready(function (ctx) {
105
+ var info = ctx && ctx.toolInfo;
106
+ if (info && info.result) apply(info.result);
94
107
  });
95
108
  `;
96
109
 
@@ -4,10 +4,9 @@
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 { listResult } = require('../apps');
8
8
 
9
- function registerAgentTools(server, client, options = {}) {
10
- const ui = () => appsEnabled(server, options);
9
+ function registerAgentTools(server, client) {
11
10
  // ─── list_agents ───────────────────────────────────────────
12
11
  server.tool(
13
12
  'list_agents',
@@ -22,22 +21,18 @@ function registerAgentTools(server, client, options = {}) {
22
21
  count: result.count || agents.length
23
22
  }, null, 2);
24
23
 
25
- if (ui()) {
26
- return uiResult(UI.list, text, {
27
- widget: 'list',
28
- title: 'Your Agents',
29
- items: agents.map(a => ({
30
- id: a.id,
31
- title: (a.emoji ? a.emoji + ' ' : '') + a.name,
32
- subtitle: a.description,
33
- badge: a.is_global ? 'preset' : null,
34
- use_hint: 'Use my "{TITLE}" agent (agent_id: {ID}) for this conversation.'
35
- })),
36
- total: agents.length
37
- });
38
- }
39
-
40
- return { content: [{ type: 'text', text }] };
24
+ return listResult(text, {
25
+ widget: 'list',
26
+ title: 'Your Agents',
27
+ items: agents.map(a => ({
28
+ id: a.id,
29
+ title: (a.emoji ? a.emoji + ' ' : '') + a.name,
30
+ subtitle: a.description,
31
+ badge: a.is_global ? 'preset' : null,
32
+ use_hint: 'Use my "{TITLE}" agent (agent_id: {ID}) for this conversation.'
33
+ })),
34
+ total: agents.length
35
+ });
41
36
  }
42
37
  );
43
38
 
package/src/tools/docs.js CHANGED
@@ -5,12 +5,11 @@
5
5
 
6
6
  const { z } = require('zod');
7
7
  const { projectIdField } = require('./_shared');
8
- const { UI, uiResult, appsEnabled } = require('../apps');
8
+ const { listResult } = require('../apps');
9
9
 
10
10
  const CONTENT_GUIDE = 'HTML body content. Use clean semantic HTML the in-app editor understands: <h1>-<h3>, <p>, <ul>/<ol>/<li>, <table>, <blockquote>, <strong>/<em>, <a>. No <script>/<style>/<iframe> (stripped server-side). Write the FULL document yourself — this is where you author the doc.';
11
11
 
12
- function registerDocTools(server, client, options = {}) {
13
- const ui = () => appsEnabled(server, options);
12
+ function registerDocTools(server, client) {
14
13
  // ─── create_doc ────────────────────────────────────────────
15
14
  server.tool(
16
15
  'create_doc',
@@ -55,21 +54,17 @@ function registerDocTools(server, client, options = {}) {
55
54
  const docs = result.docs || [];
56
55
  const text = JSON.stringify({ docs, pagination: result.pagination || null }, null, 2);
57
56
 
58
- if (ui()) {
59
- return uiResult(UI.list, text, {
60
- widget: 'list',
61
- title: 'Your AI Docs',
62
- items: docs.map(d => ({
63
- id: d.id,
64
- title: d.title,
65
- subtitle: (d.updated_at ? String(d.updated_at).slice(0, 10) : '') + (d.is_shared ? ' · shared' : ''),
66
- open_url: d.share_url || null
67
- })),
68
- total: docs.length
69
- });
70
- }
71
-
72
- return { content: [{ type: 'text', text }] };
57
+ return listResult(text, {
58
+ widget: 'list',
59
+ title: 'Your AI Docs',
60
+ items: docs.map(d => ({
61
+ id: d.id,
62
+ title: d.title,
63
+ subtitle: (d.updated_at ? String(d.updated_at).slice(0, 10) : '') + (d.is_shared ? ' · shared' : ''),
64
+ open_url: d.share_url || null
65
+ })),
66
+ total: docs.length
67
+ });
73
68
  }
74
69
  );
75
70
 
@@ -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, appsEnabled } = require('../apps');
9
+ const { UI, uiResult, listResult, appsEnabled } = 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
@@ -343,22 +343,18 @@ function registerMediaTools(server, client, options = {}) {
343
343
  const folders = result.folders || [];
344
344
  const text = JSON.stringify({ folders, count: result.count || 0 }, null, 2);
345
345
 
346
- if (ui()) {
347
- return uiResult(UI.list, text, {
348
- widget: 'list',
349
- title: 'Media Folders',
350
- items: folders.map(f => ({
351
- id: f.id,
352
- title: f.name,
353
- subtitle: f.description,
354
- meta: (f.item_count || 0) + (f.item_count === 1 ? ' item' : ' items'),
355
- use_hint: 'List media in my "{TITLE}" folder (folder_id: {ID}).'
356
- })),
357
- total: folders.length
358
- });
359
- }
360
-
361
- return { content: [{ type: 'text', text }] };
346
+ return listResult(text, {
347
+ widget: 'list',
348
+ title: 'Media Folders',
349
+ items: folders.map(f => ({
350
+ id: f.id,
351
+ title: f.name,
352
+ subtitle: f.description,
353
+ meta: (f.item_count || 0) + (f.item_count === 1 ? ' item' : ' items'),
354
+ use_hint: 'List media in my "{TITLE}" folder (folder_id: {ID}).'
355
+ })),
356
+ total: folders.length
357
+ });
362
358
  }
363
359
  );
364
360
 
@@ -5,10 +5,9 @@
5
5
 
6
6
  const { z } = require('zod');
7
7
  const { buildProjectUrl } = require('./_shared');
8
- const { UI, uiResult, appsEnabled } = require('../apps');
8
+ const { listResult } = require('../apps');
9
9
 
10
- function registerProjectTools(server, client, options = {}) {
11
- const ui = () => appsEnabled(server, options);
10
+ function registerProjectTools(server, client) {
12
11
  // ─── list_projects ─────────────────────────────────────────
13
12
  server.tool(
14
13
  'list_projects',
@@ -42,22 +41,18 @@ function registerProjectTools(server, client, options = {}) {
42
41
  _hint: 'Pass the chosen `id` as `project_id` on any generate_* tool to drop the generation into that project. Omit project_id to use the project flagged is_default:true. `open_url` opens that project\'s media in the web app (share it with the user). If `pagination.has_more` is true there are more projects — narrow with `search` rather than paging through everything.'
43
42
  }, null, 2);
44
43
 
45
- if (ui()) {
46
- return uiResult(UI.list, text, {
47
- widget: 'list',
48
- title: 'Your Projects',
49
- items: projects.map(p => ({
50
- id: p.id,
51
- title: p.name,
52
- subtitle: p.role + (p.is_default ? ' · default' : '') + (p.is_archived ? ' · archived' : ''),
53
- open_url: p.open_url,
54
- use_hint: 'Use my "{TITLE}" project (project_id: {ID}) for what I do next.'
55
- })),
56
- total: projects.length
57
- });
58
- }
59
-
60
- return { content: [{ type: 'text', text }] };
44
+ return listResult(text, {
45
+ widget: 'list',
46
+ title: 'Your Projects',
47
+ items: projects.map(p => ({
48
+ id: p.id,
49
+ title: p.name,
50
+ subtitle: p.role + (p.is_default ? ' · default' : '') + (p.is_archived ? ' · archived' : ''),
51
+ open_url: p.open_url,
52
+ use_hint: 'Use my "{TITLE}" project (project_id: {ID}) for what I do next.'
53
+ })),
54
+ total: projects.length
55
+ });
61
56
  }
62
57
  );
63
58
 
@@ -135,20 +130,17 @@ function registerProjectTools(server, client, options = {}) {
135
130
  generations,
136
131
  _hint: 'This is an inventory, not a live generation. Pass the `id` values to `move_generations_to_session` (into an existing session) or `split_session` (into a new one). `in_flight: true` means it is still running and cannot be moved yet.'
137
132
  }, null, 2);
138
- if (ui()) {
139
- return uiResult(UI.list, text, {
140
- widget: 'list',
141
- title: (result.session && result.session.name) || 'Session generations',
142
- items: generations.map((g) => ({
143
- id: g.id,
144
- title: (g.prompt && String(g.prompt).slice(0, 80)) || g.id,
145
- subtitle: [g.status, g.output_count ? g.output_count + ' outputs' : null].filter(Boolean).join(' · '),
146
- badge: g.in_flight ? 'running' : g.status
147
- })),
148
- total: generations.length
149
- });
150
- }
151
- return { content: [{ type: 'text', text }] };
133
+ return listResult(text, {
134
+ widget: 'list',
135
+ title: (result.session && result.session.name) || 'Session generations',
136
+ items: generations.map((g) => ({
137
+ id: g.id,
138
+ title: (g.prompt && String(g.prompt).slice(0, 80)) || g.id,
139
+ subtitle: [g.status, g.output_count ? g.output_count + ' outputs' : null].filter(Boolean).join(' · '),
140
+ badge: g.in_flight ? 'running' : g.status
141
+ })),
142
+ total: generations.length
143
+ });
152
144
  }
153
145
  );
154
146
 
@@ -316,26 +308,22 @@ function registerProjectTools(server, client, options = {}) {
316
308
  _hint: 'Each row has project_id — pass it on every later generate/chat/upload in this conversation. Empty leftover sessions after a move: delete_session.'
317
309
  }, null, 2);
318
310
 
319
- if (ui()) {
320
- return uiResult(UI.list, text, {
321
- widget: 'list',
322
- title: 'Sessions' + (type ? ' — ' + type : '') + ' (' + sessions.length + ')',
323
- items: sessions.map(s => ({
324
- id: s.session_id,
325
- title: s.name || s.types[0] || s.type || 'Session',
326
- subtitle: [
327
- s.session_id,
328
- (s.types || []).join(', '),
329
- s.project_id ? 'project ' + s.project_id : null,
330
- s.updated_at ? String(s.updated_at).slice(0, 10) : null
331
- ].filter(Boolean).join(' · '),
332
- badge: (s.types && s.types[0]) || undefined
333
- })),
334
- total: sessions.length
335
- });
336
- }
337
-
338
- return { content: [{ type: 'text', text }] };
311
+ return listResult(text, {
312
+ widget: 'list',
313
+ title: 'Sessions' + (type ? ' — ' + type : '') + ' (' + sessions.length + ')',
314
+ items: sessions.map(s => ({
315
+ id: s.session_id,
316
+ title: s.name || s.types[0] || s.type || 'Session',
317
+ subtitle: [
318
+ s.session_id,
319
+ (s.types || []).join(', '),
320
+ s.project_id ? 'project ' + s.project_id : null,
321
+ s.updated_at ? String(s.updated_at).slice(0, 10) : null
322
+ ].filter(Boolean).join(' · '),
323
+ badge: (s.types && s.types[0]) || undefined
324
+ })),
325
+ total: sessions.length
326
+ });
339
327
  }
340
328
  );
341
329
 
@@ -416,21 +404,17 @@ function registerProjectTools(server, client, options = {}) {
416
404
  const sources = result.sources || [];
417
405
  const text = JSON.stringify({ sources, count: result.count || 0 }, null, 2);
418
406
 
419
- if (ui()) {
420
- return uiResult(UI.list, text, {
421
- widget: 'list',
422
- title: 'Project Knowledge Base',
423
- items: sources.map(s => ({
424
- id: s.file_key,
425
- title: s.title || s.type,
426
- subtitle: s.type + ' · ' + s.status,
427
- open_url: s.url || null
428
- })),
429
- total: sources.length
430
- });
431
- }
432
-
433
- return { content: [{ type: 'text', text }] };
407
+ return listResult(text, {
408
+ widget: 'list',
409
+ title: 'Project Knowledge Base',
410
+ items: sources.map(s => ({
411
+ id: s.file_key,
412
+ title: s.title || s.type,
413
+ subtitle: s.type + ' · ' + s.status,
414
+ open_url: s.url || null
415
+ })),
416
+ total: sources.length
417
+ });
434
418
  }
435
419
  );
436
420
 
@@ -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, appsEnabled } = require('../apps');
9
+ const { UI, uiResult, listResult, appsEnabled } = require('../apps');
10
10
 
11
11
  // Visual DNA caps reference media at 25MB per file (stricter than the
12
12
  // default _shared.resolveToBuffer cap — DNA profiles only need enough
@@ -227,21 +227,17 @@ function registerVisualDnaTools(server, client, options = {}) {
227
227
  const folders = result.folders || [];
228
228
  const text = JSON.stringify({ folders, count: result.count || 0 }, null, 2);
229
229
 
230
- if (ui()) {
231
- return uiResult(UI.list, text, {
232
- widget: 'list',
233
- title: 'Visual DNA Folders',
234
- items: folders.map(f => ({
235
- id: f.id,
236
- title: f.name,
237
- meta: f.item_count != null ? (f.item_count + (f.item_count === 1 ? ' DNA' : ' DNAs')) : null,
238
- use_hint: 'List Visual DNAs in my "{TITLE}" folder (folder_id: {ID}).'
239
- })),
240
- total: folders.length
241
- });
242
- }
243
-
244
- return { content: [{ type: 'text', text }] };
230
+ return listResult(text, {
231
+ widget: 'list',
232
+ title: 'Visual DNA Folders',
233
+ items: folders.map(f => ({
234
+ id: f.id,
235
+ title: f.name,
236
+ meta: f.item_count != null ? (f.item_count + (f.item_count === 1 ? ' DNA' : ' DNAs')) : null,
237
+ use_hint: 'List Visual DNAs in my "{TITLE}" folder (folder_id: {ID}).'
238
+ })),
239
+ total: folders.length
240
+ });
245
241
  }
246
242
  );
247
243