Content
# kb-search-mcp
A local, read-only MCP server that gives Claude Code persistent memory by
searching a markdown wiki on demand instead of loading it whole at session
start.
Session-start cost on a 226-page wiki dropped from ~25k tokens to ~600
tokens + 1–3 pages retrieved on demand. About 95% reduction, same recall
on a 20-query benchmark (20/20 top-3 hits, no tuning).
## What it exposes
| Tool | Purpose |
|---|---|
| `kb_search(query, k, alpha)` | BM25 + vector retrieval with Reciprocal Rank Fusion |
| `kb_get_page(slug)` | Fetch a wiki page by slug |
| `kb_top_important(n)` | Top-N pages by frontmatter `importance_score` |
| `kb_index_status()` | Schema version, chunk/vector counts, last reindex |
The wiki stays the source of truth — markdown in a folder. SQLite (FTS5 +
sqlite-vec) is the derived index, regenerable from disk in ~2 minutes via
the companion indexer.
## Repo layout
```
kb-search-mcp/
├── mcp/ — the read-only MCP server (.NET 10)
├── indexer/ — the indexer that produces wiki.db (.NET 10 CLI)
├── sample-wiki/— a tiny seed wiki so you can index and search out of the box
└── README.md
```
## Quick start
```powershell
# 1. Clone and restore
git clone https://github.com/<your-handle>/kb-search-mcp
cd kb-search-mcp
# 2. Fetch the native sqlite-vec extension (one-time)
./indexer/scripts/fetch-vec0.ps1
# 3. Index the bundled sample wiki
$env:KB_SEARCH_PROJECT = "your-gcp-project"
dotnet run --project indexer/src/KbSearchIndexer -- --full --wiki ./sample-wiki
# 4. Wire the MCP server in your .mcp.json
{
"mcpServers": {
"kb-search": {
"command": "dotnet",
"args": ["run", "--project", "<repo>/mcp/src/KbSearchMcp", "--no-build"],
"env": { "KB_SEARCH_PROJECT": "your-gcp-project" }
}
}
}
```
## How it works
```
wiki/*.md → indexer → wiki.db (FTS5 + vec0) → MCP server → Claude Code
```
- **.NET 10** stdio MCP server, single self-contained binary
- **Microsoft.Data.Sqlite** with sqlite-vec loaded via `LoadExtension("vec0")`
- **Vertex AI** `text-embedding-005` (768d) for query embeddings, swappable
behind `IQueryEmbedder`
- **RRF** with k=60 (Cormack/Clarke/Buettcher 2009), default α=0.6 —
BM25-leaning because exact-token recall on technical jargon beat
conceptual recall in the benchmark
The indexer chunks pages on H2/H3 boundaries with an 800-token soft cap,
hashes content for incremental updates, and writes vectors as float32
BLOBs into vec0.
## Configuration
| Env var | Default | Purpose |
|---|---|---|
| `KB_SEARCH_PROJECT` | required | GCP project for Vertex embeddings |
| `KB_SEARCH_DBPATH` | `./wiki.db` | Override DB path |
| `KB_SEARCH_DEFAULTALPHA` | `0.6` | BM25 vs vector weight (0..1) |
The indexer also accepts CLI flags (`--wiki`, `--db`, `--model`, `--batch`)
that override the equivalent appsettings.
## Stack baseline
On a 226-page wiki: 1,931 chunks, 1,931 vectors, 10.5 MB DB.
Full reindex ~2 min. Incremental ~2 s.
## Known limits
- **Single-user.** WAL allows concurrent readers but the DB is a local file.
- **Index drift.** Edit the wiki without rerunning the indexer and search
returns stale hits. `kb_index_status` reports `lastReindexAt` so you can
detect this from the agent side.
- **No streaming.** Results return as a single payload.
- **Vertex dependency.** BM25-only fallback works without it but conceptual
recall drops. Drop in OpenAI / a local embedder via `IQueryEmbedder`.
## License
MIT. See `LICENSE`.
## Related
This server is the retrieval layer of a larger setup that gives Claude Code
persistent memory across sessions. The full kit — 23 production-ready
skills, a 200+-page seed wiki, auto-memory bootstrapping, and a
session-start playbook — is at
[draakaap.gumroad.com/l/second-brain](https://draakaap.gumroad.com/l/second-brain).