Skip to content
Conduit

mcp server

image2-mcp

MCP server that wraps an OpenAI-compatible image generation API and saves the returned base64 data as local image files.

sailor20/image2-mcpcommunitylistedunreviewed

Set up

Runs on your computer. Pick your agent and operating system.

Before you start

  • Node.js 20 or newer
~/Library/Application Support/Claude/claude_desktop_config.json
{
  "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, 2026

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


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:

EndpointMethodContent-TypePurpose
{base}/images/generationsPOSTapplication/jsonText-to-image
{base}/images/editsPOSTmultipart/form-dataImage edit / multi-image fusion
{base}/modelsGET—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)AliasStatusNotes
gpt-image-2base✅ OKDefault
gpt-image-2-5-flareflare✅ OKVariant, different style
gpt-image-2-5-sunburstsunburst✅ OKVariant, different style
gpt-image-2-propro / hq✅ OKHigher-detail variant

Which model gets used

SettingEffect
model parameterUsed for that one call. Any model ID works — aliases are optional.
IMAGE2_DEFAULT_MODELUsed when model is omitted. Defaults to gpt-image-2 — change it if your relay doesn't serve that name.
IMAGE2_MODEL_ALIASESDefine 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.

ParameterRequiredDescription
prompt✅Image description; more detail is better
modelA model ID your relay actually serves — see 1.3
size1024x1024 / 1024x1536 / 1536x1024 / auto
qualitylow / medium / high / auto
nCount, 1–4
backgroundopaque / transparent / auto
output_formatpng / jpeg / webp
out_dirOutput directory, default ./output
basenameFilename 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.

ParameterRequiredDescription
prompt✅What to change, e.g. "replace the background with a snowy sunset"
image_paths✅Array of local input paths, 1–8 images
mask_pathOptional mask (PNG; transparent area = region to repaint)
othersSame 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:

AliasResolves to
basegpt-image-2
pro / hqgpt-image-2-pro
flaregpt-image-2-5-flare
sunburstgpt-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

ParameterValuesDefaultNotes
modelbase / flare / sunburst / pro or full IDbasesee table above
size1024x1024 / 1024x1536 (portrait) / 1536x1024 (landscape) / auto1024x1024server may adjust
qualitylow / medium / high / autohighaffects latency and tokens
n1–41more images = slower and pricier
backgroundopaque / transparent / autoopaquetransparent yields transparent PNG
output_formatpng / jpeg / webppngsee section 9
out_dirpath./outputrelative paths resolve against the server root
basenamefilename prefixautohandy 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:

ClientConfig file
Claude Desktopclaude_desktop_config.json
Cursor~/.cursor/mcp.json
WorkBuddy~/.workbuddy/mcp.json (note: not .mcp.json)
Otherssee 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.

FieldValue
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

VariableDefaultDescription
IMAGE2_BASE_URL—Required. Relay root, without /images/...
IMAGE2_API_KEY—Required.
IMAGE2_OUTPUT_DIR./outputOutput dir; relative paths resolve against server root
IMAGE2_TIMEOUT_MS300000Per-request timeout (ms); max 2147483647
IMAGE2_DEFAULT_MODELgpt-image-2Model 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_SIZE1024x1024Default size
IMAGE2_DEFAULT_QUALITYhighDefault quality
IMAGE2_EDIT_FIELDimagemultipart 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

CommandWhat it does
npm testFull test suite via Node's built-in test runner
npm run lintESLint over the repository
npm run lint:fixESLint with automatic fixes
npm run formatFormat everything with Prettier
npm run format:checkVerify formatting without writing (what CI runs)
npm run smokeReal 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:

FieldMeaning
formatthe format actually written to disk (from the bytes)
requested_formatwhat 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 namedata[0]sizequality
imageb64_json + revised_prompt1254x1254 (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

↑↓ navigate openesc close