@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 +1 -1
- package/src/tools/_shared.js +16 -6
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,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
|
-
|
|
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
|
|
262
|
-
// back to URL-only
|
|
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
|
);
|