@kolbo/mcp 1.15.0 → 1.15.1

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,56 +1,56 @@
1
- {
2
- "name": "@kolbo/mcp",
3
- "version": "1.15.0",
4
- "description": "Kolbo AI MCP Server - Generate images, videos, music, speech, and sound effects from Claude Code",
5
- "main": "src/index.js",
6
- "bin": {
7
- "kolbo-mcp": "./bin/kolbo-mcp.js"
8
- },
9
- "scripts": {
10
- "start": "node src/index.js",
11
- "smoke": "node scripts/smoke.js",
12
- "check-parity": "node scripts/check-parity.js",
13
- "prepublishOnly": "node scripts/smoke.js && node scripts/check-parity.js"
14
- },
15
- "keywords": [
16
- "kolbo",
17
- "mcp",
18
- "ai",
19
- "image-generation",
20
- "video-generation",
21
- "music-generation",
22
- "text-to-speech",
23
- "claude-code",
24
- "claude-desktop",
25
- "model-context-protocol"
26
- ],
27
- "license": "MIT",
28
- "repository": {
29
- "type": "git",
30
- "url": "https://github.com/Zoharvan12/kolbo-code",
31
- "directory": "packages/kolbo-mcp"
32
- },
33
- "homepage": "https://docs.kolbo.ai/developer-api/claude-code-skill",
34
- "author": "Kolbo AI <support@kolbo.ai>",
35
- "publishConfig": {
36
- "access": "public"
37
- },
38
- "files": [
39
- "src/",
40
- "bin/",
41
- "README.md"
42
- ],
43
- "dependencies": {
44
- "@modelcontextprotocol/sdk": "1.29.0",
45
- "form-data": "^4.0.5",
46
- "zod": "^3.25.0"
47
- },
48
- "overrides": {
49
- "hono": "^4.12.12",
50
- "@hono/node-server": "^1.19.13",
51
- "path-to-regexp": "^8.4.2"
52
- },
53
- "engines": {
54
- "node": ">=18.0.0"
55
- }
56
- }
1
+ {
2
+ "name": "@kolbo/mcp",
3
+ "version": "1.15.1",
4
+ "description": "Kolbo AI MCP Server - Generate images, videos, music, speech, and sound effects from Claude Code",
5
+ "main": "src/index.js",
6
+ "bin": {
7
+ "kolbo-mcp": "./bin/kolbo-mcp.js"
8
+ },
9
+ "scripts": {
10
+ "start": "node src/index.js",
11
+ "smoke": "node scripts/smoke.js",
12
+ "check-parity": "node scripts/check-parity.js",
13
+ "prepublishOnly": "node scripts/smoke.js && node scripts/check-parity.js"
14
+ },
15
+ "keywords": [
16
+ "kolbo",
17
+ "mcp",
18
+ "ai",
19
+ "image-generation",
20
+ "video-generation",
21
+ "music-generation",
22
+ "text-to-speech",
23
+ "claude-code",
24
+ "claude-desktop",
25
+ "model-context-protocol"
26
+ ],
27
+ "license": "MIT",
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "https://github.com/Zoharvan12/kolbo-code",
31
+ "directory": "packages/kolbo-mcp"
32
+ },
33
+ "homepage": "https://docs.kolbo.ai/developer-api/claude-code-skill",
34
+ "author": "Kolbo AI <support@kolbo.ai>",
35
+ "publishConfig": {
36
+ "access": "public"
37
+ },
38
+ "files": [
39
+ "src/",
40
+ "bin/",
41
+ "README.md"
42
+ ],
43
+ "dependencies": {
44
+ "@modelcontextprotocol/sdk": "1.29.0",
45
+ "form-data": "^4.0.5",
46
+ "zod": "^3.25.0"
47
+ },
48
+ "overrides": {
49
+ "hono": "^4.12.12",
50
+ "@hono/node-server": "^1.19.13",
51
+ "path-to-regexp": "^8.4.2"
52
+ },
53
+ "engines": {
54
+ "node": ">=18.0.0"
55
+ }
56
+ }
package/src/polling.js CHANGED
@@ -6,9 +6,10 @@ class PollingTimeoutError extends Error {
6
6
  constructor(generationId, timeoutMs) {
7
7
  const seconds = Math.round(timeoutMs / 1000);
8
8
  super(
9
- `Generation timed out after ${seconds}s of polling. The generation may STILL be running on the server — ` +
10
- `call get_generation_status with generation_id="${generationId}" to check its current state. ` +
11
- `Videos, deep-think chat, and large batches can take longer than the default polling window.`
9
+ `Generation timed out after ${seconds}s of polling. The generation is STILL RUNNING on the server — ` +
10
+ `DO NOT re-issue the original generation tool. A retry would burn credits while the original keeps producing the result you already paid for. ` +
11
+ `Instead, IMMEDIATELY call get_generation_status with generation_id="${generationId}" (it block-polls server-side, no need to loop). ` +
12
+ `Videos, deep-think chat, and large batches can legitimately take 5–15+ minutes beyond the default polling window.`
12
13
  );
13
14
  this.name = 'PollingTimeoutError';
14
15
  this.generationId = generationId;
@@ -32,9 +32,14 @@ function registerGenerateTools(server, client) {
32
32
  reference_images, visual_dna_ids, moodboard_id, enable_web_search, resolution, preset_id
33
33
  });
34
34
 
35
+ // 60s here (not 120s) so a stuck generation surfaces faster — the
36
+ // PollingTimeoutError tells the model to switch to get_generation_status,
37
+ // which block-polls for 10min by default. Same total wait, but the
38
+ // first hand-off happens at 60s instead of 120s so the user isn't
39
+ // staring at a frozen spinner.
35
40
  const result = await pollUntilDone(client, gen.generation_id, {
36
41
  interval: (gen.poll_interval_hint || 3) * 1000,
37
- timeout: 120000
42
+ timeout: 60000
38
43
  });
39
44
 
40
45
  return {
@@ -386,10 +391,20 @@ function registerGenerateTools(server, client) {
386
391
  }
387
392
  );
388
393
 
394
+ // Track how many consecutive get_generation_status polls land on the same
395
+ // id within a short window. Models routinely ignore the "stop polling"
396
+ // hint and re-call, burning minutes of wall-clock and confusing the user.
397
+ // After STATUS_POLL_CAP attempts in a row, we hard-abandon: return a
398
+ // distinct shape (no `still_pending`) so the model can't pattern-match
399
+ // its way back into the loop.
400
+ const STATUS_POLL_CAP = 2;
401
+ const STATUS_POLL_RESET_MS = 10 * 60 * 1000; // a quiet 10min resets the counter
402
+ const statusPollHistory = new Map(); // id → { count, lastAt }
403
+
389
404
  // ─── get_generation_status ─────────────────────────────────
390
405
  server.tool(
391
406
  'get_generation_status',
392
- 'Resume polling a generation after a timeout. Pass the generation_id from a prior generation tool that timed out. This call BLOCKS server-side, polling internally — you do NOT need to call it again in a loop. Defaults to a 10-minute internal poll which covers most image edits and short videos; pass `wait_seconds` up to 1700 (~28 min) for long video / 3D / batch generations. **If you fired multiple generations in parallel and they all timed out, you MUST call get_generation_status for EACH generation_id in this same turn — do not give up on any of them, or their URLs will be lost forever.** If a single call returns `still_pending: true`, that specific generation is genuinely slow — STOP calling THAT one, tell the user it is still running, and resume on the next user turn. Never call this tool more than ONCE per generation_id consecutively in the same turn.',
407
+ 'Resume polling a generation after a timeout. Pass the generation_id from a prior generation tool that timed out. This call BLOCKS server-side, polling internally — you do NOT need to call it again in a loop. Defaults to a 10-minute internal poll which covers most image edits and short videos; pass `wait_seconds` up to 1700 (~28 min) for long video / 3D / batch generations. **If you fired multiple generations in parallel and they all timed out, you MUST call get_generation_status for EACH generation_id in this same turn — do not give up on any of them, or their URLs will be lost forever.** A response with `abandoned: true` is TERMINAL — the generation will not be polled again in this turn; report it to the user and move on. A response with `still_pending: true` means the job is genuinely slow — STOP polling THAT id immediately, tell the user, and wait for them to ask again. Never call this tool more than ONCE per generation_id consecutively in the same turn.',
393
408
  {
394
409
  generation_id: z.string().describe('The generation ID to check'),
395
410
  wait_seconds: z.number().int().min(60).max(1700).optional().describe('How long to block-poll internally before giving up (60–1700 seconds, default 600). Values below 300 are clamped up to 300 server-side because shorter waits cause the model to abandon still-running generations. Use higher values for video / 3D / large batches that can legitimately take 15+ minutes.'),
@@ -400,6 +415,56 @@ function registerGenerateTools(server, client) {
400
415
  // wasted). 300s is the floor that survived real-world cases.
401
416
  const requested = wait_seconds ?? 600;
402
417
  const timeoutMs = Math.max(requested, 300) * 1000;
418
+
419
+ // Update the consecutive-call counter for this id. We reset if the
420
+ // last attempt was a long time ago (new user turn), so this only
421
+ // catches within-turn looping. Opportunistic sweep keeps the map
422
+ // bounded on long-lived MCP servers (no setInterval — that would
423
+ // keep the Node event loop alive forever).
424
+ const now = Date.now();
425
+ if (statusPollHistory.size > 64) {
426
+ const cutoff = now - STATUS_POLL_RESET_MS;
427
+ for (const [k, v] of statusPollHistory) {
428
+ if (v.lastAt < cutoff) statusPollHistory.delete(k);
429
+ }
430
+ }
431
+ const prior = statusPollHistory.get(generation_id);
432
+ const count = prior && now - prior.lastAt < STATUS_POLL_RESET_MS ? prior.count + 1 : 1;
433
+ statusPollHistory.set(generation_id, { count, lastAt: now });
434
+
435
+ // Hard abandon — the model has hit the cap. Return a non-pending
436
+ // shape so it has nothing left to keep polling against.
437
+ if (count > STATUS_POLL_CAP) {
438
+ const current = await client
439
+ .get(`/v1/generate/${encodeURIComponent(generation_id)}/status`)
440
+ .catch(() => null);
441
+ // If the generation actually finished in the meantime, surface it.
442
+ if (current?.state === 'completed') {
443
+ statusPollHistory.delete(generation_id);
444
+ return {
445
+ content: [{ type: 'text', text: JSON.stringify(current, null, 2) }],
446
+ };
447
+ }
448
+ return {
449
+ content: [
450
+ {
451
+ type: 'text',
452
+ text: JSON.stringify(
453
+ {
454
+ generation_id,
455
+ state: current?.state ?? 'unknown',
456
+ abandoned: true,
457
+ poll_attempts: count,
458
+ _note: `This generation has been polled ${count} consecutive times in this turn and is still not done. ABANDONED — do NOT call get_generation_status for this id again. Tell the user the generation is taking unusually long and ask them to check back later.`,
459
+ },
460
+ null,
461
+ 2,
462
+ ),
463
+ },
464
+ ],
465
+ };
466
+ }
467
+
403
468
  let result;
404
469
  try {
405
470
  result = await pollUntilDone(client, generation_id, {
@@ -418,13 +483,17 @@ function registerGenerateTools(server, client) {
418
483
  state: current?.state ?? 'unknown',
419
484
  still_pending: true,
420
485
  waited_seconds: Math.round(timeoutMs / 1000),
421
- _note: `Polled for ${minutes} minute(s) — generation is taking longer than usual. STOP calling get_generation_status now. Tell the user the generation is still running and ask them to prompt you to check again later. Do NOT loop.`,
486
+ poll_attempts: count,
487
+ _note: `Polled for ${minutes} minute(s) — generation is taking longer than usual. STOP calling get_generation_status now. Tell the user the generation is still running and ask them to prompt you to check again later. Do NOT loop — one more consecutive call for this id will be hard-abandoned.`,
422
488
  }, null, 2),
423
489
  }],
424
490
  };
425
491
  }
426
492
  throw err;
427
493
  }
494
+ // Completed cleanly — clear the counter so a future generation reusing
495
+ // the id (theoretically possible) starts fresh.
496
+ statusPollHistory.delete(generation_id);
428
497
  return {
429
498
  content: [{
430
499
  type: 'text',