Skip to main content
Glama
shaynemeyer

obsidian-mcp

by shaynemeyer
README.md
# obsidian-mcp

A FastMCP server for **multiple** Obsidian vaults. It talks to the **Obsidian Local REST API** plugin when Obsidian is running, and falls back to **direct filesystem access** when it isn't — so your notes stay reachable whether or not the app is open.

Every tool takes an optional `vault` argument, and `obsidian_switch_vault` sets the default for unqualified calls.

## Why both backends

|                                     | REST (Obsidian running)           | Filesystem (always)                      |
| ----------------------------------- | --------------------------------- | ---------------------------------------- |
| Search                              | Obsidian's own index and ranking  | substring scan, all terms must match     |
| Tags                                | resolved by Obsidian, with counts | parsed from frontmatter + inline `#tags` |
| Backlinks                           | ✅ from the link graph            | ❌ not available                         |
| Active note / open in UI / commands | ✅                                | ❌                                       |
| Works with Obsidian closed          | ❌                                | ✅                                       |
| Works on a headless box or over SSH | ❌                                | ✅                                       |

The router probes REST health once and caches the result for `OBSIDIAN_HEALTH_TTL` seconds, so a closed Obsidian doesn't cost a failed connection on every call. If REST drops mid-call it retries on the filesystem automatically. Errors that mean _"what you asked for doesn't exist"_ (missing note, bad path) are **not** retried — falling back would just fail again, slower.

Every response reports which backend served it, so a caller can tell whether it's seeing Obsidian's view or the raw files.

## Setup

```bash
git clone <your-repo> obsidian-mcp && cd obsidian-mcp
uv venv && uv pip install -e ".[dev]"     # or: pip install -e ".[dev]"
pytest
```

Requires Python 3.11+. Works with MCP Python SDK 1.x (`FastMCP`) and 2.x (`MCPServer`) — `_sdk.py` papers over the rename.

### Configuring vaults

Define vaults using whichever of these fits; the first one present wins.

**A TOML file** (best for 3+ vaults) — `OBSIDIAN_VAULTS_FILE=~/.config/obsidian-mcp/vaults.toml`:

```toml
default = "personal"

[vaults.personal]
path = "/Users/you/Vaults/Personal"
description = "Homelab, hobbies"
daily_folder = "Journal"

[vaults.work]
path = "/Users/you/Vaults/Work"
description = "Employer notes"
rest_base_url = "https://127.0.0.1:27125"   # note: NOT the default port
api_key = "this-vault's-own-key"
```

**Inline JSON** — `OBSIDIAN_VAULTS='{"default":"work","vaults":{...}}'`. Accepts `port`/`protocol`/`host` shorthand instead of `rest_base_url`, and camelCase keys, so configs from other Obsidian MCP servers mostly paste straight in.

**Per-vault env vars** — `OBSIDIAN_VAULT_WORK_PATH`, `OBSIDIAN_VAULT_WORK_API_KEY`, `OBSIDIAN_VAULT_WORK_REST_BASE_URL`, etc. The segment between `OBSIDIAN_VAULT_` and the field name is the vault name.

**Legacy single-vault vars** — `OBSIDIAN_VAULT_PATH` / `OBSIDIAN_REST_BASE_URL` / `OBSIDIAN_API_KEY` still work and define one vault named `default`.

#### ⚠️ One port per vault

Every vault's Local REST API plugin **defaults to port 27124**, and only one process can bind a port. If you run two vaults at once, open each vault's Settings → Local REST API → Advanced and give it a unique port (27124, 27125, 27126…), then toggle the plugin off and on.

Each vault also generates its **own API key**, so a mismatched port usually shows up as a 401 rather than silently reading the wrong vault. To check positively:

```
obsidian_backend_status(verify=true)
```

That cross-checks each vault's REST root listing against its configured path and warns if they disagree.

#### Per-vault settings

`path`, `rest_base_url`, `api_key`, `verify_ssl`, `ca_cert`, `daily_folder`, `daily_format`, `description`

#### Global settings

| Variable                   | Default       | Notes                                            |
| -------------------------- | ------------- | ------------------------------------------------ |
| `OBSIDIAN_DEFAULT_VAULT`   | first defined | Overrides `default` in the config file.          |
| `OBSIDIAN_PREFER_REST`     | `true`        | Set `false` to always use the filesystem.        |
| `OBSIDIAN_READ_ONLY`       | `false`       | Disables every mutating tool, across all vaults. |
| `OBSIDIAN_MAX_FILE_BYTES`  | `2000000`     | Refuse to read anything larger.                  |
| `OBSIDIAN_HEALTH_TTL`      | `20`          | Seconds to cache each vault's REST health probe. |
| `OBSIDIAN_REQUEST_TIMEOUT` | `15`          | Per-request timeout in seconds.                  |

To trust the plugin's cert rather than disabling verification, per vault:

```bash
curl -k https://127.0.0.1:27125/obsidian-local-rest-api.crt -o ~/.config/obsidian-mcp/work.crt
# then in vaults.toml:  ca_cert = "~/.config/obsidian-mcp/work.crt"  and  verify_ssl = true
```

### Register it

**Claude Code:**

```bash
claude mcp add obsidian -- \
  uv --directory /path/to/obsidian-mcp run obsidian-mcp
```

