Skip to main content
Glama
adeev-mardia

mcp-router

by adeev-mardia

mcp-router

A router/orchestrator that sits in front of multiple MCP (Model Context Protocol) servers and exposes them to a single client as one unified virtual MCP server.

The problem

An MCP client (Claude Desktop, an agent framework, a custom app) is normally wired up to talk to one MCP server per connection. As soon as you want tools from several servers at once — say a finance server for numbers and a notes server for scratch space — you're stuck either:

  • manually configuring every server in every client separately, or

  • writing custom glue in your agent to fan requests out to N different connections, worrying about tool name collisions (two servers both exposing search, get, list), and handling "one server is down" so it doesn't take the rest of your tool access with it.

mcp-router solves this by being a real MCP server itself. A client connects to it once, over stdio, and sees the union of every backend server's tools. The router transparently dispatches each call to the right backend, namespaces tool names so they never collide, and isolates backend failures so one dead server doesn't break the others.

Related MCP server: Easy MCP Proxy

Architecture, in words

                 ┌─────────────────────────┐
   MCP client    │        mcp-router            │
  (Claude, an  ─▶│  (itself a FastMCP server)    │
   agent, ...)   │                               │
                 │   ┌───────────────────────┐   │
                 │   │   BackendRegistry     │   │
                 │   │ - namespacing          │   │      ┌───────────────┐
                 │   │ - capability search    │◀──┘───────│ Backend: notes   │
                 │   │ - health tracking      │   │      │ (stdio subprocess│
                 │   │ - call routing         │   │      │  or in-process)  │
                 │   └───────────────────────┘   │      └───────────────┘
                 │             │                  │
                 │             ▼                  │      ┌───────────────┐
                 │      dispatch to the            │─────▶│ Backend: calc    │
                 │      right MCPBackend            │      │  ...             │
                 └────────────────────────┘      └───────────────┘
  • MCPBackend (backends.py) is a small async protocol — list_tools() / call_tool(name, arguments) — matching the shape of a real MCP server connection. Two implementations satisfy it:

    • StdioBackend — the production path. Launches a backend MCP server as a real subprocess and speaks MCP over stdio using the official mcp SDK's ClientSession, the same way Claude Desktop launches the servers in its own mcpServers config.

    • InProcessBackend — wraps a real mcp.server.fastmcp.FastMCP instance living in the same process (no subprocess needed). Useful for bundling a small backend directly into the router process, and it's what lets the test suite exercise real MCP server objects deterministically.

  • BackendRegistry (registry.py) is the routing brain: it registers named backends, aggregates their tools under a <backend>.<tool> namespace, resolves a call back to the right backend (explicitly, or by unambiguous bare-name lookup), offers keyword-based capability search across all backends, and tracks per-backend health so a failure in one backend never brings down calls to another.

  • RouterMCPServer (router_server.py) is a FastMCP subclass that overrides list_tools()/call_tool() to serve them dynamically from a BackendRegistry instead of static @mcp.tool() functions — this is what makes the router itself a real, connectable MCP server exposing the union of every backend's tools, plus two meta-tools: router.list_backends and router.search_tools.

  • config.py defines and loads a small config file (JSON, or YAML if pyyaml is installed) listing backend servers to launch, structurally similar to Claude Desktop's own mcpServers config.

Tool namespacing & routing

Every tool from backend notes with tool name add_note is exposed to the client as notes.add_note. This avoids collisions when two backends happen to expose tools with the same name (e.g. two servers both having a search tool). A client can:

  • call a tool explicitly: notes.add_note, calculator.add, ...

  • call a bare tool name (add_note) and have the router resolve it automatically, if and only if exactly one registered backend exposes a tool by that name — otherwise the router raises a clear "ambiguous, use the namespaced form" error rather than guessing.

  • use capability search (router.search_tools with a query keyword) to find which backend(s) expose something matching a name or description, useful for a client that doesn't know the backend layout in advance.

  • use router.list_backends to see every registered backend and whether it's currently healthy.

Health & failure isolation

