@kolbo/mcp 1.2.0 → 1.3.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 +4 -3
- package/src/client.js +312 -130
- package/src/tools/chat.js +13 -12
- package/src/tools/generate.js +105 -103
- package/src/tools/media.js +7 -6
- package/src/tools/models.js +52 -53
- package/src/tools/moodboards.js +3 -1
- package/src/tools/presets.js +3 -1
- package/src/tools/visual_dna.js +9 -8
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kolbo/mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.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": {
|
|
@@ -40,8 +40,9 @@
|
|
|
40
40
|
"README.md"
|
|
41
41
|
],
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@modelcontextprotocol/sdk": "
|
|
44
|
-
"form-data": "^4.0.5"
|
|
43
|
+
"@modelcontextprotocol/sdk": "1.29.0",
|
|
44
|
+
"form-data": "^4.0.5",
|
|
45
|
+
"zod": "^3.25.0"
|
|
45
46
|
},
|
|
46
47
|
"overrides": {
|
|
47
48
|
"hono": "^4.12.12",
|
package/src/client.js
CHANGED
|
@@ -1,130 +1,312 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
this.
|
|
24
|
-
this.
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
});
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Kolbo API HTTP client wrapper
|
|
7
|
+
*
|
|
8
|
+
* Auth resolution (first match wins):
|
|
9
|
+
* 1. KOLBO_API_KEY env var — explicit key, always honored
|
|
10
|
+
* 2. CLI auth store (auth.json) — auto-shared with `kolbo auth login`
|
|
11
|
+
*
|
|
12
|
+
* API base resolution (mirrors CLI partner.ts):
|
|
13
|
+
* 1. KOLBO_API_URL env var — explicit override
|
|
14
|
+
* 2. KOLBO_API_BASE env var — same as CLI
|
|
15
|
+
* 3. partner.json on disk — whitelabel config
|
|
16
|
+
* 4. https://api.kolbo.ai/api — default
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
class KolboApiError extends Error {
|
|
20
|
+
constructor(message, { code, status, data } = {}) {
|
|
21
|
+
super(message);
|
|
22
|
+
this.name = 'KolboApiError';
|
|
23
|
+
this.code = code || null;
|
|
24
|
+
this.status = status || null;
|
|
25
|
+
this.data = data || null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// Partner / whitelabel resolution (mirrors CLI's brand/partner.ts)
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
function readJsonSync(file) {
|
|
34
|
+
try {
|
|
35
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
36
|
+
} catch (_) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Resolve the API base URL, checking the same sources as the CLI:
|
|
43
|
+
* 1. KOLBO_API_URL / KOLBO_API_BASE env vars
|
|
44
|
+
* 2. partner.json files (KOLBO_PARTNER_PROFILE, XDG_CONFIG_HOME, ~/.config)
|
|
45
|
+
* 3. Default: https://api.kolbo.ai/api
|
|
46
|
+
*/
|
|
47
|
+
function resolveApiBase() {
|
|
48
|
+
// Env vars take priority
|
|
49
|
+
const fromEnv = process.env.KOLBO_API_URL || process.env.KOLBO_API_BASE;
|
|
50
|
+
if (fromEnv) return fromEnv.replace(/\/$/, '');
|
|
51
|
+
|
|
52
|
+
// Partner profile files (same order as CLI)
|
|
53
|
+
const candidates = [];
|
|
54
|
+
if (process.env.KOLBO_PARTNER_PROFILE) {
|
|
55
|
+
candidates.push(process.env.KOLBO_PARTNER_PROFILE);
|
|
56
|
+
}
|
|
57
|
+
const xdgConfig = process.env.XDG_CONFIG_HOME || (
|
|
58
|
+
process.platform === 'win32'
|
|
59
|
+
? path.join(os.homedir(), '.config')
|
|
60
|
+
: path.join(os.homedir(), '.config')
|
|
61
|
+
);
|
|
62
|
+
candidates.push(path.join(xdgConfig, 'kolbo', 'partner.json'));
|
|
63
|
+
|
|
64
|
+
for (const file of candidates) {
|
|
65
|
+
const data = readJsonSync(file);
|
|
66
|
+
if (data && data.apiBase) return data.apiBase.replace(/\/$/, '');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return 'https://api.kolbo.ai/api';
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
// CLI auth store reader
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* XDG data dir — same logic as the `xdg-basedir` npm package the CLI uses.
|
|
78
|
+
* On Windows with Git Bash / MSYS2 this resolves to ~/.local/share (matching
|
|
79
|
+
* what the CLI actually writes to).
|
|
80
|
+
*/
|
|
81
|
+
function xdgDataDir() {
|
|
82
|
+
if (process.env.XDG_DATA_HOME) return process.env.XDG_DATA_HOME;
|
|
83
|
+
if (process.platform === 'win32') {
|
|
84
|
+
// xdg-basedir on Windows: LOCALAPPDATA → ~/.local/share fallback
|
|
85
|
+
return process.env.LOCALAPPDATA || path.join(os.homedir(), '.local', 'share');
|
|
86
|
+
}
|
|
87
|
+
if (process.platform === 'darwin') {
|
|
88
|
+
return path.join(os.homedir(), 'Library', 'Application Support');
|
|
89
|
+
}
|
|
90
|
+
return path.join(os.homedir(), '.local', 'share');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Read the Kolbo CLI's auth store. The CLI writes credentials to
|
|
95
|
+
* <xdg-data>/kolbo/auth.json after device-code login.
|
|
96
|
+
*
|
|
97
|
+
* On Windows (Git Bash / MSYS2) xdg-basedir resolves to ~/.local/share,
|
|
98
|
+
* so we check multiple candidates to be safe.
|
|
99
|
+
*/
|
|
100
|
+
function readCliAuthKey() {
|
|
101
|
+
const dataDir = xdgDataDir();
|
|
102
|
+
const candidates = [
|
|
103
|
+
path.join(dataDir, 'kolbo', 'auth.json'),
|
|
104
|
+
];
|
|
105
|
+
// Windows fallback: also check ~/.local/share if LOCALAPPDATA was primary
|
|
106
|
+
if (process.platform === 'win32' && dataDir !== path.join(os.homedir(), '.local', 'share')) {
|
|
107
|
+
candidates.push(path.join(os.homedir(), '.local', 'share', 'kolbo', 'auth.json'));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Determine the API host for namespaced auth lookup
|
|
111
|
+
const apiBase = process.env.KOLBO_API_URL || process.env.KOLBO_API_BASE || '';
|
|
112
|
+
let apiHost = null;
|
|
113
|
+
try { apiHost = new URL(apiBase).host; } catch (_) {}
|
|
114
|
+
if (!apiHost) {
|
|
115
|
+
// Check partner.json for the API host
|
|
116
|
+
const partnerCandidates = [];
|
|
117
|
+
if (process.env.KOLBO_PARTNER_PROFILE) partnerCandidates.push(process.env.KOLBO_PARTNER_PROFILE);
|
|
118
|
+
const xdgCfg = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config');
|
|
119
|
+
partnerCandidates.push(path.join(xdgCfg, 'kolbo', 'partner.json'));
|
|
120
|
+
for (const f of partnerCandidates) {
|
|
121
|
+
const p = readJsonSync(f);
|
|
122
|
+
if (p && p.apiBase) { try { apiHost = new URL(p.apiBase).host; } catch (_) {} break; }
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (!apiHost) apiHost = 'api.kolbo.ai';
|
|
126
|
+
|
|
127
|
+
for (const file of candidates) {
|
|
128
|
+
try {
|
|
129
|
+
const auth = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
130
|
+
// Try namespaced key first (e.g. "kolbo@api.kolbo.ai"), then bare "kolbo"
|
|
131
|
+
const entry = auth[`kolbo@${apiHost}`] || auth.kolbo;
|
|
132
|
+
if (!entry) continue;
|
|
133
|
+
if (entry.type === 'oauth' && entry.refresh) return entry.refresh;
|
|
134
|
+
if (entry.type === 'api' && entry.key) return entry.key;
|
|
135
|
+
} catch (_) {
|
|
136
|
+
// File doesn't exist or isn't valid JSON — try next
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
// HTTP client
|
|
144
|
+
// ---------------------------------------------------------------------------
|
|
145
|
+
|
|
146
|
+
class KolboClient {
|
|
147
|
+
constructor() {
|
|
148
|
+
this.baseUrl = resolveApiBase();
|
|
149
|
+
this._envKey = process.env.KOLBO_API_KEY || null;
|
|
150
|
+
this._authStoreKey = null; // lazy-loaded
|
|
151
|
+
this.apiKey = this._envKey || this._readAuthStore();
|
|
152
|
+
|
|
153
|
+
if (!this.apiKey) {
|
|
154
|
+
throw new Error(
|
|
155
|
+
'Kolbo API key not found.\n' +
|
|
156
|
+
'Fix: Run "kolbo auth login" in the terminal, then restart this editor.\n' +
|
|
157
|
+
'Or: Get an API key at https://app.kolbo.ai/developer and set KOLBO_API_KEY env var.'
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
_readAuthStore() {
|
|
163
|
+
this._authStoreKey = readCliAuthKey();
|
|
164
|
+
return this._authStoreKey;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* On 401, re-read the CLI auth store in case the user re-authenticated
|
|
169
|
+
* since the MCP server started. Returns true if a new key was found.
|
|
170
|
+
*/
|
|
171
|
+
_tryRefreshKey() {
|
|
172
|
+
if (this._envKey) {
|
|
173
|
+
// Env var is set but invalid — can't override it, but try auth store
|
|
174
|
+
const fresh = readCliAuthKey();
|
|
175
|
+
if (fresh && fresh !== this._envKey) {
|
|
176
|
+
this.apiKey = fresh;
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
const fresh = readCliAuthKey();
|
|
182
|
+
if (fresh && fresh !== this.apiKey) {
|
|
183
|
+
this.apiKey = fresh;
|
|
184
|
+
return true;
|
|
185
|
+
}
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async request(method, reqPath, body = null) {
|
|
190
|
+
const result = await this._doRequest(method, reqPath, body);
|
|
191
|
+
|
|
192
|
+
// On 401, try re-reading auth store and retry once
|
|
193
|
+
if (result._status === 401 && this._tryRefreshKey()) {
|
|
194
|
+
return this._doRequest(method, reqPath, body);
|
|
195
|
+
}
|
|
196
|
+
return result;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async _doRequest(method, reqPath, body = null) {
|
|
200
|
+
const url = `${this.baseUrl}${reqPath}`;
|
|
201
|
+
const options = {
|
|
202
|
+
method,
|
|
203
|
+
headers: {
|
|
204
|
+
'X-API-Key': this.apiKey,
|
|
205
|
+
'Content-Type': 'application/json'
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
if (body) {
|
|
210
|
+
options.body = JSON.stringify(body);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const response = await fetch(url, options);
|
|
214
|
+
let data;
|
|
215
|
+
try {
|
|
216
|
+
data = await response.json();
|
|
217
|
+
} catch (_) {
|
|
218
|
+
throw new KolboApiError(`API error: ${response.status} ${response.statusText}`, {
|
|
219
|
+
status: response.status,
|
|
220
|
+
data: null
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (!response.ok || data.success === false) {
|
|
225
|
+
const message = data.error || data.message || `API error: ${response.status}`;
|
|
226
|
+
const code = data.code || null;
|
|
227
|
+
let fullMessage = code ? `${message} [${code}]` : message;
|
|
228
|
+
if (response.status === 401) {
|
|
229
|
+
// Tag the response so the retry logic in request() can see it
|
|
230
|
+
data._status = 401;
|
|
231
|
+
fullMessage += '\n\nAPI key is invalid or expired. Fix: run "kolbo auth login" in the terminal, then restart this editor. Or get a new key at https://app.kolbo.ai/developer';
|
|
232
|
+
}
|
|
233
|
+
throw new KolboApiError(fullMessage, {
|
|
234
|
+
code,
|
|
235
|
+
status: response.status,
|
|
236
|
+
data
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return data;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async post(reqPath, body) {
|
|
244
|
+
return this.request('POST', reqPath, body);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async get(reqPath) {
|
|
248
|
+
return this.request('GET', reqPath);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async delete(reqPath) {
|
|
252
|
+
return this.request('DELETE', reqPath);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async postMultipart(reqPath, formData) {
|
|
256
|
+
const result = await this._doMultipart(reqPath, formData);
|
|
257
|
+
if (result._status === 401 && this._tryRefreshKey()) {
|
|
258
|
+
return this._doMultipart(reqPath, formData);
|
|
259
|
+
}
|
|
260
|
+
return result;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
async _doMultipart(reqPath, formData) {
|
|
264
|
+
const url = `${this.baseUrl}${reqPath}`;
|
|
265
|
+
const headers = {
|
|
266
|
+
'X-API-Key': this.apiKey,
|
|
267
|
+
...formData.getHeaders()
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
try {
|
|
271
|
+
const len = formData.getLengthSync();
|
|
272
|
+
if (len) headers['Content-Length'] = String(len);
|
|
273
|
+
} catch (_) { /* streaming length unavailable */ }
|
|
274
|
+
|
|
275
|
+
const response = await fetch(url, {
|
|
276
|
+
method: 'POST',
|
|
277
|
+
headers,
|
|
278
|
+
body: formData,
|
|
279
|
+
duplex: 'half'
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
let data;
|
|
283
|
+
try {
|
|
284
|
+
data = await response.json();
|
|
285
|
+
} catch (_) {
|
|
286
|
+
throw new KolboApiError(`API error: ${response.status} ${response.statusText}`, {
|
|
287
|
+
status: response.status,
|
|
288
|
+
data: null
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (!response.ok || data.success === false) {
|
|
293
|
+
const message = data.error || data.message || `API error: ${response.status}`;
|
|
294
|
+
const code = data.code || null;
|
|
295
|
+
let fullMessage = code ? `${message} [${code}]` : message;
|
|
296
|
+
if (response.status === 401) {
|
|
297
|
+
data._status = 401;
|
|
298
|
+
fullMessage += '\n\nAPI key is invalid or expired. Fix: run "kolbo auth login" in the terminal, then restart this editor. Or get a new key at https://app.kolbo.ai/developer';
|
|
299
|
+
}
|
|
300
|
+
throw new KolboApiError(fullMessage, {
|
|
301
|
+
code,
|
|
302
|
+
status: response.status,
|
|
303
|
+
data
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return data;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
module.exports = KolboClient;
|
|
312
|
+
module.exports.KolboApiError = KolboApiError;
|
package/src/tools/chat.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* `npx @kolbo/mcp` installs in the wild will break silently. Add new tools or
|
|
4
4
|
* new OPTIONAL args only. Full rules: ../index.js top-of-file and CLAUDE.md. */
|
|
5
5
|
|
|
6
|
+
const { z } = require('zod');
|
|
6
7
|
const { pollUntilDone } = require('../polling');
|
|
7
8
|
|
|
8
9
|
function registerChatTools(server, client) {
|
|
@@ -11,13 +12,13 @@ function registerChatTools(server, client) {
|
|
|
11
12
|
'chat_send_message',
|
|
12
13
|
'Send a chat message to Kolbo AI. Starts a new conversation (omit session_id) or continues an existing one. Returns the assistant response when complete. Supports web search and deep think modes.',
|
|
13
14
|
{
|
|
14
|
-
message:
|
|
15
|
-
model:
|
|
16
|
-
session_id:
|
|
17
|
-
system_prompt:
|
|
18
|
-
web_search:
|
|
19
|
-
deep_think:
|
|
20
|
-
enhance_prompt:
|
|
15
|
+
message: z.string().describe('The user message to send'),
|
|
16
|
+
model: z.string().optional().describe('Model identifier (e.g. "gpt-4o", "claude-sonnet-4-5"). Omit for Smart Select (auto).'),
|
|
17
|
+
session_id: z.string().optional().describe('Existing chat session ID to continue. Omit to start a new conversation.'),
|
|
18
|
+
system_prompt: z.string().optional().describe('System prompt for the conversation. Only applied when creating a new session.'),
|
|
19
|
+
web_search: z.boolean().optional().describe('Enable web search for this message. Default: false'),
|
|
20
|
+
deep_think: z.boolean().optional().describe('Enable deep think (extended reasoning). Default: false'),
|
|
21
|
+
enhance_prompt: z.boolean().optional().describe('Enhance the prompt. Default: true')
|
|
21
22
|
},
|
|
22
23
|
async ({ message, model, session_id, system_prompt, web_search, deep_think, enhance_prompt }) => {
|
|
23
24
|
const gen = await client.post('/v1/chat', {
|
|
@@ -65,8 +66,8 @@ function registerChatTools(server, client) {
|
|
|
65
66
|
'chat_list_conversations',
|
|
66
67
|
'List your SDK chat conversations, most-recent first. Returns session_id, name, and activity timestamps.',
|
|
67
68
|
{
|
|
68
|
-
page:
|
|
69
|
-
limit:
|
|
69
|
+
page: z.number().optional().describe('Page number, 1-indexed. Default: 1'),
|
|
70
|
+
limit: z.number().optional().describe('Results per page, max 50. Default: 20')
|
|
70
71
|
},
|
|
71
72
|
async ({ page, limit }) => {
|
|
72
73
|
const params = new URLSearchParams();
|
|
@@ -93,9 +94,9 @@ function registerChatTools(server, client) {
|
|
|
93
94
|
'chat_get_messages',
|
|
94
95
|
'Fetch messages in a chat conversation. Returns role, content, model, and any media URLs attached to each message.',
|
|
95
96
|
{
|
|
96
|
-
session_id:
|
|
97
|
-
page:
|
|
98
|
-
limit:
|
|
97
|
+
session_id: z.string().describe('The chat session ID'),
|
|
98
|
+
page: z.number().optional().describe('Page number, 1-indexed. Default: 1'),
|
|
99
|
+
limit: z.number().optional().describe('Messages per page, max 100. Default: 50')
|
|
99
100
|
},
|
|
100
101
|
async ({ session_id, page, limit }) => {
|
|
101
102
|
const params = new URLSearchParams();
|
package/src/tools/generate.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* `npx @kolbo/mcp` installs in the wild will break silently. Add new tools or
|
|
4
4
|
* new OPTIONAL args only. Full rules: ../index.js top-of-file and CLAUDE.md. */
|
|
5
5
|
|
|
6
|
+
const { z } = require('zod');
|
|
6
7
|
const FormData = require('form-data');
|
|
7
8
|
const { pollUntilDone } = require('../polling');
|
|
8
9
|
const { resolveToBuffer } = require('./_shared');
|
|
@@ -13,15 +14,15 @@ function registerGenerateTools(server, client) {
|
|
|
13
14
|
'generate_image',
|
|
14
15
|
'Generate image(s) from a text prompt using Kolbo AI. Supports Visual DNA profiles (for character/style/product consistency), moodboards (for style direction), reference images (for composition guidance), batch generation (num_images), and web-search grounding. For EDITING an existing image, use generate_image_edit instead. For a coordinated multi-scene set (storyboard, ad campaign), use generate_creative_director. Returns the final image URL(s) when complete.',
|
|
15
16
|
{
|
|
16
|
-
prompt:
|
|
17
|
-
model:
|
|
18
|
-
aspect_ratio:
|
|
19
|
-
enhance_prompt:
|
|
20
|
-
num_images:
|
|
21
|
-
reference_images:
|
|
22
|
-
visual_dna_ids:
|
|
23
|
-
moodboard_id:
|
|
24
|
-
enable_web_search:
|
|
17
|
+
prompt: z.string().describe('Text description of the image to generate'),
|
|
18
|
+
model: z.string().optional().describe('Model identifier. Use list_models type="image" to see options. Omit for Smart Select.'),
|
|
19
|
+
aspect_ratio: z.string().optional().describe('Aspect ratio (e.g., "1:1", "16:9", "9:16"). Default: "1:1"'),
|
|
20
|
+
enhance_prompt: z.boolean().optional().describe('Enhance the prompt for better results. Default: true'),
|
|
21
|
+
num_images: z.number().optional().describe('Number of images to generate in one call. Default: 1'),
|
|
22
|
+
reference_images: z.array(z.string()).optional().describe('Array of image URLs used as composition/style references (NOT as source images for editing — use generate_image_edit for that).'),
|
|
23
|
+
visual_dna_ids: z.array(z.string()).optional().describe('Array of Visual DNA profile IDs (from create_visual_dna / list_visual_dnas) to apply for character / style / product / scene consistency. Pass the `id` field of each profile. Use this when the user wants to keep the same character or style across multiple images.'),
|
|
24
|
+
moodboard_id: z.string().optional().describe('Moodboard ID (from list_moodboards / get_moodboard) whose master_prompt and style_guide should be applied to this generation.'),
|
|
25
|
+
enable_web_search: z.boolean().optional().describe('Enable web-search grounding for the prompt (useful for current events, brand references, real-world accuracy). Default: false')
|
|
25
26
|
},
|
|
26
27
|
async ({ prompt, model, aspect_ratio, enhance_prompt, num_images, reference_images, visual_dna_ids, moodboard_id, enable_web_search }) => {
|
|
27
28
|
const gen = await client.post('/v1/generate/image', {
|
|
@@ -52,15 +53,15 @@ function registerGenerateTools(server, client) {
|
|
|
52
53
|
'generate_image_edit',
|
|
53
54
|
'Edit or transform an existing image using AI. Provide the source image URL(s) in `source_images` and describe the edit in `prompt` (e.g., "remove the background", "change the car color to red", "add sunglasses to the person"). Supports Visual DNA profiles and moodboards for style-consistent edits. For creating a brand new image from scratch, use generate_image. Returns the edited image URL(s) when complete.',
|
|
54
55
|
{
|
|
55
|
-
prompt:
|
|
56
|
-
model:
|
|
57
|
-
source_images:
|
|
58
|
-
aspect_ratio:
|
|
59
|
-
enhance_prompt:
|
|
60
|
-
num_images:
|
|
61
|
-
visual_dna_ids:
|
|
62
|
-
moodboard_id:
|
|
63
|
-
enable_web_search:
|
|
56
|
+
prompt: z.string().describe('Description of the edit to apply (e.g., "remove the background", "change the sky to sunset")'),
|
|
57
|
+
model: z.string().optional().describe('Model identifier. Use list_models type="image_edit" to see options. Omit for Smart Select.'),
|
|
58
|
+
source_images: z.array(z.string()).describe('Array of source image URLs to edit. Typically one, but some models accept multiple for compositing.'),
|
|
59
|
+
aspect_ratio: z.string().optional().describe('Output aspect ratio (e.g., "1:1", "16:9", "9:16"). Default: "1:1"'),
|
|
60
|
+
enhance_prompt: z.boolean().optional().describe('Enhance the prompt for better results. Default: true'),
|
|
61
|
+
num_images: z.number().optional().describe('Number of output images. Default: 1'),
|
|
62
|
+
visual_dna_ids: z.array(z.string()).optional().describe('Array of Visual DNA profile IDs to apply for consistency with an existing character / style / product.'),
|
|
63
|
+
moodboard_id: z.string().optional().describe('Moodboard ID whose master_prompt and style_guide should be applied.'),
|
|
64
|
+
enable_web_search: z.boolean().optional().describe('Enable web-search grounding. Default: false')
|
|
64
65
|
},
|
|
65
66
|
async ({ prompt, model, source_images, aspect_ratio, enhance_prompt, num_images, visual_dna_ids, moodboard_id, enable_web_search }) => {
|
|
66
67
|
const gen = await client.post('/v1/generate/image-edit', {
|
|
@@ -91,17 +92,17 @@ function registerGenerateTools(server, client) {
|
|
|
91
92
|
'generate_creative_director',
|
|
92
93
|
'Generate a multi-scene coordinated set from ONE creative brief. Use this INSTEAD of calling generate_image/generate_video multiple times when the user wants a storyboard, multi-scene ad, product showcase, or any set of related outputs that should share visual language. Produces 1–8 scenes in a single request with consistent style. Supports image mode and video mode (`workflow_type`). Visual DNA and moodboard references keep character/style consistent across every scene.',
|
|
93
94
|
{
|
|
94
|
-
prompt:
|
|
95
|
-
scene_count:
|
|
96
|
-
model:
|
|
97
|
-
aspect_ratio:
|
|
98
|
-
workflow_type:
|
|
99
|
-
duration:
|
|
100
|
-
enhance_prompt:
|
|
101
|
-
reference_images:
|
|
102
|
-
visual_dna_ids:
|
|
103
|
-
moodboard_id:
|
|
104
|
-
moodboard_ids:
|
|
95
|
+
prompt: z.string().describe('Creative brief or concept describing the full set of scenes to generate'),
|
|
96
|
+
scene_count: z.number().optional().describe('Number of scenes to generate, 1–8. Default: 4'),
|
|
97
|
+
model: z.string().optional().describe('Model identifier applied to every scene. Omit for Smart Select.'),
|
|
98
|
+
aspect_ratio: z.string().optional().describe('Aspect ratio applied to every scene (e.g., "1:1", "16:9", "9:16"). Default: "1:1"'),
|
|
99
|
+
workflow_type: z.string().optional().describe('"image" (default) or "video"'),
|
|
100
|
+
duration: z.number().optional().describe('Duration in seconds per scene (video mode only). E.g., 5 or 10.'),
|
|
101
|
+
enhance_prompt: z.boolean().optional().describe('Enhance prompts per scene. Default: true'),
|
|
102
|
+
reference_images: z.array(z.string()).optional().describe('Array of reference image URLs to guide style/composition of every scene.'),
|
|
103
|
+
visual_dna_ids: z.array(z.string()).optional().describe('Array of Visual DNA profile IDs to apply consistently across every scene. This is the ideal way to keep a character or product looking the same in all scenes of a campaign.'),
|
|
104
|
+
moodboard_id: z.string().optional().describe('A single moodboard ID whose master_prompt and style_guide should shape every scene.'),
|
|
105
|
+
moodboard_ids: z.array(z.string()).optional().describe('Multiple moodboard IDs when blending styles. Prefer `moodboard_id` for single moodboards.')
|
|
105
106
|
},
|
|
106
107
|
async ({ prompt, scene_count, model, aspect_ratio, workflow_type, duration, enhance_prompt, reference_images, visual_dna_ids, moodboard_id, moodboard_ids }) => {
|
|
107
108
|
const gen = await client.post('/v1/generate/creative-director', {
|
|
@@ -142,13 +143,13 @@ function registerGenerateTools(server, client) {
|
|
|
142
143
|
'generate_video',
|
|
143
144
|
'Generate a video from a text prompt using Kolbo AI. For animating an existing still image into motion, use generate_video_from_image instead. For a coordinated multi-scene video campaign, use generate_creative_director with workflow_type="video". Supports Visual DNA profiles (for character consistency) and reference images (for style guidance). Returns the final video URL when complete.',
|
|
144
145
|
{
|
|
145
|
-
prompt:
|
|
146
|
-
model:
|
|
147
|
-
aspect_ratio:
|
|
148
|
-
duration:
|
|
149
|
-
enhance_prompt:
|
|
150
|
-
reference_images:
|
|
151
|
-
visual_dna_ids:
|
|
146
|
+
prompt: z.string().describe('Text description of the video to generate'),
|
|
147
|
+
model: z.string().optional().describe('Model identifier. Use list_models type="video" to see options. Check supported_durations and supported_aspect_ratios.'),
|
|
148
|
+
aspect_ratio: z.string().optional().describe('Aspect ratio (e.g., "16:9", "9:16", "1:1"). Default: "16:9"'),
|
|
149
|
+
duration: z.number().optional().describe('Duration in seconds. Must be a value the chosen model supports — check supported_durations from list_models. Default: 5'),
|
|
150
|
+
enhance_prompt: z.boolean().optional().describe('Enhance the prompt. Default: true'),
|
|
151
|
+
reference_images: z.array(z.string()).optional().describe('Array of image URLs used as visual references (style / composition / subject).'),
|
|
152
|
+
visual_dna_ids: z.array(z.string()).optional().describe('Array of Visual DNA profile IDs to keep a character / style consistent with prior generations.')
|
|
152
153
|
},
|
|
153
154
|
async ({ prompt, model, aspect_ratio, duration, enhance_prompt, reference_images, visual_dna_ids }) => {
|
|
154
155
|
const gen = await client.post('/v1/generate/video', {
|
|
@@ -180,13 +181,13 @@ function registerGenerateTools(server, client) {
|
|
|
180
181
|
'generate_video_from_image',
|
|
181
182
|
'Animate an existing still image into a video using Kolbo AI. The image comes from `image_url`; `prompt` describes the motion (not the subject — the subject is already in the image). For generating a video from scratch, use generate_video. Returns the final video URL when complete.',
|
|
182
183
|
{
|
|
183
|
-
image_url:
|
|
184
|
-
prompt:
|
|
185
|
-
model:
|
|
186
|
-
aspect_ratio:
|
|
187
|
-
duration:
|
|
188
|
-
enhance_prompt:
|
|
189
|
-
visual_dna_ids:
|
|
184
|
+
image_url: z.string().describe('URL of the source image to animate'),
|
|
185
|
+
prompt: z.string().describe('Text description of the desired MOTION (e.g., "camera slowly pans right while the character walks forward")'),
|
|
186
|
+
model: z.string().optional().describe('Model identifier. Use list_models type="video_from_image" to see options.'),
|
|
187
|
+
aspect_ratio: z.string().optional().describe('Output aspect ratio (e.g., "16:9", "9:16", "1:1"). Default: "16:9"'),
|
|
188
|
+
duration: z.number().optional().describe('Duration in seconds. Must be a value the chosen model supports. Default: 5'),
|
|
189
|
+
enhance_prompt: z.boolean().optional().describe('Enhance the motion prompt. Default: true'),
|
|
190
|
+
visual_dna_ids: z.array(z.string()).optional().describe('Array of Visual DNA profile IDs to maintain consistency with prior characters / styles.')
|
|
190
191
|
},
|
|
191
192
|
async ({ image_url, prompt, model, aspect_ratio, duration, enhance_prompt, visual_dna_ids }) => {
|
|
192
193
|
const gen = await client.post('/v1/generate/video/from-image', {
|
|
@@ -217,13 +218,13 @@ function registerGenerateTools(server, client) {
|
|
|
217
218
|
'generate_music',
|
|
218
219
|
'Generate music from a text description using Kolbo AI. Supports instrumental mode, custom lyrics, style direction, and vocal gender. Default model is Suno. Returns the final audio URL when complete.',
|
|
219
220
|
{
|
|
220
|
-
prompt:
|
|
221
|
-
model:
|
|
222
|
-
style:
|
|
223
|
-
instrumental:
|
|
224
|
-
lyrics:
|
|
225
|
-
vocal_gender:
|
|
226
|
-
enhance_prompt:
|
|
221
|
+
prompt: z.string().describe('Text description of the music to generate (e.g., "upbeat electronic dance track with synthesizers")'),
|
|
222
|
+
model: z.string().optional().describe('Model identifier. Use list_models type="music" to see options. Omit for Suno (default).'),
|
|
223
|
+
style: z.string().optional().describe('Music style / genre (e.g., "pop", "rock", "lo-fi", "electronic", "jazz")'),
|
|
224
|
+
instrumental: z.boolean().optional().describe('Generate instrumental only, no vocals. Default: false'),
|
|
225
|
+
lyrics: z.string().optional().describe('Custom lyrics for the song. If omitted, lyrics are generated automatically from the prompt unless instrumental is true.'),
|
|
226
|
+
vocal_gender: z.string().optional().describe('Preferred vocal gender: "male" or "female". Only applies when instrumental is false.'),
|
|
227
|
+
enhance_prompt: z.boolean().optional().describe('Enhance the prompt. Default: true')
|
|
227
228
|
},
|
|
228
229
|
async ({ prompt, model, style, instrumental, lyrics, vocal_gender, enhance_prompt }) => {
|
|
229
230
|
const gen = await client.post('/v1/generate/music', {
|
|
@@ -254,10 +255,10 @@ function registerGenerateTools(server, client) {
|
|
|
254
255
|
'generate_speech',
|
|
255
256
|
'Convert text to speech using Kolbo AI. Default provider is ElevenLabs. To pick a specific voice by language/gender, call list_voices first and pass the returned voice_id (or a voice display name — both work). Returns the final audio URL when complete.',
|
|
256
257
|
{
|
|
257
|
-
text:
|
|
258
|
-
voice:
|
|
259
|
-
model:
|
|
260
|
-
language:
|
|
258
|
+
text: z.string().describe('The text to convert to speech'),
|
|
259
|
+
voice: z.string().optional().describe('Voice ID (from list_voices) or voice display name (e.g., "Rachel", "Adam"). Default: "Rachel"'),
|
|
260
|
+
model: z.string().optional().describe('Model identifier. Use list_models type="speech" to see options. Default: eleven_v3'),
|
|
261
|
+
language: z.string().optional().describe('Language code (e.g., "en-US", "he-IL", "es-ES"). Default: "en-US"')
|
|
261
262
|
},
|
|
262
263
|
async ({ text, voice, model, language }) => {
|
|
263
264
|
const gen = await client.post('/v1/generate/speech', {
|
|
@@ -287,9 +288,9 @@ function registerGenerateTools(server, client) {
|
|
|
287
288
|
'generate_sound',
|
|
288
289
|
'Generate sound effects (not music, not speech) from a text description using Kolbo AI. Use this for ambient sounds, foley, impacts, atmospheres, UI sounds, etc. For music use generate_music; for voice use generate_speech. Returns the final audio URL when complete.',
|
|
289
290
|
{
|
|
290
|
-
prompt:
|
|
291
|
-
model:
|
|
292
|
-
duration:
|
|
291
|
+
prompt: z.string().describe('Text description of the sound effect (e.g., "thunder clap with rain", "door creaking open", "futuristic UI beep")'),
|
|
292
|
+
model: z.string().optional().describe('Model identifier. Use list_models type="sound" to see options. Default: elevenlabs-sound-effects-v1'),
|
|
293
|
+
duration: z.number().optional().describe('Duration in seconds. Omit for automatic duration.')
|
|
293
294
|
},
|
|
294
295
|
async ({ prompt, model, duration }) => {
|
|
295
296
|
const gen = await client.post('/v1/generate/sound', {
|
|
@@ -318,9 +319,9 @@ function registerGenerateTools(server, client) {
|
|
|
318
319
|
'list_voices',
|
|
319
320
|
'List available TTS voices for generate_speech. Returns preset voices and the user\'s own cloned/designed voices. Filter by provider, language, or gender to find the right voice. Use the returned `voice_id` as the `voice` parameter in generate_speech.',
|
|
320
321
|
{
|
|
321
|
-
provider:
|
|
322
|
-
language:
|
|
323
|
-
gender:
|
|
322
|
+
provider: z.string().optional().describe('Filter by provider (e.g., "elevenLabs", "google")'),
|
|
323
|
+
language: z.string().optional().describe('Filter by language name or code (e.g., "English", "en-US")'),
|
|
324
|
+
gender: z.string().optional().describe('Filter by gender (e.g., "Female", "Male")')
|
|
324
325
|
},
|
|
325
326
|
async ({ provider, language, gender }) => {
|
|
326
327
|
const params = new URLSearchParams();
|
|
@@ -355,7 +356,7 @@ function registerGenerateTools(server, client) {
|
|
|
355
356
|
'get_generation_status',
|
|
356
357
|
'Check the status of a generation. Use this as a FALLBACK when a generation tool returned a timeout error — the generation is probably still running on the server. Pass the generation_id from the timeout error (or from any prior generation response).',
|
|
357
358
|
{
|
|
358
|
-
generation_id:
|
|
359
|
+
generation_id: z.string().describe('The generation ID to check')
|
|
359
360
|
},
|
|
360
361
|
async ({ generation_id }) => {
|
|
361
362
|
const result = await client.get(`/v1/generate/${encodeURIComponent(generation_id)}/status`);
|
|
@@ -378,16 +379,16 @@ function registerGenerateTools(server, client) {
|
|
|
378
379
|
'generate_elements',
|
|
379
380
|
'Generate a video from reference elements (images and/or videos) + a text prompt. Use when the user wants to animate specific uploaded/referenced assets — e.g. "animate this product", "put these 3 characters into a scene". Supports Visual DNA for character consistency. For text-only → video use generate_video instead. For animating a single still image use generate_video_from_image. Returns the final video URL when complete.',
|
|
380
381
|
{
|
|
381
|
-
prompt:
|
|
382
|
-
model:
|
|
383
|
-
reference_images:
|
|
384
|
-
files:
|
|
385
|
-
duration:
|
|
386
|
-
aspect_ratio:
|
|
387
|
-
motion:
|
|
388
|
-
preset_id:
|
|
389
|
-
enhance_prompt:
|
|
390
|
-
visual_dna_ids:
|
|
382
|
+
prompt: z.string().describe('Text description of the desired video / animation'),
|
|
383
|
+
model: z.string().optional().describe('Model identifier. Use list_models type="video" to see options. Omit for Smart Select.'),
|
|
384
|
+
reference_images: z.array(z.string()).optional().describe('Array of public image URLs used as reference elements (product shots, character references, etc.). URL mode.'),
|
|
385
|
+
files: z.array(z.string()).optional().describe('Array of URLs or absolute local paths — alternative to reference_images. Use this when you have local files to upload. Each item can be a URL OR a local path.'),
|
|
386
|
+
duration: z.number().optional().describe('Duration in seconds. Default: 5'),
|
|
387
|
+
aspect_ratio: z.string().optional().describe('Aspect ratio (e.g., "16:9", "9:16", "1:1"). Default: "16:9"'),
|
|
388
|
+
motion: z.string().optional().describe('Motion style / intensity hint (optional)'),
|
|
389
|
+
preset_id: z.string().optional().describe('Preset ID from list_presets type="video" (optional)'),
|
|
390
|
+
enhance_prompt: z.boolean().optional().describe('Enhance the prompt. Default: true'),
|
|
391
|
+
visual_dna_ids: z.array(z.string()).optional().describe('Array of Visual DNA profile IDs to apply for character/style consistency across outputs.')
|
|
391
392
|
},
|
|
392
393
|
async ({ prompt, model, reference_images, files, duration, aspect_ratio, motion, preset_id, enhance_prompt, visual_dna_ids }) => {
|
|
393
394
|
if (!prompt) throw new Error('prompt is required');
|
|
@@ -441,16 +442,16 @@ function registerGenerateTools(server, client) {
|
|
|
441
442
|
'generate_first_last_frame',
|
|
442
443
|
'Generate a video that morphs / interpolates from a FIRST frame to a LAST frame. Provide the two frames as URLs (first_frame_url + last_frame_url) OR as local file paths (first_frame + last_frame). Optional prompt describes the desired motion/transition. Do NOT mix URL and file inputs. Returns the final video URL when complete.',
|
|
443
444
|
{
|
|
444
|
-
first_frame_url:
|
|
445
|
-
last_frame_url:
|
|
446
|
-
first_frame:
|
|
447
|
-
last_frame:
|
|
448
|
-
prompt:
|
|
449
|
-
model:
|
|
450
|
-
duration:
|
|
451
|
-
aspect_ratio:
|
|
452
|
-
enhance_prompt:
|
|
453
|
-
visual_dna_ids:
|
|
445
|
+
first_frame_url: z.string().optional().describe('Public URL of the first frame image (URL mode)'),
|
|
446
|
+
last_frame_url: z.string().optional().describe('Public URL of the last frame image (URL mode)'),
|
|
447
|
+
first_frame: z.string().optional().describe('URL or absolute local path to the first frame (file mode — alternative to first_frame_url)'),
|
|
448
|
+
last_frame: z.string().optional().describe('URL or absolute local path to the last frame (file mode — alternative to last_frame_url)'),
|
|
449
|
+
prompt: z.string().optional().describe('Optional description of the desired motion between the two frames (e.g. "smooth camera dolly in")'),
|
|
450
|
+
model: z.string().optional().describe('Model identifier. Use list_models type="video_from_image" to see options. Omit for Smart Select.'),
|
|
451
|
+
duration: z.number().optional().describe('Duration in seconds. Default: 5'),
|
|
452
|
+
aspect_ratio: z.string().optional().describe('Aspect ratio (auto-detected from first frame if not provided). Default: "16:9"'),
|
|
453
|
+
enhance_prompt: z.boolean().optional().describe('Enhance the prompt. Default: true'),
|
|
454
|
+
visual_dna_ids: z.array(z.string()).optional().describe('Array of Visual DNA profile IDs to apply.')
|
|
454
455
|
},
|
|
455
456
|
async ({ first_frame_url, last_frame_url, first_frame, last_frame, prompt, model, duration, aspect_ratio, enhance_prompt, visual_dna_ids }) => {
|
|
456
457
|
const urlMode = first_frame_url && last_frame_url;
|
|
@@ -508,11 +509,11 @@ function registerGenerateTools(server, client) {
|
|
|
508
509
|
'generate_lipsync',
|
|
509
510
|
'Lipsync an audio track to a source image or video. Both `source` (image or video) and `audio` can be provided as URLs or as absolute local file paths. Pass a text_prompt only if the model supports it (some lipsync models do character performance from a prompt). Returns a lipsynced video URL.',
|
|
510
511
|
{
|
|
511
|
-
source:
|
|
512
|
-
audio:
|
|
513
|
-
text_prompt:
|
|
514
|
-
model:
|
|
515
|
-
bounding_box_target:
|
|
512
|
+
source: z.string().describe('URL or absolute local path to the source image or video (the face to animate)'),
|
|
513
|
+
audio: z.string().describe('URL or absolute local path to the audio track (the voice to sync to)'),
|
|
514
|
+
text_prompt: z.string().optional().describe('Optional text prompt (for performance-capable models)'),
|
|
515
|
+
model: z.string().optional().describe('Model identifier. Use list_models type="lipsync" to see options. Omit for Smart Select.'),
|
|
516
|
+
bounding_box_target: z.array(z.number()).optional().describe('Optional bounding box [x, y, w, h] for multi-face inputs (Hedra Character3 style). Leave empty for single-face.')
|
|
516
517
|
},
|
|
517
518
|
async ({ source, audio, text_prompt, model, bounding_box_target }) => {
|
|
518
519
|
if (!source) throw new Error('source is required (URL or absolute local path to image/video)');
|
|
@@ -578,13 +579,13 @@ function registerGenerateTools(server, client) {
|
|
|
578
579
|
'generate_video_from_video',
|
|
579
580
|
'Restyle / transform an existing video using a text prompt (video-to-video). Use for style transfer, scene restyling, subject swap — anything where you want to keep the motion from the input video but change the look. Source video can be a URL or absolute local path. For animating a still image use generate_video_from_image instead. For text-only → video use generate_video.',
|
|
580
581
|
{
|
|
581
|
-
source_video:
|
|
582
|
-
prompt:
|
|
583
|
-
model:
|
|
584
|
-
aspect_ratio:
|
|
585
|
-
duration:
|
|
586
|
-
enhance_prompt:
|
|
587
|
-
visual_dna_ids:
|
|
582
|
+
source_video: z.string().describe('URL or absolute local path to the source video to restyle'),
|
|
583
|
+
prompt: z.string().describe('Text description of the desired restyle / transformation'),
|
|
584
|
+
model: z.string().optional().describe('Model identifier. Omit for Smart Select.'),
|
|
585
|
+
aspect_ratio: z.string().optional().describe('Output aspect ratio. Default: matches source'),
|
|
586
|
+
duration: z.number().optional().describe('Duration in seconds (default: matches source)'),
|
|
587
|
+
enhance_prompt: z.boolean().optional().describe('Enhance the prompt. Default: true'),
|
|
588
|
+
visual_dna_ids: z.array(z.string()).optional().describe('Array of Visual DNA profile IDs to apply for character/style consistency.')
|
|
588
589
|
},
|
|
589
590
|
async ({ source_video, prompt, model, aspect_ratio, duration, enhance_prompt, visual_dna_ids }) => {
|
|
590
591
|
if (!source_video) throw new Error('source_video is required');
|
|
@@ -633,7 +634,7 @@ function registerGenerateTools(server, client) {
|
|
|
633
634
|
'transcribe_audio',
|
|
634
635
|
'Transcribe audio or video into text + SRT subtitles. Source can be a URL or an absolute local file path. Returns the full text, SRT content, duration, and download URLs for .srt/.txt files. Works on both audio-only files (mp3, wav, m4a) and videos with audio tracks (mp4, mov, webm).',
|
|
635
636
|
{
|
|
636
|
-
source:
|
|
637
|
+
source: z.string().describe('URL or absolute local path to the audio / video file to transcribe')
|
|
637
638
|
},
|
|
638
639
|
async ({ source }) => {
|
|
639
640
|
if (!source) throw new Error('source is required (URL or absolute local path)');
|
|
@@ -660,6 +661,7 @@ function registerGenerateTools(server, client) {
|
|
|
660
661
|
text: JSON.stringify({
|
|
661
662
|
text: result.result?.text || '',
|
|
662
663
|
srt_url: result.result?.srt_url || null,
|
|
664
|
+
word_by_word_srt_url: result.result?.word_by_word_srt_url || null,
|
|
663
665
|
txt_url: result.result?.txt_url || null,
|
|
664
666
|
duration: result.result?.duration || null
|
|
665
667
|
}, null, 2)
|
|
@@ -673,15 +675,15 @@ function registerGenerateTools(server, client) {
|
|
|
673
675
|
'generate_3d',
|
|
674
676
|
'Generate a 3D model from a text prompt, a single reference image, or multiple reference images (for multi-view reconstruction). Returns model URLs in multiple formats (GLB, FBX, OBJ, USDZ). Modes: "text" (prompt-only), "single" (one image), "multi" (multiple images for better quality). The mode is auto-detected from the inputs if not specified.',
|
|
675
677
|
{
|
|
676
|
-
prompt:
|
|
677
|
-
reference_images:
|
|
678
|
-
mode:
|
|
679
|
-
texture_prompt:
|
|
680
|
-
model:
|
|
681
|
-
topology:
|
|
682
|
-
target_polycount:
|
|
683
|
-
enable_tpose:
|
|
684
|
-
enable_pbr:
|
|
678
|
+
prompt: z.string().optional().describe('Text description of the 3D object to generate (used in text mode and also as a hint in image modes)'),
|
|
679
|
+
reference_images: z.array(z.string()).optional().describe('Array of public image URLs. 1 image → single mode, 2+ → multi mode.'),
|
|
680
|
+
mode: z.string().optional().describe('Explicitly set mode: "text" | "single" | "multi". Auto-detected from reference_images if omitted.'),
|
|
681
|
+
texture_prompt: z.string().optional().describe('Optional prompt to guide texture generation'),
|
|
682
|
+
model: z.string().optional().describe('Model identifier. Use list_models type="three_d" to see options.'),
|
|
683
|
+
topology: z.string().optional().describe('Topology preset (optional, model-specific)'),
|
|
684
|
+
target_polycount: z.number().optional().describe('Target polygon count (optional, model-specific)'),
|
|
685
|
+
enable_tpose: z.boolean().optional().describe('Force T-pose for character models (optional)'),
|
|
686
|
+
enable_pbr: z.boolean().optional().describe('Enable PBR textures (optional)')
|
|
685
687
|
},
|
|
686
688
|
async ({ prompt, reference_images, mode, texture_prompt, model, topology, target_polycount, enable_tpose, enable_pbr }) => {
|
|
687
689
|
if (!prompt && !(reference_images && reference_images.length > 0)) {
|
package/src/tools/media.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* `npx @kolbo/mcp` installs in the wild will break silently. Add new tools or
|
|
4
4
|
* new OPTIONAL args only. Full rules: ../index.js top-of-file and CLAUDE.md. */
|
|
5
5
|
|
|
6
|
+
const { z } = require('zod');
|
|
6
7
|
const FormData = require('form-data');
|
|
7
8
|
const { resolveToBuffer } = require('./_shared');
|
|
8
9
|
|
|
@@ -12,8 +13,8 @@ function registerMediaTools(server, client) {
|
|
|
12
13
|
'upload_media',
|
|
13
14
|
'Upload a local file (or remote URL) to the user\'s Kolbo media library and get back a stable Kolbo CDN URL. Use this when the user wants to reference a local file in multiple subsequent generation calls — upload once, then pass the returned URL to generate_image / generate_video / visual_dna / etc. Auto-detects media type (image / video / audio) from the file extension. For a single-use reference where you already have a public URL, you can skip this and pass the URL directly to the generation tool.',
|
|
14
15
|
{
|
|
15
|
-
source:
|
|
16
|
-
description:
|
|
16
|
+
source: z.string().describe('URL or absolute local path to the file to upload. For local files this is the primary mode; for URLs, this re-hosts the file on Kolbo CDN for stability.'),
|
|
17
|
+
description: z.string().optional().describe('Optional description / caption for the uploaded media')
|
|
17
18
|
},
|
|
18
19
|
async ({ source, description }) => {
|
|
19
20
|
if (!source) throw new Error('source is required (URL or absolute local path)');
|
|
@@ -46,10 +47,10 @@ function registerMediaTools(server, client) {
|
|
|
46
47
|
'list_media',
|
|
47
48
|
'List the user\'s uploaded media from their Kolbo media library. Supports filtering by type (image / video / audio) and pagination. Returns items with stable URLs, names, sizes, and upload timestamps. Use this to discover what the user has previously uploaded before deciding whether to create new content.',
|
|
48
49
|
{
|
|
49
|
-
type:
|
|
50
|
-
page:
|
|
51
|
-
page_size:
|
|
52
|
-
search:
|
|
50
|
+
type: z.string().optional().describe('Filter by type: "image" | "video" | "audio". Omit for all types.'),
|
|
51
|
+
page: z.number().optional().describe('Page number (1-indexed). Default: 1'),
|
|
52
|
+
page_size: z.number().optional().describe('Items per page. Default: 20, max 100'),
|
|
53
|
+
search: z.string().optional().describe('Optional full-text search term matched against media names and descriptions')
|
|
53
54
|
},
|
|
54
55
|
async ({ type, page, page_size, search }) => {
|
|
55
56
|
const params = new URLSearchParams();
|
package/src/tools/models.js
CHANGED
|
@@ -1,53 +1,52 @@
|
|
|
1
|
-
/* ⛔ BACKWARD COMPATIBILITY: Tool names and arg names below are a PUBLIC
|
|
2
|
-
* CONTRACT. Never rename, remove, or break an existing tool/arg — old cached
|
|
3
|
-
* `npx @kolbo/mcp` installs in the wild will break silently. Add new tools or
|
|
4
|
-
* new OPTIONAL args only. Full rules: ../index.js top-of-file and CLAUDE.md. */
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
'
|
|
38
|
-
|
|
39
|
-
{
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
module.exports = { registerModelTools };
|
|
1
|
+
/* ⛔ BACKWARD COMPATIBILITY: Tool names and arg names below are a PUBLIC
|
|
2
|
+
* CONTRACT. Never rename, remove, or break an existing tool/arg — old cached
|
|
3
|
+
* `npx @kolbo/mcp` installs in the wild will break silently. Add new tools or
|
|
4
|
+
* new OPTIONAL args only. Full rules: ../index.js top-of-file and CLAUDE.md. */
|
|
5
|
+
|
|
6
|
+
const { z } = require('zod');
|
|
7
|
+
|
|
8
|
+
function registerModelTools(server, client) {
|
|
9
|
+
// ─── list_models ───────────────────────────────────────────
|
|
10
|
+
server.tool(
|
|
11
|
+
'list_models',
|
|
12
|
+
'List available AI models on Kolbo. Filter by type to find models for a specific generation type.',
|
|
13
|
+
{
|
|
14
|
+
type: z.string().optional().describe('Filter by type: "image", "video", "video_from_image", "music", "speech", "sound". Omit for all models.')
|
|
15
|
+
},
|
|
16
|
+
async ({ type }) => {
|
|
17
|
+
const path = type ? `/v1/models?type=${encodeURIComponent(type)}` : '/v1/models';
|
|
18
|
+
const result = await client.get(path);
|
|
19
|
+
|
|
20
|
+
// Format for readability
|
|
21
|
+
const summary = result.models.map(m =>
|
|
22
|
+
`${m.identifier} (${m.name}) - ${m.credit} credits${m.recommended ? ' [RECOMMENDED]' : ''}${m.new_model ? ' [NEW]' : ''}`
|
|
23
|
+
).join('\n');
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
content: [{
|
|
27
|
+
type: 'text',
|
|
28
|
+
text: `Available models (${result.count}):\n\n${summary}\n\nUse the "identifier" value as the "model" parameter in generate tools.`
|
|
29
|
+
}]
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
// ─── check_credits ─────────────────────────────────────────
|
|
35
|
+
server.tool(
|
|
36
|
+
'check_credits',
|
|
37
|
+
'Check your remaining Kolbo credit balance.',
|
|
38
|
+
{},
|
|
39
|
+
async () => {
|
|
40
|
+
const result = await client.get('/v1/account/credits');
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
content: [{
|
|
44
|
+
type: 'text',
|
|
45
|
+
text: `Credit Balance:\n- Total: ${result.credits.total}\n- Plan credits: ${result.credits.plan_credits}\n- Credit pack: ${result.credits.credit_pack}\n- Redemption: ${result.credits.redemption}`
|
|
46
|
+
}]
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
module.exports = { registerModelTools };
|
package/src/tools/moodboards.js
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
* `npx @kolbo/mcp` installs in the wild will break silently. Add new tools or
|
|
4
4
|
* new OPTIONAL args only. Full rules: ../index.js top-of-file and CLAUDE.md. */
|
|
5
5
|
|
|
6
|
+
const { z } = require('zod');
|
|
7
|
+
|
|
6
8
|
function registerMoodboardTools(server, client) {
|
|
7
9
|
// ─── list_moodboards ───────────────────────────────────────
|
|
8
10
|
server.tool(
|
|
@@ -28,7 +30,7 @@ function registerMoodboardTools(server, client) {
|
|
|
28
30
|
'get_moodboard',
|
|
29
31
|
'Fetch a single moodboard by ID. Returns the full moodboard including master_prompt, style_guide, and all image URLs.',
|
|
30
32
|
{
|
|
31
|
-
moodboard_id:
|
|
33
|
+
moodboard_id: z.string().describe('The moodboard ID')
|
|
32
34
|
},
|
|
33
35
|
async ({ moodboard_id }) => {
|
|
34
36
|
const result = await client.get(`/v1/moodboards/${encodeURIComponent(moodboard_id)}`);
|
package/src/tools/presets.js
CHANGED
|
@@ -3,13 +3,15 @@
|
|
|
3
3
|
* `npx @kolbo/mcp` installs in the wild will break silently. Add new tools or
|
|
4
4
|
* new OPTIONAL args only. Full rules: ../index.js top-of-file and CLAUDE.md. */
|
|
5
5
|
|
|
6
|
+
const { z } = require('zod');
|
|
7
|
+
|
|
6
8
|
function registerPresetTools(server, client) {
|
|
7
9
|
// ─── list_presets ──────────────────────────────────────────
|
|
8
10
|
server.tool(
|
|
9
11
|
'list_presets',
|
|
10
12
|
'List generation presets across image, video, music, and text-to-video catalogs. Presets bundle a specific prompt template + style direction that can be passed to a generation tool via its `preset_id` arg for a one-shot creative direction. Filter by `type` to narrow to a specific catalog. Returns id, name, description, thumbnail, category, and (for music) audio preview URL.',
|
|
11
13
|
{
|
|
12
|
-
type:
|
|
14
|
+
type: z.string().optional().describe('Filter by catalog: "image" | "video" | "music" | "text_to_video". Omit for all.')
|
|
13
15
|
},
|
|
14
16
|
async ({ type }) => {
|
|
15
17
|
const params = new URLSearchParams();
|
package/src/tools/visual_dna.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* `npx @kolbo/mcp` installs in the wild will break silently. Add new tools or
|
|
4
4
|
* new OPTIONAL args only. Full rules: ../index.js top-of-file and CLAUDE.md. */
|
|
5
5
|
|
|
6
|
+
const { z } = require('zod');
|
|
6
7
|
const FormData = require('form-data');
|
|
7
8
|
const { resolveToBuffer: sharedResolveToBuffer, VISUAL_DNA_MAX_BYTES } = require('./_shared');
|
|
8
9
|
|
|
@@ -19,12 +20,12 @@ function registerVisualDnaTools(server, client) {
|
|
|
19
20
|
'create_visual_dna',
|
|
20
21
|
'Create a Visual DNA profile from reference media. Each item in images/video/audio can be a public URL or an absolute local file path. Max 4 images, 1 video, 1 audio. Files capped at 25MB each.',
|
|
21
22
|
{
|
|
22
|
-
name:
|
|
23
|
-
dna_type:
|
|
24
|
-
prompt_helper:
|
|
25
|
-
images:
|
|
26
|
-
video:
|
|
27
|
-
audio:
|
|
23
|
+
name: z.string().describe('Name of the Visual DNA profile'),
|
|
24
|
+
dna_type: z.string().optional().describe('Type: "character", "style", "product", "scene". Default: "character"'),
|
|
25
|
+
prompt_helper: z.string().optional().describe('Optional description/notes to guide DNA extraction'),
|
|
26
|
+
images: z.array(z.string()).optional().describe('Array of image sources (URLs or absolute local paths). Max 4.'),
|
|
27
|
+
video: z.string().optional().describe('Optional video source (URL or absolute local path)'),
|
|
28
|
+
audio: z.string().optional().describe('Optional audio source (URL or absolute local path)')
|
|
28
29
|
},
|
|
29
30
|
async ({ name, dna_type, prompt_helper, images, video, audio }) => {
|
|
30
31
|
if (!name || !name.trim()) {
|
|
@@ -96,7 +97,7 @@ function registerVisualDnaTools(server, client) {
|
|
|
96
97
|
'get_visual_dna',
|
|
97
98
|
'Fetch a single Visual DNA profile by ID. Returns the full profile including system_prompt and all reference images.',
|
|
98
99
|
{
|
|
99
|
-
visual_dna_id:
|
|
100
|
+
visual_dna_id: z.string().describe('The Visual DNA profile ID')
|
|
100
101
|
},
|
|
101
102
|
async ({ visual_dna_id }) => {
|
|
102
103
|
const result = await client.get(`/v1/visual-dna/${encodeURIComponent(visual_dna_id)}`);
|
|
@@ -114,7 +115,7 @@ function registerVisualDnaTools(server, client) {
|
|
|
114
115
|
'delete_visual_dna',
|
|
115
116
|
'Delete a Visual DNA profile by ID. Only the owner can delete.',
|
|
116
117
|
{
|
|
117
|
-
visual_dna_id:
|
|
118
|
+
visual_dna_id: z.string().describe('The Visual DNA profile ID to delete')
|
|
118
119
|
},
|
|
119
120
|
async ({ visual_dna_id }) => {
|
|
120
121
|
const result = await client.delete(`/v1/visual-dna/${encodeURIComponent(visual_dna_id)}`);
|