mcp server
image2-mcp
MCP server that wraps an OpenAI-compatible image generation API and saves the returned base64 data as local image files.
Set up
Runs on your computer. Pick your agent and operating system.
Before you start
- Node.js 20 or newer
{
"mcpServers": {
"image2-mcp": {
"command": "npx",
"args": [
"-y",
"image2-mcp"
]
}
}
}same format Cursor, GitHub Copilot CLI, Junie, JetBrains AI Assistant, Augment (Auggie CLI), Factory Droid, Kiro, Devin CLI +12
Values in angle brackets are placeholders. Restart Claude Desktop after editing its config.Claude Desktop docs
README
synced Oct 23, 2026image2-mcp
An MCP Server that wraps an OpenAI-compatible image generation API, giving any MCP client (Claude Desktop / Cursor / Cline / WorkBuddy …) native image-generation tools.
Dependencies are deliberately minimal — only @modelcontextprotocol/sdk and zod at
runtime, using Node's built-in fetch, with an 80-line hand-rolled .env parser.
This repository is not affiliated with any API relay service. No relay is bundled or assumed — you must point it at a service you are authorized to use via
IMAGE2_BASE_URL.
Table of Contents
- 1. What this server wraps
- 2. The 3 exposed tools
- 3. Install & configure
- 4. Usage
- 5. Connecting to an MCP client
- 6. Environment variables
- 7. Development
- 8. Switching to another relay
- 9. Known upstream behaviors & pitfalls
- 10. Project layout
1. What this server wraps
An OpenAI-compatible image API — you supply the endpoint via IMAGE2_BASE_URL.
Its main job is turning the API response into a real image file on disk and handing back an
absolute path. That is the advantage over calling the API with raw curl: the client gets a
local file it can immediately read and display.
1.1 Endpoints used
All three are standard OpenAI shapes, so they are the same across compatible relays:
| Endpoint | Method | Content-Type | Purpose |
|---|---|---|---|
{base}/images/generations | POST | application/json | Text-to-image |
{base}/images/edits | POST | multipart/form-data | Image edit / multi-image fusion |
{base}/models | GET | — | Connectivity & model list probe |
1.2 Response shape
{
"created": 1789630481,
"data": [{ "b64_json": "iVBORw0KGgoAAAANSUhEUg..." }],
"size": "1024x1024",
"quality": "high",
"output_format": "png",
"usage": { "output_tokens": 2058, "total_tokens": 2065 }
}
⚠️ Two possible response shapes. An entry in
data[]carries eitherb64_jsonor a temporaryurl— which one you get depends on the request (in testing,output_format: "jpeg"produced aurl, while the default returnedb64_json). Both are handled: base64 is decoded locally, aurlis downloaded (with retries).
1.3 Model names depend on your relay
This server assumes no particular model names. Which models a relay exposes is up to that relay — the same capability may be named completely differently from one service to another.
Find out what yours offers:
check_image_service
It queries {base}/models and lists everything the relay advertises, highlighting the
image-related entries. Then pass whatever ID actually exists on your relay as the model
parameter.
⚠️ The table below is an example, not a standard. These are the model IDs that happened to work on the author's relay in 2026-09. Your relay will very likely use different names.
| Model ID (example) | Alias | Status | Notes |
|---|---|---|---|
gpt-image-2 | base | ✅ OK | Default |
gpt-image-2-5-flare | flare | ✅ OK | Variant, different style |
gpt-image-2-5-sunburst | sunburst | ✅ OK | Variant, different style |
gpt-image-2-pro | pro / hq | ✅ OK | Higher-detail variant |
Which model gets used
| Setting | Effect |
|---|---|
model parameter | Used for that one call. Any model ID works — aliases are optional. |
IMAGE2_DEFAULT_MODEL | Used when model is omitted. Defaults to gpt-image-2 — change it if your relay doesn't serve that name. |
IMAGE2_MODEL_ALIASES | Define your own shorthands, e.g. fast=my-turbo,cheap=my-lite. Setting it replaces the built-in aliases entirely. |
The built-in aliases (base / pro / hq / flare / sunburst) are pure convenience and
carry no meaning outside the author's relay. You can ignore them and pass explicit model IDs,
or replace them via IMAGE2_MODEL_ALIASES — no code changes needed.
2. The 3 exposed tools
generate_image — text-to-image
Generates an image from a description and writes it to disk, returning an absolute path.
| Parameter | Required | Description |
|---|---|---|
prompt | ✅ | Image description; more detail is better |
model | A model ID your relay actually serves — see 1.3 | |
size | 1024x1024 / 1024x1536 / 1536x1024 / auto | |
quality | low / medium / high / auto | |
n | Count, 1–4 | |
background | opaque / transparent / auto | |
output_format | png / jpeg / webp | |
out_dir | Output directory, default ./output | |
basename | Filename prefix |
edit_image — image edit / img2img
Generates a new image from existing ones; accepts multiple reference images for fusion and an optional mask for inpainting.
| Parameter | Required | Description |
|---|---|---|
prompt | ✅ | What to change, e.g. "replace the background with a snowy sunset" |
image_paths | ✅ | Array of local input paths, 1–8 images |
mask_path | Optional mask (PNG; transparent area = region to repaint) | |
| others | Same as generate_image |
check_image_service — connectivity self-check
Probes relay reachability, key validity, timeout and output-dir settings, and lists the image models the server advertises. Run this first when troubleshooting.
3. Install & configure
Requirements
- Node.js ≥ 20 (uses built-in
fetch/FormData/Blob; see.nvmrc) - An OpenAI-compatible relay endpoint and API key
Install
git clone https://github.com/sailor20/image2-mcp.git
cd image2-mcp
npm install
Configure
cp .env.example .env
Then edit .env and fill in both required values:
# Your relay's root URL — there is no built-in default
IMAGE2_BASE_URL=https://your-relay.example.com/v1
IMAGE2_API_KEY=sk-your-key
Alternatively pass them via your MCP client's env field
(see section 5) so no .env is written.
If either value is missing, the tools return an error that names the missing variable — the server does not guess an endpoint.
.envis gitignored and will never be committed. Never put a real key in any file that gets committed.
4. Usage
Just describe what you want
Once the tools are live you don't need to write JSON — natural language works:
Generate an image: a cyberpunk city at night, neon reflecting off rain-slicked streets
Make a 16:9 tech product poster for an AI coding assistant, dark background
For fine control, state the parameters inline:
Generate 4 forest illustrations in Ghibli style, square, high quality
Switching models
Run check_image_service first if you don't know what your relay offers — model IDs are
relay-specific (see 1.3).
A. Per call
Just name the model, either as a raw ID or (if you defined one) an alias:
Use my-relay-turbo to generate 4 forest illustrations in Ghibli style, square
Aliases are entirely optional. Define your own in .env — no code changes needed:
# 别名=模型ID,逗号分隔;设置后完全取代内置别名
IMAGE2_MODEL_ALIASES=fast=my-relay-turbo,cheap=my-relay-lite
The built-in aliases, for reference, are only meaningful on the author's relay:
| Alias | Resolves to |
|---|---|
base | gpt-image-2 |
pro / hq | gpt-image-2-pro |
flare | gpt-image-2-5-flare |
sunburst | gpt-image-2-5-sunburst |
B. Change the default (persistent)
Set it to a model your relay actually serves:
IMAGE2_DEFAULT_MODEL=my-relay-turbo
You must restart the MCP connection (toggle the server off/on, or restart the client).
C. Set it in the MCP client's env (highest priority)
"env": {
"IMAGE2_DEFAULT_MODEL": "my-relay-turbo"
}
Priority: MCP client
env>.envfile.config.jsonly fills from the file when the variable isn't already set. If editing.envseems to have no effect, check here first.
Parameter reference
| Parameter | Values | Default | Notes |
|---|---|---|---|
model | base / flare / sunburst / pro or full ID | base | see table above |
size | 1024x1024 / 1024x1536 (portrait) / 1536x1024 (landscape) / auto | 1024x1024 | server may adjust |
quality | low / medium / high / auto | high | affects latency and tokens |
n | 1–4 | 1 | more images = slower and pricier |
background | opaque / transparent / auto | opaque | transparent yields transparent PNG |
output_format | png / jpeg / webp | png | see section 9 |
out_dir | path | ./output | relative paths resolve against the server root |
basename | filename prefix | auto | handy for finding results later |
Image editing
edit_image takes local file paths:
Change the wall in ~/pics/room.png to dark green, keep the furniture as-is
Multi-image fusion (1–8 references):
Using the style of ~/pics/style1.png and the subject of ~/pics/subject.png,
generate a new illustration
Inpainting requires a mask PNG (transparent area = region to repaint):
Use ~/pics/mask.png as the mask and replace the masked area with a cat
Where output goes
By default into output/ under the project, named like {prompt-words}-{timestamp}.png.
The returned absolute path can be read, displayed, or edited further. If a name is already
taken, a numeric suffix is appended (name-2.png, name-3.png, …) so nothing is overwritten.
The extension reflects the actual file bytes (see section 9, item 2). If the real format differs from what you requested, the returned text says so explicitly.
5. Connecting to an MCP client
5.1 The config, in JSON
Merge this into your client's mcpServers:
{
"mcpServers": {
"image2": {
"type": "stdio",
"command": "node",
"args": ["/absolute/path/to/image2-mcp/src/index.js"],
"env": {
"IMAGE2_BASE_URL": "https://your-relay.example.com/v1",
"IMAGE2_API_KEY": "sk-your-key",
"IMAGE2_OUTPUT_DIR": "/absolute/path/to/image2-mcp/output",
"IMAGE2_TIMEOUT_MS": "300000"
},
"timeout": 600000
}
}
}
Where the config lives:
| Client | Config file |
|---|---|
| Claude Desktop | claude_desktop_config.json |
| Cursor | ~/.cursor/mcp.json |
| WorkBuddy | ~/.workbuddy/mcp.json (note: not .mcp.json) |
| Others | see your client's MCP / stdio docs |
Windows: prefer an absolute path to
nodeforcommand, and use either double backslashes (C:\\path\\to\\...) or forward slashes (C:/path/to/...) inargs.
5.2 Adding it from the WorkBuddy UI
The MCP page's Add MCP server dialog has three tabs — Quick / Manual / JSON (快速 / 手动 / JSON). They all produce the same server entry; they differ only in how much you type. Pick whichever suits you.
Tab 1 — Quick (快速)
One box: paste a command, a URL, or a JSON blob, and the transport is detected automatically. For a local stdio server like this one, paste the command that starts it:
node /absolute/path/to/image2-mcp/src/index.js
That's the whole thing — no environment variables needed in the dialog, provided you have
already filled in .env. The server resolves .env relative to its own script location, not
the working directory, so it finds the file even when the client launches it from elsewhere.
Switch to another tab if you need to pass env vars through the dialog instead of .env.
Tab 2 — Manual (手动)
Fill in three fields, then expand Advanced options (高级选项) if you want to set environment variables or a working directory.
| Field | Value |
|---|---|
| Name (名称) | image2-mcp |
| Transport (传输方式) | stdio |
| Command (命令) | node /absolute/path/to/image2-mcp/src/index.js |
Tab 3 — JSON
Paste the configuration exactly as shown in 5.1. This tab accepts
either form — with or without the outer mcpServers wrapper:
{ "image2": { "type": "stdio", "command": "node", "args": ["..."] } }
{ "mcpServers": { "image2": { "type": "stdio", "command": "node", "args": ["..."] } } }
After adding: the server appears in the MCP list. On clients that require it, trust or enable it there before the tools become available.
Windows tip: use forward slashes in paths (
D:/tools/image2-mcp/src/index.js) or escape the backslashes (D:\\tools\\...). Ifnodeis not on the client'sPATH, use the full path to thenodeexecutable as the command instead.
5.3 Three things to watch
-
Raise the timeout. A single image takes 40–180 s; most clients default to 60 s and will cut it off. Increase both the client-side
timeoutand the server-sideIMAGE2_TIMEOUT_MS(e.g. 300000–600000). -
Config changes often don't hot-reload. Restart the client, or trust/enable the new server in the connector/MCP settings page.
-
Never commit your key. Prefer
.env(gitignored), or the client's local config file, which should itself stay out of version control.
6. Environment variables
| Variable | Default | Description |
|---|---|---|
IMAGE2_BASE_URL | — | Required. Relay root, without /images/... |
IMAGE2_API_KEY | — | Required. |
IMAGE2_OUTPUT_DIR | ./output | Output dir; relative paths resolve against server root |
IMAGE2_TIMEOUT_MS | 300000 | Per-request timeout (ms); max 2147483647 |
IMAGE2_DEFAULT_MODEL | gpt-image-2 | Model used when model is omitted — set this to a model your relay serves |
IMAGE2_MODEL_ALIASES | — | Your own shorthands, alias=model-id,alias2=id2. Replaces the built-in aliases |
IMAGE2_DEFAULT_SIZE | 1024x1024 | Default size |
IMAGE2_DEFAULT_QUALITY | high | Default quality |
IMAGE2_EDIT_FIELD | image | multipart field name for edit_image, see section 9 item 3 |
IMAGE2_ENV_FILE | — | Path to an extra .env (useful for multi-environment setups) |
There is no built-in default relay.
IMAGE2_BASE_URLhas no fallback, on purpose: the server should never silently target a third-party service you did not choose. If either required variable is missing, tool calls return an error naming exactly which ones.
Load priority: process env (i.e. the MCP client's env) > IMAGE2_ENV_FILE > project .env.
Variables already present in the environment are never overwritten by the file.
7. Development
Everyday commands
| Command | What it does |
|---|---|
npm test | Full test suite via Node's built-in test runner |
npm run lint | ESLint over the repository |
npm run lint:fix | ESLint with automatic fixes |
npm run format | Format everything with Prettier |
npm run format:check | Verify formatting without writing (what CI runs) |
npm run smoke | Real end-to-end request — costs credits, not run in CI |
Tests
The suite lives in test/ and runs on the built-in
node:test runner — no test framework dependency.
npm test # everything
node --test test/edge-cases.test.js # a single file
It is fully offline: it never contacts a real API and never spends credits.
test/protocol.test.js spawns the real server over stdio and checks the handshake, the tool
list, input validation, and the error paths for a missing API key and unreadable input files.
test/edge-cases.test.js covers format sniffing, filename sanitization, and atomic writes.
Smoke test
Hits a live endpoint and generates one image, so it is run manually by maintainers:
npm run smoke # default prompt
node scripts/smoke.js a cat wearing sunglasses # custom prompt
node scripts/smoke.js --models # probe only, no generation
Continuous integration
.github/workflows/ci.yml runs lint, format check, and tests on Node 20 and 22, plus one
Windows job (filename handling is platform-sensitive). It also runs npm pack --dry-run to
keep the published file list honest.
Contributions are welcome — see CONTRIBUTING.md.
8. Switching to another relay
This implementation is not tied to any specific relay. As long as the target service is
OpenAI-compatible, just change IMAGE2_BASE_URL and IMAGE2_API_KEY. Four things need
attention:
- Model names — there is no universal default, and every relay names its models
differently. Run
check_image_serviceto list what yours offers, then setIMAGE2_DEFAULT_MODELto one of them (and add your ownIMAGE2_MODEL_ALIASESif you want shorthands). See 1.3. b64_jsonvsurl— both are already handled inpersistImages().- Support for
size/quality/output_format— unsupported params are either ignored or rejected by the server; error messages are passed through verbatim. - The
editsfield name — the standard isimageplus optionalmask. If your relay differs, just change theIMAGE2_EDIT_FIELDenv var — no code changes needed.
9. Known upstream behaviors & pitfalls
All items below were empirically verified, not assumed.
1. size may be adjusted by the server
Request size: 1024x1024 and you may get 1254×1254 back. Some upstream implementations apply
their own sizing policy; the size field in the response is the real value.
This is expected, not a bug.
2. output_format: "jpeg" may still return PNG bytes
Verified: when requesting output_format: "jpeg", the response echoes
output_format: "jpeg", but the decoded data[].b64_json bytes are actually PNG.
How this server handles it: the on-disk extension is derived from the magic bytes
(sniffImageExt()), ignoring both the request parameter and the response field. So you'll
get photo.png, not photo.jpg. This is intentional — it prevents producing files whose
extension contradicts their contents, which image hosts and CDNs with MIME sniffing reject.
Because of this, the tool result reports two separate fields rather than one ambiguous one:
| Field | Meaning |
|---|---|
format | the format actually written to disk (from the bytes) |
requested_format | what you asked for, or null if you didn't specify |
When the two differ, the human-readable part of the result also says so explicitly.
Every entry in files[] carries its own format as well.
3. image vs image[] route to different upstream backends
The relay is sensitive to the multipart field name:
| Field name | data[0] | size | quality |
|---|---|---|---|
image | b64_json + revised_prompt | 1254x1254 (real) | auto |
image[] | url | "" (empty) | medium |
So mixing the two field names makes 1-image and multi-image calls hit different implementations, changing both output style and response shape.
How this server handles it: it uses one field name for all images (default image),
overridable via IMAGE2_EDIT_FIELD. If your relay requires image[], set
IMAGE2_EDIT_FIELD=image[].
4. Multi-image requests may omit size / quality
When passing multiple reference images, the upstream response may lack size and quality;
those fields will be null in the result.
5. url responses can be slow to connect, and the image is already paid for
When the response uses a url, the file lives on a CDN whose connection time varies widely —
in testing, between roughly 0.5 s and beyond 10 s. Node's fetch applies its own 10-second
connect timeout (UND_ERR_CONNECT_TIMEOUT), which fires before IMAGE2_TIMEOUT_MS and
cannot be raised through that variable.
This matters because by the time the download starts, the image has already been generated and billed. A single transient network hiccup would otherwise mean paying for an image and receiving nothing.
How this server handles it: downloads are retried up to 3 times with a short backoff, and
4xx responses (a genuinely dead link) are not retried. Failure messages include the underlying
cause rather than a bare fetch failed, so the reason is visible when retries are exhausted.
10. Project layout
image2-mcp/
├── src/
│ ├── index.js # MCP server: tool definitions, validation, startup
│ ├── relay-client.js # HTTP client + format sniffing + safe writes
│ └── config.js # zero-dependency .env parser
├── test/
│ ├── protocol.test.js # spawns the server; handshake, tools, error paths
│ └── edge-cases.test.js # format sniffing, filenames, atomic writes
├── scripts/
│ └── smoke.js # manual end-to-end check (costs credits)
├── .github/workflows/ci.yml # lint + format check + tests + pack check
├── eslint.config.js # ESLint flat config
├── .prettierrc.json # formatting rules
├── .editorconfig # editor whitespace rules
├── .nvmrc # pinned Node major version
├── .env.example # config template (.env is gitignored)
└── output/ # generated images (gitignored)
Contributing
Issues and pull requests are welcome — see CONTRIBUTING.md. For security matters, please follow SECURITY.md instead of opening a public issue.