A persistent memory layer for Claude Desktop — semantic search across your entire Obsidian vault using local embeddings and PostgreSQL + pgvector.
AI assistants forget everything between sessions. You repeat context, lose continuity, and start from zero every time. Your notes, projects, and preferences sit in Obsidian but never make it into your AI conversations automatically.
Obsidian Semantic MCP turns your vault into a queryable brain for Claude. It:
- Indexes every note as a vector embedding (via Ollama +
nomic-embed-text) - Stores embeddings in PostgreSQL with pgvector for fast semantic search
- Watches your vault for changes and re-indexes automatically
- Provides full vault CRUD (read, write, search, list) — works even when Obsidian is closed
- Exposes everything through MCP so Claude can retrieve and manage vault content on the fly
No cloud services. No API keys. Everything runs locally.
Start with the bootstrap installer for your platform. If you already cloned the repo, you can skip bootstrap and run uv run osm init from the project root.
If you're in a fresh environment (no osm launcher yet), run these from the repo root:
# 1) Preview setup actions safely (recommended: persistent data)
uv run osm init --dry-run --mode 1 --vault "/path/to/your/vault" --pg-password "obsidian" --persistent --data-dir "/path/to/data"
# 2) Run setup for real
uv run osm init --mode 1 --vault "/path/to/your/vault" --pg-password "obsidian" --persistent --data-dir "/path/to/data"
# 3) Open the monitoring dashboard
uv run osm dashboardOn macOS,
--mode 1is Native install. For Docker-first installs, use mode 3 on macOS or mode 2 on Linux/Windows. For ephemeral/CI setups, use--no-persistentinstead.
Before you start:
uvmust be installed —curl -LsSf https://astral.sh/uv/install.sh | sh(macOS/Linux) orpowershell -c "irm https://astral.sh/uv/install.ps1 | iex"(Windows)- Docker Desktop — the wizard will offer to install it automatically if missing (
brewon macOS,wingeton Windows,get.docker.comon Linux). On Windows, enable the WSL2 backend. - Know your vault path — in Obsidian: Settings → About → Vault location
macOS / Linux:
curl -fsSL https://raw.githubusercontent.com/celstnblacc/obsidian-semantic-mcp/main/install.sh | bashWindows PowerShell:
powershell -c "irm https://raw.githubusercontent.com/celstnblacc/obsidian-semantic-mcp/main/install.ps1 | iex"Tip: The bootstrap installer launches
osm initfor you. After a full install, you can runosm init --dry-runfrom an existing checkout to preview every action without making any changes.If you already cloned the repo,
scripts/osm(macOS/Linux) andscripts/osm.ps1(Windows PowerShell) work the same as the bootstrap installers. If you prefer a manual checkout, clone the repo and runuv run osm initfrom the project root.One server, all projects:
obsidian-semanticis registered globally — runningosm initfrom any other project is safe and idempotent. If already configured, it skips registration and informs you.OpenCode/GitHub Copilot note: In this repository,
osmmeans the Obsidian Semantic MCP CLI, not OpenStreetMap. In a new chat session, run commands explicitly (for example,osm dashboard) to avoid acronym ambiguity.If
osmis not found: useuv run osm <command>from the repo root (for example,uv run osm init --dry-run).Session starter (copy/paste):
In this repo, "osm" means the obsidian-semantic-mcp CLI (not OpenStreetMap). Please execute shell commands directly when I type them. If `osm` is not found, use `uv run osm ...` from the repo root. Examples: osm init, osm dashboard.Exit the wizard at any prompt: type
q,quit,exit, orskip— or pressCtrl+C.
The bootstrap installers clone into the local data directory, create a PATH shim for osm, and launch the wizard.
The wizard detects your OS and asks which installation mode you want:
macOS:
1) Native Homebrew + local Postgres + local Ollama
2) Docker + host Ollama Postgres in Docker, Ollama already on this Mac
3) Full Docker Everything in containers (recommended)
4) Docker + remote Ollama Postgres in Docker, Ollama on another machine
Linux:
1) Docker + host Ollama Postgres in Docker, Ollama on this machine
2) Full Docker Everything in containers (recommended)
3) Docker + remote Ollama Postgres in Docker, Ollama on another machine
Windows (requires Docker Desktop with WSL2 backend):
1) Docker + host Ollama Postgres in Docker, Ollama already on this PC
2) Full Docker Everything in containers (recommended)
3) Docker + remote Ollama Postgres in Docker, Ollama on another machine
Which mode? Pick Full Docker (mode 3 on macOS, mode 2 on Linux/Windows) unless you already have Ollama running locally - in that case pick Docker + host Ollama to avoid re-downloading the Ollama image and model.
It then:
- Sets up the local install directory and PATH shim
- Installs prerequisites or verifies they already exist (Docker is installed and started automatically if missing)
- Pulls
nomic-embed-textif needed - Writes a
.envfile (gitignored) with your vault path and credentials - Updates MCP client config automatically for Claude Desktop, Claude Code CLI, and OpenCode (whichever are installed)
- Detects the actual
mcp-servercontainer name viadocker compose psso the entry works even whenCOMPOSE_PROJECT_NAMEis set or the repo was cloned into a renamed directory
Restart Claude Desktop / OpenCode to pick up the new server. For Claude Code CLI, the entry is registered live; verify with claude mcp list.
Manual config (only if
osm initcould not detect your client)Add the same block to
~/.opencode.json,claude_desktop_config.json, or whatever JSON config your MCP client uses:{ "mcpServers": { "obsidian-semantic": { "command": "docker", "args": ["exec", "-i", "obsidian-semantic-mcp-mcp-server-1", "python3", "src/server.py"], "env": {} } } }Replace
obsidian-semantic-mcp-mcp-server-1with the actual container name shown bydocker ps --format "{{.Names}}" | grep mcp-serverif it differs.
The server indexes your vault on first run, then watches for changes automatically.
First-run indexing takes roughly 1–2 seconds per note — expect 5–15 minutes for a 500-note vault. Monitor progress at http://localhost:8484. Claude will return no results until indexing completes.
Prefer running without Docker? See Native Install (macOS). Want to skip the wizard? See Manual start.
OBSIDIAN_VAULT="/path/to/your/vault" POSTGRES_PASSWORD=obsidian docker compose up -dDocker Compose also reads a
.envfile in the repo root (gitignored).
First run pulls all images and the nomic-embed-text model automatically. This starts:
| Service | Port | Description |
|---|---|---|
| PostgreSQL + pgvector | 5433 | Vector storage (avoids conflict with host pg) |
| Ollama | 11435 | Local embeddings (auto-pulls model) |
| MCP server | stdio | Claude Desktop connects via docker exec |
| Dashboard | 8484 | http://localhost:8484 |
# View server logs
docker compose logs -f mcp-server
# Rebuild after code changes
docker compose up -d --build mcp-server dashboard
# Stop everything
docker compose down
# Stop and wipe all data (re-index from scratch)
# ⚠️ WARNING: -v deletes all indexed embeddings. Re-indexing will restart from scratch.
docker compose down -vFor faster embeddings on Linux with NVIDIA GPU, add to the ollama service in docker-compose.yml:
deploy:
resources:
reservations:
devices:
- capabilities: [gpu]Once the MCP server is connected, Claude can access your vault directly — no special syntax needed. Just talk to it naturally.
"Search my notes for anything about project X"
"What did I write about ketosis last month?"
"Find my notes on the Zettelkasten method"
"Read my Daily/2026-03-14.md note"
"Append this meeting summary to my inbox note"
"Write a new note at Projects/obsidian-mcp.md with this content"
"List all files in my Fleeting folder"
"Show me what's been modified recently"
"Re-index my vault"
Claude will automatically choose the right MCP tool (search_vault, get_file, write_file, etc.) based on your request.
Use osm to set up, manage, and tear down the stack. The wizard installs all prerequisites, configures Docker, and updates Claude Desktop automatically.
osm in this repo means the Obsidian Semantic MCP CLI (not OpenStreetMap).
| Command | Description |
|---|---|
osm init |
Interactive setup wizard |
osm init --mode <1-4> --vault <path> |
Non-interactive setup (agent/script friendly) |
osm init --dry-run |
Preview all actions without making any changes |
osm status |
Check service health (Docker, Ollama reachability/inference, Claude Desktop) |
osm dashboard |
Open monitoring dashboard in browser |
osm rebuild |
Rebuild Docker images after a code change |
osm tunnel |
Reconnect SSH tunnel (remote Ollama mode) |
osm remove |
Stop services and wipe all volumes and config |
osm remove --yes |
Non-interactive teardown (agent/script friendly) |
osm help |
Full flag reference |
osm init flags: --mode, --vault, --pg-password, --persistent / --no-persistent, --data-dir, --ssh-host, --ssh-user, --ssh-port, --ssh-key, --vault-remote
osm status probes both Ollama reachability (/api/tags) and embeddings (/api/embeddings) so it can catch the case where the daemon is up but model execution is broken.
Windows launcher:
osm initinstallsosm.cmdinto%USERPROFILE%\.local\bin\. Windows resolves.cmdautomatically, so you invoke it asosmfrom any terminal. Ifosmis not found, add%USERPROFILE%\.local\binto yourPathenvironment variable.
When you type osm init or osm dashboard directly in your terminal, those commands execute normally.
In chat-based coding agents, a bare osm ... message can be interpreted as text (or as OpenStreetMap) instead of a shell command. To force execution, phrase it explicitly:
Run osm initRun osm dashboard
If the launcher is not installed yet (or not on PATH), use:
Run uv run osm init --dry-run --mode 1 --vault <vault-path> --pg-password <password>Run uv run osm dashboard
What to expect for new users:
- First install:
osm initsets up services and registersobsidian-semanticin global MCP config for that OS user. - Later sessions: re-running
osm initis safe and idempotent. osm dashboardopenshttp://localhost:8484; if the stack is not running yet, it warns and still opens the URL.
Session starter (copy/paste):
In this repo, "osm" means the obsidian-semantic-mcp CLI (not OpenStreetMap).
Please execute shell commands directly when I type them.
If `osm` is not found, use `uv run osm ...` from the repo root.
Examples: osm init, osm dashboard.
Claude Desktop
↓ MCP protocol (stdio)
src/server.py (unified MCP server)
├── Semantic search (pgvector cosine similarity)
├── Vault CRUD (direct filesystem access)
└── Live file watcher (watchdog)
↓
PostgreSQL + pgvector (vector storage + IVFFlat index)
↓
Ollama / nomic-embed-text (local 768-dim embeddings)
↓
Your Obsidian vault (e.g. $HOME/Documents/MyVault)
obsidian-semantic-mcp/
├── install.sh # Bootstrap installer for macOS/Linux
├── install.ps1 # Bootstrap installer for Windows PowerShell
├── scripts/
│ ├── osm # CLI wrapper (macOS/Linux) — `uv run osm init` or `scripts/osm init`
│ └── osm.ps1 # CLI wrapper (Windows) — `.\scripts\osm.ps1 init`
├── src/
│ ├── server.py # MCP server — semantic search + vault CRUD (10 tools)
│ └── dashboard.py # Monitoring dashboard (http://localhost:8484)
├── tests/
│ ├── test_unit.py # Unit tests (no real DB/Ollama needed)
│ ├── test_osm_init.py # Unit tests for the osm CLI wizard
│ ├── test_dashboard_smoke.py # Dashboard static analysis + live HTTP smoke tests
│ ├── test_setup.py # Prerequisites checker (deps, DB, Ollama) — run directly
│ └── test_e2e.py # End-to-end MCP protocol test — run directly
├── osm_init.py # Interactive setup wizard (used by scripts/osm)
├── Dockerfile # Python 3.13 + uv
├── docker-compose.yml # Full stack: postgres, ollama, server, dashboard
├── pyproject.toml # Project metadata + dependencies (osm script entry)
├── uv.lock # Pinned lockfile
└── LICENSE # Apache 2.0
- An Obsidian vault on your filesystem
- macOS native: Homebrew (auto-installs everything else)
- Docker modes: Docker Desktop (macOS/Linux/Windows) — the wizard offers to install it if missing via
brew(macOS),winget(Windows), orget.docker.com(Linux), and starts the daemon automatically - Windows: WSL2 backend enabled,
uvinstalled viapowershell -c "irm https://astral.sh/uv/install.ps1 | iex"
| Tool | Description |
|---|---|
search_vault |
Semantic search by meaning across the entire vault. Returns ranked excerpts with similarity scores. |
simple_search |
Exact text/keyword search across vault files. |
| Tool | Description |
|---|---|
list_files |
List files and directories in a vault directory. |
get_file |
Read the full content of a single file. |
get_files_batch |
Read multiple files at once. |
append_content |
Append content to a file (creates if missing). |
write_file |
Overwrites the target file completely — existing content is replaced with no undo. Use append_content if you want to add to an existing file instead. |
recent_changes |
Get recently modified files. |
| Tool | Description |
|---|---|
list_indexed_notes |
List all indexed notes with their last indexed timestamp. |
reindex_vault |
Force a full re-index of all notes. Runs in the background. |
To index multiple vaults, set OBSIDIAN_VAULTS to a comma-separated list of absolute paths:
OBSIDIAN_VAULTS="/path/to/vault1,/path/to/vault2" docker compose up -dOr in docker-compose.yml (uncomment the multi-vault lines):
environment:
OBSIDIAN_VAULTS: /vault1,/vault2 # multi-vault
volumes:
- /path/to/vault1:/vault1
- /path/to/vault2:/vault2When using multiple vaults, the search_vault MCP tool gains a vault parameter to filter results by vault name. The dashboard also shows a vault selector. Each vault is watched and indexed independently.
- Indexing — On startup, the server walks your vault, reads each
.mdfile, generates a 768-dim embedding via Ollama, and upserts it into PostgreSQL with pgvector. Unchanged files (same SHA-256 hash) are skipped on subsequent runs. First-run indexing takes roughly 1–2 seconds per note with a local Ollama instance — expect 5–15 minutes for a 500-note vault. Watch progress at http://localhost:8484 or viadocker compose logs -f mcp-server. - Watching — A file watcher (
watchdog) monitors the vault for creates, updates, deletes, and moves — re-embedding changed files automatically. - Searching — When Claude calls
search_vault, the query is embedded and matched against stored vectors using cosine similarity (IVFFlat index). The top results are returned with similarity scores and content previews. - CRUD — All file operations use direct filesystem access, so the server works whether Obsidian is open or not. Path traversal outside the vault is blocked.
| Variable | Description | Default |
|---|---|---|
OBSIDIAN_VAULT |
Absolute path to your Obsidian vault. Mounted read-write so MCP write/append tools can create and update notes. Mount with :ro if you only need search. |
required |
OBSIDIAN_VAULTS |
Comma-separated list of vault paths for multi-vault mode. Overrides OBSIDIAN_VAULT when set. |
— |
OBSIDIAN_IGNORE_PATHS |
Comma-separated vault-relative path segments to skip during indexing and watching. Defaults to archive; set it to an empty string to index archived notes. |
archive |
POSTGRES_PASSWORD |
PostgreSQL password (Docker) | required for Docker |
DATABASE_URL |
Full connection string (overrides POSTGRES_* vars) | built from POSTGRES_* vars |
POSTGRES_HOST |
PostgreSQL host | localhost |
POSTGRES_PORT |
PostgreSQL port | 5432 |
POSTGRES_DB |
Database name | obsidian_brain |
POSTGRES_USER |
Database user | obsidian |
OLLAMA_URL |
Ollama API endpoint | http://localhost:11434 |
EMBEDDING_MODEL |
Ollama model for embeddings | nomic-embed-text |
EMBED_WORKERS |
Parallel threads for bulk indexing | 4 |
RERANK_MODEL |
Optional Ollama model for cross-encoder re-ranking (e.g. llama3.2). Disabled when empty. |
— |
RERANK_CANDIDATES |
Candidate pool size fetched before re-ranking | 20 |
A built-in dashboard is available at http://localhost:8484 (started automatically with Docker). It shows:
- Service health (PostgreSQL, Ollama, embedding model)
- Indexed notes count, vault coverage, DB size
- Recently indexed files
- Re-index — incremental re-index (skips unchanged notes, fast)
- Clear & Rebuild — wipes all embeddings and re-indexes from scratch
- A "Start Ollama" button if Ollama is down
To run the dashboard without Docker:
OBSIDIAN_VAULT="/path/to/your/vault" uv run python3 src/dashboard.pyuv run pytest -qRuns 230 fast unit tests covering embedding, search, vault path safety, connection pool, dashboard stats, the osm CLI wizard, and CI governance.
Offline static analysis (always runs — no services needed) + live HTTP smoke tests (auto-skipped when the dashboard is unreachable).
# Offline + live (stack must be running)
uv run pytest tests/test_dashboard_smoke.py -v
# Target a remote instance
DASHBOARD_URL=http://host:8484 uv run pytest tests/test_dashboard_smoke.py -vChecks: JS string safety, DOM element completeness, /api/stats schema, service health, response time, and routing.
Verifies Python deps, vault path, PostgreSQL + pgvector, Ollama, and embedding smoke test. Run directly — not via pytest.
DATABASE_URL="postgresql://localhost/obsidian_brain" \
OBSIDIAN_VAULT="/path/to/your/vault" \
uv run python3 tests/test_setup.pyLaunches the server, initializes MCP protocol, waits for indexing, runs semantic search, and verifies results. Run directly — not via pytest.
Requires DATABASE_URL or POSTGRES_PASSWORD — the server will exit immediately without one.
DATABASE_URL="postgresql://localhost/obsidian_brain" \
OBSIDIAN_VAULT="/path/to/your/vault" \
uv run python3 tests/test_e2e.pyIf your vault lives on a NAS and is mounted on Windows as a drive letter (Z:\), osm init will fail to bind-mount it into the container — Docker Desktop on Windows uses WSL2, and WSL2 cannot follow a Windows-side network drive into a container. The UNC form (\\host\share\...) is rejected by the daemon outright; the drive-letter form silently mounts an empty directory.
Recommended fix: mount the share inside WSL2 and point osm init at the Linux path.
# inside your WSL2 distro (Ubuntu/Debian)
sudo apt install nfs-common
sudo mkdir -p /mnt/obsidian_vault
sudo mount -t nfs <nas-host>:/<export-path> /mnt/obsidian_vault
# add to /etc/fstab to persist across reboots
osm init --mode 2 --vault /mnt/obsidian_vaultDocker Desktop shares WSL2 paths cleanly with no volume-driver gymnastics. Starting in v0.5.11, osm init also fails fast when docker compose up is rejected and points you at this section instead of letting the postgres health check time out 90 seconds later.
If WSL2 isn't an option, osm init can generate a docker-compose.override.yml that backs each vault with a Docker named volume using NFS or CIFS driver_opts. Vault entries use protocol-specific syntax instead of host paths:
# NFS (one or more vaults; entries use host:/export/path)
osm init --mode 3 \
--vault 10.0.0.1:/exports/coredev \
--vault-fs nfs
# CIFS / SMB
osm init --mode 3 \
--vault //nas.local/share/coredev \
--vault-fs cifs \
--vault-cifs-user alice --vault-cifs-pass 'secret'For multi-vault, pass each entry to a comma-joined OBSIDIAN_VAULTS env (or repeat --vault); each generates its own named volume (obsidian_vault_<basename>). osm remove drops these volumes on teardown.
Limitations: NFSv4 with no auth, SMB with username/password only. NFS Kerberos and CIFS credential files are not supported in v0.5.12. WSL2 is still the recommended path for most users.
| Symptom | Cause | Fix |
|---|---|---|
uv: command not found |
uv not installed or not in PATH |
Run curl -LsSf https://astral.sh/uv/install.sh | sh then restart your terminal |
Cannot connect to the Docker daemon |
Docker Desktop not running | The wizard offers to start it automatically; or start Docker Desktop manually from Applications (macOS) / system tray (Windows/Linux) |
Permission denied: /path/to/vault |
Vault path not readable by Docker | On macOS: Docker Desktop → Settings → Resources → File Sharing — add your vault path |
ModuleNotFoundError: No module named 'mcp' |
System Python instead of venv | Use .venv/bin/python3 in config, or use Docker |
ModuleNotFoundError: No module named 'psycopg2' in Docker |
Container built before venv PATH fix | docker compose up -d --build mcp-server |
Search returns 0 results |
IVFFlat index built on empty table | Run psql obsidian_brain -c "REINDEX INDEX notes_embedding_idx;" |
Vault indexing is in progress — no results yet |
First-boot indexing not complete | Wait for indexing to finish (check docker compose logs -f mcp-server) |
Cannot reach Ollama |
Ollama not running | Run ollama serve or docker compose up ollama |
Skipped <file>: vector must have at least 1 dimension |
Ollama returned empty embedding (blank/tiny file) | Expected — file is skipped and indexing continues |
Skipped <file>: 500 Server Error |
Ollama internal error (file too large or model issue) | Expected — file is skipped; try ollama pull nomic-embed-text to refresh model |
pgvector extension not found |
Not installed for your PG version | Use Docker, or build from source (see native install) |
| Server crashes on startup | OBSIDIAN_VAULT not set |
Set the env var in your config or docker compose command |
| Docker container can't see vault | Wrong path or missing volume | Ensure OBSIDIAN_VAULT is an absolute path accessible to Docker |
Manual install only. Use this if you want to control every step yourself instead of using the bootstrap installer.
git clone https://github.com/celstnblacc/obsidian-semantic-mcp.git && cd obsidian-semantic-mcp
uv syncbrew install postgresql@17 pgvector ollama
brew services start postgresql@17
ollama serve &
ollama pull nomic-embed-textPostgreSQL 16: Homebrew's
pgvectorbottle requires pg17 or pg18. If you must use pg16, build pgvector from source:cd /tmp git clone --branch v0.8.2 --depth 1 https://github.com/pgvector/pgvector.git cd pgvector make PG_CONFIG=$(brew --prefix postgresql@16)/bin/pg_config make install PG_CONFIG=$(brew --prefix postgresql@16)/bin/pg_config
createdb obsidian_brain
psql obsidian_brain -c "CREATE EXTENSION vector;"DATABASE_URL="postgresql://localhost/obsidian_brain" \
OBSIDIAN_VAULT="/path/to/your/vault" \
uv run python3 tests/test_setup.pyAdd to $HOME/Library/Application Support/Claude/claude_desktop_config.json:
{
"mcpServers": {
"obsidian-semantic": {
"command": "/absolute/path/to/obsidian-semantic-mcp/.venv/bin/python3",
"args": ["/absolute/path/to/obsidian-semantic-mcp/src/server.py"],
"env": {
"OBSIDIAN_VAULT": "/absolute/path/to/your/vault",
"DATABASE_URL": "postgresql://localhost/obsidian_brain"
}
}
}
}Important: Use
.venv/bin/python3— not system Python. Homebrew Python won't have the required packages.
The server indexes your vault on first run, then watches for changes automatically.
Everything runs locally. No cloud APIs, no subscriptions. The only cost is disk space for the database (~a few MB for most vaults).
Apache 2.0