Skip to main content
Glama
README.md
# Vault RAG MCP

**Local multilingual retrieval for Markdown knowledge bases, exposed through a
read-only Model Context Protocol server.**

![Vault RAG MCP retrieving a cited policy from an allowlisted Markdown vault](docs/demo.svg)

Point it at the smallest folder an assistant should be allowed to read. It
chunks Markdown by heading, generates embeddings locally, stores a persistent
SQLite index, and gives MCP clients four narrow tools that return evidence with
paths and line numbers.

No API key. No write tool. No bulk upload of the corpus.

> Privacy boundary: indexing and embeddings stay local. If the MCP host uses a
> cloud model, the passages it retrieves can still be sent to that model. This
> project reduces the data exposed per answer; it does not turn a cloud LLM into
> a local one.

## Why this exists

Giving an assistant filesystem access is easy. Giving it the **minimum useful
access** is the engineering problem.

Vault RAG MCP separates retrieval from generation:

```text
ALLOWLISTED MARKDOWN ROOT
          │
          ▼
 heading-aware chunker ──▶ multilingual local embeddings
          │                         │
          └──────────────▶ SQLite vector index
                                      ▲
                                      │ cosine ranking
MCP HOST ──▶ search_vault(query) ─────┘
    │                 │
    │                 └── path + heading + exact line range
    └────▶ read_vault_note(path, lines)

Every operation ──▶ private JSONL audit trail
```

The server never calls an LLM. Claude, another MCP host, or a local model owns
answer generation; this process only retrieves source evidence.

## Verified sample run

The included five-note corpus produces 14 heading-aware chunks with the default
model. A real offline stdio smoke run produced:

| Check | Result |
|---|---|
| Protocol discovery | All four tools listed and callable |
| Incremental refresh with no changes | 5 unchanged files, 0 rewritten chunks, 2 ms |
| English policy query | `Projects/Support-Playbook.md`, lines 11–13, score `0.8703` |
| English query over a Spanish note | `Projects/Asistente-Universidad.md`, lines 5–7, score `0.8460` |

Timings depend on hardware. Similarity scores rank passages; they are not
probabilities or guarantees of correctness.

## What it exposes

| Tool | Purpose | Source mutation |
|---|---|---|
| `vault_status` | Show index readiness and the enforced boundary | None |
| `refresh_vault_index` | Incrementally embed changed Markdown notes | None |
| `search_vault` | Rank passages semantically, optionally under a path prefix | None |
| `read_vault_note` | Read a bounded line range from one validated note | None |

Tool descriptions tell the host to treat note content as untrusted data and to
cite paths and line numbers.

## Quickstart

