Skip to main content
Glama
Headless Obsidian MCP

Headless Obsidian MCP

by emguide

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

Find

Literal/regex search via ripgrep (search_notes), plus BM25 relevance-ranked full-text search with snippets (search_notes_ranked). Resolve a human name ("Alpha Project") or a date ("yesterday") to a note path (resolve_note, resolve_daily_note).

Browse

List notes, folders, attachments, tags, and frontmatter properties. Read a note's outline or one section without loading the whole thing.

Traverse

Resolve [[wikilinks]] and backlinks (get_links). Rank the notes most related to a given one — shared tags and link-graph structure, no embeddings (get_related_notes).

Query

Filter notes by frontmatter conditions (eq, gt, contains, …), by tag, or by recency. Every note-selecting tool accepts the same folder / tags / where filters.

Audit

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.

Protect

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 .trash.

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 start

Then 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 .env file — an MCP server inherits its environment from the client that spawns it. Set variables in your client's env block (or pass --env-file to 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 agent

Selectors 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

search_notes, search_notes_ranked

—

notes

read_notes, list_notes, list_recent_notes, resolve_note, resolve_daily_note

write_note, append_note, prepend_note, patch_note, delete_note, move_note

sections

get_outline, read_section

add_section, append_to_section, replace_section, rename_section

links

get_links, get_related_notes

—

tags

list_tags, find_by_tag

add_tag, remove_tag

properties

get_frontmatter, list_properties, list_property_values, query_notes, get_property

set_frontmatter, add_property_values, remove_property_values, rename_property

tasks

list_tasks

set_task_state

templates

list_templates

apply_template, insert_template

files

list_files, list_folders

move_file, create_folder, move_folder, delete_folder

vault

get_vault_stats, list_vault_issues

—

bulk

—

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

off (default)

No git involvement.

commit

Commits after every write, with a message naming the tool. No remote.

every-write

Commits, then pulls and pushes the remote after each write.

timer

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.md

mise 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 tools
find_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_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

    A4.1/5.0

    Scored across 25 tools

    Disambiguation5/5

    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.

    Naming Consistency5/5

    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.

    Tool Count5/5

    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.

    Completeness2/5

    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

    ActivityInactive
    ResponsivenessNo recent community issues

    Related MCP Connectors

    Related MCP Servers

    • A
      license
      Not graded
      quality
      C
      maintenance
      Provides 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.
      37
      MIT
    • A
      license
      Not graded
      quality
      D
      maintenance
      A 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.
      2
      MIT