gimp-mcp
# gimp-mcp
An MCP server that drives **GIMP 3** for scripted image editing: crop, resize,
aspect-ratio fitting, light colour touch-up, dimension-spec validation, and
batch processing across a folder.
Built and verified on **Windows with GIMP 3.2.4**, using GIMP 3's
GObject Introspection Python API (`gi.repository.Gimp`) rather than the old
2.x Script-Fu interface.
---
## What it is for
Any workflow where images need the same deterministic treatment applied
repeatedly and you would rather describe it than click through it:
* crop a photo to a target aspect ratio, or to the largest centred square
* resize a folder of images so the longest edge is at most 2000px
* check whether images meet a size/orientation requirement before publishing
* apply one crop-and-resize pipeline across a whole shoot in one pass
## The one thing that will bite you: EXIF orientation
Photos from phones and many cameras are frequently stored **landscape with an
EXIF orientation tag** telling viewers to rotate them. A photo everyone sees
as 3000x4000 portrait may be stored as 4000x3000.
**GIMP's non-interactive loader does not apply that tag.** A naive
"crop to square, centered" therefore crops the wrong axis and produces a
sideways image — while still reporting plausible-looking dimensions, so
nothing looks obviously broken until you open the output.
Every load in this project goes through `load_image()`, which calls
`Gimp.Image.policy_rotate()` first, so all geometry — and every dimension this
server reports — is in **displayed orientation**, i.e. what a viewer actually
sees. This is covered by a test.
---
## Architecture
Two execution backends, **one shared operation runtime**:
```
┌───────────────────────────────┐
MCP client ──────►│ gimp_mcp/server.py (stdio) │
└───────────┬───────────────────┘
│
┌─────────────────┴──────────────────┐
▼ ▼
HeadlessBackend BridgeBackend
spawns gimp-console-3.exe TCP 127.0.0.1:50472
(no running GIMP needed) (into a running GIMP)
│ │
▼ ▼
bootstrap.py plug-ins/gimp-mcp-bridge/
│ │
└──────────────┬─────────────────────┘
▼
gimp_mcp/gimp_runtime.py
THE single source of truth for every
image operation. Both paths share it,
so batch and live cannot drift apart.
```
`install_plugin.py` writes a `runtime_path.txt` pointer next to the installed
plug-in rather than copying `gimp_runtime.py`, so exactly one copy of the
operation code exists on disk.
**Backend choice.** `headless` is the default and is what all batch and
deterministic work uses — it needs no open GIMP and is the reliable path.
`bridge` is for live work on a document you already have open. Both are
verified to produce pixel-identical output.
### Why TCP and not D-Bus
Existing live-GIMP-control projects use D-Bus, which does not exist on
Windows. A loopback TCP socket achieves the same thing and is
cross-platform. It binds `127.0.0.1` only and is **never** exposed to the
network.
---
## Install
Requires **Python 3.10+**, GIMP 3.x (developed against 3.2.4), and the `mcp`
Python package. 3.9 cannot work — see the note below.
> **Note on the `mcp` dependency.** This targets the `mcp` **1.x** SDK and is
> pinned to `mcp>=1.0,<2`. Version 2.0 removed `mcp.server.fastmcp` and renamed
> `FastMCP` to `MCPServer`; porting to it is not done yet, and an unpinned
> install picks up 2.x and fails at import.
>
> That dependency also sets the Python floor: every published `mcp` requires
> `>=3.10`, so this package cannot install on 3.9 whatever its own metadata
> says. Tested on 3.10.20 and 3.14.6.
```bash
pip install -r requirements.txt
python install_plugin.py # install the bridge plug-in (optional)
python install_plugin.py --list # show detected GIMP config dirs
```
The bridge plug-in is only needed for the **live control** tools. The batch
and single-image tools work without installing anything into GIMP.
### Plug-in location
`install_plugin.py` discovers whatever GIMP `3.x` config directories actually
exist rather than hardcoding a version. On Windows that is:
```
%APPDATA%\GIMP\3.2\plug-ins\gimp-mcp-bridge\gimp-mcp-bridge.py
```
Note it is the **versioned** directory (`3.2` for GIMP 3.2, not `3.0`), and
GIMP 3 requires each plug-in to sit in a folder whose name **matches the .py
file**. On Linux and macOS the installer looks in `~/.config/GIMP/3.x/` and
`~/Library/Application Support/GIMP/3.x/` respectively.
### Register the MCP server
Installing the package provides a `gimp-mcp` console script, which is the
tidiest thing to register because it does not depend on a working directory:
```bash
python -m venv .venv
.venv/Scripts/python -m pip install -e . # .venv/bin/python on Unix
```
```json
{
"mcpServers": {
"gimp": {
"type": "stdio",
"command": "/path/to/gimp-mcp/.venv/Scripts/gimp-mcp.exe",
"args": []
}
}
}
```
With Claude Code, the equivalent one-liner is:
```bash
claude mcp add gimp --scope user -- /path/to/gimp-mcp/.venv/Scripts/gimp-mcp.exe
```
Running the module directly works too, if `mcp` is importable in that
interpreter:
```json
{
"mcpServers": {
"gimp": {
"command": "python",
"args": ["-m", "gimp_mcp"],
"cwd": "/path/to/gimp-mcp"
}
}
}
```
Optional environment variables:
| Variable | Purpose |
|---|---|
| `GIMP_CONSOLE` | Full path to `gimp-console-3.exe` if it is not auto-detected |
| `GIMP_MCP_BACKEND` | `headless` (default) or `bridge` |
| `GIMP_MCP_BRIDGE_PORT` | Bridge port, default `50472` |
---
## Tools
### Inspection
| Tool | Purpose |
|---|---|
| `gimp_status` | Check GIMP is reachable; reports both backends. **Start here if something is wrong.** |
| `inspect_image` | Dimensions, layers, orientation. Dimensions are as displayed. |
| `check_image_spec` | Validate against a dimension spec; pass/fail with measured dimensions and a plain-language reason. |
### Single image
| Tool | Purpose |
|---|---|
| `crop_image` | Exact pixel rectangle. Rejects out-of-bounds rather than silently clamping. |
| `crop_square` | Largest square; `anchor` = center/top/bottom/left/right/corner. |
| `crop_to_aspect` | Target ratio (1.0 square, 1.3333 for 4:3, 1.7778 for 16:9), max area. |
| `resize_image` | By width, height, or `max_edge`. Aspect preserved by default. |
| `adjust_image` | Brightness/contrast, `-1..1`, rejected outside rather than clamped. |
| `enhance_image` | Gamma shadow-lift, contrast, saturation and a high-pass sharpen in one pass. |
| `fit_to_spec` | One shot: fix orientation by cropping, upscale to a minimum, downscale to a maximum, optional touch-up. |
| `process_image` | Custom operation pipeline in one pass (one JPEG re-encode). |
### Batch
| Tool | Purpose |
|---|---|
| `batch_process` | Arbitrary pipeline over a folder. |
| `batch_fit_to_spec` | Conform a whole folder to one dimension spec. |
| `batch_check_image_spec` | Read-only audit; triage before editing. |
A whole batch runs inside **one** GIMP invocation. GIMP's console takes several
seconds to start, so spawning per file would be slow — measured at ~2.4x
cheaper per file for a small folder, and the saving grows with folder size. A
file that fails does not abort the run; it lands in `errors` and the rest
continue.
### Live control (needs the bridge plug-in)
| Tool | Purpose |
|---|---|
| `live_list_images` | What is open in the running GIMP. |
| `live_screenshot` | Flattened snapshot of the canvas, so you can see and iterate. |
| `live_run_python` | Arbitrary Python in the live context; assign to `result`. |
| `live_stop_bridge` | Stop the bridge, leave GIMP open. |
Start the bridge in GIMP: **Filters > Development > Start MCP Bridge**.
---
## Image specifications
`check_image_spec`, `fit_to_spec` and their batch equivalents share one spec
model. Every constraint is optional — `0` means no limit, and orientation
`any` means no orientation requirement.
| Field | Values |
|---|---|
| `min_width`, `min_height` | pixels, `0` for no minimum |
| `max_width`, `max_height` | pixels, `0` for no maximum |
| `orientation` | `any`, `square`, `landscape`, `portrait`, `square_or_landscape`, `square_or_portrait` |
`fit_to_spec` satisfies a spec in three ordered steps: crop to correct the
orientation, upscale to reach the minimum, downscale to respect the maximum.
Constraints already satisfied leave the framing untouched.
```jsonc
// A square image at least 1000x1000, capped at 2000x2000
{ "orientation": "square", "min_width": 1000, "min_height": 1000,
"max_width": 2000, "max_height": 2000 }
```
## Enhancement
`enhance_image` applies, in this order and each skippable at its no-op value:
| Parameter | Effect | Off at |
|---|---|---|
| `gamma` | lifts midtones and shadows via levels, black and white points untouched so nothing clips | `1.0` |
| `contrast` | GIMP 3 native `-1..1` | `0.0` |
| `saturation` | `-100..100` | `0.0` |
| `sharpen` | high-pass sharpen blended back at this percent opacity | `0.0` |
| `sharpen_radius` | blur radius in px for the high pass | default `8` |
The sharpen is a frequency-separation high pass — duplicate, blur, grain-extract
to isolate the high frequencies, grain-merge back at low opacity — not an
unsharp mask, which haloes around high-contrast edges.
### Contrast does not mean what it meant in GIMP 2.x
GIMP 3 runs brightness-contrast in **linear light**; GIMP 2.x ran it on sRGB
values. The same nominal number is therefore much stronger here. Measured on a
real photo against the 2.x transfer curve:
```
GIMP 2.x "+12" -> slope 1.161 on sRGB (the intended result)
passing 12/127 = 0.094 to GIMP 3 ~4x too strong, visibly crushes shadows
contrast = 0.020 in GIMP 3 closest match to the intended curve
```
`sharpen_radius` has the same trap: GIMP 3 dropped `plug-in-gauss`, and
`gegl:gaussian-blur` takes a **standard deviation**, not a radius. A radius is
converted using GIMP's legacy formula (radius 8 → std-dev ≈ 2.40) and the
applied value is reported back so it can be checked.
Calibrate against output, not against a remapped number.
## Colour adjustment: range, and what it actually does
`adjust_image` and `enhance_image`'s contrast both run `-1.0..1.0` — GIMP 3's
real range for this operation — and **reject** values outside it rather than
clamping. They wrap the same GIMP call, so they deliberately share one range;
an earlier version capped `adjust_image` at ±0.5 and described that as "the
native range", which was simply wrong and made two tools behave differently
for no stated reason.
Useful values are far smaller than the limits. GIMP 3 applies this in linear
light, so anything much past ±0.1 visibly changes the character of a photo —
see [Enhancement](#enhancement) for the measured comparison against GIMP 2.x.
Keep adjustments small when the image needs to represent a real subject
faithfully.
There is intentionally no "auto enhance" that guesses at settings.
---
## Verification
Run the suite:
```bash
python -m pytest tests/ -v
```
Tests that need real images are skipped unless you point them at some:
```bash
export GIMP_MCP_TEST_IMAGE=/path/to/photo.jpg # ideally EXIF-rotated
export GIMP_MCP_TEST_REFERENCE=/path/to/photo-square.jpg
```
`GIMP_MCP_TEST_REFERENCE` should be an independently produced centred square
crop of `GIMP_MCP_TEST_IMAGE` — cropped by hand in GIMP, for example. The
headline test asserts that `crop_square` **reproduces that reference**, rather
than merely running without error.
On the reference photo used during development (a 4000x3000 JPEG with EXIF
orientation 6, displaying as 3000x4000):
```
crop_square vs hand-made reference : mean abs diff 0.236, max 18, outliers 0.0014%
same crop via the bridge backend : mean abs diff 0.236, max 18, outliers 0.0014%
```
That residual is JPEG re-encode noise — re-encoding alone gives ~0.5 mean — not
a geometry difference, and both backends agree exactly.
The suite also covers displayed-orientation reporting, orientation and
minimum-size specs, out-of-bounds crops being rejected, out-of-range
adjustments being rejected, brightness moving pixels the right way, chained
pipelines, aspect-ratio cropping, batch across a folder, the read-only audit,
clear errors for missing files, and a full pass over the real MCP stdio
protocol.
---
## Troubleshooting
**`gimp-console not found`** — set `GIMP_CONSOLE` to the full path of
`gimp-console-3.exe`.
**Bridge tools fail with "Could not reach the GIMP bridge"** — GIMP is not
open, or the bridge was not started. Run Filters > Development > Start MCP
Bridge. `gimp_status` shows both backends at once.
**The menu item is missing after installing** — restart GIMP; it only scans
plug-ins at startup. Confirm the layout is
`plug-ins/gimp-mcp-bridge/gimp-mcp-bridge.py` (the folder name must match the
file name).
**Diagnosing the plug-in** — a GIMP plug-in is a separate process whose stderr
is invisible when GIMP runs as a GUI app on Windows. The bridge writes to
`bridge.log` next to the installed plug-in.
**A colour-profile dialog blocks GIMP on startup** when opening an image with
an embedded profile in GUI mode. It does not appear in headless mode, which is
another reason batch work uses the headless backend.
**Batch timed out** — the default is 600s for the whole run; very large
folders may need more.
---
## Testing
```bash
python -m pytest tests/ -q # everything
python -m pytest tests/ -q -k "not live" # skip the slow bridge tests
GIMP_MCP_SKIP_LIVE=1 python -m pytest tests/ # same, via the env var
```
Most tests drive real GIMP, which is the point — they are slow because they
are not mocking the thing under test. Those needing sample images skip unless
`GIMP_MCP_TEST_IMAGE` / `GIMP_MCP_TEST_REFERENCE` are set (see `conftest.py`).
`tests/test_live_bridge.py` covers the four `live_*` tools by starting a bridge
itself. It hosts the plug-in under **`gimp-console`**, not the GUI: plug-ins run
there just the same, so the fixture needs no window, no desktop session and no
window manager, starts in ~2s instead of ~10, and cannot be blocked by the
colour-profile dialog a GUI GIMP raises for a photo with an embedded profile.
It binds a non-default port (50573, and 50574 for the teardown test) so it
never collides with a bridge you have running on 50472, and it tears the
process down afterwards — verified across repeated runs to leave no stray
process and no open port.
## Known limitations
* **The `live_*` tests need a real GIMP and are the slow part of the suite.**
`tests/test_live_bridge.py` starts a bridge itself and covers all four
tools, but it costs ~33s and depends on GIMP being installed with the
plug-in in place. It skips with a reason — never hangs — when GIMP is
missing, the plug-in is not installed, its port is taken, or the bridge does
not answer in time. `GIMP_MCP_SKIP_LIVE=1` skips it outright.
* **The bridge executes arbitrary Python by design.** It is loopback-only and
started manually rather than automatically, but anything that can reach
localhost on the machine can drive GIMP while it is running. Stop it when
not in use.
* **Bridge start blocks its own plug-in process** — that is what keeps it
alive. It does not freeze GIMP's UI, but GIMP shows the plug-in as running.
* **The GUI menu item itself is not automated-test covered.** The procedure it
invokes is verified; the click path is not.
* **Only Windows is verified.** The code paths are cross-platform and the
installer handles Linux/macOS config directories, but neither has been
tested.
* **The `mcp` 2.x SDK is not supported yet** -- see the note under Install.
* **Python 3.10 is the floor, and 3.9 is impossible.** Not a style
preference: the `mcp` SDK has required `>=3.10` in every version ever
published (0.9.1 through 2.1.x), so on 3.9 the sole runtime dependency does
not resolve and the package cannot be installed at all. The floor was
briefly advertised as `>=3.9`, which promised something that could never
work. It is now `>=3.10`, verified by running the full suite on 3.10.20
(39 passed), and `tests/test_packaging.py` checks the declared floor against
the installed `mcp`'s own metadata so the two cannot drift apart again.
* **No AI background removal or style transfer.** Some comparable projects
advertise these without a working implementation behind them; they are
deliberately not claimed here.
## Notes on prior art
The split between a GIMP-side plug-in exposing a bridge and a standalone MCP
server process that connects to it as a client is a natural shape for this
problem and is used by other GIMP MCP projects. Batch processing and
preset-style pipelines are common to several. Live-canvas control exists
elsewhere via D-Bus, replaced here with loopback TCP for Windows support. No
code was copied from any of them; the Windows specifics — the real plug-in
path, the plug-in process lifetime, the run-callback signature, and the EXIF
behaviour — were established directly against GIMP 3.2.4.
## License
MIT — see [LICENSE](LICENSE).
TDQS
Scored across 18 tools
Several tools overlap in purpose: adjust_image and enhance_image share the same contrast call, and process_image/batch_process can reproduce the effects of most individual editing tools. The descriptions generally clarify scope, but an agent could easily hesitate between a dedicated single-op tool and its pipeline equivalent.
Most tools follow a clear verb_object snake_case pattern such as crop_image, resize_image, inspect_image, and batch_check_image_spec. The batch_ and live_ prefixes are applied consistently, with only gimp_status and fit_to_spec deviating slightly from the otherwise predictable pattern.
At 18 tools the server is slightly above the ideal 3-15 range, but the count is justified by the distinct clusters: single-image operations, spec checking/fitting, batch variants, and live GIMP bridge tools. The single/batch pairs add surface area but each serves a real workload.
The toolset covers the core image pipeline well: inspect, validate, crop, resize, adjust, enhance, process in one pass, and batch over folders. Obvious gaps like rotation or flipping are absent, but live_run_python and process_image provide workarounds for most missing operations.