@stabgan/openrouter-mcp-multimodal 2.0.0 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +198 -145
- package/dist/errors.d.ts +42 -0
- package/dist/errors.js +46 -0
- package/dist/index.js +1 -1
- package/dist/logger.d.ts +22 -0
- package/dist/logger.js +47 -0
- package/dist/model-cache.d.ts +10 -0
- package/dist/model-cache.js +31 -1
- package/dist/openrouter-api.d.ts +54 -0
- package/dist/openrouter-api.js +136 -12
- package/dist/tool-handlers/analyze-audio.d.ts +5 -9
- package/dist/tool-handlers/analyze-audio.js +41 -8
- package/dist/tool-handlers/analyze-image.d.ts +5 -9
- package/dist/tool-handlers/analyze-image.js +38 -8
- package/dist/tool-handlers/analyze-video.d.ts +19 -0
- package/dist/tool-handlers/analyze-video.js +93 -0
- package/dist/tool-handlers/audio-utils.js +7 -9
- package/dist/tool-handlers/chat-completion.d.ts +6 -10
- package/dist/tool-handlers/chat-completion.js +27 -7
- package/dist/tool-handlers/completion-utils.d.ts +27 -0
- package/dist/tool-handlers/completion-utils.js +69 -0
- package/dist/tool-handlers/fetch-utils.d.ts +32 -0
- package/dist/tool-handlers/fetch-utils.js +216 -12
- package/dist/tool-handlers/generate-audio.d.ts +32 -12
- package/dist/tool-handlers/generate-audio.js +77 -46
- package/dist/tool-handlers/generate-image.d.ts +48 -10
- package/dist/tool-handlers/generate-image.js +148 -33
- package/dist/tool-handlers/generate-video.d.ts +78 -0
- package/dist/tool-handlers/generate-video.js +353 -0
- package/dist/tool-handlers/get-model-info.d.ts +2 -9
- package/dist/tool-handlers/get-model-info.js +15 -5
- package/dist/tool-handlers/image-utils.d.ts +17 -1
- package/dist/tool-handlers/image-utils.js +66 -13
- package/dist/tool-handlers/openrouter-errors.d.ts +18 -0
- package/dist/tool-handlers/openrouter-errors.js +99 -0
- package/dist/tool-handlers/path-safety.d.ts +11 -0
- package/dist/tool-handlers/path-safety.js +88 -0
- package/dist/tool-handlers/search-models.d.ts +2 -9
- package/dist/tool-handlers/search-models.js +10 -6
- package/dist/tool-handlers/validate-model.d.ts +2 -9
- package/dist/tool-handlers/validate-model.js +15 -4
- package/dist/tool-handlers/video-utils.d.ts +29 -0
- package/dist/tool-handlers/video-utils.js +174 -0
- package/dist/tool-handlers.js +229 -21
- package/package.json +3 -3
- package/dist/__tests__/audio-utils.test.d.ts +0 -1
- package/dist/__tests__/audio-utils.test.js +0 -120
- package/dist/__tests__/fetch-utils.test.d.ts +0 -1
- package/dist/__tests__/fetch-utils.test.js +0 -76
- package/dist/__tests__/generate-audio.test.d.ts +0 -1
- package/dist/__tests__/generate-audio.test.js +0 -90
- package/dist/__tests__/image-utils.test.d.ts +0 -1
- package/dist/__tests__/image-utils.test.js +0 -75
- package/dist/__tests__/integration.test.d.ts +0 -1
- package/dist/__tests__/integration.test.js +0 -219
- package/dist/__tests__/model-cache.test.d.ts +0 -1
- package/dist/__tests__/model-cache.test.js +0 -96
package/README.md
CHANGED
|
@@ -1,56 +1,123 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="assets/logo.png" alt="OpenRouter MCP Multimodal" width="200" height="200" />
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<h1 align="center">OpenRouter MCP Multimodal Server</h1>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<strong>The all-in-one MCP server for 300+ LLMs — text, vision, audio, and video in a single package.</strong>
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
<p align="center">
|
|
12
|
+
<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>
|
|
13
|
+
<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>
|
|
14
|
+
<a href="https://github.com/stabgan/openrouter-mcp-multimodal/actions/workflows/publish.yml"><img src="https://github.com/stabgan/openrouter-mcp-multimodal/actions/workflows/publish.yml/badge.svg" alt="CI" /></a>
|
|
15
|
+
<a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="MIT" /></a>
|
|
16
|
+
<a href="https://nodejs.org"><img src="https://img.shields.io/badge/node-%E2%89%A518-43853d?logo=node.js&logoColor=white" alt="Node.js" /></a>
|
|
17
|
+
</p>
|
|
18
|
+
|
|
19
|
+
<p align="center">
|
|
20
|
+
<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>
|
|
21
|
+
<a href="https://www.npmjs.com/package/@stabgan/openrouter-mcp-multimodal"><img src="https://img.shields.io/npm/dm/@stabgan/openrouter-mcp-multimodal.svg?label=monthly&color=cb3837&logo=npm" alt="npm monthly" /></a>
|
|
22
|
+
<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>
|
|
23
|
+
<a href="https://github.com/stabgan/openrouter-mcp-multimodal/stargazers"><img src="https://img.shields.io/github/stars/stabgan/openrouter-mcp-multimodal.svg?style=social" alt="GitHub stars" /></a>
|
|
24
|
+
<a href="https://github.com/stabgan/openrouter-mcp-multimodal/network/members"><img src="https://img.shields.io/github/forks/stabgan/openrouter-mcp-multimodal.svg?style=social" alt="GitHub forks" /></a>
|
|
25
|
+
</p>
|
|
26
|
+
<p align="center">
|
|
27
|
+
<sub>3,800+ installs across npm + Docker Hub · ~950 npm installs/month and accelerating</sub>
|
|
28
|
+
</p>
|
|
29
|
+
|
|
30
|
+
<p align="center">
|
|
31
|
+
<a href="#one-click-install">Install</a> ·
|
|
32
|
+
<a href="#tools">Tools</a> ·
|
|
33
|
+
<a href="#quick-start">Quick Start</a> ·
|
|
34
|
+
<a href="#configuration">Config</a> ·
|
|
35
|
+
<a href="#usage-examples">Examples</a> ·
|
|
36
|
+
<a href="#architecture">Architecture</a> ·
|
|
37
|
+
<a href="./CHANGELOG.md">Changelog</a>
|
|
38
|
+
</p>
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
[](https://mseep.ai/app/8f27d6d4-0877-4b86-b377-8a33f451e755)
|
|
42
|
+
|
|
43
|
+
Access 300+ LLMs through [OpenRouter](https://openrouter.ai) via the [Model Context Protocol](https://modelcontextprotocol.io). Analyze images, audio, and video. Generate images, audio, and video. Chat with any model. Every tool returns structured `_meta.code` errors so MCP clients can switch on failure modes without parsing strings.
|
|
2
44
|
|
|
3
|
-
# OpenRouter MCP Multimodal Server
|
|
4
45
|
|
|
5
|
-
[](https://www.npmjs.com/package/@stabgan/openrouter-mcp-multimodal)
|
|
6
|
-
[](https://hub.docker.com/r/stabgan/openrouter-mcp-multimodal)
|
|
7
|
-
[](https://github.com/stabgan/openrouter-mcp-multimodal/actions/workflows/publish.yml)
|
|
8
|
-
[](https://opensource.org/licenses/MIT)
|
|
9
|
-
|
|
10
|
-
An OpenRouter MCP server with native vision, image generation, audio analysis, audio generation, and smart image optimization in one package.
|
|
11
|
-
|
|
12
|
-
Access 300+ LLMs through [OpenRouter](https://openrouter.ai) via the [Model Context Protocol](https://modelcontextprotocol.io), with first-class support for multimodal workflows: analyze images, analyze audio, generate images, generate audio (conversational, speech, or music), and chat — using free or paid models.
|
|
13
46
|
|
|
14
47
|
## One-Click Install
|
|
15
48
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
49
|
+
<table>
|
|
50
|
+
<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>
|
|
51
|
+
<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 to Cursor" /></a></td></tr>
|
|
52
|
+
<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>
|
|
53
|
+
<tr><td><strong>VS Code Insiders</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&quality=insiders"><img src="https://img.shields.io/badge/Add_to-VS_Code_Insiders-24bfa5?style=for-the-badge&logo=visualstudiocode&logoColor=white" alt="Add to VS Code Insiders" /></a></td></tr>
|
|
54
|
+
<tr><td><strong>Claude Desktop</strong></td><td><a href="#option-1-npx-no-install">Install Guide</a> — Add to <code>claude_desktop_config.json</code></td></tr>
|
|
55
|
+
<tr><td><strong>Windsurf</strong></td><td><a href="#option-1-npx-no-install">Install Guide</a> — Add to <code>~/.codeium/windsurf/mcp_config.json</code></td></tr>
|
|
56
|
+
<tr><td><strong>Cline</strong></td><td><a href="#option-1-npx-no-install">Install Guide</a> — Add via Cline MCP settings</td></tr>
|
|
57
|
+
<tr><td><strong>Smithery</strong></td><td><code>npx -y @smithery/cli install @stabgan/openrouter-mcp-multimodal --client claude</code></td></tr>
|
|
58
|
+
</table>
|
|
59
|
+
|
|
60
|
+
> After clicking, the target client opens a confirmation prompt. You'll need to paste your `OPENROUTER_API_KEY` — the deeplink ships a placeholder so no secrets end up in shared links.
|
|
61
|
+
|
|
62
|
+
<!--
|
|
63
|
+
Install-link audit (2026-04-20, round 4 — HTTPS redirectors only):
|
|
64
|
+
GitHub Markdown strips non-HTTPS schemes (cursor://, vscode:) from href attributes
|
|
65
|
+
and rewrites them to cursor.sh / nothing. The only deeplinks that survive GitHub's
|
|
66
|
+
sanitizer are HTTPS URLs. Fortunately both Cursor and VS Code publish official
|
|
67
|
+
HTTPS redirector endpoints that hand off to the IDE's native protocol handler:
|
|
68
|
+
|
|
69
|
+
- Kiro: https://kiro.dev/launch/mcp/add?name=<name>&config=<url-encoded JSON>
|
|
70
|
+
- Cursor: https://cursor.com/en/install-mcp?name=<name>&config=<base64>
|
|
71
|
+
(config excludes `name`; badge is cursor.com/deeplink/mcp-install-dark.svg)
|
|
72
|
+
- VS Code: https://insiders.vscode.dev/redirect/mcp/install?name=<name>&config=<urlenc JSON>
|
|
73
|
+
(config excludes `name`; append &quality=insiders for Insiders)
|
|
74
|
+
|
|
75
|
+
Earlier rounds used cursor://anysphere.cursor-deeplink/... and vscode:mcp/install?...
|
|
76
|
+
— both got stripped by GitHub and rendered as broken redirects to cursor.sh or a
|
|
77
|
+
camo.githubusercontent.com image-proxy URL. Pattern confirmed against
|
|
78
|
+
github/github-mcp-server and modelcontextprotocol/servers READMEs.
|
|
79
|
+
|
|
80
|
+
Regenerate with: node scripts/make-install-links.mjs
|
|
81
|
+
-->
|
|
26
82
|
|
|
27
83
|
## Why This One?
|
|
28
84
|
|
|
29
|
-
| Feature
|
|
30
|
-
|
|
|
31
|
-
| Text chat with 300+ models
|
|
32
|
-
| Image analysis (vision)
|
|
33
|
-
| Audio analysis
|
|
34
|
-
| Audio generation
|
|
35
|
-
| Image generation
|
|
36
|
-
|
|
|
37
|
-
|
|
|
38
|
-
|
|
|
39
|
-
|
|
|
40
|
-
|
|
|
85
|
+
| Feature | Status |
|
|
86
|
+
| :--- | :--- |
|
|
87
|
+
| Text chat with 300+ models | ✅ |
|
|
88
|
+
| Image analysis (vision) | ✅ Native with sharp optimization |
|
|
89
|
+
| Audio analysis | ✅ Transcription + analysis, base64 auto-encoded |
|
|
90
|
+
| Audio generation | ✅ Conversational, speech, and music with format auto-detection |
|
|
91
|
+
| Image generation | ✅ Path-sandboxed disk output |
|
|
92
|
+
| **Video understanding** | ✅ **v3** — mp4, mpeg, mov, webm from files, URLs, or data URLs |
|
|
93
|
+
| **Video generation** | ✅ **v3** — Veo 3.1 / Sora 2 Pro / Seedance / Wan via async API with progress notifications |
|
|
94
|
+
| Auto image resize + compress | ✅ Configurable (defaults 800px max, JPEG 80%) |
|
|
95
|
+
| Model search + validation | ✅ Filter by vision / audio / video modality |
|
|
96
|
+
| Free model support | ✅ Default: free Nemotron VL |
|
|
97
|
+
| Docker support | ✅ Multi-arch (amd64 + arm64), ~345 MB Alpine |
|
|
98
|
+
| Retry-After + jitter | ✅ Honors `Retry-After` header, avoids thundering herd |
|
|
99
|
+
| IPv4 + IPv6 SSRF blocklist | ✅ Covers mapped, compat, multicast, 6to4, Teredo, ORCHID |
|
|
100
|
+
| Structured error taxonomy | ✅ Closed `_meta.code` so clients can switch on failure modes |
|
|
101
|
+
| Reasoning-model awareness | ✅ Detects `max_tokens` cutoff during CoT, guides the caller |
|
|
102
|
+
| MCP 2025 tool annotations | ✅ `readOnlyHint` / `destructiveHint` / `idempotentHint` on every tool |
|
|
41
103
|
|
|
42
104
|
## Tools
|
|
43
105
|
|
|
44
|
-
| Tool
|
|
45
|
-
|
|
|
46
|
-
| `chat_completion` | Send messages to any OpenRouter model.
|
|
47
|
-
| `analyze_image`
|
|
48
|
-
| `analyze_audio`
|
|
49
|
-
| `
|
|
50
|
-
| `generate_image`
|
|
51
|
-
| `
|
|
52
|
-
| `
|
|
53
|
-
| `
|
|
106
|
+
| Tool | Description |
|
|
107
|
+
| :--- | :--- |
|
|
108
|
+
| `chat_completion` | Send messages to any OpenRouter model. Detects reasoning-model cutoffs. |
|
|
109
|
+
| `analyze_image` | Analyze images from local files, URLs, or data URIs. Auto-optimized with sharp. |
|
|
110
|
+
| `analyze_audio` | Analyze/transcribe audio (WAV, MP3, FLAC, OGG, etc.) from files, URLs, or data URIs. |
|
|
111
|
+
| `analyze_video` | Analyze/transcribe video (mp4, mpeg, mov, webm) from files, URLs, or data URIs. |
|
|
112
|
+
| `generate_image` | Generate images from text prompts. Optional path-sandboxed disk save. |
|
|
113
|
+
| `generate_audio` | Generate audio from text. Auto-detects format, wraps raw PCM in WAV. |
|
|
114
|
+
| `generate_video` | Generate video via OpenRouter's async API (Veo 3.1 / Sora 2 Pro / Seedance / Wan). Submits, polls, downloads, saves. |
|
|
115
|
+
| `get_video_status` | Resume polling a `generate_video` job by id. Download + save when complete. |
|
|
116
|
+
| `search_models` | Search/filter models by name, provider, or capabilities (vision / audio / video). |
|
|
117
|
+
| `get_model_info` | Get pricing, context length, and capabilities for any model. |
|
|
118
|
+
| `validate_model` | Check if a model ID exists on OpenRouter. |
|
|
119
|
+
|
|
120
|
+
> All error responses carry `_meta.code` from a closed taxonomy: `INVALID_INPUT` · `UNSAFE_PATH` · `UPSTREAM_HTTP` · `UPSTREAM_TIMEOUT` · `UPSTREAM_REFUSED` · `UNSUPPORTED_FORMAT` · `RESOURCE_TOO_LARGE` · `ZDR_INCOMPATIBLE` · `MODEL_NOT_FOUND` · `JOB_FAILED` · `JOB_STILL_RUNNING` · `INTERNAL`
|
|
54
121
|
|
|
55
122
|
## Quick Start
|
|
56
123
|
|
|
@@ -97,16 +164,12 @@ Get a free API key from [openrouter.ai/keys](https://openrouter.ai/keys).
|
|
|
97
164
|
npm install -g @stabgan/openrouter-mcp-multimodal
|
|
98
165
|
```
|
|
99
166
|
|
|
100
|
-
Then add to your MCP config:
|
|
101
|
-
|
|
102
167
|
```json
|
|
103
168
|
{
|
|
104
169
|
"mcpServers": {
|
|
105
170
|
"openrouter": {
|
|
106
171
|
"command": "openrouter-multimodal",
|
|
107
|
-
"env": {
|
|
108
|
-
"OPENROUTER_API_KEY": "sk-or-v1-..."
|
|
109
|
-
}
|
|
172
|
+
"env": { "OPENROUTER_API_KEY": "sk-or-v1-..." }
|
|
110
173
|
}
|
|
111
174
|
}
|
|
112
175
|
}
|
|
@@ -120,111 +183,109 @@ npx -y @smithery/cli install @stabgan/openrouter-mcp-multimodal --client claude
|
|
|
120
183
|
|
|
121
184
|
## Configuration
|
|
122
185
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
|
127
|
-
|
|
|
128
|
-
| `
|
|
129
|
-
| `
|
|
130
|
-
| `
|
|
131
|
-
| `
|
|
132
|
-
| `
|
|
133
|
-
| `
|
|
134
|
-
| `
|
|
135
|
-
| `
|
|
136
|
-
| `
|
|
137
|
-
| `
|
|
138
|
-
| `
|
|
186
|
+
<details>
|
|
187
|
+
<summary><strong>Environment variables</strong> (click to expand)</summary>
|
|
188
|
+
|
|
189
|
+
| Variable | Required | Default | Description |
|
|
190
|
+
| :--- | :---: | :--- | :--- |
|
|
191
|
+
| `OPENROUTER_API_KEY` | Yes | — | Your OpenRouter API key |
|
|
192
|
+
| `OPENROUTER_DEFAULT_MODEL` | No | `nvidia/nemotron-nano-12b-v2-vl:free` | Default model for chat + analyze tools |
|
|
193
|
+
| `DEFAULT_MODEL` | No | — | Alias for above |
|
|
194
|
+
| `OPENROUTER_MODEL_CACHE_TTL_MS` | No | `3600000` | Model cache TTL (ms) |
|
|
195
|
+
| `OPENROUTER_IMAGE_MAX_DIMENSION` | No | `800` | Longest edge for resize (px) |
|
|
196
|
+
| `OPENROUTER_IMAGE_JPEG_QUALITY` | No | `80` | JPEG quality (1–100) |
|
|
197
|
+
| `OPENROUTER_IMAGE_FETCH_TIMEOUT_MS` | No | `30000` | Image URL timeout |
|
|
198
|
+
| `OPENROUTER_IMAGE_MAX_DOWNLOAD_BYTES` | No | `26214400` | Image URL size cap (~25 MB) |
|
|
199
|
+
| `OPENROUTER_IMAGE_MAX_REDIRECTS` | No | `8` | Image URL redirect cap |
|
|
200
|
+
| `OPENROUTER_IMAGE_MAX_DATA_URL_BYTES` | No | `20971520` | Image data URL size cap (~20 MB) |
|
|
201
|
+
| `OPENROUTER_AUDIO_FETCH_TIMEOUT_MS` | No | `30000` | Audio URL timeout |
|
|
202
|
+
| `OPENROUTER_AUDIO_MAX_DOWNLOAD_BYTES` | No | `26214400` | Audio URL size cap (~25 MB) |
|
|
203
|
+
| `OPENROUTER_AUDIO_MAX_REDIRECTS` | No | `8` | Audio URL redirect cap |
|
|
204
|
+
| `OPENROUTER_AUDIO_MAX_DATA_URL_BYTES` | No | `20971520` | Audio data URL size cap |
|
|
205
|
+
| `OPENROUTER_DEFAULT_VIDEO_MODEL` | No | `google/gemini-2.5-flash` | Default for `analyze_video` |
|
|
206
|
+
| `OPENROUTER_DEFAULT_VIDEO_GEN_MODEL` | No | `google/veo-3.1` | Default for `generate_video` |
|
|
207
|
+
| `OPENROUTER_VIDEO_FETCH_TIMEOUT_MS` | No | `60000` | Video URL timeout |
|
|
208
|
+
| `OPENROUTER_VIDEO_MAX_DOWNLOAD_BYTES` | No | `104857600` | Video URL size cap (~100 MB) |
|
|
209
|
+
| `OPENROUTER_VIDEO_MAX_REDIRECTS` | No | `8` | Video URL redirect cap |
|
|
210
|
+
| `OPENROUTER_VIDEO_MAX_DATA_URL_BYTES` | No | `104857600` | Video data URL size cap |
|
|
211
|
+
| `OPENROUTER_VIDEO_POLL_INTERVAL_MS` | No | `15000` | Async video poll cadence |
|
|
212
|
+
| `OPENROUTER_VIDEO_MAX_WAIT_MS` | No | `600000` | Max wait before returning a resumable handle |
|
|
213
|
+
| `OPENROUTER_VIDEO_GEN_MAX_BYTES` | No | `268435456` | Generated video download cap (~256 MB) |
|
|
214
|
+
| `OPENROUTER_VIDEO_INLINE_MAX_BYTES` | No | `10485760` | Inline video ceiling (~10 MB) |
|
|
215
|
+
| `OPENROUTER_OUTPUT_DIR` | No | `process.cwd()` | Sandbox root for `save_path` |
|
|
216
|
+
| `OPENROUTER_ALLOW_UNSAFE_PATHS` | No | — | `1` disables the sandbox |
|
|
217
|
+
| `OPENROUTER_LOG_LEVEL` | No | `info` | `error` / `warn` / `info` / `debug` |
|
|
218
|
+
|
|
219
|
+
</details>
|
|
139
220
|
|
|
140
221
|
### Security notes
|
|
141
222
|
|
|
142
|
-
-
|
|
143
|
-
-
|
|
144
|
-
-
|
|
145
|
-
- **`generate_image`** `save_path` writes to disk wherever the process has permission; treat prompts and paths like shell input from the MCP client user.
|
|
223
|
+
- **Analyze tools** can read local files and fetch HTTP(S) URLs. URL fetches block private/link-local/reserved IPv4 and IPv6 targets (SSRF mitigation) and cap response size.
|
|
224
|
+
- **Generate tools** write to disk through a path sandbox: `save_path` is resolved against `OPENROUTER_OUTPUT_DIR` and any traversal attempt is rejected. Override with `OPENROUTER_ALLOW_UNSAFE_PATHS=1`.
|
|
225
|
+
- **IPv6 SSRF blocklist** covers loopback, unspecified, IPv4-mapped, IPv4-compatible, link-local, site-local, ULA, multicast, documentation, Teredo, ORCHID, and 6to4 of private IPv4.
|
|
146
226
|
|
|
147
227
|
## Usage Examples
|
|
148
228
|
|
|
149
|
-
### Chat
|
|
150
|
-
|
|
151
229
|
```
|
|
230
|
+
# Chat
|
|
152
231
|
Use chat_completion to explain quantum computing in simple terms.
|
|
153
|
-
```
|
|
154
|
-
|
|
155
|
-
### Analyze an Image
|
|
156
232
|
|
|
157
|
-
|
|
233
|
+
# Vision
|
|
158
234
|
Use analyze_image on /path/to/photo.jpg and tell me what you see.
|
|
159
|
-
```
|
|
160
235
|
|
|
161
|
-
|
|
236
|
+
# Audio transcription
|
|
237
|
+
Use analyze_audio on /path/to/recording.mp3 to transcribe it.
|
|
162
238
|
|
|
163
|
-
|
|
164
|
-
Use
|
|
165
|
-
```
|
|
239
|
+
# Video understanding
|
|
240
|
+
Use analyze_video on /path/to/clip.mp4 — what happens at 00:15?
|
|
166
241
|
|
|
167
|
-
|
|
242
|
+
# Generate audio
|
|
243
|
+
Use generate_audio with prompt "Explain neural networks" and voice "alloy", save to ./response.wav
|
|
168
244
|
|
|
169
|
-
|
|
170
|
-
Use generate_audio with
|
|
171
|
-
```
|
|
245
|
+
# Generate music
|
|
246
|
+
Use generate_audio with model "google/lyria-3-clip-preview" and prompt "upbeat jazz piano trio"
|
|
172
247
|
|
|
173
|
-
|
|
248
|
+
# Generate image
|
|
249
|
+
Use generate_image with prompt "a cat astronaut on mars" and save to ./cat.png
|
|
174
250
|
|
|
175
|
-
|
|
176
|
-
Use
|
|
177
|
-
|
|
178
|
-
Save to ./jazz.wav
|
|
179
|
-
```
|
|
180
|
-
|
|
181
|
-
The file will be auto-saved as `jazz.mp3` since Lyria returns MP3.
|
|
182
|
-
|
|
183
|
-
### Find Vision Models
|
|
251
|
+
# Generate video
|
|
252
|
+
Use generate_video with model "google/veo-3.1", prompt "a calm river at sunrise",
|
|
253
|
+
resolution 720p, duration 4, save to ./river.mp4
|
|
184
254
|
|
|
185
|
-
|
|
186
|
-
Use
|
|
187
|
-
```
|
|
188
|
-
|
|
189
|
-
### Generate an Image
|
|
190
|
-
|
|
191
|
-
```
|
|
192
|
-
Use generate_image with prompt "a cat astronaut on mars, digital art" and save to ./cat.png
|
|
255
|
+
# Resume a video job
|
|
256
|
+
Use get_video_status with video_id "vid_abc123" and save_path "./river.mp4"
|
|
193
257
|
```
|
|
194
258
|
|
|
195
259
|
## Architecture
|
|
196
260
|
|
|
197
261
|
```
|
|
198
262
|
src/
|
|
199
|
-
├── index.ts
|
|
200
|
-
├── tool-handlers.ts
|
|
201
|
-
├── model-cache.ts
|
|
202
|
-
├── openrouter-api.ts
|
|
263
|
+
├── index.ts # Entry, env validation, graceful shutdown
|
|
264
|
+
├── tool-handlers.ts # 11 tools (annotated) + dispatch
|
|
265
|
+
├── model-cache.ts # TTL + in-flight coalescing
|
|
266
|
+
├── openrouter-api.ts # REST client (chat + /videos)
|
|
267
|
+
├── errors.ts # Closed ErrorCode enum
|
|
268
|
+
├── logger.ts # JSON-line structured logger
|
|
203
269
|
└── tool-handlers/
|
|
204
|
-
├── fetch-utils.ts
|
|
205
|
-
├──
|
|
206
|
-
├──
|
|
207
|
-
├──
|
|
208
|
-
├──
|
|
209
|
-
├──
|
|
210
|
-
├──
|
|
211
|
-
├──
|
|
212
|
-
├──
|
|
213
|
-
├──
|
|
214
|
-
|
|
270
|
+
├── fetch-utils.ts # SSRF, bounded fetch, data-URL parser
|
|
271
|
+
├── openrouter-errors.ts # SDK/HTTP → ErrorCode classifier
|
|
272
|
+
├── completion-utils.ts # Reasoning-model cutoff detection
|
|
273
|
+
├── path-safety.ts # save_path sandbox
|
|
274
|
+
├── chat-completion.ts # Text + multimodal chat
|
|
275
|
+
├── analyze-image.ts # Vision analysis
|
|
276
|
+
├── analyze-audio.ts # Audio transcription
|
|
277
|
+
├── analyze-video.ts # Video understanding
|
|
278
|
+
├── generate-image.ts # Image generation
|
|
279
|
+
├── generate-audio.ts # Audio generation + streaming
|
|
280
|
+
├── generate-video.ts # Video generation (async)
|
|
281
|
+
├── image-utils.ts # Sharp optimization, MIME sniffing
|
|
282
|
+
├── audio-utils.ts # Audio format detection
|
|
283
|
+
├── video-utils.ts # Video format detection
|
|
284
|
+
├── search-models.ts # Model search
|
|
285
|
+
├── get-model-info.ts # Model detail lookup
|
|
286
|
+
└── validate-model.ts # Model existence check
|
|
215
287
|
```
|
|
216
288
|
|
|
217
|
-
Key design decisions:
|
|
218
|
-
|
|
219
|
-
- **Native `fetch`** for OpenRouter and media URLs (no axios / node-fetch dependency)
|
|
220
|
-
- **Shared security layer** — SSRF protection, IP blocking, bounded fetches in `fetch-utils.ts` (single source of truth for image + audio)
|
|
221
|
-
- **Lazy sharp loading** — `sharp` is loaded on first image operation, not at startup
|
|
222
|
-
- **Singleton model cache** — shared across tool handlers with configurable TTL (default 1 hour)
|
|
223
|
-
- **Bounded URL fetches** — timeouts, size limits, redirect cap, and blocked private networks
|
|
224
|
-
- **Audio format auto-detection** — magic-byte detection for MP3/WAV/FLAC/OGG, raw PCM auto-wrapped in WAV
|
|
225
|
-
- **Graceful error handling** — tools return structured errors instead of crashing the server
|
|
226
|
-
- **Process safety** — uncaught exceptions and unhandled rejections exit the process (no zombie servers)
|
|
227
|
-
|
|
228
289
|
## Development
|
|
229
290
|
|
|
230
291
|
```bash
|
|
@@ -236,34 +297,26 @@ npm run build
|
|
|
236
297
|
npm start
|
|
237
298
|
```
|
|
238
299
|
|
|
239
|
-
### Run Tests
|
|
240
|
-
|
|
241
300
|
```bash
|
|
242
|
-
npm test
|
|
243
|
-
npm run test:integration
|
|
301
|
+
npm test # 163 unit tests, <1s
|
|
302
|
+
npm run test:integration # Live API tests
|
|
244
303
|
npm run lint
|
|
245
|
-
|
|
304
|
+
node scripts/live-e2e.mjs # 16 live E2E scenarios
|
|
246
305
|
```
|
|
247
306
|
|
|
248
|
-
|
|
307
|
+
## Upgrading from v2
|
|
249
308
|
|
|
250
|
-
|
|
251
|
-
docker build -t openrouter-mcp .
|
|
252
|
-
docker run -i -e OPENROUTER_API_KEY=sk-or-v1-... openrouter-mcp
|
|
253
|
-
```
|
|
309
|
+
v3 is **additive** — no tool schemas or env vars were removed.
|
|
254
310
|
|
|
255
|
-
|
|
311
|
+
- Three new tools: `analyze_video`, `generate_video`, `get_video_status`
|
|
312
|
+
- Structured `_meta.code` on every error response (text messages preserved)
|
|
313
|
+
- `save_path` sandboxed by default — set `OPENROUTER_OUTPUT_DIR` or `OPENROUTER_ALLOW_UNSAFE_PATHS=1`
|
|
314
|
+
- Reasoning-model awareness: `content: null` + `finish_reason: length` now returns `INVALID_INPUT` with a preview instead of empty string
|
|
315
|
+
- IPv6 SSRF coverage extended to mapped, compat, multicast, 6to4, Teredo, ORCHID
|
|
256
316
|
|
|
257
317
|
## Compatibility
|
|
258
318
|
|
|
259
|
-
Works with any MCP client:
|
|
260
|
-
|
|
261
|
-
- [Kiro](https://kiro.dev)
|
|
262
|
-
- [Claude Desktop](https://claude.ai/download)
|
|
263
|
-
- [Cursor](https://cursor.sh)
|
|
264
|
-
- [Windsurf](https://codeium.com/windsurf)
|
|
265
|
-
- [Cline](https://github.com/cline/cline)
|
|
266
|
-
- Any MCP-compatible client
|
|
319
|
+
Works with any MCP client: [Kiro](https://kiro.dev) · [Claude Desktop](https://claude.ai/download) · [Cursor](https://cursor.sh) · [Windsurf](https://codeium.com/windsurf) · [Cline](https://github.com/cline/cline) · any MCP-compatible client.
|
|
267
320
|
|
|
268
321
|
## License
|
|
269
322
|
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Closed error-code taxonomy for MCP tool responses. Every handler uses
|
|
3
|
+
* `toolError(code, message, details?)` instead of ad-hoc text so clients
|
|
4
|
+
* can switch on `_meta.code` without regex-parsing free text.
|
|
5
|
+
*
|
|
6
|
+
* Adding a new code requires a design.md note — this set is intentionally
|
|
7
|
+
* small and stable.
|
|
8
|
+
*/
|
|
9
|
+
export declare const ErrorCode: {
|
|
10
|
+
readonly INVALID_INPUT: "INVALID_INPUT";
|
|
11
|
+
readonly UNSAFE_PATH: "UNSAFE_PATH";
|
|
12
|
+
readonly UPSTREAM_HTTP: "UPSTREAM_HTTP";
|
|
13
|
+
readonly UPSTREAM_TIMEOUT: "UPSTREAM_TIMEOUT";
|
|
14
|
+
readonly UPSTREAM_REFUSED: "UPSTREAM_REFUSED";
|
|
15
|
+
readonly UNSUPPORTED_FORMAT: "UNSUPPORTED_FORMAT";
|
|
16
|
+
readonly RESOURCE_TOO_LARGE: "RESOURCE_TOO_LARGE";
|
|
17
|
+
readonly ZDR_INCOMPATIBLE: "ZDR_INCOMPATIBLE";
|
|
18
|
+
readonly MODEL_NOT_FOUND: "MODEL_NOT_FOUND";
|
|
19
|
+
readonly JOB_FAILED: "JOB_FAILED";
|
|
20
|
+
readonly JOB_STILL_RUNNING: "JOB_STILL_RUNNING";
|
|
21
|
+
readonly INTERNAL: "INTERNAL";
|
|
22
|
+
};
|
|
23
|
+
export type ErrorCode = (typeof ErrorCode)[keyof typeof ErrorCode];
|
|
24
|
+
export interface ToolErrorResult {
|
|
25
|
+
content: Array<{
|
|
26
|
+
type: 'text';
|
|
27
|
+
text: string;
|
|
28
|
+
}>;
|
|
29
|
+
isError: true;
|
|
30
|
+
_meta: {
|
|
31
|
+
code: ErrorCode;
|
|
32
|
+
details?: Record<string, unknown>;
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/** Build a structured MCP error result. */
|
|
36
|
+
export declare function toolError(code: ErrorCode, message: string, details?: Record<string, unknown>): ToolErrorResult;
|
|
37
|
+
/**
|
|
38
|
+
* Convert a caught `unknown` error into a structured tool result. Preserves
|
|
39
|
+
* user-visible messages for known `Error` types and refuses to leak stack
|
|
40
|
+
* traces or raw objects.
|
|
41
|
+
*/
|
|
42
|
+
export declare function toolErrorFrom(code: ErrorCode, err: unknown, prefix?: string): ToolErrorResult;
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Closed error-code taxonomy for MCP tool responses. Every handler uses
|
|
3
|
+
* `toolError(code, message, details?)` instead of ad-hoc text so clients
|
|
4
|
+
* can switch on `_meta.code` without regex-parsing free text.
|
|
5
|
+
*
|
|
6
|
+
* Adding a new code requires a design.md note — this set is intentionally
|
|
7
|
+
* small and stable.
|
|
8
|
+
*/
|
|
9
|
+
export const ErrorCode = {
|
|
10
|
+
INVALID_INPUT: 'INVALID_INPUT',
|
|
11
|
+
UNSAFE_PATH: 'UNSAFE_PATH',
|
|
12
|
+
UPSTREAM_HTTP: 'UPSTREAM_HTTP',
|
|
13
|
+
UPSTREAM_TIMEOUT: 'UPSTREAM_TIMEOUT',
|
|
14
|
+
UPSTREAM_REFUSED: 'UPSTREAM_REFUSED',
|
|
15
|
+
UNSUPPORTED_FORMAT: 'UNSUPPORTED_FORMAT',
|
|
16
|
+
RESOURCE_TOO_LARGE: 'RESOURCE_TOO_LARGE',
|
|
17
|
+
ZDR_INCOMPATIBLE: 'ZDR_INCOMPATIBLE',
|
|
18
|
+
MODEL_NOT_FOUND: 'MODEL_NOT_FOUND',
|
|
19
|
+
JOB_FAILED: 'JOB_FAILED',
|
|
20
|
+
JOB_STILL_RUNNING: 'JOB_STILL_RUNNING',
|
|
21
|
+
INTERNAL: 'INTERNAL',
|
|
22
|
+
};
|
|
23
|
+
/** Build a structured MCP error result. */
|
|
24
|
+
export function toolError(code, message, details) {
|
|
25
|
+
const result = {
|
|
26
|
+
content: [{ type: 'text', text: message }],
|
|
27
|
+
isError: true,
|
|
28
|
+
_meta: { code },
|
|
29
|
+
};
|
|
30
|
+
if (details !== undefined)
|
|
31
|
+
result._meta.details = details;
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Convert a caught `unknown` error into a structured tool result. Preserves
|
|
36
|
+
* user-visible messages for known `Error` types and refuses to leak stack
|
|
37
|
+
* traces or raw objects.
|
|
38
|
+
*/
|
|
39
|
+
export function toolErrorFrom(code, err, prefix) {
|
|
40
|
+
const base = prefix ? `${prefix}: ` : '';
|
|
41
|
+
if (err instanceof Error)
|
|
42
|
+
return toolError(code, base + err.message);
|
|
43
|
+
if (typeof err === 'string')
|
|
44
|
+
return toolError(code, base + err);
|
|
45
|
+
return toolError(code, base + 'unknown error');
|
|
46
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -21,7 +21,7 @@ if (!apiKey) {
|
|
|
21
21
|
process.exit(1);
|
|
22
22
|
}
|
|
23
23
|
const defaultModel = process.env.OPENROUTER_DEFAULT_MODEL || process.env.DEFAULT_MODEL || DEFAULT_MODEL;
|
|
24
|
-
const server = new Server({ name: 'openrouter-multimodal-server', version: '1.
|
|
24
|
+
const server = new Server({ name: 'openrouter-multimodal-server', version: '3.1.0' }, { capabilities: { tools: {} } });
|
|
25
25
|
server.onerror = (error) => console.error('[MCP Error]', error);
|
|
26
26
|
new ToolHandlers(server, apiKey, defaultModel);
|
|
27
27
|
process.on('SIGINT', async () => {
|
package/dist/logger.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stderr-bound JSON line logger. stdout is the MCP transport so logs MUST
|
|
3
|
+
* go to stderr or the client will choke. Output is one JSON object per line:
|
|
4
|
+
*
|
|
5
|
+
* {"ts":"2026-04-20T14:03:10.123Z","level":"info","msg":"job_submitted",
|
|
6
|
+
* "ctx":{"model":"google/veo-3.1","id":"vid_abc"}}
|
|
7
|
+
*
|
|
8
|
+
* Level is filtered by OPENROUTER_LOG_LEVEL (error|warn|info|debug,
|
|
9
|
+
* default info). Unknown values fall through to info.
|
|
10
|
+
*/
|
|
11
|
+
export type LogLevel = 'error' | 'warn' | 'info' | 'debug';
|
|
12
|
+
/** Low-level write hook, replaceable in tests. */
|
|
13
|
+
export declare const _sink: {
|
|
14
|
+
write(line: string): void;
|
|
15
|
+
};
|
|
16
|
+
export declare function log(level: LogLevel, msg: string, ctx?: Record<string, unknown>): void;
|
|
17
|
+
export declare const logger: {
|
|
18
|
+
error: (msg: string, ctx?: Record<string, unknown>) => void;
|
|
19
|
+
warn: (msg: string, ctx?: Record<string, unknown>) => void;
|
|
20
|
+
info: (msg: string, ctx?: Record<string, unknown>) => void;
|
|
21
|
+
debug: (msg: string, ctx?: Record<string, unknown>) => void;
|
|
22
|
+
};
|
package/dist/logger.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
const LEVEL_ORDER = {
|
|
2
|
+
error: 0,
|
|
3
|
+
warn: 1,
|
|
4
|
+
info: 2,
|
|
5
|
+
debug: 3,
|
|
6
|
+
};
|
|
7
|
+
function currentLevel() {
|
|
8
|
+
const raw = (process.env.OPENROUTER_LOG_LEVEL ?? '').toLowerCase();
|
|
9
|
+
if (raw === 'error' || raw === 'warn' || raw === 'info' || raw === 'debug')
|
|
10
|
+
return raw;
|
|
11
|
+
return 'info';
|
|
12
|
+
}
|
|
13
|
+
/** Low-level write hook, replaceable in tests. */
|
|
14
|
+
export const _sink = {
|
|
15
|
+
write(line) {
|
|
16
|
+
process.stderr.write(line + '\n');
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
export function log(level, msg, ctx) {
|
|
20
|
+
if (LEVEL_ORDER[level] > LEVEL_ORDER[currentLevel()])
|
|
21
|
+
return;
|
|
22
|
+
const record = {
|
|
23
|
+
ts: new Date().toISOString(),
|
|
24
|
+
level,
|
|
25
|
+
msg,
|
|
26
|
+
};
|
|
27
|
+
if (ctx)
|
|
28
|
+
record.ctx = ctx;
|
|
29
|
+
try {
|
|
30
|
+
_sink.write(JSON.stringify(record));
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
// Fall back to a short-form record if `ctx` contains something unserializable.
|
|
34
|
+
_sink.write(JSON.stringify({
|
|
35
|
+
ts: new Date().toISOString(),
|
|
36
|
+
level,
|
|
37
|
+
msg,
|
|
38
|
+
ctx: { note: 'unserializable' },
|
|
39
|
+
}));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
export const logger = {
|
|
43
|
+
error: (msg, ctx) => log('error', msg, ctx),
|
|
44
|
+
warn: (msg, ctx) => log('warn', msg, ctx),
|
|
45
|
+
info: (msg, ctx) => log('info', msg, ctx),
|
|
46
|
+
debug: (msg, ctx) => log('debug', msg, ctx),
|
|
47
|
+
};
|