Skip to main content
Glama
serral

openrouter-media

by serral

openrouter-media-mcp

A local MCP server, logical name openrouter-media, that exposes OpenRouter's image generation, video generation, image-to-video, and model discovery to local AI clients (Claude Code, OpenCode, OMP, Codex). Everything runs stdio, local today; a remote transport is possible future work.

Architecture

 Claude Desktop  (not currently registered)

 Claude Code     ----\
 OpenCode        -----> wrapper (bin/openrouter-media-mcp) --> server.py --> OpenRouter API
 OMP             ----/        (loads the key,                 (FastMCP,
 Codex            ---/          execs server)                   stdio)

Every client points at the same wrapper executable. The wrapper is the only place that touches the secret. The server never receives the key from a client config file.

Verified API cheat sheet

Source: https://openrouter.ai/docs/guides/overview/multimodal/video-generation

Purpose

Method and path

Notes

Submit video

POST /api/v1/videos

Fields: model, prompt (required); optional duration, resolution, aspect_ratio, size, frame_images (image-to-video), input_references, generate_audio, seed, callback_url, provider. If both frame_images and input_references are set, frame_images wins.

Video status

GET /api/v1/videos/{id}

Status: pending, in_progress, completed, failed.

Video download

GET /api/v1/videos/{id}/content?index=0

Video model list

GET /api/v1/videos/models

Video models do not appear in the plain GET /api/v1/models list.

Image, dedicated

POST /api/v1/images

Fields: model, prompt, optional reference images, aspect_ratio or image_size. Response data[] with b64_json and media_type.

Image, via chat

POST /api/v1/chat/completions

With modalities: ["image","text"]. Images come back in message.images[].image_url.url as data URLs.

Model discovery

GET /api/v1/models

Filter on output_modalities containing "image" for image models. List at runtime instead of hardcoding IDs.

Auth

Header Authorization: Bearer <OPENROUTER_MEDIA_KEY>

Other notes: polling interval suggested by the docs is about 30 s; generation takes 30 s to several minutes. Pricing is per video-second with resolution tiers, see docs/models.md for current rates. Result retention window is unconfirmed by OpenRouter; download promptly on completed.

Tool surface

Tool

Purpose

list_video_models

Calls GET /videos/models.

list_image_models

Calls GET /models, filtered on output_modalities containing image.

generate_image

Calls POST /images, persists the image into the media dir, returns the file path (optionally as MCP image content). Never returns raw base64 in tool text: one image is megabytes of model context.

submit_video

Calls POST /videos, returns a job id immediately. Optional first_frame (image inside the media dir) for image-to-video. Optional generate_audio: omit to use the API default (audio-capable models bill at the with-audio rate), or pass false to force silent output at the cheaper rate where supported.

get_video_status

Calls GET /videos/{id}, returns status and progress.

download_video

Calls GET /videos/{id}/content (exposes index, default 0), writes the MP4 into the media dir, returns the path.

generate_video_and_wait

Convenience: submit, then poll every 10 to 15 s with backoff. Accepts an optional job_id to resume an existing job instead of submitting a new one (resubmission double-bills). Emits MCP progress notifications while polling. Auto-downloads when status reaches completed (retention window unconfirmed). Caps the wait below the client's tool-call timeout (default 120 s) and returns the job id if the cap runs out.

Design rules:

  • No single tool call blocks past a client's tool timeout; the submit / status / download split keeps every call short.

  • No hardcoded default video model. The caller picks from list_video_models; defaults when omitted are short and cheap (4 s, 720p).

  • Path sandboxing: the server computes all output paths from OPENROUTER_MEDIA_DIR, generates filenames itself (timestamp, model slug, short hash), and rejects absolute paths and .. in any argument.

  • Server hygiene: stderr-only logging (stdout is the MCP protocol channel); explicit httpx connect/read timeouts on every API call; a failed job surfaces the API's error text.

Setup

Prerequisites

  • Python and uv installed.

  • An OpenRouter API key. Create one dedicated to this MCP server, with a credit limit set in the dashboard as a cost backstop.

Install

