@kolbo/mcp 1.13.0 → 1.14.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/bin/kolbo-mcp.js CHANGED
@@ -1,2 +1,2 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
  require('../src/index.js');
package/package.json CHANGED
@@ -1,55 +1,56 @@
1
- {
2
- "name": "@kolbo/mcp",
3
- "version": "1.13.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-mcp"
31
- },
32
- "homepage": "https://docs.kolbo.ai/developer-api/claude-code-skill",
33
- "author": "Kolbo AI <support@kolbo.ai>",
34
- "publishConfig": {
35
- "access": "public"
36
- },
37
- "files": [
38
- "src/",
39
- "bin/",
40
- "README.md"
41
- ],
42
- "dependencies": {
43
- "@modelcontextprotocol/sdk": "1.29.0",
44
- "form-data": "^4.0.5",
45
- "zod": "^3.25.0"
46
- },
47
- "overrides": {
48
- "hono": "^4.12.12",
49
- "@hono/node-server": "^1.19.13",
50
- "path-to-regexp": "^8.4.2"
51
- },
52
- "engines": {
53
- "node": ">=18.0.0"
54
- }
55
- }
1
+ {
2
+ "name": "@kolbo/mcp",
3
+ "version": "1.14.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/client.js CHANGED
@@ -139,6 +139,63 @@ function readCliAuthKey() {
139
139
  return null;
140
140
  }
141
141
 
142
+ // ---------------------------------------------------------------------------
143
+ // Burst control
144
+ // ---------------------------------------------------------------------------
145
+ // Caps in-flight requests so multi-step tool calls (file upload + generate +
146
+ // poll status) don't fan out past the kolbo-api global rate limit. The SDK
147
+ // tier on the server is 5000 req/min per API key; 6 in-flight × typical 2s
148
+ // latency keeps us at ~180/min — plenty of headroom.
149
+ const MAX_CONCURRENT = 6;
150
+ let inFlight = 0;
151
+ const waiting = [];
152
+
153
+ function acquireSlot() {
154
+ if (inFlight < MAX_CONCURRENT) {
155
+ inFlight++;
156
+ return Promise.resolve();
157
+ }
158
+ return new Promise((resolve) => waiting.push(resolve));
159
+ }
160
+
161
+ function releaseSlot() {
162
+ if (waiting.length > 0) {
163
+ waiting.shift()();
164
+ } else {
165
+ inFlight = Math.max(0, inFlight - 1);
166
+ }
167
+ }
168
+
169
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
170
+
171
+ /**
172
+ * Parse retry hint from a 429 response. Honors (in order):
173
+ * - body.retryAfter (kolbo-api shape)
174
+ * - Retry-After header (RFC, seconds)
175
+ * - RateLimit-Reset header (seconds until reset)
176
+ * Falls back to 5s. Caps at 60s so we don't hang the tool call forever.
177
+ */
178
+ function parseRetryAfter(response, body) {
179
+ let seconds = null;
180
+ if (body && typeof body.retryAfter === 'number') seconds = body.retryAfter;
181
+ if (seconds == null) {
182
+ const ra = response.headers.get('retry-after');
183
+ if (ra) {
184
+ const n = Number(ra);
185
+ if (Number.isFinite(n)) seconds = n;
186
+ }
187
+ }
188
+ if (seconds == null) {
189
+ const reset = response.headers.get('ratelimit-reset');
190
+ if (reset) {
191
+ const n = Number(reset);
192
+ if (Number.isFinite(n)) seconds = n;
193
+ }
194
+ }
195
+ if (seconds == null || !Number.isFinite(seconds) || seconds <= 0) seconds = 5;
196
+ return Math.min(seconds, 60) * 1000;
197
+ }
198
+
142
199
  // ---------------------------------------------------------------------------
143
200
  // HTTP client
144
201
  // ---------------------------------------------------------------------------
@@ -189,13 +246,31 @@ class KolboClient {
189
246
  }
190
247
 
191
248
  async request(method, reqPath, body = null) {
192
- const result = await this._doRequest(method, reqPath, body);
249
+ await acquireSlot();
250
+ try {
251
+ const result = await this._doRequestWithRetry(method, reqPath, body);
193
252
 
194
- // On 401, try re-reading auth store and retry once
195
- if (result._status === 401 && this._tryRefreshKey()) {
196
- return this._doRequest(method, reqPath, body);
253
+ // On 401, try re-reading auth store and retry once
254
+ if (result._status === 401 && this._tryRefreshKey()) {
255
+ return this._doRequestWithRetry(method, reqPath, body);
256
+ }
257
+ return result;
258
+ } finally {
259
+ releaseSlot();
260
+ }
261
+ }
262
+
263
+ async _doRequestWithRetry(method, reqPath, body = null) {
264
+ try {
265
+ return await this._doRequest(method, reqPath, body);
266
+ } catch (err) {
267
+ if (err instanceof KolboApiError && err.status === 429) {
268
+ const delay = err.data?._kolboRetryAfterMs ?? 5000;
269
+ await sleep(delay);
270
+ return this._doRequest(method, reqPath, body);
271
+ }
272
+ throw err;
197
273
  }
198
- return result;
199
274
  }
200
275
 
201
276
  async _doRequest(method, reqPath, body = null) {
@@ -232,6 +307,13 @@ class KolboClient {
232
307
  });
233
308
  }
234
309
 
310
+ if (response.status === 429) {
311
+ throw new KolboApiError('Rate limited by Kolbo API. Retrying after backoff.', {
312
+ status: 429,
313
+ data: { ...(data || {}), _kolboRetryAfterMs: parseRetryAfter(response, data) },
314
+ });
315
+ }
316
+
235
317
  if (!response.ok || data.success === false) {
236
318
  const message = data.error || data.message || `API error: ${response.status}`;
237
319
  const code = data.code || null;
@@ -278,11 +360,29 @@ class KolboClient {
278
360
  }
279
361
 
280
362
  async postMultipart(reqPath, formData) {
281
- const result = await this._doMultipart(reqPath, formData);
282
- if (result._status === 401 && this._tryRefreshKey()) {
283
- return this._doMultipart(reqPath, formData);
363
+ await acquireSlot();
364
+ try {
365
+ const result = await this._doMultipartWithRetry(reqPath, formData);
366
+ if (result._status === 401 && this._tryRefreshKey()) {
367
+ return this._doMultipartWithRetry(reqPath, formData);
368
+ }
369
+ return result;
370
+ } finally {
371
+ releaseSlot();
372
+ }
373
+ }
374
+
375
+ async _doMultipartWithRetry(reqPath, formData) {
376
+ try {
377
+ return await this._doMultipart(reqPath, formData);
378
+ } catch (err) {
379
+ if (err instanceof KolboApiError && err.status === 429) {
380
+ const delay = err.data?._kolboRetryAfterMs ?? 5000;
381
+ await sleep(delay);
382
+ return this._doMultipart(reqPath, formData);
383
+ }
384
+ throw err;
284
385
  }
285
- return result;
286
386
  }
287
387
 
288
388
  async _doMultipart(reqPath, formData) {
@@ -319,6 +419,13 @@ class KolboClient {
319
419
  });
320
420
  }
321
421
 
422
+ if (response.status === 429) {
423
+ throw new KolboApiError('Rate limited by Kolbo API. Retrying after backoff.', {
424
+ status: 429,
425
+ data: { ...(data || {}), _kolboRetryAfterMs: parseRetryAfter(response, data) },
426
+ });
427
+ }
428
+
322
429
  if (!response.ok || data.success === false) {
323
430
  const message = data.error || data.message || `API error: ${response.status}`;
324
431
  const code = data.code || null;
@@ -5,7 +5,7 @@
5
5
 
6
6
  const { z } = require('zod');
7
7
  const FormData = require('form-data');
8
- const { pollUntilDone } = require('../polling');
8
+ const { pollUntilDone, PollingTimeoutError } = require('../polling');
9
9
  const { resolveToBuffer, creditFields } = require('./_shared');
10
10
 
11
11
  function registerGenerateTools(server, client) {
@@ -383,13 +383,38 @@ function registerGenerateTools(server, client) {
383
383
  // ─── get_generation_status ─────────────────────────────────
384
384
  server.tool(
385
385
  'get_generation_status',
386
- 'Check the status of a generation. Use this as a FALLBACK when a generation tool returned a timeout error — the generation is probably still running on the server. Pass the generation_id from the timeout error (or from any prior generation response).',
386
+ '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 it returns with `still_pending: true`, the generation is genuinely slow STOP calling this tool, tell the user the generation is still running, and resume on the next user turn. Never call this tool more than ONCE per generation_id consecutively.',
387
387
  {
388
- generation_id: z.string().describe('The generation ID to check')
388
+ generation_id: z.string().describe('The generation ID to check'),
389
+ wait_seconds: z.number().int().min(10).max(1700).optional().describe('How long to block-poll internally before giving up (10–1700 seconds, default 600). Use higher values for video / 3D / large batches that can legitimately take 15+ minutes.'),
389
390
  },
390
- async ({ generation_id }) => {
391
- const result = await client.get(`/v1/generate/${encodeURIComponent(generation_id)}/status`);
392
-
391
+ async ({ generation_id, wait_seconds }) => {
392
+ const timeoutMs = (wait_seconds ?? 600) * 1000;
393
+ let result;
394
+ try {
395
+ result = await pollUntilDone(client, generation_id, {
396
+ interval: 5000,
397
+ timeout: timeoutMs,
398
+ });
399
+ } catch (err) {
400
+ if (err instanceof PollingTimeoutError) {
401
+ const current = await client.get(`/v1/generate/${encodeURIComponent(generation_id)}/status`).catch(() => null);
402
+ const minutes = Math.round(timeoutMs / 60000);
403
+ return {
404
+ content: [{
405
+ type: 'text',
406
+ text: JSON.stringify({
407
+ generation_id,
408
+ state: current?.state ?? 'unknown',
409
+ still_pending: true,
410
+ waited_seconds: Math.round(timeoutMs / 1000),
411
+ _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.`,
412
+ }, null, 2),
413
+ }],
414
+ };
415
+ }
416
+ throw err;
417
+ }
393
418
  return {
394
419
  content: [{
395
420
  type: 'text',