process_folder
Process all images in a folder by converting formats, resizing dimensions, or both operations simultaneously while automatically skipping non-image files.
Instructions
Apply a convert, resize, or convert-and-resize operation to all image files in a folder. Non-image files are automatically skipped.
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| folder_path | Yes | Absolute path to the folder containing images | |
| operation | Yes | Operation to apply to every image | |
| output_format | No | Target format (required for convert / convert_and_resize) | |
| quality | No | JPEG/WebP quality (1-100, default 90) | |
| width | No | Target width in pixels | |
| height | No | Target height in pixels | |
| preset | No | Named size preset (overrides width/height) | |
| lock_aspect_ratio | No | Keep aspect ratio when resizing (default true) | |
| output_folder | No | Where to save the processed files (defaults to the same folder as input) |
Implementation Reference
- index.js:325-437 (handler)The implementation of the 'process_folder' MCP tool. It iterates over files in a given directory, filters them to include only images, and performs the specified image transformation (convert, resize, or convert_and_resize) on each.
server.tool( "process_folder", "Apply a convert, resize, or convert-and-resize operation to all image files in a folder. Non-image files are automatically skipped.", { folder_path: z.string().describe("Absolute path to the folder containing images"), operation: z.enum(["convert", "resize", "convert_and_resize"]).describe("Operation to apply to every image"), output_format: z.enum(["png", "jpeg", "gif", "webp", "ico"]).optional().describe("Target format (required for convert / convert_and_resize)"), quality: z.number().int().min(1).max(100).optional().describe("JPEG/WebP quality (1-100, default 90)"), width: z.number().int().positive().optional().describe("Target width in pixels"), height: z.number().int().positive().optional().describe("Target height in pixels"), preset: z.enum([ "instagram-square", "instagram-portrait", "instagram-landscape", "twitter-post", "twitter-header", "full-hd", "4k", "youtube-thumbnail", "favicon", ]).optional().describe("Named size preset (overrides width/height)"), lock_aspect_ratio: z.boolean().optional().default(true).describe("Keep aspect ratio when resizing (default true)"), output_folder: z.string().optional().describe("Where to save the processed files (defaults to the same folder as input)"), }, async ({ folder_path, operation, output_format, quality = 90, width, height, preset, lock_aspect_ratio = true, output_folder }) => { try { const stat = await fs.stat(folder_path); if (!stat.isDirectory()) { return { isError: true, content: [{ type: "text", text: `Error: ${folder_path} is not a directory.` }] }; } const entries = await fs.readdir(folder_path); const imageFiles = entries.filter(isImageFile); if (imageFiles.length === 0) { return { content: [{ type: "text", text: JSON.stringify({ success: true, processed: 0, skipped: entries.length, results: [] }) }] }; } const outDir = output_folder ?? folder_path; if (output_folder) { await fs.mkdir(output_folder, { recursive: true }); } let targetW = width; let targetH = height; if (preset) { targetW = PRESETS[preset].width; targetH = PRESETS[preset].height; } const results = []; for (const filename of imageFiles) { const inputPath = path.join(folder_path, filename); try { let outPath; if (operation === "convert") { if (!output_format) throw new Error("output_format is required for convert operation"); const outExt = output_format === "jpeg" ? "jpg" : output_format; const base = path.basename(filename, path.extname(filename)); outPath = path.join(outDir, `${base}.${outExt}`); if (output_format === "ico") { const icoBuffer = await encodeIco(inputPath); await fs.writeFile(outPath, icoBuffer); } else { await sharp(inputPath).toFormat(output_format, { quality }).toFile(outPath); } } else if (operation === "resize") { if (!targetW && !targetH) throw new Error("Provide width, height, or a preset for resize operation"); const ext = path.extname(filename).slice(1).toLowerCase() || "jpg"; outPath = path.join(outDir, filename); const fit = lock_aspect_ratio ? "inside" : "fill"; await sharp(inputPath).resize(targetW, targetH, { fit }).toFile(outPath); } else { // convert_and_resize if (!output_format) throw new Error("output_format is required for convert_and_resize operation"); const outExt = output_format === "jpeg" ? "jpg" : output_format; const base = path.basename(filename, path.extname(filename)); outPath = path.join(outDir, `${base}.${outExt}`); if (output_format === "ico") { const icoBuffer = await encodeIco(inputPath); await fs.writeFile(outPath, icoBuffer); } else { let pipeline = sharp(inputPath); if (targetW || targetH) { const fit = lock_aspect_ratio ? "inside" : "fill"; pipeline = pipeline.resize(targetW, targetH, { fit }); } await pipeline.toFormat(output_format, { quality }).toFile(outPath); } } const outStat = await fs.stat(outPath); results.push({ file: filename, output_path: outPath, size_bytes: outStat.size, success: true }); } catch (fileErr) { results.push({ file: filename, success: false, error: fileErr.message }); } } const succeeded = results.filter(r => r.success).length; const failed = results.filter(r => !r.success).length; return { content: [{ type: "text", text: JSON.stringify({ success: true, processed: succeeded, failed, skipped: entries.length - imageFiles.length, results, }) }], }; } catch (err) { return { isError: true, content: [{ type: "text", text: `Error: ${err.message}` }] }; } } );