Skip to main content
Glama
phssakaigawa

obsidian-vault-mcp

by phssakaigawa
README.md
# obsidian-vault-mcp

Read-only MCP access to an [Obsidian](https://obsidian.md) vault, straight from the filesystem.

No Obsidian plugin, no REST API, no running Obsidian. The server reads the `.md` files on
disk, so it works when Obsidian is closed, when the vault lives on a synced folder, and on a
headless machine.

## Why this one

Most Obsidian MCP servers talk to Obsidian itself through the Local REST API community
plugin. That is the right design if you want to drive the app — open a note, run a template —
but it means the app has to be running, and it puts a plugin in the path of every read.

This server does the opposite: it treats the vault as what it is on disk, a folder of Markdown
files, and only reads. That makes it dependable in the cases where the REST-API servers are
not available at all, at the cost of not being able to drive Obsidian's UI.

It also searches what the notes *say*, not just what they are called — `search_content` and
`search_meta` are the tools that make a vault actually useful to an agent.

## Install

```sh
npm install -g @phssakaigawa/obsidian-vault-mcp
```

Or run it without installing:

```sh
npx -y @phssakaigawa/obsidian-vault-mcp /path/to/vault
```

Node 18 or newer.

## Configure

The server speaks MCP over stdio, so any MCP client can use it. One or more vault directories
are passed as positional arguments.

**Claude Code**

```sh
claude mcp add obsidian -s user -- npx -y @phssakaigawa/obsidian-vault-mcp /path/to/vault
```

**Claude Desktop** — in `claude_desktop_config.json`:

```json
{
  "mcpServers": {
    "obsidian": {
      "command": "npx",
      "args": ["-y", "@phssakaigawa/obsidian-vault-mcp", "/path/to/vault"]
    }
  }
}
```

On Windows, prefer an absolute path to `node.exe` plus the installed entry point over `npx`;
it does not depend on `PATH` and does not lose quoting:

```json
{
  "command": "C:\\Program Files\\nodejs\\node.exe",
  "args": [
    "C:\\path\\to\\node_modules\\@phssakaigawa\\obsidian-vault-mcp\\dist\\index.js",
    "C:\\Users\\you\\Documents\\MyVault"
  ]
}
```

**Any other MCP client** — same `command` / `args`, in whatever config file it uses.

## Tools

All five are read-only and are annotated as such, so clients that offer per-tool
auto-approval can safely allow the lot.

| Tool | What it does |
| --- | --- |
| `read_notes` | Read the full text of one or more notes. Accepts a vault-relative path, with or without `.md`, or a bare note title. A note that cannot be read is reported in place and does not abort the others. |
| `search_notes` | Find notes by title or path. Substring by default, regular expression on request. |
| `search_content` | Full-text search across note bodies. Returns matching lines with line numbers and surrounding context. |
| `search_meta` | Find notes by YAML frontmatter key/value, or by tag. Tags are read from both the frontmatter `tags` key and inline `#tags` in the body. |
| `list_notes` | List notes, newest first, optionally under one folder. Paged. |

Every search takes a `limit`, and the result says when the limit was the reason it stopped.

## Safety

The vault directories given on the command line are the only thing the server will read.

Each requested path is resolved and checked twice: once as written, and again after symlink
resolution, so a link inside the vault cannot be used to read a file outside it. Containment
is separator-aware — a root of `/vault` does not admit `/vault-private`. Relative paths
resolve against the vault root, not the process working directory, which for an MCP server is
just wherever the client happened to launch it.

There is no write path. Nothing in this server opens a file for writing.

`.obsidian`, `.trash`, `.git` and other dot-directories are skipped, so workspace state and
plugin configuration never reach the model.

## Design notes

**Every runtime import is a declared dependency.** There are three — the MCP SDK, `zod` and
`yaml` — and all three are in `dependencies` with an explicit range. Relying on a transitive
package being hoisted into place works right up until the day the ecosystem moves and the
package resolves to a version whose API has changed; the failure then looks like a
malformed tool schema rather than a missing module, which is a genuinely confusing thing to
debug. Declaring what you import costs one line and removes the whole class of problem.

**Searches stream.** Traversal is an async generator, so a search that reaches its limit
stops touching the disk at that point instead of walking the whole vault first.

**Frontmatter goes through a real YAML parser.** Frontmatter is arbitrary YAML; a regex
approach quietly mis-parses nested maps and multi-line strings. A note whose frontmatter does
not parse yields no metadata rather than failing the search it appears in.

## Limitations

- Read-only, by design.
- Only `.md` files. Canvases, attachments and other vault contents are not indexed.
- No semantic or embedding search; `search_content` is literal and regex matching.
- Wiki-links are not resolved into a graph. `[[Note]]` is matched as text like any other.
- No index is kept between calls. Searches walk the vault each time, which is fine for
  vaults in the low thousands of notes and is the reason there is nothing to invalidate when
  the vault changes underneath you.

## Development

```sh
npm install
npm run build
node scripts/probe.mjs /path/to/vault
```

`scripts/probe.mjs` speaks MCP to the built server and prints what a client would see. It
checks the thing every client validates first — that each tool advertises an object
`inputSchema` — then calls each tool and confirms a path outside the vault is refused.

## License

MIT. See [LICENSE](LICENSE) and [NOTICE](NOTICE); the vault boundary check is derived from the
MIT-licensed Model Context Protocol reference filesystem server, and NOTICE records exactly
what was taken and what was changed.