Meta MCP Server
An MCP server for intelligent tool routing, using a Qdrant vector database and LM Studio for embeddings.
Documentation
MCP Deck
Your command deck for MCP servers. MCP Deck is an intelligent MCP (Model
Context Protocol) router: it spawns your child MCP servers, embeds their
tools, and exposes them to an MCP client through a single connection —
either proxying every tool directly (namespaced) or, via the find_tools
meta-tool, letting the client ask "what tool should I use for X?" and get
back the most relevant ones instead of the whole list.
There are two ways to run it:
mcpdeck serve— an MCP server over stdio for Claude Desktop / Claude Code (or any MCP client). This is the integration most people want.mcpdeck start— a standalone dashboard/router process with a Gradio web UI, useful for development, debugging tool selection, and inspecting child-server health outside of an MCP client.
Install via uvx/uv tool install from git as shown below, or once a
tagged release is published to PyPI, uv tool install mcpdeck /
uvx mcpdeck.
Prerequisites
- Python 3.11+
- uv — provides
the
uvxanduvcommands used throughout this README. If you only havepipx, runpipx install uvto getuvx. - Docker or Apple Container (macOS
Apple Silicon) — needed to run Qdrant, which backs vector-based tool
selection. Optional if you only ever use
--no-setupagainst an already-running Qdrant, or don't need tool-selection routing at all. - LM Studio (optional) — for local embeddings and
LLM-based tool selection. Without it, MCP Deck falls back to a bundled
sentence-transformersmodel automatically.
Use with Claude Desktop / Claude Code
This is the mcpdeck serve path: an MCP server over stdio that exposes every
child tool as {server}__{tool} plus a find_tools meta-tool. stdout is
reserved for the JSON-RPC protocol — all logs and human-readable output go to
stderr, so this is safe to run under any MCP client's process supervisor.
Add to your MCP client config (Claude Desktop's
claude_desktop_config.json, or Claude Code's .mcp.json):
{
"mcpServers": {
"mcpdeck": {
"command": "uvx",
"args": [
"--from",
"git+https://github.com/anirudhlath/mcpdeck",
"mcpdeck",
"serve",
"--mcp-servers-json",
"/absolute/path/to/mcp-servers.json"
]
}
}
}
mcp-servers.json uses the same mcpServers shape Claude Desktop itself
uses, so you can point --mcp-servers-json at your existing Claude Desktop
config to re-expose the same child servers through MCP Deck's tool-selection
layer:
{
"mcpServers": {
"filesystem": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/files"]
}
}
}
Working from a local checkout instead of git+https (e.g. while developing)?
Point uv run --project at it instead of uvx:
{
"mcpServers": {
"mcpdeck": {
"command": "uv",
"args": [
"run",
"--project",
"/path/to/mcpdeck",
"mcpdeck",
"serve",
"--mcp-servers-json",
"/absolute/path/to/mcp-servers.json"
]
}
}
}
serve supports --setup (default --no-setup) if you want it to also
detect/start a container runtime and Qdrant before serving — see
mcpdeck serve --help. Restart Claude Desktop / Claude Code after editing
the config.
The Gradio web UI is disabled on the serve path even if your config sets
web_ui.enabled: true — Gradio's launch() prints to stdout, which would
corrupt the JSON-RPC channel. Use mcpdeck start when you want the dashboard.
find_tools and tool namespacing
Every child tool is published under {server_name}__{tool_name} (dots aren't
legal in MCP tool names, so server.tool becomes server__tool; any other
disallowed character is replaced with -, and the name is truncated to the
MCP-mandated 64 characters). Call these directly like any other MCP tool.
find_tools is a built-in meta-tool, always listed first, that runs MCP
Deck's intelligent selection (vector / LLM / RAG, depending on config and what
initialized successfully) against a natural-language query:
{"name": "find_tools", "arguments": {"query": "read a file from disk", "max_results": 5}}
It returns a JSON list of {"name": ..., "description": ..., "server": ...}
for the most relevant tools, which you then call directly by their namespaced
name. This is the main point of MCP Deck: instead of a client seeing every
tool from every child server at once, it can ask for just the ones relevant
to the current task.
Quick Start (dashboard mode)
Run the dashboard/router (start) straight from this repository with uvx:
# Automatic setup: detects Docker/Apple Container, starts Qdrant, opens the
# web UI on http://localhost:8080
uvx --from git+https://github.com/anirudhlath/mcpdeck mcpdeck
# With explicit config
uvx --from git+https://github.com/anirudhlath/mcpdeck mcpdeck \
--config my-config.yaml --mcp-servers-json my-servers.json
# Or install it as a persistent CLI tool
uv tool install git+https://github.com/anirudhlath/mcpdeck
mcpdeck
Running mcpdeck with no arguments (or with top-level flags like
--config/--web-ui, with no subcommand) runs start. On startup it will:
- Detect and set up a container runtime (Docker or Apple Container Framework)
- Start the Qdrant vector database (unless
--no-setup) - Auto-detect an existing
mcp-servers.jsonor Claude Desktop config in standard locations (read-only — it does not write or modify your Claude Desktop config) - Start the MCP Deck server with the web UI at
http://localhost:8080
Architecture
flowchart TD
subgraph Server["MCP Deck server"]
Engine["Routing engine<br/>(primary strategy + fallback)"]
Vector["Vector search router"]
LLM["LLM router"]
RAG["RAG router"]
Pipeline["RAG pipeline<br/>(doc chunking + retrieval)"]
Emb["Embedding service"]
Manager["Child server manager"]
Engine --> Vector
Engine --> LLM
Engine --> RAG
RAG --> Pipeline
Vector --> Emb
Pipeline --> Emb
Engine -->|selected tools / proxied calls| Manager
end
Client["MCP client<br/>(Claude Desktop / Claude Code)"] -->|"MCP over stdio<br/>(mcpdeck serve)"| Engine
Vector --> Qdrant[("Qdrant<br/>tool + doc embeddings")]
Pipeline --> Qdrant
Emb -->|primary| LMS["LM Studio<br/>embeddings + local LLM"]
Emb -.->|fallback| ST["sentence-transformers<br/>(local model)"]
LLM --> LMS
Pipeline --> LMS
Manager --> C1["Child MCP server<br/>(e.g. filesystem)"]
Manager --> C2["Child MCP server<br/>(e.g. github)"]
Manager --> C3["Child MCP server<br/>(...)"]
Main components (all under src/mcpdeck/):
- North-bound MCP server (
server/mcp_stdio.py): themcpdeck serveentry point — wrapsMetaMCPServerin the MCP stdio protocol, publishes{server}__{tool}names, and providesfind_tools - Server core (
server/meta_server.py): initializes and owns every other component; resilient startup means a failed embedding/vector-store/LLM/RAG component is logged as a warning and leftNonerather than crashing — child tools are still exposed even with no Qdrant/LM Studio running - Routing strategies (
routing/): vector search (vector_router.py), LLM selection (llm_router.py), and RAG-based selection (rag_router.py) - RAG pipeline (
rag/pipeline.py): chunks and indexes child-server documentation, retrieves relevant context, and augments selection queries - Embedding service (
embeddings/service.py): LM Studio embeddings when available, with automatic sentence-transformers fallback and local caching - Vector store (
vector_store/qdrant_client.py): Qdrant-based storage and similarity search for tool and documentation embeddings - Child server manager (
child_servers/): spawns and manages the lifecycle of downstream MCP servers and proxies tool calls to them - Web interface (
web_ui/): Gradio-based real-time monitoring and configuration dashboard (startonly; not used byserve) - Health / auto-setup (
health/): infrastructure detection, health checks, and automatic Docker/Apple Container + Qdrant setup
Features
Intelligent Tool Selection
- Vector Search (default): fast semantic similarity using embeddings
- LLM Selection: AI-powered tool selection using a local LLM (LM Studio)
- RAG-Based Selection: context-augmented selection using retrieved child-server documentation
Automatic Setup (start / --setup)
- Container runtime detection: Apple Container Framework on Apple Silicon macOS, or Docker elsewhere
- Starts Qdrant automatically
- Auto-detects an existing
mcp-servers.jsonor Claude Desktop config
Web Dashboard (start only)
- Real-time server monitoring and logs
- Interactive configuration editor
- Tool usage analytics and metrics
- Child server status monitoring
- Optional HTTP basic auth (
web_ui.auth_enabled+username/password; fails closed — the UI refuses to start if enabled without both credentials)
Configuration
Auto-Detection
mcpdeck start (and bare mcpdeck) looks for configuration files in these
locations when --config/--mcp-servers-json aren't given:
Main Config (mcpdeck.yaml):
./config/mcpdeck.yaml./mcpdeck.yaml~/.mcpdeck/config.yaml./config/meta-server.yaml(legacy, pre-rename)./meta-server.yaml(legacy, pre-rename)~/.meta-mcp/config.yaml(legacy, pre-rename)/etc/meta-mcp/config.yaml(legacy, pre-rename)
MCP Servers Config (JSON), read-only — never written to:
./mcp-servers.json~/Library/Application Support/Claude/claude_desktop_config.json(macOS)~/.config/claude/claude_desktop_config.json(Linux/Windows)~/.claude/claude_desktop_config.json
mcpdeck serve does not auto-detect a Claude Desktop mcp-servers.json
(pass --mcp-servers-json explicitly — see the Claude Desktop/Code section
above), but when --config is omitted it still searches the same main-config
locations as start, in the order listed above (falling back to built-in
defaults if none exist).
Creating Custom Config
mcp-servers.json (Claude Desktop format):
{
"mcpServers": {
"filesystem": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/files"]
},
"fetch": {
"command": "uvx",
"args": ["mcp-server-fetch"]
}
}
}
mcpdeck.yaml (every field is real and validated — unknown fields are
rejected; see examples/simple-config.yaml and examples/advanced-config.yaml
for complete, working examples):
strategy:
primary: "vector" # vector, llm, or rag
fallback: "vector" # fallback strategy
vector_threshold: 0.4 # similarity threshold
max_tools: 10 # max tools to return
web_ui:
enabled: true
port: 8080
auth_enabled: false # set true + username/password for basic auth
embeddings:
# Primary: LM Studio (optional). Canonical endpoint form ends in /v1 —
# /v1/ and /v1/embeddings are also accepted and normalized.
lm_studio_endpoint: "http://localhost:1234/v1"
lm_studio_model: "nomic-embed-text-v1.5"
# Fallback: local sentence-transformers model (automatic)
fallback_model: "all-MiniLM-L6-v2"
vector_store:
type: "qdrant"
host: "localhost"
port: 6333
Validate any config file before relying on it:
uv run mcpdeck validate-config path/to/mcpdeck.yaml
Commands
mcpdeck [OPTIONS] COMMAND [ARGS]...
Running mcpdeck with no subcommand, or with a top-level flag (e.g.
mcpdeck --config x.yaml --web-ui), routes to start.
| Command | Purpose |
|---|---|
serve | Run the MCP server over stdio for Claude Desktop/Code (see above) |
start | Dashboard/full-stack mode with auto-setup + web UI (default command) |
run | Start the server without auto-setup or config auto-detection |
validate-config FILE | Validate a configuration file |
list-strategies | List available tool-selection strategies |
debug-vector | Run a test query against the vector search index |
regenerate-embeddings | Recompute tool embeddings (--force to clear and rebuild) |
init-config | Write a default mcpdeck.yaml |
health | Check system health and dependencies |
Every command supports --help for its exact flags, e.g.
mcpdeck serve --help. When running via uvx, prefix these with
uvx --from git+https://github.com/anirudhlath/mcpdeck.
health
uv run mcpdeck health # text output, exits non-zero on issues
uv run mcpdeck health --output-format json
uv run mcpdeck health --fix --setup-docker --download-models
Docker
docker-compose.yml runs Qdrant plus the mcpdeck dashboard service
(built from the repo Dockerfile, using config/docker.yaml which binds the
web UI to 0.0.0.0:8080 and points vector_store.host at the qdrant
service):
docker-compose up -d
# Web UI: http://localhost:8080
# Qdrant: http://localhost:6333/collections
The container's CMD is mcpdeck start --no-setup --config /app/config/docker.yaml (Qdrant is provided by compose, so setup is skipped);
its HEALTHCHECK curls http://localhost:8080/ (the Gradio dashboard root —
there is no /health HTTP endpoint).
For running Qdrant via Apple's container framework instead of Docker, see
docs/apple-container-setup.md.
Development
git clone https://github.com/anirudhlath/mcpdeck.git
cd mcpdeck
uv sync --extra dev
uv run pre-commit install
uv run pytest
uv run ruff check src/ tests/
uv run ruff format src/ tests/
uv run mypy src/
# or all at once:
./scripts/check-all.sh
# Run the stdio server against a local checkout:
uv run mcpdeck serve --no-setup --mcp-servers-json path/to/mcp-servers.json --log-level DEBUG
# Run dashboard mode against a local checkout:
uv run mcpdeck start --log-level DEBUG
Tests are marked unit, integration (may spawn real subprocesses; no
Docker/Qdrant required — resilient init is exercised directly), and slow.
Troubleshooting
Qdrant connection failed
curl http://localhost:6333/collections
uv run mcpdeck health --setup-docker
Upgrading from before v0.2.0: vector-store point IDs and embedding cache keys changed (the old scheme used a per-process salted hash that produced duplicate points on every restart). Run this once after upgrading:
uv run mcpdeck regenerate-embeddings --force
No MCP servers found: create an mcp-servers.json file, or point
--mcp-servers-json at an existing Claude Desktop config.
Web UI not accessible: check the port isn't already in use
(lsof -i :8080) or pick another with --port.
LM Studio not being used: confirm the endpoint responds at
http://localhost:1234/v1/models, and that lm_studio_endpoint is set (it's
null/unset by default — the fallback sentence-transformers model is used
unless you configure it explicitly).
Logs: stderr in serve mode; ./logs/mcpdeck.log and the web UI's log
viewer in start/run mode (path from logging.file in your config).
Security Considerations
- Run child servers with minimal privileges
- Use environment variables for sensitive configuration (
${VAR}expansion in child-serverenvblocks) - Review child server configurations before use
- Enable
web_ui.auth_enabled(+username/password) if the dashboard is reachable beyond localhost
Contributing
- Fork the repository and clone your fork
uv sync --extra dev && uv run pre-commit install- Create a feature branch, make your changes with tests (pre-commit runs Ruff format/lint and mypy on commit)
./scripts/check-all.shbefore opening a PR
License
MIT License - see LICENSE file for details.