Skip to main content
Glama
dutchbase

img-convert MCP Server

by dutchbase

img-convert

Fast CLI image converter with Web UI.

Convert images between:

  • JPG

  • PNG

  • WebP

  • AVIF

  • GIF

  • TIFF

Built for developers and AI agents.

Install

npx @dutchbase/img-convert

or

npm install -g @dutchbase/img-convert
# Convert a file
img-convert photo.jpg -f webp --json

# Inspect an image without converting
img-convert info photo.jpg

# Give Claude Code native image conversion tools
img-convert mcp

Fast, scriptable image conversion powered by Sharp. Ships as an npm package, a CLI, a REST API, and a Model Context Protocol (MCP) server — making it a first-class tool for both developers and AI agents.


Related MCP server: gimp-mcp

Table of Contents


Agent Skill

img-convert ships a SKILL.md that teaches coding agents the CLI's JSON contract, exit and error codes, flags, batch manifests and task recipes. It's included in the npm package and printed by img-convert skill.

# Claude Code, all projects
mkdir -p ~/.claude/skills/img-convert && img-convert skill > ~/.claude/skills/img-convert/SKILL.md

# Claude Code, this project only
mkdir -p .claude/skills/img-convert && img-convert skill > .claude/skills/img-convert/SKILL.md

Other agents can run img-convert skill to read the guide. A test (__tests__/skill.test.ts) fails if a CLI flag or command is missing from it.


Why img-convert

Most image conversion tools are designed for interactive use — a GUI, a web form, a one-off shell command. img-convert is designed for programmatic use: CI pipelines, build scripts, AI agent workflows, and server-side processing.

Key design principles:

  • Machine-readable output first. --json on every command. stderr carries human-facing progress. stdout carries data. Every command pipes cleanly to jq.

  • AI agent optimized. Ships a native MCP server. Claude Code, Cursor, and any MCP-compatible agent can call convert_image and get_image_info as native tools — no shell escaping, no subprocess management, full type safety.

  • Composable. CLI, Node.js API, and REST API all run the same processImage() pipeline under the hood. Behavior is identical regardless of the call path.

  • Minimal published footprint. The npm bundle is ~50 KB. The full Next.js web UI is excluded from the published package — only dist/, lib/, types/, and cli/ ship.


Installation

Global CLI

npm install -g @dutchbase/img-convert

Local dependency (Node.js API)

npm install @dutchbase/img-convert

Self-hosted web UI

git clone https://github.com/dutchbase/img-convert
cd img-convert
npm install
npm run dev        # http://localhost:3000
npm run build      # production Next.js build

Requirements

  • Node.js >= 20.9.0

  • Sharp's native bindings are pre-built for Linux x64/arm64, macOS arm64/x64, and Windows x64. For other platforms, see the Sharp installation guide.


CLI Reference

The complete reference (every flag, the JSON contract, error codes, manifest keys and recipes) lives in SKILL.md, also printed by img-convert skill. img-convert --help shows the flags with examples.

img-convert photo.jpg -f webp --json                                   # convert one file
img-convert "shots/*.png" -f jpeg --width 200 --height 200 --fit cover -o thumbs --json   # thumbnails
img-convert big.png -f jpeg --max-size 200KB --json                    # fit a byte budget
img-convert info "shots/*.png"                                         # metadata as a JSON array
img-convert batch jobs.json --json                                     # per-file settings from a manifest

With --json, stdout is always a JSON array in input order. Each item has index, ok and either the result fields or error: { code, message }.

Exit codes: 0 all ok · 1 some items failed (stdout still has the full array) · 2 usage error ({"error":{...}} on stdout with --json).


AI Agent Integration

img-convert is designed to be called directly by AI agents as a native typed tool — not as a raw shell command.

MCP Server

Model Context Protocol is the open standard for giving AI agents structured tool access. img-convert ships a production-ready MCP server.

Register with Claude Code

claude mcp add img-convert -- img-convert mcp

The server resolves file paths against, and restricts them to, its working directory.

After registering, Claude Code can call convert_image, get_image_info, batch_convert, and list_supported_formats as native tools — with full type checking, no shell escaping, and structured return values.

Register with other MCP clients

Any client that supports the MCP stdio transport works identically. Point it at img-convert mcp.

