Small Rust daemon that lets many MCP clients reuse a single STDIO server process (e.g. npx @modelcontextprotocol/server-memory) over a Unix socket. It rewrites JSON-RPC IDs per client, caches initialize, restarts the child on failure, and cleans up the socket on exit.
- One child process per service (spawned from
--cmd ...). - Many clients via Unix socket; ID rewriting keeps responses matched to the right client.
initializeis executed once; later clients get the cached response immediately.- Concurrent requests allowed; active client slots limited by
--max-active-clients(default 5). - Notifications are broadcast to all connected clients.
- Restart-on-exit for the child; pending/waiting requests receive an error on reset.
- Ctrl+C stops the mux, kills the child, and removes the socket file.
- Optional JSON status snapshots (
--status-file) for tray/automation (PID, restarts, queue depth). - Optional tray indicator (
--tray) shows live server status (running/restarting), client and pending counts, initialize cache state, and restart reason.
cargo build --release
Binaries live in target/release/rust_mux.
curl -fsSL https://raw.githubusercontent.com/LibraxisAI/rust_mux/main/tools/install.sh | sh
- Places wrapper in
$HOME/.local/bin/rust_muxand ensures PATH contains cargo bin + wrapper dir. - Env overrides:
INSTALL_DIR,CARGO_HOME,MUX_REF(branch/tag, default main),MUX_NO_LOCK=1to skip--locked.
If your MCP host wants a STDIO command, use the bundled proxy:
rust_mux_proxy --socket /tmp/mcp-memory.sock
Point host config to rust_mux_proxy with the matching socket path.
./target/release/rust_mux \
--socket /tmp/mcp-memory.sock \
--cmd npx -- @modelcontextprotocol/server-memory \
--max-active-clients 5 \
--log-level info
- Default config path:
~/.codex/mcp.json(override via--config <path>). Parser auto-detects by extension (.json,.yaml/.yml,.toml). - JSON:
{
"servers": {
"general-memory": {
"socket": "~/mcp-sockets/general-memory.sock",
"cmd": "npx",
"args": ["@modelcontextprotocol/server-memory"],
"max_active_clients": 5,
"max_request_bytes": 1048576,
"request_timeout_ms": 30000,
"restart_backoff_ms": 1000,
"restart_backoff_max_ms": 30000,
"max_restarts": 5,
"status_file": "~/.rmcp_servers/rust_mux/status.json",
"lazy_start": false,
"tray": true,
"service_name": "general-memory"
}
}
}
- YAML:
servers:
general-memory:
socket: "~/mcp-sockets/general-memory.sock"
cmd: "npx"
args: ["@modelcontextprotocol/server-memory"]
max_active_clients: 5
max_request_bytes: 1048576
request_timeout_ms: 30000
restart_backoff_ms: 1000
restart_backoff_max_ms: 30000
max_restarts: 5
status_file: "~/.rmcp_servers/rust_mux/status.json"
lazy_start: false
tray: true
service_name: "general-memory"
- TOML:
[servers.general-memory]
socket = "~/mcp-sockets/general-memory.sock"
cmd = "npx"
args = ["@modelcontextprotocol/server-memory"]
max_active_clients = 5
max_request_bytes = 1048576
request_timeout_ms = 30000
restart_backoff_ms = 1000
restart_backoff_max_ms = 30000
max_restarts = 5
status_file = "~/.rmcp_servers/rust_mux/status.json"
lazy_start = false
tray = true
service_name = "general-memory"
- Run using config entry:
./target/release/rust_mux --config ~/.codex/mcp.json --service general-memory
- CLI flags still override config (e.g.
--socket,--cmd,--tray).
socket/cmd: required (either CLI or config).--serviceis required when--configis provided.args: CLI--tail wins, otherwise config, otherwise empty.max_active_clients: CLI default 5 unless overridden by config entry.lazy_start: defaultfalse.max_request_bytes: default1_048_576(1 MiB).request_timeout_ms: default30_000(30 s).restart_backoff_ms: default1_000(1 s), capped byrestart_backoff_max_ms(default30_000).max_restarts: default5(0 = unlimited).tray: defaultfalse.service_name: CLI--service-name, else config, else socket file stem, elserust_mux.status_file: optional; accepts~and absolute/relative paths.
- Launch a guided editor (ratatui) to build/update your mux config:
rust_mux wizard --config ~/.codex/mcp-mux.toml --service general-memory
- Controls:
↑/↓move,Enteredit field,Spacetoggle tray,ssave,qquit. Saves JSON/YAML/TOML based on the extension; creates a.bakbefore overwriting. --dry-runruns the wizard without writing files.
ratatui+crosstermpower the TUI wizard; both are pure-Rust and optional (build with--no-default-featuresto skip).tempfileis dev-only for isolated FS fixtures in tests.
- Detect MCP hosts (Codex, Cursor/VSCode, Claude, JetBrains paths) and build a mux manifest + host snippets that point to the bundled proxy:
rust_mux scan --manifest ~/.codex/mcp-mux.toml --snippet ~/.codex/mcp-mux
- Rewire a host config in-place (creates
.bak; add--dry-runto preview):
rust_mux rewire --host codex --socket-dir ~/.rmcp_servers/rust_mux/sockets
- Snippets use the installed
rust_mux_proxybinary:command = "rust_mux_proxy"; args = ["--socket", "<service.sock>"]. - Check whether a host is already pointed at the mux proxy:
rust_mux status --host codex --proxy-cmd rust_mux_proxy
- Verify that config resolves and the mux socket is reachable:
rust_mux health --socket /tmp/mcp-memory.sock --cmd npx -- @modelcontextprotocol/server-memory
- With a config file:
rust_mux health --config ~/.codex/mcp.json --service general-memory
- Run with
--trayto spawn a small status icon. The drawer lists service name, server state, connected/active clients, pending requests, initialize cache state, and restart count/reason. - Click “Quit mux” in the tray menu to stop the daemon (propagates shutdown to the child and cleans the socket).
- To feed your own UI/monitor, write status snapshots to JSON:
rust_mux --status-file ~/.rmcp_servers/rust_mux/status.json .... The file is updated on every state change.
### Proxy config for MCP hosts
Use the bundled proxy instead of `socat`:
rust_mux_proxy --socket /tmp/mcp-memory.sock
Do this per service (memory, brave-search, etc.) with distinct sockets and mux instances.
### launchd (macOS) example
A template lives at `tools/launchd/rust_mux.sample.plist`. Copy to `~/Library/LaunchAgents/`, replace paths/user, then:
launchctl load -w ~/Library/LaunchAgents/rust_mux.general-memory.plist
Label should be unique per service; logs go to the paths defined in the plist.
## Runtime behavior
- New client → assigned `client_id`, messages get `global_id = c<client>:<seq>`.
- Responses are demuxed back to the original client/local ID.
- First `initialize` hits the server; the response is cached and fanned out to waiters. Later `initialize` calls are answered from cache.
- Guards: max request size (default 1 MiB), request timeout (default 30 s) with cleanup of pending calls, exponential restart backoff (1 s → 30 s) with a default limit of 5 restarts, and optional lazy start (defer child spawn until the first request).
- If the child exits or write/read fails, the mux restarts it, clears cache/pending, and sends error responses to affected clients.
- On shutdown (Ctrl+C), the mux stops the child and deletes the socket.
## Options
- `--socket <path>`: Unix socket path.
- `--cmd <prog>` `-- <args>`: command to run the MCP server.
- `--max-active-clients <n>`: limit of concurrently active clients (default 5).
- `--log-level <level>`: trace|debug|info|warn|error (default info).
## Tests and coverage
cargo test cargo clippy --all-targets --all-features cargo tarpaulin --all-targets --timeout 120
Current unit tests cover ID rewriting, initialize caching, and reset fan-out. Integration tests with a fake server can be added to raise coverage.
## Notes and TODOs
- Extend health to include initialize ping and optional metrics (per client / per request).
- Consider persistent initialize params after child restart (auto re-init).
- Add configurable child restart backoff and max retries.
- Expand host detection/rewire coverage and add automated host-side validation.