9Router MCP Server
MCP server for 9Router — exposes web search, web fetch, TTS, and other capabilities as native tools, eliminating the per-call overhead of skill-file-based approaches (re-reading instructions, manual formatting, retry-on-error) in favor of structured tool calls over stdio.
Documentation
9Router MCP Server
MCP server that exposes 9Router capabilities as native tools for any MCP client: model discovery, automatic fallback, and Zod-validated inputs.
Status: 9Router already hides provider-specific complexity behind a single API. This server exposes that API through MCP, so agents and apps can call web search, web fetch, image generation, TTS, STT, and embeddings as standard MCP tools — no skill-file loading, no per-provider glue code.
Why use it
- Tools are always registered; no manual skill loading (saves tokens and context).
- Automatic model fallback when the primary model fails.
- Zod-validated inputs with clear error messages before any network call.
- One config file for endpoint, auth, and default models with fallback chains.
- Single transport (
stdio); works with any MCP-capable client.
Chat / code generation is intentionally not included — that is what the host model is for.
Contents
Installation
The package is ninerouter-mcp on npm. The JSON body is identical across every MCP client — only the top-level key and file path differ. Paste the block into the right key in your client's config:
{
"ninerouter": {
"type": "stdio",
"command": "npx",
"args": ["-y", "ninerouter-mcp"],
"env": {
"NINEROUTER_URL": "http://localhost:20128"
}
}
}
Top-level keys by client: VS Code (servers), OpenCode (mcp, rename env → environment, set type: "local" and enabled: true), Claude Code / Cursor / Windsurf / Claude Desktop / Zed (mcpServers or context_servers, drop the type line). JetBrains uses a UI dialog at Settings → Tools → AI Assistant → MCP with the same command, args, and env.
Claude Code
claude mcp add --scope user ninerouter -e NINEROUTER_URL=http://localhost:20128 -- npx -y ninerouter-mcp
Codex CLI
codex mcp add ninerouter --env NINEROUTER_URL=http://localhost:20128 -- npx -y ninerouter-mcp
Hermes Agent
Add to ~/.hermes/config.yaml:
mcp_servers:
ninerouter:
command: npx
args:
- -y
- ninerouter-mcp
env:
NINEROUTER_URL: http://localhost:20128
Configuration
The server needs a 9Router base URL. Optionally it accepts an API key and default-model fallbacks for each tool.
Sources, in priority order (highest first):
- CLI flag
--config <path>(or--config-file,-c,NINEROUTER_CONFIGenv) ~/.config/ninerouter-mcp/config.toml- Environment variables:
NINEROUTER_URL,NINEROUTER_KEY
If the config file exists, it wins over environment variables.
Quick setup
Generate a starter config file and edit it:
npx ninerouter-mcp create-config
This writes ~/.config/ninerouter-mcp/config.toml and refuses to overwrite an existing file.
Manual setup (env vars)
Windows (PowerShell):
$env:NINEROUTER_URL = "http://localhost:20128"
$env:NINEROUTER_KEY = "sk-..." # optional
macOS / Linux:
export NINEROUTER_URL=http://localhost:20128
export NINEROUTER_KEY=sk-... # optional
NINEROUTER_KEY is optional and only required when your 9Router instance has auth enabled.
Manual setup (config file)
# 9Router base URL (required)
base_url = "http://localhost:20128"
# Optional API key
# api_key = "sk-..."
# Default models with fallback support.
# Single string = one model.
# Array = tried in order, first success wins, errors are aggregated.
[default_models]
web_search = ["tavily/search", "brave-search/search"]
web_fetch = ["firecrawl/fetch", "jina-reader/fetch"]
generate_image = "openai/dall-e-3"
text_to_speech = "openai/tts-1"
speech_to_text = ["openai/whisper-1", "groq/whisper-large-v3-turbo"]
embeddings = "openai/text-embedding-3-small"
# Defaults for the list_models tool (all optional; these are the built-in values)
[list_models]
limit = 20 # default page size
offset = 0 # default start index
cache_ttl_seconds = 60 # 0 disables caching
The ninerouter table is also accepted as an alias for top-level base_url and api_key:
[ninerouter]
base_url = "http://localhost:20128"
api_key = "sk-..."
Point to a non-default config file:
npx -y ninerouter-mcp --config /path/to/config.toml
# or
NINEROUTER_CONFIG=/path/to/config.toml npx -y ninerouter-mcp
Run
After npm install from this repo:
npm run build
npm start
Or in watch mode during development:
npm run dev
Published package users can run it directly:
npx -y ninerouter-mcp
Tools
Every tool is registered with the MCP server at startup. Unless noted, all tools return a single text content block with pretty-printed JSON.
list_models
Look up model ids exposed by 9Router. This tool is optional: every other tool works without it, because each has a configured default model or fallback chain. Call it only to target a specific model or to discover ids that differ from the defaults. It filters by search, then pages through the matches with offset/limit, so a large catalog cannot flood the context window. Each response reports total, count, offset, and nextOffset (omitted on the last page). The upstream list is cached for about a minute, because 9Router rebuilds the whole catalog on every request (several seconds), so only the first call is slow.
| Parameter | Type | Required | Description |
|---|---|---|---|
kind | string | no | One of chat, image, tts, embedding, web, stt, image-to-text. Omit for default chat models. |
search | string | no | Case-insensitive substring match against model ids/names. |
limit | number | no | Page size (default 20, max 500). Use 0 for the whole list. |
offset | number | no | Index of the first model to return (default 0). Use nextOffset to page. |
refresh | boolean | no | Bypass the cache and refetch from 9Router. |
Defaults can be overridden in config.toml:
[list_models]
limit = 20 # default page size
offset = 0 # default start index
cache_ttl_seconds = 60 # 0 disables caching
Built-in defaults (used when a key is omitted): limit = 20, offset = 0, cache_ttl_seconds = 60.
web_search
Search the web through 9Router.
| Parameter | Type | Default | Notes |
|---|---|---|---|
query | string | — | Required. |
model | string | — | 9Router model id (e.g. tavily/search). Falls back to provider, then default_models.web_search. |
provider | string | — | Alias for model. |
maxResults | number | 5 | 1–20. |
searchType | string | web | web or news. |
country | string | — | |
language | string | — | |
timeRange | string | — | |
domainFilter | string | — |
web_fetch
Fetch a URL and return it as markdown, text, or HTML.
| Parameter | Type | Default | Notes |
|---|---|---|---|
url | string | — | Required. Must be a valid URL. |
model | string | — | e.g. jina-reader/fetch, firecrawl/fetch. |
provider | string | — | Alias for model. |
format | string | markdown | markdown, text, or html. |
maxCharacters | number | 8000 | Truncation limit. |
generate_image
Text-to-image generation. Always writes a file and returns the image as a base64 content block plus { outputPath, bytes, contentType }. Omit outputPath to write to the OS temp dir.
| Parameter | Type | Default | Notes |
|---|---|---|---|
prompt | string | — | Required. |
model | string | — | e.g. openai/dall-e-3, gemini/gemini-3-pro-image-preview. |
provider | string | — | Alias for model. |
n | number | 1 | 1–10. |
size | string | 1024x1024 | |
quality | string | — | standard or hd. |
outputPath | string | OS tmpdir | Where to write the file. Default os.tmpdir()/ninerouter-<slug>-<ts>.<ext>. Ext from content-type. |
text_to_speech
Synthesize audio. Always writes a file and returns the audio as a base64 content block plus { outputPath, bytes, contentType }. Omit outputPath to write to the OS temp dir.
| Parameter | Type | Default | Notes |
|---|---|---|---|
input | string | — | Required. |
model | string | — | e.g. openai/tts-1, edge-tts/vi-VN-HoaiMyNeural. |
provider | string | — | Alias for model. |
outputPath | string | OS tmpdir | Where to write the file. Default os.tmpdir()/ninerouter-<slug>-<ts>.<ext>. Ext from content-type. |
speech_to_text
Transcribe audio. Provide exactly one of audioPath or audioBase64.
| Parameter | Type | Default | Notes |
|---|---|---|---|
audioPath | string | — | Local file path. |
audioBase64 | string | — | Base64 payload. |
fileName | string | derived | Used for the multipart upload filename. |
model | string | — | e.g. openai/whisper-1, groq/whisper-large-v3-turbo. |
provider | string | — | Alias for model. |
language | string | — | ISO-639-1 code, e.g. en, vi. |
prompt | string | — | |
responseFormat | string | json | json, text, verbose_json, srt, vtt. |
temperature | number | — | 0–1. |
embeddings
Generate embeddings for a string or a batch of strings.
| Parameter | Type | Default | Notes |
|---|---|---|---|
input | string | array | — | Required. Either one string or an array of non-empty strings. |
model | string | — | e.g. openai/text-embedding-3-small. |
provider | string | — | Alias for model. |
encodingFormat | string | float | float or base64. |
dimensions | number | — | Optional override. |
Behavior notes
- Fallback chain. For every model-using tool: if
model/provideris set, only that model is tried. Otherwise the chain indefault_models.<tool>is tried in order. If all entries fail, the tool throws anAll models failed. Errors: ...error that includes every per-model message. provideris an alias formodelon every tool that accepts a model. Set whichever reads better for your use case.- Image and audio are always written to a file and returned as a content block.
generate_imageandtext_to_speechrequestb64_json/mp3from upstream, write the bytes tooutputPath(or the OS temp dir if you omit it), and return the asset as an MCPimage/audiocontent block plus{ outputPath, bytes, contentType }. The host can display inline or just use the path. Extension is derived from upstreamcontent-type. - Config file wins over env vars. If you need different settings for a single run, prefer
--configover exporting env vars. - STT multipart upload. The tool sends the audio as
multipart/form-data;fileNameonly matters when the upstream provider inspects the filename. list_modelsis trimmed and cached. The tool caps its output (default20) and keeps the full upstream list in memory for about a minute. This is not server-side pagination:/v1/modelsignores query params and always returns the whole catalog. The cap protects the context window, and the cache protects latency, since 9Router rebuilds the catalog on every request (observed ~7s, even for/v1/models/<kind>, which returns a tiny body). Passrefresh: trueto force a refetch.- Config is read once at startup. Edit
config.tomlor changeNINEROUTER_URL/NINEROUTER_KEY, then restart the MCP server in your client. Hot-reload is not implemented.
Troubleshooting
NINEROUTER_URL is required— set the env var or create~/.config/ninerouter-mcp/config.tomlwithbase_url.No model specified and no default_models.<tool> configured— either passmodelin the call or add adefault_modelsentry to your config.All models failed. Errors: ...— every fallback model returned an error; the aggregated message includes each one for diagnosis.- Auth errors (401/403) — your 9Router instance requires a key; set
NINEROUTER_KEYorapi_keyin the config file. - STT fails with "Provide audioPath or audioBase64" — exactly one of those two must be set.
Development
npm install
npm run dev # tsx watch mode
npm run build # tsc -> dist/
npm start # node dist/index.js
npm run check # typecheck + lint + prettier --check
Project layout:
src/
index.ts # bin entry; dispatches create-config or server
server.ts # McpServer setup
ninerouter-client.ts # config + HTTP helpers
create-config.ts # `ninerouter-mcp create-config` subcommand
tools/
models.ts # list_models
web.ts # web_search, web_fetch
media.ts # generate_image, text_to_speech, speech_to_text
embeddings.ts # embeddings
common.ts # shared fallback + json helpers
config.example.toml # sample config (mirrors create-config output)
License
Apache-2.0. See LICENSE.