Cursor (~/.cursor/mcp.json), Continue, Zed, and any other MCP host follow the same pattern:

{
  "mcpServers": {
    "img-convert": {
      "command": "img-convert",
      "args": ["mcp"]
    }
  }
}

MCP Tools

convert_image

Convert a single image file. Accepts file paths and URLs.

Input schema:

Parameter

Type

Required

Description

input_path

string

Yes

File path or HTTP/HTTPS URL

output_format

string

Yes

One of: jpeg png webp avif gif tiff

output_path

string

No

Output file path. Derived from input_path with new extension if omitted.

quality

number

No

Quality 1–100, default 85

width

number

No

Resize width, maintains aspect ratio

height

number

No

Resize height, maintains aspect ratio

remove_metadata

boolean

No

Strip EXIF, default false

grayscale

boolean

No

Desaturate to grayscale

rotate

number

No

Rotation degrees

background

string

No

Background fill color (CSS color string)

Returns:

{
  "input_path": "photo.jpg",
  "output_path": "photo.webp",
  "input_bytes": 204800,
  "output_bytes": 81920,
  "reduction": 60.0,
  "width": 1920,
  "height": 1080,
  "format": "webp",
  "quality": 85
}

get_image_info

Get full metadata about an image without converting it.

Input schema:

Parameter

Type

Required

Description

input_path

string

Yes

File path or HTTP/HTTPS URL

Returns:

{
  "format": "jpeg",
  "width": 4032,
  "height": 3024,
  "filesize": 3891200,
  "hasAlpha": false,
  "hasExif": true,
  "colorSpace": "srgb",
  "isAnimated": false,
  "channels": 3,
  "density": 72
}

Use this first to make informed conversion decisions: does the image have transparency (affects JPEG conversion), is it animated (affects frame handling), what is the color space (affects print workflows)?

batch_convert

Convert multiple images in a single tool call.

Input schema:

Parameter

Type

Required

Description

items

array

Yes

Array of conversion jobs (see below)

concurrency

number

No

Parallel limit, default 4

Each item in items:

Field

Type

Required

input_path

string

Yes

output_format

string

Yes

output_path

string

No

quality

number

No

width

number

No

height

number

No

Returns: Array of result objects, one per input item.

list_supported_formats

Enumerate what the server can read and write.

Returns:

{
  "input": ["jpeg", "png", "webp", "avif", "gif", "tiff", "heic", "svg"],
  "output": ["jpeg", "png", "webp", "avif", "gif", "tiff"]
}

CLI for agents

Agents that use a shell should call the CLI with --json. The contract (ordered arrays, ok per item, stable error codes, exit code 2 for usage errors) and batch manifests are documented in SKILL.md.


Node.js API

import { convert, getInfo, batch } from '@dutchbase/img-convert'

All three functions accept file paths, HTTP/HTTPS URLs, or raw Buffer objects as input.

convert()

function convert(
  input: string | Buffer,
  options: ConvertApiOptions
): Promise<ConvertApiResult>

ConvertApiOptions:

interface ConvertApiOptions {
  format: ImageFormat;           // required — "jpeg"|"png"|"webp"|"avif"|"gif"|"tiff"
  quality?: number;              // default 85
  width?: number;
  height?: number;
  removeMetadata?: boolean;      // default false
  maintainAspectRatio?: boolean; // default true
  allowUpscaling?: boolean;      // default false (prevents enlargement)
  crop?: {
    left: number;
    top: number;
    width: number;
    height: number;
  };
  rotate?: number;               // arbitrary degrees
  autoRotate?: boolean;          // apply and strip EXIF orientation tag
  flip?: boolean;                // horizontal mirror
  flop?: boolean;                // vertical mirror
  background?: string;           // CSS color string
  grayscale?: boolean;
  blur?: number;                 // Gaussian sigma 0.3–1000
  sharpen?: boolean;
  normalize?: boolean;
  trim?: boolean;
}

ConvertApiResult:

interface ConvertApiResult {
  buffer: Buffer;
  info: {
    inputBytes: number;
    outputBytes: number;
    width: number;
    height: number;
    format: string;
  };
}

Examples:

import { convert } from '@dutchbase/img-convert'
import fs from 'fs/promises'

