obsidian-mcp
# obsidian-mcp
**Let Claude (or any MCP client) read, search, edit, and reorganize your Obsidian vault — safely.**
A [Model Context Protocol](https://modelcontextprotocol.io) server that treats your vault as what it really is: a folder of markdown files. No plugins, no cloud, no database. Obsidian can be closed while Claude works on your notes.
The implementation follows the SDK's current recommended API (`McpServer` + zod-validated tool contracts) — and pins its protocol behavior with integration tests that speak **raw JSON-RPC to the real process**, so SDK upgrades can't silently change the wire. If you want to understand what an MCP server actually does beyond `npx some-server`, start with [test/server.test.ts](test/server.test.ts) next to the [specification](https://modelcontextprotocol.io/specification).
## What you can ask Claude
> *"How many notes are in my vault, and where are the newest ones?"*
> *"Find every note that mentions 'spaced repetition' and summarize the key ideas."*
> *"Create `research/glossary.md` with these ten terms and tag it `#glossary`."*
> *"Rename `daily/2026-09-06.md` to `journal/2026-09-06.md` — don't break any links."*
> *"Which notes link to my reading list? Show the exact lines."*
> *"Research solid-state batteries on the web, then create `research/solid-state-batteries.md` — summary, key players, open questions, and frontmatter with the sources."*
## The 11 tools
| Tool | What it does |
| --- | --- |
| `vault_info` | Vault path + note count — the "is it alive?" call |
| `list_notes` | Browse folders, with sizes and modification dates |
| `read_note` | Full markdown content of one note |
| `search_notes` | Full-text search (case-insensitive substring or regex), with `path:line` references |
| `get_frontmatter` | A note's YAML properties as JSON — cheaper than reading the whole note |
| `create_note` | New note, parent folders auto-created, optional YAML frontmatter generated |
| `edit_note` | Append, prepend (after frontmatter), find & replace, or replace a heading section |
| `delete_note` | To `.trash/` — **never** a permanent delete |
| `move_note` | Rename/move and **repair every link** across the vault |
| `get_backlinks` | What references this note? (wikilinks, embeds, relative markdown links) |
All read-only tools are annotated `readOnlyHint: true`, so MCP clients can skip approval prompts for them and only ask about writes.
## Safety model
Your vault is irreplaceable; the design starts there.
- **No path escapes.** Every user-supplied path goes through one guard (`safeResolve`); `../../../etc/passwd` comes back as a contained error, never a read
- **Deletes are reversible.** Notes move to the vault's `.trash/` (Obsidian's own convention) — never `rm`
- **No silent clobbers.** `create_note` fails if the note exists, unless you explicitly say `overwrite`
- **Kernel-atomic where possible.** Creates use the `wx` flag (no check-then-write race); deletes and moves are `rename()` calls
- **Link-safe moves.** Moving a note rewrites inbound links in other notes *and* rebases the moved note's own relative links — aliases and `#anchors` preserved
- **Empty-folder cleanup** after deletes/moves uses non-recursive `rmdir` up the tree: the kernel refuses anything non-empty, so cleanup is data-loss-proof by construction
The one rule that makes this work: the vault folder is the **only** thing this server can touch.
## Quick start
Requires Node 20+.
```sh
git clone https://github.com/laurentiudanielgi-coder/obsidian-mcp.git
cd obsidian-mcp
npm install && npm run build
```
### Claude Desktop
Edit `claude_desktop_config.json` (`~/Library/Application Support/Claude/` on macOS):
```json
{
"mcpServers": {
"obsidian": {
"command": "/absolute/path/to/node",
"args": ["/absolute/path/to/obsidian-mcp/dist/index.js"],
"env": { "OBSIDIAN_VAULT_PATH": "/absolute/path/to/your/vault" }
}
}
}
```
Notes: use `which node` for the absolute path — GUI apps don't inherit your shell's PATH. Fully quit and reopen Claude Desktop. In Obsidian, set *Files & Links → Deleted files → .trash folder* so trash semantics match. And back your vault up (git works beautifully) — safety features are seatbelts, not brakes.
### Any other MCP client
The server is configured with one environment variable: `OBSIDIAN_VAULT_PATH`, pointing at the vault root. It speaks JSON-RPC over stdio — the default transport for locally-spawned MCP servers.
## How it's built
```
src/
├── index.ts — entrypoint: transport wiring; why stdout is the protocol channel
├── server.ts — protocol layer: handshake, capabilities, tools/list, tools/call
├── config.ts — env-var config; why clients spawn servers and pass settings via env
└── vault.ts — the only code that touches files: path guard, trash, links, edits
```
Architecture in one sentence: **JSON-RPC messages** arrive over a **transport** (stdio), get dispatched by the **protocol layer** to tool handlers, which delegate every filesystem operation to the **vault layer** — the single choke point where safety lives.
### Code tour
The codebase doubles as a guided tour:
| Concept | Where to look |
| --- | --- |
| The `initialize` handshake, capabilities, wire frames | `test/server.test.ts` |
| Tool contracts: descriptions are written *for the model*; zod schemas | `src/server.ts` |
| Tool errors vs protocol errors (two failure channels) | `src/server.ts`, the `guarded` wrapper |
| Server config via env vars (clients spawn servers) | `src/config.ts` |
| Stdio framing and the stdout-is-protocol rule | `src/index.ts` |
| The traversal guard when an LLM builds the paths | `src/vault.ts`, `safeResolve` |
| Obsidian link resolution & unique-basename rule | `src/vault.ts`, `linkMatches` |
| Raw wire format | `test/server.test.ts` — speaks JSON-RPC to the real process |
### Development
```sh
npm test # unit tests against real temp filesystems + wire-level tests speaking raw JSON-RPC
npm run dev # tsc --watch
```
Two probe scripts exist for debugging clients against the server:
```sh
node scripts/big-create-test.mjs 9000 # create a 9,000-char note, time it
node scripts/create-from-json.mjs note.json # replay an exact client payload
```
### Roadmap
- [x] CRUD, search, frontmatter, backlinks, link-repairing moves
- [ ] RAG: heading-aware chunking → local embeddings → sqlite-vec → `semantic_search`
- [ ] Streamable HTTP transport (same server, new transport — proving the decoupling)
Full decision log and reasoning in [PLAN.md](PLAN.md).
## License
[MIT](LICENSE)
TDQS
Scored across 10 tools
Each tool maps cleanly to a distinct resource/action: note content, frontmatter, backlinks, vault info, and file operations. Even close pairs like get_frontmatter vs read_note are explicitly differentiated by cost and scope.
Most tools follow a consistent verb_noun snake_case pattern (read_note, create_note, delete_note, move_note). vault_info is a minor outlier since it lacks a get_ or other verb prefix, but the overall pattern remains predictable and readable.
Ten tools is well-scoped for an Obsidian vault server. Each tool covers a distinct core operation with no redundancy or unnecessary bloat.
The toolset provides complete note lifecycle coverage: create, read, update, delete, move, search, plus Obsidian-specific needs like frontmatter and backlinks. Folder handling is implicit in move/create, so there are no obvious dead ends for the implied domain.