Skip to main content
Glama
ModernOps888

mcplex

by ModernOps888
README.md
<div align="center">

# πŸš€ MCPlex β€” The MCP Smart Gateway

**Semantic tool routing β€’ Security guardrails β€’ Real-time observability**

[![CI](https://github.com/ModernOps888/mcplex/actions/workflows/ci.yml/badge.svg)](https://github.com/ModernOps888/mcplex/actions/workflows/ci.yml)
[![Release](https://github.com/ModernOps888/mcplex/actions/workflows/release.yml/badge.svg)](https://github.com/ModernOps888/mcplex/releases)
[![Glama Score](https://glama.ai/mcp/servers/ModernOps888/mcplex/badges/score.svg)](https://glama.ai/mcp/servers/ModernOps888/mcplex)
[![License: MIT](https://img.shields.io/badge/License-MIT-cyan.svg)](LICENSE)
[![Rust](https://img.shields.io/badge/Built%20with-Rust-orange.svg)](https://www.rust-lang.org/)
[![MCP](https://img.shields.io/badge/MCP-2025--11--25-blue.svg)](https://modelcontextprotocol.io)

*Stop dumping 50k tokens of tool definitions into your LLM's context window.*
*MCPlex intelligently routes only the tools your agent actually needs.*

</div>

---

## The Problem

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 |

## The Solution

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
```

### Transport Support

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.

## ⚑ Quick Start

### 1. Install (Pre-built Binary)

Download the latest release from [GitHub Releases](https://github.com/ModernOps888/mcplex/releases):

```bash
# 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/mcplex
```

### 2. Build from Source

```bash
git clone https://github.com/modernops888/mcplex.git
cd mcplex
cargo build --release
```

### 3. Configure

```bash
cp mcplex.toml my-config.toml
# Edit my-config.toml with your MCP servers
```

**Minimal config for stdio servers:**

```toml
[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"]
```

### 4. Run

```bash
./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:3100
```

### 5. Connect Your Agent

Point your MCP client to `http://127.0.0.1:3100/mcp` and open the dashboard at `http://127.0.0.1:9090`.

## πŸš€ Running as a Service (Deployment)

For persistent environments, run MCPlex as a background service to ensure it starts automatically on boot and restarts if it crashes.

### Linux (`systemd`)

Create a systemd unit file at `/etc/systemd/system/mcplex.service`:

```ini
[Unit]
Description=MCPlex Gateway
After=network.target

[Service]
Type=simple
User=your_user
WorkingDirectory=/path/to/mcplex
ExecStart=/usr/local/bin/mcplex --config /path/to/mcplex/mcplex.toml
Restart=always
RestartSec=3

[Install]
WantedBy=multi-user.target
```

Enable and start the service:
```bash
sudo systemctl daemon-reload
sudo systemctl enable mcplex
sudo systemctl start mcplex
sudo systemctl status mcplex
```

### macOS (`launchd`)

Create a launchd plist file at `~/Library/LaunchAgents/com.modernops.mcplex.plist`:

```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>com.modernops.mcplex</string>
    <key>ProgramArguments</key>
    <array>
        <string>/usr/local/bin/mcplex</string>
        <string>--config</string>
        <string>/path/to/mcplex.toml</string>
    </array>
    <key>RunAtLoad</key>
    <true/>
    <key>KeepAlive</key>
    <true/>
    <key>StandardOutPath</key>
    <string>/tmp/mcplex.log</string>
    <key>StandardErrorPath</key>
    <string>/tmp/mcplex-error.log</string>
    <key>EnvironmentVariables</key>
    <dict>
        <key>PATH</key>
        <string>/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
    </dict>
</dict>
</plist>
```

Load and start the service:
```bash
launchctl load ~/Library/LaunchAgents/com.modernops.mcplex.plist
launchctl start com.modernops.mcplex
```

### Log Rotation

When running as a service, ensure you implement log rotation to prevent infinite log growth. For macOS, add an entry to `/etc/newsyslog.conf`; for Linux, use `logrotate`. MCPlex also has built-in audit log rotation configured via `max_log_size_mb`.

### Service Troubleshooting

| Issue | Resolution |
|-------|------------|
| Service fails to start immediately | Check your configuration file syntax by running `mcplex --check --config <path>` manually. |
| Port already in use | Verify no other service is bound to the `listen` port. Change `gateway.listen` in your config. |
| `launchd` permission errors | Ensure the `ProgramArguments` path is absolute and executable by the user. |
| Server respawn loop | Check the `StandardErrorPath` log for fatal bootstrap errors or missing dependencies (e.g., Node.js for `npx` servers). |

---

## πŸ”Œ How to Connect Your Agent

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.

### Claude Code / Claude Desktop (Recommended β€” stdio bridge)

Claude Code and Claude Desktop use **stdio transport**. MCPlex ships a cross-platform bridge (`bridge.mjs`) that translates stdio ↔ HTTP. Works on **macOS, Windows, and Linux**.

Add a `.mcp.json` to your project root (Claude Code auto-discovers it):

```json
{
  "mcpServers": {
    "mcplex": {
      "command": "node",
      "args": ["/path/to/mcplex/bridge.mjs"],
      "env": {
        "MCPLEX_GATEWAY": "http://127.0.0.1:3100/mcp"
      }
    }
  }
}
```

For Claude Desktop, add the same config to `claude_desktop_config.json`.

### VS Code / GitHub Copilot (agent mode)

VS Code supports MCP servers natively. Add a `.vscode/mcp.json` to your workspace (or run **MCP: Add Server** from the Command Palette) pointing at the MCPlex bridge:

```json
{
  "servers": {
    "mcplex": {
      "type": "stdio",
      "command": "node",
      "args": ["/path/to/mcplex/bridge.mjs"],
      "env": {
        "MCPLEX_GATEWAY": "http://127.0.0.1:3100/mcp"
      }
    }
  }
}
```

A ready-to-copy template lives at [examples/vscode-mcp.json](examples/vscode-mcp.json).

With the default **meta-tool mode**, Copilot's agent sees just 3 gateway tools (~200 tokens) instead of every tool definition from every connected server β€” the same 70–90% context savings apply inside your IDE session. Tool discovery happens on demand via `mcplex_find_tools`, and every call is still routed through RBAC, allowlists, audit logging, and the response cache.

### Cursor / Windsurf / HTTP-capable MCP Clients

Clients that support streamable HTTP can connect directly:

```json
{
  "mcpServers": {
    "mcplex-gateway": {
      "url": "http://127.0.0.1:3100/mcp"
    }
  }
}
```

### Custom Python Agent

```python
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-11-25", "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)
```

### How It Catches Your Agent's Calls

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 (constant-time API key compare)
                              β”œβ”€ 🚦 Rate limit check
                              β”œβ”€ πŸ§ͺ Input validation (name charset, 64KB / depth-16 args cap)
                              β”œβ”€ πŸ”’ RBAC + allowlist/blocklist (role bound to API key)
                              β”œβ”€ πŸ“ Audit log (every call)
                              └─ πŸ“Š Metrics (latency, tokens, security events)
```

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**.

## πŸ€– Compatible AI Models

MCPlex is model-agnostic β€” it routes any MCP-compliant client traffic regardless of which LLM is driving it. These are the frontier models actively tested and validated with MCPlex:

| Model | Provider | MCP Client | Best For |
|-------|----------|-----------|----------|
| **GPT-5.6 Sol** | OpenAI | ChatGPT Work, custom | Flagship reasoning + agentic tasks |
| **GPT-5.6 Terra** | OpenAI | ChatGPT Work, custom | Balanced performance / cost |
| **GPT-5.6 Luna** | OpenAI | ChatGPT Work, custom | Cost-efficient everyday tasks |
| **o3 / o3-mini** | OpenAI | Custom, Cursor, IDEs | Deep math, logic & low-latency reasoning |
| **Claude Fable 5** | Anthropic | Claude Code, Claude Desktop | Extended reasoning + autonomous code |
| **Claude Mythos 5** | Anthropic | Claude Code, Claude Desktop | Complex multi-step agent workflows |
| **Claude Sonnet 5** | Anthropic | Claude Code, Claude Desktop | High-capability, broad availability |
| **Claude 3.7 Sonnet** | Anthropic | Claude Code, Cursor, Windsurf | Hybrid reasoning + high-speed coding |
| **Gemini 3.5 Flash** | Google | Gemini Spark, custom | High-throughput, cost-efficient |
| **Gemini 3.1 Pro** | Google | Gemini Spark, custom | Multimodal + Workspace integration |
| **DeepSeek V3 / R1** | DeepSeek | Custom, Open-WebUI, Ollama | Open frontier reasoning, cost efficiency |
| **Grok 4.5** | xAI | Custom / open-weight stacks | Competitive reasoning, open ecosystem |

The meta-tool pattern (3 gateway tools, ~200 tokens) is particularly effective with models that have smaller default context budgets β€” MCPlex's token savings become more impactful as model costs rise.

> **Context savings scale with model pricing.** With GPT-5.6 Sol at frontier pricing, eliminating 40k tokens of tool definitions per request translates to measurable cost reduction at scale.

## 🧠 Semantic Tool Routing

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%** |

### How It Works

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.

### Routing Mode

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 |

### Routing Strategy

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)

```toml
[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 Engine

### Role-Based Access Control (RBAC)

```toml
[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_*"]
```

> **Role trust boundary:** a caller's role is only ever established server-side, from a verified `api_key`/`api_keys` match β€” never from anything a client sends in the request body. When `enable_rbac = true` and no `api_key`/`api_keys` are configured, every tool call is denied by default (no role can be established), and the gateway logs a startup warning so this isn't mistaken for a bug.

### Per-Server Tool Blocklists

```toml
[[servers]]
name = "database"
url = "http://localhost:8080/mcp"
blocked_tools = ["drop_table", "delete_*", "truncate_*"]
```

### Structured Audit Logging

Every tool invocation is logged as JSON Lines:

```json
{"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"}
```

## πŸ“Š Real-time Dashboard

Built-in observability dashboard at `http://localhost:9090`:

<div align="center">

![MCPlex Dashboard](docs/images/dashboard.png)

</div>

- **Global Metrics** β€” Total requests, tool calls, errors, tokens saved
- **Per-Tool Stats** β€” Invocation count, avg/p50/p95/p99 latency
- **Server Fleet** β€” Connected servers, transport type, tool/resource/prompt counts
- **Live Event Stream** β€” Real-time feed of all gateway activity with latency tags

Glassmorphism design with animated gradients. Auto-refreshes every 3 seconds. Zero configuration.

By default the dashboard's `/api/*` routes are unauthenticated β€” fine for `127.0.0.1`-only binds, but if you expose the dashboard beyond localhost, set `gateway.dashboard_api_key` so requests need an `Authorization: Bearer <key>` (or `X-API-Key`) header:

```toml
[gateway]
dashboard_api_key = "${MCPLEX_DASHBOARD_API_KEY}"
```

## πŸ“¦ Response Caching

Avoid redundant upstream calls for read-only tools:

```toml
[cache]
enabled = true
ttl_seconds = 300     # 5 minute TTL
max_entries = 1000    # Max cached responses
```

Cached responses are partitioned by caller role, so a cache entry from one role/tenant is never served to another. MCPlex **auto-detects** read-only tools by prefix (`list_*`, `get_*`, `search_*`, `query_*`, `describe_*`, `show_*`). You can override with custom patterns:

```toml
[cache]
patterns = ["my_custom_tool", "expensive_*"]
```

Write operations (`create_*`, `update_*`, `delete_*`) are **never cached** by default.

## πŸ”‘ Multi-Tenant API Keys

Map API keys to RBAC roles for team-based access:

```toml
[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.

## πŸ”₯ Hot-Reload Configuration

Config changes apply instantly β€” no restart needed:

```bash
# Edit config while MCPlex is running
vim mcplex.toml

# MCPlex automatically detects changes:
# πŸ”„ Config file changed, reloading...
# βœ… Configuration reloaded successfully
```

## πŸ“– Full MCP Capability Support

MCPlex 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 |

## Configuration Reference

### `[gateway]`

| 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}`) |
| `dashboard_api_key` | string | β€” | API key for the dashboard `/api/*` routes (supports `${ENV}`). If unset, the dashboard is unauthenticated β€” bind it to `127.0.0.1` in that case. |
| `rate_limit_rps` | int | `0` | Max requests/sec per client (0 = unlimited) |

### `[router]`

| 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 |

### `[security]`

| 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) |
| `circuit_breaker_max_crashes` | int | `5` | Max crashes within the window before respawns are blocked |
| `circuit_breaker_window_secs` | int | `60` | Sliding window (seconds) for crash counting |
| `request_timeout_secs` | int | `30` | Steady-state per-request timeout for HTTP and stdio upstream calls, once connected (0 = no timeout) |
| `default_handshake_timeout_secs` | int | `30` | Default timeout for the initial stdio MCP handshake (`initialize`), before a server is considered failed and enters the respawn loop. Overridable per server β€” see `servers[].handshake_timeout_secs` below. (0 = no timeout) |

### `[[servers]]`

| 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 |
| `handshake_timeout_secs` | int | β€” | Per-server override for the initial stdio handshake timeout. Falls back to `security.default_handshake_timeout_secs` when unset. Useful for servers with a slow cold start, e.g. `npx` resolving/downloading a package on first run (0 = no timeout) |

⚑ = One of `command` or `url` is required

> **Note:** For stdio servers, `command` should be the **executable path** (e.g. `npx`, `/usr/bin/python3`). Additional arguments go in the `args` array.

> **Handshake timeout vs. request timeout:** the initial stdio `initialize` handshake and steady-state `tools/call` requests use two independent timeouts. A slow-starting server (e.g. `npx` fetching a package on first run) can be given a longer `handshake_timeout_secs` without loosening the timeout applied to every later tool call:
> ```toml
> [[servers]]
> name = "slow-npx-server"
> command = "npx"
> args = ["-y", "@some/mcp-server"]
> handshake_timeout_secs = 90  # default 30s, 0 = no timeout
> ```


### `[roles.<name>]`

| 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`

## Environment Variables

MCPlex supports `${ENV_VAR}` syntax in configuration:

```toml
[[servers]]
name = "github"
command = "npx"
args = ["-y", "@modelcontextprotocol/server-github"]
env = { GITHUB_TOKEN = "${GITHUB_TOKEN}" }
```

## Architecture

```
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                      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)
```

## CLI Reference

```bash
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 version
```

## πŸ›‘οΈ Production Hardening

### API Key Authentication

Secure your gateway so only authorized agents can connect:

```toml
[gateway]
api_key = "${MCPLEX_API_KEY}"  # Set via environment variable
```

Clients 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.

### Rate Limiting

Prevent runaway agents from overwhelming your servers:

```toml
[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.

### Log Rotation

Audit logs rotate automatically β€” they won't fill your disk:

```toml
[security]
enable_audit_log = true
audit_log_path = "./logs/audit.jsonl"
max_log_size_mb = 100  # Rotates at 100MB, keeps 5 backups
```

Files rotate as: `audit.jsonl` β†’ `audit.jsonl.1` β†’ ... β†’ `audit.jsonl.5` (oldest deleted).

### Network Security

Bind to localhost only (default) for local agents:
```toml
[gateway]
listen = "127.0.0.1:3100"     # Localhost only
dashboard = "127.0.0.1:9090"  # Dashboard also localhost
```

For network access, use a reverse proxy (nginx/caddy) with TLS.

### Prometheus Monitoring

MCPlex exposes Prometheus-compatible metrics on the dashboard port for external monitoring:

- **`/api/metrics`** β€” JSON metrics endpoint
- **`/api/prometheus`** β€” Prometheus text exposition format (v0.4.0)

```
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
```

## πŸ” AgentLens Integration

MCPlex pairs with [AgentLens](https://github.com/ModernOps888/agentlens) for full-stack agent observability. MCPlex handles execution, AgentLens handles visualization β€” together they cover 100% of the agent lifecycle. As of v0.4.0, the AgentLens bridge is **fully wired** and active when configured via the `[agentlens]` config section.

### Enable the Bridge (opt-in)

Add to your `mcplex.toml`:

```toml
[agentlens]
enabled = true
url = "http://127.0.0.1:3000/api/ingest"
session_name = "MCPlex Gateway"
```

Every tool call, security event, and routing decision is forwarded to AgentLens's timeline replay UI. The bridge is non-blocking (fire-and-forget) β€” it never slows down the gateway, even if AgentLens is offline.

> **Note:** MCPlex works 100% independently without AgentLens. The bridge is entirely opt-in.

## πŸ”₯ The Forge Integration

MCPlex pairs naturally with [The Forge](https://github.com/ModernOps888/the-forge) β€” a multi-agent code evolution platform that pits frontier models head-to-head on coding challenges using evolutionary algorithms.

The Forge exposes its own MCP server, which means you can add it to MCPlex just like any other server:

```toml
[[servers]]
name = "forge"
url = "http://localhost:8400/mcp"   # The Forge MCP endpoint
transport = "streamable-http"
allowed_roles = ["developer", "admin"]
```

With this setup, your agents can invoke Forge tools (multi-model arena runs, evolution sessions, research synthesis) through the same gateway β€” with full RBAC, audit logging, and token-saving routing applied automatically.

| Forge Tool | What It Does |
|------------|-------------|
| `forge_run_arena` | Pit GPT-5.6 / Claude Fable 5 / Gemini / Grok head-to-head on a coding challenge |
| `forge_evolve` | Apply mutation operators to code and run fitness-gated selection |
| `forge_research` | Deep-research a topic across multiple models, synthesise consensus |
| `forge_benchmark` | Score and rank model outputs on custom evaluation criteria |

> MCPlex works 100% independently without The Forge. See [github.com/ModernOps888/the-forge](https://github.com/ModernOps888/the-forge) for setup.


Contributions are welcome! Please:

1. Fork the repository
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
3. Run `cargo fmt && cargo clippy -- -D warnings && cargo test`
4. Commit your changes (`git commit -m 'Add amazing feature'`)
5. Push to the branch (`git push origin feature/amazing-feature`)
6. Open a Pull Request

## License

MIT License β€” see [LICENSE](LICENSE) for details.

## πŸ”§ Recent Changes (v0.7.1 β€” Security Hardening &amp; Configurable Handshake Timeout)

- **RBAC Self-Assertion Bypass Fixed (HIGH)** β€” `dispatch_real_tool` previously fell back to a client-supplied `_mcplex_role` request param whenever no server-verified role was established (e.g. no `api_key`/`api_keys` configured). Any caller could self-assert `role: "admin"` and bypass RBAC entirely. The role now comes exclusively from the auth middleware's server-verified `trusted_role`.
- **Dashboard Authentication** β€” `/api/*` dashboard routes were previously unauthenticated. Added optional `gateway.dashboard_api_key`; when set, dashboard routes require a matching `Authorization: Bearer`/`X-API-Key` header. Unset by default (backward compatible), with a startup warning to bind localhost-only in that case.
- **Role-Partitioned Cache** β€” The tool response cache is now partitioned by caller role, closing a cross-tenant leakage gap in multi-key deployments.
- **Empty-Secret Rejection** β€” `validate_config` now rejects `gateway.api_key`/`dashboard_api_key`/`api_keys` that resolve to an empty string (e.g. from an unset `${ENV_VAR}`), instead of silently turning "auth required" into a no-op.
- **Configurable Stdio Handshake Timeout** (fixes [#20](https://github.com/ModernOps888/mcplex/issues/20)) β€” the initial `initialize` handshake and steady-state tool calls now use independent timeouts: `security.default_handshake_timeout_secs` (global, default 30s) and `servers[].handshake_timeout_secs` (per-server override) for the handshake, vs. `security.request_timeout_secs` for steady-state calls. Fixes spurious failures on slow-cold-start servers (e.g. `npx` downloading a package on first run). `0 = no timeout` is now actually implemented for both.
- **Audit Redaction List Expanded** β€” added `pwd`, `auth`, `cookie`, `session`, `bearer` to the sensitive-key redaction list.
- **38 tests passing** (up from 32) β€” new regression tests for the RBAC fix, cache partitioning, and handshake-timeout config.

<details>
<summary>Previous (v0.7.0 β€” Model Ecosystem Refresh)</summary>

- **Compatible Models Dashboard Panel** β€” Built-in dashboard now shows a `πŸ€– Compatible Models` panel with colour-coded provider badges for all 9 active frontier models: GPT-5.6 Sol/Terra/Luna, Claude Fable 5/Mythos 5/Sonnet 5, Gemini 3.5 Flash/3.1 Pro, Grok 4.5.
- **Ecosystem Footer** β€” Dashboard footer now links to AgentLens, The Forge, and GitHub with the current MCP protocol version displayed.
- **Expanded Server Examples** β€” `mcplex.toml` and `examples/` updated with `memory`, `fetch`, `brave-search`, `postgres`, `sequential-thinking`, and `forge` (The Forge MCP) server blocks.
- **The Forge Integration** β€” New README section with example config and tool table for connecting [The Forge](https://github.com/ModernOps888/the-forge) multi-model arena as an MCP server.
- **Compatible AI Models Table** β€” Full July 2026 model matrix in the README with MCP client and use-case guidance.
- **Fixed** Python example protocol version `2025-03-26` β†’ `2025-11-25`.
- **Fixed** missing CHANGELOG comparison links for v0.4.0–v0.6.0.

</details>

<details>
<summary>Previous (v0.6.0 β€” Audit Hardening &amp; Denial Telemetry)</summary>

- **Audit Log Secret Redaction** β€” Values under sensitive keys (`password`, `token`, `api_key`, `secret`, `credential`, etc.) are recursively replaced with `[REDACTED]` before hitting disk; credentials never persist in `audit.jsonl`.
- **Spoof-Proof Rate Limiting** β€” Client identity now comes from the real socket address (`ConnectInfo`), falling back to the first `X-Forwarded-For` hop only behind proxies; header spoofing no longer evades limits.
- **Auth/Rate-Limit Denial Telemetry** β€” `auth_denied` and `rate_limited` security events now recorded in metrics and visible in the dashboard event stream.
- **Resource/Prompt Audit Coverage** β€” `resources/read` and `prompts/get` are now written to the audit trail (`resource_read` / `prompt_get` events); resource URIs capped at 2048 chars.
- **Panic Fix** β€” `tools/list` in meta-tool mode no longer panics when fewer real tools than meta-tools are connected (saturating arithmetic).
- **2 new audit redaction tests** (31 total); live smoke test verified end-to-end.

</details>




- **Constant-Time API Key Verification** β€” Key comparison is no longer vulnerable to timing side-channels.
- **Trusted Role Binding** β€” Multi-tenant API keys now bind their RBAC role at the middleware layer (`X-MCPlex-Role` injected server-side). Clients can no longer self-assert a role via `_mcplex_role` when authenticated.
- **Tool Call Input Validation** β€” Tool names are restricted to a safe charset (max 128 chars); arguments are capped at 64KB with a max JSON nesting depth of 16. Malformed calls are rejected before touching upstream servers.
- **Request Body Limit** β€” Gateway rejects request bodies over 1MB.
- **Security Telemetry** β€” New `security_events`, `blocked_tool_calls`, and `rejected_tool_calls` counters, surfaced as dashboard cards plus a live security-posture pill (RBAC/Audit status) in the header.
- **VS Code / GitHub Copilot Integration** β€” Documented `.vscode/mcp.json` setup with a ready-to-copy template ([examples/vscode-mcp.json](examples/vscode-mcp.json)) for token-saving meta-tool routing inside IDE sessions.
- **Dynamic Version Banner** β€” CLI banner and dashboard header now display the crate version automatically.

</details>

<details>
<summary>Previous (v0.3.1 β€” Hardening)</summary>

- **Shared HTTP Client** β€” Replaced per-call `reqwest::Client::new()` with a pooled static client. Enables HTTP/2 connection reuse, TLS session resumption, and 30s request timeouts.
- **Circuit Breaker Config** β€” `circuit_breaker_max_crashes`, `circuit_breaker_window_secs`, and `request_timeout_secs` are now configurable in `[security]` (previously hardcoded).
- **Env-Var Expansion Fix** β€” `${ENV_VAR}` expansion now uses a single-pass cursor instead of a `while`-loop, preventing infinite loops if a resolved value itself contains `${`.
- **Rate Limiter Memory Fix** β€” Stale client IP entries are now pruned every 100 checks (entries idle >5 min), preventing unbounded HashMap growth.

</details>

---

<div align="center">

**Built with πŸ¦€ Rust for the AI agent community**

*If MCPlex saves your context window, give it a ⭐*

</div>

TDQS

A4/5.0

Scored across 2 tools

Disambiguation5/5

The two tools have clearly distinct purposes: one searches for tools by description, the other lists categories. There is no overlap.

Naming Consistency5/5

Both tools follow a consistent verb_noun pattern with the 'mcplex_' prefix: find_tools and list_categories.

Tool Count3/5

With only 2 tools, the server feels minimal for a discovery service. The referenced mcplex_call_tool is missing, making the count seem incomplete.

Completeness2/5

The set lacks the mcplex_call_tool mentioned in mcplex_find_tools description, leaving an obvious gap for actually executing tools.

Maintenance

ActivityMaintained
ResponsivenessWithin a week