// Convert a local file
const result = await convert('./photo.jpg', {
  format: 'webp',
  quality: 85,
  width: 1280,
})
await fs.writeFile('./photo.webp', result.buffer)
console.log(`${result.info.inputBytes} → ${result.info.outputBytes} bytes`)

// Convert from a URL
const fromUrl = await convert('https://example.com/image.png', {
  format: 'avif',
  quality: 70,
})

// Convert from an in-memory Buffer (e.g. from a multipart upload handler)
const fromBuffer = await convert(req.file.buffer, {
  format: 'jpeg',
  quality: 90,
  background: '#ffffff',  // flatten PNG transparency before JPEG encoding
})

// Crop then resize
const cropped = await convert('./screenshot.png', {
  format: 'webp',
  crop: { left: 100, top: 50, width: 800, height: 600 },
  width: 400,
})

// Strip EXIF, rotate to EXIF orientation, then re-encode
const clean = await convert('./camera.jpg', {
  format: 'jpeg',
  autoRotate: true,
  removeMetadata: true,
  quality: 88,
})

getInfo()

function getInfo(input: string | Buffer): Promise<ImageInfo>
interface ImageInfo {
  format: string;
  width: number;
  height: number;
  filesize: number;
  hasAlpha: boolean;
  hasExif: boolean;
  colorSpace: string;
  isAnimated: boolean;
  channels?: number;
  density?: number;
}

Examples:

import { getInfo, convert } from '@dutchbase/img-convert'

const info = await getInfo('./photo.jpg')
// { format: 'jpeg', width: 4032, height: 3024, filesize: 3891200,
//   hasAlpha: false, hasExif: true, colorSpace: 'srgb', isAnimated: false }

// Conditional conversion: don't flatten alpha if not needed
const { hasAlpha } = await getInfo('./image.png')
const result = await convert('./image.png', {
  format: 'jpeg',
  ...(hasAlpha ? { background: '#ffffff' } : {}),
})

// Skip animated GIFs in a batch
const infos = await Promise.all(paths.map(p => getInfo(p)))
const staticOnly = paths.filter((_, i) => !infos[i].isAnimated)

batch()

function batch(
  items: BatchApiItem[],
  options?: BatchApiOptions
): Promise<BatchApiResult[]>
interface BatchApiItem {
  input: string;           // file path or URL
  output?: string;         // output file path — auto-derived if omitted
  format: ImageFormat;
  quality?: number;
  width?: number;
  height?: number;
  removeMetadata?: boolean;
}

interface BatchApiOptions {
  concurrency?: number;    // default 4
  outputDir?: string;      // write all outputs here when output not specified per-item
}

interface BatchApiResult {
  input: string;
  output: string;
  inputBytes: number;
  outputBytes: number;
  width: number;
  height: number;
  format: string;
  quality: number;
}

Example:

import { batch } from '@dutchbase/img-convert'

const results = await batch(
  [
    { input: './src/hero.png',   format: 'webp', quality: 90 },
    { input: './src/thumb.jpg',  format: 'avif', width: 200 },
    { input: './src/banner.gif', format: 'webp' },
  ],
  { concurrency: 4 }
)

for (const r of results) {
  const pct = ((1 - r.outputBytes / r.inputBytes) * 100).toFixed(1)
  console.log(`${r.input} → ${r.output} (${pct}% smaller)`)
}
// ./src/hero.png   → ./src/hero.webp   (67.3% smaller)
// ./src/thumb.jpg  → ./src/thumb.avif  (71.0% smaller)
// ./src/banner.gif → ./src/banner.webp (44.2% smaller)

REST API

The web application exposes a single endpoint. It can be called directly from any HTTP client.

POST /api/convert

Accepts multipart/form-data. Returns the converted image as binary.

Request fields:

Field

Type

Required

Notes

file

File

Yes

Source image. Max 50 MB.

targetFormat

string

Yes

jpeg png webp avif gif tiff

quality

string

No

Integer 1–100, default 85

resizeWidth

string

No

Target width in pixels

resizeHeight

string

No

Target height in pixels

maintainAspectRatio

"true"

No

Default false

removeMetadata

"true"

No

Strip EXIF, default false

allowUpscaling

"true"

No

Allow enlargement, default false

