Obsidian-MCP-Server
Allows reading and writing Obsidian notes stored in a CouchDB-backed LiveSync vault, without materializing files on disk.
Click on "Install 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-MCP-Servershow me the note about MCP server design"
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-Server
An MCP server that makes an Obsidian vault synced through Self-hosted LiveSync readable and writable by Claude, by speaking directly to the CouchDB that already backs that sync.
The vault is never materialised as files. There is no second copy of the notes on disk for another program to serve - this understands LiveSync's storage format natively and exposes that understanding as MCP tools.
See docs/design.md for the full design.
Status
In progress. A read-only vertical slice runs end to end: it replicates a real vault, indexes it and serves it over MCP, with search. Every write path to the vault is still to come.
Unit | State |
Vault model | Verified against a live vault |
Replicator | Implemented - pull-only, decoding at the boundary |
Note parsing | Frontmatter, tags, wikilinks, headings |
Index | SQLite with FTS5: search, properties, tags, link graph |
Attachments | PDF text extraction, indexed and retrievable |
Handwriting | Transcriptions stored durably, and indexed |
Tool layer | Thirteen tools, none of which writes to the vault |
Transport | stdio and streamable HTTP, bearer token |
Deployment | Dockerfile and Compose for the target stack |
Write executor | Not started, deliberately |
OAuth 2.0 + PKCE | Not started - bearer token in the interim |
342 tests. scripts/verify-vault.ts can be pointed at a live vault read-only;
see below.
Related MCP server: obsidian-self-mcp
Running it
npm install && npm run build
COUCHDB_URL='https://user:password@couchdb.example.net/?db=obsidiandb' \
MCP_TRANSPORT=stdio \
REPLICA_PATH=./tmp/replica \
node dist/index.jsIt reads the vault's storage settings from the vault itself, replicates the
whole database locally, waits for that first pass, then serves. deploy/
holds a Dockerfile and a Compose file for a Traefik-fronted stack.
On stdio the server then waits for a client to speak MCP over stdin, which from a terminal is indistinguishable from a hang. To see it work:
COUCHDB_URL='https://user:password@couchdb.example.net/?db=obsidiandb' npm run tryThat starts the server, connects to it as a real MCP client, and calls each tool in turn.
Configuration is environment variables throughout. Every sensitive value also
accepts a NAME_FILE form naming a file to read it from, which is how Docker
secrets are consumed. deploy/obsidian-mcp.env.example lists all of them.
Why writes are absent rather than disabled
READ_ONLY defaults to true, but more to the point the write tools are not
registered at all. A tool that exists and reports "not implemented" is worse
than no tool: a model will call it, and the user will conclude that writing is
one setting away.
save_transcription is the one tool that stores something, and it stays
available under READ_ONLY because there is no code path from it to CouchDB.
The read-only setting is about the vault; a transcription is written to a local
database this server owns, and the original file is never touched. An
integration test asserts that by reading the attachment's document revision back
out of CouchDB after a transcription is saved.
Handwritten notes
The vault this was built for is largely handwritten: pages of ink exported as PDFs by an Obsidian plugin. Those files have no text layer at all, so extraction finds nothing in them and OCR does badly on cursive. The transcriber that works is a model reading the page - which this server cannot do, because it has no model. What it can do is hold the page up and keep what comes back.
list_untranscribed what has no searchable text, and what has gone out of date
get_attachment hands over the PDF itself when there is no text to extract
save_transcription stores the reading; it is searchable immediatelyTranscriptions live in their own SQLite file, not in the index, and that separation is the important design decision. The index is a cache: it is dropped and rebuilt whenever its schema moves, and everything in it can be recomputed from the replica. A transcription cannot be recomputed by anything. It is the only data in the system whose loss is permanent, and the rest of its handling follows from that:
Its own file, and never dropped by a rebuild. An integration test starts a server, saves a transcription, throws the index and the replica away, starts again, and asserts the handwriting is still searchable. Deleting the fifteen lines that read the store back makes that test fail, which is the point: the suite previously passed with those lines removed entirely.
journal_mode = DELETE, against the usual advice. WAL keeps recent commits in a sidecar file until a checkpoint, so a file-level nightly backup can copy a database missing its newest rows, or a torn one. Writes here are a few a day, so there is no throughput to trade away for a single self-contained file.synchronous = FULLfor the same reason.Superseded text is kept. The likeliest way to lose a transcription is not a crash, it is a second, worse reading overwriting a first: a model that runs out of room after page one of a forty-page notebook will call
save_transcriptionwith a fragment quite happily. What it replaced goes to a history table in the same transaction.A schema it does not recognise refuses to open. The index answers that question by dropping everything and rebuilding, which is the one response unavailable here. A server that will not start is recoverable.
Each transcription records the size and modification time of the file it was made from. Add another line of ink to a page and the transcription is reported as out of date rather than left in the index quietly describing an older version of it.
Renaming a file in Obsidian is a delete and a create, so a transcription keyed
on the old path comes unattached. Nothing deletes it, but list_untranscribed
and the startup log both report it, so the page does not silently reappear in
the queue with the reading already paid for sitting unreachable beside it.
Why the vault model came first
It is the unit whose failure is unrecoverable. Everything else can be wrong in ways you notice: a replication bug stalls, an index bug returns nothing, a tool bug throws. A vault model bug writes a note that comes back subtly altered, and you find out weeks later.
So it is built as pure functions - no network, no database handle, documents in and a note out - which is what allows it to be tested three ways:
Round-trip property tests. Splitting a note into chunks and reassembling it is the identity function, across empty files, very large files, unicode, astral-plane characters, mixed line endings, byte order marks, and content sized at every boundary the chunker derives.
Differential tests against the plugin itself.
@vrtmrz/livesync-commonlibis a dev dependency, so the suite runs our chunker, our path mapping, our compression and our document transform against the code every other device is actually running, and requires agreement. This is the strongest evidence available short of a real vault.Failure-mode tests. Most of the assembly suite asserts that the error happens - a missing chunk, an undecoded document, a payload that is not valid base64 - rather than that the success does.
What the model owns
src/vault-model/
constants.ts Storage-format literals, each with its upstream source
types.ts Document shapes as they exist in CouchDB
settings.ts Format settings, and reading them from the vault's own milestone
ids.ts Path ↔ document ID, normalisation, obfuscation
hash.ts Chunk identity, all five hash algorithms
chunking/ The V3 Rabin-Karp content-defined chunker
crypto.ts The E2EE boundary (delegates to octagonal-wheels)
compression.ts The deflate layer
transform.ts Wire form ⇄ plain form
assemble.ts Documents in, file out
compose.ts File in, documents outdocs/livesync-storage-contract.md records
the storage format this was derived from, including the parts that look like
bugs and have to be reproduced anyway.
Findings so far
Three things the design flagged as assumptions, now settled:
Chunk IDs are pure content hashes. No per-document, per-revision or per-position salt. The write path can reuse chunks across notes and treat a 409 on a chunk PUT as success, exactly as the design hoped.
Path obfuscation is one-way, but the plaintext path is in the document. Recovering a path from an ID is impossible; recovering it from the decrypted
pathfield is routine. Writing to an existing obfuscated path means recomputing the identical hash, so path normalisation has to match Obsidian's exactly - including NFC composition and non-breaking-space folding.The E2EE v2 metadata seal is not idempotent. Encrypting an already-encrypted document destroys its chunk list irrecoverably while leaving a document that still reads cleanly as an empty note. Guarded, and tested against the plugin's own transform.
Two things deliberately not implemented, and why:
The V1 and V2 chunk splitters. Chunk boundaries do not affect correctness - the plugin reassembles whatever it is given - only deduplication. Implementing two more splitters to save bandwidth on a vault that almost certainly uses the current default is not worth the surface area. Writing to such a vault throws unless
allowSplitterFallbackis set.edeninline chunks. An obsolete optimisation. Documents carrying them are rejected with a message saying so, rather than reporting their chunks as missing and leaving the caller unable to satisfy a request that never can be.
Verifying against a real vault
scripts/verify-vault.ts points at a live LiveSync database and checks that
this code understands it. It is read-only by construction - the only
request method in the file is GET, and a test asserts that a full run issues
nothing else - so it is safe against a production vault.
npm run verify -- --url 'https://user:password@couchdb.example.net/?db=obsidiandb'It reports the vault's own format settings (read from the milestone document, not assumed), flags any setting two devices disagree on, assembles a sample of notes, and then does the check that matters: re-chunks each note and compares the chunk IDs it would write against the ones the plugin actually wrote. If those match, the write path deduplicates exactly as another device does.
--attachments reports, per attachment, whether its text can be extracted at
all. That is the question that decides whether attachment search is worth
anything for a given vault: a PDF of handwriting is searchable only if
something already put a text layer into it. OneNote's handwriting recognition
does; a scanner without OCR does not. Run it before assuming either way.
--census walks the entire ID space instead and reports where every document
went: counts by type, live versus deleted files, notes versus hidden-file sync,
and how much of the chunk store is orphaned. Most documents in a LiveSync
database are chunks, not notes, so doc_count is a poor proxy for vault size - the census is what tells you the difference.
Useful flags: --all to verify every file in the vault rather than a sample,
--sample N, --passphrase for an encrypted vault, --verbose for per-file
output, and --capture out.json to save real documents as fixtures - that file
contains note content, so do not commit it.
The sample is spread evenly across the whole ID range rather than taking the first N, because IDs sort by path and attachments cluster at one end. It also reports the text/binary split and warns when a run contained no binary files at all, so a green result cannot quietly mean "attachments untested".
Development
Requires Node 22 or later.
npm install
npm test # everything
npm run test:diff # just the differential tests against the plugin
npm run typecheckThe differential tests import @vrtmrz/livesync-commonlib by explicit file
path, because its package exports map does not expose the modules involved. That
import lives in test/helpers/upstream.ts and must never appear in src/ - the
point of the vault model is that it owns this logic rather than borrowing it.
Licence
MIT.
This server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Related MCP Servers
- Alicense-qualityFmaintenanceA local MCP server that enables AI applications like Claude Desktop to securely access and work with Obsidian vaults, providing capabilities for reading notes, executing templates, and performing semantic searches.Last updated832MIT
- Alicense-qualityCmaintenanceAn MCP server providing direct programmatic access to Obsidian vaults through CouchDB, the database used by Obsidian LiveSync. It enables AI agents to read, write, and manage notes and metadata in headless environments without requiring the Obsidian desktop app.Last updated3MIT
- Alicense-qualityDmaintenanceAn MCP server that gives Claude AI direct access to your Obsidian vault, enabling natural language search, note creation, file management, and automated workflows.Last updated2,0019MIT
- Flicense-qualityCmaintenanceAn MCP server that provides full read/write access to an Obsidian vault, enabling searching, task management, wiki-link graph analysis, and attachment organization from an MCP client like Claude Code.Last updated
Related MCP Connectors
Search your AI chat history (ChatGPT, Claude, Codex) from any MCP client. Remote, private, read-only
Cloud-hosted MCP server for durable AI memory
Augments MCP Server - A comprehensive framework documentation provider for Claude Code
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/pitslug/Obsidian-MCP-Server'
If you have feedback or need assistance with the MCP directory API, please join our Discord server