Skip to content

Latest commit

 

History

History
101 lines (88 loc) · 5.41 KB

File metadata and controls

101 lines (88 loc) · 5.41 KB

CLAUDE.md

Project-level instructions for working in this repo. Package mcpdeck, console script mcpdeck, Python 3.11+, dependency/task runner uv.

Dev commands

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 commit

Run the CLI locally with uv run mcpdeck <command> ... (see uv run mcpdeck --help).

Architecture map

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`

serve vs start (the distinction that matters)

  • mcpdeck serve — MCP server over stdio for Claude Desktop/Code or any MCP client. stdout carries 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_stdio sets web_ui.enabled = False regardless of config, since Gradio's launch() prints to stdout) — use mcpdeck start for 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 bare mcpdeck / mcpdeck --config x.yaml runs (main_uvx inserts the implicit start subcommand for top-level flags).
  • mcpdeck run — like start but 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().

Testing

  • Framework: pytest + pytest-asyncio, asyncio_mode = auto (see pytest.ini).
  • Markers (registered in pytest.ini, --strict-markers enforced): 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.py is a minimal real MCP child server (stdio JSON-RPC) used by the serve end-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.py casually — check what fixtures it defines before adding new ones elsewhere.

Conventions

  • Logging via mcpdeck.utils.logging.get_logger, not stdlib logging directly, in new code under server/, 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 sets model_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}$ (see server/mcp_stdio.py::public_tool_name) — dots are not legal in MCP tool names.