@kolbo/mcp 1.48.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.
@@ -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, uiCompleted, 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) ───
@@ -43,8 +43,8 @@ function registerGenerateTools(server, client, options = {}) {
43
43
  // tool output is unchanged: a text block with the image URL.
44
44
  const inlineImages = !!options.inlineImages;
45
45
  // MCP Apps hosts (claude.ai remote connector, Claude Desktop) get an instant
46
- // "submitted" response + a live ui://kolbo/generation.html widget that polls
47
- // get_generation_status itself. Text-only hosts never take this branch.
46
+ // "submitted" response + a live ui://kolbo/generation.html widget that keeps
47
+ // one wait=true status call in flight. Text-only hosts never take this branch.
48
48
  const ui = () => appsEnabled(server, options);
49
49
  // ─── generate_image ────────────────────────────────────────
50
50
  server.tool(
@@ -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 {
@@ -183,6 +187,15 @@ function registerGenerateTools(server, client, options = {}) {
183
187
  });
184
188
 
185
189
  const cdStatusUrl = `/v1/generate/creative-director/${gen.generation_id}/status`;
190
+ if (ui()) return uiGenerating({
191
+ tool: 'generate_creative_director', kind: 'scenes', gen, client, model, prompt,
192
+ count: scene_count || 4,
193
+ settings: { duration, resolution, aspect_ratio, mode: workflow_type || 'image' },
194
+ reference_image: reference_images?.[0],
195
+ poll_tool: 'get_creative_director_status',
196
+ status_args: { generation_id: gen.generation_id, wait: true }
197
+ });
198
+
186
199
  // Video mode runs N scenes as parallel video generations, each of which
187
200
  // can take several minutes — a whole batch routinely exceeds the 10-min
188
201
  // image window. Give video batches 30 min (the server watchdog finalizes
@@ -237,15 +250,6 @@ function registerGenerateTools(server, client, options = {}) {
237
250
  _followup_hint: 'Each scene is a separate asset. If the user asks to edit one scene, find that scene by scene_number/title and pass its image_urls[0] (or video_urls[0]) to generate_image_edit / edit_image / edit_video / generate_video_from_video. Do NOT re-run generate_creative_director unless the user explicitly wants a brand-new set.'
238
251
  }, null, 2);
239
252
 
240
- // Creative Director polls a dedicated status route the widget can't reach
241
- // through get_generation_status, so it stays blocking on UI hosts too and
242
- // renders the completed scene gallery.
243
- if (ui()) return uiCompleted({
244
- tool: 'generate_creative_director', kind: 'scenes', gen, client, model, prompt,
245
- settings: { duration, resolution, mode: workflow_type }, scenes,
246
- credits_used: creditFields(result).credits_used
247
- }, cdText);
248
-
249
253
  return { content: [{ type: 'text', text: cdText }] };
250
254
  }
251
255
  );
