OpenRouter MCP Multimodal — MCP server for chat, vision, audio, and video AI tools

OpenRouter MCP Multimodal

The MCP server for multimodal AI agents.
One install · 19 tools · 300+ OpenRouter models · text, vision, audio & video — analysis and generation.

npm version PyPI version GitHub release Docker version CI status Apache 2.0 license Node.js 22+

npm downloads Docker pulls MCP Registry Smithery MCP registry

Quick start · Tools · Examples · Security · Troubleshooting · Development · Releasing · FAQ

--- ## What is this? **OpenRouter MCP Multimodal** is a production-grade [Model Context Protocol (MCP)](https://modelcontextprotocol.io) server — listed on the [official MCP Registry](https://registry.modelcontextprotocol.io/servers/io.github.stabgan/openrouter-multimodal) as `io.github.stabgan/openrouter-multimodal`. It connects AI coding agents ([Cursor](https://cursor.com), [Claude Desktop](https://claude.ai/download), [VS Code](https://code.visualstudio.com), [Windsurf](https://codeium.com/windsurf), [Cline](https://github.com/cline/cline), and others) to [OpenRouter](https://openrouter.ai)'s unified LLM API over stdio. Unlike text-only MCP servers, one install covers the **full multimodal surface**: | Capability | Tools | Highlights | | :---------- | :-------------------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Chat** | `chat_completion`, `start_chat_completion`, `get_chat_completion_status` | 300+ models, `:nitro` / `:floor` / `:free` / `:online` / `:exacto` suffixes, provider routing, web search, response caching, reasoning tokens, async jobs for long-running models | | **Vision** | `analyze_image`, `generate_image`, `generate_image_dedicated` | OCR, captioning, VQA, image generation with reference inputs, dedicated Image API with resolution/quality/format control | | **Audio** | `analyze_audio`, `generate_audio`, `text_to_speech`, `speech_to_text` | Transcription, speech/music generation, dedicated TTS (free Deepgram default; model-specific voices, mp3/pcm), dedicated STT (Whisper/GPT-4o Transcribe) | | **Video** | `analyze_video`, `generate_video`, `generate_video_from_image`, `get_video_status` | Clip understanding, Veo 3.1 / Seedance 2.0 / Wan 2.7 generation with progress notifications | | **Catalog** | `search_models`, `get_model_info`, `validate_model`, `rerank_documents`, `health_check` | Model discovery, validation, reranking, ops health | **Production hardening:** input/output path sandboxes (including analyze\_\* local files as of v4.5.2), SSRF guards, structured errors with `_meta.code`, MCP 2025-06-18 structured outputs, tool icons (2025-11-25), async video progress notifications, and **1000+** automated tests (unit, mock, regression, and live integration). ## Quick start **1. Get an API key** (free tier works) → [openrouter.ai/keys](https://openrouter.ai/keys) **2. Run the server** ```bash export OPENROUTER_API_KEY=sk-or-v1-... npx -y @stabgan/openrouter-mcp-multimodal ``` **3. Add to your MCP client** — copy one JSON block from [Install](#install) into your client config: | Client | Config location | | :----------------- | :-------------------------------------------------------------------------------------------------------------------------------- | | **Cursor** | Project: `.cursor/mcp.json` · User: Cursor Settings → MCP | | **Claude Desktop** | macOS: `~/Library/Application Support/Claude/claude_desktop_config.json` · Windows: `%APPDATA%\Claude\claude_desktop_config.json` | | **VS Code** | `.vscode/mcp.json` (workspace) or User Settings → MCP | | **Windsurf** | Windsurf Settings → MCP (same `mcpServers` JSON shape as Cursor) | Use the `mcpServers` object from [Manual config](#manual-config) below. > **No credits required to start.** Free models such as `google/gemma-4-26b-a4b-it:free` work for chat and vision. Video/audio generation typically needs credits. ## Install MCP servers are distributed through several packaging models. **This server is implemented in Node.js/TypeScript**; the table below maps each ecosystem method to how you run it here. | Method | Runtime | Best for | This server | | :-------------------------------------- | :------------------------------- | :------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------- | | **[npx](#manual-config)** | Node.js 22+ | Most MCP clients (default) | ✅ `@stabgan/openrouter-mcp-multimodal` | | **[uvx / pipx](#manual-config)** | Python 3.10+ **and** Node.js 22+ | Python-first workflows, same pattern as PyPI MCP servers | ✅ [`mcp-server-openrouter-multimodal`](https://pypi.org/project/mcp-server-openrouter-multimodal/) | | **[npm global](#manual-config)** | Node.js 22+ | Pin a version without re-downloading | ✅ | | **[node (local)](#manual-config)** | Node.js 22+ | Contributors / air-gapped builds | ✅ | | **[Docker Hub](#manual-config)** | Docker | Isolation, no Node on host | ✅ `stabgan/openrouter-mcp-multimodal` | | **[GHCR](#manual-config)** | Docker | GitHub-native OCI pulls | ✅ `ghcr.io/stabgan/openrouter-mcp-multimodal` | | **[Smithery CLI](#smithery)** | Node.js (via installer) | Interactive install into Claude/Cursor/etc. | ✅ | | **[MCP Registry](#mcp-registry)** | npm or OCI | Official discovery (`io.github.stabgan/openrouter-multimodal`) | ✅ [listing](https://registry.modelcontextprotocol.io/servers/io.github.stabgan/openrouter-multimodal) | | **[One-click deeplinks](#one-click)** | Node.js | Cursor, VS Code, Kiro | ✅ | | **[Claude Code CLI](#claude-code-cli)** | Node.js | Terminal-first Claude Code users | ✅ | | **[MCP Inspector](#mcp-inspector)** | Node.js | Debug / list tools locally | ✅ | | **Windows `cmd /c npx`** | Node.js | Claude Desktop / Cursor when `npx` not on GUI PATH | ✅ [see below](#windows-npx) | | pip / uv (direct) | — | Native Python MCP servers only | — use **uvx** row above | | DXT desktop extensions | — | Bundled Claude Desktop `.dxt` | not yet | | Remote HTTP / SSE | — | Hosted Smithery / Cloudflare endpoints | via [Smithery](https://smithery.ai/server/@stabgan/openrouter-mcp-multimodal) | > **uvx vs npx:** In the MCP ecosystem, **`npx` runs npm (Node) packages** and **`uvx` runs PyPI (Python) packages**. Because this server is Node-based, `uvx` uses a thin [Python launcher](./python/) that execs `npx -y @stabgan/openrouter-mcp-multimodal` — you still need Node installed. ### One-click
CursorAdd OpenRouter MCP to Cursor
VS CodeAdd to VS Code
KiroAdd to Kiro
Claude Desktop / Windsurf / ClineManual JSON config (pick any method below)
Smitherynpx -y @smithery/cli install @stabgan/openrouter-mcp-multimodal --client claude
MCP RegistryOfficial registry page — npm + OCI packages
Paste your `OPENROUTER_API_KEY` when prompted — deeplinks use placeholders so secrets never appear in URLs. ### Manual config
npx (recommended) ```bash export OPENROUTER_API_KEY=sk-or-v1-... npx -y @stabgan/openrouter-mcp-multimodal ``` ```json { "mcpServers": { "openrouter": { "command": "npx", "args": ["-y", "@stabgan/openrouter-mcp-multimodal"], "env": { "OPENROUTER_API_KEY": "sk-or-v1-..." } } } } ``` Pin a release: `"args": ["-y", "@stabgan/openrouter-mcp-multimodal@5.0.1"]`
uvx / pipx (Python launcher) Install [uv](https://docs.astral.sh/uv/getting-started/installation/) (includes `uvx`), ensure **Node.js 22+** is also on your `PATH`, then: ```bash export OPENROUTER_API_KEY=sk-or-v1-... uvx mcp-server-openrouter-multimodal # pin npm version: OPENROUTER_MCP_NPM_VERSION=5.0.1 uvx mcp-server-openrouter-multimodal ``` ```json { "mcpServers": { "openrouter": { "command": "uvx", "args": ["mcp-server-openrouter-multimodal"], "env": { "OPENROUTER_API_KEY": "sk-or-v1-..." } } } } ``` **pipx equivalent:** `pipx run mcp-server-openrouter-multimodal` Optional: `OPENROUTER_MCP_NPM_VERSION=5.0.1` pins the underlying npm package.
npm global ```bash npm install -g @stabgan/openrouter-mcp-multimodal ``` ```json { "mcpServers": { "openrouter": { "command": "openrouter-multimodal", "env": { "OPENROUTER_API_KEY": "sk-or-v1-..." } } } } ```
node (local clone) ```bash git clone https://github.com/stabgan/openrouter-mcp-multimodal.git cd openrouter-mcp-multimodal npm ci && npm run build ``` ```json { "mcpServers": { "openrouter": { "command": "node", "args": ["/absolute/path/to/openrouter-mcp-multimodal/dist/index.js"], "env": { "OPENROUTER_API_KEY": "sk-or-v1-..." } } } } ```
Docker ```bash docker run --rm -i -e OPENROUTER_API_KEY=sk-or-v1-... stabgan/openrouter-mcp-multimodal:latest ``` ```json { "mcpServers": { "openrouter": { "command": "docker", "args": [ "run", "--rm", "-i", "-e", "OPENROUTER_API_KEY=sk-or-v1-...", "stabgan/openrouter-mcp-multimodal:latest" ] } } } ``` Use `-i` (interactive stdio). Avoid `-t` (TTY corrupts MCP framing on some hosts).
GHCR (GitHub Container Registry) ```bash docker run --rm -i -e OPENROUTER_API_KEY=sk-or-v1-... \ ghcr.io/stabgan/openrouter-mcp-multimodal:5.0.1 ``` ```json { "mcpServers": { "openrouter": { "command": "docker", "args": [ "run", "--rm", "-i", "-e", "OPENROUTER_API_KEY=sk-or-v1-...", "ghcr.io/stabgan/openrouter-mcp-multimodal:5.0.1" ] } } } ```
Smithery Interactive install (writes config for your client): ```bash npx -y @smithery/cli install @stabgan/openrouter-mcp-multimodal --client claude # or: --client cursor | vscode | windsurf | ... ``` Listing: [smithery.ai/server/@stabgan/openrouter-mcp-multimodal](https://smithery.ai/server/@stabgan/openrouter-mcp-multimodal)
MCP Registry Official name: `io.github.stabgan/openrouter-multimodal` - Registry: [registry.modelcontextprotocol.io](https://registry.modelcontextprotocol.io/servers/io.github.stabgan/openrouter-multimodal) - npm package: `@stabgan/openrouter-mcp-multimodal` - OCI image: `docker.io/stabgan/openrouter-mcp-multimodal` Clients that support registry-driven install will offer npm or Docker; otherwise use the JSON blocks above.
Claude Code CLI ```bash claude mcp add openrouter -- npx -y @stabgan/openrouter-mcp-multimodal # project scope: claude mcp add --scope project openrouter -- npx -y @stabgan/openrouter-mcp-multimodal ``` Set `OPENROUTER_API_KEY` in your shell or client env before starting Claude Code.
MCP Inspector Debug tools/list and tool calls against a live OpenRouter key: ```bash export OPENROUTER_API_KEY=sk-or-v1-... npx -y @modelcontextprotocol/inspector npx -y @stabgan/openrouter-mcp-multimodal ```
Windows npx When Claude Desktop or Cursor cannot find `npx` (GUI apps often miss shell `PATH`), wrap with `cmd`: ```json { "mcpServers": { "openrouter": { "command": "cmd", "args": ["/c", "npx", "-y", "@stabgan/openrouter-mcp-multimodal"], "env": { "OPENROUTER_API_KEY": "sk-or-v1-..." } } } } ``` If still failing, use the full path from `where npx` as the command.
## Why this server? | Capability | This server | Typical MCP LLM servers | | :----------------------------------- | :---------: | :---------------------: | | Text chat (300+ models) | ✅ | ✅ | | Image analysis + generation | ✅ | partial | | Audio analysis + TTS | ✅ | ❌ | | Video analysis + generation | ✅ | ❌ | | Model search / validate / rerank | ✅ | ❌ | | Path sandbox + SSRF protection | ✅ | rare | | MCP 2025 structured outputs | ✅ | rare | | Async video + progress notifications | ✅ | ❌ | ## Tools 19 MCP tools. Each description includes **Use when**, **Good/Bad examples**, **Fails when**, and **Works with** so agents pick the right tool and recover from errors. | Tool | Purpose | | :--------------------------- | :------------------------------------------------------------------------- | | `chat_completion` | Text chat, web search, provider routing, caching, reasoning | | `start_chat_completion` | Async background job for long-running reasoning models | | `get_chat_completion_status` | Poll / retrieve async completion results | | `analyze_image` | Vision — local path, URL, or data URL + `question` | | `analyze_audio` | Transcribe / analyze audio files | | `analyze_video` | Describe / Q&A over video files | | `generate_image` | Text-to-image via chat completions with reference images | | `generate_image_dedicated` | Text-to-image via dedicated `/api/v1/images` (resolution, quality, format) | | `generate_audio` | Text-to-speech / music via chat completions | | `text_to_speech` | Dedicated TTS (`/api/v1/audio/speech`) — free Deepgram default, voices, speed, mp3/pcm | | `speech_to_text` | Dedicated STT (`/api/v1/audio/transcriptions`) — Whisper, GPT-4o | | `generate_video` | Text-to-video (async, resumable) | | `generate_video_from_image` | Image-to-video (narrower schema) | | `get_video_status` | Poll / resume video jobs | | `search_models` | Paginated model catalog search | | `get_model_info` | Pricing, context, modalities | | `validate_model` | Cheap model ID existence check | | `rerank_documents` | Relevance ranking for RAG | | `health_check` | API key + reachability probe | Errors use a closed `_meta.code` taxonomy: `INVALID_INPUT` · `UNSAFE_PATH` · `UPSTREAM_*` · `MODEL_NOT_FOUND` · `JOB_STILL_RUNNING` · and more. ### Binary tool results (v4.7.0+) Generate tools (`generate_image`, `generate_image_dedicated`, `generate_audio`, `text_to_speech`, `generate_video`, `generate_video_from_image`, `get_video_status`) return image, audio, or video bytes. As of **4.7.0** the behavior is explicit: | `save_path` | Tool result | | :---------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Set** | **Text pointer only** — e.g. `Image saved to: out.png (… bytes, image/png)` plus `_meta.save_path`. **No inline base64** (avoids duplicating large payloads in the MCP channel). | | **Unset, under byte ceiling** | Inline media block **and** summary text (images/audio use MCP `image` / `audio` types; video uses MCP `resource` blocks). | | **Unset, over ceiling** | Text only with a hint to pass `save_path`. | Default inline ceilings (override per kind or globally): | Kind | Default | Env vars (precedence: per-kind → global) | | :---- | :------ | :------------------------------------------------------------------ | | Image | 1 MiB | `OPENROUTER_IMAGE_INLINE_MAX_BYTES` → `OPENROUTER_INLINE_MAX_BYTES` | | Audio | 1 MiB | `OPENROUTER_AUDIO_INLINE_MAX_BYTES` → `OPENROUTER_INLINE_MAX_BYTES` | | Video | 10 MiB | `OPENROUTER_VIDEO_INLINE_MAX_BYTES` → `OPENROUTER_INLINE_MAX_BYTES` | If you previously relied on **both** a saved file **and** inline media in the same tool result, read the file from `_meta.save_path` (or omit `save_path` to get inline media when under the ceiling). ## Examples ### Chat (free model) ```json { "tool": "chat_completion", "arguments": { "model": "google/gemma-4-26b-a4b-it:free", "messages": [{ "role": "user", "content": "Summarize MCP in one sentence." }] } } ``` ### Analyze an image ```json { "tool": "analyze_image", "arguments": { "image_path": "diagram.png", "question": "List every label in this diagram." } } ``` > Use `image_path` and `question` — not `image` / `prompt`. ### Search models (vision + free) ```json { "tool": "search_models", "arguments": { "query": "gemma", "capabilities": { "vision": true }, "limit": 10, "offset": 0 } } ``` ### Generate video (async) ```json { "tool": "generate_video", "arguments": { "model": "google/veo-3.1", "prompt": "Ocean waves at sunrise, cinematic drone shot", "duration": 4, "save_path": "river.mp4" } } ``` If the job is still running when `max_wait_ms` elapses, the response succeeds with `_meta.code: JOB_STILL_RUNNING` and a `video_id` — call `get_video_status` to resume. **This is not an error.** With `save_path` set (as above), the result is a **text pointer** to the saved file once complete — not inline video. See [Binary tool results](#binary-tool-results-v470). More examples: [docs/plans/tool-description-improvement.md](./docs/plans/tool-description-improvement.md) ## Security - **Input path sandbox** — local paths on `analyze_*` and reference images must stay inside `OPENROUTER_INPUT_DIR` (falls back to `OPENROUTER_OUTPUT_DIR`, then `cwd`) - **Output path sandbox** — `save_path` must stay inside `OPENROUTER_OUTPUT_DIR` - **Async job reads** — `get_chat_completion_status` resolves disk paths only under `OPENROUTER_OUTPUT_DIR/openrouter-jobs/` (4.7.0+) - **SSRF protection** — private/reserved IPs blocked on URL fetches - **Untrusted content** — analyze outputs tagged `_meta.content_is_untrusted: true` Override sandboxes only with `OPENROUTER_ALLOW_UNSAFE_PATHS=1` (discouraged). Report vulnerabilities: **[SECURITY.md](./SECURITY.md)** (private disclosure — do not file public issues for exploits). ## Configuration
Environment variables | Variable | Required | Default | Description | | :---------------------------------- | :------: | :------------------------------------ | :---------------------------------- | | `OPENROUTER_API_KEY` | **Yes** | — | OpenRouter API key | | `OPENROUTER_DEFAULT_MODEL` | No | `google/gemma-4-26b-a4b-it:free` | Default when tools omit `model` | | `OPENROUTER_OUTPUT_DIR` | No | `cwd` | Sandbox root for `save_path` | | `OPENROUTER_INPUT_DIR` | No | `OUTPUT_DIR` or `cwd` | Sandbox root for local input files | | `OPENROUTER_INLINE_MAX_BYTES` | No | `1048576` (image/audio) | Global inline media ceiling | | `OPENROUTER_IMAGE_INLINE_MAX_BYTES` | No | falls back to global | Per-kind inline ceiling | | `OPENROUTER_AUDIO_INLINE_MAX_BYTES` | No | falls back to global | Per-kind inline ceiling | | `OPENROUTER_VIDEO_INLINE_MAX_BYTES` | No | `10485760` | Video inline ceiling | | `OPENROUTER_LOG_LEVEL` | No | `info` | `error` / `warn` / `info` / `debug` | See [`.env.example`](./.env.example) for the full list (provider routing, fetch limits, caching, video polling, async jobs, integration-test overrides).
## Development ```bash git clone https://github.com/stabgan/openrouter-mcp-multimodal.git cd openrouter-mcp-multimodal npm install cp .env.example .env # add OPENROUTER_API_KEY npm run build ``` ### Testing | Command | What it runs | | :------------------------- | :--------------------------------------------------------- | | `npm test` | **1018** unit + mock tests (no API key, <20s) | | `npm run test:regression` | Security + schema regression guards | | `npm run test:integration` | **16** live OpenRouter scenarios (**requires** `.env` key) | | `npm run test:e2e` | Full MCP stdio smoke (`scripts/live-e2e.mjs`) | | `npm run ci` | lint + format + build + **all** of the above except e2e | **Free models for CI / zero-credit accounts:** integration tests default to `google/gemma-4-26b-a4b-it:free` (override with `OPENROUTER_INTEGRATION_MODEL`). GitHub Actions requires the `OPENROUTER_API_KEY` repository secret. Mock tests live under `src/__tests__/mock/` and cover handlers, path sandboxes, SSRF blocks, model-cache pagination, tool descriptions, and structured outputs — **330+** additional cases beyond the core suite. ```bash npm run lint npm run format:check npm run version:check # package.json vs src/version.ts, server.json, pyproject.toml ``` ## Releasing Published artifacts (**npm**, **PyPI/uvx**, **Docker**, **GHCR**) all ship from the **same semver** on a git tag (`vX.Y.Z`). Pushing to `main` runs tests but does **not** publish to npm or PyPI. **Normal flow:** merge conventional commits to `main` → [Release Please](https://github.com/googleapis/release-please) opens a Release PR → merge it → tag is created → CI publishes everywhere. **Manual flow:** bump all version files → `npm run version:check` → `npm run ci` + smoke tests → commit → `git tag vX.Y.Z` → `git push origin vX.Y.Z`. Full checklist, file list, CI secrets, and agent instructions: - **[`docs/RELEASING.md`](docs/RELEASING.md)** — maintainer release guide - **[`AGENTS.md`](AGENTS.md)** — quick reference for AI agents ## Troubleshooting | Symptom | Likely cause | Fix | | :---------------------------------------------------------- | :--------------------------------------- | :-------------------------------------------------------------------------------------------------------------- | | Server exits immediately / `OPENROUTER_API_KEY is required` | Missing or empty API key | Set `OPENROUTER_API_KEY` in client `env` or shell — get one at [openrouter.ai/keys](https://openrouter.ai/keys) | | `_meta.code: INVALID_CREDENTIALS` or HTTP 401 | Bad or revoked key | Regenerate at [openrouter.ai/keys](https://openrouter.ai/keys); restart the MCP client | | `_meta.code: MODEL_NOT_FOUND` | Typo or retired model ID | Run `search_models` or `validate_model`; check [openrouter.ai/models](https://openrouter.ai/models) | | HTTP 402 / insufficient credits | Paid model or generation on zero balance | Add credits at [openrouter.ai/credits](https://openrouter.ai/credits) or use a `:free` model | | `_meta.code: UPSTREAM_HTTP` with 429 | Rate limit | Wait for `_meta.retry_after_seconds` if present; reduce concurrency | | `_meta.code: UNSAFE_PATH` | Local path outside sandbox | Put files under `OPENROUTER_INPUT_DIR` or set `OPENROUTER_OUTPUT_DIR` wider; see [Security](#security) | | `npx` not found (Windows GUI apps) | GUI `PATH` differs from terminal | Use the [Windows npx](#windows-npx) `cmd /c` wrapper | | No inline image/audio after upgrade | **v4.7.0** with `save_path` set | Expected — result is text + `_meta.save_path` only; omit `save_path` or read the saved file | | MCP client shows stale tool list | Client cache | Restart MCP / reload window after upgrading the package pin | Structured errors include `_meta.suggestions` with agent-oriented next steps when available. ## FAQ ### Do I need paid OpenRouter credits? No, to get started. Free models work for chat and vision. Audio/video **generation** usually requires credits; analysis may return `402` on some models — the server surfaces that as a structured error. ### Which MCP clients are supported? Any MCP-compatible client over stdio: Cursor, Claude Desktop, VS Code Copilot, Windsurf, Cline, Kiro, and custom agents. ### How is this different from calling OpenRouter directly? This server adds MCP tool schemas, security sandboxes, error taxonomy, model caching, async video polling with progress notifications, and agent-oriented tool descriptions — so LLMs invoke the right capability without custom HTTP glue. ### Where is the security advisory for path traversal? Fixed in 4.5.2+ — see [GHSA-3q7p-736f-x44v](https://github.com/stabgan/openrouter-mcp-multimodal/security/advisories/GHSA-3q7p-736f-x44v), [`SECURITY.md`](./SECURITY.md), and `docs/solutions/security-issues/`. ## Compatibility Works with any MCP client. Protocol: **MCP 2025-06-18**. Node **≥ 22** (Docker image uses Node 24). ## License Apache 2.0 — see [LICENSE](./LICENSE). ## Contributing Issues and PRs welcome. For large changes, open an issue first. Before submitting: run **`npm run ci`**. Use [Conventional Commits](https://www.conventionalcommits.org/) (`fix:`, `feat:`, etc.) so [Release Please](docs/RELEASING.md) can cut the next release. See **[`docs/RELEASING.md`](docs/RELEASING.md)** if you need to ship a version.