obsidian-mcp
A high-performance MCP server that gives AI agents full read/write access to Obsidian vaults. Written in Rust. Ships as a single binary.
No Obsidian plugins. No REST API. No Obsidian running. Just your vault on disk.
Why?
Existing solutions for AI + Obsidian require the Obsidian app to be running with a community plugin (obsidian-local-rest-api) and an MCP wrapper (mcp-obsidian) on top. That's fragile, slow, and limited to what the REST API exposes.
obsidian-mcp talks directly to the filesystem. It understands the Obsidian format natively — wikilinks, frontmatter, tags, block references, periodic notes, the full graph. It builds fast in-memory indices on startup and keeps them synced via a filesystem watcher. The result is a single dependency-free binary that works whether Obsidian is open or not.
Quick Start
# Install
cargo install obsidian-mcp
# Run (stdio — for MCP clients that launch the process)
obsidian-mcp /path/to/your/vault
# Run (HTTP — shared server for multiple agents)
obsidian-mcp --http /path/to/your/vault
Add to your MCP client config (Cursor, Claude Desktop, etc.) and you're done — 19 tools are available immediately.
What It Can Do
Metadata and BM25 indices update immediately through the filesystem watcher. Embeddings reconcile asynchronously through a managed background runtime, so model loading, inference, and cache writes never delay MCP startup or core tools.
Installation
From crates.io
cargo install obsidian-mcp
For semantic search local compatibility mode (OBSIDIAN_SEMANTIC_MODE=local), build with embeddings:
cargo install obsidian-mcp --features embeddings
The embeddings feature adds ~60 MB to the binary (ONNX Runtime). In daemon mode, model/runtime cache is shared under semantic home; local mode keeps in-process embedding support in the MCP binary.
On Windows, local embeddings builds require a recent MSVC Build Tools toolset for ONNX Runtime. MSVC 14.44+ is known to work; older VS 2019-era 14.2x toolsets may fail to link with __std_find_trivial_8. Use --features embeddings-api instead if you want semantic search through an external /v1/embeddings server without bundling ONNX Runtime.
For API embedding backend (OpenAI, Ollama, vLLM, LM Studio, or any /v1/embeddings-compatible endpoint):
cargo install obsidian-mcp --features embeddings-api
This adds no model weight to the binary — embeddings are computed by an external server. Both features can be compiled simultaneously; OBSIDIAN_EMBEDDING_PROVIDER selects which runs.
API backend examples
# OpenAI
OBSIDIAN_EMBEDDINGS=true
OBSIDIAN_EMBEDDING_PROVIDER=api
OBSIDIAN_EMBEDDING_API_KEY=sk-...
OBSIDIAN_EMBEDDING_API_MODEL=text-embedding-3-small
# Ollama (local, no auth)
OBSIDIAN_EMBEDDINGS=true
OBSIDIAN_EMBEDDING_PROVIDER=api
OBSIDIAN_EMBEDDING_API_BASE=http://localhost:11434/v1
OBSIDIAN_EMBEDDING_API_MODEL=nomic-embed-text
OBSIDIAN_EMBEDDING_API_KEY=unused
# Optional tuning for a slow CPU-only embedding server
OBSIDIAN_EMBEDDING_BATCH_SIZE=8
OBSIDIAN_EMBEDDING_API_TIMEOUT_SECS=300
OBSIDIAN_SEMANTIC_CONNECT_TIMEOUT_MS=310000
# vLLM / LM Studio
OBSIDIAN_EMBEDDINGS=true
OBSIDIAN_EMBEDDING_PROVIDER=api
OBSIDIAN_EMBEDDING_API_BASE=http://localhost:8000/v1
OBSIDIAN_EMBEDDING_API_MODEL=BAAI/bge-small-en-v1.5
OBSIDIAN_EMBEDDING_API_KEY=unused
# OpenRouter (25+ embedding models from multiple providers)
OBSIDIAN_EMBEDDINGS=true
OBSIDIAN_EMBEDDING_PROVIDER=api
OBSIDIAN_EMBEDDING_API_BASE=https://openrouter.ai/api/v1
OBSIDIAN_EMBEDDING_API_MODEL=openai/text-embedding-3-small
OBSIDIAN_EMBEDDING_API_KEY=sk-or-v1-...
Smaller batches reduce the work in each embedding request. Increase the API
timeout when a batch can take longer than the default 30 seconds. In daemon mode,
the separate OBSIDIAN_SEMANTIC_CONNECT_TIMEOUT_MS deadline covers the complete
daemon call, including query embedding; allow enough time for that request too.
These values are examples, not throughput guarantees.
The shared daemon uses the embedding configuration from its launch environment.
Connecting another MCP server does not replace a running daemon's provider,
endpoint, model, batch size, or timeout settings. Apply changes to the daemon's
launcher and restart it. Use separate semantic homes for different daemon backend
profiles, or OBSIDIAN_SEMANTIC_MODE=local for per-process configuration.
Pre-built binaries
Grab the latest from GitHub Releases:
Semantic daemon release assets are also published per target:
obsidian-semanticd-<version>-<target>.tar.gz(Unix)obsidian-semanticd-<version>-<target>.zip(Windows)
Install from source (edge)
If you have the repo cloned, you can install the latest main build locally:
./bin/install-edge
This pulls the latest main and runs cargo install --path . --features embeddings.
Semantic Runtime Compatibility
Current semantic daemon API version: 2 (DAEMON_API_VERSION in src/daemon/protocol.rs).
Daemon API v2 applies each search request's optional allowed_paths scope before ranking and distinguishes an unattached vault from an index that is warming. Clients must support v2; a v1 daemon cannot safely serve scoped searches.
Release binaries for Linux, Windows, and Apple Silicon macOS include local and API embedding backends. Intel macOS binaries include the API backend only because the current ONNX dependency has no prebuilt Intel macOS library. Cargo users on Intel macOS should install with --features embeddings-api instead of --features embeddings. Every release archive is smoke-tested on its native target.
Release asset compatibility expectations:
- Daemon auto-install clients expect release assets named
obsidian-semanticd-<version>-<target>.{tar.gz|zip}pluschecksums.sha256. - Plugin releases should publish
main.js,manifest.json, and optionalstyles.cssas GitHub release assets.
Upgrade guidance:
- Use
obsidian-mcp upgradefor Cargo-installedobsidian-mcpandobsidian-semanticdbinaries; it preserves the compiled feature set and reconciles the locally owned daemon. - Upgrade
obsidian-semantic-search-pluginthrough Obsidian's plugin manager. - If versions are skewed, startup/handshake fails with explicit API incompatibility errors rather than silent fallback.
Transport Modes
obsidian-mcp supports two transports. Both are always compiled in — choose the one that fits your workflow.
stdio (default)
The standard MCP transport. The AI client spawns the process and communicates over stdin/stdout.
Use when: your MCP client manages the server process (Cursor, Claude Desktop, most MCP clients).
obsidian-mcp /path/to/vault
Each client connection spawns its own process. Simple, zero-config, works everywhere.
Streamable HTTP
A persistent HTTP server that multiple clients can connect to simultaneously.
Use when: you run multiple headless agents (cursor agent -p, parallel Claude sessions, etc.) against the same vault and want to share a single server process instead of spawning one per agent.
# Foreground (for development / process managers like launchd, systemd)
obsidian-mcp --http /path/to/vault
# Background (ad-hoc daemonize — spawns child, parent exits)
obsidian-mcp serve /path/to/vault
The serve command daemonizes the server and redirects logs to a platform-specific file:
- macOS:
~/Library/Logs/obsidian-mcp.log - Linux:
$XDG_STATE_HOME/obsidian-mcp/obsidian-mcp.log - Windows:
%LOCALAPPDATA%/obsidian-mcp/obsidian-mcp.log
Default: http://127.0.0.1:37842. MCP tools are served at /mcp, health check at /health.
The same MCP endpoint supports server/discover and stateless tool requests with protocol 2026-07-28, plus the legacy initialize and session flow used by 2025-11-25 clients. Modern requests carry the protocol version and client metadata on each call. Reverse proxies must preserve MCP headers and forward an allowed loopback Host to the local server.
A stdio client may probe server/discover and then select the legacy lifecycle
with initialize on the same connection. Discovery alone does not require later
legacy requests to carry modern per-request metadata.
Benefits over stdio for multi-agent setups:
- Shared index — one in-memory BM25/embedding index instead of N copies
- Lower resource usage — single filesystem watcher, single process
- Process independence — server stays up when agents come and go
- Standard observability — HTTP health checks, logging, standard networking
Configure the address:
obsidian-mcp --http --port 9000 --host 0.0.0.0 /path/to/vault
# or via env vars
OBSIDIAN_TRANSPORT=http OBSIDIAN_HTTP_PORT=9000 obsidian-mcp /path/to/vault
Server Management
obsidian-mcp serve /path/to/vault # Start HTTP server in background
obsidian-mcp stop # Stop running server (default port)
obsidian-mcp stop --port 9000 # Stop server on specific port
obsidian-mcp restart /path/to/vault # Stop + start (picks up new binary after upgrade)
serve and restart wait for the server to pass a /health check before reporting success (up to 15s). If the server fails during startup, the exit code and log path are reported.
Client Setup
Cursor (stdio)
~/.cursor/mcp.json:
{
"mcpServers": {
"obsidian": {
"command": "obsidian-mcp",
"args": ["/path/to/your/vault"]
}
}
}
Cursor (HTTP — shared server)
Start the server once:
obsidian-mcp serve /path/to/your/vault
Then point Cursor at it:
{
"mcpServers": {
"obsidian": {
"url": "http://127.0.0.1:37842/mcp"
}
}
}
All Cursor agents (IDE, CLI, headless) share the same server.
Claude Desktop
{
"mcpServers": {
"obsidian": {
"command": "obsidian-mcp",
"env": {
"OBSIDIAN_VAULT_PATH": "/path/to/your/vault",
"OBSIDIAN_EMBEDDINGS": "true"
}
}
}
}
Config file location:
- macOS:
~/Library/Application Support/Claude/claude_desktop_config.json - Windows:
%APPDATA%\Claude\claude_desktop_config.json
Any MCP client
obsidian-mcp supports both stdio and Streamable HTTP MCP transports. Any MCP-compatible client can connect via either method. Pass the vault path as the first argument or via OBSIDIAN_VAULT_PATH.
Running as a Service
For always-on HTTP mode, use your OS process manager with --http (not serve). This lets the process manager handle restarts, logging, and lifecycle — serve daemonizes itself, which conflicts with process managers that expect to own the child process.
macOS (launchd)
Create ~/Library/LaunchAgents/com.obsidian-mcp.plist:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.obsidian-mcp</string>
<key>ProgramArguments</key>
<array>
<string>/Users/YOU/.cargo/bin/obsidian-mcp</string>
<string>--http</string>
</array>
<key>EnvironmentVariables</key>
<dict>
<key>OBSIDIAN_VAULT_PATH</key>
<string>/path/to/your/vault</string>
</dict>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>StandardOutPath</key>
<string>/Users/YOU/Library/Logs/obsidian-mcp/launchd.out.log</string>
<key>StandardErrorPath</key>
<string>/Users/YOU/Library/Logs/obsidian-mcp/launchd.err.log</string>
</dict>
</plist>
Important: launchd does not source your shell profile (
~/.zshrc,~/.bashrc). All environment variables must be defined in the plist'sEnvironmentVariablessection.
# Load
launchctl load ~/Library/LaunchAgents/com.obsidian-mcp.plist
# Unload (stop)
launchctl unload ~/Library/LaunchAgents/com.obsidian-mcp.plist
# A direct, running user LaunchAgent is restarted automatically by:
obsidian-mcp upgrade
Linux (systemd)
Create ~/.config/systemd/user/obsidian-mcp.service:
[Unit]
Description=obsidian-mcp MCP server
After=network.target
[Service]
ExecStart=%h/.cargo/bin/obsidian-mcp --http
Environment=OBSIDIAN_VAULT_PATH=/path/to/your/vault
# Add embedding env vars if needed:
# Environment=OBSIDIAN_EMBEDDINGS=true
# Environment=OBSIDIAN_EMBEDDING_PROVIDER=api
# Environment=OBSIDIAN_EMBEDDING_API_KEY=sk-...
# Environment=OBSIDIAN_EMBEDDING_API_BASE=https://api.openai.com/v1
# Environment=OBSIDIAN_EMBEDDING_API_MODEL=text-embedding-3-small
Restart=on-failure
RestartSec=5
[Install]
WantedBy=default.target
systemctl --user daemon-reload
systemctl --user enable --now obsidian-mcp
# Check status
systemctl --user status obsidian-mcp
# A direct, running user service is restarted automatically by:
obsidian-mcp upgrade
# View logs
journalctl --user -u obsidian-mcp -f
Upgrading
# Inspect the verified install and exact command without changing anything
obsidian-mcp upgrade --dry-run
# Install the latest crates.io version and activate it
obsidian-mcp upgrade
upgrade supports official crates.io installations recorded in Cargo's install tracker. It preserves the current target, build profile, and exact compiled feature set; verifies both shipped binaries after Cargo finishes; restarts directly managed running launchd/systemd user services; and reconciles a locally owned semantic daemon without changing its home, model, or caches. Explicit semantic-daemon overrides and PATH-owned daemons are left alone.
Vault settings and service definitions are never rewritten. Stdio clients cannot be restarted from inside their active protocol session, so reconnect them after an update. An ad-hoc background process started with obsidian-mcp serve must also be restarted with the same arguments or environment; the upgrader cannot safely reconstruct that launch context. If installation succeeds but activation fails, the command exits non-zero with diagnostics and does not silently roll back the Cargo installation.
Homebrew, Nix, source/path, Git, alternate-registry, and pre-built binary installs are rejected before mutation. Upgrade those through the tool that owns the installation.
See Upgrading for the support matrix, guarantees, outcomes, and recovery guidance.
Search
Three tiers, each suited to different needs.
BM25 Full-Text — search_text
On by default. Powered by Tantivy, rebuilt in-memory on startup (< 100 ms for 5k notes).
- Stemming — "deploy" matches "deployment", "deployed", "deploying"
- Field boosts — title 5x, tags 4x, headings 3x, frontmatter 2x, body 1x
- Fuzzy matching —
fuzzy: truetolerates single-character typos (edit distance 1) - Field filtering —
fields: ["title", "tags"]to restrict scope - Returns ranked results with context snippets and match offsets
Semantic — search_semantic
Uses the shared local semantic daemon by default (OBSIDIAN_SEMANTIC_MODE=auto). Results keep the same MCP schema while backend execution can run through daemon or local compatibility mode.
Semantic runtime modes:
auto(default): prefer daemon, fallback to local in-process embeddings only when the daemon is genuinely unavailable and local embeddings are enabled. The local runtime remains dormant while the daemon is healthy or warming. Once activated, local fallback remains selected until the MCP server restarts.daemon: daemon-only path; semantic calls fail clearly if daemon is unavailable. No local embedding runtime is activated.local: force the in-process embedding runtime.
A slow daemon query or a warming index does not establish that the daemon stopped
working. Those errors remain visible and do not activate a competing local index.
If bootstrap or the initial health probe times out, later semantic searches resume
startup and complete the handshake before using that daemon. An already started
process retains its endpoint and startup metadata so recovery does not launch a
second process. Passive vault_info inspection never resumes bootstrap.
Daemon startup policy:
- MCP only initializes/starts the daemon when
OBSIDIAN_WATCH=true. - When
OBSIDIAN_WATCH=false, MCP skips daemon initialization and semantic search must use local mode (if enabled) or returns a clear error.
Embedding lifecycle:
Core note, graph, BM25, write, and MCP initialization behavior stays available in every phase. Repeated note changes are coalesced by path, and the newest write or removal wins over in-flight inference.
Use vault_info to inspect semantic progress without submitting an embedding
query. Its embeddings object identifies source (local or daemon) and
availability (available, disabled, unavailable, unattached, or
unsupported). When a runtime snapshot is available, status contains phase,
queryable, indexed_notes, total_notes, pending_notes, and an optional
last_error. Otherwise, the snapshot is omitted and an error can explain the
unavailable state. available identifies a status snapshot; check queryable to
determine whether it can serve semantic queries. Inspection never attaches a daemon vault or activates local
fallback. Older API v2 daemons without passive status support report unsupported.
Existing vectors can remain indexed while awaiting refresh. The counts describe
the current store and pending work, not a completed percentage or ETA. HTTP
/health continues to describe the local runtime; use vault_info for the selected
semantic backend.
While loading or reconciling, info-level logs report counts and in-flight timing at most once every 30 seconds in addition to lifecycle changes. Debug-level batch logs include request size, duration, outcome, and sanitized failures. Ready and dormant runtimes do not emit idle progress logs.
Embedding caches are versioned and atomically replaced. A cache is reusable only when its backend, canonical model, vector dimension, embedding-input version, and—for API backends—the normalized endpoint fingerprint all match. Legacy payloads, incomplete first-pass checkpoints, corrupt data, and mismatched identities are never published; they trigger a one-time background rebuild. Cache locations do not change: local mode uses {mcp_data}/embeddings/embeddings.bin, while the daemon uses <semantic-home>/vaults/<vault_id>/embeddings.bin. Legacy-location relocation is non-destructive, runs behind the background coordinator, and publishes through the same atomic no-partial-file boundary.
For a remote API, the identity can prove the configured endpoint and model name, but it cannot detect a provider silently replacing model weights behind the same name. Change the configured model identifier (or remove the derived cache) when a provider makes such a revision.
- Finds notes by meaning, not keywords — "making money from software" surfaces notes about monetization
- Hybrid mode —
lexical_prefetch: truecombines BM25 candidate retrieval with semantic re-ranking - Tunable blending —
alphacontrols the weight between lexical and semantic scores (default 0.25) - Daemon cache-location migration is one-way and non-destructive: the active local cache is preferred, then legacy
.obsidian/obsidian-mcp/embeddings.bin; relocation runs in the background, never overwrites an existing daemon cache, and an old payload format is rebuilt rather than trusted
Regex — search_regex
Always available. Full regex syntax for pattern matching across all notes.
When to Use What
Tool Reference
Navigation
vault_list keeps its default JSON string-array response. With include_metadata: true in list mode, each indexed Markdown note instead includes path, filename-stem title, tags, size, and available filesystem timestamps from the existing index without loading note bodies. Directories, non-Markdown files, and unindexed or excluded notes remain path-only objects. Metadata is not supported in tree mode.
Note CRUD
Every tool advertises an object-shaped outputSchema and returns matching structuredContent. Text responses retain their existing format: Markdown for note reads, JSON arrays for searches and listings, and confirmation messages for writes. Structured results use content for text, results for collections, and message for confirmations. vault_info, note_inspect, and note_read_many retain their domain-specific fields; periodic includes an action discriminator.
For frontmatter, structured results use { "action": "get", "frontmatter": ... }, with null when frontmatter is absent, or { "action": "set" | "remove", "message": ... }. Clients consuming the previous bare structured frontmatter object must read structuredContent.frontmatter; the JSON text response is unchanged.
Tools explicitly advertise read-only, destructive, idempotent, and open-world annotations. Mixed-action tools use conservative annotations covering every action: frontmatter is destructive, while periodic can create notes. Semantic search is open-world because it may use a configured external embedding API. These annotations describe behavior; tool filtering and vault path validation enforce access. After upgrading, refresh the connected app in ChatGPT to reload tool schemas and annotations.
note_read_many preserves explicit path order or uses sorted directory order; directory reads are non-recursive by default. It inspects 20 files and returns up to 65,536 content bytes by default, with hard caps of 100 files and 262,144 content bytes. max_bytes counts only the UTF-8 bytes in returned note content. The response includes notes, bounded skipped details (at most 100), total skipped_count, and content_bytes. Oversized notes are skipped—even when first—and can be fetched deliberately with note_read. The typed MCP result is emitted as both structuredContent and compatibility JSON text.
For note_patch heading targets, bare heading text such as "Log" is canonical. ATX marker-prefixed targets such as "## Log" are also accepted, so headings copied from note_inspect with view: "targets" can be used directly.
Search
Introspection & Frontmatter
Graph & Links
Periodic Notes
Utility
Tool Filtering
Control which tools are exposed via the OBSIDIAN_TOOLS environment variable or the X-Obsidian-Tools HTTP header. The header can only further restrict the server-wide tool set; disabled tools are hidden from listings and cannot be called.
For legacy HTTP sessions, send the header with initialize; that filter lasts for the session. With protocol 2026-07-28, send it on every request that needs the restriction. Stateless requests do not retain a previous request's filter. A missing header uses the server-wide tool set; invalid profile names or invalid header encoding return HTTP 400.
The read profile exposes frontmatter as part of the raw Markdown returned by note_read. For structured frontmatter get, set, and remove actions, use full, core, or an explicit allow-list containing frontmatter (for example, OBSIDIAN_TOOLS=note_read,frontmatter). Allowing frontmatter enables all three actions.
Configuration
* Also accepted as the first CLI argument: obsidian-mcp /path/to/vault
Invalid explicit embedding API timeout or batch-size settings are reported as configuration errors. Both binaries read their own process environment; a running shared daemon keeps its launch settings until it is restarted.
Architecture
AI Client(s)
│
├─ stdio (1:1 process) ← default, single-client
│ OR
├─ HTTP POST /mcp (N:1) ← shared server, multi-client
│
▼
obsidian-mcp
├─ tools/ MCP handlers — translate protocol into vault ops
├─ vault/
│ ├─ index In-memory metadata (tags, links, headings)
│ ├─ tantivy BM25 full-text search
│ ├─ embeddings Model + strict cache contracts (optional)
│ ├─ embedding_runtime Background reconciliation + readiness
│ └─ watcher Filesystem events → index sync
├─ models.rs Shared types
└─ error.rs VaultError → MCP ErrorData
│ fs
▼
Vault directory (.md files + .obsidian/)
The vault layer is a pure Rust library with no knowledge of MCP. The tools layer is a thin adapter. This separation means the vault code is independently testable and reusable.
In HTTP mode, each legacy session or modern stateless request gets its own handler instance, but all handlers share a single Vault (thread-safe through shared state). One filesystem watcher, one BM25 index, and one managed embedding coordinator/store serve all connected agents.
Development
cargo build # default build
cargo build --features embeddings # with local semantic search (fastembed)
cargo build --features embeddings-api # with API semantic search (OpenAI-compatible)
# stdio mode (default)
OBSIDIAN_VAULT_PATH=~/vault cargo run
# HTTP mode
OBSIDIAN_VAULT_PATH=~/vault cargo run -- --http
cargo fmt --check && cargo clippy && cargo test
npx @modelcontextprotocol/inspector cargo run # interactive testing (stdio)
License
For suspected vulnerabilities, follow the security reporting policy.