mcp-proxy-conductor
v0.3.0
Published
Local MCP proxy that aggregates dynamically managed downstream MCP servers, managed at runtime without restarting the client.
Downloads
1,079
Maintainers
Readme
ai-conductor
A local MCP proxy / aggregator. You register it once with an MCP client (e.g. Claude Desktop or Claude Code), and it sits in front of multiple, dynamically managed downstream MCP servers.
Published on npm as
mcp-proxy-conductor.
Why
Individual MCP servers are fiddly to set up and force an agent restart on every change.
ai-conductor decouples that: downstream servers are added/removed at runtime, without
restarting the upstream client. New tools appear immediately via MCP listChanged
notifications.
Claude (MCP client)
│ upstream: stdio
▼
┌─────────────────────────┐
│ ai-conductor │ MCP server upstream, MCP client downstream
└─────────────────────────┘
│ downstream: stdio | Streamable HTTP | SSE
▼
Server A Server B Server C … (managed at runtime)Features
- Runtime management of downstream servers via built-in meta-tools — no client restart.
- Downstream transports: stdio, Streamable HTTP, SSE.
- Aggregates tools, resources, and prompts from all downstreams, with per-server
namespacing (
serverId__name) so names never collide. - Lazy connections: downstreams connect on first use and disconnect after an idle timeout; capabilities are served from a cache so tools are advertised without connecting.
- Credentials in the OS keychain — referenced from config, never stored in plaintext.
- Persistence — added servers are stored and reconnected (lazily) on the next start.
Requirements
- Node.js ≥ 20
Install & register
Register ai-conductor with your MCP client. With the Claude Code CLI:
claude mcp add conductor -s user -- npx -y mcp-proxy-conductorOr add it manually to your client's MCP config:
{
"mcpServers": {
"conductor": { "command": "npx", "args": ["-y", "mcp-proxy-conductor"] }
}
}Restart the client once. ai-conductor then exposes its meta-tools.
Managing downstream servers
Manage downstreams conversationally through the meta-tools:
add_server— add and connect a downstream server at runtime (persists).id: unique, alphanumeric/hyphen, no underscores.transport: one of{ "type": "stdio", "command": "...", "args": [...], "env": { ... } }{ "type": "http", "url": "https://…/mcp", "headers": { ... } }{ "type": "sse", "url": "https://…/sse", "headers": { ... } }
remove_server— disconnect and remove a downstream server (persists). Arg:id.list_servers— list managed servers with connection state (idle/connected/error), whether capabilities come from cache, and capability counts.
Example
{
"id": "filesystem",
"transport": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/some/dir"]
}
}Its tools then appear as filesystem__<toolname>, callable immediately — no restart.
Discovering servers from registries
Instead of typing transport details by hand, discover servers from MCP registries and install them through meta-tools:
list_registries— enumerate available registries with capability flags (canConnect,canDetectSecrets).search_registry(query, sources?)— search for servers. Defaults to the official registry (registry.modelcontextprotocol.io); passsources(ids fromlist_registries) to widen the search.install_from_registry(ref, { id?, env?, secretBindings? })— install a result by itsref. For entries that need secrets, store them withsecret setand passsecretBindings: { ENV_NAME: "<keychain-name>" }; for required non-secret env, passenv: { ENV_NAME: "<value>" }.
Example: search, then install a server that needs a secret:
search_registry { "query": "filesystem" }
→ official:com.example/fs (connectable, requiredSecrets: ["API_TOKEN"])
mcp-proxy-conductor secret set fs-token # in your terminal
install_from_registry { "ref": "official:com.example/fs", "secretBindings": { "API_TOKEN": "fs-token" } }Sources & capability asymmetry: the official registry provides structured run
details and secret flags, so auto-connect and secret detection work fully. Glama is a
discovery source (it lists servers but not a runnable command, so its entries are not
auto-connectable — use the results to then add_server manually). Per-source failures are
reported in the search result, never crashing the search.
Credentials
Secrets live in the OS keychain (macOS Keychain, Windows Credential Manager, Linux
Secret Service) — never in config.json and never passed through the chat.
1. Store a secret with the CLI (the value is read from hidden stdin, not the chat):
mcp-proxy-conductor secret set my-token
# or pipe it (e.g. in scripts):
printf '%s' "$MY_TOKEN" | mcp-proxy-conductor secret set my-token
# remove it again:
mcp-proxy-conductor secret rm my-token2. Reference it in add_server via ${keychain:<name>}:
{
"id": "uptime",
"transport": {
"type": "http",
"url": "https://example.com/mcp",
"headers": { "Authorization": "Bearer ${keychain:my-token}" }
}
}References are resolved at connect time. Supported placeholder schemes (usable in any
env or headers value, and combinable within a string):
| Reference | Resolves from |
|---|---|
| ${keychain:NAME} | OS keychain (service mcp-proxy-conductor) |
| ${env:VAR} | process environment |
| ${VAR} | process environment (shorthand) |
Headless Linux without a running Secret Service daemon cannot use the keychain — use
${env:…}there instead. A missing or unreachable secret surfaces as anerrorstate for that one downstream (visible inlist_servers); other servers keep working.
Lazy connections
To scale to many downstreams, connections are late-bound: at startup nothing is
connected — tools/resources/prompts are advertised from a persisted capability cache. A
downstream connects on the first actual call and is evicted after an idle timeout
(default 5 minutes, override with CONDUCTOR_IDLE_TIMEOUT_MS, in ms; 0 disables
eviction). The first call to an idle server therefore pays a one-time connect latency.
Development
pnpm install
pnpm test # vitest
pnpm typecheck
pnpm build # tsup → dist/index.jsStatus & limitations
Local, single-user focus. Known limitations:
- No automatic reconnect/backoff for a downstream that crashes (it returns to
idleand reconnects on the next call). - Change notifications are coarse (all
listChangedtypes emitted on any change). - HTTP downstreams do not auto-fall back to SSE; the transport type is explicit.
Planned, not yet implemented: multi-tenant/SaaS mode.
License
MIT © Patrick Cornelißen
