mcp-router
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@mcp-routerCan you list all the MCP servers and their tools?"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
mcp-router
One HTTP MCP endpoint that every Claude Code session shares, which starts a server only when a tool on it is actually called.
Why this exists
Ten Claude Code sessions were running on my Mac with twelve MCP servers configured. That produced ~190 MCP server processes and ~12 GB of combined RSS, and most of those servers were never called once.
That's not a misconfiguration; it's the protocol. stdio MCP is a 1:1 pipe. One client, one server process, no multiplexing anywhere in the spec. So ten sessions with twelve servers means up to 120 processes, and every one of them starts at session init whether the session ever touches it or not.
Nothing in Claude Code's config fixes it. --strict-mcp-config reduces how many servers a session declares, but each declared server still starts eagerly and nothing is shared between sessions. There's no lazy-start option to reach for: the per-server schema accepts command, args, env, type, url, headers and timeout, and none of those defer a spawn.
Pooling exists at exactly one place in the spec, and that's the HTTP transport. So the router speaks HTTP to Claude Code and stdio to the servers.
On this machine now: 135 tools from 11 upstreams, with 0 child processes running at rest.
flowchart LR
subgraph before["Before: 190 processes, ~12 GB"]
direction TB
S1["Session 1"] --> A1["server A"] & B1["server B"] & C1["server C"]
S2["Session 2"] --> A2["server A"] & B2["server B"] & C2["server C"]
S3["Session N"] --> A3["server A"] & B3["server B"] & C3["server C"]
end
subgraph after["After: 0 at rest, 1 per server in use"]
direction TB
T1["Session 1"] --> R(["mcp-router :8879"])
T2["Session 2"] --> R
T3["Session N"] --> R
R -. "spawned on first call" .-> X["server A"]
R -. "idle, not running" .-> Y["server B"]
R -. "idle, not running" .-> Z["server C"]
end
before ~~~ afterRelated MCP server: pokeclaw
Install
/bin/bash -c "$(curl -fsSL https://fledgeling-co.github.io/mcp-router/install.sh)"That fetches the source to ~/.local/share/mcp-router, builds it, copies your stdio servers out of ~/.claude.json, indexes them once, writes two launchd agents with this machine's own absolute paths, loads them, and adds a single router entry to ~/.claude.json. It backs that file up first.
Start a new Claude Code session afterwards; a running session fetches its tool list once at init and won't see the change.
Already have a clone? ./docs/install.sh from inside it works the same way and skips the fetch, so the agents point at your working copy.
Note: the installer is macOS-only because it uses launchd. On Linux, npm run build then run node dist/index.js serve under systemd; everything else in the router is platform-neutral.
Uninstall
/bin/bash -c "$(curl -fsSL https://fledgeling-co.github.io/mcp-router/uninstall.sh)"Add --purge to also delete ~/.claude/mcp-router and the fetched source:
/bin/bash -c "$(curl -fsSL https://fledgeling-co.github.io/mcp-router/uninstall.sh)" mcp-router --purgeThe restore is the half that matters. Every stdio server the router adopted is written back into ~/.claude.json before the agents go, so you're left with a working setup rather than no MCP servers at all. It won't overwrite a name you've since defined by hand.
How it works
The awkward part is tools/list. A client needs the full tool list at startup, and the only way to learn an stdio server's tools is to start it and ask; that's the exact cost being removed. So the router caches the tool manifest to disk.
sequenceDiagram
autonumber
participant C as Claude Code
participant R as mcp-router
participant M as manifest.json
participant U as upstream (stdio)
Note over R,M: mcp-router index runs once, up front
R->>U: spawn, initialize, tools/list
U-->>R: 47 tools
R->>M: cache them, then close the child
Note over C,U: every session after that
C->>R: tools/list
R->>M: read from cache
M-->>C: 135 tools, nothing running
C->>R: tools/call dossier__research_plan
R->>U: spawn dossier, and only dossier
U-->>C: result
Note over R,U: after idleMs with no calls, the child is closedThe cache is keyed on each server's command/args/env identity, so editing one server invalidates only its own entry. Tools are namespaced <server>__<tool> so two servers can't collide.
Spawns are single-flighted, so two concurrent calls to a cold server produce one child rather than two.
Adding a server
Add it the ordinary way. The watcher does the rest.
claude mcp add --scope user my-server -- /path/to/cmd --flagIt will look like it vanished. Within seconds the entry disappears from ~/.claude.json, and that's the watcher working rather than a bug.
flowchart TD
A["you run claude mcp add"] --> B["entry lands in ~/.claude.json"]
B --> C{{"launchd WatchPaths fires"}}
C --> D["spawn it once, read its tools"]
D -->|"indexed OK"| E["write to servers.json"]
E --> F["delete from ~/.claude.json"]
F --> G["restart the router"]
G --> H(["available lazily to every session"])
D -->|"command is wrong"| I["stays in ~/.claude.json<br/>logged as failed<br/>5-minute backoff"]
style H fill:#1a7f37,color:#fff
style I fill:#9a6700,color:#fffConfirm with mcpr tools | grep my-server, and ~/.claude/mcp-router/watch.log records every adoption.
The order is deliberate: it indexes first and adopts only on success. A server whose command is wrong stays in ~/.claude.json, is logged as failed, and never enters servers.json; a typo stays visible where you typed it instead of being swallowed into a config that can't serve it.
Three things the watcher deliberately leaves alone:
Not adopted | Why |
HTTP/SSE entries | They already pool on their own transport and carry their own OAuth; another hop would strip that context |
The | It would proxy to itself |
Project scope ( | Deliberately scoped to one repo; that's the point of them |
~/.claude.json is ~268 KB, holds live session state for every project, and Claude Code rewrites it constantly. So the watcher hashes only the mcpServers object and exits in about 100 ms when it's unchanged, which is nearly every fire. It backs the file up before writing, writes via temp file plus rename, re-reads immediately before writing so concurrent session state survives, and abandons the run without writing anything if the parse fails.
Operating it
mcpr status # what is running right now, and how long it has been idle
mcpr tools # the namespaced tool list, from cache
mcpr index --force # rebuild the whole cache
mcpr serve --verbose # foreground, with child stderr
mcpr watch --verbose # run one watcher pass by handEndpoints: /mcp, /health, /status.
Changing a server is the one case that isn't automatic, since the watcher only reacts to new entries:
mcpr import && mcpr indexA re-index reaches the running router without a restart: serve stats the manifest on each tools/list and re-reads it when the mtime moves, keeping the previous manifest if the new one won't parse. Adding a new upstream is the one change that needs a restart, because the upstream list is read once at startup, and the watcher does that restart itself.
Removing a server means taking it out of ~/.claude/mcp-router/servers.json, since that is where it lives now rather than ~/.claude.json:
node -e 'const f=process.env.HOME+"/.claude/mcp-router/servers.json",d=require(f);delete d.mcpServers["my-server"];require("fs").writeFileSync(f,JSON.stringify(d,null,2))'
mcpr index --force
launchctl kickstart -k "gui/$UID/gg.rhodes.mcp-router"Worth saying that the reason people reach for this has mostly gone. Removing a server used to be how you stopped paying for one you rarely called, because every declared server started at session init whether you touched it or not. Under the router an unused server costs a few kilobytes of cached tool schema and nothing else: no process, no memory, no startup time. So the honest advice is to leave it, unless the tools themselves are cluttering the model's tool list or the server has become a liability.
What it deliberately doesn't do
It doesn't proxy HTTP/SSE upstreams. Those are already shared endpoints with their own auth; routing them through here adds a hop and strips their OAuth context.
It doesn't bind beyond loopback. This endpoint runs every MCP server you own, with your environment. It must not be reachable from the network. Loopback alone isn't quite enough, though: a web page can point a hostname it controls at
127.0.0.1, which makes the request same-origin and skips the preflight that would otherwise stop it. So theHostheader is checked too, and anything that isn't this router's own address gets a 403.It doesn't proxy prompts or resources, only tools.
It doesn't cover Claude Desktop. That app's per-server schema requires
commandand has nourlortypefield, so an HTTP entry fails validation and gets dropped with a "Some MCP servers could not be loaded" dialog. Remote servers reach Desktop through Settings > Connectors instead.
The trade it makes
Worth being straight about, because it's a real one. One process now sits in front of every MCP server you have, and since the cutover it's the only path Claude Code has to them rather than an opt-in one. If it dies, every session loses every tool at once, where before a failure was isolated to one server in one session.
KeepAlive in the plist is what covers that, and ~/.claude/mcp-profiles/*.json with --strict-mcp-config is the way back to direct stdio if you ever need it.
A dead or broken upstream returns a JSON-RPC tool error naming the server. It doesn't propagate; one broken server can't take the other ten down, and the router stays up. That's verified against docker-mcp with the Docker engine stopped.
One launchd trap, measured rather than theorised: do not set ProcessType: Background in either plist. It throttles startup I/O hard enough that the process never reaches listen(), while launchctl reports the agent running the whole time with an empty log. It looks exactly like a hang.
Tests
node scripts/e2e.mjs # against a running router
node scripts/e2e-idle.mjs # self-contained; starts its own routerTen checks against a running router using the SDK's own client, which is the same one Claude Code uses: initialize, tools/list served from cache, namespacing, tools/call, that the called upstream started, that no other upstream started, and that an unknown server errors without crashing the router.
e2e-idle.mjs is a regression check with its own upstream and its own HOME, so it touches nothing you have configured. It proves a call that runs longer than the idle window is not cut off by the reaper — before the pool counted in-flight calls, a six-second call against a two-second window came back as MCP error -32000: Connection closed, which at the default five-minute window meant every long-running tool call died at five minutes.
Documentation map
File | What it covers |
| The tool cache and its reload semantics; why lazy spawning is possible at all |
| Child lifecycle: single-flight spawn, idle reaping |
| The stateless HTTP layer and how a dead upstream is contained |
| The adoption watcher and its refusal behaviour |
| What the one-liner actually does, in order (and what GitHub Pages serves) |
Design
The mark is a manifold: cool glass conduits converge from the left into one hub, and on the right exactly one branch is lit while the others sit dormant. Many-to-one on the way in, lazy-wake on the way out, which is the whole program in one shape.
It lives in design/icon/, alongside the layered SVG master, its build script, the alternate takes, and audit.html, where every take is scored against the 12-point macOS icon rubric at 128 / 64 / 48 / 32 / 16, losers included with the reason they lost.
fledgeling-co.github.io/mcp-router is a single-page explanation of the same idea for someone who doesn't know what MCP is. It lives in docs/, which is also what serves the install script.
This server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Related MCP Servers
- AlicenseAqualityDmaintenanceEnables Claude Code to perform programmatic tool calling by executing Python scripts that interact with multiple MCP servers in a single round-trip. This reduces latency and token consumption by keeping intermediate tool results within the local Python runtime instead of the conversation context.1MIT
- Alicense-qualityCmaintenanceEnables MCP clients to spawn and control Codex CLI and Claude Code sessions on the host machine, with session management and filesystem access.4MIT
- Alicense-qualityDmaintenanceMCP server that enables Claude Code to communicate with other Claude Code agents over HTTP, allowing users to ask questions about remote codebases or delegate coding tasks.MIT
- Alicense-qualityBmaintenanceLocal MCP servers that give Claude Code access to other tools mid-session.GPL 3.0
Related MCP Connectors
Persistent memory and cross-session learning for AI coding assistants (hosted remote MCP).
A paid remote MCP for OpenAI Codex agent coordination MCP, built to return verdicts, receipts, usage
Agent-native collaboration network: orchestrate a team of long-running agents from any MCP client.
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/fledgeling-co/mcp-router'
If you have feedback or need assistance with the MCP directory API, please join our Discord server