Obsidian MCP
Provides read and write access to a self-hosted Obsidian vault, exposing tools to save notes with folder hints, links, and types, append to daily notes or arbitrary notes, and read vault content and repository metadata, with idempotency and structured recovery handling.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@Obsidian MCPAppend today's meeting notes to my daily note"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
Obsidian MCP
Self-hosted Obsidian access over stdio or authenticated Streamable HTTP. The packaged deterministic keeper performs every vault mutation. Neither a Claude plugin nor the private llm-tools harness is required. The host running the server needs filesystem access to the vault; remote clients do not.
This initial extraction imports only the tracked packages/mcp-server tree from
nhangen/claude-obsidian-plugin at 76df3151104e37f409589fcda7ff3da65cc8c0d8,
with fresh repository history and the original MIT notice. It preserves the
legacy package identity, launcher names, and protocol contract. Version 0.2.1
prepares the first standalone release without reusing the imported 0.2.0 version.
It does not publish a new release, replace an existing deployment, or migrate
the optional plugin/client integrations. That cutover remains separate work.
Portable clients
The standalone server release is mcp-v0.2.1.
The public portable client source is in packages/client,
including the pinned bridge, lifecycle installer, optional isolated Codex capture,
and an explicit Claude plugin attachment API. The public runtime requires no
private llm-tools harness. Attachment preserves existing host settings and does
not activate writers; the plugin separately owns migration and admission gates.
Attached Claude commit capture uses canonical MCP arguments directly, with bounded
Pre-hook preparation and immutable retries. It needs no Bash stdin helper grant;
existing MCP tool authorization and recovery checks still apply.
Claude attach can snapshot a private exact-repository commit route map once with
--commit-routes /absolute/private/routes.json; user-scope children inherit it.
See commit route policy for the
bounded format, frozen-target behavior, and host limits.
Explicit attached Claude profiles can opt into --project-mode directory for
session-only non-Git workspaces. Git remains the default and is required for
commit capture. See the directory-profile boundary;
this source support does not establish a released client or whole-host acceptance.
From a trusted checkout, with Python 3.11+ and Node.js 22+:
python3 scripts/pack-obsidian-mcp-client.py --output /absolute/new/client-bundle
python3 /absolute/new/client-bundle/bin/obsidian-mcp-client-install \
--bundle /absolute/new/client-bundle --release-sha256 <trusted-printed-digest>The packer requires an output outside the repository and prints the release
inventory digest. Use the installer and digest from the trusted checkout or CI
artifact, not from an unauthenticated bundle. Host setup guides are under
integrations/codex,
integrations/claude and
integrations/opencode. Setup is explicit;
installation does not change credentials, hooks, host settings or writer ownership.
Automatic-capture host acceptance and client-release publication remain tracked in
issue #2. Fixture tests are not
evidence that an installed host delivered its hooks or persisted a real session.
Related MCP server: obsidian-vault-mcp
Build and configure
Use Node.js 20 or newer (CI pins 24.9.0), npm, Bash, Git, and standard Unix utilities including awk, sed, find, grep, and tar. Windows hosts need a compatible Unix environment such as WSL. Run from this repository's root:
git clone https://github.com/nhangen/obsidian-mcp.git
cd obsidian-mcp
npm ci
npm run build
npm test
OBSIDIAN_LOCAL_MD=/absolute/path/to/obsidian.local.md npm startCreate the referenced config outside the checkout, replacing the vault path and taxonomy with existing folders in your own vault:
---
vault_path: /absolute/path/to/vault
daily_path: Daily/
---
## Project Taxonomy
| Domain | Vault path | Precedence | Notes |
|--------|------------|------------|-------|
| Development | Projects/Development/ | 10 | Code and engineering |Keep this config private and set OBSIDIAN_LOCAL_MD explicitly on every server
host. Set MCP_REPOSITORY_ROOTS explicitly if repository metadata is needed;
do not rely on source-checkout defaults. No scheduler or automatic session saver
is installed by these commands.
The TypeScript build emits the runnable package into dist/ and copies the
canonical deterministic config and commit-metadata helpers into that package
output. The package exposes claude-obsidian-mcp for stdio and
claude-obsidian-mcp-http for authenticated Streamable HTTP. Both commands
resolve their implementation from the installed package, independent of the
caller's working directory.
An installed package can be configured in a stdio MCP client with the stable launcher and explicit configuration boundaries:
{
"mcpServers": {
"obsidian": {
"command": "claude-obsidian-mcp",
"env": {
"OBSIDIAN_LOCAL_MD": "/absolute/path/to/obsidian.local.md",
"MCP_REPOSITORY_ROOTS": "/absolute/path/to/repository"
}
}
}
}The manifest examples use /absolute/path/to/ as an explicit placeholder. Replace
that prefix with the absolute path to the installed package before importing the
manifest; clients must not expand shell variables or rely on an inherited PATH.
The client invokes the package command rather than a file inside the package;
configuration and repository authorization remain explicit environment values.
Installed packages authorize no repository roots when MCP_REPOSITORY_ROOTS
is omitted. Multiple roots use the platform path delimiter.
The stdio server defaults to the read profile, exposing only vault and
repository read tools. Set MCP_STDIO_PROFILE=write explicitly as the
host-level policy authorization for trusted routine Obsidian writes. The write
profile provides the write tools. The supported client policy requires an
idempotency key for routine writes and does not require a per-write user
approval prompt. Unknown profile values fail closed, and requests outside the
configured path, scope, or recovery contract are rejected.
Authenticated Streamable HTTP uses stateful sessions at /mcp for the
compatibility revision 2025-11-25:
MCP_HTTP_JWT_SECRET='<at least 32 bytes>' \
MCP_HTTP_JWT_ISSUER='https://issuer.example' \
MCP_HTTP_JWT_AUDIENCE='claude-obsidian' \
MCP_HTTP_ALLOWED_HOSTS='127.0.0.1,localhost' \
MCP_HTTP_ALLOWED_ORIGINS='https://client.example' \
npm run start:httpThe HTTP server binds 127.0.0.1:3000 by default. Set MCP_HTTP_BIND and
MCP_HTTP_PORT explicitly when needed. Remote deployments must terminate TLS
in a trusted reverse proxy and restrict network access to the configured bind
address. Every request requires an HS256 bearer JWT with the exact configured
issuer and audience, a future exp, and a scope accepted by the requested
surface. Vault tools and resources require vault:read; repository metadata
requires repo:read; write tools require vault:write. vault:admin is
accepted for authentication but does not expose an additional MCP operation.
Query strings are rejected, so credentials cannot be supplied in URLs.
MCP_HTTP_MAX_BODY_BYTES and MCP_HTTP_MAX_RESPONSE_BYTES default to 1 MiB.
MCP_HTTP_CONCURRENCY_LIMIT defaults to 16, and
MCP_HTTP_REQUEST_TIMEOUT_MS defaults to 30000, and
MCP_HTTP_SESSION_TTL_MS defaults to 15 minutes. Host and Origin allowlists
are checked before authentication and before any MCP handler runs. POST carries
JSON-RPC, GET opens the session SSE stream, and DELETE closes the session. The
modern 2026-07-28 Streamable HTTP envelope remains planned because the pinned
stateful adapter does not classify modern envelopes; it is not advertised as
supported.
Monitoring
The HTTP server writes one bounded mcp-http-request JSON event per request to
stderr. Events contain the HTTP status, duration, MCP method, tool name when
applicable, and sanitized JWT subject. They never contain bearer tokens,
request arguments, or vault content. status retains the legacy terminal status;
terminal_status makes that meaning explicit. wire_status is the status of
headers sent by the server, or null if no headers were sent (not proof of client
receipt). An SSE response can therefore have wire_status: 200 and
terminal_status: 408. terminal_reason distinguishes completed, http-error,
request-timeout, client-disconnect, and other request-aborted outcomes.
timeout_phase is body, transport, or response_stream when a server deadline
fires, otherwise null. These fields diagnose the responsible boundary; they do
not identify the root cause of a slow tool or change deadlines/retry policy.
A Linux systemd deployment can run the packaged
node monitor.mjs command to perform an authenticated initialize handshake and
aggregate recent request counts, failures, tool calls, clients, stream counts,
and p95 request latency from the systemd journal. The p95 excludes long-lived
GET/SSE streams, which are reported separately. Set MCP_MONITOR_URL, the MCP_HTTP_JWT_* values,
and MCP_MONITOR_EXPECTED_VERSION in the monitor's environment. The command
writes an atomic state file and exits non-zero when the handshake, version
check, or journal read fails. This monitor requires journalctl and reads the
user service journal. Set MCP_MONITOR_SERVICE to the actual user unit name;
the default is claude-obsidian-mcp.service. It is not a macOS health checker
or a checker for the system-level service example below.
The server resolves OBSIDIAN_LOCAL_MD through the existing stable resolver.
The stdio entrypoint does not start an HTTP listener. In this
repository's source checkout, repository metadata access can default to the
checkout root. Always configure explicit roots for deployment. An
installed package never infers authorization from its install location or
working directory. Set the path-delimited MCP_REPOSITORY_ROOTS environment
variable to enable obsidian_commit_meta for explicit local repository roots.
The taxonomy resource returns only the project-taxonomy table. Tool arguments are validated inside the handlers so invalid input uses the stable structured error contract instead of leaking SDK validation text.
Write contract
obsidian_keeper_save accepts title, body, required resolved: true and
folder_hint, plus optional type,
links, and session_link_date (links the note from that day's daily note; needs a valid daily_path), plus optional idempotency_key and request_id. folder_hint is
the caller-resolved vault-relative target folder; this MCP adapter does not run
librarian routing or deduplication. Streamable HTTP
writes require idempotency_key; local stdio calls retain keyless compatibility.
obsidian_daily_append accepts content, optional section, date or
target_path (an exact vault-relative .md path to append to instead of the daily note; mutually exclusive with date),
skip_if_hash (a 7-64 character hexadecimal commit hash that must appear in the submitted section heading), and
init_content (up to 65536 characters written as the note's initial content, such as frontmatter, only when the target note does not exist yet; ignored for an existing note), plus the same request fields. An exact heading hash skips; a strict abbreviation/full-hash relation returns CONFLICT without writing because the vault cannot prove commit identity. Daily writes use the configured
daily_path, and results report the vault-relative path actually written. MCP
writes that use daily_path (daily appends without target_path, and session_link_date) fail with CONFIG_INVALID when it is absent or invalid; the
adapter never silently substitutes Daily/.
Both tools return status, request_id, idempotency_key, path,
affected_paths, warnings, recovery, error_code, and retryable.
committed and skipped are successful. conflict, partial, and failed
return isError=true.
Commit-capture rollback is safe only after both the server/plugin and any copied Git-hook runtime have this guard and emit full commit IDs. Pre-upgrade plugin rollback remains unsupported.
Reuse an idempotency key only for the same payload. A repeated key and payload
returns skipped; a repeated key with different content returns
IDEMPOTENCY_CONFLICT. After a retryable timeout, cancellation, or partial
result, inspect affected_paths and recovery, then retry with the same key.
Keyless ambiguous failures, including partial outcomes, are not automatically
retryable. Their recovery action requires manual verification of
affected_paths before any retry.
If recovery.required remains true, follow its action before changing the key
or editing affected files manually.
Each write runs the keeper under a per-call-type subprocess cap:
MCP_KEEPER_SAVE_TIMEOUT_MS (default 25000) for obsidian_keeper_save, which
also reconciles the folder INDEX, MCP_DAILY_APPEND_TIMEOUT_MS (default 10000)
for obsidian_daily_append, and MCP_COMMIT_META_TIMEOUT_MS (default 5000)
for obsidian_commit_meta. Values are milliseconds between 100 and 600000; an
invalid value stops the server at startup. On the HTTP transport keep every
cap, plus the 500 ms the server allows a stopped subprocess to exit, below
MCP_HTTP_REQUEST_TIMEOUT_MS, whose maximum is 300000: a call that outlives
its HTTP request is cancelled with it and the client never sees the outcome.
The HTTP server prints an mcp-http-warning line at startup for each cap that
does not fit.
When a keeper save is stopped after the note itself was written (its cap, its
output limit, or the keeper exiting without a result), the result carries that
code (SUBPROCESS_TIMEOUT, for example) and its warnings and
recovery.action name the unfinished steps. obsidian_keeper_save does not
link a daily note, so those are INDEX and, for a keyed save, the idempotency
record. Do not write the note again. If the save had an idempotency key, the
result is partial with retryable: true: retry with that key, after raising
MCP_KEEPER_SAVE_TIMEOUT_MS when the cap was the cause (the same cap would
stop the retry at the same step). Without a key the result is failed with
retryable: false; finish the named steps by hand. A request that is
cancelled after the note was written gets the same outcome inside the server,
but MCP sends no response to a cancelled request.
The ask_vault_librarian prompt is a bounded read-only MCP workflow over the
taxonomy, librarian, pending, and search-preview surfaces. It does not invoke
the existing librarian agent, whose query path may refresh INDEX links. The
summarize_session prompt accepts the transcript as an explicit argument and
returns the provider instructions for the existing SKIP-or-Markdown summary
contract, including configured routing, taxonomy, scoring thresholds, and
keeper outcome boundaries. It does not run a provider or persist the result;
persistence remains in the client-side shell workflow. Both prompts are exposed
to clients with vault:read. The admin-only reorganize_vault workflow remains
outside the MCP MVP until a separate policy and write contract exists.
The machine-readable contract is contract.json. Its fixture
cases are in test/fixtures/contract-fixtures.json
and are checked by test/contract.test.mjs.
Admin prompts and legacy HTTP+SSE remain explicitly gated in the contract. Routine
writes are released behind the configured policy, the vault:write scope, and
authenticated HTTP idempotency.
Release and installation contract
GitHub Actions is the release builder. A pushed mcp-vVERSION tag must match the
private package version exactly. The workflow checks out that tag, installs the
locked dependencies, runs the package test suite, runs npm pack, and retains
the resulting tarball and release metadata as a CI artifact. Tag builds also
publish both files as GitHub Release assets. The package remains private and is
never published to the public npm registry.
The metadata is generated from the packed tarball. It records the source commit,
package name and version, SHA-256 of the packaged contract.json, SHA-256 of the
tarball, and the Node.js and npm versions used to build it. It intentionally has
no timestamp or runner-specific fields.
To reproduce and inspect a bundle locally, check out an exact mcp-vVERSION tag
or full commit SHA and run:
npm run release:bundle -- --revision <full-checked-out-commit-sha>
ls -l release/Use the full checked-out commit SHA instead of the tag when preparing a bundle
before tagging. The command refuses branch names and refuses a revision that does
not resolve to the checked-out commit. It also requires a clean worktree so the
recorded commit identifies every packed file. It runs npm ci, a runtime dependency
audit that blocks high/critical advisories, the package tests,
and the same pack and metadata path used by CI.
For an existing versioned tag, use that exact mcp-vVERSION instead. Do not
reuse an old version for a different release. No standalone release exists merely
because this source tree was extracted.
An npm tarball contains a package/ directory, not an immediately runnable
release root. Extract it into a new versioned directory, enter its package/
subdirectory, then install the frozen runtime dependencies:
cp dist/runtime-lock.json package-lock.json
npm ci --omit=dev --ignore-scripts
node dist/http.mjsSet the required server environment before starting. Build dependencies are not
needed for the prebuilt tarball. Point service working directories and the
current symlink to that installed package/ directory. The build copies the
canonical source package-lock.json to dist/runtime-lock.json because npm
omits root package-lock files from tarballs. Restoring that copy supports both
npm 11 and npm 12 without depending on shrinkwrap support. Source builds and
production installs use the same dependency versions and integrity hashes.
Dependency downloads still require npm registry access. Do not cut over a live service before
verifying the artifact checksum, contract, configuration and handshake.
Self-Host Deployment and Service Management
Deployments can manage the Streamable HTTP server via systemd on Linux or launchd on macOS. Services run against the locally installed package artifact with explicit environment configuration and restricted file permissions.
systemd service example (/etc/systemd/system/obsidian-mcp.service)
[Unit]
Description=Obsidian MCP Streamable HTTP Service
After=network.target
[Service]
Type=simple
User=obsidian
Group=obsidian
WorkingDirectory=/opt/obsidian-mcp/current
EnvironmentFile=/etc/obsidian-mcp/server.env
ExecStart=/usr/bin/node dist/http.mjs
Restart=always
RestartSec=5
LimitNOFILE=65536
# Security sandbox
ProtectSystem=strict
ProtectHome=read-only
ReadWritePaths=/var/log/obsidian-mcp /srv/obsidian-vault
PrivateTmp=true
NoNewPrivileges=true
[Install]
WantedBy=multi-user.targetEnvironment configuration (/etc/obsidian-mcp/server.env)
Generate a cryptographically secure 256-bit secret (64 hex characters / 32 bytes):
openssl rand -hex 32Configure /etc/obsidian-mcp/server.env with your deployment values:
OBSIDIAN_LOCAL_MD=/etc/obsidian-mcp/obsidian.local.md
MCP_HTTP_BIND=127.0.0.1
MCP_HTTP_PORT=3000
# Generate a real secret with: openssl rand -hex 32
# Placeholder and example values will fail closed on startup.
MCP_HTTP_JWT_SECRET=<generate-secret-using-openssl-rand-hex-32>
MCP_HTTP_JWT_ISSUER=https://issuer.example
MCP_HTTP_JWT_AUDIENCE=claude-obsidian
MCP_HTTP_ALLOWED_HOSTS=127.0.0.1,localhost,mcp.example.com
MCP_HTTP_ALLOWED_ORIGINS=https://client.example
MCP_STDIO_PROFILE=writeFile permissions must protect credentials (chmod 0600 /etc/obsidian-mcp/server.env).
Reverse proxy TLS termination
Expose the HTTP service exclusively through a reverse proxy terminating TLS. For example, using Nginx:
location /mcp {
proxy_pass http://127.0.0.1:3000/mcp;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto https;
proxy_read_timeout 300s;
proxy_buffering off;
}Host header validation: MCP_HTTP_ALLOWED_HOSTS validates the HTTP Host header received by the server. When Nginx forwards the incoming host via proxy_set_header Host $host;, ensure your public domain (e.g. mcp.example.com) is present in MCP_HTTP_ALLOWED_HOSTS. Alternatively, rewrite the upstream Host header to loopback (proxy_set_header Host 127.0.0.1;) if public hostname forwarding is not needed.
Upgrades, Compatibility, and Safe Rollback
Verification before cutover:
Download the target release tarball and
<version>.release.jsonmetadata asset.Verify the tarball checksum against the release metadata
artifact.sha256.Extract into a versioned release directory:
/opt/obsidian-mcp/releases/<version>.Verify the extracted
contract.jsonmatchescontract_sha256.Verify an authenticated MCP initialize handshake and its reported version with a configured MCP client. For a user systemd unit with journal access,
node monitor.mjsalso checks these and reports journal telemetry.
Atomic switchover:
Update the active symlink:
ln -sfn /opt/obsidian-mcp/releases/<version>/package /opt/obsidian-mcp/current.Restart the service:
systemctl restart obsidian-mcp.Repeat the authenticated initialize handshake and version check. Use
node monitor.mjsonly for a user systemd unit with accessible journal telemetry.
Operational rollback:
Retain the prior versioned release directory (e.g.
/opt/obsidian-mcp/releases/<prior-version>).If regressions or health check failures occur, point the
currentsymlink back to the prior directory and restart:ln -sfn /opt/obsidian-mcp/releases/<prior-version>/package /opt/obsidian-mcp/current systemctl restart obsidian-mcpRollback is atomic and does not modify vault data, commit capture contracts, or existing historical notes.
This server cannot be deployed
Maintenance
Related MCP Connectors
- GraniteOAuthmd.granite
Encrypted Markdown notes and team knowledge base. Search, read and write notes in vaults you allow.
Search, read, and safely update Markdown notes in your connected Phasoric knowledge vaults.
Notes and memory assistant: one Markdown vault that Claude, ChatGPT and any MCP client can share.
Agent-native notes, tasks, dev-docs, vaults, sync & handoffs. MCP + OpenAPI dual surface.
Related MCP Servers
- AlicenseAqualityDmaintenanceEnables AI assistants to interact with Obsidian vaults for creating, reading, searching, and managing notes, daily notes, TODOs, session reports, and backlinks through both stdio and HTTP/SSE transports.103,849 npm4MIT
- AlicenseNot gradedqualityCmaintenanceEnables reading, searching, and writing an Obsidian vault directly on disk without Obsidian running, and walking its wikilink graph for backlinks, N-hop neighbors, unresolved links, and orphans. Available over Streamable HTTP with bearer auth, stdio, or a Claude Desktop extension.26 npmMIT
- AlicenseNot gradedqualityCmaintenanceEnables multiple AI agents to share a single Obsidian vault through streamable HTTP, with per-agent bearer tokens whose folder-level read/write permissions are enforced server-side so denied folders stay invisible. Offers tools for listing, reading, searching, stating, writing, editing, appending, moving and optionally trashing notes, backed by revision checks, atomic writes, per-file locking and an audit log.MIT
- AlicenseNot gradedqualityCmaintenanceEnables AI harnesses such as ZCode and Claude Code to browse, search, edit, and create notes in an Obsidian vault through seven fine-grained tools over stdio. It works directly on the vault's filesystem without requiring Obsidian to be running, with built-in protections against accidental deletion or overwriting.3,849 npmMIT