Documentation MCP Server

Documentation MCP Server

Enables AI agents to search and query documentation from git repositories using hybrid search and structured metadata queries.

Category
Visit Server

README

Documentation MCP Server

An MCP server that indexes documentation from git repositories and makes it searchable by AI agents. Designed to run as a containerized service on a home server, providing documentation context to agents via the Model Context Protocol.

Architecture

Git Repos (local/remote)
        |
        v
  [Ingestion Worker]    Subprocess spawned per cycle (~5 min), parses markdown,
        |               chunks text, embeds; exits and releases RSS to the OS
        |
        v
  [Knowledge Base]      SQLite (WAL mode) for metadata
        |               ChromaDB sidecar (HTTP) for vector embeddings
        |
        v
  [MCP Server]          FastMCP with streamable HTTP transport. Long-running,
        |               isolated from ingestion's memory + GIL pressure.
        |
        v
  AI Agent (nanoclaw)   Queries docs via MCP tools

Docker Compose services

docker compose up -d brings up three containers:

Service Image Purpose
chroma chromadb/chroma:1.5.8 Owns /chroma-data exclusively. Serves vector queries on port 8000 over HTTP.
docserver ghcr.io/johnmathews/unified-documentation-server:latest The MCP server. Connects to chroma via HttpClient. Spawns the ingestion worker per tick.
documentation-webapp ghcr.io/johnmathews/unified-documentation-webapp:latest Optional web UI. Waits for docserver to become healthy before starting.

The chroma sidecar is required, not optional: chromadb >= 1.5.x corrupts its store when two PersistentClient instances open the same on-disk path, so the long-running server and the per-cycle ingestion worker need a single process owning the database. The HTTP server fills that role.

MCP Tools

search_docs -- Hybrid search with reranking

Find documentation relevant to a natural language question. Two-stage hybrid pipeline: BM25 (SQLite FTS5) + dense vector (ChromaDB) candidates fused with Reciprocal Rank Fusion, then reranked by a cross-encoder for final ordering. Returns chunk-level results.

Parameter Type Default Description
query str -- Natural language search query (required).
num_results int 10 Maximum number of results to return (1--100).
source str "" Optional source name to restrict results to one repo.

query_docs -- Structured metadata query

Query document metadata by source, path, title, or date range. Useful for questions like "list all docs in source Y" or "what was added after date Z".

Parameter Type Default Description
source str "" Filter by source name.
file_path_contains str "" Filter by substring in file path.
title_contains str "" Filter by substring in title.
created_after str "" ISO date string, e.g. "2024-01-01".
created_before str "" ISO date string.
limit int 20 Maximum number of results to return (1--100).

get_document -- Retrieve by ID

Retrieve a specific document or chunk by its ID. Document IDs follow the format source_name:relative/path for parent documents, or source_name:relative/path#chunkN for chunks.

Parameter Type Default Description
doc_id str -- The document ID to retrieve (required).

list_sources -- List sources and status

List all configured documentation sources and their indexing status. Returns source names, file counts, chunk counts, and last indexed time. Takes no parameters.

reindex -- Trigger re-indexing

Trigger an immediate re-indexing of documentation sources.

Parameter Type Default Description
source str "" Optional source name. If empty, re-indexes all sources.

Health Endpoint

GET /health returns the current status of the knowledge base, the most recent ingestion cycle, and the chat model configuration.

200 OK — server is reachable. The body is a structured snapshot:

{
  "status": "healthy",
  "total_sources": 3,
  "total_chunks": 542,
  "poll_interval_seconds": 1800,
  "sources": [ /* per-source health */ ],
  "last_ingestion": {
    "completed_at": "2026-04-29T17:25:00+00:00",
    "duration_s": 4.2,
    "rss_at_end_mb": 240.0,
    "flush_count": 3
  },
  "last_ingestion_failure": null,
  "chat_model_valid": true,
  "chat_model_error": null
}

Notable fields:

  • last_ingestion — duration and peak-RSS metrics from the most recent worker cycle. Populated only after the first cycle has run; null on a freshly started container.
  • last_ingestion_failure — set when the most recent worker subprocess exited non-zero, timed out, or did not emit a metrics line. Useful for spotting silent ingestion stalls without scraping logs.
  • chat_model_valid / chat_model_error — set by a startup probe that calls models.retrieve(DOCSERVER_CHAT_MODEL) on the Anthropic API. When false, /api/chat and /api/chat/stream short-circuit with HTTP 503 instead of letting every request fail at the API call.

