Headless Obsidian MCP
Headless Obsidian MCP Server gives AI assistants full, structure-aware access to an Obsidian vault for searching, browsing, traversing, querying, auditing, and optionally editing notes—without needing Obsidian running.
Search:
search_notesfor literal/regex ripgrep searches with scoping, pagination, and context;search_notes_rankedfor BM25 relevance-ranked full-text;resolve_noteandresolve_daily_notefor human-friendly name resolution.Browse & Read:
read_notes,list_notes,list_files,list_folders;get_outline,read_section;get_frontmatter,get_property;list_templates,list_tasks.Traverse:
get_links(outbound wikilinks, unresolved links, backlinks);get_related_notes(ranked by shared tags, links, co-citations).Query & Filter:
query_noteswith frontmatter operators (eq,ne,gt,contains, etc.);find_by_tag;list_recent_notes;list_tags,list_properties,list_property_values.Audit:
get_vault_stats(overall health);list_vault_issues(orphans, broken wikilinks, dead anchors, conflicts).Editing (opt-in): Create, append, prepend, move, delete notes; surgical tag/frontmatter/section edits; bulk edit; folder management. Moving/renaming auto‑updates inbound links; deletions go to
.trash; write operations report any broken links they introduce.Tasks: List checkbox tasks and toggle their state.
Templates: List and apply Obsidian core templates with variable expansion.
Git Sync (opt-in): Auto‑commits after writes; pull/push; non‑destructive merge conflict resolution.
Safety & Config: Read‑only by default; writes are opt‑in and per‑tool group restrictable;
get_configalways available to report tool policy, git sync status, and template settings.
Provides tools for interacting with an Obsidian vault, enabling full-text search, link graph navigation, tag management, structure-aware editing, and vault hygiene operations.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@Headless Obsidian MCP search my vault for meeting notes"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
Headless Obsidian MCP
An MCP server that gives AI assistants full access to an Obsidian vault — search, the link graph, tags, and structure-aware editing — without needing a running copy of Obsidian. Ideal for a headless server running your Hermes or OpenClaw agents.
It reduces token cost and removes the need for your agent to understand Obsidian vault conventions, so your agent can read your vault as a knowledge base rather than a folder of text files: [[wikilinks]] resolve the way Obsidian resolves them, tags unify inline #tags with frontmatter, and edits are surgical (change one section or one tag without rewriting the note).
Read-only by default. Out of the box the server exposes only read tools; writing is opt-in through a single environment variable.
Status: 0.x. The server is tested and in daily use, but the tool surface is still settling — tool names, parameters, and return shapes may change between releases. Following semver's 0.x convention, breaking changes bump the minor version (0.2.0) and fixes bump the patch (0.1.1). Pin a version if you depend on a specific tool contract.
What it can do
Literal/regex search via ripgrep ( | |
List notes, folders, attachments, tags, and frontmatter properties. Read a note's outline or one section without loading the whole thing. | |
Resolve | |
Filter notes by frontmatter conditions ( | |
Whole-vault stats, then drill into the actual orphaned notes, broken wikilinks, and dead heading anchors. | |
Edit (opt-in) | Create, append, prepend, move, and delete notes. Change a tag, a frontmatter field, or one section without rewriting the note. Bulk-edit many notes in one call. Create, move, and delete folders too. See Enabling writes. |
Moving a note rewrites the wikilinks that point to it; renaming a heading rewrites inbound anchors. Every write reports any broken links it introduced. Deletes go to | |
Sync (opt-in) | If the vault is a git repo, snapshot every write as a commit — optionally pulling and pushing a remote per write or on a background timer. Merge conflicts are resolved non-destructively: your version is always preserved. See Git safety net. |
Templates from Obsidian's core Templates plugin are supported ({{title}}, {{date:FORMAT}}, …); Templater scripting is not.
→ Full tool reference — all 49 tools with parameters and return shapes.
Related MCP server: Obsidian MCP Tool Server
Quick start
Requires Node.js 20+, ripgrep, and an Obsidian vault.
git clone <this-repo> && cd headless-obsidian-mcp
npm install && npm run build
export OBSIDIAN_VAULT_PATH="/path/to/your/vault"
npm startThen point an MCP client at it. For Claude Desktop (~/.config/claude/claude_desktop_config.json on macOS/Linux, %APPDATA%\Claude\claude_desktop_config.json on Windows):
{
"mcpServers": {
"obsidian": {
"command": "node",
"args": ["/absolute/path/to/headless-obsidian-mcp/dist/index.js"],
"env": {
"OBSIDIAN_VAULT_PATH": "/absolute/path/to/your/vault"
}
}
}
}Restart the client and the server appears as obsidian. To let the agent edit the vault, add "OBSIDIAN_TOOLS": "all" to that env block — see Enabling writes.
Ready-to-copy configs for Claude Desktop, Claude Code, and Docker are in examples/.
Configuration
Only OBSIDIAN_VAULT_PATH is required. .env.example documents all eight variables the server reads.
Nothing here loads a
.envfile — an MCP server inherits its environment from the client that spawns it. Set variables in your client'senvblock (or pass--env-fileto Docker).
Enabling writes
OBSIDIAN_TOOLS selects exactly which tools the server exposes. Unset, it means reads: the 24 write tools are hidden from the tool list, and calling one is rejected.
OBSIDIAN_TOOLS="all" # everything
OBSIDIAN_TOOLS="reads,tasks.write,sections.write" # read all; write only tasks and sections
OBSIDIAN_TOOLS="all,-bulk,-delete_note" # everything except the destructive ones
OBSIDIAN_TOOLS="-templates,-tasks" # reads, minus two groups
OBSIDIAN_TOOLS="search,notes.read" # minimal search-and-read agentSelectors are case-insensitive and applied left to right — a plain token adds, a - prefix subtracts. Valid tokens are the meta-groups all / reads / writes, a domain group, a mode slice like notes.write, or an individual tool name. Evaluation starts from nothing, unless the first token subtracts, in which case it starts from reads — so -templates trims the read surface and can never accidentally expose writes.
The eleven domain groups:
Group | Read | Write |
| search_notes, search_notes_ranked | — |
| read_notes, list_notes, list_recent_notes, resolve_note, resolve_daily_note | write_note, append_note, prepend_note, patch_note, delete_note, move_note |
| get_outline, read_section | add_section, append_to_section, replace_section, rename_section |
| get_links, get_related_notes | — |
| list_tags, find_by_tag | add_tag, remove_tag |
| get_frontmatter, list_properties, list_property_values, query_notes, get_property | set_frontmatter, add_property_values, remove_property_values, rename_property |
| list_tasks | set_task_state |
| list_templates | apply_template, insert_template |
| list_files, list_folders | move_file, create_folder, move_folder, delete_folder |
| get_vault_stats, list_vault_issues | — |
| — | bulk_edit |
get_config sits in no group and is always exposed — its tools section reports the active policy, so an agent can discover why a tool is missing.
Excluding tools also saves tokens: an excluded tool is a schema the client never carries in context.
An unknown selector, or a policy that selects nothing, aborts startup with the valid vocabulary listed. The policy gates the MCP server only; the query CLI is the operator's own tool and ignores it.
Git safety net (OBSIDIAN_GIT_SYNC)
If your vault is a git repository, the server can snapshot every write:
Mode | Behavior |
| No git involvement. |
| Commits after every write, with a message naming the tool. No remote. |
| Commits, then pulls and pushes the remote after each write. |
| Commits per write; pulls and pushes on a background interval instead. |
OBSIDIAN_GIT_SYNC_INTERVAL sets the timer cadence in seconds (default 300); OBSIDIAN_GIT_REMOTE names the remote (default origin).
The guard is fail-closed: in any mode but off, a write is refused before touching disk if the vault isn't a usable git repo, and a failed post-write commit throws — a write never lands without its snapshot. The lone exception is the background timer tick, whose failures are recorded in get_config's sync.last_error rather than thrown, since no write is in flight to fail.
Conflicts are never blocking or destructive. On a real merge conflict, per file: if both sides changed the note, your version is preserved as a <note> (conflicted YYYY-MM-DD HHMMSS) copy and the canonical path takes the remote's; if the remote deleted a note you'd modified, the same copy preserves your version; if you deleted a note the remote modified, the remote version is restored. Find unreconciled copies with list_vault_issues kind:"conflicts".
Docker
The Dockerfile builds a multi-stage node:20-alpine image with ripgrep and git installed — both are hard runtime dependencies, not conveniences.
docker build -t headless-obsidian-mcp .
docker run -i --rm -v "$HOME/vault:/vault:ro" headless-obsidian-mcp-i is load-bearing: this is a stdio server, and without an open stdin the transport never comes up. The vault mounts at /vault and the container runs as the unprivileged node user. For writes, drop :ro and add --user "$(id -u):$(id -g)" so new files land owned by you. Client config: examples/mcp.docker.json.
There's no docker-compose.yml by design — an MCP stdio server is spawned per client, not supervised as a service.
Development
npm run dev # watch mode via tsx, no build step
npm run build # compile to dist/
npm test # node:test via tsx
npm run query # query CLI — see docs/CLI.mdmise run <task> works for each if you use mise.
The query CLI calls the tools directly and prints raw JSON, which makes it the fastest way to try something without wiring up a client.
Tests build a throwaway fixture vault in a temp directory and cover link resolution, tag aggregation, listing and recency, index cache invalidation, and the security guards (path traversal, symlink escapes, frontmatter hardening). CI runs build and tests on Node 20 and 22 for every PR and push to main, installing real ripgrep and a git identity — the suite drives the actual rg binary and real repositories rather than stubs.
Design rationale, tool-naming taxonomy, and the invariants that keep the tool surface coherent live in CLAUDE.md.
Agent skill
skills/obsidian-vault/SKILL.md is a copyable Agent Skill — drop it in ~/.claude/skills/obsidian-vault/ to give an agent workflow guidance for this server: which tool answers which intent, anti-patterns worth avoiding, and multi-step recipes for fixing broken links, processing the daily note, and restructuring without breaking the graph.
Acknowledgments
This project began as a Node.js port of notes-mcp by Boaz Yaniv, and has since been substantially extended with knowledge-base, structure-aware editing, and vault-management tools. The original is MIT licensed; that license and copyright are retained in LICENSE.
Thanks also to mcpvault by bitbonsai, whose Obsidian MCP server was a useful reference while shaping this project's tool surface.
License
Released under the MIT License.
Available Tools
25 toolsfind_by_tagA
Find notes matching one or more tags, as note headers. High-precision retrieval based on human curation. Narrow further with folder and a frontmatter where filter (all conditions apply); match governs the tag set only.
Inputs: tags (required), limit, match, where, +2 more.
get_configA
Report the server's own configuration (not vault contents). Returns { template: { folder, date_format, time_format }, daily: { folder, format, template }, writes: { writes_enabled, git_sync }, sync: { mode, interval, remote, last_sync, last_error }, vault: { path }, tools: { policy, exposed, excluded } }. Optional section narrows the result to one unwrapped section. template.folder and daily.folder are null when unconfigured (does not error). writes_enabled means at least one write tool is exposed. sync section reports the active git-sync mode and current state. Read-only; never excluded by OBSIDIAN_TOOLS — this is how you discover the active tool policy.
Inputs: section.
get_frontmatterA
Read just a note's parsed frontmatter (YAML metadata), without its body. A cheap way to inspect a note's status, aliases, dates, or custom fields before reading or editing the whole note.
Inputs: path (required).
get_linksA
Resolve the Obsidian link graph for a note: outbound [[wikilinks]] resolved to real notes, links that resolve to nothing, and backlinks (other notes that link to this one). Use it to traverse related knowledge. With include_context: true, every row gains the linking line(s) — 'who references this note, and why' in one call (see the link-context convention).
Inputs: path (required), include_context.
get_outlineA
Return a note's heading structure (outline) without reading its body: each heading with its level, 1-based line number, full " > "-joined heading-path, and an ambiguity flag. Use it to see what sections exist before reading or editing one.
Inputs: path (required).
get_propertyA
Read a single frontmatter property value from one note. Returns { path, key, value, present }; present distinguishes an absent key from a key explicitly set to null.
Inputs: key (required), path (required).
get_vault_statsA
Summarize the whole vault: note and tag counts, link-graph health (resolved vs unresolved links, orphan notes), total size, and modification-time bounds. Use it to get a quick sense of the vault's scale and health.
Inputs: no named parameters.
list_filesA
List non-markdown files in the vault (attachments, images, PDFs) as { path, size, modified, extension } rows, e.g. to find a file to move. Never includes notes (use list_notes).
Inputs: limit, folder, offset, extension.
list_foldersA
Enumerate the vault's folders as { path, notes (direct), total_notes (recursive), subfolders } rows sorted by path — the folder-level counterpart to list_notes, for seeing the vault's shape before searching or reading. Notes-only: attachment-only folders do not appear (use list_files), and root-level notes contribute no folder.
Inputs: depth, limit, folder, offset.
list_notesA
List notes in the vault as lightweight headers (path, title, tags, first heading, size, modified time) without full contents. Use it to discover what exists and orient before searching or reading. Scope with folder/tags/where/match (match governs tags; where conditions all apply).
Inputs: tags, limit, match, where, +2 more.
list_propertiesA
List every frontmatter property key used across the vault with the number of notes using it and the distinct value types observed (string/number/boolean/array/null/date, plus object for nested YAML written by hand on disk — frontmatter writes reject nesting, reads do not), sorted by frequency. The vault's property schema; like list_tags but for arbitrary properties. No limit: the full set is returned (offset still pages; truncated is always false).
Inputs: offset, include_tags.
list_property_valuesA
List the distinct values of one frontmatter property as { value, count } rows, most frequent first. Array-valued properties count each element. A faceted index for a single key.
Inputs: key (required), limit, offset.
list_recent_notesA
List notes ordered by recency (newest first), as lightweight headers. Sort by filesystem mtime or a frontmatter date field, with an optional since cutoff. Scope with folder/tags/where/match (match governs tags; where conditions all apply). Use it to find current material.
Inputs: tags, limit, match, since, +4 more.
list_tagsA
List every tag used across the vault with the number of notes using it, sorted by frequency. Unifies inline #tags and frontmatter tags:. Use it to see the vault's topic index. No limit: the full set is returned (offset still pages; truncated is always false).
Inputs: offset.
list_tasksA
List checkbox tasks (- [ ] ...) across the vault as structured rows (path, text, status, raw marker, 1-based line, enclosing heading-path). status is a named state: open|done|in_progress|cancelled|forwarded|other. Index-backed. Scope with folder/tags/where/match and an optional status filter (any of the listed statuses).
Inputs: tags, limit, match, where, +3 more.
list_templatesA
Enumerate the vault's core Templates-plugin template folder as { path, name, size, modified } headers. Folder resolved from .obsidian/templates.json (or the OBSIDIAN_TEMPLATE_FOLDER override); errors if neither is configured. Read-only. Core Templates only — Templater scripting is not supported.
Inputs: limit, offset.
list_vault_issuesA
List the vault-hygiene issues get_vault_stats only counts. kind:'orphans' returns note headers for notes with no inbound or outbound resolved links; kind:'unresolved_links' returns, grouped by source note, the wikilink targets that resolve to nothing (the notes with broken links); kind:'broken_anchors' returns, grouped by source note, the [[note#heading]] anchors that resolve to a note but not to any heading in it; kind:'conflicts' returns the unreconciled conflict copies (notes named "… (conflicted YYYY-MM-DD HHMMSS)") each paired with the original note they diverged from. Index-backed. For the grouped kinds, limit/offset count groups (source notes), not individual targets.
Inputs: kind (required), limit, offset, include_context.
query_notesA
Find notes whose frontmatter satisfies a set of conditions, as note headers. Each condition is a bare scalar (equality / array-membership) or an operator object { eq, ne, gt, gte, lt, lte, exists, contains }. Comparisons are type-aware (numbers, ISO dates, strings). match: all (default) or any (governs the where conditions only). Narrow further with folder and tags (any of them).
Inputs: where (required), tags, limit, match, +2 more.
read_notesA
Read one or more Obsidian notes by their relative paths. Returns { notes, errors }: notes is the array of parsed notes (path, contents, frontmatter, tags); errors lists any paths that could not be read (missing/too large), so one bad path never fails the batch. Path traversal still errors the whole call.
Inputs: paths (required).
read_sectionA
Read a single section of a note without loading the whole note. Address the section by bare heading (when unique) or by a " > "-joined heading-path (e.g. "Projects > Log") when the heading repeats. Returns the heading plus its own body; set include_subsections to include nested subsections.
Inputs: path (required), section (required), include_subsections.
resolve_daily_noteA
Map a calendar date to its canonical daily-note path, using the Daily Notes core plugin's own configuration (.obsidian/daily-notes.json: folder, format, template; OBSIDIAN_DAILY_FOLDER overrides the folder). Returns { date, path, exists, template }: date is the resolved ISO day, path the canonical note path (no .md; slashes in the configured format nest folders, as in Obsidian), exists whether the note is on disk, template the configured daily template path or null. Read-only — existing tools do the rest: apply_template (which accepts the returned template path) or write_note to create it, append_note/append_to_section to log into it, read_notes/read_section to read it. Errors when daily notes are not configured. Note: {{date}}/{{time}} in an applied template expand with the current moment, not the resolved day — exact Obsidian parity for today, a known caveat when creating past/future notes.
Inputs: date.
resolve_noteA
Resolve a human-facing note name (frontmatter title, an alias, or the file basename) to its canonical note path — an exact, case-insensitive, index-backed lookup that removes the search-then-guess round trip for "what's the path of the note called X?". Matching is exact, never fuzzy (use search_notes_ranked for approximate matching). Returns { query, matches, resolved }: matches is the array of { path, title, matched_on } (matched_on is "title"|"alias"|"basename"; a note matching on several fields appears once, labeled with its strongest field, title > alias > basename), sorted by path; resolved is the single path when exactly one note matches, else null (ambiguous or no match — it never guesses).
Inputs: query (required).
search_notesA
Search notes with ripgrep, optionally scoped by folder, tags, or a frontmatter where filter (index-resolved candidates, then rg over just those notes). Paginates over matching files: returns { results, truncated, files_returned, files_skipped, files_omitted, matches_capped_in }. Each match carries line_number (file-absolute, ripgrep's) and body_line (1-based body-relative with frontmatter stripped — the same line convention as get_outline/list_tasks/set_task_state, so a hit can be handed straight to those tools; null for hits inside the frontmatter block or in a file the index does not track).
Inputs: pattern (required), tags, limit, match, +8 more.
search_notes_rankedA
Full-text search ranked by BM25 relevance, optionally scoped by folder, tags, or a frontmatter where filter. Returns the most relevant notes first (title/heading/tag matches boosted) as note headers with score and snippet. Complements search_notes (which is literal/regex, unranked). A positive limit is capped at 100; offset pages past the cap.
Inputs: query (required), tags, limit, match, +3 more.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 25 tools
Each tool has a clearly distinct purpose. For example, search_notes vs search_notes_ranked serve different search paradigms, and frontmatter tools (get_frontmatter, get_property, list_properties, list_property_values, query_notes) each target a different aspect. No two tools overlap significantly.
All tools follow a consistent `verb_noun` pattern (e.g., get_frontmatter, list_notes, search_notes_ranked). The verbs vary (get, read, search, list, find, resolve, query) but the structure is uniform and predictable, making navigation easy.
25 tools is well-scoped for a feature-rich Obsidian server. The number covers reading, searching, listing, metadata, vault analysis, and more without being excessive. Each tool earns its place, and the count aligns with the complexity of the domain.
The tool set is heavily read-focused, with no write operations (create, update, delete notes) present. While read coverage is thorough, the lack of write tools is a significant gap for an Obsidian server, limiting agent actions to inspection only.
Maintenance
Related MCP Connectors
Markdown-based note-taking with a hosted MCP server. Your notes serve you and your AI.
Google Keep-style notes app with an MCP server for AI agents to read/write notes.
Driflyte MCP server which lets AI assistants query topic-specific knowledge from web and GitHub.
An MCP server that gives your AI access to the source code and docs of all public github repos
Related MCP Servers
- AlicenseNot gradedqualityFmaintenanceA local MCP server that enables AI applications like Claude Desktop to securely access and work with Obsidian vaults, providing capabilities for reading notes, executing templates, and performing semantic searches.830MIT
- AlicenseNot gradedqualityCmaintenanceProvides an MCP server that allows AI assistants to interact with Obsidian vaults, enabling reading/writing notes, managing metadata, searching content, and working with daily notes.37MIT
- AlicenseNot gradedqualityDmaintenanceA local MCP server that wraps the Obsidian CLI to give AI assistants direct access to read, edit, and manage notes within an Obsidian vault. It enables advanced operations such as frontmatter property management, context-aware searching, and the execution of internal Obsidian commands.2MIT
- AlicenseBqualityDmaintenanceAn MCP server that enables AI assistants to access and manage your local Obsidian vault through a standardized protocol.144,522 npm2Apache 2.0