Project-level instructions for working in this repo. Package mcpdeck,
console script mcpdeck, Python 3.11+, dependency/task runner uv.
uv sync --extra dev # install deps (dev group: pytest, ruff, mypy, pre-commit)
uv run pytest # full test suite (unit + integration; no Docker/Qdrant needed)
uv run ruff check src/ tests/ # lint
uv run ruff format src/ tests/ # format
uv run mypy src/ # type check (strict-ish; config in .mypy.ini, must stay 0 errors)
./scripts/check-all.sh # all four in sequence
uv run pre-commit install # one-time; hooks run ruff + mypy on commitRun the CLI locally with uv run mcpdeck <command> ... (see uv run mcpdeck --help).
src/mcpdeck/
main.py # typer CLI: serve, start, run, health, validate-config,
# list-strategies, debug-vector, regenerate-embeddings, init-config
server/
mcp_stdio.py # MCPStdioServer: the `serve` north-bound MCP-over-stdio surface
meta_server.py # MetaMCPServer: owns all components, resilient initialize()/shutdown();
# also defines RoutingEngine (primary strategy + fallback)
child_servers/
manager.py # ChildServerManager: spawns/monitors child MCP server processes
client.py # ChildServerClient: JSON-RPC-over-stdio to one child
routing/
base.py # BaseRouter / FallbackRouter / SelectionContext / SelectionResult
vector_router.py # embeddings + Qdrant similarity search
llm_router.py # LM Studio-backed LLM tool selection
rag_router.py # RAG pipeline-backed selection
rag/pipeline.py # chunk/index/retrieve child-server docs
embeddings/service.py # LM Studio embeddings + sentence-transformers fallback + cache
vector_store/qdrant_client.py # Qdrant collections for tool/doc embeddings
llm/lm_studio_client.py # LM Studio OpenAI-compatible client
health/
checker.py # `mcpdeck health`
setup_manager.py # container runtime detection + Qdrant auto-setup (used by `start --setup`)
dependency_checker.py, docker_manager.py
web_ui/gradio_app.py # Gradio dashboard, used by `start`/`run` only, never by `serve`
config/
models.py # Pydantic models, extra="forbid" everywhere — no silently-ignored fields
loader.py # YAML + env-var expansion (${VAR}) + mcp-servers.json merge
utils/logging.py # StructuredLogger / get_logger, console-to-stderr routing for `serve`
mcpdeck serve— MCP server over stdio for Claude Desktop/Code or any MCP client.stdoutcarries only JSON-RPC — never print or log to stdout on this path. All human output goes to stderr (rich.console.Console(stderr=True);setup_logging(..., console_stderr=True)routes the logging console handler there too; file logging is unaffected). Defaults to--no-setup(assumes Qdrant is already up, or runs fine without it via resilient init). The web UI is force-disabled on this path (_serve_stdiosetsweb_ui.enabled = Falseregardless of config, since Gradio'slaunch()prints to stdout) — usemcpdeck startfor the dashboard.mcpdeck start— dashboard/full-stack mode: auto-detects config, defaults to--setup(Docker/Apple Container + Qdrant), web UI on by default. This is what baremcpdeck/mcpdeck --config x.yamlruns (main_uvxinserts the implicitstartsubcommand for top-level flags).mcpdeck run— likestartbut no auto-setup and no config auto-detection; the plain "just run it against this exact config" command.
Resilient init (MetaMCPServer.initialize): embedding service, vector store,
LLM client, and RAG pipeline each fail independently into a warning + None,
not a crash. Only the child manager and tool discovery are load-bearing for
serve to expose tools. Keep this property when touching initialize().
- Framework: pytest + pytest-asyncio,
asyncio_mode = auto(seepytest.ini). - Markers (registered in
pytest.ini,--strict-markersenforced):unit,integration(may spawn real subprocesses/child servers; must still pass with no Docker/Qdrant running — that's the point of resilient init),slow. tests/fixtures/stub_child_server.pyis a minimal real MCP child server (stdio JSON-RPC) used by theserveend-to-end test (tests/test_mcp_stdio.py::test_serve_stdio_end_to_end) — the closest thing to a smoke test that the product actually works.- Do not modify
tests/conftest.pycasually — check what fixtures it defines before adding new ones elsewhere.
- Logging via
mcpdeck.utils.logging.get_logger, not stdlibloggingdirectly, in new code underserver/,routing/,embeddings/,vector_store/,child_servers/. - Double-quoted strings, line length 88 (ruff-enforced).
- Config models live only in
config/models.py; every nested model setsmodel_config = ConfigDict(extra="forbid")— add it to any new model too. - Public MCP tool names are
{server_name}__{tool_name}, sanitized to^[a-zA-Z0-9_-]{1,64}$(seeserver/mcp_stdio.py::public_tool_name) — dots are not legal in MCP tool names.