Transparent stdio multiplexer for MCP servers — share one upstream across multiple Claude Code sessions
Transparent stdio multiplexer that lets multiple Claude Code sessions share a single MCP server process.
One line change in .mcp.json — no other configuration required.
Each Claude Code session spawns its own copy of every configured MCP server (stdio transport). With
4 parallel sessions and 12 servers, that is 48 node/Python processes consuming roughly 4.8 GB of RAM.
Most MCP servers are stateless — they don't need per-session isolation.
mcp-mux consists of two components: a thin shim (the binary CC invokes) and a long-lived daemon
that owns upstream processes. Shims connect to the daemon via IPC; the daemon spawns and manages
upstream servers on behalf of all shims.
graph TB
subgraph "CC Sessions"
CC1[CC Session 1]
CC2[CC Session 2]
CC3[CC Session 3]
end
subgraph "mcp-mux Daemon"
D[mcp-muxd]
O1[Owner: engram]
O2[Owner: tavily]
O3[Owner: aimux]
R[Reaper/GC]
end
subgraph "Upstream Servers"
U1[engram]
U2[tavily]
U3[aimux]
end
CC1 -->|"stdio → shim → IPC"| O1
CC2 -->|"stdio → shim → IPC"| O1
CC2 -->|"stdio → shim → IPC"| O2
CC3 -->|"stdio → shim → IPC"| O3
O1 -->|stdio| U1
O2 -->|stdio| U2
O3 -->|stdio| U3Each shim connects to the daemon owner for its upstream. If no daemon is running, the shim
auto-starts one. If no owner exists for a given server, the daemon spawns it.
Result: one upstream process per server instead of N — approximately 3x memory reduction.
1. Build
# Linux / macOS
go build -o mcp-mux ./cmd/mcp-mux
# Windows
go build -o mcp-mux.exe ./cmd/mcp-muxPlace the binary somewhere on your PATH, or reference it by absolute path in .mcp.json.
2. Configure
Take any MCP server entry in .mcp.json and move the command into args[0], replacingcommand with mcp-mux:
Before:
{
"mcpServers": {
"engram": {
"command": "uvx",
"args": ["engram-mcp-server", "--db", "/data/engram.db"]
}
}
}After:
{
"mcpServers": {
"engram": {
"command": "mcp-mux",
"args": ["uvx", "engram-mcp-server", "--db", "/data/engram.db"]
}
}
}3. Verify
mcp-mux statusOn the next CC session start, mcp-mux intercepts the stdio channel, connects to (or starts) the
daemon, and proxies all MCP traffic transparently.
| Mode | Behavior | Use When |
|---|---|---|
shared (default) | One upstream serves all sessions. Responses to initialize, tools/list, prompts/list, and resources/list are cached and replayed without a round-trip. | Stateless servers: search, docs, LLM proxy. |
isolated | Each session gets its own upstream process. | Per-session state: browser automation, SSH, editor buffers. |
session-aware | One upstream; sessions identified by injected _meta.muxSessionId. | Stateful servers that can partition in-process state by session key. |
Override mode for a specific server:
# Force isolation for one invocation
MCP_MUX_ISOLATED=1 mcp-mux uvx my-server
# CLI flag (equivalent)
mcp-mux --isolated uvx my-serverWhen no explicit mode is set, mcp-mux classifies each server automatically using this priority order:
x-mux capability (highest) — server declares x-mux.sharing in its initialize response.shared.flowchart TD
A[Server starts] --> B{x-mux capability\nin initialize response?}
B -->|Yes| C[Use declared mode]
B -->|No| D{Tool names match\nisolation patterns?}
D -->|Yes| E[Isolated]
D -->|No| F[Shared]If your server is stateless but has tool names that match isolation patterns, add"x-mux": { "sharing": "shared" } to your initialize capabilities to fix the classification.
In shared mode, the owner intercepts and caches the first response for each of these methods:
initializetools/listprompts/listresources/listresources/templates/listSubsequent sessions receive the cached response immediately without a round-trip to the upstream.
Cache entries are invalidated when the upstream sends the corresponding *_changed notification
(notifications/tools/list_changed, notifications/prompts/list_changed,notifications/resources/list_changed).
For initialize, the cache is keyed on protocolVersion. A new client using a different protocol
version bypasses the cache and goes to the upstream directly.
When a new owner is created, mcp-mux sends a synthetic initialize request to the upstream
immediately — before any CC session connects. This pre-populates the response cache so the first
session gets an instant cached replay instead of waiting for the upstream to start.
For slow-starting servers (serena via uvx ~3s, tavily via npx ~5s), this eliminates the CC
"failed" status that occurred when the upstream couldn't respond within CC's startup timeout.
The proactive init also sends notifications/initialized and tools/list to warm the full
cache and trigger auto-classification.
The daemon is enabled by default. It starts automatically when the first mcp-mux shim connects and
no daemon is running.
Lifecycle:
x-mux.persistent: true skip the grace period; they stay alive indefinitelyDisable daemon mode (legacy per-session owner behavior):
MCP_MUX_NO_DAEMON=1 mcp-mux uvx my-servermcp-mux shims automatically reconnect when the daemon restarts. This means:
mcp-mux upgrade swaps the binary without dropping connectionsmcp-mux stop --force triggers automatic reconnect within secondsDuring reconnect, the shim:
ensureDaemon()spawnViaDaemon()initialize request to warm the new ownerReconnect timeout: 30 seconds. If reconnect fails, the shim exits and CC restarts it.
mcp-mux v0.4.0 introduces a session transport layer that replaces the old lastActiveSessionID
heuristic with deterministic, per-session routing.
When CC spawns a shim, the daemon generates a cryptographic token tied to that spawn's working
directory. The shim sends this token as the first line on the IPC connection:
CC → shim → [token\n] → Owner (SessionManager) → upstreamThe Owner reads the token, looks up the corresponding Session.Cwd, and binds the IPC connection
to that session. From this point the session identity is authoritative — no heuristics required.
The SessionManager tracks inflight requests per session. When exactly one session has pending
requests outstanding, response routing is deterministic without needing to inspect message content.
This eliminates spurious mis-routing in high-concurrency scenarios.
roots/list requests from the upstream are forwarded to the active CC session (the one with
pending requests), so the server receives the real workspace roots for that session rather than a
static fallback.
# Show all running upstream instances (PID, sessions, classification, cache state)
mcp-mux status
# Stop all running instances and the daemon
mcp-mux stop [--drain-timeout 30s] [--force]
# Atomic binary upgrade (see section below)
mcp-mux upgrade
# Start a detached daemon process (normally auto-started by shims)
mcp-mux daemon
# Run as control-plane MCP server (exposes mux_list / mux_stop / mux_restart tools)
mcp-mux serveUpgrading the mcp-mux binary while sessions are active is safe and fast:
# One-command upgrade with graceful restart
go build -o mcp-mux.exe~ ./cmd/mcp-mux && mcp-mux upgrade --restartWhat happens:
current → .old, pending~ → current (atomic rename)Without --restart (hot swap only):
mcp-mux upgradeThe daemon keeps running with the old binary. New shim processes use the new binary.
The daemon updates on next natural restart.
Graceful restart preserves:
Not preserved (by design):
All configuration is via environment variables. No config file is required.
| Variable | Default | Description |
|---|---|---|
MCP_MUX_NO_DAEMON | 0 | Set to 1 to disable daemon mode (legacy per-session owner) |
MCP_MUX_ISOLATED | 0 | Set to 1 to force isolated mode for this invocation |
MCP_MUX_STATELESS | 0 | Set to 1 to ignore cwd in server identity hash (enables global deduplication) |
MCP_MUX_GRACE | 30s | Grace period before an idle owner stops its upstream |
MCP_MUX_IDLE_TIMEOUT | 5m | Daemon auto-exit after this period with no activity |
mcp-mux serve exposes an MCP server on stdio with management tools. Add it to .mcp.json like
any other server:
{
"mcpServers": {
"mcp-mux": {
"command": "mcp-mux",
"args": ["serve"]
}
}
}Tools:
| Tool | Description |
|---|---|
mux_list | Returns running instances for the current project (filtered by caller's cwd). Pass all: true to list instances across all projects. Includes server ID, PID, session count, pending requests, classification, and cache status. With verbose: true, includes inflight request details: method, tool name, session, elapsed time. |
mux_stop | Gracefully drains and stops an instance by server_id. Use force: true for immediate kill. |
mux_restart | Stops an instance and spawns a fresh daemon owner with the same command. When called without arguments, resolves to the instance belonging to the caller's session (e.g. mux_restart(name: "aimux") restarts this project's aimux, not another project's). Connected sessions reconnect automatically on their next tool call. |
Session-scoped control plane:
The control plane is session-aware. Each tool call is resolved in the context of the calling
session's working directory:
mux_list — shows only servers owned by the current project by default.mux_list(all: true) for a full view across all projects.mux_restart(name: "aimux") — resolves to the aimux instance started from this project'sThis prevents accidental cross-project interference when multiple projects use the same server
name.
Prompts:
| Prompt | Description |
|---|---|
mux-guide | Full reference on architecture, classification, caching, and troubleshooting. |
mux-status-summary | Calls mux_list and returns a human-readable summary. |
Declare your server's sharing preference in the initialize response capabilities:
{
"protocolVersion": "2025-11-25",
"capabilities": {
"tools": {},
"x-mux": {
"sharing": "shared"
}
}
}For stateless servers that don't depend on the client's working directory, add "stateless": true
to enable global deduplication — one upstream instance regardless of which directory CC is opened
from:
{ "x-mux": { "sharing": "shared", "stateless": true } }For session-aware servers, mcp-mux injects into every request:
_meta.muxSessionId — unique session identifier (format: sess_ + 8 hex chars)_meta.muxCwd — the CC session's project directory (for --project-from-cwd servers)_meta.muxEnv — per-session environment variable diff (API keys, config paths){ "x-mux": { "sharing": "session-aware" } }For servers that must stay alive across all session disconnects (e.g., expensive initialization,
background indexing), declare persistence:
{ "x-mux": { "sharing": "shared", "persistent": true } }Full protocol specification including implementation examples (TypeScript, Python, Go) and
migration path: docs/mux-protocol.md.
mcp-mux includes a smoke test that validates mux-specific behavior with real upstream servers:
# Basic: verify serena works through mux
SMOKE_CWD=D:/Dev/my-project SMOKE_EXPECT=isolated \
go run testdata/smoke_isolated.go uvx --from git+https://github.com/oraios/serena \
serena start-mcp-server --project-from-cwd
# Isolation check: two projects get separate owners
SMOKE_CWD=D:/Dev/project-a SMOKE_CWD2=D:/Dev/project-b SMOKE_EXPECT=isolated \
go run testdata/smoke_isolated.go uvx --from serena ...
# With tool call
SMOKE_CWD=D:/Dev/my-project SMOKE_TOOL=activate_project \
go run testdata/smoke_isolated.go uvx --from serena ...What it validates (mux behavior, not upstream correctness):
# Run tests
go test ./...
# Run vet
go vet ./...
# Build
go build ./cmd/mcp-muxPull requests are welcome. Please ensure go test ./... and go vet ./... pass before submitting.
For significant changes, open an issue first to discuss the approach.
MIT