web.fetch
Fetch web content from any URL with configurable size and time limits, and built-in anti-SSRF protection for safe retrieval.
Instructions
Fetch a URL with size/time limits and anti-SSRF.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| url | Yes | ||
| timeout | No | ||
| max_bytes | No | ||
| headers | No |
Implementation Reference
- src/tools/webFetch.ts:4-22 (handler)The core handler function that executes web.fetch logic: fetches a URL with limits, detects content type (text vs binary), and returns finalUrl/status/contentType/bodyText/bytesB64/fetchedAt.
export async function webFetch(url: string) { const res = await fetchWithLimits(url, CONFIG.fetchTimeoutMs, CONFIG.maxFetchBytes); if (!res || !res.body) { return { finalUrl: url, status: res?.status || 0, contentType: res?.contentType || 'application/octet-stream', bodyText: null, bytesB64: null, fetchedAt: new Date().toISOString() }; } const ct = (res.contentType || '').toLowerCase(); const isText = ct.startsWith('text/') || ct.includes('html') || ct.includes('xml') || ct.includes('json'); return { finalUrl: res.finalUrl || url, status: res.status, contentType: res.contentType, bodyText: isText ? res.body.toString('utf-8') : null, bytesB64: isText ? null : res.body.toString('base64'), fetchedAt: new Date().toISOString() }; } - src/utils/http.ts:40-79 (helper)Low-level HTTP fetcher with SSRF protection (private IP blocking via DNS lookup + CIDR matching), timeout abort, max byte cap, and redirect support. Used by webFetch handler.
export async function fetchWithLimits(urlStr: string, timeoutMs = CONFIG.fetchTimeoutMs, maxBytes = CONFIG.maxFetchBytes) { const u = new URL(urlStr); await assertNotPrivate(u); const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), timeoutMs); try { const res = await request(urlStr, { method: 'GET', headers: { 'user-agent': 'mcp-multitool/0.2', 'accept': '*/*' }, signal: controller.signal, maxRedirections: 3 }); const status = res.statusCode; const headersRec: Record<string, string> = {}; for (const [k, v] of Object.entries(res.headers)) { headersRec[k] = Array.isArray(v) ? v.join(', ') : String(v ?? ''); } if (status >= 400) { return { status, headers: headersRec, body: null as any }; } const chunks: Buffer[] = []; let total = 0; for await (const chunk of res.body) { const b: Buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk as any); total += b.length; if (total > maxBytes) break; chunks.push(b); } const buf = Buffer.concat(chunks); const contentType = headersRec['content-type'] || 'application/octet-stream'; const finalUrl = headersRec['content-location'] || urlStr; return { status, headers: headersRec, body: buf, finalUrl, contentType }; } finally { clearTimeout(timer); } } - src/utils/http.ts:23-38 (helper)SSRF protection: resolves DNS and blocks requests to private/reserved IP ranges (127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16, ::1/128, fc00::/7).
async function assertNotPrivate(url: URL) { const host = url.hostname; if (net.isIP(host)) { if (ipInRanges(host, BLOCKEDv4) || ipInRanges(host, BLOCKEDv6)) { throw new Error('Blocked private IP (SSRF)'); } return; } const addrs = await dns.lookup(host, { all: true }); for (const a of addrs) { if ((a.family === 4 && ipInRanges(a.address, BLOCKEDv4)) || (a.family === 6 && ipInRanges(a.address, BLOCKEDv6))) { throw new Error('Blocked private IP (SSRF)'); } } } - src/server.ts:65-70 (schema)Zod schema (webFetchShape) defining the input parameters for web.fetch: url (required string URL), timeout, max_bytes, and headers (optional).
const webFetchShape = { url: z.string().url(), timeout: z.number().int().optional(), max_bytes: z.number().int().optional(), headers: z.record(z.string()).optional() }; - src/server.ts:71-77 (registration)Registration of the 'web.fetch' tool with the McpServer, wiring the schema and the handler.
server.tool('web.fetch', 'Fetch a URL with size/time limits and anti-SSRF.', webFetchShape, OPEN, async ({ url, timeout, max_bytes }) => { const res = await webFetch(url); return { content: [{ type: 'text', text: JSON.stringify(res) }] }; } );