@kolbo/mcp 1.18.0 → 1.19.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kolbo/mcp",
3
- "version": "1.18.0",
3
+ "version": "1.19.1",
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/index.js CHANGED
@@ -90,8 +90,10 @@ function createServer(opts = {}) {
90
90
  version: '1.0.0'
91
91
  });
92
92
 
93
- // Register all tools
94
- 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 });
95
97
  registerModelTools(server, client);
96
98
  registerChatTools(server, client);
97
99
  registerVisualDnaTools(server, client);
@@ -240,6 +240,47 @@ 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
+ // Fetch the (≤4) images in parallel — they're independent, the cap already
260
+ // bounds concurrency, and this sits on the connector response path right
261
+ // after generation. Order is preserved by map-then-filter; any failure falls
262
+ // back to URL-only for that image.
263
+ const blocks = await Promise.all(
264
+ urls.slice(0, INLINE_IMG_MAX_COUNT).map(async (url) => {
265
+ try {
266
+ if (typeof url !== 'string' || !isHttpUrl(url)) return null;
267
+ const res = await safeFetch(url);
268
+ if (!res.ok) return null;
269
+ const contentType = (res.headers.get('content-type') || '').split(';')[0].trim().toLowerCase();
270
+ if (!contentType.startsWith('image/')) return null; // never embed non-images
271
+ const declaredLen = Number(res.headers.get('content-length') || 0);
272
+ if (declaredLen && declaredLen > INLINE_IMG_MAX_BYTES) return null;
273
+ const ab = await res.arrayBuffer();
274
+ if (ab.byteLength > INLINE_IMG_MAX_BYTES) return null;
275
+ return { type: 'image', data: Buffer.from(ab).toString('base64'), mimeType: contentType };
276
+ } catch (_) {
277
+ return null;
278
+ }
279
+ })
280
+ );
281
+ return blocks.filter(Boolean);
282
+ }
283
+
243
284
  module.exports = {
244
285
  MAX_FILE_BYTES,
245
286
  VISUAL_DNA_MAX_BYTES,
@@ -251,4 +292,5 @@ module.exports = {
251
292
  resolveToBuffer,
252
293
  creditFields,
253
294
  projectIdField,
295
+ inlineImageBlocks,
254
296
  };
@@ -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
  );