git clone <this repo>
cd openrouter-media-mcp
uv sync

Provide the API key

The wrapper (bin/openrouter-media-mcp) reads the key itself so no client config ever holds it:

  1. macOS Keychain (primary):

    security add-generic-password -s OPENROUTER_MEDIA_KEY -a "$USER" -w "sk-or-..."
  2. .env fallback: copy .env.example to .env in the repo root and fill in OPENROUTER_MEDIA_KEY. .env is gitignored. Used only if the Keychain item above does not exist.

The wrapper execs the project's own venv interpreter (.venv/bin/python server.py) by absolute path, so it works even under a GUI client's minimal PATH (no dependency on uv or npx at runtime).

Environment variables

Variable

Purpose

OPENROUTER_MEDIA_KEY

The OpenRouter API key dedicated to this project. Provided by the wrapper (Keychain primary, .env fallback). Never hardcode this in a committed file.

OPENROUTER_MEDIA_DIR

Where generated images and videos are written. Default: ./media inside the repo, gitignored.

Output path safety

The server only writes inside the configured media dir, rejects absolute paths and .. in tool arguments, and generates filenames itself.

Per-client registration

Server name openrouter-media, wrapper path /absolute/path/to/openrouter-media-mcp/bin/openrouter-media-mcp in every client below. Replace /absolute/path/to/openrouter-media-mcp with the real path to your clone (run pwd inside the repo). No env block is needed in any client config, because the wrapper loads the secret itself and resolves its own repo root at run time.

Registrations are scoped project-local: Claude Code --scope local, OpenCode project opencode.json, OMP project .omp/mcp.json, Codex project .codex/config.toml. No user/global config is touched, so no other project on your machine sees this server.

OpenCode, OMP, and Codex each read a project file that must contain an absolute path, so those three are generated from committed .example templates rather than committed directly:

for f in .codex/config.toml .omp/mcp.json .opencode/opencode.json; do
  sed "s#/absolute/path/to/openrouter-media-mcp#$(pwd)#g" "${f}.example" > "$f"
done

Run that once after cloning, from the repo root. The generated files contain no secrets, only your own clone's path, which is why they stay gitignored.

Claude Code

claude mcp add --scope local --transport stdio openrouter-media -- /absolute/path/to/openrouter-media-mcp/bin/openrouter-media-mcp
claude mcp list
claude mcp get openrouter-media

local scope is stored in ~/.claude.json under the project's path, visible only in sessions opened in this repo. /mcp in a session shows status and lists the 7 tools. Relevant env vars: MCP_TIMEOUT (server startup, ms), MCP_TOOL_TIMEOUT (per tool call, ms).

Note: OMP imports Claude Code's user-scope entries only; a local-scope entry like this one does not propagate to OMP. Configure OMP via its own project file below.

OpenCode

Config lives in .opencode/opencode.json in the repo (not the documented root opencode.json). Generated from the committed .opencode/opencode.json.example template (see above).

{
  "$schema": "https://opencode.ai/config.json",
  "mcp": {
    "openrouter-media": {
      "type": "local",
      "command": ["/absolute/path/to/openrouter-media-mcp/bin/openrouter-media-mcp"],
      "enabled": true,
      "timeout": 180000
    }
  }
}

The server name sits directly under "mcp": there is no "servers" level. Optional fields: cwd, environment, timeout (milliseconds; scopes tool fetching, default 5000 ms).

opencode mcp list shows connection status; opencode mcp add, opencode mcp auth, opencode mcp debug <name> are also available.

OMP (Oh My Pi)

Path: .omp/mcp.json in the repo (project scope, applies under every OMP profile). The user-scope file ~/.omp/agent/mcp.json is deliberately not used. Generated from the committed .omp/mcp.json.example template.

{
  "$schema": "https://raw.githubusercontent.com/can1357/oh-my-pi/main/packages/coding-agent/src/config/mcp-schema.json",
  "mcpServers": {
    "openrouter-media": {
      "type": "stdio",
      "command": "/absolute/path/to/openrouter-media-mcp/bin/openrouter-media-mcp",
      "timeout": 180000
    }
  }
}

