Install
$ agentstack add mcp-yuchi-chang-obsidian-mcp ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ● Shell / process execution Used
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
obsidian-mcp
MCP server that wraps the official Obsidian CLI so an LLM agent can drive a running Obsidian instance — read/write notes, search, manage frontmatter, navigate links, run plugins, and more.
This server is a thin, comprehensive wrapper. Every tool maps 1:1 to an obsidian CLI command.
Prerequisites
- Obsidian must be running. The CLI talks to the live app over IPC; it does not read the vault on disk directly.
- Register the CLI binary. In Obsidian: Settings → General → Command line interface → Register CLI. Obsidian will add
obsidianto yourPATH. - Verify:
obsidian versionprints the CLI version.
Install
Two paths depending on whether you want to build it yourself or grab a pre-published version from npm.
Option A — Clone & build (works today)
Clone the repo and build locally, then point Claude Code at the built file:
git clone https://github.com/yuchichang/obsidian-mcp.git
cd obsidian-mcp
npm install
npm run build
Register it with Claude Code (one command):
# Add (user scope — available across all projects)
claude mcp add -s user obsidian -- node /absolute/path/to/obsidian-mcp/dist/index.js
# Remove
claude mcp remove obsidian
# List configured servers
claude mcp list
-s user registers it for your whole user account. Use -s project to commit it to the repo's .mcp.json instead, or -s local for the current project only (default).
Or write it into .mcp.json manually:
{
"mcpServers": {
"obsidian": {
"command": "node",
"args": ["/absolute/path/to/obsidian-mcp/dist/index.js"]
}
}
}
Option B — Install from npm (zero-build)
> Prerequisite: the package must already be published to npm. The maintainer publishes once via npm publish; all subsequent users get it via npx automatically. If you forked this repo and want this flow under your own scope, change name in package.json to @/obsidian-mcp, then npm publish.
Once published, no clone or build needed:
claude mcp add -s user obsidian -- npx -y @yuchichang/obsidian-mcp
Or in .mcp.json / Claude Desktop's claude_desktop_config.json:
{
"mcpServers": {
"obsidian": {
"command": "npx",
"args": ["-y", "@yuchichang/obsidian-mcp"]
}
}
}
Override the CLI path
If obsidian isn't on PATH, set the OBSIDIAN_CLI env var. Works with either install path:
{
"mcpServers": {
"obsidian": {
"command": "node",
"args": ["/absolute/path/to/obsidian-mcp/dist/index.js"],
"env": {
"OBSIDIAN_CLI": "C:/Users/you/AppData/Local/Obsidian/obsidian.cmd"
}
}
}
}
Tools
Vault & files
| Tool | Wraps | |---|---| | obsidian_list_files | obsidian files | | obsidian_list_folders | obsidian folders | | obsidian_read_note | obsidian read | | obsidian_get_metadata | obsidian file | | obsidian_create_note | obsidian create | | obsidian_append_note | obsidian append | | obsidian_prepend_note | obsidian prepend | | obsidian_move_note | obsidian move | | obsidian_delete_note | obsidian delete (permanent flag supported) |
Frontmatter properties
| Tool | Wraps | |---|---| | obsidian_get_properties | obsidian properties | | obsidian_set_property | obsidian property:set | | obsidian_remove_property | obsidian property:remove |
Search
| Tool | Wraps | |---|---| | obsidian_search | obsidian search | | obsidian_search_context | obsidian search:context |
Tags & links
| Tool | Wraps | |---|---| | obsidian_list_tags | obsidian tags | | obsidian_files_with_tag | obsidian tag | | obsidian_rename_tag | obsidian tags:rename | | obsidian_get_links | obsidian links | | obsidian_get_backlinks | obsidian backlinks | | obsidian_list_unresolved | obsidian unresolved | | obsidian_list_orphans | obsidian orphans |
Daily notes
| Tool | Wraps | |---|---| | obsidian_daily_read | obsidian daily:read | | obsidian_daily_append | obsidian daily:append | | obsidian_daily_path | obsidian daily:path |
Plugins
| Tool | Wraps | |---|---| | obsidian_list_plugins | obsidian plugins | | obsidian_enable_plugin | obsidian plugin:enable | | obsidian_disable_plugin | obsidian plugin:disable | | obsidian_reload_plugin | obsidian plugin:reload |
Developer / advanced
| Tool | Wraps | Notes | |---|---|---| | obsidian_eval | obsidian eval | ⚠️ Runs arbitrary JS inside Obsidian. Treat as destructive. | | obsidian_dev_screenshot | obsidian dev:screenshot | Returns base64 PNG. | | obsidian_dev_errors | obsidian dev:errors | | | obsidian_dev_console | obsidian dev:console | |
Meta
| Tool | Wraps | |---|---| | obsidian_topic_stats | reports the persistent topic → folder map for a vault | | obsidian_register_topic | binds a topic to a folder (no prompt) | | obsidian_remove_topic | removes a topic from the persistent store | | obsidian_scan_root | lists root-level notes with preview for bulk-organize | | obsidian_organize_apply | validates + applies a routing plan (dry-run supported) | | obsidian_version | obsidian version | | obsidian_help | obsidian help |
Conventions
- Targeting a note — file-targeting tools accept either:
file— wikilink-style note name (e.g."My Note"), orpath— vault-relative file path (e.g."Folder/My Note.md").- Multi-vault setups — every tool accepts an optional
vaultparameter. When omitted, the most recently focused vault is used. - Output format — list/search/metadata tools default to JSON for easy machine parsing.
Sensitive operations & user confirmation
The following tools are gated behind a user-confirmation step:
| Tool | Reason | |---|---| | obsidian_delete_note | Removes data (especially with permanent: true). | | obsidian_move_note | Renames + rewrites wikilinks across the vault. | | obsidian_remove_property | Removes frontmatter data. | | obsidian_rename_tag | Bulk-rewrites tags across every note. | | obsidian_enable_plugin | Grants a community plugin code execution. | | obsidian_eval | Runs arbitrary JavaScript inside Obsidian. |
How the gate works:
- MCP elicitation (preferred). If the connected client supports the
elicitationcapability (Claude Code does), the server sends anelicitation/createrequest and the client shows the user a Proceed? prompt with the action and target spelled out. Onlyaccept + confirm: trueproceeds. - Explicit
confirm: trueparameter. Every sensitive tool's input schema includes an optionalconfirm: boolean. Passingconfirm: trueskips the elicitation prompt — use this only when the caller has already obtained user approval. - Refusal fallback. If the client doesn't support elicitation and
confirm: truewas not provided, the tool returns anisErrorresult that names the action and instructs the caller to retry withconfirm: true.
Bypass for batch / automation
OBSIDIAN_MCP_AUTO_CONFIRM=1
Set this env var (in your MCP client's env block) to skip every confirmation prompt. Use only in fully-trusted automation contexts.
Topic → folder routing (vault-aware, persistent)
The MCP runs a per-vault topic store at ~/.obsidian-mcp//topic-map.json. It learns where each topic of note belongs and reuses that decision next time.
Agent: create_note(path="kungpao.md", topic="recipe-chinese", vault="MyVault")
↓
MCP: is "recipe-chinese" already in the store?
├── yes → use the stored folder, increment usage, write
└── no → scan vault for similar folders ("Recipes/Chinese", "食譜/中式" …)
→ MCP elicits the user: "Where should 'recipe-chinese' notes live?
Suggestions: ..."
→ user types or picks a folder
→ MCP records the route, then writes
What lives where, and why:
- The persistent store, not env vars, is the source of truth — the MCP is the only thing that sees vault state across sessions, so storing the conventions there is what gives this layer its leverage.
- The user is asked once per topic; subsequent notes for the same topic land silently.
- Folders are auto-created by the Obsidian CLI as deep as needed — no
mkdirfrom the MCP.
Resolution order
| | Condition | Action | |---|---|---| | 1 | path contains / | Used as-is, topic ignored. | | 2 | topic present in store | Reuse stored folder, increment usage. | | 3 | folder arg passed alongside topic | Treat as pre-decided; record in store. | | 4 | topic unknown, client supports elicitation | Scan vault, prompt user, record answer. | | 5 | topic unknown, no elicitation | Auto-create / folder, record, hint at similar existing folders in the response. | | 6 | No topic, no folder | Write at vault root. |
Topic-store tools
| Tool | What it does | |---|---| | obsidian_topic_stats | Show the learned map for a vault, sorted by usage. | | obsidian_register_topic | Bind topic → folder programmatically (no elicitation). | | obsidian_remove_topic | Forget a topic from the store (existing notes untouched). |
Theory pointers
Faceted folder routing here is the simplest slice of a much larger idea. Worth reading if you want to push further:
- Ranganathan, S.R. (1933) Colon Classification — PMEST facets
- Ranganathan, S.R. (1931) Five Laws of Library Science
- Tiago Forte (2022) Building a Second Brain — PARA method (actionability axis)
- Niklas Luhmann (1981) "Kommunikation mit Zettelkästen" — graph-over-tree
- Bates, M.J. (1989) "The design of browsing and berrypicking"
Bulk organize root notes
When the vault root accumulates loose .md files, a caller LLM can sweep them into the right subfolders in three steps:
- Scan — list root notes with metadata + body preview:
``jsonc // tool: obsidian_scan_root { "ignore": ["Daily/*", "*.excalidraw.md"] } ``
- Classify (caller side) — the LLM reads each preview and proposes a routing plan:
``jsonc [ { "path": "WebRTC 連線建立流程.md", "target_folder": "webrtc", "topic": "webrtc", "reason": "covers signaling/SDP/ICE" }, { "path": "舊筆記.md", "target_folder": "Notes", "topic": "misc" } ] ``
- Apply — dry-run first to preview, then call again with
dry_run: false:
```jsonc // tool: obsidianorganizeapply { "plan": [...], "dryrun": true } // → { "summary": { "willmove": 2, "willcreatefolders": 1, ... }, "items": [...] }
{ "plan": [...], "dry_run": false, "confirm": true } // → moves files, creates new folders as needed, registers topic→folder mappings ```
Per-entry failure isolation: a single move failure marks that entry status: "failed" without aborting the rest of the batch. Successful moves with a topic field are recorded in the persistent topic store, so future single-note writes for that topic auto-route.
Long content & argv limits
The Obsidian CLI does not (yet) support reading parameter values from stdin or from files — every value travels on the command line. That collides with platform limits:
| Platform | Practical command-line limit | |---|---| | Windows (cmd.exe) | ~8,191 chars total | | macOS / Linux | ARG_MAX (typically 128 KB – 2 MB) |
To stay safe, the server automatically chunks long writes:
| Tool | Chunking strategy | |---|---| | obsidian_create_note | First chunk via create, remaining chunks via append. | | obsidian_append_note | Sequential append calls. | | obsidian_prepend_note | prepend calls in reverse order so final order is preserved. | | obsidian_daily_append | Resolves the daily note path, then chunked append. | | obsidian_eval | Not chunked — JS can't be split. Returns an error suggesting the script-via-note workaround. |
Splits happen at line boundaries when possible; oversized single lines fall back to UTF-8-safe character boundaries. Reassembled content is byte-identical to the original.
Configure the per-call byte threshold (defaults: 6,000 on Windows, 100,000 elsewhere):
OBSIDIAN_MCP_MAX_ARG_BYTES=4000
If a chunk in the middle of a multi-chunk write fails, the server returns isError with a clear message stating which chunks made it to disk so the caller can recover.
Develop
npm run dev # tsc --watch
npm run inspect # launch MCP Inspector against the built server
node scripts/smoke-test.mjs # initialize + tools/list smoke test
How it works
runObsidian() (src/exec.ts) shell-quotes arguments, invokes the obsidian binary via child_process.exec, and parses stdout. Most read-style tools request format=json; results are parsed to structuredContent for clients that consume structured tool output, while still returning a text representation in content.
Tool registry lives in src/tools.ts — adding a new wrapped command is a single entry there.
Reference
- Obsidian CLI: https://obsidian.md/help/cli
- MCP spec: https://modelcontextprotocol.io
Source & license
This open-source MCP server is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: yuchi-chang
- Source: yuchi-chang/obsidian-mcp
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.