Skip to main content
Glama
cyanheads

Obsidian MCP Server

by cyanheads

Version License Docker MCP SDK npm TypeScript Bun

Install in Claude Desktop Install in Cursor Install in VS Code

Framework


Overview

Obsidian vault notes over the Local REST API plugin. Read, search, and write notes, edit single headings, blocks, and frontmatter fields in place, and manage tags, with folder-scoped read/write permissions built in. Runs as a stdio process or a local Streamable HTTP server.

Tools

Tool

Description

obsidian_get_note

Read a note as raw content, full structured form, document map, or a single section

obsidian_list_notes

List notes and folders under a vault path, recursively, with extension and name filters

obsidian_list_tags

List vault tags with usage counts, most-used first

obsidian_search_notes

Search by text, JSONLogic, or BM25-ranked Omnisearch when that plugin is reachable

obsidian_write_note

Create a note, replace one section, or overwrite a whole file with overwrite: true

obsidian_append_to_note

Append to a note (creating it if missing) or to one heading, block, or frontmatter field

obsidian_patch_note

Append, prepend, or replace against one heading, block reference, or frontmatter field

obsidian_replace_in_note

Literal or regex search-replace inside one note, body-only by default

obsidian_manage_frontmatter

Get, set, or delete one frontmatter key

obsidian_manage_tags

Add, remove, or list a note's tags in frontmatter, inline, or both

obsidian_delete_note

Permanently delete a note, optionally after the user confirms (OBSIDIAN_DELETE_ELICITATION)

obsidian_open_in_ui

Open a file in the Obsidian app, optionally in a new pane

obsidian_list_commands

List command-palette commands (opt-in via OBSIDIAN_ENABLE_COMMANDS)

obsidian_execute_command

Run a command-palette command by ID (opt-in via OBSIDIAN_ENABLE_COMMANDS)

Resources

Resource

Description

obsidian://vault/{+path}

A note's content, frontmatter, tags, and file metadata

obsidian://tags

Every vault tag with its usage count, as an uncapped snapshot

obsidian://status

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

Related MCP server: obsidian-headless-mcp

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 set, a listing holds only readable entries and the folders leading to the scope; those folders are walked and accepted as path


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 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 whitespace-split 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, 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; its 50-hit upstream cap sets truncated: true


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

  • 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


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 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 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 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 the following to your MCP client configuration file. The Obsidian Local REST API plugin must be installed and enabled in your vault; see Prerequisites.

{
  "mcpServers": {
    "obsidian-mcp-server": {
      "type": "stdio",
      "command": "bunx",
      "args": ["obsidian-mcp-server@latest"],
      "env": {
        "MCP_TRANSPORT_TYPE": "stdio",
        "MCP_LOG_LEVEL": "info",
        "OBSIDIAN_API_KEY": "your-local-rest-api-key"
      }
    }
  }
}

Or with npx (no Bun required):

{
  "mcpServers": {
    "obsidian-mcp-server": {
      "type": "stdio",
      "command": "npx",
      "args": ["-y", "obsidian-mcp-server@latest"],
      "env": {
        "MCP_TRANSPORT_TYPE": "stdio",
        "MCP_LOG_LEVEL": "info",
        "OBSIDIAN_API_KEY": "your-local-rest-api-key"
      }
    }
  }
}

Or with Docker:

{
  "mcpServers": {
    "obsidian-mcp-server": {
      "type": "stdio",
      "command": "docker",
      "args": [
        "run", "-i", "--rm",
        "-e", "MCP_TRANSPORT_TYPE=stdio",
        "-e", "MCP_LOG_LEVEL=info",
        "-e", "OBSIDIAN_API_KEY=your-local-rest-api-key",
        "ghcr.io/cyanheads/obsidian-mcp-server:latest"
      ]
    }
  }
}

