Semantic tool routing β’ Security guardrails β’ Real-time observability
Stop dumping 50k tokens of tool definitions into your LLM's context window. MCPlex intelligently routes only the tools your agent actually needs.
Every developer building multi-agent AI systems with MCP hits the same wall:
| Pain Point | Impact |
|---|---|
| π§ Context Bloat | 20+ MCP servers = 50k+ tokens of tool definitions consuming your context window |
| π No Security | No RBAC, no audit trails, tool poisoning vulnerabilities |
| ποΈ Blind Operations | Can't track costs, latency, or debug wrong tool selection |
| π Restart Required | Config changes require full restart in production |
| πΈοΈ NΓM Complexity | Orchestrating dozens of servers is an integration nightmare |
MCPlex is a single-binary Rust gateway that sits between your AI agent and MCP servers:
Your Agent βββ MCPlex Gateway βββ GitHub MCP (stdio β persistent)
β βββ Slack MCP (stdio β persistent)
β βββ Database MCP (HTTP)
β βββ Filesystem MCP (stdio β persistent)
βΌ
π§ Smart Routing (70-90% token savings)
π RBAC + Audit Logs + API Key Auth
π Real-time Dashboard + Prometheus
π¦ Response Caching (auto-detect read-only)
π Multi-Tenant (API key β role mapping)
π₯ Hot-reload Config
MCPlex supports both MCP transport types as a first-class citizen:
| Transport | Discovery | Runtime Calls | Connection Model |
|---|---|---|---|
| Stdio | β Full MCP handshake | β Multiplexed JSON-RPC | Persistent child process (long-lived) |
| Streamable HTTP | β Full MCP handshake | β Standard HTTP POST | Stateless (connection pooling) |
Stdio servers are spawned at startup and kept alive for the gateway's lifetime. The MCP handshake (initialize β notifications/initialized) runs once, then all subsequent tools/call, resources/read, and prompts/get requests are multiplexed over the same stdin/stdout pipe using JSON-RPC ID correlation.
Download the latest release from GitHub Releases:
# Linux / macOS
curl -LO https://github.com/ModernOps888/mcplex/releases/latest/download/mcplex-linux-x86_64
chmod +x mcplex-linux-x86_64
sudo mv mcplex-linux-x86_64 /usr/local/bin/mcplexgit clone https://github.com/modernops888/mcplex.git
cd mcplex
cargo build --releasecp mcplex.toml my-config.toml
# Edit my-config.toml with your MCP serversMinimal config for stdio servers:
[gateway]
listen = "127.0.0.1:3100"
dashboard = "127.0.0.1:9090"
[router]
strategy = "semantic"
[[servers]]
name = "filesystem"
command = "npx"
args = ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"]
[[servers]]
name = "memory"
command = "npx"
args = ["-y", "@modelcontextprotocol/server-memory"]./target/release/mcplex --config my-config.toml
# Expected output:
# π Spawning stdio server 'filesystem': npx ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"]
# π€ MCP handshake complete for 'filesystem'
# π‘ Server 'filesystem': 11 tools, 0 resources, 0 prompts
# π Spawning stdio server 'memory': npx ["-y", "@modelcontextprotocol/server-memory"]
# π€ MCP handshake complete for 'memory'
# π‘ Server 'memory': 3 tools, 0 resources, 0 prompts
# β‘ MCPlex gateway listening on 127.0.0.1:3100Point your MCP client to http://127.0.0.1:3100/mcp and open the dashboard at http://127.0.0.1:9090.
MCPlex is a transparent MCP proxy β any MCP client that supports Streamable HTTP can connect to it. Your agent talks to MCPlex as if it were a single MCP server, and MCPlex handles multiplexing, routing, and security behind the scenes.
Add to your Claude Desktop config (claude_desktop_config.json):
{
"mcpServers": {
"mcplex": {
"url": "http://127.0.0.1:3100/mcp",
"headers": {
"Authorization": "Bearer YOUR_API_KEY"
}
}
}
}Any client that supports streamable HTTP MCP servers:
{
"mcpServers": {
"mcplex-gateway": {
"url": "http://127.0.0.1:3100/mcp"
}
}
}import requests
GATEWAY = "http://127.0.0.1:3100/mcp"
HEADERS = {"Authorization": "Bearer YOUR_API_KEY"} # Optional
# Initialize
resp = requests.post(GATEWAY, json={
"jsonrpc": "2.0", "id": 1, "method": "initialize",
"params": {"protocolVersion": "2025-03-26", "capabilities": {},
"clientInfo": {"name": "my-agent", "version": "1.0"}}
}, headers=HEADERS)
# List all tools (MCPlex aggregates from all servers)
resp = requests.post(GATEWAY, json={
"jsonrpc": "2.0", "id": 2, "method": "tools/list"
}, headers=HEADERS)
tools = resp.json()["result"]["tools"]
# Call a tool (MCPlex routes to the right server automatically)
resp = requests.post(GATEWAY, json={
"jsonrpc": "2.0", "id": 3, "method": "tools/call",
"params": {"name": "create_issue", "arguments": {"repo": "my-repo", "title": "Bug fix"}}
}, headers=HEADERS)MCPlex acts as a man-in-the-middle proxy for all MCP traffic:
Your Agent ββPOST /mcpβββ MCPlex Gateway βββ Upstream MCP Server
β (persistent stdio or HTTP)
ββ β
Auth check (API key)
ββ π¦ Rate limit check
ββ π RBAC + allowlist/blocklist
ββ π Audit log (every call)
ββ π Metrics (latency, tokens)
Every tools/call goes through the security engine and is logged. Every tools/list goes through the semantic router. There's no way to bypass it β if your agent uses MCPlex as its MCP endpoint, all calls are intercepted, checked, and logged.
The killer feature. Instead of dumping all tool definitions into your LLM's context, MCPlex uses a meta-tool pattern that works with every standard MCP client β no custom extensions needed:
| Scenario | Without MCPlex | With MCPlex | Savings |
|---|---|---|---|
| 5 servers, 50 tools | ~10,000 tokens | ~200 tokens | 98% |
| 10 servers, 100 tools | ~20,000 tokens | ~200 tokens | 99% |
| 20 servers, 200 tools | ~40,000 tokens | ~200 tokens | 99.5% |
When your agent calls tools/list, MCPlex returns 3 lightweight meta-tools (~200 tokens) instead of all real tools:
Agent MCPlex Gateway
β β
βββtools/listβββββββββββββββββββΊβ Returns: mcplex_find_tools, mcplex_call_tool,
β β mcplex_list_categories (~200 tokens)
β β
βββmcplex_find_toolsβββββββββββββΊβ "store a memory"
βββββββββββββββββββββββββββββββββ€ β [{name: "create_memory", desc: "...", inputSchema: {...}},
β β {name: "save_note", desc: "...", inputSchema: {...}}]
β β
βββmcplex_call_toolββββββββββββββΊβ {name: "create_memory", arguments: {...}}
βββββββββββββββββββββββββββββββββ€ β tool result (routed through security + cache + audit)
mcplex_find_tools(query)β Search for tools by natural language intent. Returns matching tools with full schemas.mcplex_call_tool(name, arguments)β Execute a discovered tool. Routes through the full security/audit/cache pipeline.mcplex_list_categories()β Browse available tool categories (server groups) with tool counts.
This works with Claude Code, Claude Desktop, Cursor, Windsurf, and any other MCP client β no custom extensions or client-side plugins required.
MCPlex supports three routing modes via router.mode:
| Mode | Behavior | Client Compatibility |
|---|---|---|
metatool (default) |
Returns 3 gateway meta-tools; agent discovers real tools via mcplex_find_tools |
β All standard MCP clients |
passthrough |
Returns all real tools directly (no routing indirection) | β All standard MCP clients |
legacy |
Uses _mcplex_query param extension for filtering |
β Custom clients only |
Within metatool and legacy modes, MCPlex uses a routing strategy to rank tools:
semanticβ Character n-gram embeddings with cosine similarity (recommended)keywordβ TF-IDF keyword matching (zero ML dependency)passthroughβ No filtering (baseline)
[router]
mode = "metatool" # "metatool", "passthrough", or "legacy"
strategy = "semantic" # "semantic", "keyword", or "passthrough"
top_k = 5 # Return top 5 most relevant tools
similarity_threshold = 0.3 # Minimum relevance score
cache_embeddings = true # Cache for faster repeated queries[security]
enable_rbac = true
[roles.developer]
allowed_tools = ["github/*", "database/query_*"]
[roles.admin]
allowed_tools = ["*"]
[roles.readonly]
allowed_tools = ["*/list_*", "*/get_*"]
blocked_tools = ["*/delete_*", "*/drop_*"][[servers]]
name = "database"
url = "http://localhost:8080/mcp"
blocked_tools = ["drop_table", "delete_*", "truncate_*"]Every tool invocation is logged as JSON Lines:
{"timestamp":"2026-04-10T10:00:00Z","event":"tool_call","tool_name":"github/create_issue","server_name":"github","duration_ms":342,"trace_id":"a1b2c3d4"}
{"timestamp":"2026-04-10T10:00:01Z","event":"tool_blocked","tool_name":"database/drop_table","reason":"security_policy","trace_id":"e5f6g7h8"}Built-in observability dashboard at http://localhost:9090:
- Global Metrics β Total requests, tool calls, errors, tokens saved
- Per-Tool Stats β Invocation count, avg/p50/p95/p99 latency
- Server Status β Connected servers, transport type, tool/resource/prompt counts
- Live Event Feed β Real-time stream of all gateway activity
The dashboard auto-refreshes every 3 seconds with zero configuration.
Avoid redundant upstream calls for read-only tools:
[cache]
enabled = true
ttl_seconds = 300 # 5 minute TTL
max_entries = 1000 # Max cached responsesMCPlex auto-detects read-only tools by prefix (list_*, get_*, search_*, query_*, describe_*, show_*). You can override with custom patterns:
[cache]
patterns = ["my_custom_tool", "expensive_*"]Write operations (create_*, update_*, delete_*) are never cached by default.
Map API keys to RBAC roles for team-based access:
[api_keys."sk-dev-team-abc123"]
role = "developer"
description = "Dev team key"
[api_keys."sk-admin-xyz789"]
role = "admin"
description = "Admin key"When a request comes in with Authorization: Bearer sk-dev-team-abc123, MCPlex automatically applies the developer role's RBAC policies.
Config changes apply instantly β no restart needed:
# Edit config while MCPlex is running
vim mcplex.toml
# MCPlex automatically detects changes:
# π Config file changed, reloading...
# β
Configuration reloaded successfullyMCPlex aggregates and forwards all three MCP capability types from upstream servers:
| Capability | List | Execute/Read | Routing |
|---|---|---|---|
| Tools | tools/list β aggregated |
tools/call β routed to owner |
β Semantic / Keyword |
| Resources | resources/list β aggregated |
resources/read β routed by URI |
Direct routing |
| Prompts | prompts/list β aggregated |
prompts/get β routed by name |
Direct routing |
| Key | Type | Default | Description |
|---|---|---|---|
listen |
string | 127.0.0.1:3100 |
MCP client connection address |
dashboard |
string | β | Dashboard address (disabled if not set) |
hot_reload |
bool | true |
Auto-reload config on file change |
name |
string | mcplex |
Gateway instance name |
api_key |
string | β | API key for client auth (supports ${ENV}) |
rate_limit_rps |
int | 0 |
Max requests/sec per client (0 = unlimited) |
| Key | Type | Default | Description |
|---|---|---|---|
mode |
string | metatool |
metatool, passthrough, or legacy |
strategy |
string | keyword |
semantic, keyword, or passthrough |
top_k |
int | 5 |
Maximum tools returned per query |
similarity_threshold |
float | 0.3 |
Minimum relevance score (0.0-1.0) |
cache_embeddings |
bool | true |
Cache tool embeddings |
| Key | Type | Default | Description |
|---|---|---|---|
enable_rbac |
bool | false |
Enable role-based access control |
enable_audit_log |
bool | false |
Enable structured audit logging |
audit_log_path |
string | ./logs/audit.jsonl |
Audit log file path |
max_log_size_mb |
int | 100 |
Max log file size before rotation (keeps 5 backups) |
| Key | Type | Required | Description |
|---|---|---|---|
name |
string | β | Unique server name |
command |
string | β‘ | Executable path for stdio transport |
args |
list | β | Command arguments |
url |
string | β‘ | URL for HTTP transport |
env |
map | β | Environment variables (supports ${VAR}) |
allowed_roles |
list | β | Roles allowed to access this server |
blocked_tools |
list | β | Tool blocklist patterns (glob) |
allowed_tools |
list | β | Tool allowlist patterns (glob) |
enabled |
bool | true |
Enable/disable this server |
β‘ = One of command or url is required
Note: For stdio servers,
commandshould be the executable path (e.g.npx,/usr/bin/python3). Additional arguments go in theargsarray.
| Key | Type | Description |
|---|---|---|
allowed_tools |
list | Tool patterns this role can access (glob) |
blocked_tools |
list | Tool patterns this role cannot access (glob) |
Glob Patterns: * matches any characters, ? matches a single character.
Examples: github/*, */query_*, database/get_?ser
MCPlex supports ${ENV_VAR} syntax in configuration:
[[servers]]
name = "github"
command = "npx"
args = ["-y", "@modelcontextprotocol/server-github"]
env = { GITHUB_TOKEN = "${GITHUB_TOKEN}" }βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β MCPlex Gateway β
β β
β βββββββββββββββ ββββββββββββββββ ββββββββββββββββββββ β
β β Semantic β β Security β β Observability β β
β β Router β β Engine β β Collector β β
β β β β β β β β
β β β’ Embeddings β β β’ RBAC β β β’ Token Savings β β
β β β’ TopK Match β β β’ Allowlist β β β’ Latency (p99) β β
β β β’ Caching β β β’ Audit Log β β β’ Dashboard β β
β ββββββββ¬βββββββ ββββββββ¬ββββββββ ββββββββββ¬ββββββββββ β
β ββββββββββββββ¬ββββ β β
β βΌ β β
β βββββββββββββββββββββββββββββββββ β β
β β MCP Protocol Multiplexer ββββββββββ β
β β + Response Cache β β
β ββββββββββββ¬βββββββββββββββββββββ β
βββββββββββββββββββΌββββββββββββββββββββββββββββββββββββββββββ
β
βββββββββββββββΌββββββββββββββββββββββ
βΌ βΌ βΌ
MCP Server MCP Server MCP Server
(stdio β (stdio β (HTTP β
persistent) persistent) stateless)
mcplex [OPTIONS]
Options:
-c, --config <FILE> Config file path [default: mcplex.toml]
-v, --verbose Enable verbose logging
--listen <ADDR> Override gateway listen address
--dashboard <ADDR> Override dashboard listen address
--check Validate config and exit
-h, --help Print help
-V, --version Print versionSecure your gateway so only authorized agents can connect:
[gateway]
api_key = "${MCPLEX_API_KEY}" # Set via environment variableClients authenticate via header:
Authorization: Bearer your-secret-key
# or
X-API-Key: your-secret-key
Health checks (/health) are always unauthenticated for load balancer probes.
Prevent runaway agents from overwhelming your servers:
[gateway]
rate_limit_rps = 50 # Max 50 requests/sec per client (burst: 100)Uses a per-client token bucket with 2x burst allowance. Returns 429 Too Many Requests when exceeded.
Audit logs rotate automatically β they won't fill your disk:
[security]
enable_audit_log = true
audit_log_path = "./logs/audit.jsonl"
max_log_size_mb = 100 # Rotates at 100MB, keeps 5 backupsFiles rotate as: audit.jsonl β audit.jsonl.1 β ... β audit.jsonl.5 (oldest deleted).
Bind to localhost only (default) for local agents:
[gateway]
listen = "127.0.0.1:3100" # Localhost only
dashboard = "127.0.0.1:9090" # Dashboard also localhostFor network access, use a reverse proxy (nginx/caddy) with TLS.
MCPlex exposes a Prometheus-compatible /api/metrics endpoint on the dashboard port for external monitoring:
mcplex_requests_total 1234
mcplex_tool_calls_total 567
mcplex_errors_total 3
mcplex_tokens_saved_total 45000
mcplex_tool_duration_ms{tool="create_issue",quantile="0.95"} 142
Contributions are welcome! Please:
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Run
cargo fmt && cargo clippy -- -D warnings && cargo test - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
MIT License β see LICENSE for details.
Built with π¦ Rust for the AI agent community
If MCPlex saves your context window, give it a β