A private, local-first retrieval stack that turns your Obsidian vault into searchable semantic memory using .NET, Qdrant, and local embedding models.
Reference document in your Obsidian vault: Open in Obsidian
Prerequisites:
- Docker Desktop installed and running.
- .NET SDK 10 installed (
dotnet --versionshould report10.x).
Environment configuration:
- Initialize
.envfrom template if needed:- PowerShell:
Copy-Item .env.example .env - Bash:
cp .env.example .env
- PowerShell:
- Edit
.envto customize ports, container names, and default model. - Current defaults:
OLLAMA_PORT=11435EMBEDDINGS_BASE_URL=http://localhost:11435/v1EMBEDDING_MODEL=nomic-embed-textQDRANT_HTTP_PORT=6333QDRANT_GRPC_PORT=6334QDRANT_GRPC_URL=http://localhost:6334QDRANT_COLLECTION=vaultrag_notesOLLAMA_MODEL=glm-5:cloud
Start services:
docker compose up -dCheck containers:
docker compose psBase URL (host): http://localhost:11435 by default (from OLLAMA_PORT in .env)
For cloud models (example: glm-5:cloud), sign in once:
docker compose exec ollama ollama signinRun a model:
docker compose exec ollama ollama run glm-5:cloudQuick API test:
curl.exe http://localhost:11435/api/chat -H "Content-Type: application/json" -d "{\"model\":\"glm-5:cloud\",\"messages\":[{\"role\":\"user\",\"content\":\"Hello\"}],\"stream\":false}"HTTP endpoint (host): http://localhost:6333 by default (from QDRANT_HTTP_PORT in .env)
gRPC endpoint (host): localhost:6334 by default (from QDRANT_GRPC_PORT in .env)
Quick health check:
curl http://localhost:6333/healthzStop services:
docker compose downBuild the ingestion app:
dotnet build src/VaultRAG.IngestEnsure the embedding model is available in Ollama:
docker compose exec ollama ollama pull nomic-embed-textRun ingestion for a vault folder:
dotnet run --project src/VaultRAG.Ingest -- --vault "E:\Brain"Useful options:
dotnet run --project src/VaultRAG.Ingest -- --helpdotnet run --project src/VaultRAG.Ingest -- --vault "E:\Brain" --force-reindexIncremental indexing state is stored in .vaultrag/ingest-state.json.
Implementation details:
- Excluded folders during indexing:
.obsidian,.trash. - Point IDs are deterministic per chunk (
source_path + chunk_index) for stable upserts. - Deleted files are removed from Qdrant only when they exist in ingest state; if state is reset, run with
--force-reindex.
Build the MCP server:
dotnet build src/VaultRAG.McpServerRun MCP server over stdio:
dotnet run --project src/VaultRAG.McpServerImplemented MCP tools:
search_notes: semantic retrieval from Qdrant (query,topK, optionalsourcePathPrefix).get_note_chunks: fetch chunks for one indexed note (sourcePath,maxChunks).list_sources: list indexed source files (query,limit).health: checks Qdrant collection and embeddings endpoint.
Tool argument limits:
search_notes.topK: clamped to1..50(default5).get_note_chunks.maxChunks: clamped to1..500(default50).list_sources.limit: clamped to1..1000(default100).
Response shape:
- Tools return
structuredContent(JSON object) andcontent[0].text(the same JSON serialized as text). search_notes.sourcePathPrefixfiltering is applied after vector retrieval (client-side in the server process).
Environment variables used by MCP server:
QDRANT_GRPC_URLQDRANT_COLLECTIONQDRANT_API_KEY(optional)EMBEDDINGS_BASE_URLEMBEDDING_MODELOPENAI_COMPAT_API_KEY(optional)MCP_SERVER_NAME(optional, defaultVaultRAG.McpServer)MCP_SERVER_VERSION(optional, default0.1.0)MCP_SERVER_LOG_PATH(optional, defaultmcp-server.lognext to the executable)
Current project scope:
- Ingestion/indexing CLI (
VaultRAG.Ingest). - Retrieval MCP server (
VaultRAG.McpServer) exposing search/list/chunk/health tools. - No built-in answer-generation API is implemented in this repository yet.
Security:
- Indexed chunk text is retrievable through MCP tools. Do not keep plaintext credentials/secrets in notes you index.