@kolbo/mcp 1.19.1 → 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.1",
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,20 +251,28 @@ 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
265
  // Fetch the (≤4) images in parallel — they're independent, the cap already
260
266
  // 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.
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.
263
269
  const blocks = await Promise.all(
264
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);
265
273
  try {
266
274
  if (typeof url !== 'string' || !isHttpUrl(url)) return null;
267
- const res = await safeFetch(url);
275
+ const res = await safeFetch(url, { signal: controller.signal });
268
276
  if (!res.ok) return null;
269
277
  const contentType = (res.headers.get('content-type') || '').split(';')[0].trim().toLowerCase();
270
278
  if (!contentType.startsWith('image/')) return null; // never embed non-images
@@ -275,6 +283,8 @@ async function inlineImageBlocks(urls, opts = {}) {
275
283
  return { type: 'image', data: Buffer.from(ab).toString('base64'), mimeType: contentType };
276
284
  } catch (_) {
277
285
  return null;
286
+ } finally {
287
+ clearTimeout(timer);
278
288
  }
279
289
  })
280
290
  );