Then set the env vars in `.mcp.json` or your shell profile.

**Claude Desktop** (`claude_desktop_config.json`):

```json
{
  "mcpServers": {
    "obsidian": {
      "command": "uv",
      "args": ["--directory", "/path/to/obsidian-mcp", "run", "obsidian-mcp"],
      "env": {
        "OBSIDIAN_VAULTS_FILE": "/Users/you/.config/obsidian-mcp/vaults.toml"
      }
    }
  }
}
```

## Tools

**Vaults** — `obsidian_list_vaults`, `obsidian_switch_vault`

**Reads** — `obsidian_list_notes`, `obsidian_read_note`, `obsidian_search_notes`, `obsidian_list_tags`, `obsidian_get_active_note`\*, `obsidian_backend_status`

**Writes** — `obsidian_write_note`, `obsidian_append_to_note`, `obsidian_patch_note`, `obsidian_append_to_daily_note`, `obsidian_move_note`, `obsidian_delete_note`

**App control** — `obsidian_open_note`_, `obsidian_list_commands`_, `obsidian_run_command`\*

\* requires Obsidian to be running. These deliberately do **not** fall back — they return a message telling you to start Obsidian, rather than silently doing something different from what you asked.

All 17 tools accept an optional `vault`. Targeting works two ways, on purpose:

- **Per-call** — `obsidian_search_notes(vault="work", query="...")`. Explicit and stateless; the right choice when a task spans vaults.
- **Session default** — `obsidian_switch_vault(vault="work")` changes where unqualified calls go, so a long stretch of work in one vault doesn't repeat the argument.

A per-call `vault` always overrides the session default. Switching affects this server session only — it doesn't touch any vault's contents or change which vault Obsidian has open.

`obsidian_backend_status` is the one to call first when something fails with a connectivity error; with no vault named it probes all of them and reports which backends are live.

## Safety choices

- **Deletes are recoverable by default.** `obsidian_delete_note` moves the note to the vault's `.trash`, restorable from inside Obsidian. `permanent=true` is opt-in. Because the plugin's `DELETE` is unconditional, trashing is routed through the filesystem backend even when REST is live.
- **Absolute paths and `..` traversal are rejected**, not silently rewritten. Resolved paths are checked against the vault root.
- **`obsidian_write_note` won't clobber** an existing note without `overwrite=true`.
- **`OBSIDIAN_READ_ONLY=true`** disables every mutating tool in one switch — useful for a research-only session.

## Known limitations

- **Moving a note does not rewrite wikilinks.** Only Obsidian does that, and only for moves made inside the app. Search for the old name afterwards if it matters.
- **Frontmatter patches reformat the YAML block.** Values round-trip through PyYAML, so `tags: [a, b]` comes back as a block list. Content is preserved; formatting isn't.
- **Filesystem search has no fuzzy matching or ranking** beyond a title-match boost. When Obsidian is running you get its real index instead.
- **The filesystem backend doesn't see unsaved editor buffers.** A note being actively edited may be stale on disk by a few seconds.
- **Cross-vault operations aren't atomic.** `obsidian_move_note` works within one vault only; moving between vaults means read, write, delete as separate calls.
- **`obsidian_get_active_note` is per-vault**, and only answers for a vault whose Obsidian window is open with the plugin bound to that vault's configured port.

## Worth knowing before you build on this

As of v3+, the Local REST API plugin **ships its own built-in MCP server** at `https://127.0.0.1:27124/mcp/` (streamable HTTP, bearer auth). If all you want is Obsidian access while Obsidian is running, point your client at that and skip this project entirely.

This server earns its place when you want the things that one can't do: working with the vault while Obsidian is closed, running on a headless machine, read-only enforcement, trash-by-default deletes, or custom workflow tools shaped around your own vault conventions.

## Development

```bash
pytest                    # 40 tests, no Obsidian required
ruff check obsidian_mcp tests
npx @modelcontextprotocol/inspector uv --directory . run obsidian-mcp
```

The test suite fakes the REST backend, so fallback and multi-vault routing are covered without a running Obsidian.

TDQS

A4.2/5.0

Scored across 17 tools

Disambiguation5/5

Every tool targets a distinct resource and action: reading (read_note, get_active_note), discovery (list_notes, search_notes, list_tags), writing (write_note, append_to_note, patch_note), and vault administration (list_vaults, switch_vault, backend_status). Overlapping write tools are clearly differentiated by insertion point and cross-reference each other.

Naming Consistency4/5

The overwhelming majority follow the obsidian_verb_noun pattern (list_notes, read_note, write_note, delete_note, run_command). Only obsidian_backend_status deviates by using a noun-noun name instead of a verb such as get_status or get_backend_status.

Tool Count4/5

17 tools is slightly above the ideal 3-15 range, but each tool earns its place covering vaults, notes, search, commands, and status. The granularity between append, patch, and append_to_daily_note is justified by real workflow differences.

Completeness4/5

Note CRUD is fully covered (create via write, read, update via write/append/patch, move, delete), plus discovery, search, tag listing, vault management, and Obsidian command execution. Minor gaps exist—such as no restore-from-trash tool and no explicit folder creation—but agents can work around them.

Maintenance

ActivitySlowing
ResponsivenessNo issues