image-processor-mcp
v0.2.3
Published
MCP server for downloading, compressing, optimizing images, OCR text extraction, and batch processing with auto-generated reports. Supports WebP, AVIF, JPEG, PNG, and more.
Maintainers
Readme
Image Processor MCP — Complete Documentation
Version: 0.2.3
Package: image-processor-mcp
Author: Dhvanil Pansuriya
Table of Contents
- Overview
- Installation & Configuration
- Tool: download_image
- Tool: compress_image
- Tool: compress_directory
- Tool: sync_filenames
- Tool: extract_text
- Tool: get_acknowledgement
- Configuration Defaults
- Supported Formats
- Credits & License
Overview
An MCP (Model Context Protocol) server that provides 6 tools for image downloading, compression, batch processing, filename synchronization, OCR text extraction, and configuration discovery.
Quick Start
npx -y image-processor-mcpKey Features
- Download images from URLs or search by keyword (Pexels integration)
- Compress and convert single images with 10 output formats
- Recursive compression to target file size
- Lossless and lossy compression with configurable quality and CPU effort
- Auto-resize oversized images
- Batch process entire directories preserving folder structure
- Filename and directory name normalization
- Auto-generated markdown compression reports
- Auto-generated filename mappings paired with reports
- Scan project files and replace old filenames with new ones
- OCR text extraction (100+ languages, runs 100% locally)
Installation & Configuration
Install from npm
npm install image-processor-mcpRequirements
- Node.js 18 or higher
MCP Configuration
Claude Desktop
{
"mcpServers": {
"image-processor-mcp": {
"command": "npx",
"args": ["-y", "image-processor-mcp"],
"env": {
"IMAGE_SEARCH_API_KEY": "your_pexels_api_key_here"
}
}
}
}VS Code (Cline / MCP extensions)
{
"mcpServers": {
"image-processor-mcp": {
"command": "npx",
"args": ["-y", "image-processor-mcp"],
"env": {
"IMAGE_SEARCH_API_KEY": "your_pexels_api_key_here"
}
}
}
}
IMAGE_SEARCH_API_KEYis optional — only needed for keyword search indownload_image. Get a free key at https://www.pexels.com/api/key/.
1. download_image
Downloads an image from a URL or searches by keyword. Optionally compress/convert the image.
Modes
| Mode | Parameter | Description |
|------|-----------|-------------|
| URL mode | url | Direct download from an image URL |
| Search mode | query | Keyword search via Pexels |
If both provided, search mode takes priority.
Parameters
| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|
| url | string | no* | — | Direct image URL |
| query | string | no* | — | Search keyword |
| outputPath | string | yes | — | Save path |
| outputFormat | string | no | original | webp, avif, jpeg, png, tiff, gif |
| quality | number | no | 85 | 1–100 |
| lossless | boolean | no | false | Lossless mode |
| effort | number | no | 6 | CPU effort 0–6 |
| width | number | no | — | Target width |
| height | number | no | — | Target height |
| maxDimension | number | no | 2000 | Auto-resize threshold |
| recursiveCompress | boolean | no | false | Iterative size targeting |
| expectedSizeKB | number | no | 100 | Target KB |
| qualityStepDown | number | no | 10 | Per-iteration decrease |
| minimumQualityFloor | number | no | 10 | Minimum quality |
* One of url or query is required.
Data Flow
Request → type guard → Controller
├─ Search mode? → Pexels API → first hit URL
└─ URL mode? → use directly
↓
axios.get(url, arraybuffer, 30s timeout)
↓
Write to temp file → check compression params
├─ YES → ImageService.compressImage → clean temp → return result
└─ NO → move to final path → return successResponse Example (search + compression)
{
"query": "mountain sunset",
"imageId": 12345,
"width": 1920,
"height": 1080,
"imageSize": 0,
"user": "John Doe",
"tags": "mountain sunset",
"inputPath": "...",
"outputPath": "./images/mountain.webp",
"originalSize": 500000,
"compressedSize": 80000,
"compressionRatio": 84.00,
"success": true,
"iterations": 1,
"qualitySteps": [85],
"finalQuality": 85,
"exceededTarget": false
}Edge Cases
- No url or query: Error — must provide one
- Empty search results: Error — no images found
- Missing API key: Error —
IMAGE_SEARCH_API_KEYnot set - Download timeout (30s): Error — timeout exceeded
- Compression fails: Temp file cleaned up, error returned
2. compress_image
Compress or convert a single image with full control over quality, dimensions, format, and recursive compression.
Parameters
| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|
| inputPath | string | yes | — | Source image path |
| outputPath | string | yes | — | Destination path |
| outputFormat | string | no | webp | Output format |
| quality | number | no | 85 | 1–100 |
| lossless | boolean | no | false | Lossless mode |
| effort | number | no | 6 | CPU effort 0–6 |
| width | number | no | — | Target width |
| height | number | no | — | Target height |
| maxDimension | number | no | 2000 | Auto-resize threshold |
| recursiveCompress | boolean | no | false | Iterative targeting |
| expectedSizeKB | number | no | 100 | Target KB |
| qualityStepDown | number | no | 10 | Per-iteration decrease |
| minimumQualityFloor | number | no | 10 | Minimum quality |
Core Compression Algorithm
1. Read original size + image metadata (sharp)
2. Compute resize:
- Explicit w/h? → use those
- Exceeds maxDimension? → scale proportionally
- Otherwise → no resize
3. Loop:
- sharp pipeline → apply format with quality
- If input === output → buffer (safe in-place)
- Else → write to file
- If recursive + over target + above floor → reduce quality, loop
- Else → break
4. Return CompressionResultFormat Conversion
| Format | Sharp Options |
|--------|---------------|
| webp | {quality, lossless, effort, smartSubsample, alphaQuality:90} |
| avif | {quality, lossless, effort} |
| jpeg | {quality, mozjpeg:true} |
| png | {compressionLevel:9} |
| tiff | {quality, compression:'lzw'} |
| gif | gif() (no options) |
| other | Falls back to WebP |
Response Example
{
"inputPath": "./input/photo.jpg",
"outputPath": "./output/photo.webp",
"originalSize": 500000,
"compressedSize": 80000,
"compressionRatio": 84.00,
"success": true,
"iterations": 1,
"qualitySteps": [85],
"finalQuality": 85,
"exceededTarget": false
}Edge Cases
- In-place compression (same path): Buffer-based to avoid corruption
- Missing input file:
success: falsewith error - Unknown format: Falls back to WebP
- Recursion floor: Stops when quality can't decrease further
- Abort: Returns partial result with
success: false
3. compress_directory
Batch compress all images in a directory recursively, preserving folder structure. Generates report + mappings.
Parameters
| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|
| inputDir | string | yes | — | Source directory |
| outputDir | string | yes | — | Output directory |
| outputFormat | string | no | webp | Output format |
| quality | number | no | 85 | 1–100 |
| lossless | boolean | no | false | Lossless mode |
| effort | number | no | 6 | CPU effort 0–6 |
| maxDimension | number | no | 2000 | Auto-resize threshold |
| normalizeFilename | boolean | no | false | Clean filenames |
| filenameReplaceChars | string | no | , -, ., & | Replace chars |
| filenameCase | string | no | lowercase | Case conversion |
| normalizeDirname | boolean | no | false | Clean dir names |
| dirnameReplaceChars | string | no | , -, ., & | Dir replace chars |
| dirnameCase | string | no | lowercase | Dir case |
| recursiveCompress | boolean | no | false | Iterative targeting |
| expectedSizeKB | number | no | 100 | Target KB |
| qualityStepDown | number | no | 5 (schema) / 10 (code) | Per-iteration decrease |
| minimumQualityFloor | number | no | 10 | Minimum quality |
Data Flow
Request → type guard → Controller
├─ Validate input directory exists
├─ Apply defaults, build FileNamingOptions
├─ Scan all image files (recursive)
├─ Filter out SVG (restricted)
├─ For each file:
│ ├─ Compute output path (preserves structure, normalizes, changes ext)
│ └─ Run ImageService.compressImage
├─ Generate report (.image-processor-mcp/reports/)
└─ Generate mappings JSON (.image-processor-mcp/mappings/)Output Path Algorithm
relativePath = inputDir → inputFile difference
1. Dir naming enabled → normalize each directory component
2. File naming enabled → normalize basename
3. Replace extension with .{outputFormat}
4. Prepend outputDirnormalizeName
Replace each char → '_', collapse multiple '_', trim edges, apply caseGenerated Files
.image-processor-mcp/
├── reports/scan-report-{timestamp}.md
└── mappings/mappings-{timestamp}.jsonResponse Example
{
"success": true,
"totalFiles": 10,
"successful": 9,
"failed": 1,
"totalOriginalSize": 5000000,
"totalCompressedSize": 1200000,
"overallReduction": 76.00,
"durationMs": 5432,
"reportPath": ".image-processor-mcp/reports/scan-report-2026-05-29-15-30-00.md",
"mappingsPath": ".image-processor-mcp/mappings/mappings-2026-05-29-15-30-00.json"
}Edge Cases
- Input dir not found: Early error, no files written
- SVG files: Silently excluded
- Abort mid-batch: Partial results processed
- All files fail: No mappings file generated
- No images found: Empty report
4. sync_filenames
Scan project files and replace all references to old filenames with new filenames after compression.
Parameters
| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|
| scanDir | string | yes | — | Directory to scan |
| mappingFilePath | string | no | — | Path to mappings JSON file |
| mappings | array | no | — | Inline {oldFileName, newFileName}[] |
| includeExtensions | array | no | html,js,ts,css,md,json... | Extensions to scan |
| excludeDirectories | array | no | node_modules,.git,build,dist... | Dirs to skip |
| dryRun | boolean | no | true | Preview only |
Validation
When using mappingFilePath:
- File exists + valid JSON
- Mappings array present and non-empty
- Each entry has
oldFileNameandnewFileName(strings) - Timestamp extracted from filename
mappings-(.+).json - Matching report must exist:
reports/scan-report-{timestamp}.md
If the matching report is missing → operation rejected (prevents stale mappings).
Data Flow
Request → type guard → Controller
├─ mappingFilePath?
│ YES → ReferenceService.readMappingFile → validate against report
│ NO → use inline mappings
├─ dryRun = args.dryRun !== false
├─ ReferenceService.scanReferences:
│ ├─ Collect files recursively (filtered by extension, excluding dirs)
│ ├─ For each file:
│ │ ├─ For each mapping:
│ │ │ ├─ Regex-escape oldFileName, count occurrences
│ │ │ ├─ Build line-by-line details
│ │ │ ├─ Build preview (3 lines context)
│ │ │ └─ If !dryRun: replace in content
│ │ └─ If modified: write file
│ └─ Return UpdateRefResult
└─ ReportService.generateSyncReport → .image-processor-mcp/syncs/Generated Files
.image-processor-mcp/syncs/sync-report-{timestamp}.md (live)
.image-processor-mcp/syncs/dry-run/sync-report-{timestamp}.md (dry-run)Response Example
Dry run — no files were modified.
Mapping source: .image-processor-mcp/mappings/mappings-2026-05-29-15-30-00.json
Files scanned: 42
Files with matches: 3
Total replacements: 12
--- Matches ---
[src/components/Header.tsx]
"logo.png" → "logo.webp" (5x)
Preview: ... context lines ...
Line 15:23 "logo.png" → "logo.webp"
... and 3 more (see report)
Report saved to: .image-processor-mcp/syncs/dry-run/sync-report-2026-05-29-15-31-00.mdEdge Cases
- Mapping file not found: Error with path
- Invalid JSON: Parse error
- No matching report: Detailed rejection message
- Neither source provided: Error explaining options
- No matches found: Informational message
- Regex special chars: Properly escaped
- Binary files: Filtered by extension list
5. extract_text
Extract visible printed text from images using OCR (tesseract.js). Runs entirely locally.
Parameters
| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|
| imagePath | string | yes | — | Path to image file |
| language | string | no | eng | Language code (use + for multiple) |
| rectangle | object | no | — | Region {top, left, width, height} |
| outputFormat | enum | no | text | text, blocks, hocr, tsv |
| preprocess | boolean | no | true | Grayscale + auto-rotate |
Data Flow
Request → type guard → OcrController → OcrService.recognize
├─ Validate path (exists, not dir, not empty, not SVG)
├─ Validate rectangle (positive dimensions)
├─ Build sharp pipeline:
│ ├─ Convert unsupported formats to PNG
│ └─ Preprocess: grayscale + rotate
├─ runOcr:
│ ├─ Serialized via jobQueue (one at a time)
│ ├─ getWorker: reuse / reinitialize / create
│ ├─ worker.recognize()
│ └─ Recycle worker every 500 jobs
└─ Return ExtractTextResultWorker Lifecycle
| State | Action |
|-------|--------|
| Worker exists, same language | Reuse directly |
| Worker exists, different language | worker.reinitialize(language) |
| No worker | createWorker(language, 1, ...) |
| After 500 jobs | setImmediate(recycleWorker) |
| Worker error | errorHandler nulls out reference |
| SIGINT / transport close | shutdown() → terminate |
Supported Input Formats
| Natively supported | Auto-converted to PNG | |-------------------|----------------------| | bmp, jpg, jpeg, png | tiff, heic, heif, webp*, gif* | | pbm, pgm, ppm | and all others |
* webp and gif ARE natively supported by tesseract.js, despite the conversion check. The code converts any extension NOT in OCR_SUPPORTED_EXTENSIONS. Since webp and gif are in that list, they are NOT converted.
Limitations
- Handwritten text: NOT supported
- SVG files: Rejected (vector format)
- Empty images: Return empty string (not an error)
- First run: Downloads language data (~2MB) from CDN
Response Example (text format)
{
"success": true,
"text": "Extracted text content",
"confidence": 92.5,
"words": 42,
"language": "eng",
"durationMs": 1234,
"preprocessed": true
}Edge Cases
- SVG: Explicit rejection
- Unsupported format: Auto-converted to PNG
- Worker crash: Auto-recovery (errorHandler nulls out)
- Abort: DOMException caught → cancelled result
- Empty file (0 bytes): Explicit rejection
- Path is directory: Explicit rejection
6. get_acknowledgement
Returns comprehensive documentation of all tools, defaults, formats, and environment variables.
Parameters
None — empty input schema.
Response
A full markdown document built dynamically from all configuration constants:
| Section | Source |
|---------|--------|
| All 6 tools | Hardcoded descriptions |
| Configuration defaults | DEFAULTS + CONFIG_DESCRIPTIONS |
| Supported input formats | SUPPORTED_EXTENSIONS + INPUT_EXTENSION_DESCRIPTIONS |
| Supported output formats | SUPPORTED_OUTPUT_FORMATS + OUTPUT_FORMAT_DESCRIPTIONS |
| Sync scan defaults | DEFAULT_EXTENSIONS + DEFAULT_EXCLUDE_DIRS |
| Restricted extensions | RESTRICTED_EXTENSIONS |
| Environment variables | Hardcoded |
| Credits | Hardcoded |
Implementation
ToolController.handleGetAcknowledgement()
// Synchronous — no abort signal, no async
// Returns { content: [{ type: 'text', text: markdown }] }Edge Cases
None — always succeeds, no parameters, no I/O.
Configuration Defaults
| Parameter | Default | Description |
|-----------|---------|-------------|
| quality | 85 | Compression quality for lossy formats (1–100) |
| lossless | false | Lossless compression (WebP, PNG, GIF, AVIF) |
| effort | 6 | CPU effort (0–6). Higher = smaller files |
| maxDimension | 2000 | Auto-resize threshold in pixels |
| expectedSizeKB | 100 | Target file size for recursive compression |
| qualityStepDown | 10 | Quality decrease per recursive iteration |
| minimumQualityFloor | 10 | Minimum quality (1–100) during recursion |
| outputFormat | webp | Default output format |
| filenameCase | lowercase | Filename case conversion |
| dirnameCase | lowercase | Directory name case conversion |
| filenameReplaceChars | [' ', '-', '.', '&'] | Characters replaced with _ in filenames |
| dirnameReplaceChars | [' ', '-', '.', '&'] | Characters replaced with _ in dir names |
Supported Formats
Input Formats (35+)
.jpg, .jpeg, .png, .gif, .bmp, .tiff, .tif, .webp, .avif, .svg, .heic, .heif, .ico, .cur, .jp2, .j2k, .jpf, .jpx, .jpm, .mj2, .jxr, .wdp, .hdp, .psd, .psb, .dds, .tga, .vda, .icb, .vst, .pbm, .pgm, .ppm, .pnm, .pfm, .exr, .hdr, .hrd, .pic, .pict, .pct, .xbm, .xpm, .wal, .cut, .ras, .sun, .sgi, .rgb, .rgba, .bw, .pcx, .pcd, .iff, .lbm
Output Formats (10)
| Format | Description |
|--------|-------------|
| webp | Google WebP. Excellent lossy/lossless. Supports transparency. |
| avif | AV1-based. 30–50% smaller than JPEG. Slower encoding. |
| jpeg | Universal compatibility. No transparency. |
| png | Lossless with transparency. Best for graphics/logos. |
| tiff | High-quality. Print/publishing. Largest sizes. |
| gif | Animation + transparency. 256 colors. |
| heif | HEVC-based. Apple ecosystem. |
| jp2 | JPEG 2000. Wavelet-based. Medical/ cinema. |
| jxl | JPEG XL. Next-gen. Lossless JPEG re-encoding. |
| pdf | Convert images to PDF pages. |
Restricted Extensions
.svg — SVG files are skipped during batch compression (vector format — sharp cannot safely compress without breaking rendering).
Credits & License
Author
Dhvanil Pansuriya
| | |
|---|---|
| GitHub | https://github.com/pansuriyadhvanil/ |
| LinkedIn | https://linkedin.com/in/dhvanil-pansuriya/ |
| Package | image-processor-mcp on npm |
Technology Credits
| Technology | Role | |------------|------| | Model Context Protocol SDK | MCP server framework | | sharp | High-performance image processing | | tesseract.js | OCR text extraction — 100% local, no API key | | Pexels | Free stock photo API for image search | | axios | HTTP client | | fs-extra | Extended file system |
License
MIT License — See LICENSE for details.