503 Service Unavailable — knowledge base is unreachable or errored:

{"status": "error"}

This endpoint is used by the Docker health check configured in docker-compose.yml and by the webapp's depends_on: condition: service_healthy gate.

Quick Start

1. Configure sources

cp config/sources.example.yaml config/sources.yaml
# Edit config/sources.yaml to add your documentation repos

2. Set required secrets

The chat agent calls the Anthropic API. Either export ANTHROPIC_API_KEY in your shell before running compose, or write it into a local .env file (Docker Compose auto-loads .env from the project root):

echo "ANTHROPIC_API_KEY=sk-ant-..." >> .env

If you do not need the chat endpoints, set ANTHROPIC_API_KEY=unset (or any non-empty value) and skip them — the search and metadata MCP tools work without an Anthropic key.

3. Run with Docker Compose

docker-compose.yml is the canonical deploy file and it works out of the box: it brings up the three services with named volumes only, no host-specific bind mounts. If you want to index a directory that lives on the host filesystem, uncomment the example bind-mount stanza in the docserver service's volumes: block and add a matching sources: entry in config/sources.yaml whose path: points at the container-side mount.

docker compose up -d

This brings up three containers — chroma, docserver, and documentation-webapp — and two named volumes (chroma-data, docserver-data). The webapp waits for the docserver's /health to be green before starting; the docserver waits for the chroma sidecar to be reachable.

Host ports (per docker-compose.yml):

Service Host Container Notes
docserver 8085 8080 MCP and REST endpoints
documentation-webapp 3002 3000 Browser UI
chroma — 8000 Internal only; not exposed

4. Connect from an MCP client

Add to your MCP client configuration (e.g., .mcp.json):

{
 "mcpServers": {
  "documentation": {
   "url": "http://localhost:8085/mcp"
  }
 }
}

Updating to a new release

The latest tag on each image is overwritten on every push to main. To pull a fresh build:

docker compose pull              # pulls all 3 images
docker compose up -d             # recreates containers using the new images

The persistent volumes (docserver-data, chroma-data) are preserved across this — no re-ingestion is needed unless the sidecar's storage format has changed in a major Chroma upgrade. Roll back with docker compose pull --policy never plus an explicit older tag if a release is broken.

Persistent volumes

Volume Mounted at What it holds
docserver-data /data in docserver SQLite (documents.db), git clones (/data/clones/), cached ONNX embedding model (/data/models/)
chroma-data /chroma-data in chroma ChromaDB vector store (chunks + embeddings)

config/sources.yaml is bind-mounted read-only into the docserver container — edit it on the host and run docker compose restart docserver to pick up changes (see docs/operations.md § Configuration Changes).

Configuration

sources.yaml

sources:
 - name: "my-docs"
   path: "/repos/my-docs" # Local path (mount in docker-compose)
   branch: "main"
   patterns:
    - "**/*.md"

 - name: "remote-docs"
   path: "https://github.com/user/repo.git"
   branch: "main"

poll_interval: 1800 # Seconds between index cycles (default: 1800 = 30 min)
data_dir: "/data" # Persistent storage path

Environment Variables

Variable Default Description
DOCSERVER_CONFIG /config/sources.yaml Path to config file
DOCSERVER_DATA_DIR /data Persistent storage directory
DOCSERVER_POLL_INTERVAL 1800 Polling interval in seconds (default 30 min). Remote sources whose HEAD did not advance skip the file walk on each cycle.
DOCSERVER_HOST 0.0.0.0 Server bind address
DOCSERVER_PORT 8080 Server port
DOCSERVER_LOG_FORMAT json Log format (json or text)
DOCSERVER_LOG_LEVEL INFO Log level
DOCSERVER_CHAT_MODEL claude-opus-4-7 Anthropic model ID for the chat agent. Use a version-aliased ID; Anthropic does not publish a -latest alias for Opus 4.
DOCSERVER_CHROMA_HOST unset (compose: chroma) Hostname of the Chroma sidecar. Required in production; tests fall back to PersistentClient when unset.
DOCSERVER_CHROMA_PORT 8000 Port the Chroma sidecar listens on.
DOCSERVER_INGEST_NICE 10 (set by supervisor) Nice offset applied to each ingestion worker subprocess. Lower priority than the docserver process.
DOCSERVER_EMBEDDING_BATCH_SIZE 8 Chunks per ONNX inference call. Per-call activation memory scales with this — larger batches peak higher. 8 is sized for a 768 MB container cgroup; raise to 16/32 on hosts with more headroom.

