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/

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

# 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.2.0.tar.gz (139.0 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.2.0-py3-none-any.whl (32.3 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: mdvault-0.2.0.tar.gz
  • Upload date:
  • Size: 139.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.10.11 {"installer":{"name":"uv","version":"0.10.11","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.2.0.tar.gz
Algorithm Hash digest
SHA256 2f3f0c85df49a4079432060f43fc7e1bb4af580a18ba6230614c3f1500ac5aa6
MD5 c6dfe02d0d8d166b0f7ca738ec400e79
BLAKE2b-256 aef1ef6112451711220f2781211465c37e2a1222735f7f82dceff899018d281f

See more details on using hashes here.

File details

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

File metadata

  • Download URL: mdvault-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 32.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.10.11 {"installer":{"name":"uv","version":"0.10.11","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.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 49c84bba65523b81a1988830cd88c87c18e0a2b82f84fdcf601c371a33e5fc38
MD5 702ec2a1a41250cb0a9da7b0ffb194fb
BLAKE2b-256 5b81396332e76f1ebe5b7da66c7d533d8121656f0467c49a4431197c23fc9093

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