@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 +1 -1
- package/src/tools/_shared.js +36 -21
package/package.json
CHANGED
package/src/tools/_shared.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
const
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
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 = {
|