@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 +1 -1
- package/src/client.js +16 -3
- package/src/index.js +25 -5
- package/src/tools/_shared.js +37 -0
- package/src/tools/generate.js +10 -4
package/package.json
CHANGED
package/src/client.js
CHANGED
|
@@ -144,11 +144,21 @@ function readCliAuthKey() {
|
|
|
144
144
|
// ---------------------------------------------------------------------------
|
|
145
145
|
|
|
146
146
|
class KolboClient {
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
74
|
-
|
|
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
|
-
|
|
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
|
package/src/tools/_shared.js
CHANGED
|
@@ -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
|
};
|
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
|
);
|