Inside a container, the default OBSIDIAN_BASE_URL (http://127.0.0.1:27123) is the container's own loopback. Add -e OBSIDIAN_BASE_URL=http://host.docker.internal:27123 (Docker Desktop) or run with --network host (Linux) to reach the plugin on your host.

For Streamable HTTP, set the transport and start the server:

MCP_TRANSPORT_TYPE=http OBSIDIAN_API_KEY=... bun run start:http
# Server listens at http://127.0.0.1:3010/mcp by default

Prerequisites

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

  • The Obsidian Local REST API plugin, v4.0.0 or later, enabled in your vault. Generate an API key under Settings → Community Plugins → Local REST API and set it as 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.

Installation

  1. Clone the repository:

    git clone https://github.com/cyanheads/obsidian-mcp-server.git
  2. Navigate into the directory:

    cd obsidian-mcp-server
  3. Install dependencies:

    bun install
  4. Configure environment:

    cp .env.example .env
    # edit .env and set OBSIDIAN_API_KEY

Configuration

Variable

Description

Default

OBSIDIAN_API_KEY

Required. Bearer token for the Local REST API plugin.

—

OBSIDIAN_BASE_URL

Local 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_SSL

Verify 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_MS

Per-request timeout in milliseconds.

30000

OBSIDIAN_ENABLE_COMMANDS

Enable obsidian_list_commands and obsidian_execute_command. Commands are opaque and can be destructive.

false

OBSIDIAN_READ_PATHS

Comma-separated folder allowlist for reads. See Path policy.

unset (full vault)

OBSIDIAN_WRITE_PATHS

Comma-separated folder allowlist for writes. See Path policy.

unset (full vault)

OBSIDIAN_READ_ONLY

Deny every write and disable the command-palette pair.

false

OBSIDIAN_DELETE_ELICITATION

Ask 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_URL

Omnisearch 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_TYPE

Transport: stdio or http.

stdio

MCP_HTTP_PORT

HTTP server port.

3010

MCP_SESSION_MODE

HTTP 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_MODE

Authentication: none, jwt, or oauth.

none

MCP_LOG_LEVEL

Log level (RFC 5424).

info

LOGS_DIR

Directory for log files (Node.js only).

<project-root>/logs

OTEL_ENABLED

Enable OpenTelemetry.

false

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

Path policy

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

Goal

Config

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/

No writes anywhere

OBSIDIAN_READ_ONLY=true

  • Matching is by prefix, recursive, and case-insensitive; trailing slashes are normalized. Write paths are also readable.

  • 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. Search hits outside the read scope are dropped silently, and 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.

  • Tag listings (obsidian_list_tags, obsidian://tags) under OBSIDIAN_READ_PATHS cover readable notes only, counting notes rather than occurrences. Write paths and read-only alone leave them vault-wide.

  • The startup log prints the active scope.

Running the server

Local development

  • Build and run the production version:

    # One-time build
    bun run rebuild
    
    # Run the built server
    bun run start:stdio
    # or
    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 build -t obsidian-mcp-server .
docker run --rm -e OBSIDIAN_API_KEY=your-key -p 3010:3010 obsidian-mcp-server

The image defaults to HTTP transport on 0.0.0.0, stateful sessions, and logs in /var/log/obsidian-mcp-server. Point OBSIDIAN_BASE_URL at http://host.docker.internal:27123 (Docker Desktop) or use --network host (Linux) to reach the plugin. 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

Directory

Purpose

src/index.ts

createApp() entry point: registers tools and resources, probes Omnisearch, applies the read-only and command gates.

src/config

Server-specific environment variable parsing (OBSIDIAN_*) with Zod.

src/services/obsidian

Local REST API client, path policy, markdown-patch format handling, frontmatter/section/tag parsing, domain types.

src/mcp-server/tools

Tool definitions (*.tool.ts) and shared schemas and helpers.

src/mcp-server/resources

Resource definitions (*.resource.ts).

src/mcp-server/prompts

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

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    D
    maintenance
    Enables AI assistants to interact with Obsidian vaults through vector search, vault indexing, and file monitoring. It provides a standardized interface for searching and managing markdown-based personal knowledge management data within the Model Context Protocol.
    6,363 npm
    1
    MIT
  • F
    license
    Not graded
    quality
    C
    maintenance
    MCP server that provides tools and resources for AI models to interact with an Obsidian vault, enabling file operations, search, and management via the Model Context Protocol.
    2
    -
  • F
    license
    Not graded
    quality
    A
    maintenance
    Enables semantic search and note management for Obsidian vaults via the Model Context Protocol, allowing LLMs to search, read, and index notes, PDFs, and web pages locally.
    -
  • F
    license
    Not graded
    quality
    D
    maintenance
    Enables AI assistants to read, search, create, and modify Obsidian notes via the Model Context Protocol, supporting tasks like daily notes, backlinks, and project management.
    -