claude-openrouter-delegate-mcp
# claude-openrouter-delegate-mcp
[](https://www.npmjs.com/package/claude-openrouter-delegate-mcp)
[](https://nodejs.org)
[](LICENSE)
Delegate tasks from an Anthropic-backed Claude Code session to **OpenRouter-backed**
Claude Code sessions — without the two ever sharing environment variables.
Pointing Claude Code at OpenRouter means exporting `ANTHROPIC_*` variables into
your shell. Those variables are process-wide, so one shell is either "Anthropic"
or "OpenRouter", never both — and an exported `ANTHROPIC_AUTH_TOKEN` holding your
OpenRouter key is one stray subprocess away from leaking.
This MCP server spawns each delegated session as a child process with an
**explicitly constructed environment**. Your Opus session keeps its own
credentials; the delegate gets OpenRouter's. They run side by side in the same
terminal.
```
┌────────────────────────────┐
│ Claude Code (Opus) │ your session, Anthropic credentials
│ │
│ └─ mcp: openrouter ──────┼──▶ spawn: claude -p (fresh env)
└────────────────────────────┘ ANTHROPIC_BASE_URL=https://openrouter.ai/api
ANTHROPIC_AUTH_TOKEN=sk-or-v1-…
→ deepseek/deepseek-v4-flash-0731
```
This is a sibling of [claude-ollama-delegate-mcp](https://github.com/histonedev/claude-ollama-delegate-mcp),
which does the same thing for a local Ollama server. The difference that shapes
everything here: **OpenRouter costs real money and offers 400+ models**, so this
package adds an API-key path, a model catalog with prices, and per-job cost
reporting.
---
## Quick start
```bash
# 1. an OpenRouter API key: https://openrouter.ai/keys
export OPENROUTER_API_KEY=sk-or-v1-...
# 2. register the server
claude mcp add openrouter --scope user -- npx -y claude-openrouter-delegate-mcp
# 3. restart your Claude Code session
```
Then ask for delegation in plain language:
> delegate this to openrouter: summarise every exported symbol in src/
By default the server only delegates **when you explicitly ask**. To let the
orchestrator decide for itself, see [Delegation modes](#delegation-modes).
---
## Contents
- [Quick start](#quick-start)
- [How it works](#how-it-works)
- [Prerequisites](#prerequisites)
- [Installation](#installation)
- [The API key](#the-api-key)
- [Configuration](#configuration)
- [Delegation modes](#delegation-modes)
- [Choosing a model](#choosing-a-model)
- [Costs](#costs)
- [Tool reference](#tool-reference)
- [Operating it](#operating-it)
- [Job artifacts](#job-artifacts)
- [Troubleshooting](#troubleshooting)
- [Platform support](#platform-support)
- [Security model](#security-model)
- [Development](#development)
---
## How it works
OpenRouter exposes an Anthropic-compatible `POST /v1/messages` endpoint
alongside its OpenAI-shaped one, so Claude Code talks to it unmodified when
pointed at the right base URL. Each delegated task runs as `claude -p` in its own
process with:
```
ANTHROPIC_BASE_URL=https://openrouter.ai/api
ANTHROPIC_AUTH_TOKEN=<your OpenRouter key>
ANTHROPIC_DEFAULT_OPUS_MODEL=<model>
ANTHROPIC_DEFAULT_SONNET_MODEL=<model>
ANTHROPIC_DEFAULT_HAIKU_MODEL=<small model>
ANTHROPIC_SMALL_FAST_MODEL=<small model>
CLAUDE_CODE_SUBAGENT_MODEL=<model>
ANTHROPIC_CUSTOM_HEADERS=HTTP-Referer: …⏎X-Title: …
CLAUDE_CODE_MAX_OUTPUT_TOKENS=<clamped per model>
```
The base URL stops at `/api` because Claude Code appends `/v1/messages` itself.
`ANTHROPIC_AUTH_TOKEN` is used rather than `ANTHROPIC_API_KEY` because it is sent
as `Authorization: Bearer <key>`, which is the scheme OpenRouter expects.
The child environment is built from a small per-platform allowlist. Anything
matching `ANTHROPIC_*`, `CLAUDE_*`, `AWS_*`, `GOOGLE_*`, `AZURE_*`, `OPENAI_*`,
`BEDROCK_*`, `VERTEX_*` or `OPENROUTER_*` is dropped **before** the OpenRouter
values are applied. That last prefix is deliberate: the delegate needs the key
only as `ANTHROPIC_AUTH_TOKEN`, so it never receives a second copy under a name
that other tooling it runs might pick up.
Delegates also start with `--strict-mcp-config` and no MCP config, which keeps
their startup fast and stops them from recursively calling this server.
### Two details that are not optional
**Output-token clamping.** Claude Code asks for `max_tokens: 32000` on every
request. Plenty of OpenRouter models cap lower — `amazon/nova-micro-v1` allows
5120 — and reject the request outright. The server reads
`top_provider.max_completion_tokens` from the catalog and pins
`CLAUDE_CODE_MAX_OUTPUT_TOKENS` to what the chosen model actually accepts.
**Tool-calling is mandatory.** Claude Code sends a ~110 KB body with a full tool
schema on every request. A model that cannot call tools fails on turn one, after
you have paid for it. The server refuses such models up front using the
catalog's `supported_parameters`.
---
## Prerequisites
| Requirement | Notes |
| --- | --- |
| **Node.js 20+** | `node --version`. Built and tested on 22. |
| **An OpenRouter account** | [openrouter.ai/keys](https://openrouter.ai/keys). Credits must be topped up. |
| **Claude Code CLI** | [claude.com/code](https://claude.com/code). `claude --version`. |
```bash
node --version
claude --version
curl -s -H "Authorization: Bearer $OPENROUTER_API_KEY" \
https://openrouter.ai/api/v1/credits # {"data":{"total_credits":…}}
```
---
## Installation
### From npm (recommended)
```bash
claude mcp add openrouter --scope user \
--env OPENROUTER_API_KEY=sk-or-v1-... \
-- npx -y claude-openrouter-delegate-mcp
```
Or install it globally, which also puts the settings CLI on your `PATH`:
```bash
npm install -g claude-openrouter-delegate-mcp
openrouter-mcp-config --api-key sk-or-v1-...
claude mcp add openrouter --scope user -- claude-openrouter-delegate-mcp
```
### From source
```bash
git clone https://github.com/histonedev/claude-openrouter-delegate-mcp.git
cd claude-openrouter-delegate-mcp
npm install # builds automatically via the prepare script
claude mcp add openrouter --scope user -- node "$(pwd)/dist/index.js"
```
Run the settings CLI as `node dist/cli.js …`, or `npm link` to get
`openrouter-mcp-config` on your `PATH`.
### Confirm
```bash
claude mcp list # openrouter: ... - ✔ Connected
```
Then **restart your Claude Code session** — the tool list is read at startup.
---
## The API key
Resolved from the first of these that is set:
1. `OPENROUTER_API_KEY` (highest — the name the rest of the ecosystem uses)
2. `OPENROUTER_MCP_API_KEY`
3. `apiKeyFile` in a config file — a path to a file containing the key
4. `apiKey` in a config file
```bash
openrouter-mcp-config --api-key sk-or-v1-... # writes ~/.openrouter-mcp/config.json, mode 0600
openrouter-mcp-config --api-key-file ~/.secrets/or # or keep it somewhere else entirely
```
The key is never exposed through an MCP tool. `openrouter_models` reports it as
`sk-or-v1-a...7f2e`, the CLI prints the same masked form, and the config file is
written `0600`. A key passed with `--api-key --scope project` is redirected to
the user config, because project configs get committed.
If you would rather have no key on disk at all, pin it on the MCP registration
with `--env OPENROUTER_API_KEY=…` and skip the config file.
---
## Configuration
Settings resolve from four layers, later winning over earlier:
1. built-in defaults
2. user config — `~/.openrouter-mcp/config.json` (override with `$OPENROUTER_MCP_CONFIG`)
3. project config — `./openrouter-mcp.config.json` in the server's working directory
4. environment variables
```json
{
"delegationMode": "ondemand",
"defaultModel": "deepseek/deepseek-v4-flash-0731",
"smallModel": "google/gemini-3.7-flash",
"allowedModels": ["deepseek/deepseek-v4-flash-0731", "qwen/qwen3.7-flash"],
"defaultPermissionMode": "auto",
"maxOutputTokens": 16000,
"requireToolSupport": true
}
```
| Setting | Env var | Default | Meaning |
| --- | --- | --- | --- |
| `delegationMode` | `OPENROUTER_MCP_DELEGATION_MODE` | `ondemand` | How eagerly delegation is used |
| `apiKey` | `OPENROUTER_API_KEY` | — | Your OpenRouter key |
| `defaultModel` | `OPENROUTER_MCP_DEFAULT_MODEL` | `deepseek/deepseek-v4-flash-0731` | Model when a call omits one |
| `smallModel` | `OPENROUTER_MCP_SMALL_MODEL` | same as `defaultModel` | Model for the delegate's background/utility slot |
| `allowedModels` | `OPENROUTER_MCP_ALLOWED_MODELS` (comma-separated) | `[]` (all) | Models delegation may use |
| `defaultPermissionMode` | `OPENROUTER_MCP_PERMISSION_MODE` | `auto` | Permission mode for delegates |
| `maxOutputTokens` | `OPENROUTER_MCP_MAX_OUTPUT_TOKENS` | `16000` | Ceiling, clamped again per model |
| `requireToolSupport` | `OPENROUTER_MCP_REQUIRE_TOOL_SUPPORT` | `true` | Reject models that cannot call tools |
| `baseUrl` | `OPENROUTER_MCP_BASE_URL` | `https://openrouter.ai/api` | Endpoint (point at a proxy if you have one) |
| `referer` / `title` | `OPENROUTER_MCP_REFERER` / `_TITLE` | this repo | `HTTP-Referer` / `X-Title` attribution |
| `claudeBin` | `OPENROUTER_MCP_CLAUDE_BIN` | `claude` | Path to the Claude Code CLI |
| `stateDir` | `OPENROUTER_MCP_STATE_DIR` | `~/.openrouter-mcp/jobs` | Prompts, transcripts, results |
| `jobTimeoutMs` | `OPENROUTER_MCP_JOB_TIMEOUT_MS` | `1800000` | Hard kill for one turn |
| `maxInlineChars` | `OPENROUTER_MCP_MAX_INLINE_CHARS` | `60000` | Output above this is truncated; full text on disk |
### Changing settings
Settings are changed from a terminal, never by the model:
```bash
openrouter-mcp-config # show current settings + active layers
openrouter-mcp-config --mode auto # off | ondemand | auto
openrouter-mcp-config --default-model qwen/qwen3.7-flash
openrouter-mcp-config --small-model google/gemini-3.7-flash
openrouter-mcp-config --allow deepseek/deepseek-v4-flash-0731,qwen/qwen3.7-flash
openrouter-mcp-config --permission-mode acceptEdits
openrouter-mcp-config --scope project # write ./openrouter-mcp.config.json
```
Then **restart your Claude Code session** so the server re-reads its config.
**There is deliberately no MCP tool for this.** See [Security model](#security-model).
---
## Delegation modes
This controls **how eagerly the orchestrator reaches for delegation**, by
rewriting the tool descriptions the model actually reads. Changing it requires a
session restart, by design.
| Mode | Effect |
| --- | --- |
| `off` | The `delegate_*` tools are hidden entirely. `openrouter_models` remains so the model can still report the setup. |
| `ondemand` *(default)* | Delegate **only when you explicitly ask** — "delegate this", "use openrouter", "ask deepseek". |
| `auto` | The orchestrator **decides for itself**, using criteria baked into the description. |
Both modes carry an explicit cost warning, because unlike a local model every
delegated turn is billed and every request carries tens of thousands of input
tokens.
---
## Choosing a model
OpenRouter serves 400+ models. `openrouter_models` filters to tool-capable ones
and truncates by default, since dumping the whole catalog into the orchestrator's
context is expensive in itself.
```
openrouter_models({ filter: "gemini", limit: 10 })
openrouter_models({ free_only: true })
```
The default is `deepseek/deepseek-v4-flash-0731`: tool-capable, 1.3M context, and
cheap. Set `allowedModels` to pin delegation to a shortlist you trust — the
allowed set is embedded in the `delegate_start` description, so the orchestrator
knows the menu without an extra call, and any other model is rejected by name
rather than silently substituted.
### Model quality varies, and that is your problem to manage
These are not Claude models, and Claude Code leans on the model harder than a
chat UI does. Observed while building this package, on real runs:
- A model may call its tools correctly and then **return no final text**. The
work happened; the closing message did not. The server detects this and hands
back the recorded activity trail instead of an empty result.
- A model may emit **its own native tool-call markup as plain text** — DeepSeek
producing `<|DSML|tool_calls>` inside a reasoning block — instead of a
structured tool call. Nothing executes and the tokens are still billed. Longer
and more convoluted prompts make this more likely.
- The `tool calls: 0` annotation on a finished job is there for the classic
failure: a confident answer about your repository that no tool call backs.
Prefer short, single-purpose prompts for weaker models, cap work with
`max_turns`, and check `delegate_status` when a claim matters.
---
## Costs
Every finished job reports a cost, and the label tells you how much to trust it:
```
cost: $0.00045 (billed by OpenRouter, 1 generation(s))
cost: >=$0.00045 (billed so far -- 1 of 2 generations recorded)
cost: $0.00924 (rough list-price estimate; actual routing may differ several-fold)
```
OpenRouter returns its generation id as the Anthropic message id, so the server
collects them and asks `/v1/generation` what each request actually cost. That is
the authoritative number. Billing records land a moment after the generation
finishes, so a job may briefly report a partial sum — shown as `>=`, never as a
final figure, because the one direction a cost display must not be wrong in is
downward.
The estimate is the fallback when a record cannot be fetched. Treat it as an
order of magnitude only: in testing, identical token counts on the same model
were billed **ten times apart** depending on provider routing and cache hits, and
list-price arithmetic came out 20× high on one run.
Two things worth knowing about token counts:
- Claude Code reports almost the entire prompt as `cache_creation_input_tokens`,
not `input_tokens`. The `tokens: in …` line sums all input classes. A job that
looks like "12 input tokens" is really ~42,000.
- OpenRouter routes to providers that often do not honour the cache
(`native_tokens_cached: 0`), so a resumed conversation is not as cheap as the
cache-read figures suggest.
**Cutting the bill.** `smallModel` is the biggest lever: Claude Code's haiku slot
serves background utility calls, and pointing it at something cheap while the
main model stays capable costs almost nothing in quality. After that: `max_turns`
caps an agentic loop, and `allowed_tools` stops a delegate exploring more of the
repo than the task needs.
---
## Tool reference
| Tool | Purpose |
| --- | --- |
| `openrouter_models` | List models with prices and context windows, report settings and credit balance (read-only) |
| `delegate_start` | Start a task; returns a `job_id` immediately |
| `delegate_followup` | Send another message to the same session |
| `delegate_status` | Poll state plus a tail of the delegate's tool calls |
| `delegate_result` | Collect final output |
| `delegate_cancel` | Terminate a running delegate and everything it started |
| `delegate_list` | List jobs, grouped by conversation, with total spend |
### `delegate_start`
| Parameter | Type | Notes |
| --- | --- | --- |
| `prompt` | string | The task. Mutually exclusive with `prompt_file`. |
| `prompt_file` | string | Path to a file holding the prompt. Preferred when long. |
| `model` | string | OpenRouter id, e.g. `google/gemini-3.7-flash`. Must be in the allowed list. |
| `small_model` | string | Override the background/utility model for this call. |
| `cwd` | string | Working directory for the delegate. |
| `permission_mode` | enum | `auto`, `acceptEdits`, `bypassPermissions`, `manual`, `dontAsk`, `plan` |
| `allowed_tools` | string[] | e.g. `["Read","Grep","Bash(git *)"]` |
| `disallowed_tools` | string[] | e.g. `["Write","Edit"]` |
| `append_system_prompt` | string | Extra instructions for the delegate |
| `max_turns` | number | Cap the delegate's agentic turns — also a cost cap |
| `add_dirs` | string[] | Additional accessible directories |
| `wait_seconds` | number | Block up to N seconds (0–600). Default 0 = return immediately. |
`delegate_followup` takes `job_id` **or** `session_id`, plus the same
prompt/`prompt_file` pair and optional `permission_mode`, `max_turns`,
`wait_seconds`.
---
## Operating it
### Asynchronous by default
`delegate_start` returns a `job_id` in milliseconds; the delegate keeps running
in the background. This keeps a long task from stalling your session or tripping
an MCP client timeout — most clients give up on a single request after 60
seconds, which is less than many delegated tasks take.
```
delegate_start({ prompt: "Audit src/ for unused exports" })
→ job_id A, session_id S, turn 1, state: running
delegate_status({ job_id: "A" })
→ recent activity:
[tool] Grep: export
[tool] Read: /repo/src/index.ts
delegate_result({ job_id: "A" })
→ the final text
```
Pass `wait_seconds` on any of those to block instead — useful for short tasks,
but keep it under your client's request timeout.
### Two-way conversations
Every job carries a `session_id`. Passing its `job_id` to `delegate_followup`
resumes the session with full history; the `session_id` stays stable across turns
while each turn gets a fresh `job_id`.
```
delegate_start({ prompt: "Summarise the auth flow in this repo" })
→ job A, session S, turn 1
delegate_followup({ job_id: "A", prompt: "Now list every place it can fail" })
→ job B, session S, turn 2 (delegate still remembers turn 1)
```
Resuming replays the conversation as input tokens, so a long thread costs more
per turn than a fresh one — but still far less than rebuilding the same context.
### Long prompts
Every prompt parameter has a `prompt_file` counterpart. Internally the prompt is
always written to disk and fed to the CLI over **stdin** — never as an argv entry
and never through a shell. Backticks, `$(...)`, quotes, newlines and glob
characters pass through verbatim, and there is no argv length limit.
### Permissions
```
// read-only review
delegate_start({ prompt: "...", disallowed_tools: ["Write", "Edit", "NotebookEdit"] })
// tightly scoped
delegate_start({ prompt: "...", allowed_tools: ["Read", "Grep", "Glob"] })
```
### Cancelling
```
delegate_cancel({ job_id: "A" })
```
Kills the delegate **and everything it started**, and stops it spending any more
credits. The server also kills running delegates when it shuts down.
---
## Job artifacts
Each job writes to `~/.openrouter-mcp/jobs/<job_id>/`:
| File | Contents |
| --- | --- |
| `prompt.txt` | Exactly what was sent |
| `stream.jsonl` | Full `stream-json` transcript, including every tool call and tool result |
| `result.json` | Metadata: state, model, tokens, cost, generation ids, timings, exit code |
| `result.txt` | Final output text |
`stream.jsonl` is where to look when a delegate's summary is unconvincing: it
holds the actual tool results, not the model's account of them. Nothing is pruned
automatically — delete the directory whenever you like.
---
## Troubleshooting
**`No OpenRouter API key configured`**
Set `OPENROUTER_API_KEY`, or `openrouter-mcp-config --api-key sk-or-v1-...`, then
restart the session.
**`OpenRouter rejected the API key (HTTP 401)`**
Check it at [openrouter.ai/keys](https://openrouter.ai/keys). Note that the key
is read at server start, so a fresh key needs a session restart.
**`Model "x" does not support tool calling`**
Working as intended — Claude Code sends a tool schema on every request. Pick
another model, or set `requireToolSupport: false` to try anyway.
**`Model "x" is not in the allowed list`**
Working as intended. `openrouter-mcp-config --allow <models>`, then restart.
**The delegate returns no text, or its tool calls do nothing**
A model-quality problem, not a wiring one — see
[Model quality varies](#model-quality-varies-and-that-is-your-problem-to-manage).
Try a shorter prompt or a stronger model, and read `stream.jsonl` to see what
actually happened.
**`Request timed out` from your MCP client**
`wait_seconds` exceeded the client's per-request timeout (often 60s). Drop it and
poll with `delegate_status` instead — that is what the async design is for.
**HTTP 400 about `max_tokens`**
The model's output cap is below what was requested. The server clamps to the
catalog's value automatically; if the catalog is wrong, lower `maxOutputTokens`.
**Tools do not appear in Claude Code**
The tool list is read at session start. Restart, or check `claude mcp list`.
**Delegate fails instantly with a launch error**
The CLI was not found. Set `OPENROUTER_MCP_CLAUDE_BIN` to the absolute path of
`claude`.
---
## Platform support
| Platform | Status |
| --- | --- |
| Windows | Tested end to end |
| macOS | Supported; same POSIX code path as Linux |
| Linux | Supported |
Platform differences are isolated in `src/platform.ts`:
**Binary resolution.** On POSIX, `spawn` searches `PATH`. On Windows a native
install gives `claude.exe` while an npm install gives `claude.cmd`, which
`CreateProcess` cannot execute directly — so the server walks `PATH` × `PATHEXT`
preferring `.exe`, and falls back to routing a `.cmd` shim through `cmd.exe`.
**Argument escaping.** That fallback applies two layers: MSVCRT argv quoting, then
a caret escape of cmd's own metacharacters (`& | < > ^ " ( ) % !`). Skipping the
second layer is the classic `.cmd` command-injection hole. Prompts never touch
this path — they travel over stdin. One limitation: a *multi-line*
`append_system_prompt` cannot cross a `cmd.exe` command line, so the server raises
a clear error pointing at `OPENROUTER_MCP_CLAUDE_BIN` instead of silently
mangling it.
**Environment allowlist.** Windows preserves a much larger set than POSIX.
`SystemRoot` and `windir` are not optional — strip them and Winsock fails to
initialise, so the child cannot open a socket at all.
**Cancellation.** POSIX children are spawned `detached` as process-group leaders
and cancelled with `process.kill(-pid)`; Windows uses `taskkill /T /F`.
---
## Security model
**Credential isolation is the point.** The child environment is constructed from
scratch rather than inherited, and provider variables are stripped before the
OpenRouter values are applied. `test/env-unit.mjs` asserts that the key reaches
the child in exactly one variable and that no parent secret survives;
`test/e2e.mjs` poisons the parent with a fake `ANTHROPIC_API_KEY` and a real
`ANTHROPIC_BASE_URL=https://api.anthropic.com` and confirms the delegate sees
neither.
**The key is never readable through a tool.** Every diagnostic path masks it, and
`test/readonly.mjs` asserts that no tool output and no CLI output contains the
key. This matters more here than in the Ollama sibling, where the token is the
literal string `ollama`.
**Delegation policy is not model-writable.** There is no MCP tool to change
`delegationMode`, `allowedModels`, or the API key. A model that finds `ondemand`
inconvenient cannot flip itself to `auto` and start spending. Settings load once
at startup, are never mutated at runtime, and the tool descriptions state that
the policy is not the model's to change.
**This is a guardrail, not a security boundary.** An agent with shell access can
still edit the config file. What removing the tool buys you is that such a change
is a *visible file edit that only takes effect on the next restart*, rather than a
single silent tool call mid-task. To make it airtight, pin the values via `--env`
on the MCP registration, which overrides the config files:
```bash
claude mcp add openrouter --scope user \
--env OPENROUTER_API_KEY=sk-or-v1-... \
--env OPENROUTER_MCP_DELEGATION_MODE=ondemand \
--env OPENROUTER_MCP_ALLOWED_MODELS=deepseek/deepseek-v4-flash-0731 \
-- node /path/to/claude-openrouter-delegate-mcp/dist/index.js
```
**Spending is real.** A runaway `auto`-mode orchestrator spends your credits, not
your patience. Keep an OpenRouter key limit on the key you give this server —
that is a ceiling the model cannot argue with.
**Delegates inherit your filesystem.** They run as your user in the `cwd` you
give them, with `defaultPermissionMode`. Treat a delegated session as you would
any Claude Code session — use `disallowed_tools` or a read-only permission mode
when handing work to a model you trust less.
---
## Development
```bash
npm install # installs and builds
npm run build # tsc
npm run dev # tsc --watch
```
### Tests
```bash
node test/env-unit.mjs # env isolation, key handling, model slot wiring
node test/pricing-unit.mjs # cost arithmetic, catalog filtering, URL normalisation
node test/quoting.mjs # Windows argv/cmd escaping, incl. an injection probe
node test/killtree-unit.mjs # process-tree termination (cross-platform)
OPENROUTER_API_KEY=... node test/e2e.mjs # full MCP round trip (costs ~$0.01)
OPENROUTER_API_KEY=... node test/async.mjs # async polling, prompt_file, cancel
CFG_PATH=/tmp/c.json CFG_CWD=/tmp node test/readonly.mjs # config is read-only to the model
```
`npm test` runs the four that need no network. They pass on Windows and POSIX
alike — the process-tree test builds its tree out of node processes rather than
shell built-ins, so it is not POSIX-only.
### Publishing a release
```bash
npm login # interactive, once per machine
npm version patch # or minor / major -- tags and bumps
npm publish # prepare script builds first
git push --follow-tags
```
Run `npm publish` from a **real terminal**, not a script or a non-interactive
shell. With WebAuthn/security-key 2FA the CLI completes the challenge by opening
a browser; without a TTY it cannot, and falls back to demanding a TOTP code that
a security key cannot produce (`npm error code EOTP`). For CI, use a granular
access token with Bypass 2FA instead.
The package ships only `dist/`, `README.md` and `LICENSE`. `publishConfig.access`
is `public`, and `prepare` runs `tsc` before packing, so a stale `dist/` can never
be published.
### Layout
| File | Responsibility |
| --- | --- |
| `src/index.ts` | MCP server, tool registration and handlers |
| `src/settings.ts` | Layered config loading, API-key resolution, masking |
| `src/config.ts` | Startup-resolved settings singleton |
| `src/descriptions.ts` | Mode-dependent tool descriptions, cost warnings |
| `src/env.ts` | Child-environment construction and the provider-variable blocklist |
| `src/platform.ts` | Windows/POSIX spawn, argument escaping, process-tree kill |
| `src/jobs.ts` | Job lifecycle, `stream-json` parsing, cost accounting, cancellation |
| `src/models.ts` | Catalog, pricing, tool-support enforcement, billing lookups |
| `src/cli.ts` | `openrouter-mcp-config` settings CLI |
---
## License
MIT — see [LICENSE](LICENSE).
TDQS
Scored across 7 tools
Each tool maps to a distinct stage or aspect of the delegation lifecycle: models/config discovery, start, follow-up, status, final result, cancel, and list. The only superficially similar pair (delegate_status vs delegate_result) is clearly separated by monitoring vs collecting final output.
Six of seven tools share the clean delegate_ prefix with a readable action, making the lifecycle easy to scan. Minor deviation: delegate_status, delegate_result, and delegate_followup use nouns rather than verbs, and openrouter_models breaks the prefix pattern.
Seven tools is well-scoped for a delegation server: one discovery/config tool plus six lifecycle operations. Each tool covers a necessary step without redundancy or bloat.
The delegation workflow is fully covered: inspect models/config, start, monitor, collect, resume, cancel, and list jobs. Deliberately missing config-editing tools are documented as user-controlled via a separate CLI, so there are no dead ends for the stated purpose.