Meta MCP Server

An MCP server for intelligent tool routing, using a Qdrant vector database and LM Studio for embeddings.

Documentation

MCP Deck

CI

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 uvx and uv commands used throughout this README. If you only have pipx, run pipx install uv to get uvx.
  • Docker or Apple Container (macOS Apple Silicon) — needed to run Qdrant, which backs vector-based tool selection. Optional if you only ever use --no-setup against 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-transformers model 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.json or 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): the mcpdeck serve entry point — wraps MetaMCPServer in the MCP stdio protocol, publishes {server}__{tool} names, and provides find_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 left None rather 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 (start only; not used by serve)
  • 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.json or 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.

CommandPurpose
serveRun the MCP server over stdio for Claude Desktop/Code (see above)
startDashboard/full-stack mode with auto-setup + web UI (default command)
runStart the server without auto-setup or config auto-detection
validate-config FILEValidate a configuration file
list-strategiesList available tool-selection strategies
debug-vectorRun a test query against the vector search index
regenerate-embeddingsRecompute tool embeddings (--force to clear and rebuild)
init-configWrite a default mcpdeck.yaml
healthCheck 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-server env blocks)
  • Review child server configurations before use
  • Enable web_ui.auth_enabled (+ username/password) if the dashboard is reachable beyond localhost

Contributing

  1. Fork the repository and clone your fork
  2. uv sync --extra dev && uv run pre-commit install
  3. Create a feature branch, make your changes with tests (pre-commit runs Ruff format/lint and mypy on commit)
  4. ./scripts/check-all.sh before opening a PR

License

MIT License - see LICENSE file for details.