@kolbo/mcp 1.16.4 → 1.17.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.
package/README.md CHANGED
@@ -4,7 +4,20 @@ 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
- ## Quick Setup
7
+ ## Recommended install: the Kolbo Claude Code plugin
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
8
21
 
9
22
  ### 1. Get an API Key
10
23
 
@@ -32,18 +45,39 @@ Add to `.claude/settings.json`:
32
45
 
33
46
  Just ask Claude naturally:
34
47
 
48
+ **Generation**
35
49
  - *"Generate an image of a sunset over mountains"*
36
50
  - *"Create a 5-second video of waves crashing"*
37
51
  - *"Build a 4-scene storyboard for a coffee shop ad"*
38
52
  - *"Remove the background from this image"*
39
53
  - *"Make a lo-fi hip hop beat"*
40
54
  - *"Read this out loud with a British female voice"*
55
+
56
+ **Marketing & UGC**
57
+ - *"Make me a UGC ad for my sneaker brand — 9:16, talking-head style"*
58
+ - *"TV spot for my new beverage, 15 seconds, cinematic"*
59
+ - *"Unboxing video for this product photo"*
60
+
61
+ **Brand & product imagery**
62
+ - *"Pinterest pin for my candle brand, cottagecore mood"*
63
+ - *"Hero banner for my landing page, wide format"*
64
+ - *"Lifestyle shot of my product in a kitchen"*
65
+ - *"4 ad creative variants for Meta and TikTok"*
66
+
67
+ **Marketplace listings**
68
+ - *"Generate Amazon main image + 5 secondary images for my product"*
69
+ - *"Full A+ content set for my Shopify listing"*
70
+
71
+ **Analysis & utility**
41
72
  - *"Ask Claude about the latest AI news with web search on"*
42
73
  - *"Analyze this video and tell me what prompts are shown on screen"*
43
74
  - *"What's in this image?"*
44
75
  - *"Create a Visual DNA profile called 'Alex' from these images"*
