@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 +1 -1
- package/src/index.js +4 -2
- package/src/tools/_shared.js +42 -0
- package/src/tools/generate.js +10 -4
package/package.json
CHANGED
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
|
-
|
|
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);
|
package/src/tools/_shared.js
CHANGED
|
@@ -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
|
};
|
package/src/tools/generate.js
CHANGED
|
@@ -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
|
);
|