Success response:

  • Status: 200

  • Body: raw image bytes

  • Headers:

    • Content-Type — format MIME type

    • Content-Disposition: attachment; filename="<name>.<ext>"

    • X-Output-Size — output size in bytes (string)

    • X-Output-Filename — sanitized output filename

Error response shape:

interface ApiErrorResponse {
  error: string;     // machine-readable error code
  message: string;   // human-readable description
  field?: string;    // which form field caused the error, if applicable
}

Error codes:

HTTP status

Error code

Cause

400

MISSING_FILE

No file in request

400

MISSING_TARGET_FORMAT

targetFormat not provided

400

UNSUPPORTED_TARGET_FORMAT

Requested output format is input-only

400

INVALID_QUALITY

Quality is not an integer in 1–100

400

INVALID_DIMENSION

Width or height is not a positive integer

413

FILE_TOO_LARGE

File exceeds 50 MB

415

UNSUPPORTED_FORMAT

Magic-byte check failed (declared MIME ≠ actual content)

422

IMAGE_TOO_LARGE

Pixel dimensions exceed 25 megapixels

422

LIVE_PHOTO_NOT_SUPPORTED

HEIC live photo detected

500

CONVERSION_FAILED

Unhandled Sharp error

curl example:

curl -s -X POST http://localhost:3000/api/convert \
  -F "file=@photo.jpg" \
  -F "targetFormat=webp" \
  -F "quality=85" \
  -o output.webp

# Check output size from response header
curl -sI -X POST http://localhost:3000/api/convert \
  -F "file=@photo.jpg" \
  -F "targetFormat=webp" \
  | grep X-Output-Size

Format Support

Input formats

Format

MIME type(s)

Notes

JPEG

image/jpeg

PNG

image/png

Transparency supported

WebP

image/webp

Animated WebP supported

AVIF

image/avif

GIF

image/gif

Animated GIF supported

TIFF

image/tiff

HEIC / HEIF

image/heic, image/heif, image/heic-sequence, image/heif-sequence

Pre-decoded via heic-convert. Adds ~200–500 ms per file.

SVG

image/svg+xml

Rasterized via librsvg (Sharp built-in). Output size = SVG declared dimensions unless overridden with --width/--height.

Output formats

Format

Quality flag

Typical use

jpeg

Yes

Photos, no transparency requirement

png

Compression derived

Lossless, transparency, screenshots

webp

Yes

Web images — best size/quality trade-off for most content

avif

Yes

Smallest files, highest quality per byte. Slower encoding.

gif

No

Animated images

tiff

Yes

Print workflows, archival storage

Format conversion notes

Transparency → JPEG. JPEG has no alpha channel. Without --background, transparent pixels become black. Always pass --background "#ffffff" (or your target fill color) when converting PNG/WebP/AVIF with transparency to JPEG.

Animated GIF to static format. Converting an animated GIF to JPEG or PNG captures only the first frame. To preserve animation, convert to WebP (which supports animation).

SVG rasterization. Sharp uses librsvg to rasterize SVGs. The default raster size is the SVG's declared width/height attributes. Pass --width or --height to control the output pixel dimensions.

HEIC decoding. Apple's HEIC format cannot be decoded by Sharp directly. img-convert uses the heic-convert library to decode HEIC to a PNG buffer first, then passes it to Sharp. This adds latency and is single-threaded per file.

PNG quality. PNG is lossless, so --quality controls Sharp's compressionLevel (derived as Math.round((100 - quality) / 11)). Higher quality = lower compression = faster encoding + larger files. The image data is identical either way.


Processing Options

The pipeline runs in this fixed order. Each step is opt-in and independent.

Input
  → HEIC pre-decode (if source is HEIC)
  → Decompression bomb guard (rejects > 25 megapixels)
  → Metadata handling (strip or preserve)
  → Auto-rotate / Rotate
  → Flip / Flop
  → Crop
  → Resize
  → Grayscale
  → Normalize
  → Blur
  → Sharpen
  → Trim
  → Background flatten (before JPEG encoding)
  → Format encode
  → Output

Option

CLI

API field

Notes

Quality

--quality

quality

1–100. Applies to JPEG, WebP, AVIF, TIFF.

Resize

--width / --height

width / height

Fits within dimensions. No upscaling unless allowUpscaling: true.

Metadata

--no-metadata

removeMetadata

