@kolbo/mcp 1.119.1 → 1.120.0

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/README.md CHANGED
@@ -321,6 +321,7 @@ Requires DaVinci Resolve Studio with the Kolbo plugin open (Workspace → Workfl
321
321
  | `generate_character_sheet` | Generate a multi-angle character sheet from reference images (credits) → pass URL to create_visual_dna or update_visual_dna |
322
322
  | `list_visual_dna_folders` / `create_visual_dna_folder` / `update_visual_dna_folder` / `delete_visual_dna_folder` / `move_visual_dna_to_folder` | Organize Visual DNA characters into user folders (create/rename/recolor/delete + move DNAs in/out) |
323
323
  | `create_project` / `update_project` / `archive_project` / `unarchive_project` | Project lifecycle (create/rename/describe/archive; deletion stays in-app) |
324
+ | `transfer_visual_dna` / `list_visual_dna_transfers` / `respond_visual_dna_transfer` | Send Visual DNAs you own to another Kolbo account (they accept a copy with new ids; optionally move instead of copy) |
324
325
  | `transfer_project` / `list_project_transfers` / `respond_project_transfer` | Hand a project you own to another Kolbo account (they accept; same ids; cast assets copied; payer history unchanged) |
325
326
  | `list_skills` / `create_skill` / `update_skill` / `delete_skill` | Skills — reusable named personas for the chat tool; `description` is the system instruction |
326
327
  | `list_agents` / `create_agent` / `update_agent` / `delete_agent` | Same four tools under their original names. Still supported and unchanged; new integrations should use the `*_skill` names |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kolbo/mcp",
3
- "version": "1.119.1",
3
+ "version": "1.120.0",
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": {
@@ -55,10 +55,32 @@ const BRIDGE_JS = `
55
55
 
56
56
  function post(msg) { window.parent.postMessage(msg, '*'); }
57
57
 
58
+ // Longer than the 180s stdio status wait plus transport overhead.
59
+ // This bounds tracking only; it never cancels or resubmits a paid job.
60
+ var REQUEST_TIMEOUT_MS = 240000;
61
+ function timeoutError() {
62
+ var error = new Error('The host did not answer. Tracking paused; the job may still be running.');
63
+ error.code = 'KOLBO_HOST_TIMEOUT';
64
+ return error;
65
+ }
66
+ function boundedCall(run) {
67
+ return new Promise(function (resolve, reject) {
68
+ var timer = setTimeout(function () { reject(timeoutError()); }, REQUEST_TIMEOUT_MS);
69
+ Promise.resolve().then(run).then(function (value) {
70
+ clearTimeout(timer); resolve(value);
71
+ }, function (error) { clearTimeout(timer); reject(error); });
72
+ });
73
+ }
74
+
58
75
  function request(method, params) {
59
76
  return new Promise(function (resolve, reject) {
60
77
  var id = nextId++;
61
- pending[id] = { resolve: resolve, reject: reject };
78
+ var timer = setTimeout(function () {
79
+ delete pending[id];
80
+ queue = queue.filter(function (item) { return item.id !== id; });
81
+ reject(timeoutError());
82
+ }, REQUEST_TIMEOUT_MS);
83
+ pending[id] = { resolve: resolve, reject: reject, timer: timer };
62
84
  var msg = { jsonrpc: '2.0', id: id, method: method, params: params || {} };
63
85
  if (initialized || method === 'ui/initialize') post(msg);
64
86
  else queue.push(msg);
@@ -78,6 +100,7 @@ const BRIDGE_JS = `
78
100
  var p = pending[m.id];
79
101
  if (!p) return;
80
102
  delete pending[m.id];
103
+ clearTimeout(p.timer);
81
104
  if (m.error) p.reject(new Error(m.error.message || 'host error'));
82
105
  else p.resolve(m.result);
83
106
  return;
@@ -282,7 +305,7 @@ const BRIDGE_JS = `
282
305
  var oaCall = function (method) { return typeof oa[method] === 'function'; };
283
306
  window.kolbo.callTool = function (name, args) {
284
307
  if (!oaCall('callTool')) return Promise.reject(new Error('host cannot call tools'));
285
- return Promise.resolve(oa.callTool(name, args || {})).then(function (r) {
308
+ return boundedCall(function () { return oa.callTool(name, args || {}); }).then(function (r) {
286
309
  // The SDK resolves with the CallToolResult (content + structuredContent);
287
310
  // some builds unwrap to the structured payload — normalise either.
288
311
  if (r && (r.structuredContent || r.content)) return r;
@@ -802,7 +802,10 @@ function schedulePoll(sc) {
802
802
  // (~45s over the remote connector — see WAIT_WINDOW_MS in tools/generate.js).
803
803
  // This short pause only separates successive wait windows — the FIRST call
804
804
  // goes out immediately, so a card revealed by scrolling resolves at once.
805
- var delay = pollStart ? 1500 : 0;
805
+ // After failed calls, back off exponentially (capped at 30s): a host that
806
+ // rejects tools/call for a while (busy, rate-limited, reconnecting) used to
807
+ // burn all MAX_POLL_ERRORS retries in ~45s and strand finished generations.
808
+ var delay = pollStart ? Math.min(1500 * Math.pow(2, pollErrors), 30000) : 0;
806
809
  if (!pollStart) pollStart = Date.now();
807
810
  clearTimeout(pollTimer);
808
811
  pollTimer = setTimeout(function () { poll(sc); }, delay);
@@ -813,6 +816,9 @@ function poll(sc) {
813
816
  return renderTrackingIssue('This is still running longer than the tracking window. Do not retry it — any completed result will appear in your Kolbo library.');
814
817
  }
815
818
  var args = sc.status_args || { generation_id: sc.generation_id, wait: true };
819
+ // Host-bridged calls may not reset their timeout on progress: keep each
820
+ // wait window inside the common 60s request timeout.
821
+ if (args.wait) args = Object.assign({}, args, { wait_seconds: 45 });
816
822
  pollInFlight = true;
817
823
  window.kolbo.callTool(sc.poll_tool || 'get_generation_status', args).then(function (res) {
818
824
  pollInFlight = false;
@@ -870,7 +876,7 @@ function poll(sc) {
870
876
  pollErrors = 0; // a valid in-progress response resets the failure streak
871
877
  schedulePoll(sc);
872
878
  }
873
- }).catch(function () {
879
+ }).catch(function (error) {
874
880
  pollInFlight = false;
875
881
  if (cancelRequested || (state && (state.phase === 'completed' || state.phase === 'failed'))) return;
876
882
  if (++pollErrors >= MAX_POLL_ERRORS) return renderTrackingIssue('Tracking paused after repeated connection errors. The generation may still be running.');
@@ -1270,13 +1276,18 @@ function renderError(msg) {
1270
1276
 
1271
1277
  function renderTrackingIssue(msg) {
1272
1278
  clearTimeout(pollTimer);
1273
- setPhaseChip('Still working', false);
1279
+ setPhaseChip('Tracking paused', false);
1274
1280
  el('stage').innerHTML = '<div class="k-error">' + ICONS.clock + ' ' + esc(msg) + '</div>';
1275
1281
  el('actions').innerHTML = '<button class="k-btn primary" id="status-btn">' + ICONS.clock + ' Check status</button>' +
1276
1282
  '<button class="k-btn ghost" id="tracking-open">Open in Kolbo ' + ICONS.open + '</button>';
1283
+ // Resume tracking in place: the card re-checks the SAME generation ids
1284
+ // (never a resubmit). A chat message alone left the card red even after
1285
+ // the agent learned the job had finished.
1277
1286
  el('status-btn').onclick = function () {
1278
- window.kolbo.sendMessage('Check the existing Kolbo generation status without retrying it.' +
1279
- (state && state.generation_id ? '\\nGeneration ID: ' + state.generation_id : ''));
1287
+ if (!state) return;
1288
+ pollErrors = 0; pollStart = 0;
1289
+ setPhaseChip('Checking', true);
1290
+ schedulePoll(state);
1280
1291
  };
1281
1292
  el('tracking-open').onclick = function () {
1282
1293
  window.kolbo.openLink(kolboUrl(state));
@@ -40,6 +40,7 @@ var state = null, pollTimer = null, originArgs = {};
40
40
 
41
41
  function boot(sc) {
42
42
  if (!sc) return;
43
+ clearTimeout(pollTimer);
43
44
  state = sc;
44
45
  el('credits').textContent = sc.credits_used != null ? fmtCredits(sc.credits_used) : '';
45
46
  if (sc.phase === 'generating') {
@@ -93,6 +94,8 @@ function poll() {
93
94
  return trackingIssue('This transcription is still running longer than the tracking window. Do not retry it — any completed result will appear in your Kolbo library.');
94
95
  }
95
96
  var args = state.status_args || { generation_id: state.generation_id, wait: true };
97
+ // Keep each host-bridged wait inside the common 60s request timeout.
98
+ if (args.wait) args = Object.assign({}, args, { wait_seconds: 45 });
96
99
  window.kolbo.callTool(state.poll_tool || 'get_generation_status', args)
97
100
  .then(function (res) {
98
101
  var st = structured(res) || {};
@@ -107,7 +110,8 @@ function poll() {
107
110
  } else if (s === 'failed' || s === 'cancelled') {
108
111
  boot(Object.assign({}, state, { phase: 'failed', error: st.error }));
109
112
  } else { pollErrors = 0; pollTimer = setTimeout(poll, pollDelay()); }
110
- }).catch(function () {
113
+ }).catch(function (error) {
114
+ if (state && state.phase !== 'generating') return;
111
115
  if (++pollErrors >= MAX_POLL_ERRORS) return trackingIssue('Tracking paused after repeated connection errors. The transcription may still be running.');
112
116
  pollTimer = setTimeout(poll, pollDelay());
113
117
  });
@@ -115,20 +119,24 @@ function poll() {
115
119
 
116
120
  function pollDelay() {
117
121
  // The call itself long-waits server-side; only briefly back off between
118
- // successive wait windows.
119
- return 1500;
122
+ // successive wait windows; after failed calls, back off exponentially
123
+ // (capped at 30s) so a briefly unavailable host can't exhaust the retries.
124
+ return Math.min(1500 * Math.pow(2, pollErrors), 30000);
120
125
  }
121
126
 
122
127
  function trackingIssue(msg) {
123
128
  clearTimeout(pollTimer);
124
129
  el('phase-chip').style.display = '';
125
- el('phase-chip').textContent = 'Still working';
130
+ el('phase-chip').textContent = 'Tracking paused';
126
131
  el('stage').classList.remove('k-empty');
127
132
  el('stage').innerHTML = '<div class="k-error">' + ICONS.clock + ' ' + esc(msg) + '</div>';
128
133
  el('actions').innerHTML = '<button class="k-btn primary" id="transcript-status">' + ICONS.clock + ' Check status</button>';
134
+ // Resume tracking in place (same generation id, never a resubmit).
129
135
  el('transcript-status').onclick = function () {
130
- window.kolbo.sendMessage('Check the existing Kolbo transcription status without retrying it.' +
131
- (state && state.generation_id ? '\\nGeneration ID: ' + state.generation_id : ''));
136
+ if (!state) return;
137
+ pollErrors = 0;
138
+ el('actions').innerHTML = '';
139
+ boot(Object.assign({}, state, { phase: 'generating' }));
132
140
  };
133
141
  window.kolbo.notifySize();
134
142
  }
@@ -604,16 +604,17 @@ function registerGenerateTools(server, client, options = {}) {
604
604
  'Check the status of a Creative Director batch (from generate_creative_director) by its generation_id. Returns overall state ("processing" until EVERY scene is terminal, then "completed"/"failed") plus each scene\'s number, title, per-scene status, and image_urls/video_urls. Set wait=true to block until the batch is terminal or the wait window closes, whichever comes first — a batch longer than one window returns state="processing" and you simply call again with wait=true. Prefer this over the generic get_generation_status for Creative Director ids — the generic tool now returns the same scene data (it delegates here), but this one is the direct route.',
605
605
  {
606
606
  generation_id: z.string().describe('The Creative Director generation_id returned by generate_creative_director.'),
607
- wait: z.boolean().optional().describe(`If true, block until the batch is terminal, for at most ~${WAIT_WINDOW_S}s per call. A batch that outlives one window comes back state="processing" (not an error) — call again with wait=true until it is terminal. Always prefer this over polling with wait=false.`)
607
+ wait: z.boolean().optional().describe(`If true, block until the batch is terminal, for at most ~${WAIT_WINDOW_S}s per call. A batch that outlives one window comes back state="processing" (not an error) — call again with wait=true until it is terminal. Always prefer this over polling with wait=false.`),
608
+ wait_seconds: z.number().min(1).max(180).optional().describe('Optional cap on one wait=true window, in seconds (default and maximum: the transport window). Interactive cards pass 45 so each call fits a host 60s request timeout.')
608
609
  },
609
- async ({ generation_id, wait }) => {
610
+ async ({ generation_id, wait, wait_seconds }) => {
610
611
  const statusUrl = `/v1/generate/creative-director/${encodeURIComponent(generation_id)}/status`;
611
612
  let status;
612
613
  if (wait) {
613
614
  try {
614
615
  status = await pollUntilDone(client, generation_id, {
615
616
  interval: 15000,
616
- timeout: WAIT_WINDOW_MS,
617
+ timeout: wait_seconds ? Math.min(wait_seconds * 1000, WAIT_WINDOW_MS) : WAIT_WINDOW_MS,
617
618
  statusUrl
618
619
  });
619
620
  } catch (err) {
@@ -1348,14 +1349,20 @@ function registerGenerateTools(server, client, options = {}) {
1348
1349
  {
1349
1350
  generation_id: z.string().optional().describe('A single generation ID to check'),
1350
1351
  generation_ids: z.array(z.string()).max(MAX_STATUS_IDS).optional().describe(`Multiple generation IDs to check in ONE call (max ${MAX_STATUS_IDS}). Returns { all_done, pending, generations[] } — always prefer this over checking IDs one by one.`),
1351
- wait: z.boolean().optional().describe(`If true, block until every generation reaches a terminal state (completed/failed), for at most ~${WAIT_WINDOW_S}s per call, then return whatever state they are in. Anything still processing is reported, not errored — re-issue with wait=true and only the still-pending ids. This is always better than polling with wait=false.`)
1352
+ wait: z.boolean().optional().describe(`If true, block until every generation reaches a terminal state (completed/failed), for at most ~${WAIT_WINDOW_S}s per call, then return whatever state they are in. Anything still processing is reported, not errored — re-issue with wait=true and only the still-pending ids. This is always better than polling with wait=false.`),
1353
+ wait_seconds: z.number().min(1).max(180).optional().describe('Optional cap on one wait=true window, in seconds (default and maximum: the transport window). Interactive cards pass 45 so each call fits a host 60s request timeout.')
1352
1354
  },
1353
- async ({ generation_id, generation_ids, wait }) => {
1355
+ async ({ generation_id, generation_ids, wait, wait_seconds }) => {
1354
1356
  const ids = (generation_ids && generation_ids.length > 0)
1355
1357
  ? [...new Set(generation_ids)]
1356
1358
  : (generation_id ? [generation_id] : []);
1357
1359
  if (ids.length === 0) throw new Error('Provide generation_id or generation_ids');
1358
1360
  if (ids.length > MAX_STATUS_IDS) throw new Error(`At most ${MAX_STATUS_IDS} generation IDs can be checked at once.`);
1361
+ // Widget polls arrive through the host's iframe bridge, which (unlike the
1362
+ // model's own calls) need not reset its request timeout on our progress
1363
+ // ticks — a 180s stdio window there timed out every poll, stranding cards
1364
+ // on Generating and then "Tracking paused". Widgets pass wait_seconds.
1365
+ const windowMs = wait_seconds ? Math.min(wait_seconds * 1000, WAIT_WINDOW_MS) : WAIT_WINDOW_MS;
1359
1366
 
1360
1367
  // One status check (or blocking poll) per id. Never let one bad id
1361
1368
  // reject the whole batch — surface it as a failed entry instead.
@@ -1366,7 +1373,7 @@ function registerGenerateTools(server, client, options = {}) {
1366
1373
  // completion responsive without multiplying backend traffic for
1367
1374
  // every card left open in a host conversation. The window itself is
1368
1375
  // bounded by the transport — see WAIT_WINDOW_MS above.
1369
- const result = await pollUntilDone(client, id, { interval: 15000, timeout: WAIT_WINDOW_MS });
1376
+ const result = await pollUntilDone(client, id, { interval: 15000, timeout: windowMs });
1370
1377
  return { generation_id: id, ...result };
1371
1378
  }
1372
1379
  const result = await client.get(`/v1/generate/${encodeURIComponent(id)}/status`);
@@ -44,16 +44,17 @@ function registerPresetTools(server, client, options = {}) {
44
44
  : 'Pass search next time if you already know the name. Pass the chosen exact id as preset_id.',
45
45
  });
46
46
 
47
- if (lookup) return { content: [{ type: 'text', text }] };
48
-
49
47
  // TOOL_WIDGETS declares mediaGrid for this tool, so hosts that mount from
50
- // the declaration (Claude desktop) rendered a media grid fed a list-shaped
51
- // 8-item page with no page_tool: a "Load more" that could never load. Ship
52
- // the whole catalog (a few hundred small rows) to the grid it actually mounts.
48
+ // the declaration (Claude desktop) prepare a grid on EVERY call — the
49
+ // search lookup included. A text-only lookup left that grid guessing from
50
+ // the compact JSON: thumbnail-less file tiles under the UNFILTERED total.
51
+ // structuredContent also shadows the text on those hosts, so it carries
52
+ // the ids, total and note the agent needs.
53
+ const total = search && !result.search ? presets.length : (result.count || presets.length);
53
54
  return uiResult(UI.mediaGrid, text, {
54
55
  widget: 'mediaGrid',
55
56
  title: 'Presets' + (type ? ' — ' + type : ''),
56
- items: presets.slice(0, 300).map(p => ({
57
+ items: presets.slice(0, lookup ? 8 : 300).map(p => ({
57
58
  id: p.id,
58
59
  title: p.name,
59
60
  subtitle: p.category,
@@ -63,8 +64,10 @@ function registerPresetTools(server, client, options = {}) {
63
64
  url: p.thumbnail_url || p.thumbnail,
64
65
  use_hint: 'Use preset "{TITLE}" (preset_id: {ID}) for my next generation — ask me for the prompt.'
65
66
  })),
66
- total: result.count || presets.length,
67
- has_more: presets.length > 300
67
+ total,
68
+ has_more: presets.length > (lookup ? 8 : 300),
69
+ ...(lookup ? { _lookup: true, note: 'Pick the closest id and pass it as preset_id.' } : {}),
70
+ ...(result.warning ? { warning: result.warning } : {}),
68
71
  });
69
72
  }
70
73
  );