vault-mcp
README.md
# vault-mcp
[](https://github.com/julianosirtori/vault-mcp/actions/workflows/ci.yml)
[](#)
[](LICENSE)
> [!WARNING]
> **Work in progress.** The security model and core workflows are implemented
> and tested, but the project has not reached a stable release. Expect breaking
> configuration changes, review the threat model before deployment, and do not
> treat the current `main` branch as production-ready yet.
A **remote MCP server for your Obsidian vault**. Add it as a connector in
Claude or ChatGPT and your notes become available in conversations, with **no
Obsidian desktop app required anywhere**. The server reads the markdown files
directly from disk on a small VPS that stays in sync with your vault.
Your vault stays a folder of markdown files. No database, no proprietary
index, no custom format. If this project disappears tomorrow, your notes are
exactly where they were.
## How it works
Three zones with strictly separated responsibilities:
```
Claude / ChatGPT
│
│ MCP over HTTPS (OAuth 2.1 bearer token)
▼
┌─────────────────────────────────────┐
│ EDGE — Cloudflare Worker │
│ · OAuth 2.1 + PKCE, consent page │
│ · token storage (Workers KV) │
│ · fixed redirect_uri allowlist │
│ · forwards with x-origin-secret │
│ (never stores vault content) │
└─────────────────────────────────────┘
│
│ Cloudflare Tunnel — outbound-only
│ from the VPS; no inbound ports
▼
┌─────────────────────────────────────┐
│ ORIGIN — your VPS │
│ · mcp-server, bound to loopback │
│ answers 404 without the secret │
│ · vault-core: path validation, │
│ atomic writes, search │
│ · vault-guards: read/write │
│ sanitization, warn heuristics │
│ · ~/vault ◀─ sync client ─▶ your │
│ sync service (bidirectional) │
│ · git autocommit every 30 min │
└─────────────────────────────────────┘
```
The client never reaches the VPS directly. The Worker knows nothing about the
vault. The origin is not reachable from the internet — it only receives
traffic through the outbound tunnel, and answers **404** to anything that does
not carry the shared origin secret.
## The sixteen tools
| Tool | Type | What it actually does |
| --- | --- | --- |
| `search_notes` | read | Case-insensitive **literal substring** search across markdown notes (no regex, no semantic ranking). Returns path, line number and snippet. Supports pagination (`offset`), folder scoping (`path_prefix`), tag filtering (`tag`) and visiting newest notes first (`sort_by: mtime`). Low-trust folders (imported clippings) are excluded unless explicitly included. |
| `read_note` | read | Full content of one note by vault-relative path, with a header reporting size and the note's **version hash** (for `expected_hash` on later edits). Output is sanitized (see below); very large notes are truncated and flagged. |
| `read_notes` | read | Up to 10 notes in one call; per-note errors are reported inline. |
| `list_recent` | read | Most recently modified notes, newest first (paths and timestamps only). |
| `get_vault_tree` | read | Every folder with its note count — the vault's table of contents. Structure only, never content. |
| `get_daily_note` | read | Resolves the daily note for a date (default today) using the vault's own settings — core Daily Notes (`.obsidian/daily-notes.json`) **or the Periodic Notes plugin**. If the note doesn't exist it returns the path it *would* have — it **never creates it**. |
| `create_daily_note` | write | Creates the daily note at the configured location, seeded from the configured **daily-notes template** (`{{title}}`, `{{date}}`, `{{time}}`, `{{date:FORMAT}}`). Idempotent: an existing note is left untouched. |
| `create_note` | write | Creates a new note. **Fails if the note already exists** — it never overwrites. Parent folders are created as needed. Writes are atomic. |
| `append_to_note` | write | Appends to the **end** of an **existing** note — the note must already exist (create it first), and existing content is never edited or overwritten. |
| `append_to_section` | write | Inserts at the end of a specific **heading's section** (before the next same-or-higher-level heading; code fences don't count as headings) — capture into `## 📥 Inbox` without landing after a trailing dataview block. |
| `edit_note` | write | Exact search-and-replace edits, **atomic and all-or-nothing**: each `old_string` must match exactly once; supports `expected_hash`, checked again immediately before replacement, so stale edits normally fail with `CONFLICT`. Plain filesystems provide no portable CAS against unrelated external writers, so a narrow final race remains. |
| `move_note` | write | Moves/renames a note. Never overwrites the destination. Wiki-links are **not** rewritten. |
| `delete_note` | write | Moves the note to the vault's own `.trash/` (same as Obsidian's "move to vault trash") — nothing is permanently erased. |
| `list_tasks` | read | Checkbox tasks across the vault, parsed with **Obsidian Tasks plugin** conventions (📅 ⏳ 🛫 ✅, priorities, 🔁). Filters by status, due-date window and folder; sorted by due date; each task reports path, line and note version hash. |
| `complete_task` | write | Flips `[ ]` to `[x]` and appends `✅ YYYY-MM-DD` in the exact Tasks-plugin format. Recurring tasks are completed but the next occurrence is not generated. |
| `postpone_task` | write | Changes (or sets) a task's `📅` due date in the exact Tasks-plugin format. |
Remote images in written content are de-embedded into plain links before
touching disk, and note content returned to the model is stripped of channels
for invisible instructions (HTML comments, CSS-hidden elements, invisible
characters, the Unicode tag block). See the
[threat model](docs/threat-model.md) for why.
## Non-goals — on purpose
- **Single user, single vault.** One instance serves one person. Multi-tenancy
is a **declared non-goal**: it reintroduces an entire class of isolation
problems that simply doesn't exist today. Each user runs their own instance.
- **No Obsidian runtime.** No Templater, no Dataview, no plugins, no `eval`.
Notes created through the server are plain markdown; plugin syntax in your
templates will not be expanded.
- **No `delete_note`, no `move_note`.** Deliberately absent: they turn noise
into data loss, and a bad move breaks wikilinks across every synced device.
The minimal tool inventory is a security control, not an oversight.
- **No HTTP-request tools, no shell, no JavaScript execution.** Closing these
channels is what keeps the worst case of a successful prompt injection at
"junk in a note" instead of exfiltration.
- **No semantic index / embeddings** in this version. Full-text search covers
most cases and adds no state to maintain.
## Getting started
Follow [docs/setup.md](docs/setup.md) end to end: VPS bootstrap, sync client,
tunnel, Worker deploy, and adding the connector in Claude or ChatGPT. There is
also a local-only mode for trying the server on your own machine without any
of the edge pieces.
Before hosting this, read [docs/threat-model.md](docs/threat-model.md) — you
are exposing personal notes to the internet, even behind authentication, and
you should understand exactly what protects them and what the residual risks
are. Day-2 procedures (token revocation, secret rotation, restore from git)
live in [docs/operations.md](docs/operations.md).
## Repository layout
```
apps/
mcp-server/ MCP server on the VPS; executes the tools over HTTP
auth-worker/ Cloudflare Worker at the edge; OAuth + authenticated proxy
packages/
vault-core/ reading, atomic writes, path validation, search
vault-guards/ input/output sanitization, warn-only heuristics
tool-contract/ tool schemas and descriptions, shared
infra/
scripts/ bootstrap · configure · start · doctor · autocommit
systemd/ user units for server, sync, tunnel, autocommit
tunnel/ cloudflared configuration example
docs/
setup.md step-by-step installation
threat-model.md assets, adversaries, defenses, residual risk
operations.md runbooks: revoke, rotate, restore
```
The architecture rationale (in Portuguese) is in
[ARCHITECTURE.md](ARCHITECTURE.md). The exact tool surface — names, schemas
and the descriptions the model sees — lives in
[packages/tool-contract/src/index.ts](packages/tool-contract/src/index.ts).
## Contributing
See [CONTRIBUTING.md](CONTRIBUTING.md) — including the dependency policy (a
server with access to personal notes is a supply-chain target) and the
standing answer to multi-tenancy requests.
## License
[MIT](LICENSE) © Juliano Sirtori
This server cannot be deployed
Maintenance
ActivitySlowing
ResponsivenessNo issues