ninerouter-mcp 1.0.1 → 1.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 CHANGED
@@ -1,182 +1,315 @@
1
- # NineRouter MCP
2
-
3
- MCP server for 9Router with model discovery, automatic fallback, and type-safe validation.
4
-
5
- ## Why this exists
6
-
7
- 9Router already hides provider-specific complexity behind a single API. This project exposes that API through MCP so clients can use 9Router capabilities directly as native tools.
8
-
9
- **Benefits over skill-based approaches:**
10
- - No repeated skill file loading (saves tokens and reduces context usage)
11
- - Tools are always available without manual skill invocation
12
- - Automatic model fallback when primary models fail
13
- - Type-safe validation with proper error messages
14
- - Direct integration into MCP-native workflows
15
-
16
- The goal is not to replace 9Router or duplicate its docs. The goal is to make 9Router capabilities immediately accessible from any MCP client.
17
-
18
- ## What it provides
19
-
20
- - Model discovery for choosing valid model ids
21
- - Web search for finding current information
22
- - Web fetch for turning URLs into readable text or markdown
23
- - Image generation for text-to-image workflows
24
- - Text-to-speech for voice output
25
- - Speech-to-text for transcription
26
- - Embeddings for retrieval and semantic search
27
-
28
- ## When to use it
29
-
30
- Use this server when you want an AI agent or app to work with 9Router through MCP instead of custom integration code for each capability.
31
-
32
- It is especially useful when you want one backend that can access multiple providers but still present a single tool interface to the model.
33
-
34
- If your client can already call MCP tools directly, you do not need separate 9Router skill docs; the MCP server becomes the integration layer.
35
-
36
- Chat/code-gen is intentionally left out of this MCP server.
37
-
38
- ## Installation
39
-
40
- **Kilo CLI / VS Code**
41
-
42
- Add to your `kilo.json`:
43
-
44
- ```json
45
- {
46
- "mcp": {
47
- "ninerouter": {
48
- "type": "local",
49
- "command": ["npx", "-y", "ninerouter-mcp"],
50
- "environment": {
51
- "NINEROUTER_URL": "http://localhost:20128"
52
- },
53
- "enabled": true
54
- }
55
- }
56
- }
57
- ```
58
-
59
- **Claude Code**
60
-
61
- ```bash
62
- claude mcp add --scope user ninerouter -e NINEROUTER_URL=http://localhost:20128 -- npx -y ninerouter-mcp
63
- ```
64
-
65
- **Codex CLI**
66
-
67
- ```bash
68
- codex mcp add ninerouter --env NINEROUTER_URL=http://localhost:20128 -- npx -y ninerouter-mcp
69
- ```
70
-
71
- **Agent Manager (VS Code Extension)**
72
-
73
- Use the MCP settings in your workspace `kilo.json` (same as Kilo CLI above). Agent Manager automatically inherits MCP servers from Kilo config.
74
-
75
- ## Configuration
76
-
77
- ### Quick Setup
78
-
79
- Create a config file with defaults:
80
-
81
- ```bash
82
- npx ninerouter-mcp create-config
83
- ```
84
-
85
- This creates `~/.config/ninerouter-mcp/config.toml` with sample configuration. Edit it to set your 9Router base URL and optionally configure default models.
86
-
87
- ### Manual Setup
88
-
89
- 1. Install dependencies:
90
-
91
- ```bash
92
- npm install
93
- ```
94
-
95
- 2. Configure the 9Router endpoint:
96
-
97
- ```bash
98
- set NINEROUTER_URL=http://localhost:20128
99
- set NINEROUTER_KEY=sk-...
100
- ```
101
-
102
- `NINEROUTER_KEY` is optional when your 9Router instance does not require auth.
103
-
104
- You can also use a config file at `~/.config/ninerouter-mcp/config.toml`.
105
- TOML settings override environment variables.
106
-
107
- Example:
108
-
109
- ```toml
110
- base_url = "http://localhost:20128"
111
- api_key = "sk-..."
112
-
113
- # Optional: default models with fallback support
114
- [default_models]
115
- web_search = ["tavily/search", "brave-search/search"]
116
- web_fetch = "firecrawl/fetch"
117
- generate_image = "openai/dall-e-3"
118
- text_to_speech = "openai/tts-1"
119
- speech_to_text = ["openai/whisper-1", "groq/whisper-large-v3-turbo"]
120
- embeddings = "openai/text-embedding-3-small"
121
- ```
122
-
123
- Default models can be a single string or array of strings. When array, models are tried in order until one succeeds. If all fail, errors are aggregated.
124
-
125
- To pass a different config file at startup, use `--config`:
126
-
127
- ```bash
128
- npm start -- --config D:\path\to\config.toml
129
- ```
130
-
131
- 3. Build the server:
132
-
133
- ```bash
134
- npm run build
135
- ```
136
-
137
- 4. Start the MCP server over stdio:
138
-
139
- ```bash
140
- npm start
141
- ```
142
-
143
- ## Run
144
-
145
- For local development in this repo:
146
-
147
- ```bash
148
- npm run build
149
- npm start
150
- ```
151
-
152
- If you publish the package to npm, users can run it directly with npx:
153
-
154
- ```bash
155
- npx -y ninerouter-mcp
156
- ```
157
-
158
- That works because the package exposes a `bin` entry and builds `dist/index.js` during packing.
159
-
160
- If you want to run from the repo without publishing, use:
161
-
162
- ```bash
163
- npx tsx src/index.ts
164
- ```
165
-
166
- ## Available Tools
167
-
168
- - `list_models`
169
- - `web_search`
170
- - `web_fetch`
171
- - `generate_image`
172
- - `text_to_speech`
173
- - `speech_to_text`
174
- - `embeddings`
175
-
176
- ## Notes
177
-
178
- - The server expects a local or reachable 9Router base URL in `NINEROUTER_URL`.
179
- - If present, `~/.config/ninerouter-mcp/config.toml` is loaded first and wins over env vars.
180
- - `list_models` can list default chat models or a specific capability kind like `image`, `tts`, `embedding`, `web`, `stt`, or `image-to-text`.
181
- - STT accepts either a local `audioPath` or a base64 payload.
182
- - Image and audio tools return JSON text, including base64 data when the upstream API returns binary content.
1
+ 9Router MCP Server
2
+ ==================
3
+
4
+ [![CI](https://github.com/k1rana/ninerouter-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/k1rana/ninerouter-mcp/actions/workflows/ci.yml)
5
+ [![npm version](https://img.shields.io/npm/v/ninerouter-mcp.svg)](https://www.npmjs.com/package/ninerouter-mcp)
6
+ [![Node](https://img.shields.io/badge/node-%3E%3D20-339933.svg)](https://www.npmjs.com/package/ninerouter-mcp)
7
+
8
+ MCP server that exposes 9Router capabilities as native tools for any MCP client: model discovery, automatic fallback, and Zod-validated inputs.
9
+
10
+ > **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.
11
+
12
+ ## Why use it
13
+
14
+ - Tools are always registered; no manual skill loading (saves tokens and context).
15
+ - Automatic model fallback when the primary model fails.
16
+ - Zod-validated inputs with clear error messages before any network call.
17
+ - One config file for endpoint, auth, and default models with fallback chains.
18
+ - Single transport (`stdio`); works with any MCP-capable client.
19
+
20
+ Chat / code generation is intentionally **not** included — that is what the host model is for.
21
+
22
+ ## Contents
23
+
24
+ - [Installation](#installation)
25
+ - [Configuration](#configuration)
26
+ - [Run](#run)
27
+ - [Tools](#tools)
28
+ - [Troubleshooting](#troubleshooting)
29
+ - [Development](#development)
30
+ - [License](#license)
31
+
32
+ ## Installation
33
+
34
+ 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:
35
+
36
+ ```json
37
+ {
38
+ "ninerouter": {
39
+ "type": "stdio",
40
+ "command": "npx",
41
+ "args": ["-y", "ninerouter-mcp"],
42
+ "env": {
43
+ "NINEROUTER_URL": "http://localhost:20128"
44
+ }
45
+ }
46
+ }
47
+ ```
48
+
49
+ 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.
50
+
51
+ ### Claude Code
52
+
53
+ ```bash
54
+ claude mcp add --scope user ninerouter -e NINEROUTER_URL=http://localhost:20128 -- npx -y ninerouter-mcp
55
+ ```
56
+
57
+ ### Codex CLI
58
+
59
+ ```bash
60
+ codex mcp add ninerouter --env NINEROUTER_URL=http://localhost:20128 -- npx -y ninerouter-mcp
61
+ ```
62
+
63
+ ### Hermes Agent
64
+
65
+ Add to `~/.hermes/config.yaml`:
66
+
67
+ ```yaml
68
+ mcp_servers:
69
+ ninerouter:
70
+ command: npx
71
+ args:
72
+ - -y
73
+ - ninerouter-mcp
74
+ env:
75
+ NINEROUTER_URL: http://localhost:20128
76
+ ```
77
+
78
+ ## Configuration
79
+
80
+ The server needs a 9Router base URL. Optionally it accepts an API key and default-model fallbacks for each tool.
81
+
82
+ Sources, in priority order (highest first):
83
+
84
+ 1. CLI flag `--config <path>` (or `--config-file`, `-c`, `NINEROUTER_CONFIG` env)
85
+ 2. `~/.config/ninerouter-mcp/config.toml`
86
+ 3. Environment variables: `NINEROUTER_URL`, `NINEROUTER_KEY`
87
+
88
+ If the config file exists, it wins over environment variables.
89
+
90
+ ### Quick setup
91
+
92
+ Generate a starter config file and edit it:
93
+
94
+ ```bash
95
+ npx ninerouter-mcp create-config
96
+ ```
97
+
98
+ This writes `~/.config/ninerouter-mcp/config.toml` and refuses to overwrite an existing file.
99
+
100
+ ### Manual setup (env vars)
101
+
102
+ **Windows (PowerShell):**
103
+
104
+ ```powershell
105
+ $env:NINEROUTER_URL = "http://localhost:20128"
106
+ $env:NINEROUTER_KEY = "sk-..." # optional
107
+ ```
108
+
109
+ **macOS / Linux:**
110
+
111
+ ```bash
112
+ export NINEROUTER_URL=http://localhost:20128
113
+ export NINEROUTER_KEY=sk-... # optional
114
+ ```
115
+
116
+ `NINEROUTER_KEY` is optional and only required when your 9Router instance has auth enabled.
117
+
118
+ ### Manual setup (config file)
119
+
120
+ ```toml
121
+ # 9Router base URL (required)
122
+ base_url = "http://localhost:20128"
123
+
124
+ # Optional API key
125
+ # api_key = "sk-..."
126
+
127
+ # Default models with fallback support.
128
+ # Single string = one model.
129
+ # Array = tried in order, first success wins, errors are aggregated.
130
+ [default_models]
131
+ web_search = ["tavily/search", "brave-search/search"]
132
+ web_fetch = ["firecrawl/fetch", "jina-reader/fetch"]
133
+ generate_image = "openai/dall-e-3"
134
+ text_to_speech = "openai/tts-1"
135
+ speech_to_text = ["openai/whisper-1", "groq/whisper-large-v3-turbo"]
136
+ embeddings = "openai/text-embedding-3-small"
137
+ ```
138
+
139
+ The `ninerouter` table is also accepted as an alias for top-level `base_url` and `api_key`:
140
+
141
+ ```toml
142
+ [ninerouter]
143
+ base_url = "http://localhost:20128"
144
+ api_key = "sk-..."
145
+ ```
146
+
147
+ Point to a non-default config file:
148
+
149
+ ```bash
150
+ npx -y ninerouter-mcp --config /path/to/config.toml
151
+ # or
152
+ NINEROUTER_CONFIG=/path/to/config.toml npx -y ninerouter-mcp
153
+ ```
154
+
155
+ ## Run
156
+
157
+ After `npm install` from this repo:
158
+
159
+ ```bash
160
+ npm run build
161
+ npm start
162
+ ```
163
+
164
+ Or in watch mode during development:
165
+
166
+ ```bash
167
+ npm run dev
168
+ ```
169
+
170
+ Published package users can run it directly:
171
+
172
+ ```bash
173
+ npx -y ninerouter-mcp
174
+ ```
175
+
176
+ ## Tools
177
+
178
+ Every tool is registered with the MCP server at startup. Unless noted, all tools return a single text content block with pretty-printed JSON.
179
+
180
+ ### `list_models`
181
+
182
+ Discover valid model ids before calling other tools.
183
+
184
+ | Parameter | Type | Required | Description |
185
+ | --------- | ------ | -------- | -------------------------------------------------------------------------------------------------------- |
186
+ | `kind` | string | no | One of `chat`, `image`, `tts`, `embedding`, `web`, `stt`, `image-to-text`. Omit for default chat models. |
187
+
188
+ ### `web_search`
189
+
190
+ Search the web through 9Router.
191
+
192
+ | Parameter | Type | Default | Notes |
193
+ | -------------- | ------ | ------- | ---------------------------------------------------------------------------------------------------- |
194
+ | `query` | string | — | Required. |
195
+ | `model` | string | — | 9Router model id (e.g. `tavily/search`). Falls back to `provider`, then `default_models.web_search`. |
196
+ | `provider` | string | — | Alias for `model`. |
197
+ | `maxResults` | number | `5` | 1–20. |
198
+ | `searchType` | string | `web` | `web` or `news`. |
199
+ | `country` | string | — | |
200
+ | `language` | string | — | |
201
+ | `timeRange` | string | — | |
202
+ | `domainFilter` | string | — | |
203
+
204
+ ### `web_fetch`
205
+
206
+ Fetch a URL and return it as markdown, text, or HTML.
207
+
208
+ | Parameter | Type | Default | Notes |
209
+ | --------------- | ------ | ---------- | -------------------------------------------- |
210
+ | `url` | string | — | Required. Must be a valid URL. |
211
+ | `model` | string | — | e.g. `jina-reader/fetch`, `firecrawl/fetch`. |
212
+ | `provider` | string | — | Alias for `model`. |
213
+ | `format` | string | `markdown` | `markdown`, `text`, or `html`. |
214
+ | `maxCharacters` | number | `8000` | Truncation limit. |
215
+
216
+ ### `generate_image`
217
+
218
+ 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.
219
+
220
+ | Parameter | Type | Default | Notes |
221
+ | ------------ | ------ | ----------- | --------------------------------------------------------------------------------------------------- |
222
+ | `prompt` | string | — | Required. |
223
+ | `model` | string | — | e.g. `openai/dall-e-3`, `gemini/gemini-3-pro-image-preview`. |
224
+ | `provider` | string | — | Alias for `model`. |
225
+ | `n` | number | `1` | 1–10. |
226
+ | `size` | string | `1024x1024` | |
227
+ | `quality` | string | — | `standard` or `hd`. |
228
+ | `outputPath` | string | OS tmpdir | Where to write the file. Default `os.tmpdir()/ninerouter-<slug>-<ts>.<ext>`. Ext from content-type. |
229
+
230
+ ### `text_to_speech`
231
+
232
+ 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.
233
+
234
+ | Parameter | Type | Default | Notes |
235
+ | ------------ | ------ | --------- | --------------------------------------------------------------------------------------------------- |
236
+ | `input` | string | — | Required. |
237
+ | `model` | string | — | e.g. `openai/tts-1`, `edge-tts/vi-VN-HoaiMyNeural`. |
238
+ | `provider` | string | — | Alias for `model`. |
239
+ | `outputPath` | string | OS tmpdir | Where to write the file. Default `os.tmpdir()/ninerouter-<slug>-<ts>.<ext>`. Ext from content-type. |
240
+
241
+ ### `speech_to_text`
242
+
243
+ Transcribe audio. Provide exactly one of `audioPath` or `audioBase64`.
244
+
245
+ | Parameter | Type | Default | Notes |
246
+ | ---------------- | ------ | ------- | ------------------------------------------------------- |
247
+ | `audioPath` | string | — | Local file path. |
248
+ | `audioBase64` | string | — | Base64 payload. |
249
+ | `fileName` | string | derived | Used for the multipart upload filename. |
250
+ | `model` | string | — | e.g. `openai/whisper-1`, `groq/whisper-large-v3-turbo`. |
251
+ | `provider` | string | — | Alias for `model`. |
252
+ | `language` | string | — | ISO-639-1 code, e.g. `en`, `vi`. |
253
+ | `prompt` | string | — | |
254
+ | `responseFormat` | string | `json` | `json`, `text`, `verbose_json`, `srt`, `vtt`. |
255
+ | `temperature` | number | — | 0–1. |
256
+
257
+ ### `embeddings`
258
+
259
+ Generate embeddings for a string or a batch of strings.
260
+
261
+ | Parameter | Type | Default | Notes |
262
+ | ---------------- | --------------- | ------- | ------------------------------------------------------------- |
263
+ | `input` | string \| array | — | Required. Either one string or an array of non-empty strings. |
264
+ | `model` | string | — | e.g. `openai/text-embedding-3-small`. |
265
+ | `provider` | string | — | Alias for `model`. |
266
+ | `encodingFormat` | string | `float` | `float` or `base64`. |
267
+ | `dimensions` | number | — | Optional override. |
268
+
269
+ ## Behavior notes
270
+
271
+ - **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.
272
+ - **`provider` is an alias for `model`** on every tool that accepts a model. Set whichever reads better for your use case.
273
+ - **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`.
274
+ - **Config file wins over env vars.** If you need different settings for a single run, prefer `--config` over exporting env vars.
275
+ - **STT multipart upload.** The tool sends the audio as `multipart/form-data`; `fileName` only matters when the upstream provider inspects the filename.
276
+ - **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.
277
+
278
+ ## Troubleshooting
279
+
280
+ - **`NINEROUTER_URL is required`** — set the env var or create `~/.config/ninerouter-mcp/config.toml` with `base_url`.
281
+ - **`No model specified and no default_models.<tool> configured`** — either pass `model` in the call or add a `default_models` entry to your config.
282
+ - **`All models failed. Errors: ...`** — every fallback model returned an error; the aggregated message includes each one for diagnosis.
283
+ - **Auth errors (401/403)** — your 9Router instance requires a key; set `NINEROUTER_KEY` or `api_key` in the config file.
284
+ - **STT fails with "Provide audioPath or audioBase64"** — exactly one of those two must be set.
285
+
286
+ ## Development
287
+
288
+ ```bash
289
+ npm install
290
+ npm run dev # tsx watch mode
291
+ npm run build # tsc -> dist/
292
+ npm start # node dist/index.js
293
+ npm run check # typecheck + lint + prettier --check
294
+ ```
295
+
296
+ Project layout:
297
+
298
+ ```
299
+ src/
300
+ index.ts # bin entry; dispatches create-config or server
301
+ server.ts # McpServer setup
302
+ ninerouter-client.ts # config + HTTP helpers
303
+ create-config.ts # `ninerouter-mcp create-config` subcommand
304
+ tools/
305
+ models.ts # list_models
306
+ web.ts # web_search, web_fetch
307
+ media.ts # generate_image, text_to_speech, speech_to_text
308
+ embeddings.ts # embeddings
309
+ common.ts # shared fallback + json helpers
310
+ config.example.toml # sample config (mirrors create-config output)
311
+ ```
312
+
313
+ ## License
314
+
315
+ Apache-2.0. See [LICENSE](LICENSE).
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
- import { writeFile, mkdir, access } from "node:fs/promises";
3
- import { join } from "node:path";
4
- import { homedir } from "node:os";
5
- import { constants } from "node:fs";
6
- const DEFAULT_CONFIG_DIR = join(homedir(), ".config", "ninerouter-mcp");
7
- const DEFAULT_CONFIG_PATH = join(DEFAULT_CONFIG_DIR, "config.toml");
2
+ import { writeFile, mkdir, access } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+ import { homedir } from 'node:os';
5
+ import { constants } from 'node:fs';
6
+ const DEFAULT_CONFIG_DIR = join(homedir(), '.config', 'ninerouter-mcp');
7
+ const DEFAULT_CONFIG_PATH = join(DEFAULT_CONFIG_DIR, 'config.toml');
8
8
  const DEFAULT_CONFIG = `# NineRouter MCP Configuration
9
9
  # Generated on ${new Date().toISOString()}
10
10
 
@@ -48,23 +48,23 @@ async function createConfig() {
48
48
  try {
49
49
  await access(DEFAULT_CONFIG_PATH, constants.F_OK);
50
50
  console.log(`⚠️ Config file already exists at: ${DEFAULT_CONFIG_PATH}`);
51
- console.log("To avoid overwriting your config, the operation was cancelled.");
52
- console.log("\nTo create a new config, first backup or delete the existing file.");
51
+ console.log('To avoid overwriting your config, the operation was cancelled.');
52
+ console.log('\nTo create a new config, first backup or delete the existing file.');
53
53
  return;
54
54
  }
55
55
  catch {
56
56
  // File doesn't exist, safe to create
57
57
  }
58
58
  // Write config file
59
- await writeFile(DEFAULT_CONFIG_PATH, DEFAULT_CONFIG, "utf8");
59
+ await writeFile(DEFAULT_CONFIG_PATH, DEFAULT_CONFIG, 'utf8');
60
60
  console.log(`✅ Config file created at: ${DEFAULT_CONFIG_PATH}`);
61
- console.log("\nNext steps:");
62
- console.log("1. Edit the config file to set your 9Router base URL");
63
- console.log("2. Optionally configure default models for each tool");
64
- console.log("3. Restart your MCP client to load the new config");
61
+ console.log('\nNext steps:');
62
+ console.log('1. Edit the config file to set your 9Router base URL');
63
+ console.log('2. Optionally configure default models for each tool');
64
+ console.log('3. Restart your MCP client to load the new config');
65
65
  }
66
66
  catch (error) {
67
- console.error("❌ Failed to create config file:", error);
67
+ console.error('❌ Failed to create config file:', error);
68
68
  process.exit(1);
69
69
  }
70
70
  }
package/dist/index.js CHANGED
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  // Check if user wants to create config
3
- if (process.argv.includes("create-config")) {
4
- await import("./create-config.js");
3
+ if (process.argv.includes('create-config')) {
4
+ await import('./create-config.js');
5
5
  }
6
6
  else {
7
7
  // Start MCP server
8
- await import("./server.js");
8
+ await import('./server.js');
9
9
  }
10
10
  export {};