OmniMCP Router
by Simonc44
README.md
<div align="center">
# OmniMCP Router
### The Universal MCP Gateway — One Entry Point to Rule All Your AI Tools
*Multi-role auth, hot-reload, auto-healing, Prometheus metrics*
[](https://github.com/Simonc44/OmniMCP/releases)
[](https://www.python.org/downloads/)
[](https://modelcontextprotocol.io)
[](LICENSE)
[](https://github.com/Simonc44/OmniMCP/actions)
[](https://github.com/psf/black)
*Plug any MCP server. Claude sees them all as one.*
[Features](#features) • [Quick Start](#quick-start) • [Configuration](#configuration) • [Tests](#tests) • [Client Setup](#client-setup) • [Contributing](#contributing)
</div>
---
## The Problem
You have 10 MCP servers: GitHub, Reddit, Notion, Stripe, a custom scraper…
Your Claude Desktop config is a mess. Each client can only talk to one server at a time.
Every crash brings everything down. There's no observability. No resilience.
**OmniMCP fixes all of that.**
---
## Features
| Feature | Description |
|---|---|
| **Zero Hard-Coded Tools** | Dynamically discovers tools from every sub-server at startup |
| **Async Non-Blocking Routing** | Parallel requests routed concurrently via `anyio` — no bottleneck |
| **Auto-Healing** | Exponential backoff reconnection when a sub-server crashes |
| **Hot-Reload** | Detects `mcp_router_config.json` changes live — no restart needed |
| **Hook System** | Mutate, intercept, and validate requests/responses in middleware pipelines |
| **Performance Monitoring** | Real-time profiling with `PERF_WARNING` for tools exceeding 5s |
| **Isolated Lifecycle** | Each sub-server has its own `AsyncExitStack` — one crash ≠ global failure |
| **Safe Namespacing** | Tools exposed as `{server}__{tool}`, sanitized to MCP spec (`[a-zA-Z0-9_-]{1,64}`) |
| **Persistent Logging** | All logs written to `mcp_router.log` + `stderr` (captured by Claude) |
| **JSON Schema Validation** | Strict input validation before forwarding any tool call |
| **Response Truncation Hook** | Auto-truncates responses >50k chars to protect context windows |
| **Built-in Status Tool** | `__router__status` exposes live server health + per-tool usage stats |
| **Per-Server Timeout** | Optional `timeout` (seconds) prevents a hung sub-server from freezing the client |
| **Multi-Transport** | Exposez le routeur en `stdio`, **Streamable HTTP** (`/mcp`) ou **WebSocket** (`/ws`) |
| **Prometheus Metrics** | Endpoint `/metrics` : compteurs/gauges/histogrammes d'utilisation des outils |
| **Bearer Auth** | Protégez les transports `/mcp` et `/ws` par `Authorization: Bearer <token>` |
| **Pluggable Hooks** | Chargez des hooks depuis des modules Python externes via la config (`hooks`) |
| **Docker Ready** | `Dockerfile` + `docker-compose.yml` pour un déploiement conteneurisé |
| **Windows + Linux** | Signal handling for both platforms |
---
## Quick Start
```bash
# 1. Clone the repo
git clone https://github.com/Simonc44/OmniMCP.git
cd OmniMCP
# 2. Install dependencies (Python 3.10+ required)
pip install -r requirements.txt
# 3. Edit your config
notepad mcp_router_config.json # Windows
# or: nano mcp_router_config.json
# 4. Run it
python router.py --config mcp_router_config.json # stdio (default, for Claude Desktop / Cursor)
# Or serve it over the network:
python router.py --config mcp_router_config.json --transport http --port 8000 # Streamable HTTP at /mcp
python router.py --config mcp_router_config.json --transport websocket --port 8000 # WebSocket at /ws
# Or run it in a container:
docker compose up --build
```
> **That's it.** OmniMCP starts, connects to all your sub-servers, and exposes a single unified MCP interface.
>
> The `http`/`websocket` transports also expose a **Prometheus** endpoint at `/metrics` (tool call counts, latencies, server health).
---
## Project Structure
```
OmniMCP/
├── router.py # Core gateway — routing, healing, hot-reload, hooks
├── mock_server.py # Lightweight mock MCP server for testing
├── run_integration_test.py # stdio integration test suite (async, healing, hot-reload)
├── run_http_integration_test.py # HTTP transport integration test (/mcp + /metrics)
├── mcp_router_config.json # Production config — your real MCP servers go here
├── test_config.json # Test config — uses mock_server.py instances
├── tests/ # Pytest unit tests (sanitization, validation, collisions)
├── requirements.txt # Dependencies: mcp, pydantic, jsonschema, anyio + http/metrics
├── Dockerfile # Container image for the http transport
├── docker-compose.yml # One-command container deployment
├── docs/ # Architecture diagrams and assets
├── .github/
│ ├── workflows/ci.yml # GitHub Actions CI pipeline
│ └── ISSUE_TEMPLATE/ # Bug report & feature request templates
├── CHANGELOG.md # Version history
├── CONTRIBUTING.md # Contribution guide
└── LICENSE # MIT License
```
---
## Configuration
The config file follows the exact same syntax as `claude_desktop_config.json` — so you can **copy-paste** your existing Claude Desktop config directly.
```json
{
"mcpServers": {
"github": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"],
"env": { "GITHUB_PERSONAL_ACCESS_TOKEN": "ghp_xxxx" }
},
"reddit": {
"command": "python",
"args": ["C:/path/to/reddit/server.py"],
"env": {
"REDDIT_CLIENT_ID": "your_client_id",
"REDDIT_CLIENT_SECRET": "your_secret",
"REDDIT_USER_AGENT": "OmniMCP/1.0"
}
},
"trend-mining": {
"command": "python",
"args": ["-m", "trend_mining.server"],
"env": { "PLAYWRIGHT_HEADLESS": "true" }
}
}
}
```
Tools are exposed as `{server_name}__{tool_name}` — e.g. `github__create_issue`, `reddit__search_posts`.
An optional `timeout` (in seconds) per server guards against a hung sub-server freezing the client (default `120`):
```json
"github": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"],
"env": { "GITHUB_PERSONAL_ACCESS_TOKEN": "ghp_xxxx" },
"timeout": 60
}
```
### Hot-Reload
OmniMCP watches your config file every 2 seconds. Add, remove, or modify a server — it reconnects live and sends `notifications/tools/list_changed` to your client. **No restart needed.**
---
## Tests
The integration test suite validates the full feature set end-to-end:
```bash
python run_integration_test.py
```
| # | Test | What it validates |
|---|---|---|
| 1 | **Aggregation** | All tools from all sub-servers are discovered and exposed |
| 2 | **Async Parallelism** | Two 2s calls finish in ~2s total, not 4s |
| 3 | **Perf Monitoring** | A 6s call triggers `PERF_WARNING` in logs |
| 4 | **Auto-Healing** | Server crash → automatic reconnect → *real* successful tool call (not a silent error) |
| 5 | **Hot-Reload** | Config change → `list_changed` notification → updated tool list |
| 6 | **Status Tool** | `__router__status` returns live health + routing stats |
Additional suites:
```bash
python -m pytest tests/ -q # Unit tests (sanitization, validation, collisions, hooks, auth)
python run_http_integration_test.py # HTTP: /mcp, /metrics, auth legacy, multi-roles, hot-reload, PERF_WARNING (parallel)
```
The HTTP suite runs 5 tests in parallel, each on its own port:
| # | Test | What it validates |
|---|---|---|
| 1 | **HTTP Transport** | `/mcp` initialize, list, call + `/metrics` |
| 2 | **Bearer legacy** | 401 without/wrong token, success with token, `/metrics` protected |
| 3 | **Multi-role auth** | `read` blocked from calls, `write`/`admin` allowed, `/metrics` admin-only |
| 4 | **Hot-Reload HTTP** | Config change → `notifications/tools/list_changed` → tool list updated live |
| 5 | **PERF_WARNING HTTP** | A 6s call over HTTP triggers the `PERF_WARNING` log |
---
## HTTP / WebSocket Deployment
When using `--transport http` or `--transport websocket`, the router exposes:
| Endpoint | Description |
|---|---|
| `POST /mcp` (and `GET` for SSE) | MCP Streamable HTTP transport — point your MCP client here |
| `/ws` | MCP WebSocket transport |
| `/metrics` | Prometheus metrics (tool calls, latencies, server health) |
Connect any Streamable-HTTP-capable MCP client to `http://<host>:8000/mcp`. In your config you can fine-tune
connection per server (`timeout`) and load external hooks.
### Authentication — Multi-Role Bearer
Protect the network transports with Bearer token authentication. Three role levels are supported:
| Role | `list_tools` | `call_tool` | `/metrics` |
|---|---|---|---|
| `read` | Visible | **Blocked** | **403 Forbidden** |
| `write` | Visible | Allowed | **403 Forbidden** |
| `admin` | Visible | Allowed | 200 OK |
Each user can optionally be restricted to specific sub-servers via `server_access`.
```json
{
"mcpServers": { },
"auth": {
"users": [
{ "name": "reader", "token": "tok-read", "role": "read" },
{ "name": "writer", "token": "tok-write", "role": "write", "server_access": ["github"] },
{ "name": "admin", "token": "tok-admin", "role": "admin" }
]
}
}
```
**Legacy mode** — a single token (admin by default):
```json
{ "auth": { "bearer_token": "my-secret-token" } }
```
Or via the environment variable `OMNIMCP_BEARER_TOKEN` (config key takes precedence).
Every request to `/mcp` and `/ws` must include `Authorization: Bearer <token>` (returns `401` otherwise).
Without any `auth` config the network transports are open.
### Pluggable Hooks
Load request/response hooks from external Python files without modifying the router. In `mcp_router_config.json`:
```json
{
"mcpServers": { },
"hooks": ["path/to/my_hooks.py"]
}
```
The referenced module must expose a `register_hooks(hook_system)` function:
```python
def register_hooks(hook_system):
@hook_system.register_request_hook
async def inject_tenant(server, tool, args):
args["tenant"] = "acme"
return args
```
---
## Client Setup
### Claude Desktop
Replace your entire `claude_desktop_config.json` with just OmniMCP:
```json
{
"mcpServers": {
"omni-mcp": {
"command": "python",
"args": [
"C:/path/to/OmniMCP/router.py",
"--config",
"C:/path/to/OmniMCP/mcp_router_config.json"
]
}
}
}
```
### Cursor
In Cursor MCP settings, add a stdio server:
- **Name**: `OmniMCP`
- **Command**: `python C:/path/to/OmniMCP/router.py --config C:/path/to/OmniMCP/mcp_router_config.json`
---
## Hook System
OmniMCP ships with a middleware pipeline for request/response mutation:
```python
# Register a custom request hook (e.g. inject auth)
@gateway.hook_system.register_request_hook
async def inject_auth(server_name: str, tool_name: str, arguments: dict) -> dict:
if server_name == "my-api":
arguments["api_key"] = os.environ["MY_SECRET_KEY"]
return arguments
# Register a custom response hook (e.g. redact PII)
@gateway.hook_system.register_response_hook
async def redact_pii(server_name, tool_name, result):
# process result.content here
return result
```
Built-in hooks:
- **Response Truncation** — auto-truncates responses >50,000 chars with a clear notice
---
## Resilience Architecture
```
┌──────────────────────────────────────────────────────┐
│ Claude Desktop / Cursor │
└──────────────────────┬───────────────────────────────┘
│ stdio (single MCP connection)
┌──────────────────────▼───────────────────────────────┐
│ OmniMCP Router │
│ ┌─────────────────────────────────────────────────┐ │
│ │ Hook System │ Schema Validator │ Profiler │ │
│ └─────────────────────────────────────────────────┘ │
│ ┌──────────────┐ ┌──────────────┐ ┌────────────┐ │
│ │SubServer A │ │SubServer B │ │SubServer C │ │
│ │ connected │ │ reconnecting│ │ connected│ │
│ │Auto-Healing │ │Backoff: 4s │ │ │ │
│ └──────────────┘ └──────────────┘ └────────────┘ │
└──────────────────────────────────────────────────────┘
```
If Sub-Server B crashes:
- Its tools are hidden from the tool list
- A reconnect loop starts with exponential backoff (1s → 2s → 4s → 8s → 16s)
- On success: tools reappear, client gets `notifications/tools/list_changed`
- After 5 failed attempts: marked `failed`, loop stops
- **Sub-Servers A and C are completely unaffected**
---
## Contributing
PRs are welcome! See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
1. Fork the repo
2. Create your branch: `git checkout -b feat/my-feature`
3. Run tests: `python run_integration_test.py`
4. Open a PR against `main`
---
## Changelog
See [CHANGELOG.md](CHANGELOG.md).
## Releases
- **v1.1.0** (2026-08-28) — multi-role auth, Streamable HTTP + WebSocket transports, Prometheus metrics, Docker, full test suite. See the [release notes](https://github.com/Simonc44/OmniMCP/releases/tag/v1.1.0).
- **v1.0.0** (2025-07-14) — core gateway: aggregation, auto-healing, hot-reload, hooks, perf monitoring, stdio transport.
---
## License
MIT — see [LICENSE](LICENSE).
---
<div align="center">
Made for the MCP ecosystem
*If this saved you hours, drop a star*
</div>
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues