Obsidian MCP - Technical Architecture

Claude Desktop extension for Obsidian. Zero plugins required.

See also: docs/user-story.html for vision and user journey.

System Overview

flowchart TB subgraph "User's Machine" CD[Claude Desktop] MCP[obsidian-mcp server] OBS[Obsidian App] FS[(Filesystem)] CFG[obsidian.json] end CD -->|MCP Protocol| MCP MCP -->|Read config| CFG MCP -->|Read/Write| FS MCP -->|obsidian:// URI| OBS OBS -->|Reads/Writes| FS

The Hybrid Approach

We use two native interfaces that require no plugins:

flowchart LR subgraph "Data Operations" READ[Read Notes] WRITE[Write Notes] SEARCH[Search] end subgraph "Action Operations" OPEN[Open in Obsidian] CREATE[Create via Obsidian] DAILY[Daily Note] end FS[(Filesystem)] --> READ FS --> WRITE FS --> SEARCH URI[obsidian:// URI] --> OPEN URI --> CREATE URI --> DAILY

Package Architecture

flowchart TB subgraph "PyPI: obsidian-sdk" CLIENT[ObsidianClient] VAULT[VaultDiscovery] NOTES[NotesManager] SEARCH[SearchEngine] URI[URIHandler] end subgraph "PyPI: obsidian-mcp" SERVER[MCP Server] TOOLS[MCP Tools] end subgraph "Distribution" UVX[uvx obsidian-mcp] end SERVER --> CLIENT TOOLS --> CLIENT CLIENT --> VAULT CLIENT --> NOTES CLIENT --> SEARCH CLIENT --> URI UVX --> SERVER

Vault Discovery

Obsidian stores vault paths in obsidian.json:

OS Config Path
Linux ~/.config/obsidian/obsidian.json
macOS ~/Library/Application Support/obsidian/obsidian.json
Windows %APPDATA%/obsidian/obsidian.json
sequenceDiagram participant User participant MCP as obsidian-mcp participant Config as obsidian.json participant FS as Filesystem User->>MCP: list_vaults() MCP->>Config: Read obsidian.json Config-->>MCP: {"vaults": {"id": {"path": "/path"}}} MCP->>FS: Verify paths exist FS-->>MCP: Paths validated MCP-->>User: [Vault1, Vault2, ...]

MCP Tools

Tool Description Method
list_vaults List all discovered Obsidian vaults Read obsidian.json
list_notes List notes in a vault/folder Filesystem glob
read_note Read note content Filesystem read
write_note Create or update a note Filesystem write
search_notes Full-text search across vault Filesystem grep
open_in_obsidian Open note in Obsidian app obsidian://open URI
create_daily_note Open/create today's daily note obsidian://daily URI

Native URI Scheme

flowchart LR subgraph "obsidian:// Actions" OPEN["obsidian://open?vault=X&file=Y"] NEW["obsidian://new?vault=X&name=Y&content=Z"] SEARCH["obsidian://search?vault=X&query=Y"] DAILY["obsidian://daily?vault=X"] end subgraph "Result" APP[Obsidian App] end OPEN -->|Opens note| APP NEW -->|Creates note| APP SEARCH -->|Opens search| APP DAILY -->|Opens daily| APP
Limitation: URI scheme is fire-and-forget. We can trigger actions but can't get data back. That's why we use filesystem for reads.

Component Status

Component Status Notes
Vault Discovery VALIDATED Linux POC complete. See poc/vault_discovery.py
Note Read/Write VALIDATED Read + Write POC complete. See poc/notes_*.py
Search VALIDATED In-memory index chosen. See poc/notes_search.py
URI Handler VALIDATED Cross-platform URI opening via obsidian://
MCP Server VALIDATED Full MCP server with all tools
PyPI Package VALIDATED Published as mcp-obsidian-vault on PyPI

POC Findings

Vault Discovery (Issue #1)

Validated on Linux (WSL2). See poc/vault_discovery.py.

Finding Details
JSON Structure Confirmed {"vaults": {"<id>": {"path": "...", "ts": ..., "open": bool}}}
Vault ID Opaque string (likely UUID or hash). Use path.name for display name.
"open" field Optional. Defaults to false when missing.
Vault paths Absolute paths. May or may not exist on filesystem.

Edge cases handled:

Cross-platform notes:

Note Reading (Issue #5)

Validated on Linux (WSL2). See poc/notes_reader.py.

Finding Details
Glob pattern **/*.md finds all notes recursively
Hidden files Skip files/folders starting with . (e.g., .obsidian/)
Encoding UTF-8 works for standard Obsidian notes
Performance 1000 notes listed in ~0.085s, read in ~0.021s

API Design:

Edge cases handled:

Note Writing (Issue #6)

Validated on Linux (WSL2). See poc/notes_writer.py.

Finding Details
Atomic writes Write to temp file, then os.replace() for crash safety
Path validation Resolve paths and check they stay within vault bounds
Auto-create dirs Parent directories created automatically
Performance 10MB file written in ~0.01s

API Design:

Security:

Full-Text Search (Issue #7)

Benchmarked three approaches. See poc/search_comparison.py.

Approach Init Time Storage Avg Query Best For
Grep (regex) 0ms None ~88ms Small vaults (<100 notes)
In-Memory Index ~120ms ~5MB RAM ~2.6ms Medium vaults, speed priority
SQLite FTS5 ~120ms ~650KB disk ~2.9ms Large vaults, persistence

Decision: In-Memory Index

API Design:

Open Technical Questions

  1. Frontmatter parsing: Should we parse YAML frontmatter or treat notes as plain text?
  2. File watching: Should we detect changes while running?
  3. Conflict handling: What if Obsidian and MCP write simultaneously?