@kolbo/mcp 1.14.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/README.md +1 -19
- package/bin/kolbo-mcp.js +1 -1
- package/package.json +56 -55
- package/src/client.js +116 -9
- package/src/tools/artifacts.js +4 -11
- package/src/tools/generate.js +31 -6
package/README.md
CHANGED
|
@@ -4,20 +4,7 @@ Use [Kolbo AI](https://kolbo.ai) as native tools in Claude Code and Claude Deskt
|
|
|
4
4
|
|
|
5
5
|
Generate images, videos, music, speech, sound effects, multi-scene campaigns, and conversational chat — all from natural language in your coding environment. 100+ AI models behind Smart Select routing, with reusable Visual DNA profiles for character/style consistency.
|
|
6
6
|
|
|
7
|
-
##
|
|
8
|
-
|
|
9
|
-
The easiest way to use this MCP from Claude Code is the official **Kolbo plugin**, which bundles this server + the routing skill + a first-run API-key prompt — no `settings.json` editing required:
|
|
10
|
-
|
|
11
|
-
```bash
|
|
12
|
-
claude plugin marketplace add Zoharvan12/kolbo-claude-plugin
|
|
13
|
-
claude plugin install kolbo@kolbo
|
|
14
|
-
```
|
|
15
|
-
|
|
16
|
-
You'll be prompted for your API key once and it's stored in your OS keychain. The plugin lives in its own tiny repo at [`kolbo-claude-plugin`](https://github.com/Zoharvan12/kolbo-claude-plugin); the routing skill it ships is auto-synced from the canonical source in [`kolbo-code`](https://github.com/Zoharvan12/kolbo-code), so the Claude Code plugin and the Kolbo Code CLI always carry the same skill.
|
|
17
|
-
|
|
18
|
-
Continue below if you'd rather wire the MCP in by hand (Claude Desktop, Cursor, custom setups).
|
|
19
|
-
|
|
20
|
-
## Manual setup
|
|
7
|
+
## Quick Setup
|
|
21
8
|
|
|
22
9
|
### 1. Get an API Key
|
|
23
10
|
|
|
@@ -128,11 +115,6 @@ Every generation tool also accepts an optional `resolution` arg. Images use `"1K
|
|
|
128
115
|
| `move_folder_contents` | Move every item in a folder to a project |
|
|
129
116
|
| `get_media_stats` | Counts + storage bytes per type (optionally per project) |
|
|
130
117
|
|
|
131
|
-
**Artifacts**
|
|
132
|
-
| Tool | Description |
|
|
133
|
-
|------|-------------|
|
|
134
|
-
| `publish_html_artifact` | Publish an HTML page, SVG, or Mermaid diagram and get a public shareable URL on `sites.kolbo.ai`. Pass `share_token` from a prior publish to update the same URL in place (old content kept in version history). |
|
|
135
|
-
|
|
136
118
|
**Discovery & Account**
|
|
137
119
|
| Tool | Description |
|
|
138
120
|
|------|-------------|
|
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.14.
|
|
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-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
"
|
|
40
|
-
"
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
"
|
|
45
|
-
"
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
249
|
+
await acquireSlot();
|
|
250
|
+
try {
|
|
251
|
+
const result = await this._doRequestWithRetry(method, reqPath, body);
|
|
193
252
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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;
|
package/src/tools/artifacts.js
CHANGED
|
@@ -9,29 +9,23 @@ function registerArtifactTools(server, client) {
|
|
|
9
9
|
// ─── publish_html_artifact ─────────────────────────────────────
|
|
10
10
|
server.tool(
|
|
11
11
|
'publish_html_artifact',
|
|
12
|
-
'Publish an HTML page (or SVG / Mermaid diagram) to kolbo.ai and return a public shareable URL. Use this when the user explicitly asks to share, publish, or deploy a built artifact so they can send the URL to someone. The content is hosted at https://sites.kolbo.ai/<slug>; the page is served with restrictive CSP (no fetch/XHR/form-action) so it cannot exfiltrate data. Identical content uploaded twice returns the same URL (server dedup).
|
|
12
|
+
'Publish an HTML page (or SVG / Mermaid diagram) to kolbo.ai and return a public shareable URL. Use this when the user explicitly asks to share, publish, or deploy a built artifact so they can send the URL to someone. The content is hosted at https://sites.kolbo.ai/<slug>; the page is served with restrictive CSP (no fetch/XHR/form-action) so it cannot exfiltrate data. Identical content uploaded twice returns the same URL (server dedup).',
|
|
13
13
|
{
|
|
14
14
|
title: z.string().describe('Human-friendly title for the page (also used to generate the SEO slug). Keep under ~60 chars.'),
|
|
15
15
|
content: z.string().describe('The raw artifact body. For type="html" this is a full HTML document (DOCTYPE + html/head/body). For "svg" it is an <svg> document. For "mermaid" it is the Mermaid source text.'),
|
|
16
16
|
type: z.enum(['html', 'svg', 'mermaid']).optional().describe('Artifact type. Default: "html".'),
|
|
17
17
|
allow_js: z.boolean().optional().describe('Allow inline <script> execution on the published page. Default: false. Required for Tailwind JIT, Chart.js, Three.js, React-from-CDN etc.'),
|
|
18
|
-
share_token: z.string().optional().describe('Optional. Pass the `shareToken` returned from a previous publish to update that artifact in place. The public URL stays the same and the old content is moved into version history. Omit this on the first publish.'),
|
|
19
18
|
},
|
|
20
|
-
async ({ title, content, type, allow_js
|
|
19
|
+
async ({ title, content, type, allow_js }) => {
|
|
21
20
|
if (!title || !title.trim()) throw new Error('title is required');
|
|
22
21
|
if (typeof content !== 'string' || !content.length) throw new Error('content is required');
|
|
23
22
|
|
|
24
|
-
const
|
|
23
|
+
const result = await client.post('/artifact/quick-share', {
|
|
25
24
|
title: title.trim(),
|
|
26
25
|
content,
|
|
27
26
|
type: type || 'html',
|
|
28
27
|
allowJs: allow_js === true,
|
|
29
|
-
};
|
|
30
|
-
if (typeof share_token === 'string' && share_token.trim()) {
|
|
31
|
-
body.shareToken = share_token.trim();
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const result = await client.post('/artifact/quick-share', body);
|
|
28
|
+
});
|
|
35
29
|
|
|
36
30
|
const artifact = result?.data || {};
|
|
37
31
|
const slug = artifact.shareableSlug || artifact.shareToken;
|
|
@@ -55,7 +49,6 @@ function registerArtifactTools(server, client) {
|
|
|
55
49
|
shareToken: artifact.shareToken,
|
|
56
50
|
shareableSlug: slug,
|
|
57
51
|
duplicate: result?.duplicate === true,
|
|
58
|
-
updated: result?.updated === true,
|
|
59
52
|
title: artifact.title,
|
|
60
53
|
}),
|
|
61
54
|
}],
|
package/src/tools/generate.js
CHANGED
|
@@ -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
|
-
'
|
|
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
|
|
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',
|