@kolbo/mcp 1.87.7 → 1.87.8

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.87.7",
3
+ "version": "1.87.8",
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": {
@@ -47,6 +47,8 @@ var state = null;
47
47
  function boot(sc) {
48
48
  var list = listPayload(sc);
49
49
  if (!list) return false;
50
+ // listPayload() normalises plain shapes; carry the paging contract through.
51
+ if (sc && sc.page_tool && !list.page_tool) { list.page_tool = sc.page_tool; list.next_args = sc.next_args; list.page = sc.page; }
50
52
  state = list;
51
53
  el('title').textContent = list.title || 'List';
52
54
  var total = list.total != null ? list.total : list.items.length;
@@ -58,13 +60,41 @@ function boot(sc) {
58
60
  window.kolbo.notifySize();
59
61
  return true;
60
62
  }
61
- el('stage').innerHTML = list.items.slice(0, 40).map(itemHTML).join('');
63
+ // Render every item the server sent (the old slice(0, 40) silently dropped
64
+ // rows, and nothing told the user). Load more only when the server said how.
65
+ var h = list.items.map(itemHTML).join('');
66
+ var shown = list.items.length;
67
+ if (list.page_tool && list.next_args) {
68
+ h += '<button class="k-btn" id="load-more" style="width:100%;margin-top:10px">Load more' +
69
+ (total > shown ? ' (' + shown + ' of ' + total + ' shown)' : '') + '</button>';
70
+ }
71
+ el('stage').innerHTML = h;
62
72
  el('stage').classList.remove('k-empty');
63
73
  wire();
74
+ var lm = el('load-more');
75
+ if (lm) lm.onclick = function () { fetchNextPage(lm); };
64
76
  window.kolbo.notifySize();
65
77
  return true;
66
78
  }
67
79
 
