by cyanheads
Provides a local service that lets agents read, write, search, and surgically edit notes, tags, and frontmatter inside an Obsidian vault through the Model Context Protocol tools.
Enables programmatic interaction with an Obsidian vault—fetching notes, listing tags, performing structured searches, and applying fine‑grained edits (append, prepend, replace, frontmatter management) via MCP‑defined tools.
npx):
{
"command": "npx",
"args": ["-y", "obsidian-mcp-server@latest"]
}
OBSIDIAN_API_KEY (the token from the Local REST API plugin). Optional variables control transport, path permissions, and command‑palette exposure.npx -y obsidian-mcp-server@latest (default transport = stdio).MCP_TRANSPORT_TYPE=http and run the same command; the server will listen on http://127.0.0.1:3010/mcp.obsidian_get_note, obsidian_search_notes, obsidian_write_note, etc.tags: and inline #tag syntax.OBSIDIAN_READ_PATHS, OBSIDIAN_WRITE_PATHS, and a global OBSIDIAN_READ_ONLY kill switch.OBSIDIAN_ENABLE_COMMANDS).@cyanheads/mcp-ts-core for declarative tool/resource registration and unified error handling.Q: Do I need to install Obsidian locally? A: Yes. The server talks to the Obsidian Local REST API plugin running inside your desktop Obsidian vault.
Q: How do I enable the command‑palette tools?
A: Set the environment variable OBSIDIAN_ENABLE_COMMANDS=true before starting the server.
Q: Can I restrict which folders the server can read or write?
A: Use OBSIDIAN_READ_PATHS and OBSIDIAN_WRITE_PATHS (comma‑separated, prefix‑based). Setting OBSIDIAN_READ_ONLY=true disables all write operations.
Q: What transport should I choose?
A: STDIO is simplest for local usage. Use HTTP (MCP_TRANSPORT_TYPE=http) for remote clients, remembering to secure it with JWT or OAuth.
Q: How do I provide the API key?
A: Export OBSIDIAN_API_KEY with the token generated in the Local REST API plugin settings.
Q: What happens if a write attempts to overwrite an existing file?
A: obsidian_write_note refuses without overwrite:true. Use obsidian_patch_note, obsidian_append_to_note, or obsidian_replace_in_note for in‑place modifications.
Q: Is there a limit to the number of search results? A: Yes, search results are capped at 100 hits. When the limit is exceeded, an overflow indicator is returned.
Q: How are errors reported?
A: Errors are typed (e.g., path_forbidden, file_missing, conflict) with JSON‑RPC codes, allowing agents to react programmatically.
Fourteen tools grouped by shape — readers fetch notes and metadata, writers create or surgically edit content, managers reconcile tags and frontmatter, and a guarded escape hatch dispatches Obsidian command-palette commands.
| Tool Name | Description |
|---|---|
obsidian_get_note |
Read a note as raw content, full structured form (content + frontmatter + tags + stat, with optional outgoing links), structural document map, or a single section. |
obsidian_list_notes |
List notes and subdirectories at a vault path with a recursive walk (default depth 2 — structural overview; max 20) bounded by a 1000-entry cap. Optional extension and nameRegex filters apply across the tree; regex-filtered directories are skipped without recursing into them. Returns flat entries[] plus a box-drawing tree in the rendered output; per-directory truncated: true flags where the depth limit cut off recursion. |
obsidian_list_tags |
List every tag found across the vault with usage counts, including hierarchical parents. |
obsidian_list_commands |
List Obsidian command-palette commands available for execution. Opt-in via OBSIDIAN_ENABLE_COMMANDS=true (paired with obsidian_execute_command). |
obsidian_search_notes |
Search the vault by text, Dataview DQL, or JSONLogic. Text-mode matches return surrounding context windows (contextLength) — capped at 100 hits with overflow indicator. |
obsidian_write_note |
Create a note, replace a single section in place, or — with overwrite: true — clobber an existing file. Refuses whole-file writes against an existing path by default. |
obsidian_append_to_note |
Append content to a note. Without section it creates the file if missing — your content becomes the entire file. With section, appends to that heading/block/frontmatter (PATCH; the file must exist). |
obsidian_patch_note |
Surgical append / prepend / replace against a heading, block reference, or frontmatter field. |
obsidian_replace_in_note |
Body-wide search-replace inside a single note. Literal or regex matching, with wholeWord, flexibleWhitespace, caseSensitive, replaceAll, and $1/$& capture groups. |
obsidian_manage_frontmatter |
Atomic get / set / delete on a single frontmatter key. |
obsidian_manage_tags |
Add, remove, or list tags — reconciles frontmatter tags: and inline #tag syntax. |
obsidian_delete_note |
Permanently delete a note. Elicits human confirmation when the client supports it. |
obsidian_open_in_ui |
Open a file in the Obsidian app UI, with failIfMissing and newLeaf toggles. |
obsidian_execute_command |
Execute an Obsidian command-palette command by ID. Opt-in via OBSIDIAN_ENABLE_COMMANDS=true. |
obsidian_get_noteRead a note in one of four projections, addressed by vault path, the active file, or a periodic note (daily, weekly, monthly, quarterly, yearly).
format: "content" — raw markdown bodyformat: "full" — content, frontmatter, tags, and file metadata; pass includeLinks: true to also parse outgoing wiki and markdown link references from the body (vault-internal only — external URLs are filtered)format: "document-map" — catalog of headings, block references, and frontmatter fieldsformat: "section" — single heading/block/frontmatter section value (requires section); heading sections include the full subtree under that headingPair the document-map projection with obsidian_patch_note to discover edit targets before patching.
obsidian_search_notesThree search modes selected by mode:
text — substring match with surrounding context windows. contextLength controls characters of context per side of each match (default 100; bump it for more context per hit). Optional pathPrefix filter (text mode only — passing pathPrefix in dataview or jsonlogic mode is rejected with path_prefix_invalid_mode).dataview — Dataview DQL (TABLE …) for path/date/metadata queries; file.mtime, file.path, etc. are queryablejsonlogic — JSONLogic tree evaluated against path, content, frontmatter.<key>, tags, and stat.{ctime,mtime,size}; custom glob and regexp operatorsResults are capped at 100 hits. When the upstream returns more, an excluded indicator surfaces the overflow count and a hint to narrow the query. Text-mode hits are additionally clipped per file at maxMatchesPerHit (default 10) so a single match-heavy note can't blow the response budget — clipped hits carry truncated: true and totalMatches.
obsidian_write_noteCreate or surgically replace, with a protective default against accidental whole-file overwrites.
section — full-file PUT. Refuses to clobber an existing file unless overwrite: true is set. The file_exists (Conflict) error suggests obsidian_patch_note / obsidian_append_to_note / obsidian_replace_in_note for in-place edits.section — PATCH-with-replace against the named heading/block/frontmatter field, leaving the rest of the file untouched. The overwrite flag is ignored in section mode.The output reports created: true when the call brought a new file into existence; false when it replaced an existing one or targeted a section. Every mutating tool also returns previousSizeInBytes and currentSizeInBytes so an agent can spot accidental clobbers, unexpected upstream behavior, or a typo path that landed at the wrong file.
obsidian_append_to_noteA combined upsert + section-append primitive that mirrors the upstream Local REST API behavior:
section — POST to /vault/{path}. Appends when the file exists, creates the file with your content as the entire body when it doesn't. The output's created: true flags the second branch so the agent can notice when a typo path or a not-yet-created daily note silently turned into a brand-new file.section — PATCH-with-append against the named heading, block reference, or frontmatter field. The file must exist (PATCH preflight throws note_missing otherwise). Pass createTargetIfMissing: true to bring the section itself into existence inside an existing file. Block-reference targets concatenate adjacent to the block line without a separator — include a leading newline in content if you want one.previousSizeInBytes is 0 on the upsert-create branch and the actual file size otherwise; currentSizeInBytes is the post-write size read from the upstream after the operation. Compare deltas against Buffer.byteLength(content) to detect auto-newline injection or concurrent writers.
obsidian_patch_noteSurgical edits at a single document target.
operation: "append" adds after the sectionoperation: "prepend" adds before the sectionoperation: "replace" swaps it outUse obsidian_get_note with format: "document-map" to discover what targets exist before patching.
obsidian_replace_in_noteBody-wide search-replace for edits that don't fit obsidian_patch_note's structural targets. The note is fetched, replacements are applied sequentially (each sees the previous output), and the result is written back in a single PUT.
Per-replacement options:
useRegex — treat search as an ECMAScript regex. With useRegex: true, the replacement honors $1 / $& capture-group references.caseSensitive — when false, match case-insensitivelywholeWord — wrap the pattern in \b…\b; works in both literal and regex modesflexibleWhitespace — substitute any run of whitespace in search with \s+. Literal mode only — has no effect when useRegex: true (express it directly).replaceAll — when false, only the first match is replacedLiteral mode preserves $1 / $& in the replacement verbatim — only useRegex: true expands capture-group references.
obsidian_manage_tagsAdd, remove, or list tags on a note. Reconciles both representations:
tags: array#tag syntax in the bodyadd ensures the tag is present in the requested location(s); remove strips it. Inline #tag occurrences inside fenced code blocks are intentionally left alone.
obsidian_delete_notePermanently delete a note. When the client supports elicit, the server requests human confirmation before issuing the DELETE and the prompt includes the file's byte size — destructive blast radius visible before the user confirms. Without elicitation, the destructiveHint annotation surfaces the operation in the host's approval flow. The output reports previousSizeInBytes (size at the moment of deletion) and currentSizeInBytes: 0.
obsidian_execute_commandDispatch an Obsidian command-palette command by ID (discoverable via obsidian_list_commands). Behavior is command-dependent — some commands open UI, others delete files or close the vault.
Off by default. When OBSIDIAN_ENABLE_COMMANDS is unset, both obsidian_execute_command and its discovery partner obsidian_list_commands are wrapped with disabledTool() — absent from tools/list (the LLM can't invoke them) but still visible in the operator-facing manifest with a hint to enable them.
Three optional env vars gate which vault paths each tool can target. Default unset = full vault for both reads and writes — backwards compatible.
| Goal | Config |
|---|---|
| Default (current behavior) | all unset |
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-only deployment — no writes anywhere | OBSIDIAN_READ_ONLY=true |
Matching is prefix-based with implicit recursion, case-insensitive, with trailing slashes normalized. projects/ matches projects/a.md, projects/sub/b.md, etc.
Write paths are implicitly readable — you can't sanely edit what you can't see. So a read passes when the target matches READ_PATHS or WRITE_PATHS.
OBSIDIAN_READ_ONLY=true short-circuits before the path checks — every write tool and the command-palette pair are wrapped with disabledTool() at startup (absent from tools/list), and any write that still reaches the service is denied at runtime regardless of WRITE_PATHS.
Denies are typed path_forbidden (JSON-RPC code Forbidden) with the active scope echoed back in data.recovery.hint and data.activeScope, so the LLM can self-correct without inspecting server logs. Search results from obsidian_search_notes are filtered against READ_PATHS silently — surfacing a "we hid N hits" indicator would defeat the gate.
The startup banner logs the active scope so operators can verify their config at boot.
| Type | URI | Description |
|---|---|---|
| Resource | obsidian://vault/{+path} |
A note in the vault — content, frontmatter, tags, and file metadata. |
| Resource | obsidian://tags |
All tags found across the vault, with usage counts. |
| Resource | obsidian://status |
Server reachability, auth status, plugin/Obsidian version info, and the plugin manifest. |
All resource data is also reachable via tools — obsidian_get_note for obsidian://vault/{+path}, obsidian_list_tags for obsidian://tags. Resources exist for clients that prefer attaching a specific note or vault snapshot to a conversation.
Built on @cyanheads/mcp-ts-core:
errors[] contracts.instructions on initialize — surfaces deployment-specific orientation (active path policy, read-only mode, command-palette toggle) to spec-compliant clients alongside the static tool/resource catalognone, jwt, oauthThe server itself is stateless — every tool call hits the Local REST API directly. The framework's storage backends, request-state KV, and progress streams aren't used here; Obsidian is single-vault and there's nothing to persist between calls.
Obsidian-specific:
PATCH-with-target operationstags: array and inline #tag syntax (skipping fenced code blocks)ctx.elicitOBSIDIAN_READ_PATHS / OBSIDIAN_WRITE_PATHS and a global OBSIDIAN_READ_ONLY kill switch — denies are typed path_forbidden with the active scope echoed back in the error dataobsidian_list_commands + obsidian_execute_command) — registered only when OBSIDIAN_ENABLE_COMMANDS=trueobsidian_get_note and obsidian_open_in_ui — silently retries case-mismatched paths against the canonical filename, throws Conflict on ambiguous case matches, and enriches NotFound with Did you mean: …? suggestions when only near-matches exist. obsidian_delete_note is deliberately excluded — a destructive op shouldn't silently rewrite the target path.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": {
"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": {
"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"
}
}
}
}
For Streamable HTTP, set the transport and start the server. Inline env vars work for one-off runs; for repeated use, copy values into .env (see .env.example) and run bun run start:http.
MCP_TRANSPORT_TYPE=http OBSIDIAN_API_KEY=... bun run start:http
# Server listens at http://127.0.0.1:3010/mcp by default
OBSIDIAN_API_KEY.http://127.0.0.1:27123 for simplicity. Enable "Non-encrypted (HTTP) Server" in the plugin settings to use it. To use the always-on HTTPS port instead, set OBSIDIAN_BASE_URL=https://127.0.0.1:27124; the plugin's self-signed cert is handled by OBSIDIAN_VERIFY_SSL=false (the default).Clone the repository:
git clone https://github.com/cyanheads/obsidian-mcp-server.git
Navigate into the directory:
cd obsidian-mcp-server
Install dependencies:
bun install
Configure environment:
cp .env.example .env
# edit .env and set OBSIDIAN_API_KEY
| Variable | Description | Default |
|---|---|---|
OBSIDIAN_API_KEY |
Required. Bearer token for the Obsidian Local REST API plugin. | — |
OBSIDIAN_BASE_URL |
Base URL of the Local REST API plugin. Use https://127.0.0.1:27124 for the always-on HTTPS port (self-signed cert). |
http://127.0.0.1:27123 |
OBSIDIAN_VERIFY_SSL |
Verify the TLS certificate. Default false because the plugin uses a self-signed cert. On Node, the dispatcher's rejectUnauthorized option handles this without any process-wide change. On Bun, the runtime ignores that option, so the service additionally sets NODE_TLS_REJECT_UNAUTHORIZED=0 — that fallback is scoped to Bun only. |
false |
OBSIDIAN_REQUEST_TIMEOUT_MS |
Per-request timeout in milliseconds. | 30000 |
OBSIDIAN_ENABLE_COMMANDS |
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. |
false |
OBSIDIAN_READ_PATHS |
Comma-separated vault-relative folder allowlist for read operations. Prefix-based with implicit recursion; case-insensitive; trailing slashes normalized. Unset = full vault. Write paths are implicitly readable. | unset |
OBSIDIAN_WRITE_PATHS |
Comma-separated vault-relative folder allowlist for write operations. Same syntax as OBSIDIAN_READ_PATHS. Unset = full vault. |
unset |
OBSIDIAN_READ_ONLY |
Global kill switch. When true, denies every write regardless of OBSIDIAN_WRITE_PATHS, and suppresses the OBSIDIAN_ENABLE_COMMANDS pair (commands can mutate). |
false |
MCP_TRANSPORT_TYPE |
Transport: stdio or http. |
stdio |
MCP_HTTP_HOST |
Host for the HTTP server. | 127.0.0.1 |
MCP_HTTP_PORT |
Port for the HTTP server. | 3010 |
MCP_HTTP_ENDPOINT_PATH |
Endpoint path for the JSON-RPC handler. | /mcp |
MCP_PUBLIC_URL |
Public origin override for TLS-terminating reverse-proxy deployments (landing page, Server Card, RFC 9728 metadata). | unset |
MCP_AUTH_MODE |
Auth mode: none, jwt, or oauth. |
none |
MCP_AUTH_SECRET_KEY |
Required when MCP_AUTH_MODE=jwt. ≥32-char shared secret used to verify incoming JWTs. |
— |
MCP_AUTH_DISABLE_SCOPE_CHECKS |
When true, bypasses per-tool scope enforcement after the auth-context presence check. Token signature, audience, issuer, and expiry validation remain intact. Use only when a custom claim can't be injected and combine with OBSIDIAN_READ_PATHS / OBSIDIAN_WRITE_PATHS / OBSIDIAN_READ_ONLY for access control. A WARNING is logged at startup whenever the bypass is active. |
false |
MCP_LOG_LEVEL |
Log level (RFC 5424). | info |
LOGS_DIR |
Directory for log files (Node.js only). | <project-root>/logs |
OTEL_ENABLED |
Enable OpenTelemetry instrumentation (spans, metrics, completion logs). | false |
See .env.example for the full list of optional overrides.
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 build -t obsidian-mcp-server .
docker run --rm -e OBSIDIAN_API_KEY=your-key -p 3010:3010 obsidian-mcp-server
The Dockerfile defaults to HTTP transport, stateless session mode, and logs to /var/log/obsidian-mcp-server. OpenTelemetry peer dependencies are installed by default — build with --build-arg OTEL_ENABLED=false to omit them.
The image binds to 0.0.0.0 inside the container (required for Docker port mapping). For any deployment reachable beyond your own machine, set MCP_AUTH_MODE=jwt (with MCP_AUTH_SECRET_KEY) or oauth — otherwise the listener forwards your OBSIDIAN_API_KEY to the vault on behalf of every caller.
| Directory | Purpose |
|---|---|
src/index.ts |
createApp() entry point — registers tools/resources and inits the Obsidian service. |
src/config |
Server-specific environment variable parsing (OBSIDIAN_*) with Zod. |
src/services/obsidian |
Local REST API client, frontmatter operations, section extractor, domain types. |
src/mcp-server/tools |
Tool definitions (*.tool.ts) and shared input schemas. |
src/mcp-server/resources |
Resource definitions (*.resource.ts). |
src/mcp-server/prompts |
Prompt definitions (currently empty — CRUD/search shape doesn't benefit from a structured template). |
tests/ |
Vitest tests mirroring src/. |
docs/ |
Upstream OpenAPI spec for the Local REST API plugin and the generated tree.md. |
changelog/ |
Per-version release notes; CHANGELOG.md is the regenerated rollup. |
See CLAUDE.md for development guidelines and architectural rules. The short version:
try/catch in tool logicctx.log for request-scoped logging, ctx.state for tenant-scoped storagesrc/mcp-server/*/definitions/index.tsIssues and pull requests are welcome. Run checks and tests before submitting:
bun run devcheck
bun run test
Apache-2.0 — see LICENSE for details.
Please log in to share your review and rating for this MCP.
Explore related MCPs that share similar capabilities and solve comparable challenges
by modelcontextprotocol
A basic implementation of persistent memory using a local knowledge graph. This lets Claude remember information about the user across chats.
by topoteretes
Provides dynamic memory for AI agents through modular ECL (Extract, Cognify, Load) pipelines, enabling seamless integration with graph and vector stores using minimal code.
by basicmachines-co
Enables persistent, local‑first knowledge management by allowing LLMs to read and write Markdown files during natural conversations, building a traversable knowledge graph that stays under the user’s control.
by agentset-ai
Provides an open‑source platform to build, evaluate, and ship production‑ready retrieval‑augmented generation (RAG) and agentic applications, offering end‑to‑end tooling from ingestion to hosting.
by smithery-ai
Provides read and search capabilities for Markdown notes in an Obsidian vault for Claude Desktop and other MCP clients.
by chatmcp
Summarize chat messages by querying a local chat database and returning concise overviews.
by dmayboroda
Provides on‑premises conversational retrieval‑augmented generation (RAG) with configurable Docker containers, supporting fully local execution, ChatGPT‑based custom GPTs, and Anthropic Claude integration.
by qdrant
Provides a Model Context Protocol server that stores and retrieves semantic memories using Qdrant vector search, acting as a semantic memory layer.
by doobidoo
Provides a universal memory service with semantic search, intelligent memory triggers, OAuth‑enabled team collaboration, and multi‑client support for Claude Desktop, Claude Code, VS Code, Cursor and over a dozen AI applications.
{
"mcpServers": {
"obsidian": {
"command": "npx",
"args": [
"-y",
"obsidian-mcp-server@latest"
],
"env": {
"OBSIDIAN_API_KEY": "<YOUR_API_KEY>"
}
}
}
}claude mcp add obsidian npx -y obsidian-mcp-server@latest