loci-mcp
Enables indexing and searching of Obsidian vaults, including frontmatter tags, wikilinks, and link graph traversal, so AI agents can query notes across multiple vaults.
Integrates with Ollama's local API for 100% offline embeddings and chat completions, enabling retrieval-augmented generation without external API calls.
Integrates with OpenAI's API (and compatible endpoints) for embeddings and chat completions, powering hybrid retrieval and answer generation over local files.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@loci-mcpsearch my second brain for T8 vs final-render mode and cite the sections"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
loci ๐ง
Two thousand years ago, orators stored their speeches in the rooms of a palace and walked through them to remember. loci does the same for your files.
Loci is the method behind every memory palace: place knowledge in locations, recall it by walking the path.

A queryable "second brain" for the project docs, notes, and chat logs scattered across a dozen directories โ and an MCP server so your AI agents can use it too.
Local files โ heading-aware chunking โ embeddings โ hybrid retrieval (vector + BM25) โ LLM answer with section-level citations. The index lives entirely on your machine; only embedding/chat calls go out, to any OpenAI-compatible API (Zhipu / DeepSeek / Kimi / OpenAI / โฆ).
The thesis (from studying the 90k-star platforms and the graveyard of dead lightweight tools โ see our competitive landscape study): don't build another chat app. Build the memory layer that every chat app can mount. Claude Desktop, Cursor, Cline, or any MCP host becomes this project's UI, for free.
Demo
Real session, indexed against the docs of minimax-h3-turing (paths shortened for display):
$ python main.py search "what the 22G card can and cannot do" -k 3
[1] minimax-h3-turing/docs/en/01-hardware-limits.md > 01 ยท What a 2080Ti 22G Can and Cannot Do (similarity 0.562)
[2] minimax-h3-turing/docs/en/02-w4a8-vs-w4a4.md > 02 ยท Quantization Measured > You Can Try Without 22G (similarity 0.446)
[3] minimax-h3-turing/docs/en/01-hardware-limits.md > ... > 3. VRAM is just barely enough โ manage it (similarity 0.504)
$ python main.py ask "How should I choose between T8 aggressive mode and the final-render mode, and why?"
Answer:
* Drafts / preview / shot selection: use T8 aggressive mode โ a 43% speedup
(2.7 min/clip), and "a different picture of equal quality" is fine for picking shots.
* Final shots: use final-render mode (no T8). T8 makes the numerical trajectory
fork, so re-running with the same seed produces a different clip โ which breaks
the reproducibility final outputs need.
[source: docs/en/08-t8-blockcache-4step.md > Practical Advice (4-step Turbo route)]
[source: docs/en/06-faq.md > 12. Cache-style accelerators break "same-seed re-runs"]Hybrid retrieval means a Chinese query still finds the English doc (and vice
versa) โ keyword evidence (BM25) catches what embeddings miss, and every
citation points at a section, not just a file.
Does hybrid actually help? (mini-eval, 10 bilingual queries)
$ python scripts/eval_retrieval.py scripts/eval_cases.example.jsonl
vector-only: 9/10 โ hybrid: 10/10Hybrid also fixed the #1 ranking on keyword-ish queries (e.g. "T8 block cache threshold speedup": vector put an FAQ first, hybrid puts the actual T8 writeup first). Run it against your own corpus with your own cases file.
Reranking: two providers
--rerank reorders the fused candidates for precision:
Provider | How | Cost |
| pointwise 0โ3 relevance scoring by your chat model | one extra LLM call |
| cross-encoder, via | ~30โ70 ms for 5 pairs on GPU โ offline, free |
python main.py search "T8 speedup" --rerank # provider from config
python main.py search "T8 speedup" --rerank local # cross-encoder (BAAI/bge-reranker-base)The local model downloads on first use (~1.1 GB; set HF_ENDPOINT=https://hf-mirror.com
if HuggingFace is slow in your region). Measured on a 2080 Ti, bilingual query.
Office documents, PDF tables, web pages, org files, chat logs
PDFs: with the
[pdf]extra, PyMuPDF4LLM extracts pages as markdown โ tables come through as pipe rows (plain pypdf text is the fallback)Word: with the
[docx]extra,.docxparagraphs and table rows are indexedHTML:
.html/.htmpages become text with headings preserved (stdlibhtml.parser, zero dependencies โ<meta charset>honored, script/style skipped)org-mode:
.orgnotes convert faithfully โ#+TITLEbecomes the h1 with*-sections nested under it,#+FILETAGSbecome searchable tagsChat exports: drop a ChatGPT or Claude
conversations.jsoninto any source directory โ it becomes one searchable document per conversation, taggedchatlog(search --tag chatlogscopes to chat history)
Related MCP server: Keepygaga RAG
How it relates to Obsidian / your note app
It doesn't compete โ the two layer up. Obsidian (or any editor) is the
note-taking frontend; this is the cross-vault search engine: point
sources at any directories (Obsidian vaults, project docs, chat exports)
and query all of them at once โ from your terminal, your scripts, or your AI
agent via MCP. Obsidian-native details are understood: frontmatter tags:
(filter with search --tag), [[wikilinks]] (walk the graph with links),
code blocks are never cut mid-block, and one-line notes stay searchable.
How it works
%%{init: {'theme':'base','themeVariables':{'background':'#000000','primaryColor':'#000000','primaryTextColor':'#00FF41','primaryBorderColor':'#00FF41','lineColor':'#00FF41','secondaryColor':'#001a00','tertiaryColor':'#000000','clusterBkg':'#000000','clusterBorder':'#00FF41','edgeLabelBackground':'#000000','fontSize':'14px','fontFamily':'trebuchet ms, verdana, arial, sans-serif'},'themeCSS':'.nodeLabel { color: #00FF41 !important; } .edgeLabel { background: #000 !important; color: #00FF41 !important; } .cluster-label { color: #00FF41 !important; }'}}%%
flowchart LR
subgraph sources["๐ฅ Your machine"]
notes["Obsidian / markdown notes"]
docs["PDF tables ยท docx ยท project docs"]
chats["ChatGPT / Claude exports"]
mem["memories/ โ agent-written notes"]
wikidir["wiki/ โ consolidated pages"]
end
subgraph loci["๐ง loci โ local index, nothing leaves the machine"]
ingest["ingest / watch<br>loaders โ chunker โ embedder"]
store[("ChromaDB<br>hybrid index")]
retrieve["hybrid retrieval<br>vector + BM25 โ RRF"]
mcp["loci-mcp<br>8 tools ยท resources ยท prompts"]
end
subgraph hosts["๐ฅ๏ธ Your AI hosts"]
ide["Claude Code ยท Qoder ยท Trae<br>Cursor ยท Cline"]
desktop["Claude Desktop"]
term["Terminal<br>search / ask / chat / wiki"]
end
api["โ๏ธ OpenAI-compatible API<br>Zhipu / DeepSeek / Kimi / OpenAI<br>or 100% offline via Ollama"]
sources --> ingest --> store
mem -. auto-indexed .-> store
wikidir -. auto-indexed .-> store
store --> retrieve
retrieve --> term
retrieve --> mcp
mcp <--> ide
mcp <-.-> desktop
retrieve -. "embedding + chat calls only" .-> apiThe write path in one line: loaders โ chunker (heading-aware split) โ embedder โ store (ChromaDB, persistent) โ incremental, deduplicated by content hash.
Install & quick start
Requires Python 3.11+ (uses the stdlib tomllib).
# option A: install from PyPI (adds `loci` and `loci-mcp` commands)
pip install "loci-rag[pdf,docx]" # optional extras: PDF w/ tables, Word documents
# option B: zero-install quickstart
pip install -r requirements.txt
# 1. Configure: copy the example and fill in your values
cp config.example.toml config.toml
# Any OpenAI-compatible endpoint works โ see the provider table below.
# Switching the embedding model later? Vectors from different models don't
# mix: re-run `loci ingest --force` to rebuild the index.
# 2. Ingest (incremental โ deduplicated by content hash, safe to re-run)
loci ingest # or: python main.py ingest
# 3. Ask
loci ask "what did I write about X?"Providers
[llm] and [embed] can point at different providers (chat via DeepSeek,
embeddings via OpenAI โ mixing is fine). The only rule: the [embed] endpoint
must offer an OpenAI-style /embeddings API.
Provider |
| chat model | embedding model | Notes |
OpenAI (default) |
|
|
| |
Zhipu (GLM) |
|
|
| |
DeepSeek |
|
| โ | no |
Kimi (Moonshot) |
|
| โ | same โ pair |
Ollama (local) |
|
|
| 100% offline; any api_key accepted |
The workflow
%%{init: {'theme':'base','themeVariables':{'background':'#000000','primaryColor':'#000000','primaryTextColor':'#00FF41','primaryBorderColor':'#00FF41','lineColor':'#00FF41','secondaryColor':'#001a00','tertiaryColor':'#000000','clusterBkg':'#000000','clusterBorder':'#00FF41','edgeLabelBackground':'#000000','fontSize':'14px','fontFamily':'trebuchet ms, verdana, arial, sans-serif'},'themeCSS':'.nodeLabel { color: #00FF41 !important; } .edgeLabel { background: #000 !important; color: #00FF41 !important; } .cluster-label { color: #00FF41 !important; }'}}%%
flowchart TD
A["pip install loci-rag"] --> B["cp config.example.toml config.toml<br>fill API keys + source dirs"]
B --> C["loci ingest โ hybrid index built"]
C --> D["loci watch โ index stays fresh (optional)"]
C --> E{"What do you need?"}
E -->|"a synthesized answer"| F["loci ask --verify<br>claim-by-claim audit"]
E -->|"raw excerpts to quote"| G["loci search --tag memory"]
E -->|"back-and-forth"| H["loci chat"]
E -->|"scattered notes on a topic"| I["loci wiki topic<br>consolidate into a wiki page"]
F --> J["loci remember โ<br>keep what you learned"]
I --> JCommands
Command | What it does |
| scan sources, index new/changed files, prune deleted ones ( |
| retrieval only โ ranked excerpts with |
| retrieval + LLM answer with |
| additionally audit the answer claim-by-claim against the sources (โ supported, ~ partial, โ unsupported) |
Filter operators (combine freely, on search and ask):
Flag | Filters to |
| files whose frontmatter tags contain |
| files whose path contains the substring |
| files modified on/after that date |
| chunks containing the exact phrase |
| return N hits (default 5) |
| show the |
| multi-turn Q&A loop with conversation memory ( |
| keep the index current by polling sources (interval in |
| LLM-rewrite the query (keyword + cross-language variants) before retrieval |
| rate the chunks used in the last ask; bad-rated chunks sink in future results |
| suggest wiki-worthy topics that don't have a page yet |
| retrieval benchmark: hit@k, vector-only vs hybrid |
| sync memories/wiki across machines via git ([sync] remote) |
| HTTP REST API (search/ask/remember/stats) with Bearer auth |
| knowledge graph over memories/wiki (LLM-extracted triples in graph.json) |
| what's in the index: chunks per source, models, retrieval settings |
| health check: config, source dirs, embed/LLM endpoints, store (exit code 1 on failure โ CI-friendly) |
| MCP server over stdio (see below) |
One memory, every IDE
Because every MCP host mounts the same loci server (same config.toml, same
index), memory written from one tool is recalled from every other:
# Claude Code
claude mcp add loci -- loci-mcp// Cursor / Cline / Qoder / Trae (mcpServers JSON โ same shape everywhere)
{ "mcpServers": { "loci": { "command": "loci-mcp" } } }Then, from any of them: "remember that the staging password rotates on
Mondays" โ brain_remember โ later, from a different IDE:
"when does the staging password rotate?" โ answered, with the memory cited.
Memories live as plain markdown in the memories directory (git-friendly, no
lock-in) and are tagged memory, so loci search --tag memory scopes to them.
Cross-IDE tip: the default
store/memoriespaths are relative to the directory loci is launched from. If your IDEs start in different project folders, point both at one absolute location inconfig.tomlโ e.g.store.path = "~/.loci/store"andmemories.path = "~/.loci/memories"โ and every IDE shares the exact same memory store.
%%{init: {'theme':'base','themeVariables':{'background':'#000000','primaryColor':'#000000','primaryTextColor':'#00FF41','primaryBorderColor':'#00FF41','lineColor':'#00FF41','actorBkg':'#000000','actorBorder':'#00FF41','actorTextColor':'#00FF41','signalColor':'#00FF41','signalTextColor':'#00FF41','noteBkgColor':'#001a00','noteBorderColor':'#00FF41','activationBkgColor':'#001a00','edgeLabelBackground':'#000000','fontSize':'14px','fontFamily':'trebuchet ms, verdana, arial, sans-serif'},'themeCSS':'.messageText { fill: #00FF41 !important; } .actor { fill: #000 !important; stroke: #00FF41 !important; } text.actor { fill: #00FF41 !important; }'}}%%
sequenceDiagram
participant CC as Claude Code
participant L as loci-mcp
participant S as ChromaDB (local)
participant T as Trae / Qoder / any IDE
CC->>L: brain_remember("deploy rotates Mondays")
L->>S: write memory.md + embed + index
Note over S: persists across sessions and IDEs
T->>L: brain_search("password rotation")
L->>S: hybrid retrieval
L-->>T: cited answer โ the memory is recalledEcosystem
loci-dsh โ visual plugin for DeepSeek Harness: search, ask, quick-capture memories and watch index stats from a sidebar in the dsh web UI, talking to
loci serve-httpover local REST.
Mount it in any MCP host
Add to claude_desktop_config.json (Claude Desktop) or your MCP client's
config:
{
"mcpServers": {
"loci": {
"command": "python",
"args": ["/path/to/loci/mcp_server.py"]
}
}
}The server exposes three tools (zero dependencies beyond the core):
Tool | Purpose |
| ranked excerpts with breadcrumbs |
| grounded answer with citations; |
| outbound/inbound |
| index overview (chunks per source) |
| knowledge-graph relations for an entity (omit for hub entities) |
| write a memory โ durable, shared across sessions and IDEs |
| soft-delete matching memories (they go to a |
| memory consolidation โ distill the index into a curated wiki page about a topic |
| incremental re-index |
Beyond tools, the server speaks the full protocol:
Resources โ
resources/listexposesbrain://statsplus onebrain://note/โฆresource per indexed file (raw markdown viaresources/read)Prompts โ three ready-made templates:
brain-briefing,study-plan,contradiction-check; hosts render them with your topic pre-filled
Fully offline with Ollama
The index is local by design โ and the embedding/chat calls can be too. Any OpenAI-compatible server works; Ollama is verified end-to-end:
[llm]
base_url = "http://localhost:11434/v1"
api_key = "ollama" # any non-empty placeholder
model = "qwen2.5:0.5b"
[embed]
base_url = "http://localhost:11434/v1"
api_key = "ollama"
model = "all-minilm"With this config, ingest / search / ask make zero cloud calls.
Swap in a bigger local chat model for better answers โ the pipeline is
model-agnostic.
Configuration
Key | Meaning |
| base_url / api_key / model โ any OpenAI-compatible endpoint |
| same; the model must be an embedding model (e.g. |
| document directories, scanned recursively for |
| optional per-directory chunking override โ wins over the global |
| chunking params (default 800 chars / 100 overlap) |
| number of hits per search (default 5) |
|
|
| poll |
API keys can also come from the environment variables BRAIN_LLM_API_KEY /
BRAIN_EMBED_API_KEY (these override the config file).
Development
git clone https://github.com/IvenKooLab/loci && cd loci
pip install -e ".[pdf,docx]" # editable install for hacking on loci
pip install -r requirements-dev.txt
pytest # fully offline, no API keys neededSee CONTRIBUTING.md for the ground rules (no frameworks, tests stay offline, citations are sacred).
Design decisions
~300 lines of core, no LangChain โ every stage is readable, hackable, and learnable. The whole engine fits in one sitting.
MCP-first โ the agent ecosystem is the UI layer. No web app to maintain.
Hybrid retrieval on by default โ vector search fused with a native ~60-line BM25 (CJK-aware tokenizer) via Reciprocal Rank Fusion.
Citations always, with breadcrumbs โ
path > section, so claims are verifiable at a glance.Robust, inspectable indexing โ defensive loaders (skip what can't be parsed, never hang), content-hash incrementality, real pruning,
statsanddoctorso the index is never a black box.Tiny notes stay searchable โ no minimum-chunk filter; a one-line note is still indexed (a lesson from watching other tools drop or choke on them).
Keys never in code โ
config.toml(gitignored) or env vars.
Where it sits
loci | AnythingLLM (65kโ ) | Khoj (37kโ ) | RAGFlow (90kโ ) | |
Positioning | personal retrieval backend + MCP | all-in-one chat platform |
This server cannot be deployed
Maintenance
Related MCP Connectors
Agentic search over your Dewey document collections from any MCP-compatible client.
Multi-engine search for AI agents. Trust scoring, local corpus, MCP-native. Self-hostable, BYOK.
Cloud or self-hosted knowledge for AI agents: hybrid search, reranking, GraphRAG, scoped MCP tools.
Serve a folder of Markdown notes as an MCP server: hybrid search, reading, and sourced answers.
Related MCP Servers
- AlicenseNot gradedqualityBmaintenanceEnables AI tools to query a user's private, locally stored memories (notes, documents) with source citations, using the MCP protocol.7 npmMIT
- AlicenseAqualityCmaintenanceEnables local-first hybrid knowledge retrieval from authorized Markdown and plain-text files, combining full-text and vector search with reranking and traceable source references via a single search tool.1MIT
- AlicenseNot gradedqualityCmaintenanceEnables local semantic and full-text search over Markdown notes through MCP, returning relevant passages ranked by meaning and exact terms without requiring a GPU or hosted APIs.MIT
- FlicenseNot gradedqualityAmaintenanceEnables AI agents and MCP hosts to query locally indexed notes, docs, chat logs, and other files via hybrid vector/BM25 retrieval with section-level citations. Exposes tools, resources, and prompts for searching and asking over the indexed content.-