OpenRouter MCP Multimodal Server
<p align="center">
<img src="assets/logo.png" alt="OpenRouter MCP Multimodal — MCP server for chat, vision, audio, and video AI tools" width="128" height="128" />
</p>
<h1 align="center">OpenRouter MCP Multimodal</h1>
<p align="center">
<strong>The MCP server for multimodal AI agents.</strong><br/>
One install · 21 tools · 300+ OpenRouter models · text, vision, audio & video — analysis and generation.
</p>
<p align="center">
<a href="https://www.npmjs.com/package/@stabgan/openrouter-mcp-multimodal"><img src="https://img.shields.io/npm/v/@stabgan/openrouter-mcp-multimodal.svg?label=npm&color=cb3837&logo=npm" alt="npm version" /></a>
<a href="https://pypi.org/project/mcp-server-openrouter-multimodal/"><img src="https://img.shields.io/pypi/v/mcp-server-openrouter-multimodal.svg?label=pypi&color=3775A9&logo=pypi&logoColor=white" alt="PyPI version" /></a>
<a href="https://github.com/stabgan/openrouter-mcp-multimodal/releases"><img src="https://img.shields.io/github/v/release/stabgan/openrouter-mcp-multimodal?label=release&color=6366f1" alt="GitHub release" /></a>
<a href="https://hub.docker.com/r/stabgan/openrouter-mcp-multimodal"><img src="https://img.shields.io/docker/v/stabgan/openrouter-mcp-multimodal/latest?label=docker&color=2496ed&logo=docker&logoColor=white" alt="Docker version" /></a>
<a href="https://github.com/stabgan/openrouter-mcp-multimodal/actions/workflows/ci.yml"><img src="https://github.com/stabgan/openrouter-mcp-multimodal/actions/workflows/ci.yml/badge.svg" alt="CI status" /></a>
<a href="https://www.apache.org/licenses/LICENSE-2.0"><img src="https://img.shields.io/badge/License-Apache_2.0-blue.svg" alt="Apache 2.0 license" /></a>
<a href="https://nodejs.org"><img src="https://img.shields.io/badge/node-%E2%89%A522-43853d?logo=node.js&logoColor=white" alt="Node.js 22+" /></a>
</p>
<p align="center">
<a href="https://www.npmjs.com/package/@stabgan/openrouter-mcp-multimodal"><img src="https://img.shields.io/npm/dt/@stabgan/openrouter-mcp-multimodal.svg?label=npm%20downloads&color=cb3837&logo=npm" alt="npm downloads" /></a>
<a href="https://hub.docker.com/r/stabgan/openrouter-mcp-multimodal"><img src="https://img.shields.io/docker/pulls/stabgan/openrouter-mcp-multimodal.svg?label=docker%20pulls&color=2496ed&logo=docker&logoColor=white" alt="Docker pulls" /></a>
<a href="https://registry.modelcontextprotocol.io/servers/io.github.stabgan/openrouter-multimodal"><img src="https://img.shields.io/badge/MCP_Registry-listed-6366f1" alt="MCP Registry" /></a>
<a href="https://smithery.ai/server/@stabgan/openrouter-mcp-multimodal"><img src="https://img.shields.io/badge/Smithery-Install-6366f1" alt="Smithery MCP registry" /></a>
</p>
<p align="center">
<a href="#quick-start">Quick start</a> ·
<a href="#tools">Tools</a> ·
<a href="#examples">Examples</a> ·
<a href="#security">Security</a> ·
<a href="#troubleshooting">Troubleshooting</a> ·
<a href="#development">Development</a> ·
<a href="#releasing">Releasing</a> ·
<a href="#faq">FAQ</a>
</p>
---
## 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, reasoning effort control, structured output (JSON Schema), async jobs, multi-model fusion, subagent delegation, response healing |
| **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 (Deepgram, ElevenLabs, OpenAI; free Deepgram default, voices, mp3/pcm), dedicated STT (Whisper, GPT-4o Transcribe, Voxtral, ElevenLabs Scribe) |
| **Video** | `analyze_video`, `generate_video`, `generate_video_from_image`, `get_video_status` | Clip understanding, Veo 3.1 / Seedance 2.5 / Kling v3.0 / Wan 2.7 / Grok Imagine Video generation with progress notifications |
| **Embedding** | `embed_text` | 37+ embedding models (text-embedding-3-small/large, Nomic, Jina, Voyage, etc.) |
| **Decisions** | `decide` | Structured yes/no, choice, or score decisions with calibrated probabilities (System One models) |
| **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-11-25 structured outputs + tool icons, async video progress notifications, and **3929** automated tests (3905 unit/mock + 7 regression + 17 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
<table>
<tr><td><strong>Cursor</strong></td><td><a href="https://cursor.com/en/install-mcp?name=openrouter&config=eyJ0eXBlIjoic3RkaW8iLCJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIkBzdGFiZ2FuL29wZW5yb3V0ZXItbWNwLW11bHRpbW9kYWwiXSwiZW52Ijp7Ik9QRU5ST1VURVJfQVBJX0tFWSI6InNrLW9yLXYxLS4uLiJ9fQ%3D%3D"><img src="https://cursor.com/deeplink/mcp-install-dark.svg" alt="Add OpenRouter MCP to Cursor" /></a></td></tr>
<tr><td><strong>VS Code</strong></td><td><a href="https://insiders.vscode.dev/redirect/mcp/install?name=openrouter&config=%7B%22type%22%3A%22stdio%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40stabgan%2Fopenrouter-mcp-multimodal%22%5D%2C%22env%22%3A%7B%22OPENROUTER_API_KEY%22%3A%22sk-or-v1-...%22%7D%7D"><img src="https://img.shields.io/badge/Add_to-VS_Code-007ACC?style=for-the-badge&logo=visualstudiocode&logoColor=white" alt="Add to VS Code" /></a></td></tr>
<tr><td><strong>Kiro</strong></td><td><a href="https://kiro.dev/launch/mcp/add?name=openrouter&config=%7B%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40stabgan%2Fopenrouter-mcp-multimodal%22%5D%2C%22env%22%3A%7B%22OPENROUTER_API_KEY%22%3A%22sk-or-v1-...%22%7D%2C%22disabled%22%3Afalse%2C%22autoApprove%22%3A%5B%5D%7D"><img src="https://img.shields.io/badge/Add_to-Kiro-232F3E?style=for-the-badge&logo=amazonaws&logoColor=white" alt="Add to Kiro" /></a></td></tr>
<tr><td><strong>Claude Desktop / Windsurf / Cline</strong></td><td><a href="#manual-config">Manual JSON config</a> (pick any method below)</td></tr>
<tr><td><strong>Smithery</strong></td><td><a href="https://smithery.ai/server/@stabgan/openrouter-mcp-multimodal"><code>npx -y @smithery/cli install @stabgan/openrouter-mcp-multimodal --client claude</code></a></td></tr>
<tr><td><strong>MCP Registry</strong></td><td><a href="https://registry.modelcontextprotocol.io/servers/io.github.stabgan/openrouter-multimodal">Official registry page</a> — npm + OCI packages</td></tr>
</table>
Paste your `OPENROUTER_API_KEY` when prompted — deeplinks use placeholders so secrets never appear in URLs.
### Manual config
<details open>
<summary><strong>npx (recommended)</strong></summary>
```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.1.0"]`
</details>
<details>
<summary><strong>uvx / pipx (Python launcher)</strong></summary>
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.1.0 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.1.0` pins the underlying npm package.
</details>
<details>
<summary><strong>npm global</strong></summary>
```bash
npm install -g @stabgan/openrouter-mcp-multimodal
```
```json
{
"mcpServers": {
"openrouter": {
"command": "openrouter-multimodal",
"env": { "OPENROUTER_API_KEY": "sk-or-v1-..." }
}
}
}
```
</details>
<details>
<summary><strong>node (local clone)</strong></summary>
```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-..." }
}
}
}
```
</details>
<details>
<summary><strong>Docker</strong></summary>
```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).
</details>
<details>
<summary><strong>GHCR (GitHub Container Registry)</strong></summary>
```bash
docker run --rm -i -e OPENROUTER_API_KEY=sk-or-v1-... \
ghcr.io/stabgan/openrouter-mcp-multimodal:5.1.0
```
```json
{
"mcpServers": {
"openrouter": {
"command": "docker",
"args": [
"run",
"--rm",
"-i",
"-e",
"OPENROUTER_API_KEY=sk-or-v1-...",
"ghcr.io/stabgan/openrouter-mcp-multimodal:5.1.0"
]
}
}
}
```
</details>
<details>
<summary><strong>Smithery</strong></summary>
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)
</details>
<details>
<summary><strong>MCP Registry</strong></summary>
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.
</details>
<details>
<summary><strong>Claude Code CLI</strong></summary>
```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.
</details>
<details>
<summary><strong>MCP Inspector</strong></summary>
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
```
</details>
<details>
<summary><strong>Windows npx</strong></summary>
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.
</details>
## Why this server?
| Capability | This server | Typical MCP LLM servers |
| :----------------------------------- | :---------: | :---------------------: |
| Text chat (300+ models) | ✅ | ✅ |
| Image analysis + generation | ✅ | partial |
| Audio analysis + TTS + STT | ✅ | ❌ |
| Video analysis + generation | ✅ | ❌ |
| Vector embeddings (37+ models) | ✅ | ❌ |
| Structured decisions (System One) | ✅ | ❌ |
| Model search / validate / rerank | ✅ | ❌ |
| Path sandbox + SSRF protection | ✅ | rare |
| MCP 2025 structured outputs | ✅ | rare |
| Async video + progress notifications | ✅ | ❌ |
## Tools
21 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, structured output |
| `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`) — Deepgram, ElevenLabs, OpenAI; voices, speed, mp3/pcm |
| `speech_to_text` | Dedicated STT (`/api/v1/audio/transcriptions`) — Whisper, GPT-4o Transcribe, Voxtral, ElevenLabs Scribe |
| `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 |
| `embed_text` | Vector embeddings (37+ models: text-embedding-3-small/large, Nomic, etc.) |
| `decide` | Structured decisions with calibrated probabilities (System One models) |
| `health_check` | API key + reachability probe |
Every error carries `_meta.code` from a closed taxonomy (13 codes). Switch on these for programmatic handling:
| `_meta.code` | Meaning | Recovery |
| :-------------------- | :-------------------------------------------------- | :---------------------------------------------------------------- |
| `INVALID_INPUT` | Bad or missing arguments | Check tool description for correct schema |
| `INVALID_CREDENTIALS` | API key missing, invalid, or revoked | Regenerate at [openrouter.ai/keys](https://openrouter.ai/keys) |
| `UNSAFE_PATH` | Local path escaped the input/output sandbox | Move files under `OPENROUTER_INPUT_DIR` / `OPENROUTER_OUTPUT_DIR` |
| `UPSTREAM_HTTP` | Non-auth HTTP error from OpenRouter (4xx/5xx) | Check `_meta.details.status`; retry if transient |
| `UPSTREAM_TIMEOUT` | Upstream did not respond in time | Retry, or use `start_chat_completion` for slow models |
| `UPSTREAM_REFUSED` | Content policy, rate limit, or insufficient credits | Check `_meta.retry_after_seconds`; review prompt or add credits |
| `UNSUPPORTED_FORMAT` | File is not a recognized media format | Verify file type matches tool expectations |
| `RESOURCE_TOO_LARGE` | File exceeds fetch/inline size cap | Trim file or use `save_path` to skip inline |
| `ZDR_INCOMPATIBLE` | Model/provider requires zero data retention | Use a ZDR-compatible provider or different model |
| `MODEL_NOT_FOUND` | Model slug not in OpenRouter catalog | Run `validate_model` or `search_models` first |
| `JOB_FAILED` | Async job (video/chat) failed on provider side | Check `_meta.details`; retry or change model |
| `JOB_STILL_RUNNING` | Async job not yet complete (not an error) | Call `get_video_status` / `get_chat_completion_status` to poll |
| `INTERNAL` | Unexpected server-side error | Report if reproducible |
Errors include `_meta.suggestions` with agent-oriented next steps when available.
### 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." }]
}
}
```
### Chat with structured output (JSON Schema)
```json
{
"tool": "chat_completion",
"arguments": {
"model": "openai/gpt-4o",
"messages": [{ "role": "user", "content": "Extract the name and age from: 'Alice is 30 years old.'" }],
"response_format": {
"type": "json_schema",
"json_schema": {
"name": "person",
"schema": {
"type": "object",
"properties": {
"name": { "type": "string" },
"age": { "type": "integer" }
},
"required": ["name", "age"]
}
}
}
}
}
```
> For simpler JSON without a schema, use `"response_format": { "type": "json_object" }`. Add `"response_healing": true` to auto-fix malformed JSON.
### 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).
### Embed text (vector embeddings)
```json
{
"tool": "embed_text",
"arguments": {
"texts": ["What is MCP?", "Model Context Protocol enables tool use for LLMs."],
"model": "openai/text-embedding-3-small",
"dimensions": 1024
}
}
```
Returns embedding arrays for RAG, semantic search, or clustering. 37+ models supported — pass a single string or an array of up to 2048 texts.
### Structured decision (System One)
```json
{
"tool": "decide",
"arguments": {
"questions": {
"is_bug": {
"type": "noul",
"instructions": "Is this a bug report or a feature request?",
"criteria": {
"true": "Describes broken or incorrect behavior",
"false": "Requests new functionality"
}
}
},
"state": {
"ticket": "The login button does not respond when clicked on Safari."
}
}
}
```
Returns calibrated probabilities instead of generated text. Three question types: `noul` (yes/no), `choice` (pick one category), `score` (ordinal rubric). Models: `typesafe/jev-1.13` (default), `upstage/solar-decide`, `microsoft/decision-1`.
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`
- **Prototype-pollution defense** — all API output boundaries (model records, embeddings, decisions, rerank results, log context) recursively strip `__proto__`, `constructor`, and `prototype` keys before forwarding to MCP clients
- **Secret redaction** — the stderr JSON logger redacts API keys and sensitive values; stdout is reserved for MCP protocol traffic
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
<details>
<summary><strong>Environment variables</strong></summary>
| 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).
</details>
## 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` | **3905** unit + mock tests (no API key, <20s) |
| `npm run test:regression` | **7** security + schema regression guards |
| `npm run test:integration` | **17** live OpenRouter scenarios (**requires** `.env` key) |
| `npm run test:e2e` | Full MCP stdio smoke (`scripts/live-e2e.mjs`) |
| `npm run ci` | lint + format + version sync + build + **all** tests |
**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 — **396** 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-11-25** (backwards-compatible with 2025-06-18 and earlier). 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.
TDQS
Scored across 11 tools
Each tool targets a distinct function: analyzing audio/image/video, generating content, chat completions, and model queries. No two tools have overlapping purposes, and even the video generation status tool is clearly a helper for async workflows.
Most tools follow a verb_noun pattern (analyze_, generate_, get_, search_, validate_). The exception is 'chat_completion', which combines two nouns rather than a verb_noun, causing a minor inconsistency in the naming style.
With 11 tools, the set covers all major modalities (audio, image, video, text) and supporting functions (model info, search, validation). The count is well-balanced—not excessive or too sparse for the server's multimodal purpose.
Core analysis, generation, and query tools are present. Some minor gaps exist (e.g., no explicit tool for listing all models, though search_models and get_model_info cover it). Overall, the surface is comprehensive for typical multimodal workflows.