@kolbo/mcp 1.17.2 → 1.19.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kolbo/mcp",
3
- "version": "1.17.2",
3
+ "version": "1.19.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": {
package/src/client.js CHANGED
@@ -144,11 +144,21 @@ function readCliAuthKey() {
144
144
  // ---------------------------------------------------------------------------
145
145
 
146
146
  class KolboClient {
147
- constructor() {
148
- this.baseUrl = resolveApiBase();
147
+ /**
148
+ * @param {object} [opts]
149
+ * @param {string} [opts.apiKey] Explicit key. Takes precedence over env +
150
+ * auth store. Used by a remote HTTP host that injects the caller's key per
151
+ * request (one KolboClient per request) instead of reading a process-wide
152
+ * env var. When set, the auth-store 401 refresh path is disabled — the host
153
+ * owns the key lifecycle.
154
+ * @param {string} [opts.apiBase] Explicit API base URL override.
155
+ */
156
+ constructor(opts = {}) {
157
+ this.baseUrl = opts.apiBase ? String(opts.apiBase).replace(/\/$/, '') : resolveApiBase();
158
+ this._explicitKey = opts.apiKey || null;
149
159
  this._envKey = process.env.KOLBO_API_KEY || null;
150
160
  this._authStoreKey = null; // lazy-loaded
151
- this.apiKey = this._envKey || this._readAuthStore();
161
+ this.apiKey = this._explicitKey || this._envKey || this._readAuthStore();
152
162
 
153
163
  if (!this.apiKey) {
154
164
  // No key in env OR auth store. The Kolbo Code parent process should
@@ -171,6 +181,9 @@ class KolboClient {
171
181
  * since the MCP server started. Returns true if a new key was found.
172
182
  */
173
183
  _tryRefreshKey() {
184
+ // Host-injected per-request key is authoritative — never override it from
185
+ // the local CLI auth store (which may not even exist in a server context).
186
+ if (this._explicitKey) return false;
174
187
  if (this._envKey) {
175
188
  // Env var is set but invalid — can't override it, but try auth store
176
189
  const fresh = readCliAuthKey();
package/src/index.js CHANGED
@@ -70,16 +70,30 @@ const { registerAppBuilderTools } = require('./tools/app_builder');
70
70
  const { registerArtifactTools } = require('./tools/artifacts');
71
71
  const { registerProjectTools } = require('./tools/projects');
72
72
 
73
- async function main() {
74
- const client = new KolboClient();
73
+ /**
74
+ * Build a fully-configured Kolbo MCP server (all tool groups registered)
75
+ * WITHOUT connecting a transport. This is the reusable core shared by:
76
+ * - the stdio entrypoint below (npx / Kolbo Code), and
77
+ * - a remote HTTP host (kolbo-api) that creates one server per request with
78
+ * the caller's key injected via `opts.apiKey`.
79
+ *
80
+ * @param {object} [opts]
81
+ * @param {string} [opts.apiKey] Per-instance Kolbo API key (overrides env).
82
+ * @param {string} [opts.apiBase] API base URL override.
83
+ * @returns {McpServer} a server ready to `.connect(transport)`.
84
+ */
85
+ function createServer(opts = {}) {
86
+ const client = new KolboClient(opts);
75
87
 
76
88
  const server = new McpServer({
77
89
  name: 'kolbo',
78
90
  version: '1.0.0'
79
91
  });
80
92
 
81
- // Register all tools
82
- registerGenerateTools(server, client);
93
+ // Register all tools. `inlineImages` (off by default) is opt-in: only the
94
+ // remote HTTP host enables it, so stdio clients (Kolbo Code / Desktop / Cursor)
95
+ // keep identical text-URL output.
96
+ registerGenerateTools(server, client, { inlineImages: !!opts.inlineImages });
83
97
  registerModelTools(server, client);
84
98
  registerChatTools(server, client);
85
99
  registerVisualDnaTools(server, client);
@@ -90,12 +104,18 @@ async function main() {
90
104
  registerArtifactTools(server, client);
91
105
  registerProjectTools(server, client);
92
106
 
107
+ return server;
108
+ }
109
+
110
+ async function main() {
111
+ const server = createServer();
112
+
93
113
  // Start the server with stdio transport
94
114
  const transport = new StdioServerTransport();
95
115
  await server.connect(transport);
96
116
  }
97
117
 
98
- module.exports = { main };
118
+ module.exports = { main, createServer };
99
119
 
100
120
  // Auto-run when invoked directly (e.g. `node src/index.js` or via the published
101
121
  // bin/kolbo-mcp.js wrapper). Consumers that `require()` this module to embed it
@@ -240,6 +240,42 @@ const projectIdField = z.string().optional().describe(
240
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
241
  );
242
242
 
243
+ // ─── Optional inline-image content blocks ────────────────────────────────────
244
+ // When a host opts in (the remote HTTP connector sets inlineImages:true), turn
245
+ // generated IMAGE urls into MCP `image` content blocks so clients render them
246
+ // inline instead of a "Show Image" link. Strictly gated + bounded:
247
+ // - only runs when opts.enabled is true (stdio/Kolbo Code never enables it,
248
+ // so their behavior is byte-identical: text URL only);
249
+ // - caps the number of images and the bytes per image;
250
+ // - ONLY embeds responses whose content-type is image/* — a video/audio URL
251
+ // can never be base64-embedded even if mistakenly passed in;
252
+ // - any fetch/decoding failure silently falls back to URL-only.
253
+ const INLINE_IMG_MAX_COUNT = 4;
254
+ const INLINE_IMG_MAX_BYTES = 8 * 1024 * 1024; // 8 MB per image
255
+
256
+ async function inlineImageBlocks(urls, opts = {}) {
257
+ if (!opts || !opts.enabled) return [];
258
+ if (!Array.isArray(urls) || urls.length === 0) return [];
259
+ const out = [];
260
+ for (const url of urls.slice(0, INLINE_IMG_MAX_COUNT)) {
261
+ try {
262
+ if (typeof url !== 'string' || !isHttpUrl(url)) continue;
263
+ const res = await safeFetch(url);
264
+ if (!res.ok) continue;
265
+ const contentType = (res.headers.get('content-type') || '').split(';')[0].trim().toLowerCase();
266
+ if (!contentType.startsWith('image/')) continue; // never embed non-images
267
+ const declaredLen = Number(res.headers.get('content-length') || 0);
268
+ if (declaredLen && declaredLen > INLINE_IMG_MAX_BYTES) continue;
269
+ const ab = await res.arrayBuffer();
270
+ if (ab.byteLength > INLINE_IMG_MAX_BYTES) continue;
271
+ out.push({ type: 'image', data: Buffer.from(ab).toString('base64'), mimeType: contentType });
272
+ } catch (_) {
273
+ // fall back to URL-only for this image
274
+ }
275
+ }
276
+ return out;
277
+ }
278
+
243
279
  module.exports = {
244
280
  MAX_FILE_BYTES,
245
281
  VISUAL_DNA_MAX_BYTES,
@@ -251,4 +287,5 @@ module.exports = {
251
287
  resolveToBuffer,
252
288
  creditFields,
253
289
  projectIdField,
290
+ inlineImageBlocks,
254
291
  };
@@ -6,9 +6,13 @@
6
6
  const { z } = require('zod');
7
7
  const FormData = require('form-data');
8
8
  const { pollUntilDone } = require('../polling');
9
- const { resolveToBuffer, creditFields, projectIdField } = require('./_shared');
9
+ const { resolveToBuffer, creditFields, projectIdField, inlineImageBlocks } = require('./_shared');
10
10
 
11
- function registerGenerateTools(server, client) {
11
+ function registerGenerateTools(server, client, options = {}) {
12
+ // Only enabled by hosts that explicitly opt in (the remote HTTP connector).
13
+ // stdio hosts (Kolbo Code, Claude Desktop, Cursor) leave this false, so their
14
+ // tool output is unchanged: a text block with the image URL.
15
+ const inlineImages = !!options.inlineImages;
12
16
  // ─── generate_image ────────────────────────────────────────
13
17
  server.tool(
14
18
  'generate_image',
@@ -38,6 +42,7 @@ function registerGenerateTools(server, client) {
38
42
  timeout: 120000
39
43
  });
40
44
 
45
+ const images = await inlineImageBlocks(result.result.urls, { enabled: inlineImages });
41
46
  return {
42
47
  content: [{
43
48
  type: 'text',
@@ -48,7 +53,7 @@ function registerGenerateTools(server, client) {
48
53
  prompt_used: result.result.prompt_used,
49
54
  _followup_hint: 'If the user asks to edit/change/modify this image next, pass urls[0] to generate_image_edit (free-form edits) or edit_image (upscale/reframe/removebg/enhance_skin/magic_edit). Do NOT call generate_image again.'
50
55
  }, null, 2)
51
- }]
56
+ }, ...images]
52
57
  };
53
58
  }
54
59
  );
@@ -85,6 +90,7 @@ function registerGenerateTools(server, client) {
85
90
  timeout: heavy ? 240000 : 120000
86
91
  });
87
92
 
93
+ const images = await inlineImageBlocks(result.result.urls, { enabled: inlineImages });
88
94
  return {
89
95
  content: [{
90
96
  type: 'text',
@@ -95,7 +101,7 @@ function registerGenerateTools(server, client) {
95
101
  prompt_used: result.result.prompt_used,
96
102
  _followup_hint: 'If the user asks for another edit on this output, pass urls[0] back into generate_image_edit as source_images. For targeted ops (upscale/reframe/removebg/enhance_skin) use edit_image instead. Do NOT call generate_image from scratch.'
97
103
  }, null, 2)
98
- }]
104
+ }, ...images]
99
105
  };
100
106
  }
101
107
  );