- Python 98.7%
- Dockerfile 1.3%
Self-hosted semantic-search MCP server for Obsidian. Read-only vector search over notes synced to CouchDB via self-hosted LiveSync, powered by ChromaDB + Ollama embeddings. Stack: CouchDB 3.4.3, ChromaDB 0.6.3, Ollama, obsidian-sync (CouchDB->_changes -> Chroma indexer), obsidian-mcp (FastMCP HTTP server, 4 tools, Bearer auth). Docker Compose, local builds, no private registry, no baked-in secrets (all config via .env). Includes sync unit tests (14) and stdio->HTTP client bridges for stdio-only MCP clients. |
||
|---|---|---|
| client | ||
| config/couchdb | ||
| docs | ||
| mcp-server | ||
| sync | ||
| .env.example | ||
| .gitignore | ||
| docker-compose.yml | ||
| LICENSE | ||
| README.md | ||
obsidian-brain
A self-hosted semantic-search MCP server for Obsidian. Turns your Obsidian notes (synced to CouchDB via the self-hosted LiveSync plugin) into a vector database that any MCP-compatible AI client (Claude Code, Cursor, etc.) can query — read-only, Bearer-token authenticated, no filesystem access.
This is the "brain" half of a two-server setup:
| Server | This repo | The npm obsidian-mcp package |
|---|---|---|
| Backing store | ChromaDB (vectors) | Local vault files |
| Transport | HTTP + Bearer auth | stdio, local-only |
| Tools | search_notes, get_note, list_notes, search_by_filename |
read/create/edit/move/tag notes |
| Use when | You want semantic search over a remote/synced vault | You want an agent to edit notes on the same machine |
See docs/architecture.md for the full data flow.
Stack
Five Docker Compose services, all local builds / official images:
- CouchDB 3.4.3 — note storage (Obsidian LiveSync backend)
- ChromaDB 0.6.3 — vector database
- Ollama — embeddings model (
nomic-embed-textby default) - obsidian-sync — watches CouchDB, embeds notes, upserts into Chroma
- obsidian-mcp — the MCP server (this is what your AI client talks to)
No private container registry, no baked-in secrets.
Prerequisites
- Docker + Docker Compose v2
- Obsidian desktop + the self-hosted LiveSync community plugin
- An MCP-capable client (Claude Code, Cursor, etc.) — only if you want to query it
Quick start
git clone <this-repo> obsidian-brain
cd obsidian-brain
# 1. Create your config (secrets live here, never committed)
cp .env.example .env
# 2. Fill in real values
# - COUCHDB_PASSWORD: a long random string
# - BRAIN_API_KEYS: a comma-separated list of Bearer tokens
# generate each with: openssl rand -hex 32
$EDITOR .env
# 3. Build + start (first run pulls the embedding model — can take minutes)
docker compose up -d --build
docker compose logs -f ollama-init # wait for "Embedding model ready"
Verify the brain is up:
# Should return 401 (no token) — confirms auth is working
curl -i http://localhost:8080/mcp
# Authenticated tool call
TOKEN=<one of your BRAIN_API_KEYS>
curl -s http://localhost:8080/mcp/tools/call \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"search_notes","arguments":{"query":"test","n_results":3}}'
Connect Obsidian to CouchDB
The brain only knows about notes that LiveSync replicates into CouchDB.
- Install the self-hosted LiveSync plugin in Obsidian.
- Set its remote database URI to your CouchDB:
http://<COUCHDB_USER>:<COUCHDB_PASSWORD>@<host>:5984/<COUCHDB_DB>(e.g.http://admin:...@localhost:5984/obsidian) - Run a full sync / rebuild from the plugin so your notes are pushed.
obsidian-syncwill pick up the CouchDB changes and index them. Watch:docker compose logs -f obsidian-sync— you should seeIndexed <path>lines.
If you change sync.py or switch embedding models later, force a full reindex —
see sync/REINDEX.md.
Point an AI client at the brain
Option A — HTTP directly (Claude Code, Cursor with HTTP MCP)
{
"mcpServers": {
"obsidian-brain": {
"url": "http://localhost:8080/mcp",
"headers": { "Authorization": "Bearer <BRAIN_API_KEYS token>" }
}
}
}
Option B — stdio bridge (clients that only speak stdio)
Use client/mcp-bridge.py or
client/local-mcp-server.py. Requires
pip install fastmcp==2.3.4 requests.
{
"mcpServers": {
"obsidian-brain": {
"command": "python3",
"args": ["/abs/path/to/client/mcp-bridge.py"],
"env": {
"BRAIN_URL": "http://localhost:8080",
"BRAIN_API_KEY": "<BRAIN_API_KEYS token>"
}
}
}
}
Tools exposed
| Tool | Args | Returns |
|---|---|---|
search_notes |
query: str, n_results: int = 5 |
Semantic hits: path, title, folder, excerpt, score, similarity |
get_note |
path: str |
{path, title, content} |
list_notes |
folder: str = "" |
All notes (optionally filtered by folder prefix): path, title, folder |
search_by_filename |
query: str |
Notes whose path/title contains the query (case-insensitive): path, title, folder |
Configuration reference (.env)
| Variable | Default | Purpose |
|---|---|---|
COUCHDB_USER |
admin |
CouchDB admin user (created on first boot) |
COUCHDB_PASSWORD |
(required) | CouchDB admin password |
COUCHDB_DB |
obsidian |
Database name LiveSync replicates into |
EMBED_MODEL |
nomic-embed-text |
Ollama embedding model. Alternatives: qwen3-embedding:0.6b, mxbai-embed-large, bge-m3 |
BRAIN_API_KEYS |
(required) | Comma-separated Bearer tokens accepted by the brain |
FULL_REINDEX |
0 |
Set 1 once to rebuild the Chroma collection from scratch |
Running the tests
The sync indexer has a unit-test suite (no live services needed — everything is mocked):
cd sync
pip install -r requirements.txt pytest
python -m pytest
Ports
| Host port | Service | Bind |
|---|---|---|
| 5984 | CouchDB | all interfaces |
| 8001 | ChromaDB | all interfaces |
| 8080 | obsidian-mcp (the brain) | all interfaces |
| 11434 | Ollama | localhost only |
CouchDB and the brain are bound to all interfaces so they're reachable from other machines / a reverse proxy. Put them behind a firewall / TLS proxy in production. Ollama is loopback-only by default.
Production notes
- Reverse-proxy
obsidian-mcp(:8080) with TLS for remote access; keep Bearer auth on. - Back up the
couchdb-dataandchromadb-datavolumes — they hold your indexed vault. - If you change
EMBED_MODEL, run aFULL_REINDEX=1cycle: existing vectors were made with the old model and won't match queries embedded with the new one. ollama-initonly runs ondocker compose up. If you changeEMBED_MODELlater, re-run it:docker compose run --rm ollama-init.
License
MIT — see LICENSE.