Skip to main content

CLI + MCP server for hybrid BM25 + semantic search over local Markdown vaults

Project description

mdvault

CI PyPI

Your Markdown notes, searchable in Claude Code.

Index any folder of .md files — Obsidian vault, ~/.claude/ history, project docs — and search them in natural language, directly from Claude Code or the terminal. Zero infrastructure. One .db file.

What it does

Search combines FTS5 BM25 and 256-dim vectors, fused with RRF and re-ranked on 7 signals (term coverage, heading match, path match, etc).

Each chunk carries its document context as a prefix ([path > title > heading]) before indexing, following Anthropic's contextual retrieval approach.

Other things worth knowing:

  • Parses Markdown links and [[wikilinks]], finds backlinks and similar files
  • Optional query expansion via local Ollama LLM
  • Memories decay over time, gain confidence when hit, and get auto-promoted from recurring queries
  • potion-base-8M embeddings, CPU-only, ~30MB download
  • Incremental indexing (SHA256 change detection, only reprocesses what changed)
  • MCP server so Claude Code can search your vault directly
  • Everything in one .db file (FTS5 index + vectors + link graph + metadata)

Quick Start

Paste install https://github.com/sderosiaux/mdvault in Claude Code — it will follow these steps.

1. Install

uv tool install mdvault

2. Index your notes

mdvault index ~/.claude/

# Keep rotated session logs in the index even after Claude Code deletes them
mdvault index ~/.claude/ --keep-deleted projects

3. Add MCP server to ~/.claude/mcp.json:

{
  "mcpServers": {
    "mdvault": { "command": "uvx", "args": ["mdvault", "serve"] }
  }
}

4. Restart Claude Code, then ask: "search my notes for kubernetes setup"

Keep the index fresh

Indexing is incremental (SHA256 change detection). Set up a cron to keep it updated:

# Every 30 minutes
(crontab -l; echo '*/30 * * * * uvx mdvault index ~/.claude/ 2>/dev/null') | crontab -

Other install methods

uvx mdvault --help              # run without installing
pipx install mdvault             # without uv

Example

$ mdvault search "memory system LLM"

[1] 0.983  .claude/projects/.../ae863d59.jsonl:70
### Dedicated Memory Platforms
- **Mem0**: Universal memory layer. $24M raised (YC-backed).
  41K GitHub stars, 13M+ PyPI downloads...

[2] 0.870  .claude/projects/.../agent-a581b10.jsonl:2
## The mapping: CPU, RAM, disk, I/O
Andrej Karpathy posted in October 2023 that LLMs should be
understood "not as a chatbot, but the kernel process of a new OS."

CLI Usage

# Index your notes (downloads ~30MB model on first run)
mdvault index ~/.claude/

# Incremental update (only changed/new/deleted files)
mdvault index ~/.claude/ --incremental

# Retain entries when matching files are removed from disk (repeatable).
# Pattern is a path prefix or fnmatch glob, relative to the vault root.
# --full also honors keep-deleted: it rebuilds from disk while preserving
# matching entries. Drop the flag to wipe everything.
mdvault index ~/.claude/ --keep-deleted projects --keep-deleted '*.jsonl'

# Search
mdvault search "nginx reverse proxy config"
mdvault search "ssh tunnel" --top-k 10

# Search with query expansion (requires Ollama running locally)
mdvault search "ssh tunnel" --expand
mdvault search "ssh tunnel" --expand --expand-model qwen3:0.6b  # default model

# Related notes: links, backlinks, and semantically similar files
# (file path is relative to the vault root)
mdvault related path/to/note.md

# Stats (includes memory & query analytics)
mdvault stats

# Store a memory (searchable alongside your files)
mdvault remember "Kafka timeout is controlled by max.poll.interval.ms"

# List stored memories (with confidence, hits, decay)
mdvault memories

# Show knowledge gaps (recurring queries with poor results)
mdvault gaps

# Delete a memory
mdvault forget <memory-id>

# Custom DB location
mdvault index ~/.claude/ --db ~/vault.db
mdvault search "query" --db ~/vault.db

Claude Code integration (MCP)

Add to ~/.claude/mcp.json:

{
  "mcpServers": {
    "mdvault": {
      "command": "uvx",
      "args": ["mdvault", "serve"],
      "env": {
        "VAULT_DB": "/absolute/path/to/vault.db"
      }
    }
  }
}

If VAULT_DB is omitted, defaults to ~/.local/share/mdvault/vault.db (Linux) or ~/Library/Application Support/mdvault/vault.db (macOS).

MCP tools exposed:

