Self-hosted, MCP-first RAG backend over Obsidian vaults.
Project description
VaultRAG
A self-hosted, MCP-first RAG backend that coding agents query instead of loading docs into context — and can write back into.
Getting started · Architecture · CLI · MCP tools · Benchmarks · All docs
Point it at an Obsidian vault. It indexes your Markdown — plus PDFs, Mermaid, Excalidraw and OCR'd images — and serves cited snippets over MCP and REST. Your agent searches instead of reading files, and can persist what it learns as new notes that are git-committed and searchable in the same session.
Everything runs locally. No API key is required for anything.
uv sync
docker compose up -d qdrant
ollama pull qwen3-embedding:0.6b
uv run vaultrag project add mynotes --vault ~/Documents/MyVault
uv run vaultrag reindex --project mynotes
claude mcp add vaultrag -- uv run --directory "$PWD" vaultrag mcp
That's it — step-by-step version here.
Why
An agent that loads your docs into context burns tokens on everything it might need. An agent that searches loads only what it does need — and gets a citation it can follow.
VaultRAG is built around four opinions:
- The vault is the source of truth. Every index, vector and graph edge is a disposable
cache. Disaster recovery is one
reindex, and your knowledge stays as Markdown files in a folder you own. - Return snippets, not answers. The caller is already an LLM. Synthesizing server-side burns compute to lose information.
- Hybrid always. Dev notes are full of
ERR_CONN_RESETandMER-1422. Dense-only retrieval misses exact matches; BM25-only misses paraphrases. You need both. - Quality is measured, not asserted. A merge-blocking eval gate scores retrieval on every PR. Numbers on this page come from runs that were executed, not estimated.
Architecture
flowchart TB
V[("Obsidian vault(s)<br/><b>source of truth</b> · git-backed")]
subgraph ING["INDEXER"]
direction LR
W["watch<br/>+ debounce"] --> P["parse<br/>md · PDF · diagrams · OCR"] --> C["chunk<br/>heading tree"] --> E["embed<br/>Qwen3 + BM25"]
end
QD[("<b>Qdrant</b><br/>dense + BM25 · int8 · HNSW<br/>one collection, payload-partitioned")]
SQ[("<b>SQLite</b><br/>documents · links · jobs<br/>query_log · api_keys")]
subgraph RET["QUERY SERVICE"]
direction LR
H["hybrid<br/>+ RRF"] --> RR["rerank<br/><i>opt-in</i>"] --> PS["parent<br/>swap"] --> G["graph<br/>expand"] --> CO["cite<br/>+ compress"]
end
A["<b>Coding agent</b><br/>MCP stdio · MCP http · REST /v1"]
V --> ING --> QD
ING --> SQ
QD --> RET
SQ --> RET
RET -->|cited snippets| A
A -->|write_note| RET
RET -->|write → git commit → reindex| V
style V fill:#2d4a22,stroke:#5a8f4a,color:#fff
style QD stroke-dasharray: 5 5
style SQ stroke-dasharray: 5 5
The dashed stores are the point: delete both, reindex, and the system is fully rebuilt
from bare Markdown. Full system design →
What it does
| Hybrid retrieval | BM25 + dense vectors, fused with RRF in one Qdrant round-trip. Exact identifiers rank as well as paraphrases. |
| Structure-aware chunking | Heading-tree, not sliding windows. Every snippet cites path ▸ H1 ▸ H2 ▸ lines and carries a clickable obsidian:// link. |
| Parent-child context | Small chunks rank precisely; retrieval returns the whole enclosing section, reconstructed live from the vault. |
| The vault's own graph | Wikilinks become a retrieval signal (authority) and an exploration tool (related). Zero LLM calls, sub-millisecond. |
| Optional cross-encoder | bge-reranker-v2-m3 as never / auto / always — auto decides per query from retriever agreement, costing nothing to decide. |
| Token budgets | Every response respects budget_tokens. Near-dupe drop → MMR → per-doc cap → trim. Deterministic and free. |
| Write-back = agent memory | write_note / append_section / patch_frontmatter, git-committed and reindexed synchronously — searchable in the same session. |
| Multi-project tenancy | One deployment, many vaults. Server-side enforcement; a client filter can narrow, never widen. Verified adversarially. |
| Full-format ingestion | Markdown, PDF (Docling), Mermaid, Excalidraw, OCR'd images, frontmatter, callouts, dataview fields. |
| Live indexing | watchdog + debounce + content-hash manifest. Renames re-embed nothing. |
| Secrets never indexed | Non-overridable filename deny list + a two-tier content scan, enforced in code at scan and parse time. |
| Production posture | Scoped argon2 API keys, rate limits, Prometheus metrics, provisioned Grafana dashboard, five alert rules. |
Measured
| Retrieval gate (72 cases, frozen corpus) | recall@10 1.000 · MRR@5 0.892 · nDCG@10 0.920 |
With auto rerank |
MRR@5 0.946 |
| Memory at 100k chunks | ≈1.8 GB total (727 MiB containers + host Ollama) |
| Cold index, CPU only | 67 docs → 261 chunks in 326 s |
| Warm reindex, no changes | 95–139 ms |
| Graph expand · compression | p50 0.39 ms · 1.5 ms |
| Tests | 923, plus mypy --strict and a merge-blocking quality gate |
Methodology, the full record, and the known gaps →
Documentation
| Getting started | Install → vault → index → Claude Code. Step by step. |
| CLI reference | Every command and flag. |
| MCP tools | What your agent sees: search, open, related, overview, write tools. |
| REST API | /v1 endpoints, bodies, status codes. |
| Configuration | Every setting and why its default is what it is. |
| Architecture | System design and diagrams. |
| Indexing pipeline | The write path. |
| Retrieval pipeline | The read path, stage by stage. |
| Design decisions | Techniques, alternatives, and evidence. |
| Benchmarks | Measured quality, latency, memory. |
| Security model | Tenancy, secrets, keys, write-back safety. |
| Operations | Docker, metrics, alerts, backup, runbook. |
| Troubleshooting | Symptom → cause → fix. |
| Development | Tests, the eval gate, CI, conventions. |
| Resources | Every external link, and the map to the design vault. |
Stack
| Layer | Choice |
|---|---|
| Vector + keyword | Qdrant — one collection, named vectors dense + bm25, int8, payload-partitioned |
| Dense embeddings | Qwen3-Embedding-0.6B via Ollama (1024d, Matryoshka-truncatable) |
| Sparse | BM25 via FastEmbed → Qdrant sparse vectors |
| Reranker | bge-reranker-v2-m3, ONNX int8, opt-in |
| Metadata + graph | SQLite (WAL) → Postgres 16; wikilink graph = links table + recursive CTEs |
| Parsing | markdown-it-py + python-frontmatter + custom Obsidian rules; Docling for PDFs |
| Jobs | In-process asyncio + a SQLite jobs table |
| API / MCP | FastAPI + FastMCP — one image serves REST and MCP (stdio & http) |
| Deploy | Docker Compose, one box |
Every vendor sits behind a protocol (Embedder, Reranker, VectorStore, MetaStore), so
a paid API is a config change, never a code change.
Why each was chosen →
Deployment
One image, two roles (VAULTRAG_ROLE=api|indexer), three services.
ollama pull qwen3-embedding:0.6b # Ollama stays on the HOST (GPU)
docker compose run --rm api project add mydocs --vault /vaults/my-notes
docker compose run --rm api reindex --project mydocs
docker compose up -d --build
docker compose logs api | grep -i "api key" # bootstrap key, printed once
curl -H "Authorization: Bearer $VAULTRAG_KEY" \
-H 'Content-Type: application/json' -d '{"query":"how does chunking work","k":5}' \
http://127.0.0.1:8000/v1/mydocs/search
Optional observability profile:
docker compose --profile observability up -d
open http://127.0.0.1:3000 # Grafana — dashboard provisioned
The API publishes on 127.0.0.1 only; remote access is Tailscale/WireGuard, not an open
port. The vault's git remote is the backup — the volumes hold a derived cache.
Full deployment guide →
Development
uv sync # install
uv sync --extra pdf # + Docling/OCR (heavy: torch)
uv run pytest # 923 tests
uv run vaultrag eval # the retrieval-quality gate
uv run ruff check src tests && uv run ruff format --check src tests && uv run mypy src
Any change to chunking parameters, the embedding model, fusion weights or rerank config must run the eval harness. Regressions block merge — that is what makes model swaps safe instead of vibes. Contributing guide →
Project state
P3 — production posture. Stages 1–6 shipped: deterministic stage-7 compression, the
multi-project registry, the query log and its mining CLI, the REST /v1 transport, scoped
API keys, and the Docker deployment.
See MEMORY.md for exactly where things are, and CLAUDE.md if
you are an agent working in this repo.
The design and knowledge for this system live in an Obsidian vault at ../Rag — that vault
decides what to build and why, this repo executes it, and the running index is a
disposable cache derived from the vault.
MIT · built with Claude Code
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file vaultrag-0.1.0.tar.gz.
File metadata
- Download URL: vaultrag-0.1.0.tar.gz
- Upload date:
- Size: 1.4 MB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.28 {"installer":{"name":"uv","version":"0.11.28","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Arch Linux","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4d3d803b157d8da5687df3dd67c4cc42f2ed0e06c503c3c0a410cf740b229681
|
|
| MD5 |
87f7648085bdd02301c795751c693e06
|
|
| BLAKE2b-256 |
7b33c9d84ed5444cbe81d7e137fa668100357e0810a18bfb9909e1ad70700ea1
|
File details
Details for the file vaultrag-0.1.0-py3-none-any.whl.
File metadata
- Download URL: vaultrag-0.1.0-py3-none-any.whl
- Upload date:
- Size: 293.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.28 {"installer":{"name":"uv","version":"0.11.28","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Arch Linux","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
36bfda23976a70142bcd6d5a3b4402cce1ca660889cc1bd514f7a9f1723232e7
|
|
| MD5 |
b1e9877189939ec5c4f0aa53117ae802
|
|
| BLAKE2b-256 |
12086ddcb6d22e0dc3f2cccd3fd4becddbf56a5b2370b7539854eefb028e94d2
|