Modular RAG skills for a self-hosted LLM agent, backed by Qdrant.
Most "chat with your Obsidian vault" projects embed a vector store inside the Obsidian app and stop there. This is a different shape. Any knowledge source (an Obsidian vault, a folder of notes, a reference set) becomes a skill the agent can search, all behind one interface. It targets a standalone agent (Ollama, an agent gateway, an MCP client) instead of living inside the note-taking app.
The Obsidian vault indexer is the main worked example. It is one skill, not the whole project.
skill = (Qdrant collection) + (manifest: where the docs live + how to chunk) + (retrieval hook)
Everything downstream is generic. Adding a knowledge skill is a manifest, not a rewrite:
NOTES = SkillManifest(name="work_notes", source_glob="~/notes/work/**/*.md")
register(NOTES) # chunk -> embed -> upsert
retrieve(NOTES.name, q) # embed query -> search -> context block sources ingestion Qdrant agent
vault / ----> chunk + embed ----> one collection ----> gateway / MCP
notes / (via Ollama) per skill injects context,
reference sets then answers
The Obsidian loader (agent_skills/vault.py) does not treat notes as flat text:
- YAML frontmatter is parsed into payload metadata, not embedded as prose.
[[wikilinks]]are flattened to their display text for the embedding, and kept as alinkslist in the payload for graph-aware retrieval.#tags(inline and frontmatter) become filterable payload metadata.- Image embeds (
![[...]]) are dropped before embedding. - Incremental sync: each note is SHA-256 hashed, only changed notes are re-embedded, deleted notes are purged. The collection is not wiped per run.
- Deterministic chunk IDs (
uuid5(path:index)) so edits overwrite cleanly instead of duplicating.
pip install -r requirements.txt
# 1. bring up Qdrant locally
docker run -p 6333:6333 -v $(pwd)/qdrant_storage:/qdrant/storage qdrant/qdrant
# 2. pull an embedding model in Ollama
ollama pull nomic-embed-text
# 3. set VAULT_PATH in agent_skills/config.py, then index
python examples/index_vault.py sync
# 4. query
python examples/index_vault.py ask "how did I configure the network?"- Copy
examples/register_notes.py. - Write a
SkillManifestpointing at your docs. register()it. It is now a collection the agent queries like any other.
examples/mcp_server.py exposes retrieval as MCP tools (list_skills,
search_vault, search_skill), so an MCP client can query the knowledge. The
surface is read-only: it retrieves, it does not write or take actions.
pip install "mcp[cli]"
python examples/mcp_server.py # stdio, for local MCP clientsFor a local client (Claude Code, Cursor, Claude Desktop), point its MCP config at the script:
{
"mcpServers": {
"agent-skills": {
"command": "python",
"args": ["/absolute/path/to/examples/mcp_server.py"]
}
}
}For a networked gateway, switch the transport to sse at the bottom of
mcp_server.py and point the gateway at http://<host>:8000/sse. The agent
calls search_vault(...), the tool returns a context block, and the gateway
feeds it into the model's context.
Everything lives in agent_skills/config.py: Ollama URL, embedding model and
dimension, Qdrant host/port, chunk size and overlap. Swap the embedder freely,
but keep EMBED_DIM matched to the model or the upsert will reject.
- Local-first. Embeddings run on your own Ollama host; nothing leaves the network.
- Retrieval only. This gives an agent knowledge to reason over. It does not hand the agent tools to act on the world.
- Not an Obsidian plugin. It reads the vault as files; it does not run in the app.
- Early stage: the code is organized and syntax-clean, but run it against a live Qdrant and Ollama before relying on it.
MIT. See LICENSE.