76
+ - *"Use the same brand as last time"* (loads a persisted brand kit from the workspace)
77
+
78
+ The skill that ships with the [Kolbo plugin](https://github.com/Zoharvan12/kolbo-claude-plugin) routes each of these to the right MCP tool with the right defaults — UGC mode picks 9:16 + sound-off + no-captions, marketplace mode enforces compliance (pure white bg, no text, no props), product photoshoot mode uses the right aspect for the platform (2:3 Pinterest, 16:9 hero banner, 1:1 IG feed), etc. See the canonical skill at [`kolbo-code/packages/opencode/skills/kolbo/`](https://github.com/Zoharvan12/kolbo-code/tree/dev/packages/opencode/skills/kolbo).
45
79
 
46
- ## Available Tools (51)
80
+ ## Available Tools (52)
47
81
 
48
82
  **Generation**
49
83
  | Tool | Description |
@@ -67,6 +101,8 @@ Every image/video/creative-director tool accepts `visual_dna_ids` and `moodboard
67
101
 
68
102
  Every generation tool also accepts an optional `resolution` arg. Images use `"1K"` (~1024px) / `"2K"` (Full HD) / `"3K"` (QHD) / `"4K"` (UHD); videos use vertical-pixel tiers like `"720p"` / `"1080p"` / `"1440p"` / `"2160p"`. Values are model-dependent — call `list_models` and read the chosen model's `supported_resolutions` and `resolutionMultipliers`. Omit to use the model default.
69
103
 
104
+ Every generation tool also accepts an optional `project_id` arg that routes the generation into a specific project (owned or shared with edit+). Call `list_projects` to discover IDs. When omitted, generations land in the user's auto-created "API Generations" project.
105
+
70
106
  **Chat & Vision**
71
107
  | Tool | Description |
72
108
  |------|-------------|
@@ -115,12 +151,18 @@ Every generation tool also accepts an optional `resolution` arg. Images use `"1K
115
151
  | `move_folder_contents` | Move every item in a folder to a project |
116
152
  | `get_media_stats` | Counts + storage bytes per type (optionally per project) |
117
153
 
154
+ **Artifacts**
155
+ | Tool | Description |
156
+ |------|-------------|
157
+ | `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). |
158
+
118
159
  **Discovery & Account**
119
160
  | Tool | Description |
120
161
  |------|-------------|
121
162
  | `list_models` | Current model catalog with costs and capabilities |
122
163
  | `list_voices` | TTS voices (presets + cloned) |
123
164
  | `list_presets` | Generation presets across image/video/music/text-to-video catalogs |
165
+ | `list_projects` | List owned + shared projects (id, name, role, is_default) — call first to resolve a project name into the `project_id` you pass to generation tools |
124
166
  | `check_credits` | Check credit balance |
125
167
  | `get_generation_status` | Poll a generation by ID (fallback if a tool times out) |
126
168
 
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,6 +1,6 @@
1
1
  {
2
2
  "name": "@kolbo/mcp",
3
- "version": "1.16.4",
3
+ "version": "1.17.0",
4
4
  "description": "Kolbo AI MCP Server - Generate images, videos, music, speech, and sound effects from Claude Code",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -27,8 +27,7 @@
27
27
  "license": "MIT",
28
28
  "repository": {
29
29
  "type": "git",
30
- "url": "https://github.com/Zoharvan12/kolbo-code",
31
- "directory": "packages/kolbo-mcp"
30
+ "url": "https://github.com/Zoharvan12/kolbo-mcp"
32
31
  },
33
32
  "homepage": "https://docs.kolbo.ai/developer-api/claude-code-skill",
34
33
  "author": "Kolbo AI <support@kolbo.ai>",
package/src/client.js CHANGED
@@ -139,63 +139,6 @@ 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
-
199
142
  // ---------------------------------------------------------------------------
200
143
  // HTTP client
201
144
  // ---------------------------------------------------------------------------
@@ -246,31 +189,13 @@ class KolboClient {
246
189
  }
247
190
 
248
191
  async request(method, reqPath, body = null) {
249
- await acquireSlot();
250
- try {
251
- const result = await this._doRequestWithRetry(method, reqPath, body);
192
+ const result = await this._doRequest(method, reqPath, body);
252
193
 
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;
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);
273
197
  }
198
+ return result;
274
199
  }
275
200
 
276
201
  async _doRequest(method, reqPath, body = null) {
@@ -307,13 +232,6 @@ class KolboClient {
307
232
  });
308
233
  }
309
234
 
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
-
317
235
  if (!response.ok || data.success === false) {
318
236
  const message = data.error || data.message || `API error: ${response.status}`;
319
237
  const code = data.code || null;
@@ -360,29 +278,11 @@ class KolboClient {
360
278
  }
361
279
 
362
280
  async postMultipart(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;
281
+ const result = await this._doMultipart(reqPath, formData);
282
+ if (result._status === 401 && this._tryRefreshKey()) {
283
+ return this._doMultipart(reqPath, formData);
385
284
  }
285
+ return result;
386
286
  }
387
287
 
388
288
  async _doMultipart(reqPath, formData) {
@@ -419,13 +319,6 @@ class KolboClient {
419
319
  });
420
320
  }
421
321
 
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
-
429
322
  if (!response.ok || data.success === false) {
430
323
  const message = data.error || data.message || `API error: ${response.status}`;
431
324
  const code = data.code || null;
package/src/index.js CHANGED
@@ -68,6 +68,7 @@ const { registerMediaTools } = require('./tools/media');
68
68
  const { registerPresetTools } = require('./tools/presets');
69
69
  const { registerAppBuilderTools } = require('./tools/app_builder');
70
70
  const { registerArtifactTools } = require('./tools/artifacts');
71
+ const { registerProjectTools } = require('./tools/projects');
71
72
 
72
73
  async function main() {
73
74
  const client = new KolboClient();
@@ -87,6 +88,7 @@ async function main() {
87
88
  registerPresetTools(server, client);
88
89
  registerAppBuilderTools(server, client);
89
90
  registerArtifactTools(server, client);
91
+ registerProjectTools(server, client);
90
92
 
91
93
  // Start the server with stdio transport
92
94
  const transport = new StdioServerTransport();
package/src/polling.js CHANGED
@@ -6,10 +6,9 @@ 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 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.`
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.`
13
12
  );
14
13
  this.name = 'PollingTimeoutError';
15
14
  this.generationId = generationId;
@@ -230,6 +230,16 @@ function creditFields(polledResult) {
230
230
  return out;
231
231
  }
232
232
 
233
+ // Shared zod schema for the optional `project_id` arg every generation tool
234
+ // accepts. Keep this in one place so the description never drifts across the
235
+ // 17 tools that use it. When omitted, the generation lands in the user's
236
+ // auto-created "API Generations" project. Call `list_projects` first to
237
+ // resolve a name → ObjectId.
238
+ const { z } = require('zod');
239
+ const projectIdField = z.string().optional().describe(
240
+ 'Project ObjectId to drop this generation into. Call `list_projects` to discover IDs (the API has no concept of project names — only ObjectIds). Omit to use the user\'s default "API Generations" project. Requires owner / edit / full permission on the project; view-only is rejected.'
241
+ );
242
+
233
243
  module.exports = {
234
244
  MAX_FILE_BYTES,
235
245
  VISUAL_DNA_MAX_BYTES,
@@ -240,4 +250,5 @@ module.exports = {
240
250
  guessContentType,
241
251
  resolveToBuffer,
242
252
  creditFields,
253
+ projectIdField,
243
254
  };
@@ -9,23 +9,29 @@ 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). To update a previously-published page in place (keeping the same URL), pass the `share_token` returned from the prior publish — the old content is preserved in version history.',
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.'),
18
19
  },
19
- async ({ title, content, type, allow_js }) => {
20
+ async ({ title, content, type, allow_js, share_token }) => {
20
21
  if (!title || !title.trim()) throw new Error('title is required');
21
22
  if (typeof content !== 'string' || !content.length) throw new Error('content is required');
22
23
 
23
- const result = await client.post('/artifact/quick-share', {
24
+ const body = {
24
25
  title: title.trim(),
25
26
  content,
26
27
  type: type || 'html',
27
28
  allowJs: allow_js === true,
28
- });
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);
29
35
 
30
36
  const artifact = result?.data || {};
31
37
  const slug = artifact.shareableSlug || artifact.shareToken;
@@ -49,6 +55,7 @@ function registerArtifactTools(server, client) {
49
55
  shareToken: artifact.shareToken,
50
56
  shareableSlug: slug,
51
57
  duplicate: result?.duplicate === true,
58
+ updated: result?.updated === true,
52
59
  title: artifact.title,
53
60
  }),
54
61
  }],