@kolbo/mcp 1.18.0 → 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.18.0",
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/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,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
  );