
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.
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:stdioRun 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.
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 paramsRead 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.
format*stringcontent · full · document-map · sectionincludeLinksbooleansectionobjecttarget*valueobsidian_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 paramsList 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`...
depthintegerextensionstringnameRegexstringpathstringobsidian_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 paramsList 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...
nameRegexstringobsidian_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 paramsOpen 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.
failIfMissingbooleannewLeafbooleanpath*stringobsidian_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 paramsSearch 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...
contextLengthintegercursorstringlogicobjectmaxMatchesPerHitintegermode*stringtext · jsonlogicpathPrefixstringquerystringobsidian_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 paramsCreate 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...
content*stringcontentTypestringmarkdown · jsondefault: markdownoverwritebooleansectionobjecttarget*valueobsidian_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 paramsAppend 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...
content*stringcontentTypestringmarkdown · jsondefault: markdowncreateTargetIfMissingbooleansectionobjecttarget*valueobsidian_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 paramsEdit 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.
content*stringcontentTypestringmarkdown · jsondefault: markdownoperation*stringappend · prepend · replacepatchOptionsobjectsection*objecttarget*valueobsidian_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 paramsSearch 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.
replacements*arraytarget*valueobsidian_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 paramsGet, set, or delete a single frontmatter key on a note, atomically. `set` requires a JSON-typed `value` (string, number, boolean, array, or object).
key*stringoperation*stringget · set · deletetarget*valuevaluevalueobsidian_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 paramsAdd, 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...
locationstringfrontmatter · inline · bothdefault: frontmatteroperation*stringadd · remove · listtagsarraytarget*valueobsidian_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 paramsPermanently 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.
target*valueRead, write, search, and surgically edit Obsidian vault notes, tags, and frontmatter via MCP. STDIO or Streamable HTTP.
Read, write, search, and surgically edit Obsidian vault notes — sections, frontmatter, tags — over the Local REST API plugin, with folder-scoped read/write permissions built in. Runs as a stdio process or a local Streamable HTTP server.
| Tool | 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 under a vault path. Recursive walk (default depth 2, max depth 20; 1000-entry cap) with optional extension and nameRegex filters. |
obsidian_list_tags | List vault tags with usage counts, including hierarchical parents. Ordered by count descending and capped at limit (default 200, max 10000), with the withheld remainder disclosed. Optional nameRegex and minCount narrow the set first. |
obsidian_list_commands | List Obsidian command-palette commands, optionally filtered by nameRegex on display name. Opt-in via OBSIDIAN_ENABLE_COMMANDS=true (paired with obsidian_execute_command). |
obsidian_search_notes | Search the vault by text, JSONLogic, or BM25-ranked Omnisearch (when the plugin is reachable). Results paginate via opaque cursors. |
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, creates the file if missing. With section, appends to a specific heading, block, or frontmatter field (file must exist). |
obsidian_patch_note | Surgical append / prepend / replace against a heading, block reference, or frontmatter field. |
obsidian_replace_in_note | Search-replace inside a single note, scoped to the body by default. Literal or regex matching with whole-word, whitespace-flexible, and case-sensitivity options; supports capture-group replacement. |
obsidian_manage_frontmatter | Atomic get / set / delete on a single frontmatter key. |
obsidian_manage_tags | Add, remove, or list tags. Defaults to the frontmatter tags: array; location: 'inline' or 'both' opts into mutating the note body. |
obsidian_delete_note | Permanently delete a note. Always asks the user to confirm first — the call is answered with a confirmation request and retried with the answer. |
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. |
| Resource | Description |
|---|---|
obsidian://vault/{+path} | A note in the vault — content, frontmatter, tags, and file metadata. |
obsidian://tags | All tags found across the vault, with usage counts (full snapshot). |
obsidian://status | Server reachability, auth status, plugin/Obsidian version info, and registered API extensions. |
Vault-note and tag data are also reachable via tools — obsidian_get_note for obsidian://vault/{+path}, obsidian_list_tags for obsidian://tags (count-ranked and capped, unlike the resource's raw snapshot). obsidian://status has no tool equivalent. Resources exist for clients that prefer attaching a note or vault snapshot to a conversation.
obsidian_get_note toolformat: "content" | "full" | "document-map" | "section" selects the projection; full accepts includeLinks: true for outgoing wiki/markdown links (vault-internal only — external URLs are filtered)path, the active file, or a periodic note (daily / weekly / monthly / quarterly / yearly)Parent::Child syntax; a bare leaf name matching several headings returns the first match and lists every colliding path in candidatespath resolution: a case-mismatched path retries against the canonical filename, an ambiguous case match fails with Conflict, and a NotFound carries Did you mean: …? suggestions when near-matches existnote_missing, path_forbidden, no_active_file, periodic_unsupported / periodic_disabled, and path_traversalobsidian_list_notes toolpath (default vault root); depth 1–20 (default 2 = target plus immediate children)extension and nameRegex (≤256 chars, no nested quantifiers) filters; a directory failing nameRegex is skipped without recursing into itexcluded.reason: "entry_cap" signals a truncated walk; narrow path or the filters to see the resttruncated: true marks entries cut off by the depth limit or by path policyobsidian_list_tags toolwork/tasks contributes to both work and work/tasks)limit (default 200, max 10000); optional nameRegex and minCount narrow the candidate set before rankingtruncated / shown / cap when the limit withheld resultsOBSIDIAN_READ_PATHS — tag names (never note contents) can surface from outside the read scopeobsidian_list_commands toolnameRegex filters on display nameOBSIDIAN_ENABLE_COMMANDS=true — absent from tools/list when unsetobsidian_execute_commandobsidian_search_notes toolmode: "text" | "jsonlogic" always; "omnisearch" is added to the schema only when the Omnisearch plugin's HTTP server is reachable at startup (restart to re-probe)text — substring match with contextLength-sized context windows (default 100) and an optional pathPrefix; jsonlogic — a JSONLogic tree with var paths into path / content / frontmatter.<key> / tags / stat.{ctime,mtime,size}, plus glob / regexp operators taking [PATTERN, VALUE]; omnisearch — BM25-ranked, quoted phrases, -exclusion, path: / ext: filters, typo tolerance, PDF/OCR via Text Extractor, hard-capped at 50 upstream hits (truncated: true when likely hit)cursor for page one, pass nextCursor from the prior response; text-mode hits additionally clip to maxMatchesPerHit (default 10), flagged with truncated / totalMatchesjsonlogic: {"regexp": ["\\[\\[Target Note(\\||#|\\]\\])", {"var": "content"}]}obsidian_write_note toolsection — full-file write; refuses to clobber an existing note unless overwrite: true (file_exists conflict otherwise, naming the surgical-edit tools as the alternative)section — PATCH-with-replace against a heading/block/frontmatter target, leaving the rest of the file untouched (overwrite is ignored); a bare heading leaf shared by several headings fails with ambiguous_sectioncreated, plus previousSizeInBytes / currentSizeInBytes on every call to spot an accidental clobber or a mistyped pathobsidian_append_to_note toolsection — appends to an existing file, or creates it with the given content as the whole body (created: true flags the second case)section — appends to a heading/block/frontmatter target; the file must already exist, and createTargetIfMissing: true brings the section itself into existencecontent for onepreviousSizeInBytes / currentSizeInBytes bracket every call for drift detectionobsidian_patch_note tooloperation: "append" | "prepend" | "replace" against one heading, block reference, or frontmatter field per callParent::Child path or an unambiguous bare leaf name; a leaf matching several headings fails with ambiguous_section and lists the candidatespatchOptions: createTargetIfMissing, applyIfContentPreexists (idempotency guard — otherwise content_preexists), trimTargetWhitespaceobsidian_replace_in_note toolreplacements, applied in array order, each over the previous one's outputscope: "body" (default, frontmatter left byte-identical) | "frontmatter" | "both"; frontmatter/both re-parse the rewritten YAML afterward and write nothing if it breaks (frontmatter_invalid)useRegex (≤1024 chars, no nested quantifiers), caseSensitive, wholeWord (\b…\b in both modes), flexibleWhitespace (literal mode only), replaceAll (default true)perReplacement[] reports bodyCount / frontmatterCount per entry; totalReplacements sums themobsidian_manage_frontmatter tooloperation: "get" | "set" | "delete" on a single frontmatter key; set requires a JSON-typed value (string, number, boolean, array, or object)get needs read access; set / delete need the path inside OBSIDIAN_WRITE_PATHS with OBSIDIAN_READ_ONLY=falseset / delete return the full frontmatter after the change plus previousSizeInBytes / currentSizeInBytesobsidian_manage_tags tooloperation: "add" | "remove" | "list"; location: "frontmatter" (default, canonical tags: array) | "inline" (body #tag, add appends at end-of-file) | "both" (reconciles both)[[...]], [text](...), [text][ref]), and \#-escaped hashes, so a heading anchor or wikilink alias is never mistaken for a tagadd / remove report applied vs. skipped tags plus the full tags set after the change; list ignores the input tags arrayobsidian_delete_note toolcancelled and issues no DELETEobsidian_open_in_ui toolfailIfMissing (default true) controls open-vs-create: opening an existing file needs read access, opening a missing one (with failIfMissing: false) creates it and needs write accessnewLeaf opens in a split pane instead of the active oneobsidian_get_note (case fallback, Did you mean suggestions); obsidian_delete_note deliberately doesn't get it — a destructive op never silently rewrites its targetcreatedIfMissing so the caller can tell which branch ranobsidian_execute_command toolcommandId (discover via obsidian_list_commands); runs with the same authority as a keyboard invocationOBSIDIAN_ENABLE_COMMANDS=true — absent from tools/list when unsetobsidian://vault/{+path} resource{+path} segment captures everything after /vault/, including slashesobsidian_get_note with format: "full" — content, frontmatter, tags, statOBSIDIAN_READ_PATHS / OBSIDIAN_WRITE_PATHS like the tool equivalentobsidian://tags resource/tags/ payload — unsorted, uncapped, includes hierarchical parentsobsidian_list_tags: no count-descending order, no limit / nameRegex / minCountobsidian://status resourceauthenticated (whether the configured OBSIDIAN_API_KEY was accepted), and plugin manifest infoapiExtensions[] lists registered plugin extensions — check for local-rest-api-periodic-notes before relying on periodic targets on plugin v5.0.2 and laterauthenticated reflects the key's validityThree 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.
Tag listing is vault-wide. obsidian_list_tags and the obsidian://tags resource aggregate tag names across the whole vault and are not narrowed by OBSIDIAN_READ_PATHS — they take no path to gate, so tag names (never note contents) from outside the read scope can surface.
The startup banner logs the active scope so operators can verify their config at boot.
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:
PATCH-with-target operationstags: array and inline #tag syntaxOBSIDIAN_READ_PATHS / OBSIDIAN_WRITE_PATHS and a global OBSIDIAN_READ_ONLY kill switch; opt-in command-palette pair gated by OBSIDIAN_ENABLE_COMMANDSinstructions on initialize report the active deployment — path policy, read-only mode, command-palette toggleAgent-friendly output:
reason, a JSON-RPC code, and a recovery.hint written for that case, so a rejection names what to do next instead of only what brokepreviousSizeInBytes / currentSizeInBytes, so a caller can spot an accidental clobber or unexpected upstream behavior without a follow-up readcandidates instead of silently picking one; tag operations report applied vs. skipped so a caller sees exactly what changedformat on obsidian_get_note, operation on obsidian_manage_frontmatter and obsidian_manage_tags, mode on obsidian_search_notes — callers branch on typed fields instead of parsing textAdd 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"
]
}
}
}
The default OBSIDIAN_BASE_URL (http://127.0.0.1:27123) points at the container's own loopback, not your host — add -e OBSIDIAN_BASE_URL=http://host.docker.internal:27123 (Docker Desktop) or run with --network host (Linux) so the container can reach the plugin.
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. Plugin v6.0 removes the markdown-patch 1.x wire format this server pins for section-targeted writes and the document map.target: { "type": "periodic" }) work across that whole range: natively on plugin v5.0.1 and earlier, and on v5.0.2 and later — which moved the /periodic/ routes out of the plugin — once the companion periodic-notes API extension is installed. Without that extension on v5.0.2+, periodic targets fail with a periodic_unsupported error naming it; obsidian://status lists the registered extensions if you want to check first. Every other target type is unaffected.obsidian_delete_note always asks for confirmation before deleting, so a client without that support can read and write notes but cannot delete one.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 |
OBSIDIAN_OMNISEARCH_URL | Override URL for the Omnisearch plugin's HTTP server. When unset, derives from OBSIDIAN_BASE_URL host with port 51361 (falling back to http://localhost:51361). Probed once at startup — if reachable, the omnisearch mode is added to obsidian_search_notes; otherwise it's omitted from the tool schema. Restart the server to re-probe. | derived |
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_SESSION_MODE | Session handling for the HTTP transport: stateless, stateful, or auto. Pinned to stateful — obsidian_delete_note confirms via an elicitation round, which stateless disables. | stateful |
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, stateful session mode (required for the obsidian_delete_note confirmation round), and logs to /var/log/obsidian-mcp-server. Point OBSIDIAN_BASE_URL at http://host.docker.internal:27123 (Docker Desktop) or run with --network host (Linux) so the container reaches the plugin on your host. 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.tsBugs, 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
Apache-2.0 — see LICENSE for details.
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:27123Base 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: falseWhether to verify the TLS certificate on the Obsidian endpoint. Default false because the plugin uses a self-signed cert.
OBSIDIAN_REQUEST_TIMEOUT_MSdefault: 30000Per-request timeout in milliseconds.
OBSIDIAN_ENABLE_COMMANDSdefault: falseOpt-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_PATHSOptional 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_PATHSOptional comma-separated vault-relative folder allowlist for writes. Same syntax as OBSIDIAN_READ_PATHS. Unset = full vault. Example: 'projects/,scratch/'.
OBSIDIAN_READ_ONLYdefault: falseGlobal 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: infoSets the minimum log level for output (e.g., 'debug', 'info', 'warn').
MCP_HTTP_HOSTdefault: 127.0.0.1The hostname for the HTTP server.
MCP_HTTP_PORTdefault: 3010The port to run the HTTP server on.
MCP_HTTP_ENDPOINT_PATHdefault: /mcpThe endpoint path for the MCP server.
MCP_AUTH_MODEdefault: noneAuthentication mode to use: 'none', 'jwt', or 'oauth'.