BackendRegistry tracks per-backend health. If a backend's list_tools() or call_tool() raises, only that backend is marked unhealthy — list_all_tools(only_healthy=True) then omits its tools, and calls to other backends are entirely unaffected. A failure surfaces to the client as a normal MCP tool-call error (via isError=True on the CallToolResult, courtesy of the underlying MCP SDK), never as a crash of the router process.

Configuration

{
  "name": "example-router",
  "instructions": "Unified router exposing a notes server and a calculator server as one MCP server.",
  "backends": {
    "notes": { "command": "python3", "args": ["examples/notes_server.py"] },
    "calculator": { "command": "python3", "args": ["examples/calculator_server.py"] }
  }
}

Each entry under backends accepts command (required), args, env, and cwd — exactly what's needed to launch that backend as a subprocess over stdio, the same shape as Claude Desktop's mcpServers entries. Backend names may not contain . (reserved as the namespace separator) and may not be router (reserved for the router's own meta-tools).

Running it for real

pip install -e ".[dev]"

# Runs mcp-router as a real MCP server over stdio, launching both example
# backends as subprocesses and aggregating their tools.
mcp-router --config examples/router_config.json

Point any MCP client at that command (e.g. as an entry in Claude Desktop's own config, with mcp-router as the command and --config <path> as an arg) and it will see notes.add_note, notes.get_note, notes.list_notes, calculator.add, calculator.divide, calculator.is_prime, router.list_backends, and router.search_tools — all through one connection.

What the test suite actually proves — and what it doesn't

Be honest about this distinction, because it matters:

  • What's fully, deterministically proven by automated tests: all of the router's actual logic — namespacing, resolution (explicit and bare), capability search, failure isolation, health tracking, config parsing/validation, and the RouterMCPServer dispatch layer — tested against real mcp.server.fastmcp.FastMCP server objects via InProcessBackend. This is not a mock or a stub: it's the actual MCP SDK tool manager being called through the actual FastMCP list_tools/ call_tool methods. This is where the interesting bugs (namespacing edge cases, error isolation, resolution ambiguity) actually live, and it's fully covered.

  • What additionally has real, non-skipped coverage in this environment: StdioBackend was tested against the two example servers spawned as real subprocesses speaking MCP over stdio (tests/test_stdio_backend.py) — the literal production code path — and it worked reliably here, so those tests run for real rather than being skipped. Subprocess spawning can be flakier in some sandboxes/CI runners than plain in-process calls; if you see those specific tests skipped or flaking on your machine, that's a sandboxing/CI limitation, not a statement that StdioBackend doesn't work — the code path is identical to what mcp-router uses when you run it for real (see "Running it for real" above), and manual mcp-router --config examples/router_config.json` connected to a real client is the strongest way to confirm it end-to-end in your own environment.

Package layout

src/mcp_router/
  __init__.py        # public exports
  backends.py         # MCPBackend protocol, InProcessBackend, StdioBackend
  registry.py         # BackendRegistry: namespacing, search, health, routing
  router_server.py    # RouterMCPServer (FastMCP subclass) + config-driven runner
  config.py            # RouterConfig schema + JSON/YAML loader
examples/
  notes_server.py      # tiny demo FastMCP backend (3 tools)
  calculator_server.py # tiny demo FastMCP backend (3 tools)
  router_config.json   # example config wiring both of the above together
tests/                 # pytest + pytest-asyncio, deterministic, no network

Development

pip install -e ".[dev]"
python3 -m pytest -v

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    D
    maintenance
    Aggregates multiple MCP servers into a single endpoint, enabling LLM clients to access tools, resources, and prompts from various backends through one connection.
    4 npm
    MIT
  • A
    license
    Not graded
    quality
    B
    maintenance
    Enables aggregation, filtering, transformation, and composition of tools from multiple MCP servers through a single proxy with tool views.
    5
    AGPL 3.0
  • A
    license
    A
    quality
    A
    maintenance
    Enables AI harnesses to connect to a single MCP endpoint that routes to multiple downstream MCP servers, discovering and executing capabilities on demand while keeping tool schemas out of context.
    4
    66 npm
    Apache 2.0
  • A
    license
    Not graded
    quality
    A
    maintenance
    Enables clients to access multiple backend MCP servers through a single endpoint, with OAuth 2.1 authorization, namespaced tools, and secure credential management.
    1
    MIT