80
+ function fetchNextPage(btn) {
81
+ if (!state || !state.page_tool || !state.next_args || btn.disabled) return;
82
+ btn.disabled = true;
83
+ var label = btn.textContent;
84
+ btn.innerHTML = '<span class="k-spin"></span> Loading';
85
+ window.kolbo.callTool(state.page_tool, state.next_args).then(function (res) {
86
+ var sc = structured(res);
87
+ var next = sc ? listPayload(sc) || sc : null;
88
+ var more = (next && next.items) || [];
89
+ if ((res && res.isError) || !next) { btn.disabled = false; btn.textContent = 'Could not load more — try again'; return; }
90
+ if (!more.length) { btn.textContent = 'No more results'; return; }
91
+ state.items = state.items.concat(more);
92
+ state.next_args = sc.next_args;
93
+ if (sc.total != null) state.total = sc.total;
94
+ boot(Object.assign({}, state, { widget: 'list', page_tool: state.page_tool, next_args: state.next_args }));
95
+ }).catch(function () { btn.disabled = false; btn.textContent = label; });
96
+ }
97
+
68
98
  function apply(result) {
69
99
  if (!result) return false;
70
100
  var inner = result.result || result;
@@ -60,9 +60,14 @@ function boot(sc) {
60
60
  h += audioItems.map(audioRowHTML).join('');
61
61
  }
62
62
  var shown = visualItems.length + audioItems.length;
63
- if (sc.total != null && sc.total > shown) {
63
+ // Only a tool that told us HOW to page (page_tool + next_args / query) gets a
64
+ // button. Every other grid used to render a Load more that did nothing —
65
+ // fetchNextPage returned early with no page_tool — which read as "broken".
66
+ if (sc.total != null && sc.total > shown && sc.page_tool && (sc.next_args || sc.query || sc.page)) {
64
67
  h += '<button class="k-btn" id="load-more" style="width:100%;margin-top:10px">Load more (' +
65
68
  shown + ' of ' + sc.total + ' shown)</button>';
69
+ } else if (sc.total != null && sc.total > shown) {
70
+ h += '<div style="text-align:center;font-size:12px;color:var(--text-muted);margin-top:10px">' + shown + ' of ' + sc.total + ' shown</div>';
66
71
  }
67
72
  el('stage').innerHTML = h;
68
73
  el('stage').classList.remove('k-empty');
@@ -177,14 +182,26 @@ function fetchNextPage(btn) {
177
182
  btn.disabled = true;
178
183
  var label = btn.textContent;
179
184
  btn.innerHTML = '<span class="k-spin"></span> Loading';
185
+ // The server knows its own paging arg names (page/limit, offset, cursor…):
186
+ // when it shipped next_args, send exactly that. The query+page shape is the
187
+ // legacy fallback list_media has always used.
180
188
  var args = {};
181
- var q = state.query || {};
182
- for (var k in q) { if (q[k] !== undefined && q[k] !== null && q[k] !== '') args[k] = q[k]; }
183
- args.page = next;
184
- if (state.page_size) args.page_size = state.page_size;
189
+ if (state.next_args) {
190
+ for (var nk in state.next_args) { if (state.next_args[nk] !== undefined && state.next_args[nk] !== null) args[nk] = state.next_args[nk]; }
191
+ } else {
192
+ var q = state.query || {};
193
+ for (var k in q) { if (q[k] !== undefined && q[k] !== null && q[k] !== '') args[k] = q[k]; }
194
+ args.page = next;
195
+ if (state.page_size) args.page_size = state.page_size;
196
+ }
185
197
 
186
198
  window.kolbo.callTool(state.page_tool, args).then(function (res) {
187
199
  var sc = structured(res);
200
+ if (res && res.isError) {
201
+ btn.disabled = false;
202
+ btn.textContent = 'Could not load more — try again';
203
+ return;
204
+ }
188
205
  var more = (sc && sc.items) || [];
189
206
  if (!more.length) {
190
207
  // Nothing came back: say so rather than restoring a button that still
@@ -194,6 +211,9 @@ function fetchNextPage(btn) {
194
211
  }
195
212
  state.items = (state.items || []).concat(more);
196
213
  state.page = (sc && sc.page) || next;
214
+ // The next page's args come from THIS response; none means this was the last page.
215
+ state.next_args = sc && sc.next_args;
216
+ if (sc && sc.next_args == null && sc.page_tool == null && state.next_args === undefined) state.next_args = undefined;
197
217
  if (sc && sc.total != null) state.total = sc.total;
198
218
  boot(state); // re-renders the grid + a fresh Load more button
199
219
  }).catch(function () {
@@ -40,7 +40,7 @@ function registerColorPaletteTools(server, client, options = {}) {
40
40
  return uiResult(UI.mediaGrid, text, {
41
41
  widget: 'media-grid',
42
42
  title: 'Color DNA Palettes',
43
- items: palettes.slice(0, 24).map(p => ({
43
+ items: palettes.slice(0, 300).map(p => ({
44
44
  id: p.id,
45
45
  title: p.is_active ? `${p.name} (active)` : p.name,
46
46
  // First source image when available, else an SVG strip of the palette itself.
@@ -48,8 +48,8 @@ function registerColorPaletteTools(server, client, options = {}) {
48
48
  media_type: 'image',
49
49
  use_hint: 'Activate Color DNA palette "{TITLE}" (color_palette_id: {ID}) so it grades my generations.'
50
50
  })),
51
- total: result.pagination?.total || palettes.length,
52
- has_more: palettes.length > 24
51
+ total: palettes.length,
52
+ has_more: palettes.length > 300
53
53
  });
54
54
  }
55
55
 
package/src/tools/docs.js CHANGED
@@ -63,7 +63,9 @@ function registerDocTools(server, client) {
63
63
  subtitle: (d.updated_at ? String(d.updated_at).slice(0, 10) : '') + (d.is_shared ? ' · shared' : ''),
64
64
  open_url: d.share_url || null
65
65
  })),
66
- total: docs.length
66
+ total: docs.length,
67
+ page_tool: 'list_docs',
68
+ next_args: docs.length >= (limit || 20) ? { project_id, page: (page || 1) + 1, limit } : undefined
67
69
  });
68
70
  }
69
71
  );
@@ -11,14 +11,16 @@ function registerMoodboardTools(server, client, options = {}) {
11
11
  // ─── list_moodboards ───────────────────────────────────────
12
12
  server.tool(
13
13
  'list_moodboards',
14
- 'List moodboards. By default returns ALL (personal + system presets + organization). Use "scope" to filter: "personal" (user\'s own), "preset" or "global" (system presets), or "organization" (org-shared). Returns id, name, master_prompt, thumbnail, and image URLs for each.',
14
+ 'List moodboards. Default scope is "personal" the user\'s OWN custom moodboards, which is what the card shows. Pass scope "preset"/"global" for the system presets, "organization" for org-shared, or "all" for everything. Returns id, name, master_prompt, thumbnail, and image URLs for each.',
15
15
  {
16
- scope: z.enum(['all', 'personal', 'preset', 'global', 'organization']).optional().describe('Filter by scope. Default: "all" (everything accessible). "personal" = only your own. "preset"/"global" = system presets. "organization" = org-shared.'),
16
+ scope: z.enum(['all', 'personal', 'preset', 'global', 'organization']).optional().describe('Filter by scope. Default: "personal" (only the user\'s own custom moodboards). "preset"/"global" = system presets. "organization" = org-shared. "all" = everything accessible.'),
17
17
  project_id: projectScopeReadField
18
18
  },
19
19
  async ({ scope, project_id } = {}) => {
20
20
  const params = new URLSearchParams();
21
- if (scope && scope !== 'all') params.set('scope', scope);
21
+ // Users asked to see only THEIR moodboards on the card, not the system presets.
22
+ const effectiveScope = scope || 'personal';
23
+ if (effectiveScope !== 'all') params.set('scope', effectiveScope);
22
24
  if (project_id) params.set('project_id', project_id);
23
25
  const qs = params.toString();
24
26
  const result = await client.get(`/v1/moodboards${qs ? '?' + qs : ''}`);
@@ -36,7 +38,7 @@ function registerMoodboardTools(server, client, options = {}) {
36
38
  return uiResult(UI.mediaGrid, text, {
37
39
  widget: 'media-grid',
38
40
  title: 'Moodboards',
39
- items: moodboards.slice(0, 24).map(mb => ({
41
+ items: moodboards.slice(0, 300).map(mb => ({
40
42
  id: mb.id,
41
43
  title: mb.name,
42
44
  // API returns thumbnail_url + images[] (sdk listMoodboards) — both
@@ -46,8 +48,8 @@ function registerMoodboardTools(server, client, options = {}) {
46
48
  media_type: 'image',
47
49
  use_hint: 'Apply moodboard "{TITLE}" (moodboard_id: {ID}) to my next generation.'
48
50
  })),
49
- total: result.count || moodboards.length,
50
- has_more: moodboards.length > 24
51
+ total: moodboards.length,
52
+ has_more: moodboards.length > 300
51
53
  });
52
54
  }
53
55
  );
@@ -26,7 +26,7 @@ function trackItem(track) {
26
26
  };
27
27
  }
28
28
 
29
- function tracksResult(ui, title, tracks, total) {
29
+ function tracksResult(ui, title, tracks, total, paging) {
30
30
  if (!tracks.length) return { content: [{ type: 'text', text: 'No SYNCI tracks found.' }] };
31
31
  const text = [
32
32
  `Found ${tracks.length} track${tracks.length === 1 ? '' : 's'}${total ? ` (of ${total})` : ''}.`,
@@ -38,8 +38,11 @@ function tracksResult(ui, title, tracks, total) {
38
38
  return uiResult(UI.mediaGrid, text, {
39
39
  widget: 'media-grid',
40
40
  title,
41
- items: tracks.slice(0, 20).map(trackItem),
41
+ items: tracks.map(trackItem),
42
42
  total: total != null ? total : tracks.length,
43
+ ...(paging && total != null && (paging.offset + tracks.length) < total
44
+ ? { page_tool: paging.tool, next_args: { ...paging.args, offset: paging.offset + tracks.length, limit: paging.limit } }
45
+ : {}),
43
46
  });
44
47
  }
45
48
 
@@ -90,7 +93,8 @@ function registerMusicLibraryTools(server, client, options = {}) {
90
93
  },
91
94
  async (args) => {
92
95
  const result = await client.post('/v1/music-library/search', args);
93
- return tracksResult(ui, `SYNCI — ${args.query || 'Search'}`, result.tracks || [], result.total);
96
+ return tracksResult(ui, `SYNCI — ${args.query || 'Search'}`, result.tracks || [], result.total,
97
+ { tool: 'search_music_library', args, offset: args.offset || 0, limit: args.limit || (result.tracks || []).length || 20 });
94
98
  },
95
99
  );
96
100
 
@@ -118,7 +122,8 @@ function registerMusicLibraryTools(server, client, options = {}) {
118
122
  if (limit != null) params.set('limit', String(limit));
119
123
  if (offset != null) params.set('offset', String(offset));
120
124
  const result = await client.get(`/v1/music-library/catalog?${params.toString()}`);
121
- return tracksResult(ui, 'SYNCI Music Library', result.tracks || [], result.total);
125
+ return tracksResult(ui, 'SYNCI Music Library', result.tracks || [], result.total,
126
+ { tool: 'browse_music_library', args: { sort }, offset: offset || 0, limit: limit || (result.tracks || []).length || 20 });
122
127
  },
123
128
  );
124
129
 
@@ -60,7 +60,9 @@ function registerProjectTools(server, client) {
60
60
  open_url: p.open_url,
61
61
  use_hint: 'Use my "{TITLE}" project (project_id: {ID}) for what I do next.'
62
62
  })),
63
- total: projects.length
63
+ total: projects.length,
64
+ page_tool: 'list_projects',
65
+ next_args: projects.length >= (limit || 50) ? { search, include_archived, page: (page || 1) + 1, limit } : undefined
64
66
  });
65
67
  }
66
68
  );
@@ -359,7 +361,9 @@ function registerProjectTools(server, client) {
359
361
  ].filter(Boolean).join(' · '),
360
362
  badge: (s.types && s.types[0]) || undefined
361
363
  })),
362
- total: sessions.length
364
+ total: sessions.length,
365
+ page_tool: 'list_sessions',
366
+ next_args: sessions.length >= (limit || 20) ? { project_id, type, types, page: (page || 1) + 1, limit } : undefined
363
367
  });
364
368
  }
365
369
  );
@@ -84,7 +84,7 @@ function registerStockLibraryTools(server, client, options = {}) {
84
84
  // the media grid on them, so the card rendered one broken-file glyph per cell.
85
85
  // media.js and listResult() have always done it this way; these five lagged.
86
86
  {
87
- const items = assets.slice(0, 24).map((a) => {
87
+ const items = assets.map((a) => {
88
88
  const mt = widgetMediaType(a.mediaType);
89
89
  return {
90
90
  id: a.source + ':' + a.sourceId,
@@ -107,7 +107,12 @@ function registerStockLibraryTools(server, client, options = {}) {
107
107
  title: 'Stock — "' + (args.query || 'browse') + '"',
108
108
  items,
109
109
  total: result.total != null ? result.total : assets.length,
110
- has_more: !!result.hasMore
110
+ has_more: !!result.hasMore,
111
+ page: args.page || 1,
112
+ page_tool: 'search_stock_media',
113
+ next_args: result.hasMore
114
+ ? { ...args, page: (args.page || 1) + 1, ...(result.nextCursor ? { cursor: result.nextCursor } : {}) }
115
+ : undefined
111
116
  });
112
117
  }
113
118
 
@@ -172,7 +177,7 @@ function registerStockLibraryTools(server, client, options = {}) {
172
177
  // media.js and listResult() have always done it this way; these five lagged.
173
178
  {
174
179
  const collections = result.collections || [];
175
- const items = collections.slice(0, 24).map((c) => ({
180
+ const items = collections.slice(0, 300).map((c) => ({
176
181
  id: c.id,
177
182
  title: c.title,
178
183
  subtitle: [c.kind, c.mediaType].filter(Boolean).join(' · '),
@@ -154,7 +154,7 @@ function registerVisualDnaTools(server, client, options = {}) {
154
154
  return uiResult(UI.mediaGrid, text, {
155
155
  widget: 'media-grid',
156
156
  title: 'Visual DNA Profiles',
157
- items: dnas.slice(0, 24).map(d => ({
157
+ items: dnas.map(d => ({
158
158
  id: d.id,
159
159
  title: d.name,
160
160
  subtitle: (d.dna_type || '') + (Array.isArray(d.tags) && d.tags.length ? ' · ' + d.tags.slice(0, 3).join(', ') : ''),
@@ -164,7 +164,12 @@ function registerVisualDnaTools(server, client, options = {}) {
164
164
  use_hint: 'Use Visual DNA "{TITLE}" (id: {ID}) in my next generation for character/style consistency.'
165
165
  })),
166
166
  total,
167
- has_more: result.has_more || dnas.length > 24
167
+ has_more: !!result.has_more,
168
+ page: page && page > 0 ? Math.floor(page) : 1,
169
+ page_tool: 'list_visual_dnas',
170
+ next_args: result.has_more
171
+ ? { scope, search, collection, tags, project_id, page: (page && page > 0 ? Math.floor(page) : 1) + 1, limit }
172
+ : undefined
168
173
  });
169
174
  }
170
175
  );
@@ -99,7 +99,10 @@ function registerVoiceTools(server, client, options = {}) {
99
99
  use_hint: 'Use voice "{TITLE}" (voice_id: {ID}) for text-to-speech — ask me what text to speak.'
100
100
  })),
101
101
  total: voices.length,
102
- has_more: more > 0
102
+ has_more: more > 0,
103
+ page: pageNum,
104
+ page_tool: 'list_voices',
105
+ next_args: more > 0 ? { language, gender, provider, page: pageNum + 1, limit: perPage } : undefined
103
106
  });
104
107
  }
105
108