obsidian-vault-mcp
by QEbellavita
README.md
<p align="center">
<img src="./assets/header.svg" alt="obsidian-icloud-mcp — an Obsidian MCP that survives iCloud eviction" width="100%">
</p>
<p align="center">
<a href="https://github.com/QEbellavita/obsidian-icloud-mcp/actions/workflows/test.yml"><img src="https://github.com/QEbellavita/obsidian-icloud-mcp/actions/workflows/test.yml/badge.svg" alt="tests"></a>
<a href="https://doi.org/10.5281/zenodo.21485168"><img src="https://zenodo.org/badge/DOI/10.5281/zenodo.21485168.svg" alt="DOI"></a>
</p>
An MCP server for Obsidian vaults that doesn't hang on iCloud.
Read, search, create and update notes from your assistant — and see exactly how much
of the vault iCloud has actually left on disk. Seven tools, no daemon, no index to rebuild.
## Why another one
Because the obvious implementation breaks on iCloud Drive, and breaks in the worst
possible way: it hangs forever.
If your vault lives in iCloud with **Optimize Mac Storage** on, macOS evicts note bodies
and leaves *dataless placeholders* behind. They look like ordinary files — `stat` reports
a real size — but the first `read()` blocks while macOS fetches the content over the
network. On an evicted vault, a naive search hits hundreds of those in a row and never
returns. No error, no timeout, just a search that never finishes.
A deadline alone doesn't fix it: a single blocking read never yields control back, so the
between-files clock check is never reached. The only reliable signal is `stat`'s **block
count** — a dataless placeholder reports `blocks == 0` despite a non-zero size.
This server checks that before every read, and bounds total search time on top. Evicted
notes are skipped and reported, not waited on:
```json
{
"matches": [ ... ],
"scanned": 112,
"total": 486,
"unavailable": 43,
"incomplete": true,
"note": "Searched 112/486 notes; 43 are not downloaded from iCloud yet and were skipped. Results may be incomplete."
}
```
`incomplete: true` means "there may be more" — rather than silently returning partial
results as if they were the whole answer. The counts distinguish the two reasons: notes
that weren't materialised (`unavailable`) versus hitting the time budget.
Reading one specific evicted note fails honestly with *"not downloaded from iCloud"*
instead of blocking.
<p align="center"><img src="./assets/demo.gif" alt="Animated demo — a search reports 5 of 8 notes scanned with 3 iCloud-evicted notes skipped and flagged, then vault_health shows materialized vs evicted counts and points at obsidian_warm_notes" width="90%"></p>
## Install
Point your MCP client at it:
```json
{
"mcpServers": {
"obsidian": {
"command": "npx",
"args": ["-y", "obsidian-icloud-mcp"],
"env": {
"OBSIDIAN_VAULTS": "{\"Personal\":\"/Users/you/Documents/Notes\"}"
}
}
}
}
```
Or from source: `git clone https://github.com/QEbellavita/obsidian-icloud-mcp` and point
`command: node, args: [/absolute/path/to/obsidian-icloud-mcp/server.js]` at the checkout.
Formerly named `obsidian-vault-mcp`; renamed because that npm name belongs to an unrelated
project by another author — `npx obsidian-vault-mcp` is not this server. The old GitHub URL
redirects here.
`OBSIDIAN_VAULTS` is a JSON object of name → absolute path. Multiple vaults are fine:
```json
"OBSIDIAN_VAULTS": "{\"Personal\":\"/Users/you/Notes\",\"Work\":\"/Users/you/Work/vault\"}"
```
With no vaults configured the server starts and advertises zero tools, rather than
crashing in a way your host reports as a broken server.
## Tools
| Tool | |
|---|---|
| `obsidian_list_notes` | List Markdown notes, optionally under a subdirectory |
| `obsidian_search_notes` | Full-text search with path, line number and excerpt |
| `obsidian_read_note` | Read one note |
| `obsidian_create_note` | Create a note; refuses to overwrite |
| `obsidian_update_note` | Atomic overwrite of an existing note |
| `obsidian_vault_health` | Materialized vs evicted counts, bytes pending, largest evicted notes — stat-only, never blocks |
| `obsidian_warm_notes` | Opt-in background downloads for evicted notes, bounded per call |
## Safety
The vault boundary is enforced, not assumed:
- Paths must be relative and stay inside the configured vault — `../` is rejected
- **Symlinks that resolve outside the vault are rejected**, not followed
- Only `.md` files are read or written
- `create` refuses to clobber; `update` refuses to create
- Writes are atomic (temp file + rename), so an interrupted write can't truncate a note
Directory walking skips anything that can't hold notes — `.git`, `node_modules`,
`.obsidian` — which also stops a vault that happens to be a source checkout from taking
minutes to list. And it never descends into a stalling bundle directory, where even
`readdir` can block indefinitely on iCloud.
## Tests
```bash
npm test
```
23 tests. Nineteen cover the service directly — including every iCloud failure mode above,
driven through an injected `fs` so eviction is simulated deterministically rather than
depending on your actual sync state. Four drive the real server over a real stdio
transport.
## Citing
If this server is useful in your workflow, cite it via the repo's
[CITATION.cff](CITATION.cff) (GitHub's "Cite this repository" button), or the
Zenodo DOI, [10.5281/zenodo.21485168](https://doi.org/10.5281/zenodo.21485168) — a
concept DOI that always resolves to the latest archived version.
## Licence
MIT — see [LICENSE](LICENSE).
Maintenance
ActivityStale
ResponsivenessNo issues