mcp-orchestrate
v0.2.3
Published
MCP server that routes a coding task to a user-configured model tier (light/standard/heavy) with capability filtering and provably-safe cross-backend fallback. Backend-agnostic; you configure every model.
Downloads
836
Maintainers
Readme
mcp-orchestrate
Send every coding task to the right-priced model automatically — cheap models for
the grunt work, your heavy hitter for the hard problems — and never lose work to a
failed backend. mcp-orchestrate is one MCP server that maps three tiers
(light / standard / heavy) onto the coding CLIs you already run — opencode,
grok, codex, and claude — with capability filtering, cooldowns, and a fallback that
only retries on a fast, clean failure with a byte-identical git tree.
Works with any MCP client — Claude Code, Codex, Cursor, opencode, or your own. The calling assistant routes by intent (it picks a tier with full task context); the router owns reliability (the tier→model mapping, capability filtering, cooldowns, and a safety-gated fallback). It never invents a model — nothing about any author's setup is baked in; every mapping is yours, supplied in config.
mcp-orchestrate --init # guided setupIt spawns CLIs, not MCPs
your MCP client ──calls──▶ mcp-orchestrate ──spawns──▶ opencode / grok / codex / claude (CLI binaries)
mcp-opencode, mcp-grok ── siblings: separate MCP servers over the same CLIs
(the router never calls them)The router is only an MCP server — it has no MCP client inside it, so it never
calls mcp-opencode, mcp-grok, or the codex/claude MCPs. Whether those are
installed has no effect on routing. Prefer plain single-backend MCPs with no routing at all?
Install them directly (npx -y mcp-opencode, npx -y mcp-grok) and let your
assistant call whichever it needs — they're siblings over the same CLIs, not layers
beneath the router.
Is it for you?
Use it if you run more than one coding-agent CLI and want cost/capability control without hand-picking a model every time, plus safe automatic failover. If you only use a single backend or a single model, you don't need the router — install that backend's own MCP instead.
Requirements
The router spawns the backend CLIs you enable, so each must be installed and
authenticated: opencode, grok, codex, and/or claude (whichever your
tiers use), plus Node.js >= 20. The claude backend authenticates via the Claude
Code CLI's own login — run claude login once to use your Claude subscription
(no API key needed).
Install
Register the stdio server with your MCP client — generic form first, then examples:
npx -y mcp-orchestrate # any MCP client spawns this
claude mcp add orchestrate -- npx -y mcp-orchestrate # Claude Code
codex mcp add orchestrate -- npx -y mcp-orchestrate # CodexThe published package is
mcp-orchestrateand its CLI command ismcp-orchestrate. The config directory (~/.config/mcp-router/) and theMCP_ROUTER_*environment variables keep themcp-routerprefix for backward compatibility.
Backends vs. models
A backend is the CLI program that runs the task (opencode / grok / codex /
claude); a model is the AI it runs (e.g. grok-4.5, glm-5p2, gpt-5.6-terra,
opus). One backend can front many models — which ones you can route to depends on
how you've configured that CLI. The router never adds models; it routes to
whatever your installed CLIs already expose. Per backend:
- opencode — spawned; any provider/model your OpenCode install exposes.
- grok — spawned; the Grok Build CLI (always provider
xai). - codex — spawned by default, or set
"advisory": trueon the entry to have the router return a "use the Codex MCP directly" hint instead of spawning it (an advisory entry needs no model and never sends your prompt anywhere). Per-entrysandbox:"read-only"|"workspace-write"(default) |"danger-full-access"."danger-full-access"removes the OS sandbox entirely (risk-flagged by--init);"read-only"can't write files. - claude — spawned; runs Claude Code headless (
claude -p) on your Claude subscription (no API key needed); provideranthropic; model isopus/sonnet/haikuor a full concrete id. Or set"advisory": trueto have the router return a "spawn a subagent yourself (model …)" hint instead of spawning — the right choice when the caller is Claude Code itself, which can run a native subagent (no redundantclaude -p, no prompt/repo sent anywhere).
Claude backend: safety & scope
- No OS sandbox — unlike codex's
--sandbox workspace-write,clauderuns at the same unsandboxed posture asgrok/opencodetoday (containment: "none"). OS-level sandboxing (sandbox-exec/bwrap) is deferred future work. - Default
permissionMode: "acceptEdits"lets it edit files but Bash is denied in headless mode — set"permissionMode": "bypassPermissions"per entry for full autonomy (--initflags this as a risk: it means no sandbox + full host access, so an untrusted prompt can act as you). allowedTools(claude-only) is a pass-through to claude's--allowedTools, pre-approving scoped tools without prompting and without a full bypass, e.g."allowedTools": ["WebFetch", "Read(./docs/**)"].Bash(...)allow-rules are NOT a sandbox — they're escapable via command chaining (an untrusted prompt can append arbitrary commands), so a Bash rule grants effectively full shell to an untrusted prompt, the same risk asbypassPermissions;--initflags it the same way. Non-shell rules are genuinely scoped.- The router isolates the spawned
claudefrom your own MCP servers, hooks, andCLAUDE.mdon every spawn (--strict-mcp-configwith an empty--mcp-config, plus--setting-sources ''). - This is subscription use of
claude -pvia Anthropic's official CLI — confirm it fits your plan's terms before enabling it.
Honest scope (v1 fallback). Fallback fires only for fast, clean failures — a backend that isn't installed, is rate-limited/overloaded immediately, or fails auth at launch, with a byte-identical git tree. A run that edited files, timed out, or exhausted the budget is terminal and returned as-is. This is graceful fallback on fast/clean failure, not a mid-task rescue.
Configuration
~/.config/mcp-router/config.json (or $XDG_CONFIG_HOME/mcp-router/config.json,
or $MCP_ROUTER_CONFIG). The file must be mode 0600 and owned by you, or
the server refuses to read it.
{
"tiers": {
"light": null,
"standard": { "backend": "opencode", "model": "<provider>/<model>" },
"heavy": { "backend": "claude", "model": "opus", "permissionMode": "acceptEdits" }
},
"capabilities": {
"vision": { "backend": "opencode", "model": "<provider>/<vision-model>", "capabilities": ["vision"] }
},
"fallbacks": { "light": "standard", "heavy": "standard" },
"allowCrossProviderFallback": false
}
// All ids are placeholders. Supply your own backend/provider/model per slot.- Tier names are fixed (
light/standard/heavy); you only choose which model fills each slot. A tier name denotes your intent for the slot, not a model's size. - Provider is derived from the opencode model prefix (
<provider>/...) and a declaredproviderthat disagrees is rejected. grok is alwaysxai, codex is alwaysopenai, claude is alwaysanthropic. permissionModeis claude-only ("acceptEdits"default or"bypassPermissions"); declaring it on any other backend is rejected.allowedToolsis claude-only (a string array, e.g.["WebFetch", "Read(./docs/**)"]); declaring it on any other backend is rejected.sandboxis codex-only ("read-only"|"workspace-write"default |"danger-full-access"); declaring it on any other backend is rejected.allowCrossProviderFallback(defaultfalse): a fallback that would ship your prompt + repo content to a different provider requires this opt-in. Aggregator prefixes (openrouter,github-copilot) are always treated as cross-provider.- No config file? The server still runs (zero-config), but every tier is unconfigured until you write one — it never fabricates a model.
Environment
MCP_ROUTER_ROOTS— colon-separated allowlist of directories acwdmust be inside (default$HOME/Projects). This is the security boundary.MCP_ROUTER_CONFIG— override the config path.
Running orchestrate in multiple clients
Registering orchestrate in more than one MCP client (say Claude Code and
Codex) is fully supported. Each client spawns its own server process, and the
server has no idea which client launched it — so routing is driven entirely by the
config file each process reads.
Shared config (the default). With plain
claude mcp add orchestrate -- npx -y mcp-orchestrate and the equivalent
codex mcp add, neither sets MCP_ROUTER_CONFIG, so both read the same
~/.config/mcp-router/config.json. A route(tier: "heavy", …) call does the same
thing from either client. This is the normal setup and needs nothing extra.
One caveat — self-routing. If a tier maps to the claude backend and you call
it from Claude Code, the route is Claude → orchestrate → a fresh claude -p
sub-run. It still works (an isolated, autonomous sub-run with clean context), but
it's redundant if your intent was "reach a different model." The same applies to
routing a codex tier from Codex.
Per-client configs (for cross-model setups). To make each client route to a
different model, point each at its own config with MCP_ROUTER_CONFIG:
# Claude Code → its own config (e.g. heavy → codex/grok, a non-Claude model)
claude mcp add orchestrate \
-e MCP_ROUTER_CONFIG=$HOME/.config/mcp-router/claude.json \
-- npx -y mcp-orchestrate
# Codex → its own config (e.g. heavy → claude · opus on your subscription)
codex mcp add orchestrate \
--env MCP_ROUTER_CONFIG=$HOME/.config/mcp-router/codex.json \
-- npx -y mcp-orchestrateEach config file must still be mode 0600 and owned by you. MCP_ROUTER_ROOTS can
differ per client the same way. Validate each with
MCP_ROUTER_CONFIG=<path> mcp-orchestrate --check. Changes take effect when the
client next restarts (the running server process holds its config in memory).
Validate your config: mcp-orchestrate --check
Run the config doctor in your terminal (this is a CLI command, separate from the MCP stdio server) to confirm the server will accept your config before wiring it up:
mcp-orchestrate --check # checks the default config path
mcp-orchestrate --check <path> # or an explicit pathIt loads the config through the exact same hardened path the server uses
(permissions 0600, ownership, parent-directory checks, schema, and provider
derivation), prints the configured tiers/capabilities and which backend CLIs are
installed (warning on any configured-but-missing backend), and exits 0 when the
config is valid, 1 when it is invalid or absent.
Interactive setup: mcp-orchestrate --init
Rather than hand-writing the config, run the wizard in your terminal:
mcp-orchestrate --init # writes the default config path
mcp-orchestrate --init <path> # or an explicit path (handy for testing)It detects installed backends, discovers each backend's models (best-effort — the
list is untrusted, you confirm every pick), walks each tier, shows the diff +
risks, asks to confirm, then writes a 0600 config and self-runs --check. It
requires a TTY and replaces the config (it starts blank, not a merge). The
parent directory must not be group/world-writable (so /tmp is refused by design).
Conversational setup from an MCP client (opt-in)
You can also drive setup from inside an MCP client (Claude Code, codex, opencode).
Because config is the router's trust boundary, these tools are registered only
when MCP_ROUTER_ALLOW_INIT=1 is set in the server's environment — never on by
default:
init_status()— installed backends, discovered models (untrusted), and which tiers/capabilities are configured (presence only). Writes nothing.init_preview(spec)— validates a proposed config through the exact server path and returns the diff + server-computed risks. Writes nothing.init_apply(spec)— stages the config as<config>.pending. It can never write the live config; promotion requires a human running the accept step.
mcp-orchestrate --accept-pending [path] # review the staged diff + risks, confirm, commit--accept-pending requires a TTY and has no non-interactive bypass: the human
review at a real terminal is the attestation that a prompt-injected assistant
cannot forge. Enable MCP_ROUTER_ALLOW_INIT=1 only during setup.
Tools
route(prompt, cwd, tier, caps?, timeoutSec?)— dispatch to the tier (with safe fallback). Returns served-by, the full attempt trace, any cross-provider notice, then the assistant text (redacted).list_tiers()— minimal discovery: which tiers/capabilities are configured (presence only — never model ids, candidate counts, or the config path), which are cooling, and which CLIs are installed. Sends no prompt, probes no provider.router_dry_run(cwd, tier, caps?, timeoutSec?)— the explicit recipient preview: the exact ordered recipients (backend/model), authorization decisions, cross-provider crossings, and budget split, without sending the prompt.
Safety limitations
- The fallback fingerprint is cwd-scoped: it proves the git working tree in
cwdis unchanged, not that the machine is unchanged (an agent's shell can write to$HOME, sibling repos, or git config). "cwd tree unchanged" ≠ "nothing happened". - A non-git
cwdcannot be fingerprinted, so any post-spawn failure there is terminal (never falls back). - Cooldowns and traces surface a coarse reason (
unavailable), neverauthvscapacity, to avoid leaking per-provider credential health.
License
MIT.