Requirements: Python 3.11–3.13 and [`uv`](https://docs.astral.sh/uv/). CI tests
all three supported Python versions on Linux; the release smoke run was also
verified locally on macOS. The project defaults to Python 3.12 in
`.python-version`, and `uv` can download it automatically.

```bash
git clone https://github.com/rickquant/vault-rag-mcp
cd vault-rag-mcp
uv sync
```

Build the included sample index:

```bash
uv run vault-rag-mcp index --vault sample_vault
```

The first run downloads `intfloat/multilingual-e5-small`. Source notes are
embedded on the same machine.

Search it:

```bash
uv run vault-rag-mcp search \
  "When should a delayed package be investigated?" \
  --vault sample_vault \
  --limit 3
```

Read the exact source lines returned by search:

```bash
uv run vault-rag-mcp read \
  Projects/Support-Playbook.md \
  --vault sample_vault \
  --start-line 10 \
  --end-line 14
```

## Connect it to Claude Desktop

Use absolute paths. Claude Desktop may launch stdio servers from an undefined
working directory, and GUI applications often do not inherit the same `PATH`
as a terminal.

Open **Claude Desktop → Settings → Developer → Edit Config** and add:

```json
{
  "mcpServers": {
    "vault-rag": {
      "command": "/ABSOLUTE/PATH/TO/uv",
      "args": [
        "--directory",
        "/ABSOLUTE/PATH/TO/vault-rag-mcp",
        "run",
        "vault-rag-mcp",
        "serve"
      ],
      "env": {
        "VAULT_ROOT": "/ABSOLUTE/PATH/TO/YOUR/MARKDOWN/FOLDER",
        "VAULT_MODEL_LOCAL_FILES_ONLY": "true"
      }
    }
  }
}
```

Fully quit and reopen Claude Desktop after changing the config.

Example prompt:

> Search my vault for the decision about local embeddings. Answer only from the
> retrieved evidence and cite the note plus line range.

## Connect it to Claude Code

```bash
claude mcp add vault-rag \
  --scope user \
  --env VAULT_ROOT=/ABSOLUTE/PATH/TO/YOUR/MARKDOWN/FOLDER \
  --env VAULT_MODEL_LOCAL_FILES_ONLY=true \
  -- /ABSOLUTE/PATH/TO/uv \
  --directory /ABSOLUTE/PATH/TO/vault-rag-mcp \
  run vault-rag-mcp serve

claude mcp get vault-rag
```

## Configuration

| Environment variable | Default | Meaning |
|---|---|---|
| `VAULT_ROOT` | required | The single allowlisted Markdown root |
| `VAULT_EMBEDDING_MODEL` | `intfloat/multilingual-e5-small` | Local Sentence Transformers model |
| `VAULT_EMBEDDING_DEVICE` | `cpu` | Torch device used for embeddings |
| `VAULT_MODEL_LOCAL_FILES_ONLY` | `false` | Refuse model-network access; enable after the model is cached |
| `VAULT_EXCLUDE_DIRS` | `.git,.obsidian,.trash,.venv,__pycache__,node_modules` | Directory names skipped recursively |
| `VAULT_MAX_FILE_BYTES` | `1000000` | Per-note indexing and read limit |
| `VAULT_MAX_READ_CHARS` | `20000` | Maximum text returned by one read |
| `VAULT_MAX_RESULTS` | `10` | Hard cap on search results |
| `VAULT_INDEX_PATH` | OS cache directory | SQLite index location |
| `VAULT_AUDIT_PATH` | OS state directory | JSONL audit location |
| `VAULT_AUDIT_QUERY_TEXT` | `false` | Store raw searches instead of hashes |

The default index and audit paths are namespaced by a hash of the resolved vault
root. The server refuses to place either file inside `VAULT_ROOT`.

The first model load needs network access. After that succeeds once, set
`VAULT_MODEL_LOCAL_FILES_ONLY=true` in the MCP server environment to make model
loading fail closed instead of checking the network.

## Security boundary

Enforced in code:

- One resolved root is the filesystem allowlist.
- Only `.md` files are scanned or read.
- Absolute paths, `..`, null bytes, non-Markdown files, oversized files, and
  every symlinked path are rejected.
- Hidden and configured directories are skipped.
- There are no create, edit, move, delete, shell, network, or arbitrary-path
  tools.
- Search returns at most 10 bounded excerpts; note reads are character-capped.
- The SQLite index and JSONL audit log are created with owner-only permissions
  where the operating system supports them.
- Audit records hash query text by default.

Not guaranteed:

- Retrieved passages may leave the machine through the MCP host's model
  provider.
- This server cannot constrain other tools enabled in the same host.
- Local processes running as the same operating-system user may read the source
  folder, index, or audit log.
- Semantic retrieval can miss relevant text. Source citations make results
  inspectable; they do not make ranking infallible.

See [docs/SECURITY.md](docs/SECURITY.md) for the threat boundaries and design
decisions.

## How indexing works

1. Scan Markdown files without following symlinks.
2. Remove YAML frontmatter from retrieval text.
3. Split each note by its Markdown heading hierarchy, then into overlapping
   bounded chunks.
4. Prefix passages and queries according to the multilingual E5 retrieval
   model.
5. Normalize vectors and store them as float32 blobs in SQLite.
6. On refresh, re-embed only files whose size or nanosecond modification time
   changed; remove deleted files.
7. Rank the in-memory matrix with cosine similarity and return inspectable
   source metadata.

This deliberate brute-force search is appropriate for personal and small-team
knowledge bases. A distributed vector database would add machinery without
improving this demo's real use case.

## Development

Tests inject a deterministic network-free embedder. They exercise incremental
indexing, retrieval, deletion, source reads, path attacks, audit behavior, and
all four tools through an in-memory MCP client/server transport.

```bash
uv sync --group dev
uv run ruff check .
uv run ruff format --check .
uv run pytest
```

To verify the packaged command through a real stdio subprocess after the model
is cached:

```bash
uv run python scripts/smoke_stdio.py
```

Current scope and non-goals live in [docs/SCOPE.md](docs/SCOPE.md).

## Limitations

- Manual or tool-invoked refresh; no filesystem watcher.
- One local user and one configured root.
- Markdown only.
- Stdio transport only in the portfolio release.
- No answer generator, UI, OAuth, cloud deployment, or write operations.
- Intended for small knowledge bases, not millions of chunks.

## License

MIT

TDQS

A3.7/5.0

Scored across 4 tools

Disambiguation5/5

Each tool has a clearly distinct responsibility: status, indexing, search, and reading. There is no overlap or ambiguity between actions an agent could confuse.

Naming Consistency4/5

Most tools follow a verb_noun pattern (refresh_vault_index, search_vault, read_vault_note). vault_status deviates slightly by using a noun phrase instead of a verb prefix, but the naming style is otherwise uniform and readable.

Tool Count5/5

Four tools is a well-scoped size for a focused RAG-over-vault server. Each tool covers a necessary part of the workflow without redundancy or bloat.

Completeness5/5

For a read-only RAG server, the set covers the full lifecycle: checking index health, refreshing the index, searching semantically, and reading source passages. No obvious missing operations are needed to accomplish its stated purpose.

Maintenance

ActivityStale
ResponsivenessNo issues