Skip to main content
Glama

image2-mcp

CI License: MIT Node.js 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


Related MCP server: GPT Image MCP

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 either b64_json or a temporary url — which one you get depends on the request (in testing, output_format: "jpeg" produced a url, while the default returned b64_json). Both are handled: base64 is decoded locally, a url is 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-2change 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.

.env is 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 > .env file. config.js only fills from the file when the variable isn't already set. If editing .env seems 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 node for command, and use either double backslashes (C:\\path\\to\\...) or forward slashes (C:/path/to/...) in args.

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\\...). If node is not on the client's PATH, use the full path to the node executable as the command instead.

5.3 Three things to watch

  1. 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 timeout and the server-side IMAGE2_TIMEOUT_MS (e.g. 300000–600000).

  2. Config changes often don't hot-reload. Restart the client, or trust/enable the new server in the connector/MCP settings page.

  3. 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_URL has 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:

  1. Model names — there is no universal default, and every relay names its models differently. Run check_image_service to list what yours offers, then set IMAGE2_DEFAULT_MODEL to one of them (and add your own IMAGE2_MODEL_ALIASES if you want shorthands). See 1.3.

  2. b64_json vs url — both are already handled in persistImages().

  3. Support for size / quality / output_format — unsupported params are either ignored or rejected by the server; error messages are passed through verbatim.

  4. The edits field name — the standard is image plus optional mask. If your relay differs, just change the IMAGE2_EDIT_FIELD env 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.

License

MIT

Related MCP Connectors

Related MCP Servers