@kolbo/mcp 1.49.0 → 1.50.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kolbo/mcp",
3
- "version": "1.49.0",
3
+ "version": "1.50.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": {
package/src/index.js CHANGED
@@ -70,9 +70,9 @@ const { registerArtifactTools } = require('./tools/artifacts');
70
70
  const { registerProjectTools } = require('./tools/projects');
71
71
  const { registerAgentTools } = require('./tools/agents');
72
72
  const { registerDocTools } = require('./tools/docs');
73
- const { registerVoiceTools } = require('./tools/voices');
74
- const { registerMusicLibraryTools } = require('./tools/music_library');
75
- const { registerStockLibraryTools } = require('./tools/stock_library');
73
+ const { registerVoiceTools } = require('./tools/voices');
74
+ const { registerMusicLibraryTools } = require('./tools/music_library');
75
+ const { registerStockLibraryTools } = require('./tools/stock_library');
76
76
  const { registerApps, attachToolWidgetMeta } = require('./apps');
77
77
 
78
78
  /**
@@ -115,7 +115,7 @@ function createServer(opts = {}) {
115
115
  '3. Misplaced work is fixable: `move_media` / `bulk_move_media` / `move_folder_contents` move media items between projects; `move_session` moves a whole session (plus its media) to another project. If the user says a generation landed in the wrong project, move it rather than regenerating.',
116
116
  '4. If the user has not mentioned any project, omit `project_id` — the default bucket is correct in that case. Do not ask which project to use unless the user\'s intent is ambiguous.',
117
117
  '5. Written deliverables (plans, briefs, scripts, research summaries) can live in Kolbo too: author them as AI Docs with `create_doc` (project-scoped, editable in the app, shareable via `share_doc`).',
118
- '6. DIRECTOR / BATCH JOBS: generate_creative_director runs its scenes (image OR video) in parallel and only reports state="completed" once EVERY scene is terminal. Video batches can take many minutes. If the tool returns `_timed_out:true`, the batch is STILL RUNNING on the server call `get_creative_director_status` with the returned generation_id and keep checking until state="completed" to collect all scene outputs. NEVER conclude a Director run failed and fall back to plain generate_image/generate_video without first checking statusdoing so wastes the user\'s credits by paying twice. If scenes already carry image_urls/video_urls, they are done; do not regenerate.',
118
+ '6. TIMEOUT HANDLING (applies to EVERY generate_* / edit_* / chat_send_message / transcribe_audio tool): each tool blocks and polls internally, then gives up after its own window if the job is not yet terminal. A timeout is NOT a failure it returns `_timed_out:true` with the `generation_id` (not an error), because the job is almost always STILL RUNNING on the server (or already finished). Call `get_generation_status` with that `generation_id` and `wait=true` to keep checking until state="completed". NEVER conclude the generation failed and re-run the same tool from scratch after a `_timed_out:true` result that wastes the user\'s credits by paying twice. DIRECTOR / BATCH JOBS follow the same convention through a dedicated tool: generate_creative_director runs its scenes (image OR video) in parallel and only reports state="completed" once EVERY scene is terminal; on `_timed_out:true` call `get_creative_director_status` (not get_generation_status) with the returned generation_id and keep checking. If scenes already carry image_urls/video_urls, they are done; do not regenerate.',
119
119
  '7. SESSION CONTINUITY — one task, one session, always: every generation tool returns a `session_id`. For ANY follow-up, refinement, retry, or next step on the SAME task, pass that session_id back — never start fresh. BATCH RULE (critical): when a single user request produces multiple parallel generations (e.g. "animate these 5 images", "generate 3 variants"), do NOT launch them all at once without a session_id. Instead: (1) run the FIRST generation without session_id to create the session, (2) capture the session_id from its response, (3) pass that session_id to ALL remaining generations in the batch. This keeps the entire batch in one session. Exception: only omit session_id and start fresh when the user explicitly starts an unrelated new task.',
120
120
  '8. LOCAL FILES / REFERENCE MEDIA — HOW TO HANDLE EVERY CASE: (A) User has a LOCAL file (audio, video, image, document) on their machine: if you have filesystem access (Claude Desktop / Code / IDE / any stdio MCP client) → call `upload_media` with the absolute local path OR pass the path directly to tools like `transcribe_audio` which accept local paths natively. If you have NO filesystem access (claude.ai browser/mobile) → call `media_upload_widget` IMMEDIATELY, an upload card appears, the user uploads, and a `media.kolbo.ai` CDN URL comes back — use that URL for any follow-up tool call. (B) You already have a public URL (media.kolbo.ai, any CDN, any direct link) → pass it directly to the tool. All Kolbo tools accept public URLs. NEVER search for DO Spaces keys, DigitalOcean credentials, or server-side upload credentials. NEVER ask the user to put the file on Google Drive, Dropbox, or Loom. NEVER invent or guess a URL. NEVER base64 a large file — use upload_media instead.',
121
121
  '9. MODEL SELECTION: ALWAYS pass a specific `model` on every generation tool — do NOT omit it. Omitting falls back to "Smart Select" auto-routing, which we deliberately avoid because it hides the model choice from the user and often picks a generic default. Choose the model that best fits the task and the user\'s intent (quality, speed, style, capability). If you are unsure which model to use for a given type, call `list_models` with the matching `type` and pick the recommended/flagship one, then pass its `identifier`. Only use Smart Select (omit `model`) if the user EXPLICITLY asks you to auto-pick.',
@@ -139,9 +139,9 @@ function createServer(opts = {}) {
139
139
  registerArtifactTools(server, client, toolOptions);
140
140
  registerProjectTools(server, client, toolOptions);
141
141
  registerAgentTools(server, client, toolOptions);
142
- registerDocTools(server, client, toolOptions);
143
- registerMusicLibraryTools(server, client, toolOptions);
144
- registerStockLibraryTools(server, client, toolOptions);
142
+ registerDocTools(server, client, toolOptions);
143
+ registerMusicLibraryTools(server, client, toolOptions);
144
+ registerStockLibraryTools(server, client, toolOptions);
145
145
 
146
146
  // MCP Apps widget resources (ui://kolbo/*). Registering resources is inert
147
147
  // for text-only hosts — they never fetch them.
@@ -214,6 +214,45 @@ async function resolveToBuffer(source, kind, opts = {}) {
214
214
  };
215
215
  }
216
216
 
217
+ // ─── Universal graceful-timeout convention ───────────────────────────────────
218
+ // pollUntilDone throws PollingTimeoutError (err.timedOut === true) when the
219
+ // CLIENT-SIDE poll window elapses — the generation is almost always still
220
+ // running server-side (or already done). Originally only
221
+ // generate_creative_director caught this and returned a non-throwing
222
+ // "_timed_out" result; every other tool let it propagate, so the MCP SDK
223
+ // wrapped it as isError:true and recovery depended entirely on the calling
224
+ // LLM reading hint text. pollOrTimedOut() is the one place that decision now
225
+ // lives — every generation/chat tool routes its pollUntilDone call through
226
+ // it instead of duplicating the try/catch. Genuine failures (state
227
+ // failed/cancelled → GenerationFailedError) are NOT caught here; they still
228
+ // throw and surface as a real tool error.
229
+ const { pollUntilDone } = require('../polling');
230
+
231
+ /**
232
+ * @returns {Promise<{result: object}|{timedOut: object}>}
233
+ * Callers do: `const poll = await pollOrTimedOut(...); if (poll.timedOut) return poll.timedOut; const result = poll.result;`
234
+ */
235
+ async function pollOrTimedOut(client, generationId, pollOpts) {
236
+ try {
237
+ return { result: await pollUntilDone(client, generationId, pollOpts) };
238
+ } catch (err) {
239
+ if (!err || !err.timedOut) throw err;
240
+ return {
241
+ timedOut: {
242
+ content: [{
243
+ type: 'text',
244
+ text: JSON.stringify({
245
+ state: 'processing',
246
+ generation_id: generationId,
247
+ _timed_out: true,
248
+ _hint: `Still running on the server after the poll window — this is NOT a failure, no credits were lost. Call get_generation_status with generation_id="${generationId}" (wait=true) to keep checking until state="completed". Do NOT re-run this tool.`
249
+ }, null, 2)
250
+ }]
251
+ }
252
+ };
253
+ }
254
+ }
255
+
217
256
  /**
218
257
  * Extract real, multiplier-adjusted credit cost from a polled getStatus
219
258
  * response. kolbo-api returns `credits_used` (final number deducted) and
@@ -462,6 +501,7 @@ module.exports = {
462
501
  guessFilename,
463
502
  guessContentType,
464
503
  resolveToBuffer,
504
+ pollOrTimedOut,
465
505
  creditFields,
466
506
  projectIdField,
467
507
  projectScopeReadField,
package/src/tools/chat.js CHANGED
@@ -4,8 +4,7 @@
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 { pollUntilDone } = require('../polling');
8
- const { creditFields, projectIdField } = require('./_shared');
7
+ const { pollOrTimedOut, creditFields, projectIdField } = require('./_shared');
9
8
 
10
9
  function registerChatTools(server, client) {
11
10
  // ─── chat_send_message ─────────────────────────────────────
@@ -40,10 +39,12 @@ function registerChatTools(server, client) {
40
39
  // extra time when web_search is on (may fetch + analyze multiple pages).
41
40
  const timeout = deep_think ? 600000 : (web_search ? 240000 : 120000);
42
41
 
43
- const result = await pollUntilDone(client, gen.message_id, {
42
+ const poll = await pollOrTimedOut(client, gen.message_id, {
44
43
  interval: (gen.poll_interval_hint || 2) * 1000,
45
44
  timeout
46
45
  });
46
+ if (poll.timedOut) return poll.timedOut;
47
+ const result = poll.result;
47
48
 
48
49
  // Chat status shape (from extractResult in kolbo-api sdk/controller.js):
49
50
  // { content, reasoning_content, image_urls?, video_urls?, audio_urls?, model, created_at }
@@ -6,7 +6,7 @@
6
6
  const { z } = require('zod');
7
7
  const FormData = require('form-data');
8
8
  const { pollUntilDone } = require('../polling');
9
- const { resolveToBuffer, creditFields, projectIdField, inlineImageBlocks, buildOpenUrl, uiGenerating, appsEnabled } = require('./_shared');
9
+ const { resolveToBuffer, pollOrTimedOut, creditFields, projectIdField, inlineImageBlocks, buildOpenUrl, uiGenerating, appsEnabled } = require('./_shared');
10
10
  const { UI, uiResult, canonicalModelId } = require('../apps');
11
11
 
12
12
  // ─── Cinematic Dimensions schema (shared by generate_image + generate_image_edit) ───
@@ -79,10 +79,12 @@ function registerGenerateTools(server, client, options = {}) {
79
79
  reference_image: reference_images?.[0]
80
80
  });
81
81
 
82
- const result = await pollUntilDone(client, gen.generation_id, {
82
+ const poll = await pollOrTimedOut(client, gen.generation_id, {
83
83
  interval: (gen.poll_interval_hint || 3) * 1000,
84
84
  timeout: 120000
85
85
  });
86
+ if (poll.timedOut) return poll.timedOut;
87
+ const result = poll.result;
86
88
 
87
89
  const images = await inlineImageBlocks(result.result.urls, { enabled: inlineImages });
88
90
  return {
@@ -135,10 +137,12 @@ function registerGenerateTools(server, client, options = {}) {
135
137
  // server-side. Extend the polling window in those cases to avoid forcing
136
138
  // every call into the timeout-and-recover path via get_generation_status.
137
139
  const heavy = (source_images && source_images.length > 1) || (visual_dna_ids && visual_dna_ids.length > 0);
138
- const result = await pollUntilDone(client, gen.generation_id, {
140
+ const poll = await pollOrTimedOut(client, gen.generation_id, {
139
141
  interval: (gen.poll_interval_hint || 3) * 1000,
140
142
  timeout: heavy ? 240000 : 120000
141
143
  });
144
+ if (poll.timedOut) return poll.timedOut;
145
+ const result = poll.result;
142
146
 
143
147
  const images = await inlineImageBlocks(result.result.urls, { enabled: inlineImages });
144
148
  return {
@@ -337,10 +341,15 @@ function registerGenerateTools(server, client, options = {}) {
337
341
  reference_image: reference_images?.[0]
338
342
  });
339
343
 
340
- const result = await pollUntilDone(client, gen.generation_id, {
344
+ // 15 min Kling O3 4K, Veo 3.1 at higher durations/resolutions, Hailuo 2.3,
345
+ // and Seedance 2 routinely run 5-12 min under provider queue load; the old
346
+ // 5-min window forced routine calls into the timeout-and-recover path.
347
+ const poll = await pollOrTimedOut(client, gen.generation_id, {
341
348
  interval: (gen.poll_interval_hint || 8) * 1000,
342
- timeout: 300000
349
+ timeout: 900000
343
350
  });
351
+ if (poll.timedOut) return poll.timedOut;
352
+ const result = poll.result;
344
353
 
345
354
  return {
346
355
  content: [{
@@ -387,10 +396,14 @@ function registerGenerateTools(server, client, options = {}) {
387
396
  reference_image: image_url
388
397
  });
389
398
 
390
- const result = await pollUntilDone(client, gen.generation_id, {
399
+ // 15 min same real-world generation times as generate_video (Kling O3 4K,
400
+ // Veo 3.1, Hailuo 2.3, Seedance 2 at higher durations/resolutions).
401
+ const poll = await pollOrTimedOut(client, gen.generation_id, {
391
402
  interval: (gen.poll_interval_hint || 8) * 1000,
392
- timeout: 300000
403
+ timeout: 900000
393
404
  });
405
+ if (poll.timedOut) return poll.timedOut;
406
+ const result = poll.result;
394
407
 
395
408
  return {
396
409
  content: [{
@@ -448,10 +461,12 @@ function registerGenerateTools(server, client, options = {}) {
448
461
  settings: { mode: instrumental ? 'instrumental' : (style || undefined) },
449
462
  });
450
463
 
451
- const result = await pollUntilDone(client, gen.generation_id, {
464
+ const poll = await pollOrTimedOut(client, gen.generation_id, {
452
465
  interval: (gen.poll_interval_hint || 8) * 1000,
453
466
  timeout: 300000
454
467
  });
468
+ if (poll.timedOut) return poll.timedOut;
469
+ const result = poll.result;
455
470
 
456
471
  return {
457
472
  content: [{
@@ -520,10 +535,12 @@ function registerGenerateTools(server, client, options = {}) {
520
535
  settings: { voice: voice || 'Rachel', style: selected_style || emotion || style_instructions }
521
536
  });
522
537
 
523
- const result = await pollUntilDone(client, gen.generation_id, {
538
+ const poll = await pollOrTimedOut(client, gen.generation_id, {
524
539
  interval: (gen.poll_interval_hint || 5) * 1000,
525
540
  timeout: 120000
526
541
  });
542
+ if (poll.timedOut) return poll.timedOut;
543
+ const result = poll.result;
527
544
 
528
545
  return {
529
546
  content: [{
@@ -577,10 +594,12 @@ function registerGenerateTools(server, client, options = {}) {
577
594
  settings: { duration }
578
595
  });
579
596
 
580
- const result = await pollUntilDone(client, gen.generation_id, {
597
+ const poll = await pollOrTimedOut(client, gen.generation_id, {
581
598
  interval: (gen.poll_interval_hint || 5) * 1000,
582
599
  timeout: 120000
583
600
  });
601
+ if (poll.timedOut) return poll.timedOut;
602
+ const result = poll.result;
584
603
 
585
604
  return {
586
605
  content: [{
@@ -625,7 +644,7 @@ function registerGenerateTools(server, client, options = {}) {
625
644
  return { generation_id: id, ...result };
626
645
  } catch (err) {
627
646
  if (err.timedOut) {
628
- return { generation_id: id, state: 'processing', note: 'Still running after 3 min of waiting — call get_generation_status again with wait=true.' };
647
+ return { generation_id: id, state: 'processing', _timed_out: true, note: 'Still running after 3 min of waiting — call get_generation_status again with wait=true.' };
629
648
  }
630
649
  if (err.name === 'GenerationFailedError') {
631
650
  return { generation_id: id, state: 'failed', error: err.message };
@@ -734,10 +753,12 @@ function registerGenerateTools(server, client, options = {}) {
734
753
  reference_image: reference_images?.[0]
735
754
  });
736
755
 
737
- const result = await pollUntilDone(client, startResponse.generation_id, {
756
+ const poll = await pollOrTimedOut(client, startResponse.generation_id, {
738
757
  interval: (startResponse.poll_interval_hint || 8) * 1000,
739
758
  timeout: 600000
740
759
  });
760
+ if (poll.timedOut) return poll.timedOut;
761
+ const result = poll.result;
741
762
 
742
763
  return {
743
764
  content: [{
@@ -813,10 +834,12 @@ function registerGenerateTools(server, client, options = {}) {
813
834
  reference_image: first_frame_url || undefined
814
835
  });
815
836
 
816
- const result = await pollUntilDone(client, startResponse.generation_id, {
837
+ const poll = await pollOrTimedOut(client, startResponse.generation_id, {
817
838
  interval: (startResponse.poll_interval_hint || 8) * 1000,
818
839
  timeout: 300000
819
840
  });
841
+ if (poll.timedOut) return poll.timedOut;
842
+ const result = poll.result;
820
843
 
821
844
  return {
822
845
  content: [{
@@ -923,10 +946,12 @@ function registerGenerateTools(server, client, options = {}) {
923
946
  reference_image: sourceIsUrl && !/\.(mp4|mov|webm|mkv|avi|m4v)(\?|$)/i.test(source) ? source : undefined,
924
947
  });
925
948
 
926
- const result = await pollUntilDone(client, startResponse.generation_id, {
949
+ const poll = await pollOrTimedOut(client, startResponse.generation_id, {
927
950
  interval: (startResponse.poll_interval_hint || 8) * 1000,
928
951
  timeout: 600000
929
952
  });
953
+ if (poll.timedOut) return poll.timedOut;
954
+ const result = poll.result;
930
955
 
931
956
  return {
932
957
  content: [{
@@ -1023,10 +1048,12 @@ function registerGenerateTools(server, client, options = {}) {
1023
1048
  reference_image: reference_images?.[0]
1024
1049
  });
1025
1050
 
1026
- const result = await pollUntilDone(client, startResponse.generation_id, {
1051
+ const poll = await pollOrTimedOut(client, startResponse.generation_id, {
1027
1052
  interval: (startResponse.poll_interval_hint || 8) * 1000,
1028
1053
  timeout: 600000
1029
1054
  });
1055
+ if (poll.timedOut) return poll.timedOut;
1056
+ const result = poll.result;
1030
1057
 
1031
1058
  return {
1032
1059
  content: [{
@@ -1097,10 +1124,12 @@ function registerGenerateTools(server, client, options = {}) {
1097
1124
  });
1098
1125
  }
1099
1126
 
1100
- const result = await pollUntilDone(client, startResponse.generation_id, {
1127
+ const poll = await pollOrTimedOut(client, startResponse.generation_id, {
1101
1128
  interval: (startResponse.poll_interval_hint || 5) * 1000,
1102
1129
  timeout: 1800000 // 30 minutes — long podcasts are a thing
1103
1130
  });
1131
+ if (poll.timedOut) return poll.timedOut;
1132
+ const result = poll.result;
1104
1133
 
1105
1134
  return {
1106
1135
  content: [{
@@ -1159,10 +1188,12 @@ function registerGenerateTools(server, client, options = {}) {
1159
1188
  reference_image: reference_images?.[0]
1160
1189
  });
1161
1190
 
1162
- const result = await pollUntilDone(client, startResponse.generation_id, {
1191
+ const poll = await pollOrTimedOut(client, startResponse.generation_id, {
1163
1192
  interval: (startResponse.poll_interval_hint || 8) * 1000,
1164
1193
  timeout: 900000 // 15 minutes — 3D generation is slow
1165
1194
  });
1195
+ if (poll.timedOut) return poll.timedOut;
1196
+ const result = poll.result;
1166
1197
 
1167
1198
  return {
1168
1199
  content: [{
@@ -1281,10 +1312,12 @@ function registerGenerateTools(server, client, options = {}) {
1281
1312
  reference_image: image_url
1282
1313
  });
1283
1314
 
1284
- const result = await pollUntilDone(client, gen.generation_id, {
1315
+ const poll = await pollOrTimedOut(client, gen.generation_id, {
1285
1316
  interval: (gen.poll_interval_hint || 5) * 1000,
1286
1317
  timeout: 300000 // 5 min — split_upscale and multi_shot can take longer
1287
1318
  });
1319
+ if (poll.timedOut) return poll.timedOut;
1320
+ const result = poll.result;
1288
1321
 
1289
1322
  return {
1290
1323
  content: [{
@@ -1439,10 +1472,12 @@ function registerGenerateTools(server, client, options = {}) {
1439
1472
  reference_image: image_url
1440
1473
  });
1441
1474
 
1442
- const result = await pollUntilDone(client, gen.generation_id, {
1475
+ const poll = await pollOrTimedOut(client, gen.generation_id, {
1443
1476
  interval: (gen.poll_interval_hint || 8) * 1000,
1444
1477
  timeout: 600000
1445
1478
  });
1479
+ if (poll.timedOut) return poll.timedOut;
1480
+ const result = poll.result;
1446
1481
 
1447
1482
  return {
1448
1483
  content: [{