https://github.com/toabctl/mcpmux
MCP multiplexer: one MCP endpoint proxying several backend MCP servers, holding their credentials
https://github.com/toabctl/mcpmux
Last synced: 3 days ago
JSON representation
MCP multiplexer: one MCP endpoint proxying several backend MCP servers, holding their credentials
- Host: GitHub
- URL: https://github.com/toabctl/mcpmux
- Owner: toabctl
- License: apache-2.0
- Created: 2026-05-30T08:24:51.000Z (5 days ago)
- Default Branch: main
- Last Pushed: 2026-05-31T07:38:06.000Z (4 days ago)
- Last Synced: 2026-05-31T09:17:09.058Z (4 days ago)
- Language: Go
- Size: 43.9 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# mcpmux
A minimal **Model Context Protocol (MCP) multiplexer**. It connects to several
upstream MCP servers ("backends"), merges their tools under namespaced
`__` names, and re-exposes them through a **single MCP endpoint**
(stdio or streamable HTTP).
The client (e.g. Claude Code) authenticates **once to mcpmux**; mcpmux holds each
backend's credentials and forwards calls to the right backend.
```
┌──────────────── mcpmux ────────────────┐
MCP client ──────► │ MCP server ─► router ─► client(s) │ ─► backend A (command, env secrets)
(one endpoint) │ │ ─► backend B (http, bearer token)
└─────────────────────────────────────────┘ ─► backend C (http, custom header)
```
## Install
```sh
go install github.com/toabctl/mcpmux@latest # installs `mcpmux`
# or build from a checkout:
make build # produces ./mcpmux
```
## Configure
Copy `mcpmux.example.yaml` to a config file and edit. `${ENV_VAR}` references are
expanded from the environment at load time, so secrets stay out of the file.
`mcpmux` looks for its config in this order (first match wins); override with
`--config/-c`:
1. `./mcpmux.yaml`
2. `$XDG_CONFIG_HOME/mcpmux/config.yaml` (Linux: `~/.config/mcpmux/config.yaml`)
3. `$XDG_CONFIG_HOME/mcpmux.yaml`
- `listen.transport`: `stdio` or `http`.
- `listen.address` / `listen.path`: bind address and URL path for `http`
(clients connect to `http://
`, e.g. `http://127.0.0.1:8080/mcp`).
- `backends[]`: each has a unique `name` (used as the tool prefix) and a
`transport`:
- `command` — launched as a subprocess; pass secrets via `env`.
- `http` — a streamable-HTTP endpoint; authenticate with `auth.type` of:
- `none` — no credentials.
- `bearer` — static `token` sent as `Authorization: Bearer `.
- `header` — a custom `header` name + `value`.
- `command` — run a credential helper (`command: [...]`) whose stdout is
the bearer token. The token is cached and re-run only when it nears
expiry (a JWT `exp` claim is honored; `ttl` caps caching for opaque
tokens) or when the backend returns 401/403. Ideal for CLIs that already
hold a login, e.g. `chainctl auth token --audience ` — no
browser, ever.
- `oauth` — interactive authorization-code + PKCE flow with dynamic client
registration (RFC 7591). A browser opens **once when the daemon starts**;
the SDK handles discovery, PKCE, token exchange and in-memory refresh.
Options: `scopes`, `client_name`, `open_browser` (default true; false
just logs the URL for headless use), `callback_port` (0 = ephemeral).
## Run
```sh
mcpmux serve -c mcpmux.yaml # run the proxy
mcpmux list -c mcpmux.yaml # debug: print the aggregated tool catalog
```
Logs go to **stderr**, leaving stdout clean for the stdio MCP transport.
## Run as a daemon (systemd user service)
Running mcpmux as a long-lived user service is the intended setup: you
authenticate backends **once when the daemon starts**, and every Claude Code
session reuses the daemon's live tokens instead of re-authenticating per
session.
```sh
make install # binary -> ~/.local/bin, units -> ~/.config/systemd/user
systemctl --user daemon-reload
```
mcpmux ships a **`.service`** and a **`.socket`** (socket activation). systemd
owns the listening socket, so it survives service restarts — clients never get
"connection refused" while mcpmux restarts or re-authenticates. Two modes:
**A) Always-on + socket** (recommended when you have interactive OAuth backends
like Linear — it authenticates once at startup and stays up):
```sh
systemctl --user enable --now mcpmux.socket mcpmux.service
```
**B) On-demand** (only if *all* backends are non-interactive — the service
starts on the first connection):
```sh
systemctl --user enable --now mcpmux.socket
```
```sh
journalctl --user -u mcpmux -f # watch logs / OAuth URLs
```
When socket-activated, the **socket's `ListenStream` address is authoritative**
and `listen.address` in the config is ignored (the passed socket is used);
`listen.path` still applies. mcpmux signals readiness via `sd_notify`
(`Type=notify`), and the unit allows 5 minutes for an interactive browser
authorization at startup.
For `oauth` backends to auto-open a browser from the service, make the graphical
session visible to systemd once after login:
```sh
systemctl --user import-environment DISPLAY WAYLAND_DISPLAY XAUTHORITY DBUS_SESSION_BUS_ADDRESS
```
Otherwise set `open_browser: false` on those backends and click the URL printed
in the journal.
> Note: mode B (on-demand) is a poor fit for interactive OAuth backends — the
> first connection would block while the browser flow completes and likely time
> out. Use mode A when any backend uses `auth.type: oauth`.
## Use from Claude Code
Start mcpmux, then register the single endpoint with Claude Code:
```sh
mcpmux serve # uses ~/.config/mcpmux/config.yaml
claude mcp add --transport http mcpmux http://127.0.0.1:8080/mcp
```
Tools appear namespaced, e.g. `github__create_issue`. Remove the individual
servers you configured into mcpmux (`claude mcp remove `) so their tools
don't show up twice.
For `listen.transport: stdio`, let Claude Code launch it instead:
```sh
claude mcp add mcpmux -- /path/to/mcpmux serve -c /path/to/mcpmux.yaml
```
## Scope & limitations
- **Backend auth supported:** env vars for stdio (`command` transport) backends;
for `http` backends `none`/`bearer`/`header`, the `command` credential helper
(dynamic bearer token from an external CLI, auto-refreshed), and interactive
`oauth` (authorization-code + PKCE + dynamic client registration; tokens held
in memory for the daemon's lifetime).
- **Not yet:** persisting OAuth tokens across restarts (each daemon start
re-authenticates `oauth` backends), and authenticating the *client→mcpmux* hop
(run it on localhost or behind your own reverse proxy / auth gateway).
- Tool-name collisions are avoided by the `__` prefix. Resources and
prompts are not yet aggregated — tools only.
## Development
```sh
pre-commit install && pre-commit install --hook-type commit-msg
```
Hooks run gofmt/golangci-lint, `go vet`, `govulncheck`, `go mod tidy`, and
Conventional Commits checks; `go test` runs on push. Commit messages must follow
[Conventional Commits](https://www.conventionalcommits.org).
## License
[Apache-2.0](LICENSE) © Thomas Bechtold