Tool Description
search_vault Hybrid BM25 + semantic search. Filter by vault, source, namespace
related_notes Links, backlinks, and semantically similar files for a given note
store_memory Store a memory (auto-chunked, searchable alongside files)
delete_memory Delete memories by id or namespace

Then ask Claude things like "search my notes for how I configured SSH tunnels" or "what notes are related to my kubernetes setup?".

Search pipeline

Query
  ├── FTS5 BM25 search  → top-75 (NEAR bigrams + focused AND clause)
  └── Vector search     → top-75 nearest neighbors
          │
          ▼
    Reciprocal Rank Fusion  (k=15, BM25 weight 4×)
          │
          ▼
    Re-ranking (7 signals)
      ├── Cosine similarity (continuous, from vec distance)
      ├── Query term coverage (squared, bonuses at ≥80% and 100%)
      ├── First-chunk coverage (intro paragraph = topic signal)
      ├── Heading match (H2/H3 vs query terms)
      ├── Title match (H1 vs query terms)
      ├── Path match (filename + parent dirs vs query terms)
      └── Overview boost (about/intro pages with high coverage)
          │
          ▼
    Content-hash dedup → top-N results

Files are split on ##/### headings (max 400 words, 50-word overlap, small sections merged). Each chunk gets a context prefix ([path > title > heading]) before embedding and FTS indexing.

Query expansion (--expand) calls a local Ollama model (default: qwen3:0.6b) to generate a paragraph a relevant document might contain, then appends it to the original query for vector search. BM25 always uses the raw query. Pull the model with ollama pull qwen3:0.6b.

Memories

Memories are searchable alongside files. Three things happen behind the scenes:

Decay. Memories fade over 180 days (floor 0.1). Every search hit resets the clock. Search for something regularly and it stays relevant. Stop, and it drops in ranking.

Confidence. Base score depends on source (user=0.7, agent=0.5, promoted=0.3) plus a log hit boost (capped at +0.3). Memories that keep getting matched climb higher.

Auto-promotion. Every search is logged and clustered by embedding similarity (cosine > 0.85). When a cluster hits 5+ occurrences:

  • Good results (avg score >= 0.3): the best result becomes a permanent memory
  • Bad results (avg score < 0.15): a knowledge gap is recorded
Search query
  ├── query_log + query_vec (logged)
  ├── hit tracking on memory results
  └── every 20 queries:
        ├── cluster_recent_queries (embedding similarity)
        └── maybe_promote (crystallize or flag gap)

Run mdvault gaps to see what you keep searching for but can't find.

Tech Stack

Component Library
Embeddings model2vec (potion-base-8M)
Vector search sqlite-vec
Full-text search SQLite FTS5
MCP server mcp (official Python SDK)
CLI typer
Linter ruff
Query expansion Ollama (optional)

Limitations

  • English-optimized: potion-base-8M is trained mostly on English. Semantic search degrades on other languages (BM25 keyword search still works)
  • Markdown only (no PDF, DOCX)
  • Exact vector search — scales to ~500k chunks on commodity hardware

Development

git clone https://github.com/sderosiaux/mdvault
cd mdvault
uv sync --dev
uv run pytest -q

Install pre-commit hooks (ruff lint + format):

uv run pre-commit install

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

mdvault-0.4.1.tar.gz (145.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

mdvault-0.4.1-py3-none-any.whl (37.5 kB view details)

Uploaded Python 3

File details

Details for the file mdvault-0.4.1.tar.gz.

File metadata

  • Download URL: mdvault-0.4.1.tar.gz
  • Upload date:
  • Size: 145.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.8 {"installer":{"name":"uv","version":"0.11.8","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for mdvault-0.4.1.tar.gz
Algorithm Hash digest
SHA256 d9ff5589018dd59b324b28d9f112859e1cb734e876e213bc31b7443d0eacf39c
MD5 58137e36664793496092fddaf15adc89
BLAKE2b-256 c7e553e6a9cf67f69016c4b2a0aa0284656196197e46830422f506444651edc0

See more details on using hashes here.

File details

Details for the file mdvault-0.4.1-py3-none-any.whl.

File metadata

  • Download URL: mdvault-0.4.1-py3-none-any.whl
  • Upload date:
  • Size: 37.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.8 {"installer":{"name":"uv","version":"0.11.8","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for mdvault-0.4.1-py3-none-any.whl
Algorithm Hash digest
SHA256 b7ba8bbceabded77fceaf58b34fe4f14bf6893d8aadbfc265c06ef1282452b2e
MD5 3fa3f9ef8aaabba22d0786a20aef731a
BLAKE2b-256 aec889cb255cb554956b49c9acb1b49b2d10eae59eebb6c43ae7b20e938bacc2

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page