muse-image-mcp
v0.1.3
Published
MCP server for Meta Muse image generation
Maintainers
Readme
muse-image-mcp
An MCP server that gives any MCP-capable agent — Claude Code, Claude Desktop, Cursor — image generation powered by the Meta Muse image model.
What it does
Three tools, covering the full loop of working with images in a conversation:
| Tool | What it's for |
|---|---|
| generate_image | Text to image. 1–10 images per call. |
| edit_image | Edit from reference images — local files or URLs. |
| iterate_image | Conversational refinement. Keep saying "make it warmer" and it remembers. |
Why this server
Your context window survives. Every tool writes images to disk and returns an absolute file path — never the image bytes. Generating a dozen images costs you a dozen lines of context instead of a dozen megabytes of base64. When you actually want to look at an image, open the path with a file-reading tool.
Multi-turn refinement without state. iterate_image returns a response_id; feed it back as previous_response_id and the next turn continues the same conversation. The server itself stores nothing — conversation state lives on Meta's side, so the server stays restartable and stateless.
New models don't require a new release. Switch models with MUSE_MODEL or a per-call model parameter, and pass parameters this server has never heard of through extra_params. Fields that determine request structure are protected from being overwritten; everything else is a deliberate escape hatch.
You always know what it cost. Every response ends with the estimated cost of that call.
Requirements
- Node.js >= 20.12.0
- A Meta Muse API key
Getting an API key
Muse Image runs on the Meta Model API, so you register with Meta — not with this project:
- Go to https://dev.meta.ai and sign in to the Meta Model API dashboard.
- Open API keys.
- Click Create API key and copy the value.
That value is what you pass as MUSE_API_KEY below. Meta's own documentation calls this variable MODEL_API_KEY; this server reads it as MUSE_API_KEY, talks to https://api.meta.ai/v1, and defaults to the model muse-image-1.0.
Reference: Model API docs · Image generation · Muse Image announcement
Keep the key out of source control — use the MCP config's env block or a .env file, both described below.
Install
Option 1: npx (recommended — no clone required)
claude mcp add muse-image --scope user --env MUSE_API_KEY=your-key -- npx -y muse-image-mcpnpx fetches and runs the latest version on demand — nothing to install first.
--scope userapplies it to every project; use--scope localfor the current project only.-yskips npx's install prompt. Without it the server hangs on an interactive question and the handshake fails.- Everything after
--is the launch command;--envbefore it belongs toclaude mcp add.
For other MCP clients (Claude Desktop, Cursor), write the config by hand:
{
"mcpServers": {
"muse-image": {
"command": "npx",
"args": ["-y", "muse-image-mcp"],
"env": { "MUSE_API_KEY": "your-key" }
}
}
}On Windows, if npx can't be found, use "command": "cmd" with "args": ["/c", "npx", "-y", "muse-image-mcp"].
Option 2: Local clone (when you want to change the code)
git clone https://github.com/kevintsai1202/muse-image-mcp.git
cd muse-image-mcp
npm install
npm run build
claude mcp add muse-image --scope user --env MUSE_API_KEY=your-key -- node <your-clone-path>/dist/index.jsAfter installing
Start a new session — MCP servers are loaded at session start, so an existing session won't pick it up. Then confirm with claude mcp list, which should show muse-image: ... - Connected, and check that the three mcp__muse-image__* tools are available.
Configuring the API key
Precedence is the env block in your MCP config > a .env file.
Using the MCP config env block (the only route when installed via npx)
See --env MUSE_API_KEY=your-key above.
Using a .env file
The server searches these locations in order and uses the first one that exists:
<package root>/.env— convenient for a local clone~/.muse-image-mcp/.env— the only location you control when installed via npx
MUSE_API_KEY=your-keyNote that .env is not read from the directory you launched Claude Code in — an MCP server's working directory is decided by the client, which makes it a poor place for configuration. When installed via npx the package itself lives in a hashed npm cache directory that gets cleaned up, so a .env there would be pointless.
Environment variables
All of these work in either .env or your MCP config's env block.
| Variable | Required | Default | Description |
|---|---|---|---|
| MUSE_API_KEY | Yes | — | API key. Without it the server exits immediately and explains itself on stderr |
| MUSE_MODEL | No | muse-image-1.0 | Global default model ID |
| MUSE_EXTRA_PARAMS | No | {} | JSON object string — global default extra parameters |
| MUSE_OUTPUT_DIR | No | <cwd>/generated-images | Output directory, created if missing |
| MUSE_BASE_URL | No | https://api.meta.ai/v1 | API base URL |
| MUSE_TIMEOUT_MS | No | 120000 | Per-request timeout in milliseconds |
The
cwdinMUSE_OUTPUT_DIR's default is the working directory the MCP client launched the server from. In Claude Code that's the project root of your session, so images land in that project'sgenerated-images/. If your client behaves differently, or you want a fixed location, setMUSE_OUTPUT_DIRto an absolute path.Since v0.1.0 the default output directory changed from
muse-output/togenerated-images/. The old directory is not deleted or migrated automatically.
Switching models and passing new parameters
When a new model ships you don't have to wait for this project to update — switch models with an environment variable, send new parameters through extra_params.
Switching models
Globally, in .env or your MCP config:
MUSE_MODEL=muse-image-2.0Per call, just ask for it in conversation and the agent will pass model:
{ "prompt": "a red fox", "model": "muse-image-2.0" }Passing new parameters
Global defaults as a JSON object string:
MUSE_EXTRA_PARAMS={"quality":"ultra"}Per-call overrides via extra_params, merged with the global setting — the per-call value wins:
{ "prompt": "a red fox", "extra_params": { "style_preset": "anime" } }Protected core fields
model, prompt, response_format, images, input, store, and previous_response_id determine the structure of the request and cannot be overwritten by extra_params. Setting them there has no effect, and the response will end with a warning listing the ignored keys.
To change models, use the model parameter or MUSE_MODEL — not extra_params.
Outside those core fields, extra_params does override same-named regular parameters, including n, size, output_format, and reasoning_strength. The tool schema's validation for these (for example n being limited to 1–10) does not apply on this path — that's a deliberate escape hatch so a future model that changes parameter semantics isn't blocked by today's limits. When overriding n this way, watch your image count and cost.
Tools
Every tool saves images locally and returns absolute paths, never the image content itself — this keeps base64 out of your conversation context. Open the path with a file-reading tool when you want to see the image.
generate_image — text to image
| Parameter | Required | Default | Description |
|---|---|---|---|
| prompt | Yes | — | Image description |
| n | No | 1 | Number of images, 1–10 |
| size | No | — | Aspect ratio string such as 1792x1024 — not an exact pixel resolution |
| output_format | No | png | png / webp / jpeg |
| reasoning_strength | No | high | high / low — priced the same |
| filename_prefix | No | muse | Output filename prefix |
| model | No | — | Model ID; omit to use the server default (see MUSE_MODEL) |
| extra_params | No | — | Object of extra parameters, merged with MUSE_EXTRA_PARAMS with per-call priority; core fields are protected (see above) |
edit_image — edit from reference images
| Parameter | Required | Default | Description |
|---|---|---|---|
| prompt | Yes | — | Image description |
| images | Yes | — | Array of local file paths (png/jpg/jpeg/webp/gif) or http(s) URLs. Local files are base64-encoded automatically |
| n | No | 1 | Number of images, 1–10 |
| size | No | — | Aspect ratio string such as 1792x1024 — not an exact pixel resolution |
| output_format | No | png | png / webp / jpeg |
| reasoning_strength | No | high | high / low — priced the same |
| filename_prefix | No | muse-edit | Output filename prefix |
| model | No | — | Model ID; omit to use the server default |
| extra_params | No | — | Same merge and protection rules as above |
iterate_image — conversational refinement
| Parameter | Required | Description |
|---|---|---|
| prompt | Yes | This turn's instruction |
| previous_response_id | No | The id returned by the previous turn; omit to start a new conversation |
| images | No | Reference images for the first turn |
| reasoning_strength | No | Defaults to high |
| size | No | Aspect ratio string such as 1024x1536 |
| output_format | No | png / webp / jpeg. Unlike the other two tools, omitting it yields webp — that's Meta's default for this endpoint |
| filename_prefix | No | Defaults to muse-iter |
| model | No | Model ID; omit to use the server default |
| extra_params | No | Same merge and protection rules as above |
This tool has no n parameter — the /v1/responses endpoint returns one image per turn. It does support size and output_format, but they travel inside the request's tools entry rather than at the top level; see the findings below.
The response always includes a response_id. Pass it as previous_response_id on the next call to continue the same conversation. This server stores no conversation state; Meta does.
Findings from /v1/responses
Request body: per-image settings live inside tools
reasoning_strength, size and output_format are not top-level parameters on this endpoint — they belong to the image_generation tool entry. Sending reasoning_strength at the top level is not merely ignored; the API rejects the whole request:
HTTP 400 — unknown parameter `reasoning_strength`The correct shape:
{
"model": "muse-image-1.0",
"input": "now make the background deep navy",
"store": true,
"previous_response_id": "resp_abc123",
"tools": [
{ "type": "image_generation", "reasoning_strength": "low", "size": "1024x1536", "output_format": "png" }
]
}Versions up to 0.1.0 sent reasoning_strength at the top level, which made iterate_image fail on every call. generate_image and edit_image were never affected — they use /images/generations and /images/edits, where these are legitimate top-level parameters.
This also corrects an earlier claim in this README: size and output_format are supported here. Specifying output_format: "png" returns a genuine PNG (verified by the base64 header) rather than the endpoint's webp default. Only n is genuinely unavailable — one image per turn.
scripts/probe-responses-api.mjs reproduces all of this against the live API, one hypothesis per case.
Response shape
Meta doesn't publish the response schema for /v1/responses. The original implementation was an educated guess modeled on OpenAI's Responses convention; it was later verified against real API calls. The actual structure:
{
"model": "muse-image-1.0",
"id": "resp_6aa4c23f99592ae0ac454928",
"object": "response",
"status": "completed",
"output": [
{ "type": "reasoning", "summary": [{ "type": "summary_text", "text": "..." }] },
{ "type": "message", "role": "assistant", "content": [{ "type": "output_text", "text": "" }] },
{ "type": "image_generation_call", "id": "ig_...", "status": "completed", "result": "<base64 image data>" }
]
}Differences from the guessed shape:
idlocation:raw.idwas correct, confirmed by testing. No change needed.- Image data location: not in any
b64_jsonfield, but in theresultfield of theoutput[]entry wheretype === "image_generation_call", as raw base64 with no data URL prefix.src/muse-client.ts'sextractB64Imagesnow recognizes bothb64_json(kept for other possible shapes) and this verified shape. - No
output_formatfield: the response has none. The original fallback default of"png"was wrong — since the iterate request doesn't send anoutput_formatparameter, Meta applies the same default as/images/generations, which iswebp. The returned base64 decodes to a genuine WebP file (RIFF/WEBP header). The fallback is now"webp".
Multi-turn conversation is verified
Two real API calls were made in sequence: one iterate_image to obtain a response_id, then a second call using that id as previous_response_id. The second turn returned only the one image generated in that turn — it did not re-send the first turn's image. extractB64Images's deep-traversal logic needed no changes for this.
(The test tooling in that run didn't capture the second turn's raw response byte-for-byte, so this conclusion rests on filesystem evidence — exactly one output file, with no -2/-3 suffixes — rather than a byte-level comparison. The pinning test added in tests/muse-client.test.ts uses the real response shape verified earlier.)
Pricing
US$0.01 per generated image, regardless of reasoning_strength. Every tool response discloses the estimated cost of that call.
This server is free and MIT-licensed — you pay Meta for API usage, nothing else.
Development
npm test # Unit tests (never hits the real API)
npm run build # Compile to dist/
npm run smoke # Real-API smoke test; needs MUSE_E2E=1 and a real key. Generates 6 images, about US$0.06License
MIT © Kevin Tsai
