Claude Marketplaces
SkillsMCP serversMarketplacesDigestTools
Submit

This week in Claude

Every Monday: Claude Code, Agent SDK, MCP, and the Anthropic platform moves worth your time.

Skills by category
Frontend DevelopmentBackend & APIsTesting & QASecurityDevOps & CI/CDGit & Pull RequestsDocumentationCode Review & QualityAI & Agent BuildingSkill Development
MCP servers by category
Sales & MarketingWeb & Browser AutomationDatabasesAI & LLM ToolsCloud & InfrastructureCommunication & MessagingDeveloper ToolsDesign & CreativeDocuments & KnowledgeSearch & Web Crawling
Marketplaces by category
AI Agents & OrchestrationLLM IntegrationDevelopment ToolsFrontend & UIBackend & APIsDatabasesTesting & Code QualityDevOps & CloudSecurity & ComplianceGit & Version Control
Claude Marketplaces

An independent directory of skills, plugins and MCP servers for Claude Code. Not affiliated with Anthropic.

Browse

  • Skills
  • MCP servers
  • Marketplaces
  • Weekly digest
  • Skill index
  • MCP index
  • Marketplace index

Site

  • About
  • Tools
  • Submit
  • Feedback
  • Privacy
  • llms.txt
  • Plugins reference

Maker

  • Snug
  • Agent Caffeine
  • knobs
  • Wax
  • Vectoys
  • mertduzgun.com
Crafted at
MCP servers/Documents & Knowledge
cyanheads avatar

Obsidian Mcp Server

cyanheads/obsidian-mcp-server

The Obsidian MCP Server provides AI agents and LLMs with comprehensive access to Obsidian vaults through the Model Context Protocol, enabling read, write, search, and management operations on notes and files via the Obsidian Local REST API plugin. It exposes specialized tools including obsidian_read_note, obsidian_update_note, obsidian_search_replace, and obsidian_global_search, with capabilities such as flexible content formatting, regex-based search-and-replace, case-insensitive path fallback, and metadata retrieval. The server solves the problem of integrating AI assistants and development tools with Obsidian knowledge bases by providing a standardized, secure interface for programmatic vault interaction.

GitHub stars
582
tools
12
last updated
Jun 2, 2026
View on GitHubSave

Install

Verified command
$ claude mcp add obsidian --env OBSIDIAN_API_KEY=YOUR_OBSIDIAN_API_KEY --env OBSIDIAN_BASE_URL=http://127.0.0.1:27123 --env OBSIDIAN_VERIFY_SSL=false --env OBSIDIAN_REQUEST_TIMEOUT_MS=30000 --env OBSIDIAN_ENABLE_COMMANDS=false --env OBSIDIAN_READ_PATHS=YOUR_OBSIDIAN_READ_PATHS --env OBSIDIAN_WRITE_PATHS=YOUR_OBSIDIAN_WRITE_PATHS --env OBSIDIAN_READ_ONLY=false --env MCP_LOG_LEVEL=info -- node -y obsidian-mcp-server run start:stdio

Run in your terminal. Replace YOUR_* placeholders with real values; add --scope user to install for every project.

Review the command, arguments and environment values before installing. MCP servers run with your local permissions.

Tools

Verified live · 12 tools

Verified live against the running server on Jun 10, 2026.

obsidian_get_noteRead a note from the vault — by path, the active file, or a periodic note. Choose a `format` projection: raw body, full object, structural document map, or a single section.4 params

Read a note from the vault — by path, the active file, or a periodic note. Choose a `format` projection: raw body, full object, structural document map, or a single section.

Parameters* required
format*string
Which projection to return. `content` — raw markdown body. `full` — content plus parsed frontmatter, tags, and file metadata. `document-map` — catalog of headings, block IDs, and frontmatter field...one of content · full · document-map · section
includeLinksboolean
When true with `format: "full"`, parses outgoing wiki and markdown link references from the note body. Skipped for other formats.default: false
sectionobject
Required when `format` is `"section"`. Identifies the heading/block/frontmatter to extract.
target*value
Where the note lives.
obsidian_list_notesList notes and subdirectories at a vault path. Defaults to the vault root when `path` is omitted. Tune recursion with `depth`, or filter the walk with `extension` / `nameRegex`. Capped at 1000 entries per call — when reached, walking stops and `excluded` is set; narrow `path`...4 params

List notes and subdirectories at a vault path. Defaults to the vault root when `path` is omitted. Tune recursion with `depth`, or filter the walk with `extension` / `nameRegex`. Capped at 1000 entries per call — when reached, walking stops and `excluded` is set; narrow `path`...