timeout is not optional here. OMP's default MCP request timeout is 30 s; a 120 s generate_video_and_wait call would be killed without this field. The process-wide OMP_MCP_TIMEOUT_MS env var overrides per-server values.

OMP also imports MCP definitions from other tools' user-scope files (Claude Code, Codex, OpenCode, Cursor, Windsurf, VS Code, Gemini CLI). Because this rollout registers nothing at user scope, nothing leaks into OMP through discovery. In-app commands: /mcp list, /mcp add, /mcp reload, /mcp test openrouter-media.

Codex CLI

Project file .codex/config.toml, generated from the committed .codex/config.toml.example template. Do not touch the global ~/.codex/config.toml.

[mcp_servers.openrouter-media]
command = "/absolute/path/to/openrouter-media-mcp/bin/openrouter-media-mcp"
codex mcp add openrouter-media -- /absolute/path/to/openrouter-media-mcp/bin/openrouter-media-mcp

Claude Desktop

Not currently registered: Claude Desktop's config is app-global rather than project-scoped, so it is out of scope for this project-local rollout. If you want it anyway, add an openrouter-media entry under mcpServers in ~/Library/Application Support/Claude/claude_desktop_config.json pointing at the same wrapper path, then restart Claude Desktop.

Security notes

  • Key handling. No client config contains the key; every client references only the wrapper's path. Only the wrapper touches the secret.

  • Key naming and source. OPENROUTER_MEDIA_KEY is the reserved name for the OpenRouter API key dedicated to this project. Primary source: macOS Keychain via security find-generic-password (no install needed). Fallback: the gitignored .env in the repo root, only if Keychain is unavailable. Shell-only exports (e.g. .zshrc) are not sufficient for GUI apps such as Claude Desktop, which do not inherit shell environment.

  • Dedicated key. Create one OpenRouter API key for this MCP with a credit limit set in the dashboard as the cost backstop.

  • Residual exposure. The wrapper exports the key into the server process environment, where the same user can read it via ps -E. Acceptable for a single-user machine; if that assumption changes, move the key read inside the server, per call.

  • Never commit secrets. .gitignore covers .env, media/, and .venv/ from the first commit.

  • Path sandboxing. See "Output path safety" above.

Pricing

Video is billed per video-second, with resolution tiers, and can range from cents to several dollars per clip depending on model, duration, and resolution. Defaults are short and cheap (4 s, 720p); opt into a specific model and duration for anything larger. Set a credit limit on the dedicated key in the OpenRouter dashboard as a backstop. See docs/models.md for current per-model rates.

Troubleshooting

Symptom

Likely cause

Claude Desktop can't find the key

GUI apps do not inherit .zshrc. The wrapper loads the key itself; check the Keychain item exists: security find-generic-password -s OPENROUTER_MEDIA_KEY -a "$USER" -w.

Server fails to start only from a GUI client

A PATH-dependent binary inside the wrapper (uv, npx). The wrapper must exec .venv/bin/python by absolute path.

One-time Keychain prompt on first GUI use

Expected Keychain ACL behavior. Choose "Always Allow" once, re-test.

An OMP tool call dies after ~30 s

OMP's default MCP request timeout. Set "timeout": 180000 on the server entry (or OMP_MCP_TIMEOUT_MS).

A video tool call times out client-side

Long single-call polling. Use the submit_video / get_video_status / download_video split, or generate_video_and_wait with its 120 s cap.

A video model does not show up in model lists

Video models are not in GET /api/v1/models. Use GET /api/v1/videos/models.

download_video 404s long after completion

OpenRouter's result retention window is unconfirmed. Download promptly; generate_video_and_wait auto-downloads on completed.

OpenCode config seems to be ignored

Wrong nesting: the server name goes directly under "mcp", with no "servers" level.

Codex does not pick up the config

Confirm the project .codex/config.toml exists and Codex is installed and on PATH; the global ~/.codex/config.toml is deliberately left alone.

Documentation

  • docs/models.md: current image and video model list and pricing.

  • docs/design.md: design notes and rationale behind the architecture and client-registration approach.

Related MCP Connectors