See docs/operations.md for the full table including all options.

Development

uv sync --group dev
uv run pytest tests/ -v

How It Works

  1. Ingestion runs as a separate process. An IngesterSupervisor in the docserver process owns an APScheduler timer. On each tick (and on every POST /rescan), it spawns python -m docserver.ingestion_worker as a subprocess. The worker loads the embedding model, runs one cycle, and exits — its peak RSS is fully released to the OS, so the long-running docserver process stays at its small steady-state working set even when a cycle peaks high. If the worker OOMs or crashes, the docserver keeps serving requests; only the cycle is lost.

  2. Sync. The worker polls configured git repos. For remote repos, it clones on first run then pulls updates. For local repos (mounted as volumes), it pulls if they have a remote, or just reads the files directly.

  3. Parsing. Markdown files are parsed to extract titles (first # heading), creation dates (from git history), and modification times. Documents are split into ~400-character chunks at section and paragraph boundaries, with each chunk prefixed by its heading hierarchy (e.g. [Setup > Ports]) and ~100 chars of overlap between chunks. Lists and code fences are kept intact.

  4. Storage. Parent document metadata goes into SQLite (in WAL mode so the docserver can read while the worker writes). Each chunk lands in three places: the SQLite documents table (raw content + metadata), the chunks_fts FTS5 virtual table (BM25 inverted index over content + title), and the ChromaDB sidecar (dense embeddings). Only pre-computed vectors cross the wire to Chroma, so the sidecar stays small (~256 MB).

  5. Search. Two-stage hybrid pipeline. L1: SQLite FTS5 BM25 and ChromaDB cosine each return their top-100 candidates; results are merged with Reciprocal Rank Fusion (k=60) and the top 50 chunks pass to L2. L2: a cross-encoder (ms-marco-MiniLM-L6-v2, ONNX int8) reranks the candidates with full query–passage attention, then dedup-to-parent picks the best chunk per parent. Both models are pre-baked into the Docker image so cold start avoids any network download.

  6. Serving. The FastMCP server exposes tools over streamable HTTP. Agents can search hybrid-style, query by metadata, or retrieve specific documents. A /health endpoint returns indexing status (including the most recent worker cycle's RSS / duration) for container orchestration and operator visibility.

Recommended Servers

playwright-mcp

playwright-mcp

A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.

Official
Featured
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

An AI-powered tool that generates modern UI components from natural language descriptions, integrating with popular IDEs to streamline UI development workflow.

Official
Featured
Local
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

Enables interaction with Audiense Insights accounts via the Model Context Protocol, facilitating the extraction and analysis of marketing insights and audience data including demographics, behavior, and influencer engagement.

Official
Featured
Local
TypeScript
VeyraX MCP

VeyraX MCP

Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.

Official
Featured
Local
graphlit-mcp-server

graphlit-mcp-server

The Model Context Protocol (MCP) Server enables integration between MCP clients and the Graphlit service. Ingest anything from Slack to Gmail to podcast feeds, in addition to web crawling, into a Graphlit project - and then retrieve relevant contents from the MCP client.

Official
Featured
TypeScript
Kagi MCP Server

Kagi MCP Server

An MCP server that integrates Kagi search capabilities with Claude AI, enabling Claude to perform real-time web searches when answering questions that require up-to-date information.

Official
Featured
Python
E2B

E2B

Using MCP to run code via e2b.

Official
Featured
Neon Database

Neon Database

MCP server for interacting with Neon Management API and databases

Official
Featured
Exa Search

Exa Search

A Model Context Protocol (MCP) server lets AI assistants like Claude use the Exa AI Search API for web searches. This setup allows AI models to get real-time web information in a safe and controlled way.

Official
Featured
Qdrant Server

Qdrant Server

This repository is an example of how to create a MCP server for Qdrant, a vector search engine.

Official
Featured