leann-dotnet
2.5.6
dotnet tool install --global leann-dotnet --version 2.5.6
dotnet new tool-manifest
dotnet tool install --local leann-dotnet --version 2.5.6
#tool dotnet:?package=leann-dotnet&version=2.5.6
nuke :add-package leann-dotnet --version 2.5.6
LEANN .NET
A native .NET 10 MCP server for semantic code search. Chunks source repositories, computes embeddings via ONNX (GPU-accelerated), and serves results over the Model Context Protocol.
Ported from LEANN by Yichuan Wang — an innovative Python-based vector database and MCP server for personal AI. The original project pioneered graph-based selective recomputation for ultra-compact vector indexes (97% less storage than traditional solutions) and supports indexing everything from codebases to emails to browser history — all locally with zero cloud costs. This .NET port focuses on the semantic code search and MCP server pipeline, rebuilt natively for Windows/macOS with GPU acceleration via ONNX Runtime.
Why a .NET port?
The original LEANN requires Python, WSL (on Windows), PyTorch, and several other dependencies — a significant setup burden, especially on corporate machines with limited install permissions. This port is a single self-contained executable with zero external dependencies. No Python, no WSL, no pip, no virtual environments. Just download and run.
It works natively on Windows (DirectML GPU acceleration) and macOS (CoreML on Apple Silicon), with automatic CPU fallback on any platform.
How It Works
Source Code → Chunk → Embed (GPU) → Index → MCP Search
- Chunk — splits source files into overlapping passages (code-aware: respects functions, classes, blocks)
- Embed — computes 768-dim vectors using jinaai/jina-embeddings-v2-base-code (default; code-aware, 30 programming languages, 8192 max sequence length) via ONNX Runtime (DirectML on Windows, CoreML on macOS). The legacy facebook/contriever model is still selectable with
--model facebook/contrieverorLEANN_MODEL=facebook/contriever. - Index — stores embeddings in a flat vector index with L2-normalized cosine similarity
- Search — any MCP client (VS Code Copilot, Claude Desktop, etc.) queries the index via semantic search
Quick Start
Prerequisites
- .NET 10 SDK
- GPU recommended but not required (falls back to CPU)
- macOS: Apple Silicon (M1/M2/M3/M4) required — Intel Macs are not supported
Option A: Install from NuGet (recommended)
dotnet tool install -g leann-dotnet
leann-dotnet --setup # downloads the default jina-embeddings-v2-base-code model (~282 MB zip → ~306 MB ONNX)
# or explicitly:
leann-dotnet --setup --model jinaai/jina-embeddings-v2-base-code
leann-dotnet --setup --model facebook/contriever # legacy 418 MB English-prose model
The model is extracted to ~/.leann/models/<sanitized-id>/ and is idempotent (re-running --setup is a no-op once the SHA256 marker is present; pass --force to re-download).
Option B: Build from source
# Git LFS required — the repo includes the 418 MB ONNX model
git lfs install
git clone https://github.com/d-german/leann-dotnet.git
cd leann-dotnet
dotnet publish src/LeannMcp -r win-x64 --self-contained -c Release -o publish/win-x64
Forgot
git lfs install? Rungit lfs pullto downloadmodel.onnx.Note:
leann-dotnetis both the NuGet package id and the installed command name. Afterdotnet tool install -g leann-dotnettheleann-dotnetexecutable is on yourPATHand is what every example below invokes.
Index Your Code
cd <data-root>
# Step 1: Chunk source code into passages
leann-dotnet --build-passages --docs /path/to/my-repo --index-name my-repo
# Step 2: Compute embeddings (GPU-accelerated)
leann-dotnet --build-indexes --index my-repo
# Or do both in one command:
leann-dotnet --rebuild --docs /path/to/my-repo --index-name my-repo
# Restrict to specific file types (whitelist; comma- or space-separated):
leann-dotnet --rebuild --docs /path/to/my-repo --index-name my-repo \
--file-types .cs,.csproj,.sln,.props,.targets
This creates <data-root>/.leann/indexes/my-repo/ with passages + embeddings.
Tip: Use
--file-typesto dramatically shrink the index on large polyglot repos. Restricting a 2.4M-passage C#/JS/CSS mono-repo to.cs,.csproj,.slntypically cuts the embeddings file from ~7 GB down to ~1–2 GB and removes noise (minified JS, vendored libraries, test fixtures) from search results.
4. Connect an MCP Client
Workspace auto-detection (new!): Register once globally — LEANN auto-resolves its data directory to whatever workspace your MCP client is currently in, via the MCP
rootscapability. Nocwd, no per-projectLEANN_DATA_ROOT. Seedocs/workspace-roots-design.md.
Add to your MCP client config (e.g., .vscode/mcp.json):
{
"servers": {
"leann": {
"type": "stdio",
"command": "leann-dotnet",
"args": []
}
}
}
That's it — switching VS Code workspaces hot-swaps indexes without a restart.
Resolution priority (MCP-server mode, evaluated per tool call):
LEANN_DATA_ROOTenv var — explicit override- MCP client-advertised
roots(e.g., VS Code workspace folder) Directory.GetCurrentDirectory()— fallback
Override (only if your client doesn't advertise roots and isn't launched from the workspace folder):
"env": { "LEANN_DATA_ROOT": "${workspaceFolder}" }
LEANN_MODEL is only needed to change the default model used when building indexes or to pick which model the server warms up at startup. At query time, the server automatically loads each index's own embedding model from its manifest — you can mix models freely in one workspace.
Index compatibility: every index records the embedding model + dimensions used to build it. The server reads the manifest at load time and uses the matching model to embed queries. Cross-model querying that previously errored out (
IndexCompatibility: refusing index ...) now Just Works as of v2.4.0.
Mixing models in one workspace
Code repositories generally retrieve better with jinaai/jina-embeddings-v2-base-code; PDF manuals and prose retrieve better with facebook/contriever. You can build both kinds of index side-by-side and query them all from one MCP session:
# Build a code index with the default Jina model
leann-dotnet --rebuild --docs C:\repos\my-app --index-name my-app
# Build a PDF index with Contriever (text-domain model)
leann-dotnet --rebuild --docs C:\docs\manuals --index-name manuals `
--model facebook/contriever --file-types .pdf
Both indexes are now queryable from a single MCP server — leann_search index_name="my-app" uses Jina, leann_search index_name="manuals" uses Contriever, no restart and no LEANN_MODEL change needed.
5. Search
From your MCP client, use these tools:
leann_search— semantic search across all indexed reposleann_list— list available indexesleann_warmup— pre-load embedding model (faster first search)
Data Layout
<data-root>/
└── .leann/
└── indexes/
├── my-repo/
│ ├── documents.leann.meta.json
│ ├── documents.leann.passages.jsonl
│ ├── documents.ids.txt
│ ├── documents.embeddings.bin
│ └── documents.embeddings.meta.json
└── another-repo/
└── ...
~/.leann/
└── models/
├── jinaai-jina-embeddings-v2-base-code/ # default code-aware model
│ ├── model.onnx
│ ├── tokenizer.json
│ ├── vocab.json
│ ├── merges.txt
│ └── .sha256.ok # idempotency marker
└── facebook-contriever/ # legacy English-prose model (optional)
├── model.onnx
└── vocab.txt
CLI Reference
Modes
| Mode | Command | Description |
|---|---|---|
| MCP Server | leann-dotnet |
Default. Starts MCP server on stdio |
| Chunk | leann-dotnet --build-passages |
Split source files into passages |
| Embed | leann-dotnet --build-indexes |
Compute embeddings for passages |
| Full Pipeline | leann-dotnet --rebuild |
Chunk + embed in one step |
| Watch | leann-dotnet --watch |
Auto-sync git repos and rebuild on changes |
| Setup | leann-dotnet --setup [--model ID] [--force] |
Download ONNX model (~282 MB jina, ~418 MB contriever; one-time per model) |
Passage Builder Flags
| Flag | Description | Default |
|---|---|---|
--docs <path> [...] |
Source directories to chunk (required) | — |
--index-name NAME |
Index name | cwd directory name |
--chunk-size N |
Text chunk size in chars | 256 |
--chunk-overlap N |
Text chunk overlap | 128 |
--code-chunk-size N |
Code chunk size in chars | 512 |
--code-chunk-overlap N |
Code chunk overlap | 64 |
--include-hidden |
Include hidden files/dirs | false |
--file-types EXT [EXT...] |
Whitelist of extensions (e.g. .cs .csproj or .cs,.csproj). When set, overrides the built-in extension defaults |
(built-in defaults) |
--exclude-paths PAT [PAT...] |
Gitignore-style globs to skip (e.g. "**/Tests/**" "**/Mocks/**"). Supports **, *, ?. Combined with any .gitignore files found in the tree |
(none) |
--no-ast |
Disable AST-aware code chunking (Roslyn for C#, brace-balanced for TS/JS/Java/C-family) and fall back to the legacy line-based sliding-window chunker | false (AST enabled) |
--force |
Overwrite existing passages | false |
Index Builder Flags
| Flag | Description | Default |
|---|---|---|
--force |
Rebuild even if embeddings exist | false |
--index NAME |
Build only this index | all |
--exclude NAME [...] |
Skip specified indexes | — |
--batch-size N |
Passages per GPU batch | 32 |
--max-tokens N |
Max token sequence length | 512 |
Watch Mode Flags
| Flag | Description | Default |
|---|---|---|
--interval N |
Check interval in seconds | 300 |
--repos-config PATH |
Path to repos.json config | .leann/repos.json |
--force |
Force a full rebuild on the first sweep, then resume normal change-detection. Useful for one-shot reindex of every repo without changing the daemon model | false |
Per-repo settings in repos.json
Each entry supports optional per-repo filters and chunking overrides:
{
"intervalSeconds": 300,
"repos": [
{
"folder": "C:\\OnBase.NET",
"gitUrl": "git@github.com:org/OnBase.NET.git",
"branch": "main",
"indexName": "onbase-dotnet",
"enabled": true,
// Optional — same semantics as the CLI flags
"fileTypes": [".cs", ".props", ".targets", ".json"],
"excludePaths": [
"**/Tests/**", "**/*.Tests/**", "**/*Tests.cs",
"**/Mocks/**", "**/third-party-assemblies/**",
"**/project.assets.json", "**/*.deps.json"
],
"codeChunkSize": 1024, // overrides default 512
"codeChunkOverlap": 128, // overrides default 64
"useAst": true // AST-aware chunking (default true)
},
{
"folder": "C:\\angular-app",
"gitUrl": "git@github.com:org/angular-app.git",
"branch": "main",
"indexName": "angular-app",
"enabled": true,
"fileTypes": [".ts", ".html", ".scss"],
"excludePaths": ["**/node_modules/**", "**/dist/**", "**/*.spec.ts"]
}
]
}
All per-repo fields are optional — existing repos.json files keep working unchanged.
AST-Aware Code Chunking
Starting in 1.0.15, code files are chunked by language structure rather than by raw character windows. This dramatically improves search relevance by ensuring each passage is a complete logical unit (a method, a property, a function) instead of a half-method that happens to start mid-line.
| Language(s) | Strategy | Notes |
|---|---|---|
C# (.cs) |
Roslyn AST (Microsoft.CodeAnalysis.CSharp) |
One chunk per method, constructor, property, indexer, operator, event, field, enum, delegate. Nested types and file-scoped namespaces supported. Each chunk is prefixed with a // {namespace}.{type}.{member} context comment so embeddings carry symbolic context. |
| TypeScript, JavaScript, Java, C/C++, Go, Rust, Kotlin, Scala, Swift, PHP | Brace-balanced walker | A small state machine that tracks strings, line/block comments, and template-literal interpolation (${...}) to split on top-level {...} blocks without being confused by braces inside strings. No native dependencies. |
PDF (.pdf) |
PdfPig text extraction + page markers | Each page's text is extracted via UglyToad.PdfPig and joined with \n\n--- Page N ---\n\n separators. The prose chunker then splits on those paragraph breaks, so chunks naturally fall on page boundaries and search results stay citeable to a specific page. Passages emit source_type=pdf metadata. See PDF Support for limitations. |
| Markdown, JSON, YAML, plain text, etc. | Sliding-window character chunker (legacy) | Unchanged from previous releases. |
After chunking, every passage (regardless of strategy) goes through a quality filter that drops:
- Trimmed length < 20 chars (e.g. lone
}lines) - Punctuation ratio > 70% (e.g.
} } } } }or;;; ; ;;;) - A run of ≥ 200 base64 characters (
[A-Za-z0-9+/]{200,}) — strips PDF/image blobs accidentally embedded in source - An underscore run > 10 (e.g.
__________)
The filter is the reason that, after upgrading, you'll typically see fewer passages in the index than before (1-3% drop is normal) — the eliminated chunks were noise that previously dominated unrelated queries.
Opting out
If a Roslyn parse error or an exotic file format causes problems on your repo, fall back to the legacy chunker:
# Per-build (CLI flag)
leann-dotnet --build-passages --docs C:\repo --no-ast
# Per-repo (repos.json)
{
"folder": "C:\\repo",
"useAst": false
}
The CLI also prints which mode is active in its startup banner:
LEANN Passage Builder
...
Code chunk: 512 (overlap 64)
AST chunk: enabled (Roslyn for C#, brace-balanced for C-family)
File types: ...
PDF Support
Starting in 2.2.0, .pdf files are first-class citizens of the index alongside source code and Markdown.
How it works
- Files are extracted page-by-page via UglyToad.PdfPig (pure managed .NET, MIT licensed, zero native deps).
- Pages are joined with
\n\n--- Page N ---\n\nmarkers so the prose chunker splits on paragraph breaks, keeping each chunk citeable to a specific page. - Every PDF passage carries
source_type: "pdf"in its metadata, so MCP search consumers can filter or visually distinguish PDF results from code/Markdown.
What works
- Text-based PDFs (technical docs, runbooks, design specs, API references, exported Word/HTML output).
- Multi-column layouts (best-effort — see PdfPig docs for the underlying letter-positioning heuristics).
- Mixed indexes with PDFs, source code, and Markdown in the same
--docsdirectory.
What does NOT work (yet)
- Scanned / image-only PDFs — these contain no embedded text. PdfPig will return zero text for affected pages and the file will be indexed as an empty document. There is no built-in OCR. If you need OCR, pre-extract with a tool like Tesseract or
pdftotext(Poppler) and index the resulting.txt/.mdinstead. - Encrypted / password-protected PDFs — these are skipped with a
warn-level log message; the build continues with the remaining files. - Corrupt PDFs — same skip-with-warning behavior. One bad file in a 5,000-file repo will not abort the run.
# Mixed code + PDF index
leann-dotnet --rebuild --docs C:\projects\my-app C:\docs\architecture --index-name my-app
Environment Variables
| Variable | Description | Default |
|---|---|---|
LEANN_DATA_ROOT |
Directory containing .leann/indexes/ |
Current working directory |
LEANN_MODEL |
Default model id for setup, passage building, rebuilds, and watch-mode rebuilds when no --model flag is given. Not a query-time override — query routing is automatic per index. |
jinaai/jina-embeddings-v2-base-code |
LEANN_MODEL_DIR |
Override the model directory location (rarely needed; computed from the user profile + sanitized model id by default) | ~/.leann/models/<sanitized-id> |
LEANN_FORCE_CPU |
Set to 1 or true to disable GPU acceleration |
(GPU enabled) |
GPU Acceleration
| Platform | Provider | Automatic |
|---|---|---|
| Windows | DirectML (any GPU) | ✅ Yes |
| macOS | CoreML (Apple Silicon GPU + Neural Engine) | ✅ Yes |
| Linux | CPU only | — |
GPU is used for both indexing and search query embedding. For MCP server use (search only),
you can set LEANN_FORCE_CPU=1 to free your GPU — single query embedding is fast on CPU.
Examples
# Index a single repo
leann-dotnet --rebuild --docs ~/projects/my-app --index-name my-app
# Index multiple directories into one index
leann-dotnet --build-passages --docs ~/proj/frontend ~/proj/backend --index-name my-app
# Index only specific file types (e.g. C# source only)
leann-dotnet --rebuild --docs ./my-repo --index-name my-repo \
--file-types .cs,.csproj,.sln,.props,.targets
# Skip test projects, mocks, fixtures (gitignore-style globs)
leann-dotnet --rebuild --docs ./my-repo --index-name my-repo \
--file-types .cs,.csproj,.sln,.props,.targets \
--exclude-paths "**/Tests/**" "**/*.Tests/**" "**/Mocks/**" "**/*Test.cs" "**/*Tests.cs"
# Rebuild all embeddings with smaller batches (low VRAM GPU)
leann-dotnet --build-indexes --force --batch-size 8
# Rebuild everything except one large index
leann-dotnet --build-indexes --exclude large-mono-repo
# Use shorter token sequences for faster indexing (slight quality trade-off)
leann-dotnet --build-indexes --force --max-tokens 256
# Auto-watch repos for changes
leann-dotnet --watch --interval 120
# One-shot full reindex of every watched repo, then keep watching incrementally
leann-dotnet --watch --force
Tuning Guide
Chunk Size vs. Batch Size vs. Max Tokens
These three settings control different parts of the pipeline:
| Setting | What It Controls | CPU/GPU | When to Change |
|---|---|---|---|
--chunk-size |
Characters per text passage | CPU (chunking) | Adjust search granularity |
--code-chunk-size |
Characters per code passage | CPU (chunking) | Adjust code search granularity |
--batch-size |
Passages processed per GPU call | GPU (embedding) | Match to your VRAM |
--max-tokens |
Token sequence length per passage | GPU (embedding) | Trade speed vs. context |
Chunk Size (search quality)
Chunk size controls how much context each passage contains. This is independent of GPU power.
- Smaller chunks (128-256 chars) → more precise search hits, less context per result
- Larger chunks (512-1024 chars) → more context per result, but may dilute relevance
- Code chunks default larger (512) because functions/methods need more context than prose
Note: Passages longer than
--max-tokens(default 512 tokens ≈ ~2000 chars) are truncated during embedding. Making chunks larger than ~2000 chars wastes disk without improving search.
Batch Size (GPU utilization)
Batch size determines how many passages are embedded simultaneously. This is where GPU VRAM matters.
| GPU VRAM | Recommended --batch-size |
|---|---|
| 4 GB | 32 (default) |
| 8 GB | 64 |
| 12+ GB | 128 |
# RTX 3500 Ada (12 GB) — crank up the batch size
leann-dotnet --rebuild --docs ./my-repo --index-name my-repo --batch-size 128
# Low-VRAM GPU or integrated graphics
leann-dotnet --rebuild --docs ./my-repo --index-name my-repo --batch-size 8
Max Tokens (speed vs. context)
The contriever model processes up to 512 tokens per passage. Lowering this speeds up embedding (attention is O(n²)) at the cost of truncating longer passages.
# Faster indexing, slight quality trade-off on long passages
leann-dotnet --build-indexes --max-tokens 256
# Full context (default)
leann-dotnet --build-indexes --max-tokens 512
Performance
Embedding throughput on NVIDIA RTX A1000 (4GB VRAM):
| Passage Type | Avg Tokens | Throughput |
|---|---|---|
| Short text/docs | ~100 | 150-175 passages/s |
| Long code (C#) | ~300 | 8-20 passages/s |
Optimizations included:
- Length-sorted batching (groups similar-length passages to minimize padding waste)
- Bulk file writes (single I/O call for embedding output, critical for network drives)
- Configurable
--max-tokensto reduce O(n²) attention cost on long passages
Platform Support
| Platform | GPU Provider | Accelerator |
|---|---|---|
| Windows x64 | DirectML | NVIDIA, AMD, Intel Arc |
| macOS ARM64 | CoreML | Apple Silicon GPU + Neural Engine |
| Linux x64 | CPU | (GPU EPs can be added) |
GPU support is automatic with graceful fallback — if the GPU provider isn't available, it logs a warning and uses CPU.
Building & Publishing
# Build
dotnet build
# Test
dotnet test
# Publish self-contained binary
dotnet publish src/LeannMcp -r win-x64 --self-contained -c Release -o publish/win-x64
# macOS
dotnet publish src/LeannMcp -r osx-arm64 --self-contained -c Release -o publish/osx-arm64
# Install as dotnet tool (framework-dependent)
dotnet pack src/LeannMcp -c Release
dotnet tool install --global --add-source src/LeannMcp/bin/Release leann-dotnet
Troubleshooting
| Problem | Solution |
|---|---|
| "No ONNX model found" | Run leann-dotnet --setup (downloads the active model). The model directory is ~/.leann/models/<sanitized-model-id>/. |
| "Pre-computed embeddings not found" | Run leann-dotnet --build-indexes |
IndexCompatibility: refusing index ... built with <other-model> |
(Pre-v2.4.0 only.) As of v2.4.0 the server loads the manifest's model automatically — upgrade if you still see this message. The remaining cause is a dimension mismatch, which means the index is corrupt; rebuild with leann-dotnet --rebuild .... |
| "DirectML not available" | Falls back to CPU automatically. Update GPU drivers. |
| Slow first search | Call leann_warmup to pre-load the model |
| Out of GPU memory (4 GB VRAM) | Use --batch-size 8 and/or --max-tokens 256. Set LEANN_FORCE_CPU=1 if it still OOMs — single-query search is fast on CPU. |
| Network drive writes slow | Already fixed — uses bulk writes. Update to latest build. |
Migrating from 1.0.x (contriever-only) to 1.0.16+ (jina default)
dotnet tool update -g leann-dotnetleann-dotnet --setup(downloads jina; existing contriever model is not deleted).- Rebuild your indexes:
leann-dotnet --rebuild --docs <repo> --index-name <name>— required because index files record the model that built them and the new compatibility guard refuses cross-model loads. - Optional: keep using contriever by setting
LEANN_MODEL=facebook/contriever(env var) or passing--model facebook/contrieverto--setup/--rebuild.
License
MIT — see LICENSE
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
This package has no dependencies.