@kolbo/mcp 1.19.0 → 1.19.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.19.0",
3
+ "version": "1.19.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": {
@@ -106,11 +106,11 @@ function assertSafeUrl(rawUrl) {
106
106
  return u;
107
107
  }
108
108
 
109
- async function safeFetch(rawUrl) {
109
+ async function safeFetch(rawUrl, opts = {}) {
110
110
  let current = rawUrl;
111
111
  for (let i = 0; i <= MAX_REDIRECTS; i++) {
112
112
  assertSafeUrl(current);
113
- const res = await fetch(current, { redirect: 'manual' });
113
+ const res = await fetch(current, { redirect: 'manual', signal: opts.signal });
114
114
  if (res.status >= 300 && res.status < 400 && res.headers.get('location')) {
115
115
  const next = new URL(res.headers.get('location'), current).toString();
116
116
  current = next;
@@ -251,29 +251,44 @@ const projectIdField = z.string().optional().describe(
251
251
  // can never be base64-embedded even if mistakenly passed in;
252
252
  // - any fetch/decoding failure silently falls back to URL-only.
253
253
  const INLINE_IMG_MAX_COUNT = 4;
254
- const INLINE_IMG_MAX_BYTES = 8 * 1024 * 1024; // 8 MB per image
254
+ // Cap kept conservative on purpose: a base64 image rides inside the JSON-RPC
255
+ // tool result, and chat clients (claude.ai etc.) drop the WHOLE result if it's
256
+ // too large — which looks like "no image at all". Anything over the cap is left
257
+ // to the URL in the text payload (clients render a "Show Image" affordance from
258
+ // it), so a big image degrades to a click instead of vanishing.
259
+ const INLINE_IMG_MAX_BYTES = 1.5 * 1024 * 1024; // 1.5 MB per image
260
+ const INLINE_IMG_FETCH_TIMEOUT_MS = 8000; // never hang the tool response on a slow CDN
255
261
 
256
262
  async function inlineImageBlocks(urls, opts = {}) {
257
263
  if (!opts || !opts.enabled) return [];
258
264
  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;
265
+ // Fetch the (≤4) images in parallel — they're independent, the cap already
266
+ // bounds concurrency, and this sits on the connector response path right
267
+ // after generation. Order is preserved by map-then-filter; any failure (size,
268
+ // type, timeout, network) returns null and falls back to URL-only.
269
+ const blocks = await Promise.all(
270
+ urls.slice(0, INLINE_IMG_MAX_COUNT).map(async (url) => {
271
+ const controller = new AbortController();
272
+ const timer = setTimeout(() => controller.abort(), INLINE_IMG_FETCH_TIMEOUT_MS);
273
+ try {
274
+ if (typeof url !== 'string' || !isHttpUrl(url)) return null;
275
+ const res = await safeFetch(url, { signal: controller.signal });
276
+ if (!res.ok) return null;
277
+ const contentType = (res.headers.get('content-type') || '').split(';')[0].trim().toLowerCase();
278
+ if (!contentType.startsWith('image/')) return null; // never embed non-images
279
+ const declaredLen = Number(res.headers.get('content-length') || 0);
280
+ if (declaredLen && declaredLen > INLINE_IMG_MAX_BYTES) return null;
281
+ const ab = await res.arrayBuffer();
282
+ if (ab.byteLength > INLINE_IMG_MAX_BYTES) return null;
283
+ return { type: 'image', data: Buffer.from(ab).toString('base64'), mimeType: contentType };
284
+ } catch (_) {
285
+ return null;
286
+ } finally {
287
+ clearTimeout(timer);
288
+ }
289
+ })
290
+ );
291
+ return blocks.filter(Boolean);
277
292
  }
278
293
 
279
294
  module.exports = {