obsidian-mcp-brain 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +457 -0
- package/package.json +5 -4
package/README.md
ADDED
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
# obsidian-mcp-brain
|
|
2
|
+
|
|
3
|
+
A thin Node.js HTTP server that provides remote MCP access to an Obsidian vault.
|
|
4
|
+
It implements all MCP tools natively and exposes them to remote clients such as
|
|
5
|
+
Claude Code and Claude Desktop via HTTP.
|
|
6
|
+
|
|
7
|
+
For the full project (including `mcp-shim`, a Claude Desktop relay) and
|
|
8
|
+
development setup, see the [repo README](https://github.com/a1ecbr0wn/obsidian-mcp-brain#readme).
|
|
9
|
+
|
|
10
|
+
## Why this server is needed
|
|
11
|
+
|
|
12
|
+
Remote MCP clients (Claude Code 2.x, Claude Desktop) connect over HTTP, not stdio,
|
|
13
|
+
and the standard approaches have issues:
|
|
14
|
+
|
|
15
|
+
### No HTTP transport for local MCP servers
|
|
16
|
+
|
|
17
|
+
Existing solutions like [`mcp-proxy`](https://github.com/sparfenyuk/mcp-proxy) wrap
|
|
18
|
+
stdio MCP servers and expose them over HTTP. However, mcp-proxy has a session-management
|
|
19
|
+
bug: when Claude Code opens its GET `/mcp` notification stream at the same time
|
|
20
|
+
as sending tool-list requests (which it always does), mcp-proxy's response routing
|
|
21
|
+
gets confused and `tools/list` silently times out. This server implements the MCP
|
|
22
|
+
HTTP transport layer directly and implements all tools natively, eliminating that
|
|
23
|
+
class of bug.
|
|
24
|
+
|
|
25
|
+
### No OAuth 2.0 discovery
|
|
26
|
+
|
|
27
|
+
The [MCP 2025-03-26 spec](https://spec.modelcontextprotocol.io) requires every
|
|
28
|
+
non-localhost remote MCP server to expose OAuth 2.0 discovery endpoints
|
|
29
|
+
(`/.well-known/oauth-protected-resource`, `/.well-known/oauth-authorization-server`,
|
|
30
|
+
`/authorize`, `/token`, `/register`). Without them, Claude Code refuses to connect.
|
|
31
|
+
This server serves a public (no credentials required) OAuth flow so that Claude
|
|
32
|
+
Code's auth handshake completes without needing real credentials.
|
|
33
|
+
|
|
34
|
+
### What this server does
|
|
35
|
+
|
|
36
|
+
```text
|
|
37
|
+
Claude Code / Claude Desktop
|
|
38
|
+
│ HTTPS (e.g. Tailscale)
|
|
39
|
+
▼
|
|
40
|
+
obsidian-mcp-brain :3002
|
|
41
|
+
├─ OAuth 2.0 discovery endpoints
|
|
42
|
+
├─ MCP Streamable HTTP transport (POST /mcp, GET /mcp)
|
|
43
|
+
├─ Path deny list (access control)
|
|
44
|
+
└─ Vault access (node:fs/promises)
|
|
45
|
+
│
|
|
46
|
+
▼
|
|
47
|
+
Obsidian vault (filesystem)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
### Prerequisites
|
|
53
|
+
|
|
54
|
+
- Node.js 18+
|
|
55
|
+
- A way to expose the server over HTTPS to your remote client —
|
|
56
|
+
[Tailscale Serve](https://tailscale.com/kb/1312/serve) is what I use, but any
|
|
57
|
+
HTTPS reverse proxy works
|
|
58
|
+
- **Optional:** The `query-graph` tool (which queries a vault using a natural-language
|
|
59
|
+
knowledge graph) requires the `graphify` CLI to be installed and on `PATH`, and
|
|
60
|
+
a knowledge graph to be pre-built for that vault (run `graphify --obsidian`
|
|
61
|
+
against it once, before calling the tool). `query-graph` is always listed, but
|
|
62
|
+
calling it against a vault with no graph returns a clear error rather than an
|
|
63
|
+
answer.
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
### Installation
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
npm install -g obsidian-mcp-brain
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
This puts an `obsidian-mcp-brain` command on your `PATH`. Then set it up as a
|
|
74
|
+
persistent service.
|
|
75
|
+
|
|
76
|
+
### systemd (Linux)
|
|
77
|
+
|
|
78
|
+
First, create your config file — see [Configuration](#configuration) below for its
|
|
79
|
+
full shape. By default the server reads `~/.config/obsidian-mcp.json`, so no extra
|
|
80
|
+
environment variable is needed unless you want the config somewhere else.
|
|
81
|
+
|
|
82
|
+
Then create `~/.config/systemd/user/obsidian-mcp.service`:
|
|
83
|
+
|
|
84
|
+
```ini
|
|
85
|
+
[Unit]
|
|
86
|
+
Description=Obsidian MCP Server
|
|
87
|
+
After=network.target
|
|
88
|
+
|
|
89
|
+
[Service]
|
|
90
|
+
ExecStart=/usr/bin/env obsidian-mcp-brain
|
|
91
|
+
Restart=on-failure
|
|
92
|
+
RestartSec=5
|
|
93
|
+
|
|
94
|
+
[Install]
|
|
95
|
+
WantedBy=default.target
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
(If your config file lives somewhere other than `~/.config/obsidian-mcp.json`, add
|
|
99
|
+
`Environment=CONFIG_PATH=/path/to/your/config.json` under `[Service]`.)
|
|
100
|
+
|
|
101
|
+
Then enable and start it:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
systemctl --user daemon-reload
|
|
105
|
+
systemctl --user enable --now obsidian-mcp.service
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### macOS (launchd)
|
|
109
|
+
|
|
110
|
+
Create `~/Library/LaunchAgents/com.obsidian-mcp-brain.plist`:
|
|
111
|
+
|
|
112
|
+
```xml
|
|
113
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
114
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
|
|
115
|
+
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
116
|
+
<plist version="1.0">
|
|
117
|
+
<dict>
|
|
118
|
+
<key>Label</key>
|
|
119
|
+
<string>com.obsidian-mcp-brain</string>
|
|
120
|
+
|
|
121
|
+
<key>ProgramArguments</key>
|
|
122
|
+
<array>
|
|
123
|
+
<string>/usr/local/bin/obsidian-mcp-brain</string>
|
|
124
|
+
</array>
|
|
125
|
+
|
|
126
|
+
<!-- Only needed if your config file isn't at the default
|
|
127
|
+
~/.config/obsidian-mcp.json -->
|
|
128
|
+
<key>EnvironmentVariables</key>
|
|
129
|
+
<dict>
|
|
130
|
+
<key>CONFIG_PATH</key>
|
|
131
|
+
<string>/path/to/your/obsidian-mcp.json</string>
|
|
132
|
+
</dict>
|
|
133
|
+
|
|
134
|
+
<key>RunAtLoad</key>
|
|
135
|
+
<true/>
|
|
136
|
+
<key>KeepAlive</key>
|
|
137
|
+
<true/>
|
|
138
|
+
|
|
139
|
+
<key>StandardOutPath</key>
|
|
140
|
+
<string>/tmp/obsidian-mcp-brain.log</string>
|
|
141
|
+
<key>StandardErrorPath</key>
|
|
142
|
+
<string>/tmp/obsidian-mcp-brain.log</string>
|
|
143
|
+
</dict>
|
|
144
|
+
</plist>
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Load it:
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
launchctl load ~/Library/LaunchAgents/com.obsidian-mcp-brain.plist
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
To restart after a config change:
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
launchctl unload ~/Library/LaunchAgents/com.obsidian-mcp-brain.plist
|
|
157
|
+
launchctl load ~/Library/LaunchAgents/com.obsidian-mcp-brain.plist
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### Tailscale Serve (HTTPS tunnel)
|
|
161
|
+
|
|
162
|
+
Point Tailscale at the server's local port:
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
sudo tailscale serve --bg --https 4001 http://localhost:3002
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
This exposes the server at `https://<your-tailscale-hostname>:4001`.
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## Configuration
|
|
173
|
+
|
|
174
|
+
All configuration lives in one JSON file — no environment variables are read except
|
|
175
|
+
`CONFIG_PATH`, which says where to find it.
|
|
176
|
+
|
|
177
|
+
- **Location**: `CONFIG_PATH` env var if set, otherwise `~/.config/obsidian-mcp.json`.
|
|
178
|
+
- The server serves one or more named vaults from a single process; a client picks
|
|
179
|
+
which one a call applies to via the `vault` argument every tool already takes.
|
|
180
|
+
|
|
181
|
+
### Shape
|
|
182
|
+
|
|
183
|
+
```json
|
|
184
|
+
{
|
|
185
|
+
"listenPort": 3002,
|
|
186
|
+
"mcpBaseUrl": "https://your-hostname:4001",
|
|
187
|
+
"denyPaths": ["private"],
|
|
188
|
+
"graphifyQueryTimeoutMs": 60000,
|
|
189
|
+
"fetchMaxBytes": 10485760,
|
|
190
|
+
"fetchTimeoutMs": 30000,
|
|
191
|
+
"vaults": {
|
|
192
|
+
"knowledge": {
|
|
193
|
+
"path": "/path/to/your/obsidian/vault"
|
|
194
|
+
},
|
|
195
|
+
"work": {
|
|
196
|
+
"path": "/path/to/another/vault",
|
|
197
|
+
"denyPaths": ["confidential"]
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
| Field | Required | Default | Description |
|
|
204
|
+
| ------------------------ | -------- | ---------- | ----------------------------------------------------------------------------------------- |
|
|
205
|
+
| `mcpBaseUrl` | Yes | — | Public HTTPS base URL of the server (used in OAuth responses and SSE endpoint events) |
|
|
206
|
+
| `vaults` | Yes | — | Non-empty object of `{ "name": { "path": "..." } }`. Each vault needs at least a `path` |
|
|
207
|
+
| `listenPort` | No | `3002` | Local port the server listens on |
|
|
208
|
+
| `denyPaths` | No | `[]` | Vault-relative paths to block, applied to every vault. See below |
|
|
209
|
+
| `graphifyQueryTimeoutMs` | No | `60000` | Timeout for a `graphify query` subprocess (milliseconds) |
|
|
210
|
+
| `fetchMaxBytes` | No | `10485760` | Default max response size for `fetch-binary-file` (bytes); overridable per call |
|
|
211
|
+
| `fetchTimeoutMs` | No | `30000` | Default request timeout for `fetch-binary-file` (milliseconds); overridable per call |
|
|
212
|
+
|
|
213
|
+
Each vault entry can also set its own `denyPaths`, which are added on top of the
|
|
214
|
+
global list for that vault only (see below).
|
|
215
|
+
|
|
216
|
+
### Path deny list (`denyPaths`)
|
|
217
|
+
|
|
218
|
+
`denyPaths` lets you prevent the MCP client from reading or writing specific
|
|
219
|
+
folders in a vault. Paths are relative to the vault root and prefix-matched, so
|
|
220
|
+
denying `people` blocks `people/`, `people/alice/notes.md`, and so on.
|
|
221
|
+
|
|
222
|
+
The top-level `denyPaths` applies to every configured vault. A vault's own
|
|
223
|
+
`denyPaths` (if any) is added on top of that global list, restricting that vault
|
|
224
|
+
further without affecting any other vault:
|
|
225
|
+
|
|
226
|
+
```json
|
|
227
|
+
{
|
|
228
|
+
"denyPaths": ["private"],
|
|
229
|
+
"vaults": {
|
|
230
|
+
"knowledge": { "path": "/export/knowledge" },
|
|
231
|
+
"work": { "path": "/export/work", "denyPaths": ["confidential", "drafts"] }
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
Here, both vaults block `private`; `work` additionally blocks `confidential` and
|
|
237
|
+
`drafts`, while `knowledge` is unaffected by that extra restriction.
|
|
238
|
+
|
|
239
|
+
The deny list is enforced in the server before any tool handler executes.
|
|
240
|
+
Blocked requests receive a structured MCP error (`isError: true`) rather than a
|
|
241
|
+
transport-level failure, so the client can report the reason clearly.
|
|
242
|
+
|
|
243
|
+
Affected tools: `read-note`, `create-note`, `edit-note`, `delete-note`, `move-note`
|
|
244
|
+
(source and destination), `create-binary-file`, `fetch-binary-file`, `delete-binary-file`,
|
|
245
|
+
`move-binary-file` (source and destination), `find-backlinks`, `resolve-wikilink`,
|
|
246
|
+
`add-tags`, `remove-tags`, `set-frontmatter-field`, `remove-frontmatter-field`,
|
|
247
|
+
`create-folder`, `search-vault` (when a `path` scope is given).
|
|
248
|
+
|
|
249
|
+
`list-notes`, `list-tags`, `search-tags`, `new-notes`, `changed-notes`, and `rename-tag`
|
|
250
|
+
are equally protected, just via a different mechanism: instead of a single check up
|
|
251
|
+
front, they filter out denied files individually as they walk the vault.
|
|
252
|
+
|
|
253
|
+
`list-vaults` doesn't touch vault files at all — it just returns configured
|
|
254
|
+
vault names — so it's the only tool genuinely unaffected by the deny list.
|
|
255
|
+
`query-graph` takes a free-text question rather than a vault path, so it isn't subject
|
|
256
|
+
to the deny list either.
|
|
257
|
+
|
|
258
|
+
### Write preconditions (`expectedMtime`)
|
|
259
|
+
|
|
260
|
+
Every tool that writes to an existing file accepts an optional `expectedMtime`: the
|
|
261
|
+
ISO 8601 last-modified timestamp you previously got back from `read-note` or
|
|
262
|
+
`list-notes`. Pass it back on a later write and the server refuses the write — with
|
|
263
|
+
no changes made — if the file's mtime has moved since, telling you both the
|
|
264
|
+
timestamp you expected and its actual current one so you know to re-read and retry.
|
|
265
|
+
|
|
266
|
+
This is a compare-and-swap check, not a lock: it's meant to catch "I read this note
|
|
267
|
+
a while ago, and something else touched it since," not to serialize concurrent
|
|
268
|
+
writers. It's optional so existing callers are unaffected, but a client following a
|
|
269
|
+
read-then-write pattern (read a note, decide what to change, write it back) should
|
|
270
|
+
always pass it — otherwise an edit made by something else in between is silently
|
|
271
|
+
overwritten.
|
|
272
|
+
|
|
273
|
+
```
|
|
274
|
+
read-note → note content + Last-Modified: 2026-09-20T10:15:00.000Z
|
|
275
|
+
...decide what to change...
|
|
276
|
+
edit-note → { operation: "replace", content: "...", expectedMtime: "2026-09-20T10:15:00.000Z" }
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
Applies to: `edit-note` (all operations), `delete-note`, `move-note` (checked against
|
|
280
|
+
the source file), `set-frontmatter-field`, `remove-frontmatter-field`,
|
|
281
|
+
`move-binary-file` and `delete-binary-file` (checked against the source file). For
|
|
282
|
+
`add-tags`/`remove-tags`, which operate on a `files[]` array, `expectedMtime` is
|
|
283
|
+
instead an object mapping each vault-relative path to its expected timestamp; every
|
|
284
|
+
listed file's precondition is checked before any file in the batch is written, so
|
|
285
|
+
the batch either applies wholly or not at all.
|
|
286
|
+
|
|
287
|
+
Not applicable to `create-note`, `create-binary-file`, or `fetch-binary-file`, which
|
|
288
|
+
already fail if the destination exists, nor to `rename-tag`, which sweeps the whole
|
|
289
|
+
vault rather than targeting one file.
|
|
290
|
+
|
|
291
|
+
### edit-note operations
|
|
292
|
+
|
|
293
|
+
Beyond `append`, `prepend`, and `replace`, `edit-note` supports targeted,
|
|
294
|
+
section-scoped edits so a large note doesn't need to be resent in full for a small
|
|
295
|
+
change:
|
|
296
|
+
|
|
297
|
+
- **`replace-section`** — replaces the content under a heading (matched by exact
|
|
298
|
+
text), leaving the heading line itself in place.
|
|
299
|
+
- **`delete-section`** — removes a heading and everything under it, heading line
|
|
300
|
+
included.
|
|
301
|
+
- **`toggle-checkbox`** — flips (or explicitly sets) a `- [ ]`/`- [x]` line, matched
|
|
302
|
+
by its exact text.
|
|
303
|
+
|
|
304
|
+
If a `heading` or `taskText` match isn't unique in the note, the call fails with a
|
|
305
|
+
list of every match (line number, and heading level where relevant); pass the
|
|
306
|
+
1-based `occurrence` from that list on a follow-up call to disambiguate.
|
|
307
|
+
|
|
308
|
+
### fetch-binary-file
|
|
309
|
+
|
|
310
|
+
`create-binary-file` requires the client to send the file as base64 — expensive
|
|
311
|
+
through an LLM client, since base64 is read in and written out again on top of its
|
|
312
|
+
already-larger-than-binary size. `fetch-binary-file` instead has the server download
|
|
313
|
+
a URL itself and write the result, so the client only ever sends a URL string.
|
|
314
|
+
|
|
315
|
+
```
|
|
316
|
+
fetch-binary-file → { filename: "photo.jpg", folder: "attachments", url: "https://example.com/photo.jpg" }
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
Because the server performs the request itself, a caller-supplied URL is effectively
|
|
320
|
+
asking this host to make an arbitrary outbound call — on any deployment, this host
|
|
321
|
+
may be able to reach private network services that shouldn't be exposed to a remote
|
|
322
|
+
MCP client. `fetch-binary-file` treats this as its primary risk:
|
|
323
|
+
|
|
324
|
+
- Only `http`/`https` URLs are accepted.
|
|
325
|
+
- The hostname is resolved and the request is refused if any resolved address is
|
|
326
|
+
loopback, link-local, unique-local, or in RFC1918 private space.
|
|
327
|
+
- Every redirect hop is re-validated the same way — not just the initial URL — since
|
|
328
|
+
a public hostname can redirect to a private address.
|
|
329
|
+
- The response body is size-checked while streaming, so an oversized response is
|
|
330
|
+
aborted mid-transfer rather than after it's already been downloaded.
|
|
331
|
+
- A hard timeout (`fetchTimeoutMs`, overridable per call) aborts a slow or hanging
|
|
332
|
+
response.
|
|
333
|
+
- The destination path is deny-path- and collision-checked *before* any network
|
|
334
|
+
call, so a denied or already-occupied path never causes an outbound request.
|
|
335
|
+
|
|
336
|
+
`maxBytes` and `timeoutMs` can be overridden per call; otherwise they default to the
|
|
337
|
+
config file's `fetchMaxBytes`/`fetchTimeoutMs`.
|
|
338
|
+
|
|
339
|
+
---
|
|
340
|
+
|
|
341
|
+
## Connecting Claude Code
|
|
342
|
+
|
|
343
|
+
Add the server to your Claude Code config (`~/.claude.json` or via `claude mcp add`):
|
|
344
|
+
|
|
345
|
+
```json
|
|
346
|
+
{
|
|
347
|
+
"mcpServers": {
|
|
348
|
+
"obsidian": {
|
|
349
|
+
"type": "http",
|
|
350
|
+
"url": "https://your-hostname:4001/mcp"
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
Claude Code will prompt you to authenticate the first time — click through the
|
|
357
|
+
OAuth flow (it uses a public/no-credentials token, so no real account is needed).
|
|
358
|
+
|
|
359
|
+
---
|
|
360
|
+
|
|
361
|
+
## Connecting Claude Desktop
|
|
362
|
+
|
|
363
|
+
Unlike Claude Code, Claude Desktop doesn't speak the MCP Streamable HTTP transport
|
|
364
|
+
directly — it only launches local stdio subprocesses. To reach a remote server like
|
|
365
|
+
this one, it needs a small relay in between that translates its stdio JSON-RPC
|
|
366
|
+
traffic into HTTP+SSE calls against the server's `/mcp` endpoint. Two options:
|
|
367
|
+
|
|
368
|
+
- **[`mcp-remote`](https://www.npmjs.com/package/mcp-remote) via `npx`** (below) —
|
|
369
|
+
the quickest option, no install or local files needed, good for a plain
|
|
370
|
+
no-credentials setup like this server's public OAuth flow.
|
|
371
|
+
- **[`mcp-shim`](https://github.com/a1ecbr0wn/obsidian-mcp-brain#mcp-shim)** —
|
|
372
|
+
a zero-dependency local script from this project's repo, worth using instead if
|
|
373
|
+
you need a bearer token, a custom request timeout, or want to avoid an `npx`
|
|
374
|
+
download on every Claude Desktop launch.
|
|
375
|
+
|
|
376
|
+
Edit `claude_desktop_config.json` (find it via **Claude Desktop → Settings →
|
|
377
|
+
Developer → Edit Config**) and add an entry under `mcpServers`, replacing the URL
|
|
378
|
+
below with your server's actual address:
|
|
379
|
+
|
|
380
|
+
```json
|
|
381
|
+
{
|
|
382
|
+
"mcpServers": {
|
|
383
|
+
"obsidian": {
|
|
384
|
+
"command": "npx",
|
|
385
|
+
"args": [
|
|
386
|
+
"mcp-remote@latest",
|
|
387
|
+
"https://your-hostname:4001/mcp"
|
|
388
|
+
]
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
Restart Claude Desktop after saving.
|
|
395
|
+
|
|
396
|
+
---
|
|
397
|
+
|
|
398
|
+
## Available Tools
|
|
399
|
+
|
|
400
|
+
| Tool | Description |
|
|
401
|
+
| --- | --- |
|
|
402
|
+
| `list-notes` | List all notes in the vault, or scoped to a folder. Returns sorted vault-relative paths, each with a last-modified timestamp |
|
|
403
|
+
| `list-tags` | List all unique tags (YAML frontmatter) used across vault notes, optionally scoped to a subdirectory |
|
|
404
|
+
| `search-tags` | Find notes that have ALL of the specified tags |
|
|
405
|
+
| `new-notes` | List notes created in the last 7 days, or since a provided timestamp |
|
|
406
|
+
| `changed-notes` | List notes modified in the last 7 days, or since a provided timestamp |
|
|
407
|
+
| `list-vaults` | List all configured vaults |
|
|
408
|
+
| `read-note` | Read a note's content and last-modified timestamp |
|
|
409
|
+
| `create-note` | Create a new note. Fails if it already exists |
|
|
410
|
+
| `edit-note` | Edit a note: append, prepend, replace, replace/delete the content under a heading, or toggle a checkbox |
|
|
411
|
+
| `delete-note` | Delete a note, moving it to `.trash` by default |
|
|
412
|
+
| `move-note` | Move or rename a note, rewriting all vault-wide wikilinks to the old path |
|
|
413
|
+
| `create-binary-file` | Create a new binary file (e.g. an image) from base64-encoded content. Fails if it already exists |
|
|
414
|
+
| `fetch-binary-file` | Create a new binary file by downloading a URL server-side, so the client only sends a URL, not the file content |
|
|
415
|
+
| `move-binary-file` | Move or rename a binary file, rewriting all vault-wide wikilink embeds pointing at the old path |
|
|
416
|
+
| `delete-binary-file` | Delete a binary file, moving it to `.trash` by default |
|
|
417
|
+
| `find-backlinks` | Find all notes that link to or embed a given note or binary file |
|
|
418
|
+
| `resolve-wikilink` | Resolve a wikilink target string to the vault-relative file(s) it points to |
|
|
419
|
+
| `create-folder` | Create a new folder (and any missing parents) in the vault |
|
|
420
|
+
| `search-vault` | Search vault notes by content, filename, or both |
|
|
421
|
+
| `add-tags` | Add tags to notes in frontmatter and/or inline body content |
|
|
422
|
+
| `remove-tags` | Remove tags from notes in frontmatter and/or inline body content |
|
|
423
|
+
| `rename-tag` | Rename a tag throughout the entire vault (frontmatter and inline) |
|
|
424
|
+
| `set-frontmatter-field` | Set a single frontmatter field (not `tags`) to a scalar value, creating it if missing |
|
|
425
|
+
| `remove-frontmatter-field` | Remove a single frontmatter field (not `tags`) entirely |
|
|
426
|
+
| `query-graph` | Ask a natural-language question against a vault's graphify knowledge graph |
|
|
427
|
+
|
|
428
|
+
---
|
|
429
|
+
|
|
430
|
+
## How obsidian-mcp-brain works
|
|
431
|
+
|
|
432
|
+
The server implements the [MCP Streamable HTTP transport (2024-11-05)](https://spec.modelcontextprotocol.io/specification/2024-11-05/basic/transports/#streamable-http):
|
|
433
|
+
|
|
434
|
+
- **`POST /mcp`** — receives JSON-RPC requests from the client. `initialize` creates
|
|
435
|
+
a session and returns server capabilities. Notifications return 202 code. All other
|
|
436
|
+
requests are dispatched to the corresponding tool handler and the response is
|
|
437
|
+
returned as an inline SSE event.
|
|
438
|
+
|
|
439
|
+
- **`GET /mcp`** — keeps a long-lived SSE stream open per session for server-to-client
|
|
440
|
+
notifications (e.g. `tools/list_changed`).
|
|
441
|
+
|
|
442
|
+
All MCP tools are implemented natively in the server and operate directly on vault files
|
|
443
|
+
via `node:fs/promises`. Each tool resolves its `vault` argument against the configured
|
|
444
|
+
vaults and validates access control (that vault's effective deny list) before executing.
|
|
445
|
+
|
|
446
|
+
`resources/list` and `prompts/list` return empty results — the server does not expose
|
|
447
|
+
vault files as resources or prompts, only as tools.
|
|
448
|
+
|
|
449
|
+
---
|
|
450
|
+
|
|
451
|
+
## Security
|
|
452
|
+
|
|
453
|
+
The OAuth flow is intentionally public — there are no real credentials. Access control
|
|
454
|
+
relies on the network layer (Tailscale node authentication in the reference setup).
|
|
455
|
+
The config file's `denyPaths` feature provides coarse-grained control over which
|
|
456
|
+
parts of a vault the MCP client can touch, but it is not a substitute for
|
|
457
|
+
network-level access control.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "obsidian-mcp-brain",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "MCP server exposing Obsidian vaults to remote clients (Claude Code, Claude Desktop) with OAuth discovery and path-level access control",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "obsidian-mcp-brain.mjs",
|
|
@@ -19,10 +19,11 @@
|
|
|
19
19
|
},
|
|
20
20
|
"keywords": [
|
|
21
21
|
"mcp",
|
|
22
|
+
"mcp-server",
|
|
23
|
+
"model-context-protocol",
|
|
22
24
|
"obsidian",
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
"oauth"
|
|
25
|
+
"obsidian-vault",
|
|
26
|
+
"obsidian-brain"
|
|
26
27
|
],
|
|
27
28
|
"license": "Apache-2.0",
|
|
28
29
|
"author": "a1ecbr0wn",
|