# mcp-multiplexer
[](https://github.com/johgirard/mcp-multiplexer/actions/workflows/ci.yml)
[](https://crates.io/crates/mcp-multiplexer)
[](https://mcpservers.org/servers/johgirard/mcp-multiplexer)
One MCP server fronting many. Point your AI client at the multiplexer and it
presents **7 meta-tools** instead of every upstream server's full tool schemas —
slashing the tokens spent loading tool definitions into the model's context at
session start.
## What it does
Every MCP server you configure pushes all of its tool schemas into the model's
context up front. With a dozen servers that's tens of thousands of tokens the
model mostly never uses. mcp-multiplexer is **one** MCP server (stdio command-
based and remote HTTP upstreams) that exposes only:
| `list_servers` | Overview: name, status, tool count, instructions |
| `list_tools(server)` | Tool names, one-line descriptions, annotations — no schemas |
| `search_tools(query, server?, limit=5)` | Matching tools **with full input schemas** |
| `describe_tool(server, tool)` | One exact tool's full input schema |
| `call_tool(server, tool, arguments)` | Proxied call; results returned verbatim |
| `refresh_tools(server?)` | Reconnect and rebuild the tool index |
| `authorize_server(server, pasted_url?)` | Start/complete OAuth login for a server |
The model discovers tools lazily — list and search first, fetch a full schema
only when it's about to call. The tool index is cached at
`~/.cache/mcp-multiplexer/index.json` for instant startup, and upstream servers
connect lazily.
## Install
**Prebuilt binary** (Linux x86_64/ARM64, macOS Intel/ARM, Windows x86_64 — no
Rust needed): grab the archive for your platform from
[Releases](https://github.com/johgirard/mcp-multiplexer/releases) and put
`mcp-multiplexer` on your `PATH`.
```sh
# example: Linux x86_64
curl -L https://github.com/johgirard/mcp-multiplexer/releases/latest/download/mcp-multiplexer-x86_64-unknown-linux-musl.tar.gz | tar xz
sudo install mcp-multiplexer /usr/local/bin/
```
**From source:**
```sh
cargo install mcp-multiplexer
```
Either also installs `mcp-mock`, a tiny echo server used by the test suite —
harmless, ignore it.
## Configuration
Standard `mcpServers` format (Claude Code / Claude Desktop compatible), plus
per-server extras:
- `expose`: boolean — this server's tools also appear directly as
`server__tool`, bypassing the meta-tools. See
[Hybrid mode: `expose`](#hybrid-mode-expose).
- `allow`: list of exact names or `prefix*` globs — only these tools are visible.
- `deny`: list, always wins over `allow`.
- `connect_timeout`: seconds — connection/startup timeout (default 10). Raise
it for slow-to-start local servers, e.g. `uvx --from git+…` that builds on
every cold start.
Strings in `command`, `args`, `env`, `url`, and `headers` support `${VAR}`
environment expansion (same as Claude Code). An unset variable or unclosed
`${` fails startup with a clear error — so keep secrets out of the config:
```json
{
"$schema": "https://raw.githubusercontent.com/johgirard/mcp-multiplexer/main/schema.json",
"mcpServers": {
"filesystem": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/home/me/docs"],
"allow": ["read_file", "list_directory"]
},
"github": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"],
"env": { "GITHUB_TOKEN": "${GITHUB_TOKEN}" }
},
"web": {
"url": "https://example.com/mcp",
"headers": { "Authorization": "Bearer ${API_TOKEN}" },
"deny": ["admin_*"]
},
"linear": {
"url": "https://mcp.linear.app/mcp",
"oauth": true
},
"fast": {
"command": "mcp-fast-server",
"expose": true
}
}
}
```
Run with `mcp-multiplexer --config /path/to/.mcp.json` (defaults to
`./.mcp.json`).
## Hybrid mode: `expose`
Multiplexing trades a discovery hop (`list_tools` → `describe_tool` →
`call_tool`) for a near-empty startup context. For servers you call *every
session, many times*, that hop is pure overhead — you already know the tool,
and its schema costs a handful of tokens. Set `"expose": true` and that
server's tools mount **directly** as first-class `server__tool` tools, schema
and all, next to the meta-tools:
```json
"serena": { "command": "uvx", "args": ["serena", "start-mcp-server"], "expose": true }
```
The model then calls `serena__find_symbol` like any directly-connected tool —
no search, no describe, no proxy hop. The server stays reachable through the
meta-tools too, and `allow`/`deny` still apply. Rule of thumb: multiplex the
fleet, expose the favorites.
## OAuth
Remote servers that speak OAuth 2.1 (the MCP authorization spec) work with
`"oauth": true` on a `url` server — no other setup in the common case:
- First use fails with an authorization URL. Open it, approve; a temporary
`127.0.0.1` listener completes the exchange. Retry the call and it works.
The model can also drive this itself via the `authorize_server` meta-tool.
- Tokens live in `~/.cache/mcp-multiplexer/tokens.json` (mode 0600), refresh
automatically, and survive restarts — authorize once per server.
Scopes are auto-discovered; dynamic client registration is used when the
provider supports it — and self-heals when a provider forgets the
registration. Providers that delegate auth to a different domain than the MCP
endpoint (cross-domain issuers) work out of the box. Full
guide — headless paste flow, `oauth_client_id` / `oauth_scopes` /
`oauth_redirect_port` tuning, provider notes (GitLab's group toggle gotcha),
troubleshooting: **[docs/oauth.md](docs/oauth.md)**.
## Claude Code
Replace all your `mcpServers` entries with one pointing at the multiplexer:
```json
{
"mcpServers": {
"mux": {
"command": "mcp-multiplexer",
"args": ["--config", "/home/me/.mcp.json"]
}
}
}
```
**Plugin (does this for you):** this repo is a Claude Code plugin whose
`setup` skill installs the binary, migrates your existing servers into a
multiplexer config (secrets become `${VAR}` references, OAuth servers get
flagged), rewires your client config, and verifies:
```
/plugin marketplace add JohGirard/mcp-multiplexer
/plugin install mcp-multiplexer@mcp-multiplexer
```
Then ask Claude to "set up mcp-multiplexer" (or run `/mcp-multiplexer:setup`).
## When upstream tools change
The index is built on first connect and cached on disk. If an upstream server
adds or removes tools, call `refresh_tools` (optionally with a server name) to
re-index — no restart needed. `call_tool` also self-heals: a failed call
triggers one reconnect, re-index, and retry before surfacing the error.
## Docker
```sh
docker build -t mcp-multiplexer .
docker run -i -v $HOME/.mcp.json:/config/.mcp.json:ro mcp-multiplexer
```
Docker mode is for **HTTP/remote upstreams only** — stdio upstreams need their
runtimes (node, uv, …) inside the image.
## Logging
- `--log-file <path>` appends logs to a file; otherwise logs go to stderr
(stdout is protocol-only — do not pipe or redirect it).
- `RUST_LOG` env var controls the level (e.g. `RUST_LOG=debug`); `--verbose`
is shorthand for debug logging.
## Stats
`mcp-multiplexer --stats` prints what the mux is saving you (no server start;
reads `~/.cache/mcp-multiplexer/`):
```
Startup context per session:
without mux: ~16333 tokens (46 tools)
with mux: ~553 tokens (meta-tools)
saved: ~15780 tokens (96%)
```
(example: one GitLab server) plus on-demand schema bytes served, proxied call
counts, and per-meta-tool usage. Tokens are estimated as bytes/4 — a
heuristic, not a real tokenizer. With the Claude Code plugin installed,
`/mcp-multiplexer:gain` shows the same report in chat.
## Non-goals
- MCP resources and prompts (tools only).
- Upstream sampling, elicitation, and roots.
- No truncation of tool results — returned verbatim.
- OAuth for `url` servers only (stdio servers use `env` for secrets).
- No `tools/list_changed` notification forwarding — use `refresh_tools`.
## Development
Contributions welcome — see [CONTRIBUTING.md](CONTRIBUTING.md) for setup and
the CI checklist, and [SECURITY.md](SECURITY.md) for reporting vulnerabilities.
`src/bin/mcp-mock.rs` builds an `mcp-mock` dev binary (echo/add/fail tools)
used by the integration tests.
```sh
cargo test
```
Debug interactively with the MCP Inspector — note the `--`, which keeps the
inspector's own `--config` flag from eating ours:
```sh
npx @modelcontextprotocol/inspector --web -- \
mcp-multiplexer --config /path/to/.mcp.json
```
## Releases
[CHANGELOG.md](CHANGELOG.md) documents every release. Cutting one:
1. Add a `## [X.Y.Z]` section to CHANGELOG.md and bump `version` in Cargo.toml.
2. Commit as `chore: release vX.Y.Z`, tag `vX.Y.Z`, push commit and tag.
The tag workflow verifies the tag matches the crate version, creates the
GitHub release with the changelog section as its notes, attaches per-platform
binaries **with SHA256 checksums**, publishes to crates.io, and pushes
`ghcr.io/johgirard/mcp-multiplexer` images tagged `latest` and `vX.Y.Z`.
## License
MIT. Free for any use, including commercial — the only requirement is keeping the copyright notice.