@kolbo/mcp 1.3.0 → 1.3.2

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.3.0",
3
+ "version": "1.3.2",
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/client.js CHANGED
@@ -1,130 +1,312 @@
1
- /**
2
- * Kolbo API HTTP client wrapper
3
- */
4
-
5
- /**
6
- * Structured error thrown when the Kolbo API returns a non-OK response.
7
- * Preserves the SDK's error code, HTTP status, and full response data so
8
- * MCP tools (and the LLM consuming them) can distinguish NOT_FOUND from
9
- * INSUFFICIENT_CREDITS from VALIDATION_ERROR etc.
10
- */
11
- class KolboApiError extends Error {
12
- constructor(message, { code, status, data } = {}) {
13
- super(message);
14
- this.name = 'KolboApiError';
15
- this.code = code || null;
16
- this.status = status || null;
17
- this.data = data || null;
18
- }
19
- }
20
-
21
- class KolboClient {
22
- constructor() {
23
- this.apiKey = process.env.KOLBO_API_KEY;
24
- this.baseUrl = (process.env.KOLBO_API_URL || 'https://api.kolbo.ai/api').replace(/\/$/, '');
25
-
26
- if (!this.apiKey) {
27
- throw new Error('KOLBO_API_KEY environment variable is required');
28
- }
29
- }
30
-
31
- async request(method, path, body = null) {
32
- const url = `${this.baseUrl}${path}`;
33
- const options = {
34
- method,
35
- headers: {
36
- 'X-API-Key': this.apiKey,
37
- 'Content-Type': 'application/json'
38
- }
39
- };
40
-
41
- if (body) {
42
- options.body = JSON.stringify(body);
43
- }
44
-
45
- const response = await fetch(url, options);
46
- let data;
47
- try {
48
- data = await response.json();
49
- } catch (_) {
50
- // Non-JSON body (gateway error, HTML etc.)
51
- throw new KolboApiError(`API error: ${response.status} ${response.statusText}`, {
52
- status: response.status,
53
- data: null
54
- });
55
- }
56
-
57
- if (!response.ok || data.success === false) {
58
- const message = data.error || data.message || `API error: ${response.status}`;
59
- const code = data.code || null;
60
- // Surface the code in the message so the LLM sees it even if it ignores the .code property
61
- const fullMessage = code ? `${message} [${code}]` : message;
62
- throw new KolboApiError(fullMessage, {
63
- code,
64
- status: response.status,
65
- data
66
- });
67
- }
68
-
69
- return data;
70
- }
71
-
72
- async post(path, body) {
73
- return this.request('POST', path, body);
74
- }
75
-
76
- async get(path) {
77
- return this.request('GET', path);
78
- }
79
-
80
- async delete(path) {
81
- return this.request('DELETE', path);
82
- }
83
-
84
- async postMultipart(path, formData) {
85
- const url = `${this.baseUrl}${path}`;
86
- const headers = {
87
- 'X-API-Key': this.apiKey,
88
- ...formData.getHeaders()
89
- };
90
-
91
- // form-data exposes getLengthSync for known-size parts; set Content-Length when available.
92
- try {
93
- const len = formData.getLengthSync();
94
- if (len) headers['Content-Length'] = String(len);
95
- } catch (_) { /* streaming length unavailable — let fetch handle it */ }
96
-
97
- const response = await fetch(url, {
98
- method: 'POST',
99
- headers,
100
- body: formData,
101
- duplex: 'half'
102
- });
103
-
104
- let data;
105
- try {
106
- data = await response.json();
107
- } catch (_) {
108
- throw new KolboApiError(`API error: ${response.status} ${response.statusText}`, {
109
- status: response.status,
110
- data: null
111
- });
112
- }
113
-
114
- if (!response.ok || data.success === false) {
115
- const message = data.error || data.message || `API error: ${response.status}`;
116
- const code = data.code || null;
117
- const fullMessage = code ? `${message} [${code}]` : message;
118
- throw new KolboApiError(fullMessage, {
119
- code,
120
- status: response.status,
121
- data
122
- });
123
- }
124
-
125
- return data;
126
- }
127
- }
128
-
129
- module.exports = KolboClient;
130
- module.exports.KolboApiError = KolboApiError;
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
+ // Serialize form-data to a Buffer before passing to fetch(). Node's
271
+ // built-in fetch (undici) can't consume legacy Node.js streams from
272
+ // the `form-data` package, causing "fetch failed" on local file uploads.
273
+ const body = formData.getBuffer();
274
+ headers['Content-Length'] = String(body.length);
275
+
276
+ const response = await fetch(url, {
277
+ method: 'POST',
278
+ headers,
279
+ body
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;
@@ -661,6 +661,7 @@ function registerGenerateTools(server, client) {
661
661
  text: JSON.stringify({
662
662
  text: result.result?.text || '',
663
663
  srt_url: result.result?.srt_url || null,
664
+ word_by_word_srt_url: result.result?.word_by_word_srt_url || null,
664
665
  txt_url: result.result?.txt_url || null,
665
666
  duration: result.result?.duration || null
666
667
  }, null, 2)