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

CI npm version Node

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):

  1. CLI flag --config <path> (or --config-file, -c, NINEROUTER_CONFIG env)
  2. ~/.config/ninerouter-mcp/config.toml
  3. 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.

ParameterTypeRequiredDescription
kindstringnoOne of chat, image, tts, embedding, web, stt, image-to-text. Omit for default chat models.
searchstringnoCase-insensitive substring match against model ids/names.
limitnumbernoPage size (default 20, max 500). Use 0 for the whole list.
offsetnumbernoIndex of the first model to return (default 0). Use nextOffset to page.
refreshbooleannoBypass 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.

ParameterTypeDefaultNotes
querystring—Required.
modelstring—9Router model id (e.g. tavily/search). Falls back to provider, then default_models.web_search.
providerstring—Alias for model.
maxResultsnumber51–20.
searchTypestringwebweb or news.
countrystring—
languagestring—
timeRangestring—
domainFilterstring—

web_fetch

Fetch a URL and return it as markdown, text, or HTML.

ParameterTypeDefaultNotes
urlstring—Required. Must be a valid URL.
modelstring—e.g. jina-reader/fetch, firecrawl/fetch.
providerstring—Alias for model.
formatstringmarkdownmarkdown, text, or html.
maxCharactersnumber8000Truncation 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.

ParameterTypeDefaultNotes
promptstring—Required.
modelstring—e.g. openai/dall-e-3, gemini/gemini-3-pro-image-preview.
providerstring—Alias for model.
nnumber11–10.
sizestring1024x1024
qualitystring—standard or hd.
outputPathstringOS tmpdirWhere 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.

ParameterTypeDefaultNotes
inputstring—Required.
modelstring—e.g. openai/tts-1, edge-tts/vi-VN-HoaiMyNeural.
providerstring—Alias for model.
outputPathstringOS tmpdirWhere 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.

ParameterTypeDefaultNotes
audioPathstring—Local file path.
audioBase64string—Base64 payload.
fileNamestringderivedUsed for the multipart upload filename.
modelstring—e.g. openai/whisper-1, groq/whisper-large-v3-turbo.
providerstring—Alias for model.
languagestring—ISO-639-1 code, e.g. en, vi.
promptstring—
responseFormatstringjsonjson, text, verbose_json, srt, vtt.
temperaturenumber—0–1.

embeddings

Generate embeddings for a string or a batch of strings.

ParameterTypeDefaultNotes
inputstring | array—Required. Either one string or an array of non-empty strings.
modelstring—e.g. openai/text-embedding-3-small.
providerstring—Alias for model.
encodingFormatstringfloatfloat or base64.
dimensionsnumber—Optional override.

Behavior notes

  • Fallback chain. For every model-using tool: if model/provider is set, only that model is tried. Otherwise the chain in default_models.<tool> is tried in order. If all entries fail, the tool throws an All models failed. Errors: ... error that includes every per-model message.
  • provider is an alias for model on 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_image and text_to_speech request b64_json / mp3 from upstream, write the bytes to outputPath (or the OS temp dir if you omit it), and return the asset as an MCP image / audio content block plus { outputPath, bytes, contentType }. The host can display inline or just use the path. Extension is derived from upstream content-type.
  • Config file wins over env vars. If you need different settings for a single run, prefer --config over exporting env vars.
  • STT multipart upload. The tool sends the audio as multipart/form-data; fileName only matters when the upstream provider inspects the filename.
  • list_models is trimmed and cached. The tool caps its output (default 20) and keeps the full upstream list in memory for about a minute. This is not server-side pagination: /v1/models ignores 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). Pass refresh: true to force a refetch.
  • Config is read once at startup. Edit config.toml or change NINEROUTER_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.toml with base_url.
  • No model specified and no default_models.<tool> configured — either pass model in the call or add a default_models entry 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_KEY or api_key in 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.