@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 +56 -56
- package/src/polling.js +4 -3
- package/src/tools/generate.js +72 -3
package/package.json
CHANGED
|
@@ -1,56 +1,56 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@kolbo/mcp",
|
|
3
|
-
"version": "1.15.
|
|
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
|
|
10
|
-
`
|
|
11
|
-
`
|
|
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;
|
package/src/tools/generate.js
CHANGED
|
@@ -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:
|
|
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.**
|
|
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
|
-
|
|
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',
|