Give any AI agent access to your Obsidian vault over MCP. Run it locally against your vault files, or pair it with Self-hosted LiveSync and deploy to the cloud so it works even when your machine is off.
Example: From your phone, ask your AI: "What's in my daily note for today?" — and get the full content back, with a link to open it in Obsidian.
The server connects to your vault in two ways:
- Filesystem mode — reads
.mdfiles directly from your vault folder. No database needed. - CouchDB mode — reads from a CouchDB database, locally or in the cloud. Your vault syncs to CouchDB via Self-hosted LiveSync, the community Obsidian plugin (600k+ downloads). The MCP server reads from CouchDB directly using livesync-commonlib — the same library that powers the plugin — for proper chunk handling and E2E encryption support.
Both modes expose the same MCP tools over HTTP, so any MCP-compatible agent can connect: Claude, Copilot, custom agents, anything that speaks the Model Context Protocol.
| Need it always available? | Have LiveSync? | Go to |
|---|---|---|
| Yes | Yes | Setup A — add MCP alongside your existing CouchDB |
| Yes | No | Setup B — CouchDB + MCP + LiveSync from scratch |
| No | — | Setup C — filesystem or CouchDB, npx or Docker |
You already have LiveSync and CouchDB on an always-on server. You just need the MCP server deployed alongside it.
Using Fly.io setup script (macOS/Linux, or WSL on Windows):
git clone https://github.com/es617/obsidian-sync-mcp.git
cd obsidian-sync-mcp
./deploy/setup.sh # choose option 2 (MCP only)The script asks for your CouchDB connection details, vault name, and encryption passphrase.
Or run the Docker image on any always-on server:
docker run -p 8787:8787 \
-v mcp-data:/data -e DATA_DIR=/data \
-e COUCHDB_URL=https://your-couchdb:5984 \
-e COUCHDB_USER=admin -e COUCHDB_PASSWORD=yourpassword \
-e COUCHDB_DATABASE=obsidian -e VAULT_NAME=MyVault \
-e COUCHDB_PASSPHRASE=your-encryption-passphrase \
-e COUCHDB_OBFUSCATE_PROPERTIES=false \
-e MCP_AUTH_TOKEN=yourpassword \
-e BASE_URL=https://your-server-url \
ghcr.io/es617/obsidian-sync-mcp:latestSet COUCHDB_PASSPHRASE if you use E2E encryption in LiveSync. Set COUCHDB_OBFUSCATE_PROPERTIES=true if "Obfuscate Properties" is also enabled in your LiveSync settings. For an existing vault the server detects the actual setting from the database at startup and corrects a mismatch with a warning; only for a brand-new empty database does the value need to match your LiveSync settings. If the vault was created with LiveSync 1.0.33+ and path obfuscation, also set COUCHDB_ID_DERIVATION_KEY to your LiveSync recovery code (sls-id-v1:..., from "Show current recovery code") so the server can resolve note paths. Unicode NFC path identity (the default for databases created with LiveSync 1.0.49+) is detected from the database automatically; a brand-new empty database gets NFC unless you set COUCHDB_PATH_NORMALIZATION=legacy. Set BASE_URL to your public URL (required for OAuth callbacks when agents connect over HTTPS).
Your MCP endpoint is https://your-app.fly.dev/mcp (Fly.io) or https://your-server:8787/mcp (Docker behind HTTPS).
See Cost for Fly.io pricing.
Requires flyctl for the Fly.io path:
curl -L https://fly.io/install.sh | sh
export PATH="$HOME/.fly/bin:$PATH" # add to ~/.zshrc or ~/.bashrc
fly auth loginStarting fresh — no LiveSync yet. Deploy CouchDB and MCP together, then set up LiveSync in Obsidian.
Using Fly.io setup script (macOS/Linux, or WSL on Windows):
git clone https://github.com/es617/obsidian-sync-mcp.git
cd obsidian-sync-mcp
./deploy/setup.sh # choose option 1 (CouchDB + MCP)The script generates credentials, creates the database, and deploys. Save the credentials it prints.
Or with Docker Compose on any always-on server:
git clone https://github.com/es617/obsidian-sync-mcp.git
cd obsidian-sync-mcp
cat > .env <<EOF
COUCHDB_PASSWORD=changeme
MCP_AUTH_TOKEN=changeme-too
VAULT_NAME=MyVault
EOF
docker compose up -dAfter deployment:
- In Obsidian, install Self-hosted LiveSync and configure it with the credentials from the setup output
- Your MCP endpoint is
https://your-app.fly.dev/mcp(Fly.io). With Docker Compose the MCP port is published onlocalhost:8787only; to reach it from other machines, put an HTTPS tunnel or reverse proxy in front (see remote access) and setBASE_URLto its public URL - The
MCP_AUTH_TOKENis the password you enter when an agent connects
Always-on server
├── CouchDB + persistent storage
└── MCP server
↑ ↑
Obsidian + LiveSync AI agents
Requires flyctl for the Fly.io path:
curl -L https://fly.io/install.sh | sh
export PATH="$HOME/.fly/bin:$PATH" # add to ~/.zshrc or ~/.bashrc
fly auth loginApplies to both Setup A and Setup B.
| Component | Cost |
|---|---|
| CouchDB + MCP VM (shared, 512MB) | ~$3-4/month (kept alive by LiveSync) |
| MCP-only VM (shared, 256MB) | ~$0-2/month (suspends when idle) |
| 1GB persistent volume | ~$0.15/month |
As of March 2026, Fly.io may waive charges under $5/month, which could make this effectively free with a shared IPv4. Either way, cheaper than Obsidian Sync ($4/month) and you own the data.
Run the MCP server locally. Works with filesystem mode (reads vault files directly) or CouchDB mode (if you have LiveSync). Machine must stay on for agents to reach it.
Filesystem mode (simplest):
VAULT_PATH=~/Documents/MyVault \
VAULT_NAME=MyVault \
npx obsidian-sync-mcpCouchDB mode (if you have LiveSync):
COUCHDB_URL=http://localhost:5984 \
COUCHDB_USER=admin \
COUCHDB_PASSWORD=yourpassword \
COUCHDB_DATABASE=obsidian \
COUCHDB_PASSPHRASE=your-encryption-passphrase \
COUCHDB_OBFUSCATE_PROPERTIES=false \
VAULT_NAME=MyVault \
npx obsidian-sync-mcpOmit COUCHDB_PASSPHRASE if you don't use E2E encryption in LiveSync. Set COUCHDB_OBFUSCATE_PROPERTIES=true if "Obfuscate Properties" is also enabled in your LiveSync settings. For an existing vault the server detects the actual setting from the database at startup and corrects a mismatch with a warning; only for a brand-new empty database does the value need to match your LiveSync settings. If the vault was created with LiveSync 1.0.33+ and path obfuscation, also set COUCHDB_ID_DERIVATION_KEY to your LiveSync recovery code (sls-id-v1:...) so the server can resolve note paths. Unicode NFC path identity (the default for databases created with LiveSync 1.0.49+) is detected from the database automatically; a brand-new empty database gets NFC unless you set COUCHDB_PATH_NORMALIZATION=legacy.
Or with Docker:
docker run -p 127.0.0.1:8787:8787 \
-v mcp-data:/data -e DATA_DIR=/data \
-e VAULT_PATH=/vault -v ~/Documents/MyVault:/vault \
-e VAULT_NAME=MyVault \
ghcr.io/es617/obsidian-sync-mcp:latestYour MCP endpoint is http://localhost:8787/mcp.
Want remote access? Add a tunnel (machine must stay on):
cloudflared tunnel --url http://localhost:8787 # free
tailscale funnel 8787 # or Tailscale
ngrok http 8787 # or ngrokSet BASE_URL to the tunnel URL when using authentication.
| Tool | Description |
|---|---|
read_note |
Read a note's markdown content by path |
write_note |
Create or overwrite a note (replaces entire content) |
edit_note |
Edit a note without rewriting it — append, prepend (after frontmatter), or replace exact text |
list_folders |
List all folders in the vault with note counts — use to discover folder names |
list_tags |
List all tags in the vault with counts — use to discover tags before filtering |
list_notes |
List notes with timestamps. Filter by folder, name, tag, or date. Sort by name or modified. |
search_notes |
Search note text for terms (OR, case-insensitive substrings); paths and frontmatter titles are searched first. Filter by folder, tag, or date. Returns paths with one context line each, never the note body, after a status line that states which CouchDB sequence the index is caught up with. |
delete_note |
Delete a note |
move_note |
Move or rename a note — works across folders, creates destination folders automatically |
get_note_metadata |
Get frontmatter, tags, outgoing links, backlinks, size, and timestamps — navigate the knowledge graph |
With ATTACHMENTS_ENABLED=true, agents can also store images, PDFs, audio and video in the vault. In CouchDB mode they are written through livesync-commonlib, chunked and encrypted the same way as a file added in Obsidian, so they sync to every device.
| Tool | Description |
|---|---|
create_attachment_upload |
Get a one-time upload URL for a vault path. The agent sends the raw bytes there with HTTP PUT, e.g. curl -fsS -X PUT --data-binary @shot.png '<url>'. Use this for anything but tiny files |
write_attachment |
Fallback for clients that cannot make HTTP requests: save a tiny file from base64 (default limit 32 KB). Every byte is a model output token, so a 32 KB file is already ~13k tokens and minutes of generation |
move_attachment |
Move or rename an attachment (embeds in notes are not rewritten; the response lists notes that still point at the old path) |
delete_attachment |
Delete an attachment |
get_attachment_metadata |
Size, timestamps, and the notes that embed or link it |
list_attachments |
List attachments with size, date and how many notes use each. Filter by folder, name or extension, sort by name, date or size, or pass unreferenced=true to find files no note embeds or links |
The agent then embeds the file in a note with edit_note, e.g. ![[Attachments/shot.png]].
Upload URLs are served by this server at BASE_URL/attachments/upload/<token>, so BASE_URL must be reachable by the agent. The token in the URL is the only credential for that request: 32 random bytes, valid for one upload, one path, and ATTACHMENT_UPLOAD_TTL_SECONDS. Path checks, WRITE_FOLDERS and the size limit are applied when the URL is created, and existing files are never replaced unless the agent passed overwrite=true. That last rule is checked again when the file is written, in the same step as the write, so of two concurrent uploads to one new path only the first succeeds and the other gets 409.
Behind a reverse proxy that allowlists paths: match the exact upload URL, PUT with path ^/attachments/upload/[A-Za-z0-9_-]{43}$, not the /attachments/upload/ prefix. The server resolves .. and %2e%2e in the path after the proxy has matched it, so with a prefix rule /attachments/upload/../../mcp reaches /mcp.
Allowed extensions: avif, bmp, gif, jpeg, jpg, png, webp, flac, m4a, mp3, ogg, wav, 3gp, mkv, mov, mp4, ogv, webm, pdf. Text formats (.md, .svg, .html, .js, .json, .canvas, ...) and dot-folders such as .obsidian/ are always refused. Attachments never appear in list_notes or search_notes. For list_attachments the server keeps their path, size and modification time in its metadata index, learned from the CouchDB change feed (from the metadata alone, so file contents are never downloaded) or, in filesystem mode, from a scan at startup and the file watcher. References are matched by file name, the way Obsidian resolves ![[shot.png]], from [[wikilinks]], ![[embeds]] and markdown links such as . Two files with the same name in different folders therefore count as referenced if either one is, which errs on the safe side when cleaning up.
Every tool response includes an Obsidian deep link (obsidian://open?vault=...&file=...) that works on Mac and iOS.
"Add a bullet point to my daily note." "Find my notes about the MCP server and fix the typo in the second one."
Set MCP_AUTH_TOKEN to a password to enable authentication:
MCP_AUTH_TOKEN=mysecretpassword npx obsidian-sync-mcpThe server includes a self-contained OAuth 2.1 provider. When an agent connects:
- A browser window opens with a password page
- Enter the
MCP_AUTH_TOKENpassword - The agent gets an access token and refreshes it transparently
The session is shared across all your Claude interfaces (Desktop, Web, Mobile) and persists across server restarts. You'll need to re-enter the password after 14 days of inactivity (configurable via MCP_REFRESH_DAYS).
For non-OAuth clients (curl, MCP Inspector, custom agents), you can also pass the token directly as Authorization: Bearer <MCP_AUTH_TOKEN>.
Without MCP_AUTH_TOKEN, the server runs without authentication — suitable for local use or behind a private network.
| Variable | Required | Default | Description |
|---|---|---|---|
VAULT_PATH |
Filesystem mode | — | Path to your Obsidian vault directory |
COUCHDB_URL |
CouchDB mode | — | CouchDB server URL |
COUCHDB_USER |
CouchDB mode | admin |
CouchDB username |
COUCHDB_PASSWORD |
CouchDB mode | — | CouchDB password (required) |
COUCHDB_DATABASE |
CouchDB mode | obsidian |
CouchDB database name. The server never creates it: if it doesn't exist, startup fails with an error |
COUCHDB_PASSPHRASE |
CouchDB mode | — | LiveSync E2E encryption passphrase (must match plugin setting) |
COUCHDB_OBFUSCATE_PROPERTIES |
CouchDB mode | false |
Set to true if "Obfuscate Properties" is enabled in LiveSync (obfuscates file paths, sizes, dates in the database). For existing vaults the actual setting is auto-detected at startup; this value only decides the format for a brand-new empty database |
COUCHDB_ID_DERIVATION_KEY |
CouchDB mode | — | LiveSync "independent ID derivation" recovery code (sls-id-v1:...). Needed to read and write notes on vaults created with LiveSync 1.0.33+ that use path obfuscation; leave unset for older (passphrase-derived) vaults. Get it from LiveSync's "Show current recovery code" |
COUCHDB_CASE_SENSITIVE |
CouchDB mode | false |
Set to true only if your LiveSync vault has "Handle filenames as case-sensitive" enabled. It must match the vault, or obfuscated document IDs won't resolve (the server fails to start with a clear message). Leave unset otherwise |
COUCHDB_PATH_NORMALIZATION |
CouchDB mode | auto |
LiveSync path identity: how file names map to document IDs. auto follows the database, and uses nfc (Unicode NFC, so Café.md typed on macOS or Linux is the same note) for a brand-new empty database, matching LiveSync 1.0.49+. The server declares the choice in an empty database at startup, so set legacy before first starting it only if your LiveSync devices use legacy identity (vaults set up before 1.0.49). An explicit value that contradicts an existing database fails startup, since changing it needs a rebuild from LiveSync |
VAULT_NAME |
Both | MyVault |
Vault name (used for deep links and index storage) |
MCP_AUTH_TOKEN |
Optional | — | Password for authentication |
BASE_URL |
Optional | http://localhost:PORT |
Public URL (for OAuth callbacks when using a tunnel) |
PORT |
Optional | 8787 |
HTTP port |
HOST |
Optional | 0.0.0.0 |
Bind address (127.0.0.1 to restrict to localhost) |
MCP_ALLOWED_HOSTS |
Optional | — | Comma-separated extra Host values accepted in no-auth mode (e.g. 192.168.1.5,mybox.local). No-auth mode rejects any other Host to block browser DNS-rebinding; localhost is always allowed. Ignored when MCP_AUTH_TOKEN is set. |
DATA_DIR |
Optional | ~/.obsidian-mcp |
Directory for persisted data (metadata index, auth tokens) |
LOG_LEVEL |
Optional | — | Set to debug for verbose logging (library logs, change feed, index sync) |
DISPLAY_TIMEZONE |
Optional | server's zone | IANA time zone for the human-readable half of the server time in search_notes status lines (UTC is always printed beside it), e.g. Europe/Berlin. An invalid zone falls back to UTC with a warning at startup |
MCP_REFRESH_DAYS |
Optional | 14 |
Days before auth session expires |
READ_ONLY |
Optional | false |
Set to true to disable all write tools (write_note, edit_note, delete_note, move_note). Only read tools are exposed via MCP, and the vault backend rejects writes as well. Useful when sharing the server with multiple AI clients and write access should be opt-in. This protects the vault from MCP clients; it is not a database-level guarantee. To make CouchDB itself refuse writes, use CouchDB-side controls, such as a validate_doc_update function that rejects the server's CouchDB user. |
WRITE_FOLDERS |
Optional | — | Comma-separated list of vault-relative folders where writes are allowed (e.g. MCP,Inbox). When set, the whole vault stays readable but write_note, edit_note, delete_note, and move_note refuse paths outside these folders (move_note requires both source and destination to be writable). Enforced server-side, unlike MCP_INSTRUCTIONS. Matching is case-sensitive and folder-boundary-aware (MCP matches MCP/note.md but not MCP-private/note.md). Ignored when READ_ONLY=true; unset means the whole vault is writable. |
ATTACHMENTS_ENABLED |
Optional | false |
Set to true to enable the attachment tools and the upload endpoint. Under READ_ONLY=true only get_attachment_metadata and list_attachments are exposed and the endpoint is not mounted. With CouchDB, startup then always reads the change feed from the beginning to learn existing attachments (it already does so whenever notes are indexed, to rebuild the search text) |
ATTACHMENT_MAX_BYTES |
Optional | 20971520 (20 MB) |
Largest file accepted by an upload URL. The whole file is held in memory while it is written |
ATTACHMENT_INLINE_MAX_BYTES |
Optional | 32768 (32 KB) |
Largest decoded file accepted by write_attachment. Base64 costs about one output token per 2.5 bytes, so raising this mainly makes slow, error-prone calls possible |
ATTACHMENT_UPLOAD_TTL_SECONDS |
Optional | 600 |
How long an upload URL stays valid |
ATTACHMENT_EXTENSIONS |
Optional | all supported | Comma-separated subset of the supported extensions to allow, e.g. png,jpg,pdf. Can only narrow the list; an unsupported extension is a startup error |
MCP_INSTRUCTIONS |
Optional | — | Extra text appended to the server's MCP instructions (the string clients inject into the system prompt). Use this to bake vault-specific conventions into the server — e.g. folder structure, naming rules, folders to avoid — so they apply across every MCP client without per-client config. Best-effort: not all clients respect instructions. |
MCP_INSTRUCTIONS_FILE |
Optional | — | Path to a file (e.g. markdown) whose contents are appended to the MCP instructions. Easier than MCP_INSTRUCTIONS for multi-line conventions. If both are set, the file wins and MCP_INSTRUCTIONS is ignored (with a startup warning). Missing/unreadable file or files larger than 32 KB are fatal startup errors. Store this file somewhere only the service user can write (e.g. chmod 600) — its contents land in every MCP session's system prompt, so write access to it = prompt-injection access to every client. |
Set VAULT_PATH for filesystem mode or COUCHDB_URL for CouchDB mode.
Test the server interactively using the MCP Inspector:
VAULT_PATH=~/Documents/MyVault npx obsidian-sync-mcp &
npx @modelcontextprotocol/inspectorSet transport to Streamable HTTP, enter http://localhost:8787/mcp, and connect.
| How you run it | How to update |
|---|---|
npx obsidian-sync-mcp |
Automatic — npx pulls latest |
| Fly.io | From the same directory where you ran setup: fly deploy. If you lost the fly.toml, run fly config save --app your-app-name to restore it. |
| Docker | docker pull ghcr.io/es617/obsidian-sync-mcp:latest and restart. The image runs as the unprivileged node user (uid 1000). If your data volume was created by an older image that ran as root, make it writable once: docker run --rm -v mcp-data:/data alpine chown -R 1000:1000 /data (for a bind mount, chown -R 1000:1000 the host directory) |
- Single vault per instance. Each server connects to one vault. For multiple vaults, run multiple instances on different ports.
- Single machine on Fly.io. Auth state is in-memory, so multiple machines break the OAuth flow. The setup script enforces this automatically.
- No conflict resolution. If an agent and Obsidian edit the same note simultaneously, last write wins.
- Attachments are write-and-describe only. With
ATTACHMENTS_ENABLED=trueagents can store, move and delete media files, but the server does not return attachment contents to the model. - Deep links depend on the client. Obsidian
obsidian://deep links are included in every tool response. They work on Claude Mobile and in browsers, but some clients (Claude Desktop) may not render them as clickable links. - Node 22+ required.
- Setup script requires bash. The
deploy/setup.shscript works on macOS and Linux. On Windows, use WSL or Git Bash.
This server gives an AI agent read/write access to your Obsidian vault.
Agents can modify and delete notes. Keep backups. Use tool approval deliberately.
Authentication is optional. Always set MCP_AUTH_TOKEN when exposing to the internet.
Use HTTPS in production. Use a tunnel or deploy behind a reverse proxy.
Don't leave LOG_LEVEL=debug on in production. Debug logging records every tool call's arguments, including note contents, and full error stacks. Credentials embedded in COUCHDB_URL are redacted from logs at every level.
This software is provided as-is under the MIT license. You are responsible for what agents do with your vault.
git clone --recursive https://github.com/es617/obsidian-sync-mcp.git
cd obsidian-sync-mcp
npm install && npm run build
npm test # unit tests
npm run test:e2e # integration tests
npm run test:couchdb # CouchDB harness (needs a throwaway CouchDB on localhost:5985)
npm run test:smoke # full smoke test: built server over MCP against CouchDB, all vault configurationsThe CouchDB tests drop and recreate their own databases, so never point them at a real vault. Start a throwaway CouchDB with docker run -d -p 5985:5984 -e COUCHDB_USER=admin -e COUCHDB_PASSWORD=test couchdb:3. Set TEST_PATH_NORMALIZATION=legacy to run the harness on legacy path identity.
- Commit the version bump to main:
package.json,package-lock.json,server.json, and a## <version>section inCHANGELOG.md. - Either push a
v<version>tag, or run the Release workflow (Actions → Release → Run workflow, which also works from a phone browser). The workflow checks the versions match, waits for CI on main's latest commit to pass, creates the tag and starts CI/CD on it, which publishes the GitHub release, the npm package and the Docker image.
MIT — see LICENSE.
- Self-hosted LiveSync by vrtmrz — the Obsidian plugin and CouchDB sync protocol
- livesync-commonlib by vrtmrz — the shared library for reading/writing the LiveSync document format
- FastMCP — TypeScript MCP framework
- CouchDB — document database
- Fly.io — deployment platform