@@ -258,12 +262,30 @@ function registerGenerateTools(server, client, options = {}) {
258
262
  // blocking poll window) needs this tool to be re-checked until done.
259
263
  server.tool(
260
264
  'get_creative_director_status',
261
- '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. Use this to resume checking a batch that was still running when generate_creative_director returned `_timed_out: true` — poll it until state="completed" to collect ALL parallel scene outputs at once. Do NOT use the generic get_generation_status for Creative Director ids; it points at the wrong endpoint.',
265
+ '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 for up to ~3 minutes instead of repeatedly calling this tool. Do NOT use the generic get_generation_status for Creative Director ids; it points at the wrong endpoint.',
262
266
  {
263
- generation_id: z.string().describe('The Creative Director generation_id returned by generate_creative_director.')
267
+ generation_id: z.string().describe('The Creative Director generation_id returned by generate_creative_director.'),
268
+ wait: z.boolean().optional().describe('If true, block until the batch is terminal, up to ~3 minutes. Use this instead of repeatedly checking in a loop.')
264
269
  },
265
- async ({ generation_id }) => {
266
- const status = await client.get(`/v1/generate/creative-director/${encodeURIComponent(generation_id)}/status`);
270
+ async ({ generation_id, wait }) => {
271
+ const statusUrl = `/v1/generate/creative-director/${encodeURIComponent(generation_id)}/status`;
272
+ let status;
273
+ if (wait) {
274
+ try {
275
+ status = await pollUntilDone(client, generation_id, {
276
+ interval: 15000,
277
+ timeout: 180000,
278
+ statusUrl
279
+ });
280
+ } catch (err) {
281
+ // A tracking timeout is not a failed paid generation. Return the
282
+ // latest batch state so the widget can start one new long wait.
283
+ if (!err?.timedOut) throw err;
284
+ status = await client.get(statusUrl);
285
+ }
286
+ } else {
287
+ status = await client.get(statusUrl);
288
+ }
267
289
  const scenes = (status.scenes || []).map(s => ({
268
290
  scene_number: s.scene_number,
269
291
  status: s.status,
@@ -273,6 +295,7 @@ function registerGenerateTools(server, client, options = {}) {
273
295
  }));
274
296
  const completed = scenes.filter(s => s.status === 'completed').length;
275
297
  return { content: [{ type: 'text', text: JSON.stringify({
298
+ ...creditFields(status),
276
299
  state: status.state,
277
300
  generation_id,
278
301
  progress: status.progress,
@@ -281,7 +304,7 @@ function registerGenerateTools(server, client, options = {}) {
281
304
  completed_scenes: completed,
282
305
  _hint: status.state === 'completed'
283
306
  ? 'All scenes terminal. Every completed scene\'s image_urls/video_urls are final.'
284
- : 'Still running call get_creative_director_status again in a few seconds until state="completed".'
307
+ : 'Still running. Call get_creative_director_status once with wait=true; do not poll it in a loop.'
285
308
  }, null, 2) }] };
286
309
  }
287
310
  );
@@ -318,10 +341,15 @@ function registerGenerateTools(server, client, options = {}) {
318
341
  reference_image: reference_images?.[0]
319
342
  });
320
343
 
321
- 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, {
322
348
  interval: (gen.poll_interval_hint || 8) * 1000,
323
- timeout: 300000
349
+ timeout: 900000
324
350
  });
351
+ if (poll.timedOut) return poll.timedOut;
352
+ const result = poll.result;
325
353
 
326
354
  return {
327
355
  content: [{
@@ -368,10 +396,14 @@ function registerGenerateTools(server, client, options = {}) {
368
396
  reference_image: image_url
369
397
  });
370
398
 
371
- 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, {
372
402
  interval: (gen.poll_interval_hint || 8) * 1000,
373
- timeout: 300000
403
+ timeout: 900000
374
404
  });
405
+ if (poll.timedOut) return poll.timedOut;
406
+ const result = poll.result;
375
407
 
376
408
  return {
377
409
  content: [{
@@ -429,10 +461,12 @@ function registerGenerateTools(server, client, options = {}) {
429
461
  settings: { mode: instrumental ? 'instrumental' : (style || undefined) },
430
462
  });
431
463
 
432
- const result = await pollUntilDone(client, gen.generation_id, {
464
+ const poll = await pollOrTimedOut(client, gen.generation_id, {
433
465
  interval: (gen.poll_interval_hint || 8) * 1000,
434
466
  timeout: 300000
435
467
  });
468
+ if (poll.timedOut) return poll.timedOut;
469
+ const result = poll.result;
436
470
 
437
471
  return {
438
472
  content: [{
@@ -501,10 +535,12 @@ function registerGenerateTools(server, client, options = {}) {
501
535
  settings: { voice: voice || 'Rachel', style: selected_style || emotion || style_instructions }
502
536
  });
503
537
 
504
- const result = await pollUntilDone(client, gen.generation_id, {
538
+ const poll = await pollOrTimedOut(client, gen.generation_id, {
505
539
  interval: (gen.poll_interval_hint || 5) * 1000,
506
540
  timeout: 120000
507
541
  });
542
+ if (poll.timedOut) return poll.timedOut;
543
+ const result = poll.result;
508
544
 
509
545
  return {
510
546
  content: [{
@@ -558,10 +594,12 @@ function registerGenerateTools(server, client, options = {}) {
558
594
  settings: { duration }
559
595
  });
560
596
 
561
- const result = await pollUntilDone(client, gen.generation_id, {
597
+ const poll = await pollOrTimedOut(client, gen.generation_id, {
562
598
  interval: (gen.poll_interval_hint || 5) * 1000,
563
599
  timeout: 120000
564
600
  });
601
+ if (poll.timedOut) return poll.timedOut;
602
+ const result = poll.result;
565
603
 
566
604
  return {
567
605
  content: [{
@@ -596,14 +634,17 @@ function registerGenerateTools(server, client, options = {}) {
596
634
  const checkOne = async (id) => {
597
635
  try {
598
636
  if (wait) {
599
- const result = await pollUntilDone(client, id, { interval: 5000, timeout: 180000 });
637
+ // Widgets use this long-wait path. A 15s API check cadence keeps
638
+ // completion responsive without multiplying backend traffic for
639
+ // every card left open in a host conversation.
640
+ const result = await pollUntilDone(client, id, { interval: 15000, timeout: 180000 });
600
641
  return { generation_id: id, ...result };
601
642
  }
602
643
  const result = await client.get(`/v1/generate/${encodeURIComponent(id)}/status`);
603
644
  return { generation_id: id, ...result };
604
645
  } catch (err) {
605
646
  if (err.timedOut) {
606
- 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.' };
607
648
  }
608
649
  if (err.name === 'GenerationFailedError') {
609
650
  return { generation_id: id, state: 'failed', error: err.message };
@@ -621,8 +662,8 @@ function registerGenerateTools(server, client, options = {}) {
621
662
  : 'Some generations are still processing. Do NOT re-call this tool in a loop — call it ONCE with wait=true (and all pending generation_ids) to block until they finish.';
622
663
 
623
664
  // Single-id calls keep the original flat shape — the generation widget
624
- // polls this tool with { generation_id } and reads state/result at the
625
- // top level.
665
+ // waits on this tool with { generation_id, wait:true } and reads
666
+ // state/result at the top level.
626
667
  if (!generation_ids || generation_ids.length === 0) {
627
668
  const single = results[0];
628
669
  single._hint = pending.length === 0
@@ -712,10 +753,12 @@ function registerGenerateTools(server, client, options = {}) {
712
753
  reference_image: reference_images?.[0]
713
754
  });
714
755
 
715
- const result = await pollUntilDone(client, startResponse.generation_id, {
756
+ const poll = await pollOrTimedOut(client, startResponse.generation_id, {
716
757
  interval: (startResponse.poll_interval_hint || 8) * 1000,
717
758
  timeout: 600000
718
759
  });
760
+ if (poll.timedOut) return poll.timedOut;
761
+ const result = poll.result;
719
762
 
720
763
  return {
721
764
  content: [{
@@ -791,10 +834,12 @@ function registerGenerateTools(server, client, options = {}) {
791
834
  reference_image: first_frame_url || undefined
792
835
  });
793
836
 
794
- const result = await pollUntilDone(client, startResponse.generation_id, {
837
+ const poll = await pollOrTimedOut(client, startResponse.generation_id, {
795
838
  interval: (startResponse.poll_interval_hint || 8) * 1000,
796
839
  timeout: 300000
797
840
  });
841
+ if (poll.timedOut) return poll.timedOut;
842
+ const result = poll.result;
798
843
 
799
844
  return {
800
845
  content: [{
@@ -901,10 +946,12 @@ function registerGenerateTools(server, client, options = {}) {
901
946
  reference_image: sourceIsUrl && !/\.(mp4|mov|webm|mkv|avi|m4v)(\?|$)/i.test(source) ? source : undefined,
902
947
  });
903
948
 
904
- const result = await pollUntilDone(client, startResponse.generation_id, {
949
+ const poll = await pollOrTimedOut(client, startResponse.generation_id, {
905
950
  interval: (startResponse.poll_interval_hint || 8) * 1000,
906
951
  timeout: 600000
907
952
  });
953
+ if (poll.timedOut) return poll.timedOut;
954
+ const result = poll.result;
908
955
 
909
956
  return {
910
957
  content: [{
@@ -1001,10 +1048,12 @@ function registerGenerateTools(server, client, options = {}) {
1001
1048
  reference_image: reference_images?.[0]
1002
1049
  });
1003
1050
 
1004
- const result = await pollUntilDone(client, startResponse.generation_id, {
1051
+ const poll = await pollOrTimedOut(client, startResponse.generation_id, {
1005
1052
  interval: (startResponse.poll_interval_hint || 8) * 1000,
1006
1053
  timeout: 600000
1007
1054
  });
1055
+ if (poll.timedOut) return poll.timedOut;
1056
+ const result = poll.result;
1008
1057
 
1009
1058
  return {
1010
1059
  content: [{
@@ -1069,15 +1118,18 @@ function registerGenerateTools(server, client, options = {}) {
1069
1118
  widget: 'transcript', phase: 'generating',
1070
1119
  generation_id: startResponse.generation_id,
1071
1120
  poll_tool: 'get_generation_status',
1121
+ status_args: { generation_id: startResponse.generation_id, wait: true },
1072
1122
  audio_url: isUrl ? source : undefined,
1073
1123
  open_url: buildOpenUrl('transcribe_audio', startResponse),
1074
1124
  });
1075
1125
  }
1076
1126
 
1077
- const result = await pollUntilDone(client, startResponse.generation_id, {
1127
+ const poll = await pollOrTimedOut(client, startResponse.generation_id, {
1078
1128
  interval: (startResponse.poll_interval_hint || 5) * 1000,
1079
1129
  timeout: 1800000 // 30 minutes — long podcasts are a thing
1080
1130
  });
1131
+ if (poll.timedOut) return poll.timedOut;
1132
+ const result = poll.result;
1081
1133
 
1082
1134
  return {
1083
1135
  content: [{
@@ -1136,10 +1188,12 @@ function registerGenerateTools(server, client, options = {}) {
1136
1188
  reference_image: reference_images?.[0]
1137
1189
  });
1138
1190
 
1139
- const result = await pollUntilDone(client, startResponse.generation_id, {
1191
+ const poll = await pollOrTimedOut(client, startResponse.generation_id, {
1140
1192
  interval: (startResponse.poll_interval_hint || 8) * 1000,
1141
1193
  timeout: 900000 // 15 minutes — 3D generation is slow
1142
1194
  });
1195
+ if (poll.timedOut) return poll.timedOut;
1196
+ const result = poll.result;
1143
1197
 
1144
1198
  return {
1145
1199
  content: [{
@@ -1258,10 +1312,12 @@ function registerGenerateTools(server, client, options = {}) {
1258
1312
  reference_image: image_url
1259
1313
  });
1260
1314
 
1261
- const result = await pollUntilDone(client, gen.generation_id, {
1315
+ const poll = await pollOrTimedOut(client, gen.generation_id, {
1262
1316
  interval: (gen.poll_interval_hint || 5) * 1000,
1263
1317
  timeout: 300000 // 5 min — split_upscale and multi_shot can take longer
1264
1318
  });
1319
+ if (poll.timedOut) return poll.timedOut;
1320
+ const result = poll.result;
1265
1321
 
1266
1322
  return {
1267
1323
  content: [{
@@ -1416,10 +1472,12 @@ function registerGenerateTools(server, client, options = {}) {
1416
1472
  reference_image: image_url
1417
1473
  });
1418
1474
 
1419
- const result = await pollUntilDone(client, gen.generation_id, {
1475
+ const poll = await pollOrTimedOut(client, gen.generation_id, {
1420
1476
  interval: (gen.poll_interval_hint || 8) * 1000,
1421
1477
  timeout: 600000
1422
1478
  });
1479
+ if (poll.timedOut) return poll.timedOut;
1480
+ const result = poll.result;
1423
1481
 
1424
1482
  return {
1425
1483
  content: [{