Strips EXIF/XMP/IPTC. ICC profile always kept.

Crop

—

crop: { left, top, width, height }

Runs before resize. Pixel coordinates in original image space.

Auto-rotate

—

autoRotate

Applies EXIF orientation and strips the tag.

Rotate

--rotate <deg>

rotate

Any angle. Empty corners filled with background color.

Flip

--flip

flip

Left–right mirror.

Flop

--flop

flop

Top–bottom mirror.

Background

--background <color>

background

CSS color string. Used for rotation corners and JPEG flattening.

Grayscale

--grayscale

grayscale

Desaturates to single luminance channel.

Blur

--blur <sigma>

blur

Gaussian blur, sigma 0.3–1000.

Sharpen

--sharpen

sharpen

Unsharp mask with Sharp defaults.

Normalize

--normalize

normalize

Stretches histogram to full range.

Trim

--trim

trim

Removes uniform-color edge pixels.


Architecture

img-convert/
├── cli/
│   ├── index.ts           # Commander CLI — convert, info, batch, mcp subcommands
│   ├── helpers.ts         # Pure functions: path building, format detection, option mapping
│   └── mcp.ts             # MCP server — registers tools, handles stdio transport
├── lib/
│   ├── imageProcessor.ts  # Core Sharp pipeline — single source of truth for all interfaces
│   ├── api.ts             # Programmatic Node.js API: convert(), getInfo(), batch()
│   ├── heicDecoder.ts     # HEIC → PNG buffer pre-decode step
│   └── processingQueue.ts # Concurrency semaphore for the REST endpoint
├── types/
│   ├── index.ts           # Shared types: ImageFormat, ConvertOptions, API types
│   └── client.ts          # Browser-safe re-export + MIME → ImageFormat detection helper
├── app/
│   ├── api/convert/
│   │   └── route.ts       # Next.js Route Handler: POST /api/convert
│   ├── layout.tsx
│   └── page.tsx
├── components/
│   ├── ImageConverter.tsx # Top-level stateful client component
│   ├── DropZone.tsx       # Drag-and-drop file input
│   ├── ConvertOptions.tsx # Format selector, quality slider, resize controls
│   ├── ConvertResult.tsx  # Download link + size comparison
│   ├── BatchQueue.tsx     # Multi-file batch UI with per-item status
│   └── ImagePreview.tsx   # Source image preview
├── dist/
│   └── cli/               # Compiled CLI output (CommonJS, aliases resolved by tsc-alias)
└── __tests__/
    ├── imageProcessor.test.ts
    ├── cli.test.ts
    ├── route.test.ts
    ├── batchQueue.test.ts
    └── ...

Single pipeline, four interfaces

The processImage() function in lib/imageProcessor.ts is the canonical Sharp pipeline. It is called by:

  1. CLI (cli/index.ts) — reads files or stdin, writes to disk

  2. Node.js API (lib/api.ts) — wraps processImage with input resolution and structured result objects

  3. REST API (app/api/convert/route.ts) — validates multipart form fields and returns binary HTTP response

  4. MCP server (cli/mcp.ts) — translates tool call arguments into processImage options, writes files, returns JSON

All four interfaces produce identical output for identical inputs. There is no separate code path for any interface.

Concurrency model

Interface

Mechanism

Default limit

CLI

p-limit per invocation

--concurrency 4

Node.js API

p-limit per batch() call

options.concurrency ?? 4

REST API

async-sema semaphore across all requests

processingQueue (1 slot)

MCP batch

p-limit per batch_convert call

concurrency ?? 4

The REST endpoint's semaphore is intentionally conservative (single slot) to prevent memory exhaustion under concurrent browser requests. CLI and API concurrency is user-controlled.

Build system

Config

Purpose

tsconfig.json

Next.js app — moduleResolution: "bundler", noEmit: true

tsconfig.cli.json

CLI + API — moduleResolution: "node", module: "CommonJS", emits to dist/cli/

tsc-alias