Parameters* required
depthinteger
How many directory levels to walk. `1` = target directory only (no recursion); `2` = target plus its immediate children — a structural overview; bump higher to drill in. Prefer narrowing `path` to...default: 2
extensionstring
Only include files matching this extension, with or without leading dot. Applies to files only — directories are returned regardless.
nameRegexstring
Optional ECMAScript regex (no flags) applied to entry names. Matches both files and directories; directories that fail the regex are skipped without recursing into them.
pathstring
Vault-relative directory path. Omit for the vault root.
obsidian_list_tagsList every tag found across the vault, with usage counts. Includes hierarchical parents — `work/tasks` contributes to both `work` and `work/tasks`. Filter to a subset with the optional `nameRegex`. To find notes by tag, use `obsidian_search_notes` in jsonlogic mode (e.g. `{"in...1 params

List every tag found across the vault, with usage counts. Includes hierarchical parents — `work/tasks` contributes to both `work` and `work/tasks`. Filter to a subset with the optional `nameRegex`. To find notes by tag, use `obsidian_search_notes` in jsonlogic mode (e.g. `{"in...

Parameters* required
nameRegexstring
Optional ECMAScript regex (no flags, ≤256 chars, no nested quantifiers like `(a+)+`) matched against the bare tag name (no leading `#`). Hierarchical tags like `work/tasks` are matched as the full...
obsidian_open_in_uiOpen a file in the Obsidian app UI. By default fails when the path does not exist; the `failIfMissing` flag controls the open-or-create behavior.3 params

Open a file in the Obsidian app UI. By default fails when the path does not exist; the `failIfMissing` flag controls the open-or-create behavior.

Parameters* required
failIfMissingboolean
When true (default), fails if the file does not exist. When false, allows Obsidian to create the file on open.default: true
newLeafboolean
Open in a new leaf (split pane) instead of the active one.default: false
path*string
Vault-relative path of the file to open.
obsidian_search_notesSearch the vault by text substring or JSONLogic predicate. Pick the mode that matches the query shape. Results paginate via opaque cursors: omit `cursor` for the first page, then pass `nextCursor` from the prior response. Text-mode hits additionally clip per file at `maxMatche...7 params

Search the vault by text substring or JSONLogic predicate. Pick the mode that matches the query shape. Results paginate via opaque cursors: omit `cursor` for the first page, then pass `nextCursor` from the prior response. Text-mode hits additionally clip per file at `maxMatche...

Parameters* required
contextLengthinteger
Characters of context on each side of the match (text mode only).default: 100
cursorstring
Opaque cursor from a prior response. Omit for the first page. Page size is server-determined; do not assume a fixed value.
logicobject
JSONLogic tree. Required for `jsonlogic` mode; ignored in `text` and `omnisearch` modes (use `query` instead — passing a string here will fail Zod validation since this field must be an object).
maxMatchesPerHitinteger
Cap on match contexts returned per file in text mode. When clipped, the hit carries `truncated: true` and `totalMatches`.default: 10
mode*string
Which search algorithm to run. `text` matches a substring case-insensitively across filenames and note bodies, returning surrounding context windows. `jsonlogic` evaluates a JSONLogic tree against...one of text · jsonlogic
pathPrefixstring
Filter returned filenames by prefix (text mode only, applied client-side).
querystring
The query string. Required for `text` and `omnisearch` modes; ignored in `jsonlogic` mode (use `logic` instead — passing a JSONLogic tree here will fail Zod validation since this field must be a st...
obsidian_write_noteCreate or overwrite a note. With `section`, replaces just that heading/block/frontmatter section in place; nested headings need `Parent::Child` syntax — use `obsidian_get_note` with `format: "document-map"` to discover available targets. Whole-file writes fail with `file_exist...5 params

Create or overwrite a note. With `section`, replaces just that heading/block/frontmatter section in place; nested headings need `Parent::Child` syntax — use `obsidian_get_note` with `format: "document-map"` to discover available targets. Whole-file writes fail with `file_exist...

Parameters* required
content*string
Body to write. For heading sections, the new section body — do not repeat the heading line (it stays in place). Markdown unless `contentType` is `json`.
contentTypestring
Content body format. Use "json" for typed frontmatter values or block-targeted table rows. JSON values must be valid JSON literals — strings need quoting (`"\"draft\""`, not `"draft"`), and numbers...one of markdown · jsondefault: markdown
overwriteboolean
Whole-file mode only (ignored when `section` is set). When `false` (default), the call fails with `file_exists` if the target note already exists — read it first and use `obsidian_patch_note` / `ob...default: false
sectionobject
Optional sub-document target. When set, only this section is replaced; rest of the note is untouched.
target*value
Where the note lives.
obsidian_append_to_noteAppend content to a note. **Without `section`: appends to the end of the file, or creates the file if it does not exist (your content becomes the full file).** With `section`: appends to the end of that heading/block/frontmatter; nested headings need `Parent::Child` syntax — u...5 params

Append content to a note. **Without `section`: appends to the end of the file, or creates the file if it does not exist (your content becomes the full file).** With `section`: appends to the end of that heading/block/frontmatter; nested headings need `Parent::Child` syntax — u...

Parameters* required
content*string
Body to append. Markdown unless `contentType` is `json`.
contentTypestring
Content body format. Use "json" for typed frontmatter values or block-targeted table rows. JSON values must be valid JSON literals — strings need quoting (`"\"draft\""`, not `"draft"`), and numbers...one of markdown · jsondefault: markdown
createTargetIfMissingboolean
When `section` is provided, create the section if it does not already exist (otherwise the call fails when the section is missing).default: false
sectionobject
Optional sub-document target. When set, content is appended to that section instead of the file.
target*value
Where the note lives.
obsidian_patch_noteEdit a heading, block reference, or frontmatter field in place — append to, prepend to, or replace the target's body. Use `obsidian_get_note` with `format: "document-map"` to discover available targets first; nested headings need `Parent::Child` syntax.6 params

Edit a heading, block reference, or frontmatter field in place — append to, prepend to, or replace the target's body. Use `obsidian_get_note` with `format: "document-map"` to discover available targets first; nested headings need `Parent::Child` syntax.

Parameters* required
content*string
Body to insert/replace. Markdown unless `contentType` is `json`.
contentTypestring
Content body format. Use "json" for typed frontmatter values or block-targeted table rows. JSON values must be valid JSON literals — strings need quoting (`"\"draft\""`, not `"draft"`), and numbers...one of markdown · jsondefault: markdown
operation*string
How to apply `content` relative to the targeted section. `append` — at the end of the target's body (for headings, before the next sibling/parent heading; for frontmatter array fields, as a new arr...one of append · prepend · replace
patchOptionsobject
Optional flags: createTargetIfMissing, applyIfContentPreexists, trimTargetWhitespace.
section*object
Which heading/block/frontmatter field to edit.
target*value
Where the note lives.
obsidian_replace_in_noteSearch and replace inside a single note, literally or by regex. Replacements run in array order, each over the previous one's output. Use for edits that don't fit `obsidian_patch_note`'s structural targets — e.g., body-wide find-and-replace.2 params

Search and replace inside a single note, literally or by regex. Replacements run in array order, each over the previous one's output. Use for edits that don't fit `obsidian_patch_note`'s structural targets — e.g., body-wide find-and-replace.

Parameters* required
replacements*array
Replacements to apply in array order over the evolving content.
target*value
Where the note lives.
obsidian_manage_frontmatterGet, set, or delete a single frontmatter key on a note, atomically. `set` requires a JSON-typed `value` (string, number, boolean, array, or object).4 params

Get, set, or delete a single frontmatter key on a note, atomically. `set` requires a JSON-typed `value` (string, number, boolean, array, or object).

Parameters* required
key*string
Frontmatter field name.
operation*string
Operation to perform on `key`. `get` — read the current value. `set` — write `value`, creating the key if absent. `delete` — remove the key from frontmatter.one of get · set · delete
target*value
Where the note lives.
valuevalue
Required when `operation` is `"set"`. JSON-typed value to write — strings, numbers, booleans, arrays, and objects all accepted.
obsidian_manage_tagsAdd, remove, or list a note's tags. Defaults to the frontmatter `tags:` array — set `location` to `inline` or `both` to mutate the note body. `add` ensures the tag is present in the requested location(s); `remove` strips it; `both` reconciles across both representations. Inlin...4 params

Add, remove, or list a note's tags. Defaults to the frontmatter `tags:` array — set `location` to `inline` or `both` to mutate the note body. `add` ensures the tag is present in the requested location(s); `remove` strips it; `both` reconciles across both representations. Inlin...

Parameters* required
locationstring
Where to apply the change. Defaults to `frontmatter` (the canonical Obsidian tag location, leaves the body untouched). `inline` mutates the note body — `add` appends `#tag` at end-of-file. `both` i...one of frontmatter · inline · bothdefault: frontmatter
operation*string
`add` and `remove` mutate the note; `list` reads the current tag set.one of add · remove · list
tagsarray
Tags to add or remove. Omit the leading `#`. Required for add/remove.
target*value
Where the note lives.
obsidian_delete_notePermanently delete a note from the vault. Confirms with the user before deleting when the client supports interactive confirmation. Recovery requires the local trash in Obsidian — there is no API-level undo.1 params

Permanently delete a note from the vault. Confirms with the user before deleting when the client supports interactive confirmation. Recovery requires the local trash in Obsidian — there is no API-level undo.

Parameters* required
target*value
Which note to delete.

Sponsors

SnugThe clipboard manager you and your agent can use, safelyAgent CaffeineClose your lid, come back to finished workWaxBlock anything with supervision, without erasing your iPhoneOpen StepsSponsoredFree and open-source skills that make AI coding agents easier to understand, verify, and control.

Configuration

OBSIDIAN_API_KEY*

Bearer token for the Obsidian Local REST API plugin (Settings → Community Plugins → Local REST API).

OBSIDIAN_BASE_URLdefault: http://127.0.0.1:27123

Base URL of the Obsidian Local REST API. Default: http://127.0.0.1:27123 (enable "Non-encrypted (HTTP) Server" in plugin settings). Use https://127.0.0.1:27124 for the always-on HTTPS port (self-signed cert; pair with OBSIDIAN_VERIFY_SSL=false).

OBSIDIAN_VERIFY_SSLdefault: false

Whether to verify the TLS certificate on the Obsidian endpoint. Default false because the plugin uses a self-signed cert.

OBSIDIAN_REQUEST_TIMEOUT_MSdefault: 30000

Per-request timeout in milliseconds.

OBSIDIAN_ENABLE_COMMANDSdefault: false

Opt-in flag for the command-palette pair (obsidian_list_commands + obsidian_execute_command). Off by default — Obsidian commands are opaque and can be destructive.

OBSIDIAN_READ_PATHS

Optional comma-separated vault-relative folder allowlist for reads. Prefix-based with implicit recursion; case-insensitive; trailing slashes normalized. Unset = full vault. Write paths are implicitly readable. Example: 'public/,projects/'.

OBSIDIAN_WRITE_PATHS

Optional comma-separated vault-relative folder allowlist for writes. Same syntax as OBSIDIAN_READ_PATHS. Unset = full vault. Example: 'projects/,scratch/'.

OBSIDIAN_READ_ONLYdefault: false

Global read-only kill switch. When true, every write is denied regardless of OBSIDIAN_WRITE_PATHS, and the command-palette pair is suppressed (commands can mutate). Useful for shared or public-facing deployments.

MCP_LOG_LEVELdefault: info

Sets the minimum log level for output (e.g., 'debug', 'info', 'warn').

MCP_HTTP_HOSTdefault: 127.0.0.1

The hostname for the HTTP server.

MCP_HTTP_PORTdefault: 3010

The port to run the HTTP server on.

MCP_HTTP_ENDPOINT_PATHdefault: /mcp

The endpoint path for the MCP server.

MCP_AUTH_MODEdefault: none

Authentication mode to use: 'none', 'jwt', or 'oauth'.

Readme

obsidian-mcp-server

Read, write, search, and surgically edit Obsidian vault notes, tags, and frontmatter via MCP. STDIO or Streamable HTTP.

14 Tools • 3 Resources

Version License Docker MCP SDK npm TypeScript Bun

Install in Claude Desktop Install in Cursor Install in VS Code

Framework


Overview

Your Obsidian vault, through the Local REST API plugin. Read, search, and write notes, edit a single heading, block, or frontmatter field in place, and manage tags, with folder-scoped read/write permissions built in.

Tools

ToolDescription
obsidian_get_noteRead a note as raw content, full structured form, document map, or a single section
obsidian_list_notesList notes and folders under a vault path, recursively, with extension and name filters
obsidian_list_tagsList vault tags with usage counts, most-used first
obsidian_search_notesSearch by text, JSONLogic, or BM25-ranked Omnisearch when that plugin is reachable
obsidian_write_noteCreate a note, replace one section, or overwrite a whole file with overwrite: true
obsidian_append_to_noteAppend to a note (creating it if missing) or to one heading, block, or frontmatter field
obsidian_patch_noteAppend, prepend, or replace against one heading, block reference, or frontmatter field
obsidian_replace_in_noteLiteral or regex search-replace inside one note, body-only by default
obsidian_manage_frontmatterGet, set, or delete one frontmatter key
obsidian_manage_tagsAdd, remove, or list a note's tags in frontmatter, inline, or both
obsidian_delete_notePermanently delete a note, optionally after the user confirms (OBSIDIAN_DELETE_ELICITATION)
obsidian_open_in_uiOpen a file in the Obsidian app, optionally in a new pane
obsidian_list_commandsList command-palette commands (opt-in via OBSIDIAN_ENABLE_COMMANDS)
obsidian_execute_commandRun a command-palette command by ID (opt-in via OBSIDIAN_ENABLE_COMMANDS)

Resources

ResourceDescription
obsidian://vault/{+path}A note's content, frontmatter, tags, and file metadata
obsidian://tagsEvery vault tag with its usage count, as an uncapped snapshot
obsidian://statusPlugin reachability, auth status, versions, and registered API extensions

Note and tag data are also reachable through tools (obsidian_get_note, obsidian_list_tags); obsidian://status has no tool equivalent.

Capability reference

obsidian_get_note tool

  • target is a vault path, the active file, or a periodic note (daily through yearly, optional date); format is content, full, document-map, or section, and full takes includeLinks: true for vault-internal outgoing links
  • result.format discriminates the payload; a section read that matches several headings returns the first and lists every full path in candidates
  • A path with no exact match but one case-insensitive match in its folder reads that file: result.path is the real name, requestedPath the one sent, and a notice names both

obsidian_list_notes tool

  • Walks from path (default vault root) to depth 1–20 (default 2), filtered by extension and nameRegex (≤256 chars); a folder that fails nameRegex is not walked
  • Returns entries[] (file / directory), totals, and appliedFilters; the walk stops at 1,000 entries with excluded.reason: "entry_cap", and a folder the depth limit stopped carries truncated: true
  • With OBSIDIAN_READ_PATHS or OBSIDIAN_DENY_PATHS set, a listing holds only readable entries and the folders leading to the scope; those folders are walked and accepted as path, and a denied folder is absent at every depth

obsidian_list_tags tool

  • nameRegex (≤256 chars) and minCount narrow the set, then tags are ranked by count and capped at limit (default 200, max 10000); hierarchical parents count (work/tasks adds to work)
  • When the cap withholds tags, the response carries truncated, shown, and cap
  • With OBSIDIAN_READ_PATHS or OBSIDIAN_DENY_PATHS set, only tags from readable notes are listed, and count is the number of readable notes carrying the tag or a tag nested under it

obsidian_search_notes tool

  • mode: "text" requires every space-separated token of query as a case-insensitive substring of the filename or body (quotes are literal), shaped by contextLength (default 100), pathPrefix, and maxMatchesPerHit (default 10); mode: "jsonlogic" evaluates a logic tree over path, content, frontmatter.<key>, tags, and stat (plus links, backlinks, and unresolvedLinks on plugin versions that provide them, and not under OBSIDIAN_READ_PATHS or OBSIDIAN_DENY_PATHS), with glob / regexp taking [PATTERN, VALUE]
  • result.mode discriminates the payload; every mode reports totalCount and pages via nextCursor, and a text hit clipped to maxMatchesPerHit carries truncated and totalMatches
  • mode: "omnisearch" (BM25 ranking, quoted phrases, -exclusion, path: / ext: filters) is offered only when the Omnisearch plugin answered at startup. It keeps its top 50 hits; path:, ext:, and plain terms apply before that cap, quoted phrases and -exclusion after it. truncated: true marks a result the cap may have cut, with truncatedReason cap_reached, filtered_after_cap, or read_scope; under OBSIDIAN_READ_PATHS or OBSIDIAN_DENY_PATHS it is always true, and a hit's membership and score still depend on the whole vault

obsidian_write_note tool

  • target and content, with optional section and contentType (markdown / json); a whole-file write to an existing note fails with file_exists unless overwrite: true
  • On a case-insensitive filesystem (macOS, Windows), a whole-file write to a path that differs from an existing note only in letter case fails with case_variant_exists and writes nothing; existingPath names that note when the parent folder's listing shows exactly one case match, and is omitted when several match or when a file Obsidian does not index (a dot-file) sits at the path
  • With section, replaces only that heading, block, or frontmatter field and keeps the heading line; output reports created, sectionTargeted, and the resolved sectionTarget

obsidian_append_to_note tool

  • Without section, appends to the file or creates it (created: true); with section, appends to that heading, block, or frontmatter field of an existing note, and createTargetIfMissing: true creates the section
  • A section append whose content is already at the target fails with content_preexists; block targets add no separator, so start content with a newline if you want one
  • A whole-file append to a path that differs from an existing note only in letter case fails with case_variant_exists on a case-insensitive filesystem, with existingPath naming that note when the parent folder's listing shows exactly one case match

obsidian_patch_note tool

  • operation: "append" | "prepend" | "replace" against one section of an existing note; patchOptions takes createTargetIfMissing, applyIfContentPreexists, and trimTargetWhitespace (plugin v4.x only)
  • Echoes the resolved section and operation; a repeat of content already at the target fails with content_preexists unless applyIfContentPreexists: true

obsidian_replace_in_note tool

  • replacements[] run in order, each over the previous one's output; each takes useRegex (≤1024 chars), caseSensitive (default true), wholeWord, flexibleWhitespace (literal mode only), and replaceAll (default true)
  • Returns totalReplacements and perReplacement[] with bodyCount / frontmatterCount
  • scope: "body" (default) leaves frontmatter byte-identical; "frontmatter" and "both" re-parse the YAML afterward and write nothing if it breaks (frontmatter_invalid)

obsidian_manage_frontmatter tool

  • operation: "get" | "set" | "delete" on one key; set requires a JSON-typed value
  • get returns exists and value (null when absent); set and delete return the full frontmatter after the change, and a delete against unparseable YAML fails with frontmatter_invalid without writing

obsidian_manage_tags tool

  • operation: "add" | "remove" | "list" with tags; location: "frontmatter" (default, the tags: array), "inline" (body #tag; add appends at end of file), or "both"
  • add / remove report applied, skipped, and the resulting tags; list returns frontmatter, inline, and all
  • Inline detection follows Obsidian's tag grammar: code, wikilinks, images, link destinations, HTML, and math are skipped, and %% … %% comments are read

obsidian_delete_note tool

  • Takes a target (path, active file, or periodic note) and checks it against the write scope first; a folder path fails with path_is_directory. There is no API-level undo, only Obsidian's local trash
  • The path must match exactly, letter case included, even on a case-insensitive filesystem. A different-case or extension-less spelling fails note_missing before anything is deleted, naming the near matches in suggestions
  • By default the note is deleted on the first call, with no confirmation; OBSIDIAN_WRITE_PATHS, OBSIDIAN_DENY_PATHS, and OBSIDIAN_READ_ONLY bound what it can reach
  • With OBSIDIAN_DELETE_ELICITATION=true, the first call answers with a confirmation request naming the path and byte size, and the note is deleted only after the user accepts. Declining fails with cancelled, and a client without elicitation support cannot delete
  • In that mode an answer counts only against the single-use consent record stored when the prompt was shown, bound to the caller, the path, and the note's content then. A pre-supplied or replayed answer, or one given after the note changed, gets a fresh prompt instead
  • Consent records live in the server's storage provider (STORAGE_PROVIDER_TYPE, default in-memory, process-local). That works for stdio or a single HTTP instance; several instances behind one endpoint need a shared provider (filesystem, supabase, or cloudflare-d1, never cloudflare-kv)

obsidian_open_in_ui tool

  • path, failIfMissing (default true), and newLeaf (open in a split pane); with failIfMissing: false a missing file is created, which needs write access
  • createdIfMissing reports which branch ran
  • A case-mismatched path with one case-insensitive match opens that file instead of creating another, reporting the path sent as requestedPath with a notice

obsidian_list_commands tool

  • Optional nameRegex (≤256 chars) matched against each command's display name
  • Returns commands[] of id and name, where id feeds obsidian_execute_command
  • Listed only when OBSIDIAN_ENABLE_COMMANDS=true and OBSIDIAN_READ_ONLY is off

obsidian_execute_command tool

  • commandId from obsidian_list_commands; returns executed: true, or fails with command_unknown for an unregistered ID
  • Runs with the authority of a keyboard shortcut, so some commands are destructive (delete file, close vault); gated like obsidian_list_commands

obsidian://vault/{+path} resource

  • {+path} captures everything after /vault/, slashes included; literal and percent-encoded paths resolve to the same note
  • Returns path, content, frontmatter, tags, and stat, the same shape as obsidian_get_note with format: "full"; failures are path_forbidden, note_missing, or path_is_directory

obsidian://tags resource

  • Every tag with its count, uncapped and unsorted, hierarchical parents included
  • With OBSIDIAN_READ_PATHS or OBSIDIAN_DENY_PATHS set, only tags from readable notes, counted the same way as obsidian_list_tags
  • No ranking or filters; obsidian_list_tags gives the count-ranked, capped view

obsidian://status resource

  • status, service, authenticated, versions, manifest, and apiExtensions[]; still answers with a wrong API key, reporting authenticated: false
  • On plugin v5.0.2 and later, check apiExtensions for local-rest-api-periodic-notes before using periodic targets

Features

Built on @cyanheads/mcp-ts-core: stdio and Streamable HTTP transports, pluggable auth (none / jwt / oauth), swappable storage (in-memory, filesystem, Supabase, Cloudflare KV/R2/D1), structured logging with optional OpenTelemetry tracing.

Obsidian-specific:

  • Typed client for the Obsidian Local REST API plugin; section writes speak markdown-patch 2.0 to plugin v5.0 and later and 1.x to v4.x, chosen from the reported plugin version
  • Heading targets take a full Parent::Child path or a bare leaf name; writes reject an ambiguous one with ambiguous_section and its candidates, unless exactly one match is a top-level heading. On plugin v5.0 and later, content added to a heading is set off by a blank line (a list item continues an adjacent list), and a heading inside it must sit below the section's level (heading_outside_section)
  • Folder-scoped read/write permissions, a deny list that withholds paths from both, a read-only switch, and an opt-in command-palette pair (see Path policy); server-level instructions on initialize report the active policy
  • obsidian_get_note and obsidian_open_in_ui retry a case-mismatched path against the real filename, disclosed as requestedPath plus a notice, and add Did you mean suggestions to a miss; writes and deletes match the exact path
  • Regex inputs are capped (nameRegex at 256 chars, useRegex at 1024) and rejected with regex_unsafe when they nest quantifiers
  • Backlinks have no dedicated tool; obsidian_search_notes in jsonlogic mode finds them with {"regexp": ["\\[\\[Target Note(\\\\?\\||#|\\]\\])", {"var": "content"}]}, which also matches the table-safe [[Target Note\|Alias]]

Agent-friendly output:

  • Recovery-guided errors: every declared failure carries a reason, a JSON-RPC code, and a recovery.hint written for that case
  • Size deltas: every mutating tool returns previousSizeInBytes / currentSizeInBytes, so a caller can spot an accidental clobber without a follow-up read
  • Ambiguity surfaced as data: shared heading names return candidates, and tag operations report applied vs. skipped
  • Discriminated output contracts: format on obsidian_get_note, mode on obsidian_search_notes, operation on obsidian_manage_frontmatter and obsidian_manage_tags

Getting started

Add it to Claude Code:

claude mcp add obsidian-mcp-server -e OBSIDIAN_API_KEY=your-api-key -- npx -y obsidian-mcp-server

For other clients, add this to the MCP config file:

{
  "mcpServers": {
    "obsidian-mcp-server": {
      "command": "npx",
      "args": ["-y", "obsidian-mcp-server"],
      "env": { "OBSIDIAN_API_KEY": "your-api-key" }
    }
  }
}

Generate the key in Obsidian under Settings → Community Plugins → Local REST API, with the plugin set up as Prerequisites describes. For Docker or a source build, see Running the server.

Prerequisites

  • The Obsidian Local REST API plugin, v4.0.0 or later, enabled in your vault. Its API key is OBSIDIAN_API_KEY.
  • The server defaults to http://127.0.0.1:27123, so enable "Non-encrypted (HTTP) Server" in the plugin settings, or set OBSIDIAN_BASE_URL=https://127.0.0.1:27124 for the always-on HTTPS port (its self-signed cert is accepted while OBSIDIAN_VERIFY_SSL=false, the default).
  • Periodic-note targets work natively on plugin v5.0.1 and earlier. From v5.0.2 they need the periodic-notes API extension; without it they fail with periodic_unsupported.
  • On plugin v4.x, heading section writes to a note with CRLF (Windows) line endings land mid-line: the plugin's markdown-patch 1.x engine misplaces them, and the server can't correct it. Plugin v5.0 and later handle CRLF notes.
  • Plugin v6.0 drops markdown-patch 1.x, which two table-row writes (contentType: "json") still use: rows under a heading, and rows through a block ID on its own line below the table. On v6.0, target the table by an ID on its last row.
  • With OBSIDIAN_DELETE_ELICITATION=true, an MCP client that supports elicitation and renders its form, to use obsidian_delete_note. Every other tool, and the default delete, works without it.

Configuration

VariableDescriptionDefault
OBSIDIAN_API_KEYRequired. Bearer token for the Local REST API plugin.—
OBSIDIAN_BASE_URLLocal REST API base URL; https://127.0.0.1:27124 is the always-on HTTPS port. A trailing slash is stripped. When nothing answers, calls fail with obsidian_unreachable.http://127.0.0.1:27123
OBSIDIAN_VERIFY_SSLVerify the plugin's TLS certificate. Off by default for its self-signed cert; the relaxation applies only to an https: OBSIDIAN_BASE_URL. With true, an untrusted cert fails calls with certificate_rejected.false
OBSIDIAN_REQUEST_TIMEOUT_MSPer-request timeout in milliseconds.30000
OBSIDIAN_ENABLE_COMMANDSEnable obsidian_list_commands and obsidian_execute_command. Commands are opaque and can be destructive.false
OBSIDIAN_READ_PATHSComma-separated folder allowlist for reads. See Path policy.unset (full vault)
OBSIDIAN_WRITE_PATHSComma-separated folder allowlist for writes. See Path policy.unset (the read scope, or the full vault without one)
OBSIDIAN_DENY_PATHSComma-separated paths withheld from reads and writes alike, overriding both allowlists. See Path policy.unset (nothing denied)
OBSIDIAN_READ_ONLYDeny every write and disable the command-palette pair.false
OBSIDIAN_DELETE_ELICITATIONAsk the user to confirm each obsidian_delete_note call through an elicitation form before deleting. Needs a client that renders elicitation, and a stateful session over HTTP (not required when OBSIDIAN_READ_ONLY=true disables the tool). Off, the delete runs on the first call.false
OBSIDIAN_OMNISEARCH_URLOmnisearch HTTP server URL. Unset derives from the OBSIDIAN_BASE_URL host on port 51361. Probed once at startup; the omnisearch search mode appears only if it answers.derived
MCP_TRANSPORT_TYPETransport: stdio or http.stdio
MCP_HTTP_PORTHTTP server port.3010
MCP_SESSION_MODEHTTP session mode: stateful, stateless, or auto. With OBSIDIAN_DELETE_ELICITATION=true and OBSIDIAN_READ_ONLY off, the server requires a stateful session, because the confirmation needs one on 2025-era clients, so stateless fails startup over HTTP. No effect on stdio.stateful
MCP_AUTH_MODEAuthentication: none, jwt, or oauth.none
MCP_LOG_LEVELLog level (RFC 5424).info
LOGS_DIRDirectory for log files (Node.js only).<project-root>/logs
OTEL_ENABLEDEnable OpenTelemetry.false

See .env.example for the full list of optional overrides.

Path policy

Four optional env vars limit which vault paths the tools can touch. Unset, reads and writes cover the full vault.

GoalConfig
Read everywhere, write only in projects/ and scratch/OBSIDIAN_WRITE_PATHS=projects/,scratch/
Read only public/, write only public/inbox/OBSIDIAN_READ_PATHS=public/, OBSIDIAN_WRITE_PATHS=public/inbox/
Read and write everywhere except private/OBSIDIAN_DENY_PATHS=private/
Read and write only projects/, except projects/secret/OBSIDIAN_READ_PATHS=projects/, OBSIDIAN_DENY_PATHS=projects/secret/
No writes anywhereOBSIDIAN_READ_ONLY=true
  • Matching is by prefix, recursive, and case-insensitive. Entries are normalized the way a path reaches the plugin: \ becomes /, repeated and trailing slashes collapse, . segments drop, and names are compared in Unicode NFC, so journal//therapy and ./private cover the folders they name. Request paths get the same slash, case, and NFC folding, so notes//private/x.md or an NFD spelling of a denied name is refused like the plain one. An entry naming the vault root (., ./) fails startup. Write paths are also readable.
  • OBSIDIAN_DENY_PATHS wins over both allowlists, write-implies-read included: a denied path is neither readable nor writable, and it is left out of listings, search hits, and tag counts. An allow entry inside a deny entry grants nothing, and the startup log warns about it.
  • OBSIDIAN_READ_ONLY=true removes every write tool and the command-palette pair from tools/list, including obsidian_manage_frontmatter and obsidian_manage_tags (so their get / list go too). obsidian_open_in_ui still opens existing files but won't create one.
  • A denial fails with path_forbidden, echoing the active scope in data.activeScope and the recovery hint. A path the deny list withholds fails with subreason denied_path instead, which carries no activeScope and names OBSIDIAN_DENY_PATHS but none of its entries; a path outside an allowlist reports outside_read_paths or outside_write_paths even when it is also denied. A denial on an active or periodic target names the target ((active file), daily note for 2026-10-01), not the note it resolved to. obsidian_list_notes lists only readable entries plus the folders leading to the scope, so a nested scope can be browsed to from the root; a denied folder never appears, even on the way to an allow entry.
  • Under OBSIDIAN_READ_PATHS or OBSIDIAN_DENY_PATHS, searches return readable notes only. With neither set, a JSONLogic tree goes to the plugin unchanged; with either:
    • JSONLogic: the server evaluates the tree over the readable notes, so it never reads a note outside the scope. Notes carry no links, backlinks, or unresolvedLinks, since Obsidian resolves a link across the whole vault: a tree naming one by a fixed var, missing, or missing_some key fails with link_fields_unavailable, and a computed key reads null. Match link text in content instead. A tree that names content reads each readable note with one request per note, at most 32 in flight across the server.
    • Evaluation is bounded (1,000 levels of nesting, a work budget per note and per search, 10 s of evaluation, 45 s for the whole search); a tree past a bound fails with logic_too_costly. A readable note nested too deep for the evaluator (about 3,500 levels under Node) fails the search with note_too_deep.
    • Text: the server never calls the plugin's text search. It asks the plugin which readable notes may hold every token, reads each candidate note, and applies Obsidian's search rule itself, so hits and context windows match the unscoped search. Past 45 s it fails with text_search_too_costly, and context_length_too_large fails where the plugin's response for the readable matches alone would pass its size ceiling.
    • Omnisearch ranks the whole vault before the scope filters it (see obsidian_search_notes).
  • Tag listings (obsidian_list_tags, obsidian://tags) under OBSIDIAN_READ_PATHS or OBSIDIAN_DENY_PATHS cover readable notes only, counting notes rather than occurrences. Write paths and read-only alone leave them vault-wide.
  • The server instructions state the effective scope: readable is the read paths plus the write paths, and writable is the write paths, or the read scope when only reads are restricted. With a deny list set, each scope reads "…, except paths the operator withholds", naming none of them, and an empty scope reads none. The startup log prints the configured lists, the deny list included.

Running the server

Local development

Requires Bun v1.4.0 or higher (or Node.js v24+).

  • Clone and install:

    git clone https://github.com/cyanheads/obsidian-mcp-server.git
    cd obsidian-mcp-server && bun install
    cp .env.example .env   # set OBSIDIAN_API_KEY
    
  • Build and run the production version:

    # One-time build
    bun run rebuild
    
    # Run the built server
    bun run start:stdio
    # or, listening at http://127.0.0.1:3010/mcp
    bun run start:http
    
  • Run checks and tests:

    bun run devcheck   # Lint, format, typecheck, security, changelog sync
    bun run test       # Vitest test suite
    bun run lint:mcp   # Validate MCP definitions against spec
    

Docker

docker run --rm -e OBSIDIAN_API_KEY=your-key -p 3010:3010 ghcr.io/cyanheads/obsidian-mcp-server:latest

The image defaults to HTTP transport on 0.0.0.0, stateful sessions, and logs in /var/log/obsidian-mcp-server. Inside a container, the default OBSIDIAN_BASE_URL (http://127.0.0.1:27123) is the container's own loopback: point it at http://host.docker.internal:27123 (Docker Desktop) or use --network host (Linux) to reach the plugin on your host. To run it over stdio from an MCP client, use "command": "docker" with "args": ["run", "-i", "--rm", "-e", "MCP_TRANSPORT_TYPE=stdio", "-e", "OBSIDIAN_API_KEY=your-key", "-e", "OBSIDIAN_BASE_URL=http://host.docker.internal:27123", "ghcr.io/cyanheads/obsidian-mcp-server:latest"]. To build locally, docker build -t obsidian-mcp-server .; OpenTelemetry peer dependencies are installed by default — build with --build-arg OTEL_ENABLED=false to omit them.

If the port is reachable from other machines, set MCP_AUTH_MODE=jwt (with MCP_AUTH_SECRET_KEY) or oauth. With the default none, every caller acts on your vault with your OBSIDIAN_API_KEY.

Project structure

DirectoryPurpose
src/index.tscreateApp() entry point: registers tools and resources, probes Omnisearch, applies the read-only and command gates.
src/configServer-specific environment variable parsing (OBSIDIAN_*) with Zod.
src/services/obsidianLocal REST API client, path policy, markdown-patch format handling, frontmatter/section/tag parsing, domain types.
src/mcp-server/toolsTool definitions (*.tool.ts) and shared schemas and helpers.
src/mcp-server/resourcesResource definitions (*.resource.ts).
src/mcp-server/promptsPrompt definitions (none registered).
tests/Vitest tests for tools, resources, services, and config.
docs/Local REST API OpenAPI spec and the generated tree.md.
changelog/Per-version release notes; CHANGELOG.md is the regenerated rollup.

Development guide

See CLAUDE.md for development guidelines and architectural rules. The short version:

  • Handlers throw, framework catches — no try/catch in tool logic
  • Use ctx.log for request-scoped logging; route every Local REST API call through getObsidianService()
  • Register new tools and resources via the barrels in src/mcp-server/*/definitions/index.ts
  • Wrap external API calls: validate raw → normalize to domain type → return output schema; never fabricate missing fields

Contributing

Bugs, feature requests, and documentation gaps belong in an issue; see CONTRIBUTING.md for what makes one actionable and CODE_OF_CONDUCT.md for how we work together. Security reports go through SECURITY.md, never a public issue.

Run checks and tests before submitting:

bun run devcheck
bun run test

License

Apache-2.0 — see LICENSE for details.

Details

Repository
github.com/cyanheads/obsidian-mcp-server
Categories
Documents & Knowledge, Search & Web Crawling
Package
obsidian-mcp-server
Transport
STDIO, HTTP
Auth
Required
Registry
MCP registry, active
Resources
2
Tools verified
Jun 10, 2026

More from cyanheads

  • Hn Mcp Server3
  • Git Mcp Server221
  • Secedgar Mcp Server5
  • Pubchem Mcp Server9
  • Cdc Health Mcp Server3
  • Met Museum Mcp Server3
  • Mcp Ts Core145
  • Courtlistener Mcp Server2
  • Open Meteo Mcp Server2
  • Treasury Fiscaldata Mcp Server2
  • Usaspending Mcp Server2
  • Worldbank Mcp Server2
  • Bls Labor Mcp Server1
  • Cpsc Recalls Mcp Server1
  • Devops Status Mcp Server1
  • Earthquake Mcp Server1
  • Eia Energy Mcp Server1
  • Eurostat Mcp Server1
  • Fcc Broadband Mcp Server1
  • Gdelt Mcp Server1
  • Onebusaway Mcp Server1
  • Openfda Mcp Server1
  • Openfec Mcp Server1
  • Openfoodfacts Mcp Server1

Related Documents & Knowledge MCP servers

View all
1domdomegg avatarAirtabledomdomegg/airtable-mcp-serverRead and write access to Airtable database schemas, tables, and records.447stars
2newtype-01 avatarObsidiannewtype-01/obsidian-mcpObsidian MCP (Model Context Protocol) Server305stars
3iansinnott avatarObsidian Claude Codeiansinnott/obsidian-claude-code-mcpProvides a dual-transport MCP server for Claude Code and Claude Desktop to read and write your Obsidian vault via MCP.287stars
4ergut avatarMCP LogSeq Serverergut/mcp-logseqMCP server to interact with LogSeq via its Local HTTP API - enabling AI assistants like Claude to seamlessly read, write, and manage your LogSeq graph.267stars
5danhilse avatarNotion MCP Integrationdanhilse/notion_mcpA simple MCP integration that allows Claude to read and manage a personal Notion todo list207stars
6kaliaboi avatarZoterokaliaboi/mcp-zoteroA connector for Claude Desktop to work with collection and sources on your Zotero Cloud.159stars