Configuration

The MCP tool surface is pinned at the nine task-shaped tools (frozen contract: docs/tool-contracts.md). Every other knob — ranking weights, fusion algorithm, embedder identity, reference-graph capture toggles, chunking strategies, output limits, formatter choice — lives in AppConfig (pydantic-settings) with layered defaults:

shipped defaults/default_config.yaml   (lowest priority)
  → shipped pipeline blueprints (pipelines/*.yaml)
  → your overlay — the first of: --config PATH → PYDOCS_CONFIG_PATH
      → ./pydocs-mcp.yaml → ~/.config/pydocs-mcp/config.yaml
  → env vars (PYDOCS_*)                (highest priority)

The overlay is auto-discovered: with no --config flag and no PYDOCS_CONFIG_PATH, a pydocs-mcp.yaml in the current directory wins, falling back to ~/.config/pydocs-mcp/config.yaml, then to the shipped baseline alone.

This keeps MCP clients (Claude Code, Cursor, IDE extensions) stable across deployments while giving you per-project experiment tracking: two YAMLs produce two comparable retrieval runs with nothing rebuilt client-side. (This is exactly what the benchmark harness exploits.)

Shipped blueprints

  • pipelines/chunk_search_graph.yaml / …_graph_ranked.yamldefault chunk search: dense retrieval + graph_expand reference-graph expansion.

  • pipelines/chunk_search.yaml — BM25 chunk search (the former default).

  • pipelines/chunk_search_ranked.yaml — BM25, ranked top-K (no composite collapse).

  • pipelines/chunk_search_dense.yaml / …_dense_ranked.yaml — dense retrieval.

  • pipelines/chunk_search_hybrid.yaml / …_hybrid_ranked.yaml — BM25 + dense fused via RRF.

  • pipelines/decision_search.yaml — an active default route, not an opt-in preset: kind=decision queries route here via the kind_is_decision predicate (BM25 ∥ dense over mined decision records, RRF-fused).

  • pipelines/chunk_search_deps.yaml — also an active default route: scope=deps queries route here via the scope_is_dependencies_only predicate (BM25 over all dep chunks ∥ dense over their doc pages, RRF-fused).

  • pipelines/chunk_search_late_interaction.yaml / …_late_interaction_ranked.yaml — late-interaction (ColBERT / PyLate) MaxSim re-scoring ([late-interaction] extra).

  • pipelines/tree_only.yaml — LLM tree-reasoning only (vectorless).

  • pipelines/chunk_search_with_tree_reasoning_parallel.yaml — hybrid + LLM tree-reasoning in a parallel branch, fused downstream.

  • pipelines/chunk_search_with_tree_reasoning_after.yaml — hybrid first, then LLM tree-reasoning as a downstream reranker.

  • pipelines/member_search.yaml — default member search.

  • pipelines/ingestion.yaml — default ingestion (discovery → read → chunk → reference capture → flatten → content-hash → embed → package build).

  • pipelines/ingestion_late_interaction.yaml — ingestion with the multi-vector embed stage ([late-interaction] extra).

Pipeline schema

name: chunk_search
steps:
  - name: fetch                 # addressable + greppable
    type: chunk_fetcher         # a registered step type
    params: { schema_name: chunk }
  - name: score
    type: bm25_scorer
    params: {}
  # …

Each step entry needs a name:, a registered type:, and params: matching the step’s dataclass fields.

Example overlay

# my-pydocs.yaml
extraction:
  chunking:
    markdown:
      max_heading_level: 4         # default: 3
search:
  output:
    default_limit: 20              # default: 10 — bounds the client `limit` param
                                   # (multi-repo unions); single-project count =
                                   # the pipeline YAML's limit step
reference_graph:
  capture:
    enabled: true
    kinds: [calls, imports, inherits, mentions]   # opt into MENTIONS
pydocs-mcp --config ./my-pydocs.yaml serve .    # --config is global — it precedes the subcommand
# or: PYDOCS_CONFIG_PATH=./my-pydocs.yaml pydocs-mcp serve .

Description surface, suggestions, and session-start context

The LLM-facing text and the deterministic nudges around it have their own knobs (decision records: docs/adr/00050008; authoring guide: docs/description-authoring.md):

Key

Default

Effect

serve.descriptions_path

null

Serve the tool descriptions / server instructions / session-start preamble from an override document instead of the packaged defaults/descriptions.md. Precedence: serve --descriptions PATH flag > PYDOCS_SERVE__DESCRIPTIONS_PATH env var > this key > packaged. An explicitly named source that is missing or invalid is a hard startup error — never a silent fallback.

serve.session_start_context.enabled

false

Inject the session-start context pack (marker line + preamble + overview card + installed-package version inventory) into the ask-your-docs agent prompt at agent-session start. pydocs-mcp session-start-context prints the same pack on demand regardless of the flag.

serve.session_start_context.budget_tokens

2000

Hard cap on the pack in real (tiktoken) tokens; the overview card is trimmed before the version inventory, and truncation is always noted in the pack.

output.suggestions.grep_zero_hit

true

Zero-hit grep responses append a fixed [suggestion: …] line redirecting conceptual queries to search_codebase.

output.suggestions.grep_truncated

true

Truncated grep responses append a fixed narrowing hint (path= / glob= / head_limit=).

output.suggestions.search_zero_hit

true

Zero-hit search_codebase / get_why responses append the get_overview pointer.

The suggestion lines are deterministic machinery with a fixed [suggestion: prefix — server-initiated nudges, distinguishable in any transcript from model-chosen routing. Each flag ablates one rule independently; with a flag off, that rule’s responses are byte-identical to output with no suggestion machinery at all. The startup log pins which description document a run served: descriptions artifact <hash12> source=packaged|<path>.

Every tunable is listed in python/pydocs_mcp/defaults/default_config.yaml — read it as the canonical reference.


Database schema

The SQLite file holds ten tables (schema v15): the six core tables diagrammed below plus chunk_multi_vector_ids, node_scores, decision_records, and index_metadata (covered in the read-path table underneath). The schema is versioned via PRAGMA user_version; known older versions are migrated in place with additive, data-preserving sweeps — only an unrecognized version drops the known tables and re-indexes from scratch. Dense vectors are not in SQLite — they live in the per-project .tq TurboQuant sidecar.

        erDiagram
    packages {
        TEXT name PK
        TEXT version
        TEXT content_hash "xxh3 of (path,mtime) pairs"
        TEXT origin "site-packages | __project__"
        TEXT local_path
    }
    chunks {
        INTEGER id PK
        TEXT package FK
        TEXT module
        TEXT title
        TEXT text "indexed by chunks_fts (FTS5)"
        TEXT content_hash
        TEXT source_path "v15: span provenance"
        INTEGER start_line
        INTEGER end_line
    }
    chunks_fts {
        FTS5 virtual "porter + unicode61 tokenizer"
    }
    module_members {
        INTEGER id PK
        TEXT package FK
        TEXT module
        TEXT name "function/class/method"
        TEXT kind
        TEXT signature
        TEXT docstring
    }
    document_trees {
        TEXT package PK
        TEXT module PK
        TEXT tree_json "DocumentNode tree"
        TEXT content_hash
    }
    node_references {
        TEXT from_package PK
        TEXT from_node_id PK
        TEXT to_name PK
        TEXT kind PK "CALLS | IMPORTS | INHERITS | MENTIONS | SIMILAR (opt-in) | GOVERNS"
        TEXT to_node_id "null if unresolved"
    }
    packages ||--o{ chunks                : has
    packages ||--o{ module_members        : has
    packages ||--o{ document_trees        : has
    packages ||--o{ node_references       : owns
    chunks   ||--|| chunks_fts            : indexed_by
    

Table

What it stores

Where it’s read

packages

One row per indexed package + the cache-skip content_hash. Project source is stored under the sentinel name = "__project__".

Indexing skip-check, get_overview (list packages)

chunks

Documentation + source chunks (markdown sections, docstrings, code blocks). content_hash powers the chunk-level reindex-skip; the dense vector for each chunk lives in the .tq sidecar, not in this row.

search_codebase(query, kind="docs") via FTS5 + dense scoring

chunks_fts

FTS5 virtual table mirroring chunks.title + chunks.text + chunks.package, with Porter stemming + unicode61.

search_codebase BM25 ranking (fused with dense via RRF)

module_members

Functions, classes, methods, attributes — name + signature + docstring + kind.

search_codebase(query, kind="api"), get_symbol(target)

document_trees

The hierarchical DocumentNode tree per module.

get_symbol(…, depth="tree")

node_references

The reference graph: one row per (from_node, to_name, kind) edge.

get_references(…, direction="callers"|"callees"|"inherits"|"impact"|"governed_by")

chunk_multi_vector_ids

Bridges chunk_id ↔ fast-plaid plaid_doc_id for late-interaction retrieval ([late-interaction] extra; the multi-vectors themselves live in fast-plaid’s on-disk index).

late_interaction_scorer retrieval step

node_scores

Per-symbol in-degree, PageRank, and Louvain community (opt-in via reference_graph.node_scores.enabled).

centrality_prior / community_diversity rerank steps, direction="impact" ranking

decision_records

Mined architectural-decision records.

get_why

index_metadata

Per-bundle stamp: project name/root, embedder identity, indexed_at.

Multi-repo loading (serve --workspace / --db) + the embedder guard

The schema is documented in python/pydocs_mcp/db.py.