Post-processes compiled JS to rewrite @/* path aliases to relative paths

The two tsconfig approach is intentional: the Next.js bundler handles module resolution differently from Node.js require(). Sharing one config would require compromises in both directions.


Development

Setup

git clone https://github.com/dutchbase/img-convert
cd img-convert
npm install

Commands

npm run dev            # Start Next.js dev server at http://localhost:3000
npm run build          # Production Next.js build + type-check
npm run build:cli      # Compile CLI + API to dist/cli/ (required before running img-convert locally)
npm run lint           # ESLint
npm test               # Jest unit tests
npm run test:coverage  # Jest with coverage report
npm run test:e2e       # Playwright end-to-end tests
npm run test:all       # Unit + E2E

Adding a new output format

  1. Add the format key to the ImageFormat union in types/index.ts

  2. Add entries to FORMAT_LABELS, FORMAT_MIME, FORMAT_EXTENSIONS

  3. Add the format to OUTPUT_FORMATS (or INPUT_ONLY_FORMATS if Sharp cannot encode it)

  4. Add a case to applyFormat() in lib/imageProcessor.ts

  5. Add the MIME type to detectFormat() in lib/imageProcessor.ts

  6. Add the MIME type to detectFormatFromMime() in types/client.ts

  7. Add the extension to EXT_TO_FORMAT in cli/helpers.ts

  8. Add the MIME type to the accept attribute in components/DropZone.tsx

Adding a new processing option

  1. Add the field to ConvertOptions in types/index.ts

  2. Add to ConvertApiOptions in types/index.ts if it should be part of the public API

  3. Apply in lib/imageProcessor.ts in the correct pipeline position

  4. Add the CLI flag to program in cli/index.ts

  5. Wire through buildConvertOptions() in cli/helpers.ts

  6. Expose in the MCP convert_image tool input schema in cli/mcp.ts

  7. Add a UI control in components/ConvertOptions.tsx if it should be in the web UI

Test structure

Tests live in __tests__/ and run with Jest + ts-jest. The test environment is configured per-file in jest.config.ts:

  • Node environment: imageProcessor.test.ts, route.test.ts, cli.test.ts, heicDecoder.test.ts, animatedGif.test.ts

  • JSDOM environment: imageConverter.test.tsx, dropZone.test.ts, batchQueue.test.ts, processingQueue.test.ts

Sharp operations use the actual Sharp library in tests (no mocking) with small fixture images in __tests__/fixtures/.

Security considerations

The REST endpoint applies multiple defense layers:

  1. File size limit — 50 MB hard cap before reading body

  2. MIME allowlist — source format must be a recognized image type

  3. Magic-byte verification — file-type checks actual file contents, not just the browser-supplied MIME header

  4. Pixel dimension check — rejects images exceeding 25 megapixels before allocating decode buffers

  5. Sharp decompression limit — limitInputPixels: 25_000_000 passed to every Sharp constructor

  6. Filename sanitization — Content-Disposition filename is stripped of all characters except [a-zA-Z0-9._-]

CI/CD

.github/workflows/ci.yml runs on every push and PR:

  • Tests on Node 18, 20, and 22

  • Runs npm test, npm run build, npm run build:cli

  • Verifies the compiled CLI binary executes without error

.github/workflows/release.yml triggers on v* tags:

# Publish a new release
npm version patch    # or minor / major
git push --follow-tags
# GitHub Actions handles the rest

Contributing

Pull requests are welcome.

  1. Open an issue first for non-trivial changes.

  2. Keep processImage() as the single pipeline — don't fork processing logic between CLI, API, REST, and MCP.

  3. Maintain the stderr/stdout contract: data on stdout, progress on stderr. --json should always produce parseable output.

  4. Add tests for new features and bug fixes. The test suite should remain green with npm test.

  5. Run npm test && npm run build:cli before submitting.


License

MIT

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    C
    maintenance
    Enables AI agents to perform GIMP-style image operations such as open, resize, crop, flip, rotate, blur, desaturate, text overlay, export, and batch processing via MCP tools, supporting both mock (Pillow) and live GIMP backends.
    1
    MIT
  • A
    license
    Not graded
    quality
    B
    maintenance
    Enables AI agents to process images locally via file paths—converting, resizing, removing backgrounds, smart cropping, upscaling, reading or stripping metadata, and batch processing—without files ever leaving the device.
    MIT
  • A
    license
    A
    quality
    B
    maintenance
    Enables AI agents to perform local image operations such as circle cropping, cropping, resizing, compressing, converting, and inspecting images without uploading files or making network calls.
    7
    87 npm
    MIT