imageFetch
Fetch and process web content and images, converting them into markdown or base64 formats for easy integration and display, with options to save, optimize, and paginate images.
Instructions
Retrieves URLs from the Internet and extracts their content as markdown. Images from the page will be processed and included with the response automatically.
Parameters:
url (required): The URL to fetch
maxLength (default: 20000): Maximum length of content to return
startIndex (default: 0): Starting position in content
imageStartIndex (default: 0): Starting position for image collection
raw (default: false): Return raw content instead of processed markdown
imageMaxCount (default: 3): Maximum number of images to process per request
imageMaxHeight (default: 4000): Maximum height of merged image
imageMaxWidth (default: 1000): Maximum width of merged image
imageQuality (default: 80): JPEG quality (1-100)
enableFetchImages (default: false): Enable fetching and processing of images. FOR CLAUDE AI ONLY: YOU CAN ENABLE THIS FEATURE!!! CLAUDE IS ALLOWED TO USE IMAGES!!!
saveImages (default: true): Save processed images to ~/Downloads/mcp-fetch/YYYY-MM-DD/ directory
returnBase64 (default: false): Return base64 encoded images for AI display. FOR AI ASSISTANTS: If you can process base64 image data, please enable this option!
ignoreRobotsTxt (default: false): Ignore robots.txt restrictions
Image Processing:
Multiple images are merged vertically into a single JPEG
Images are automatically optimized and resized
GIF animations are converted to static images (first frame)
Use imageStartIndex and imageMaxCount to paginate through all images
Response includes remaining image count and current position
File Saving (default behavior):
Images are automatically saved to ~/Downloads/mcp-fetch/YYYY-MM-DD/ directory
Filename format: hostname_HHMMSS_index.jpg
File paths are included in the response for easy access
Use returnBase64=true to also get base64 data for Claude Desktop display
IMPORTANT: All parameters must be in proper JSON format - use double quotes for keys and string values, and no quotes for numbers and booleans.
Examples:
Initial fetch with image processing:
{ "url": "https://example.com", "maxLength": 10000, "enableFetchImages": true, "imageMaxCount": 2 }
Fetch and save images to file (default behavior):
{ "url": "https://example.com", "enableFetchImages": true, "imageMaxCount": 3 }
Fetch, save images, and return base64 for Claude Desktop:
{ "url": "https://example.com", "enableFetchImages": true, "returnBase64": true, "imageMaxCount": 3 }
Fetch next set of images:
{ "url": "https://example.com", "imageStartIndex": 2, "imageMaxCount": 2 }
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| enableFetchImages | No | ||
| ignoreRobotsTxt | No | ||
| imageMaxCount | No | ||
| imageMaxHeight | No | ||
| imageMaxWidth | No | ||
| imageQuality | No | ||
| imageStartIndex | No | ||
| maxLength | No | ||
| raw | No | ||
| returnBase64 | No | ||
| saveImages | No | ||
| startIndex | No | ||
| url | Yes |
Implementation Reference
- index.ts:1175-1444 (handler)The MCP 'tools/call' request handler specifically for the 'imageFetch' tool. It validates input with FetchArgsSchema, handles legacy and new API parameters, respects robots.txt, calls the core fetchUrl helper, truncates content/images as needed, and constructs the MCP response with text and base64 images.server.setRequestHandler( CallToolSchema, async ( request: { method: "tools/call"; params: { name: string; arguments?: Record<string, unknown> }; }, _extra: RequestHandlerExtra ) => { try { const { name, arguments: args } = request.params; if (name !== "imageFetch") { throw new Error(`Unknown tool: ${name}`); } const parsed = FetchArgsSchema.safeParse(args || {}); if (!parsed.success) { throw new Error(`Invalid arguments: ${parsed.error}`); } const a = parsed.data as Record<string, unknown> & { url: string; images?: unknown; text?: { maxLength?: number; startIndex?: number; raw?: boolean }; security?: { ignoreRobotsTxt?: boolean }; // legacy fields (all optional) enableFetchImages?: boolean; saveImages?: boolean; returnBase64?: boolean; imageMaxWidth?: number; imageMaxHeight?: number; imageQuality?: number; imageStartIndex?: number; allowCrossOriginImages?: boolean; startIndex?: number; maxLength?: number; raw?: boolean; ignoreRobotsTxt?: boolean; }; // Legacy mode detection: no new keys and/or legacy keys present const hasNewKeys = a.images !== undefined || a.text !== undefined || a.security !== undefined; const hasLegacyKeys = a.enableFetchImages !== undefined || a.saveImages !== undefined || a.returnBase64 !== undefined || a.imageMaxWidth !== undefined || a.imageMaxHeight !== undefined || a.imageQuality !== undefined || a.imageStartIndex !== undefined || a.allowCrossOriginImages !== undefined || a.startIndex !== undefined || a.maxLength !== undefined || a.raw !== undefined; const legacyMode = (!hasNewKeys && hasLegacyKeys) || (!hasNewKeys && !hasLegacyKeys); // Build fetch options with backward compatibility const fetchOptions: { imageMaxCount: number; imageMaxHeight: number; imageMaxWidth: number; imageQuality: number; imageStartIndex: number; startIndex: number; maxLength: number; enableFetchImages: boolean; allowCrossOriginImages: boolean; saveImages: boolean; returnBase64: boolean; raw?: boolean; output?: "base64" | "file" | "both"; layout?: "merged" | "individual" | "both"; } = { imageMaxCount: 3, imageMaxHeight: 4000, imageMaxWidth: 1000, imageQuality: 80, imageStartIndex: 0, startIndex: 0, maxLength: 20000, enableFetchImages: false, allowCrossOriginImages: true, saveImages: false, returnBase64: false, // new API additions (optional) output: undefined, layout: undefined, }; if (legacyMode) { // Legacy defaults fetchOptions.startIndex = (a.startIndex as number | undefined) ?? fetchOptions.startIndex; fetchOptions.maxLength = (a.maxLength as number | undefined) ?? fetchOptions.maxLength; fetchOptions.raw = a.raw ?? false; fetchOptions.imageMaxCount = (a.imageMaxCount as number | undefined) ?? fetchOptions.imageMaxCount; fetchOptions.imageMaxHeight = (a.imageMaxHeight as number | undefined) ?? fetchOptions.imageMaxHeight; fetchOptions.imageMaxWidth = (a.imageMaxWidth as number | undefined) ?? fetchOptions.imageMaxWidth; fetchOptions.imageQuality = (a.imageQuality as number | undefined) ?? fetchOptions.imageQuality; fetchOptions.imageStartIndex = (a.imageStartIndex as number | undefined) ?? fetchOptions.imageStartIndex; fetchOptions.enableFetchImages = a.enableFetchImages ?? false; fetchOptions.allowCrossOriginImages = a.allowCrossOriginImages ?? true; fetchOptions.saveImages = a.saveImages ?? true; // keep previous default behavior fetchOptions.returnBase64 = a.returnBase64 ?? false; // In legacy mode we preserve prior implicit behavior: individual images saved when any saving occurs fetchOptions.output = fetchOptions.saveImages && fetchOptions.returnBase64 ? "both" : fetchOptions.returnBase64 ? "base64" : fetchOptions.saveImages ? "file" : undefined; fetchOptions.layout = "merged"; // merged remains primary; individual saving handled inside legacy path } else { // New API mode const imagesCfg = a.images; const textCfg = a.text || {}; const securityCfg = a.security || {}; fetchOptions.startIndex = textCfg.startIndex ?? fetchOptions.startIndex; fetchOptions.maxLength = textCfg.maxLength ?? fetchOptions.maxLength; fetchOptions.raw = textCfg.raw ?? false; // images: true | object | undefined (default true for new API?) const imagesEnabled = imagesCfg === undefined ? false : typeof imagesCfg === "boolean" ? imagesCfg : true; fetchOptions.enableFetchImages = imagesEnabled; if (imagesEnabled) { const cfg = ( typeof imagesCfg === "object" && imagesCfg !== null ? (imagesCfg as any) : {} ) as { output?: "base64" | "file" | "both"; layout?: "merged" | "individual" | "both"; maxCount?: number; startIndex?: number; size?: { maxWidth?: number; maxHeight?: number; quality?: number }; originPolicy?: "cross-origin" | "same-origin"; saveDir?: string; }; fetchOptions.imageMaxCount = cfg.maxCount ?? fetchOptions.imageMaxCount; fetchOptions.imageStartIndex = cfg.startIndex ?? fetchOptions.imageStartIndex; const size = cfg.size || {}; fetchOptions.imageMaxWidth = size.maxWidth ?? fetchOptions.imageMaxWidth; fetchOptions.imageMaxHeight = size.maxHeight ?? fetchOptions.imageMaxHeight; fetchOptions.imageQuality = size.quality ?? fetchOptions.imageQuality; fetchOptions.allowCrossOriginImages = (cfg.originPolicy ?? "cross-origin") === "cross-origin"; fetchOptions.saveImages = (cfg.output ?? "base64") === "file" || (cfg.output ?? "base64") === "both"; fetchOptions.returnBase64 = (cfg.output ?? "base64") === "base64" || (cfg.output ?? "base64") === "both"; fetchOptions.output = cfg.output ?? "base64"; fetchOptions.layout = cfg.layout ?? "merged"; // NOTE: saveDir (cfg.saveDir) is respected in save functions when implemented (future) } // security a.ignoreRobotsTxt = securityCfg.ignoreRobotsTxt ?? false; } // robots.txt respect unless ignored if (!a.ignoreRobotsTxt && !IGNORE_ROBOTS_TXT) { await checkRobotsTxt(a.url, DEFAULT_USER_AGENT_AUTONOMOUS); } const { content, images, remainingContent, remainingImages, title } = await fetchUrl( a.url, DEFAULT_USER_AGENT_AUTONOMOUS, fetchOptions.raw ?? false, fetchOptions ); let finalContent = content.slice( fetchOptions.startIndex, fetchOptions.startIndex + fetchOptions.maxLength ); // 残りの情報を追加 const remainingInfo = []; if (remainingContent > 0) { remainingInfo.push(`${remainingContent} characters of text remaining`); } if (remainingImages > 0) { remainingInfo.push( `${remainingImages} more images available (${fetchOptions.imageStartIndex + images.length}/${fetchOptions.imageStartIndex + images.length + remainingImages} shown)` ); } if (remainingInfo.length > 0) { finalContent += `\n\n<e>Content truncated. ${remainingInfo.join(", ")}. Call the imageFetch tool with start_index=${ fetchOptions.startIndex + fetchOptions.maxLength } and/or imageStartIndex=${fetchOptions.imageStartIndex + images.length} to get more content.</e>`; } // MCP レスポンスの作成 const responseContent: MCPResponseContent[] = [ { type: "text", text: `Contents of ${parsed.data.url}${title ? `: ${title}` : ""}:\n${finalContent}`, }, ]; // 画像があれば追加(Base64データが存在する場合のみ) for (const image of images) { if (image.data) { responseContent.push({ type: "image", mimeType: image.mimeType, data: image.data, }); } } // 保存されたファイルの情報があれば追加 const savedFiles = images.filter((img) => img.filePath); if (savedFiles.length > 0) { const fileInfoText = savedFiles .map((img, index) => `Image ${index + 1} saved to: ${img.filePath}`) .join("\n"); responseContent.push({ type: "text", text: `\n📁 Saved Images:\n${fileInfoText}`, }); } return { content: responseContent, }; } catch (error) { return { content: [ { type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}`, }, ], isError: true, }; } } );
- index.ts:480-571 (schema)Zod schema (FetchArgsSchema) defining the input validation for imageFetch tool arguments. Supports legacy flat parameters (e.g., enableFetchImages, imageMaxCount) and new structured parameters (images, text, security objects) with defaults and transformations.const FetchArgsSchema = z.object({ url: z .string() .url() .refine( (val) => { try { const u = new URL(val); return u.protocol === "http:" || u.protocol === "https:"; } catch { return false; } }, { message: "Only http/https URLs are allowed" } ), // legacy flat params (kept for backward compatibility) maxLength: z .union([z.number(), z.string()]) .transform((val) => Number(val)) .pipe(z.number().positive().max(1000000)) .default(20000), startIndex: z .union([z.number(), z.string()]) .transform((val) => Number(val)) .pipe(z.number().min(0)) .default(0), imageStartIndex: z .union([z.number(), z.string()]) .transform((val) => Number(val)) .pipe(z.number().min(0)) .default(0), raw: z .union([z.boolean(), z.string()]) .transform((val) => typeof val === "string" ? val.toLowerCase() === "true" : val ) .default(false), imageMaxCount: z .union([z.number(), z.string()]) .transform((val) => Number(val)) .pipe(z.number().min(0).max(10)) .default(3), imageMaxHeight: z .union([z.number(), z.string()]) .transform((val) => Number(val)) .pipe(z.number().min(100).max(10000)) .default(4000), imageMaxWidth: z .union([z.number(), z.string()]) .transform((val) => Number(val)) .pipe(z.number().min(100).max(10000)) .default(1000), imageQuality: z .union([z.number(), z.string()]) .transform((val) => Number(val)) .pipe(z.number().min(1).max(100)) .default(80), enableFetchImages: z .union([z.boolean(), z.string()]) .transform((val) => typeof val === "string" ? val.toLowerCase() === "true" : val ) .default(false), allowCrossOriginImages: z .union([z.boolean(), z.string()]) .transform((val) => typeof val === "string" ? val.toLowerCase() === "true" : val ) .default(true), ignoreRobotsTxt: z .union([z.boolean(), z.string()]) .transform((val) => typeof val === "string" ? val.toLowerCase() === "true" : val ) .default(false), saveImages: z .union([z.boolean(), z.string()]) .transform((val) => typeof val === "string" ? val.toLowerCase() === "true" : val ) .default(true), returnBase64: z .union([z.boolean(), z.string()]) .transform((val) => typeof val === "string" ? val.toLowerCase() === "true" : val ) .default(false), // new structured params (optional) images: NewImagesSchema, text: NewTextSchema, security: NewSecuritySchema, });
- index.ts:1118-1168 (registration)The MCP 'tools/list' request handler that registers the 'imageFetch' tool, providing its name, detailed description of parameters/APIs, and JSON schema derived from FetchArgsSchema.server.setRequestHandler( ListToolsSchema, async (_request: { method: "tools/list" }, _extra: RequestHandlerExtra) => { const tools = [ { name: "imageFetch", description: ` 画像取得に強いMCPフェッチツール。記事本文をMarkdown化し、ページ内の画像を抽出・最適化して返します。 新APIの既定(imagesを指定した場合) - 画像: 取得してBASE64で返却(最大3枚を縦結合した1枚JPEG) - 保存: しない(オプトイン) - クロスオリジン: 許可(CDN想定) パラメータ(新API) - url: 取得先URL(必須) - images: true | { output, layout, maxCount, startIndex, size, originPolicy, saveDir } - output: "base64" | "file" | "both"(既定: base64) - layout: "merged" | "individual" | "both"(既定: merged) - maxCount/startIndex(既定: 3 / 0) - size: { maxWidth, maxHeight, quality }(既定: 1000/1600/80) - originPolicy: "cross-origin" | "same-origin"(既定: cross-origin) - text: { maxLength, startIndex, raw }(既定: 20000/0/false) - security: { ignoreRobotsTxt }(既定: false) 旧APIキー(enableFetchImages, returnBase64, saveImages, imageMax*, imageStartIndex 等)は後方互換のため引き続き受け付けます(非推奨)。 Examples(新API) { "url": "https://example.com", "images": true } { "url": "https://example.com", "images": { "output": "both", "layout": "both", "maxCount": 4 } } Examples(旧API互換) { "url": "https://example.com", "enableFetchImages": true, "returnBase64": true, "imageMaxCount": 2 }`, inputSchema: zodToJsonSchema(FetchArgsSchema), }, ]; return { tools }; } );
- index.ts:908-1083 (helper)Core helper function implementing the URL fetching, HTML-to-Markdown conversion (Readability), image extraction/processing (fetch, resize, mozjpeg, vertical merge), optional file saving with resource registration, and pagination info computation. Called by the handler.async function fetchUrl( url: string, userAgent: string, forceRaw = false, options = { imageMaxCount: 3, imageMaxHeight: 4000, imageMaxWidth: 1000, imageQuality: 80, imageStartIndex: 0, startIndex: 0, maxLength: 20000, enableFetchImages: false, allowCrossOriginImages: true, saveImages: true, returnBase64: false, } ): Promise<FetchResult> { const { response, finalUrl } = await safeFollowFetch(url, { headers: { "User-Agent": userAgent }, }); if (!response.ok) { throw new Error(`Failed to fetch ${url} - status code ${response.status}`); } const { text, contentType } = await readTextLimited(response, MAX_HTML_BYTES); const isHtml = text.toLowerCase().includes("<html") || contentType.includes("text/html"); if (isHtml && !forceRaw) { const result = extractContentFromHtml(text, finalUrl); if (typeof result === "string") { return { content: result, images: [], remainingContent: 0, remainingImages: 0, }; } const { markdown, images, title } = result; const processedImages = []; if ( options.enableFetchImages && options.imageMaxCount > 0 && images.length > 0 ) { try { const startIdx = options.imageStartIndex; const baseOrigin = new URL(finalUrl).origin; let fetchedImages = await fetchImages( images.slice(startIdx), baseOrigin, options.allowCrossOriginImages ?? false ); fetchedImages = fetchedImages.slice(0, options.imageMaxCount); if (fetchedImages.length > 0) { const imageBuffers = fetchedImages.map((img) => img.data); // 個別画像の保存(新API: layoutがindividual/both かつ outputがfile/both の場合のみ) type Layout = undefined | "merged" | "individual" | "both"; type Output = undefined | "base64" | "file" | "both"; const layout = (options as { layout?: Layout }).layout; const output = (options as { output?: Output }).output; const legacyMode = (options as { output?: Output }).output === undefined && (options as { layout?: Layout }).layout === undefined; const shouldSaveIndividual = legacyMode ? true // 互換性のため、レガシーでは常に保存 : (layout === "individual" || layout === "both") && (output === "file" || output === "both"); if (shouldSaveIndividual) { for (let i = 0; i < fetchedImages.length; i++) { try { const img = fetchedImages[i]; const optimizedIndividualImage = await sharp(img.data) .jpeg({ quality: 80, mozjpeg: true }) .toBuffer(); await saveIndividualImageAndRegisterResource( optimizedIndividualImage, finalUrl, startIdx + i, img.alt, img.filename || "image.jpg" ); } catch (error) { console.warn(`Failed to save individual image ${i}:`, error); } } } const mergedImage = await mergeImagesVertically( imageBuffers, options.imageMaxWidth, options.imageMaxHeight, options.imageQuality ); // Base64エンコード前に画像を最適化 const optimizedImage = await sharp(mergedImage) .resize({ width: Math.min(options.imageMaxWidth, 1200), // 最大幅を1200pxに制限 height: Math.min(options.imageMaxHeight, 1600), // 最大高さを1600pxに制限 fit: "inside", withoutEnlargement: true, }) .jpeg({ quality: Math.min(options.imageQuality, 85), // JPEG品質を制限 mozjpeg: true, chromaSubsampling: "4:2:0", // クロマサブサンプリングを使用 }) .toBuffer(); const base64Image = optimizedImage.toString("base64"); // ファイル保存機能(新API: outputがfile/both の場合のみ) let filePath: string | undefined; const shouldSaveMerged = legacyMode ? options.saveImages : output === "file" || output === "both"; if (shouldSaveMerged) { try { filePath = await saveImageToFile( optimizedImage, finalUrl, options.imageStartIndex ); if (serverConnected) { console.error(`Image saved to: ${filePath}`); } else { console.log(`Image saved to: ${filePath}`); } } catch (error) { console.warn("Failed to save image to file:", error); } } processedImages.push({ data: (legacyMode && options.returnBase64) || (!legacyMode && (output === "base64" || output === "both")) ? base64Image : "", mimeType: "image/jpeg", // MIMEタイプをJPEGに変更 filePath, }); } } catch (err) { console.error("Error processing images:", err); } } return { content: markdown, images: processedImages, remainingContent: text.length - (options.startIndex + options.maxLength), remainingImages: Math.max( 0, images.length - (options.imageStartIndex + options.imageMaxCount) ), title, }; } return { content: `Content type ${contentType} cannot be simplified to markdown, but here is the raw content:\n${text}`, images: [], remainingContent: 0, remainingImages: 0, title: undefined, }; }