obsidian-mcp-server 3.7.1 → 3.8.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/AGENTS.md +28 -13
- package/CLAUDE.md +28 -13
- package/Dockerfile +7 -0
- package/README.md +43 -99
- package/changelog/3.7.x/3.7.2.md +34 -0
- package/changelog/3.8.x/3.8.0.md +38 -0
- package/dist/config/server-config.d.ts +1 -0
- package/dist/config/server-config.d.ts.map +1 -1
- package/dist/config/server-config.js +24 -15
- package/dist/config/server-config.js.map +1 -1
- package/dist/index.js +6 -1
- package/dist/index.js.map +1 -1
- package/dist/instructions.d.ts.map +1 -1
- package/dist/instructions.js +21 -7
- package/dist/instructions.js.map +1 -1
- package/dist/mcp-server/resources/definitions/index.d.ts +2 -2
- package/dist/mcp-server/resources/definitions/obsidian-tags.resource.d.ts +3 -2
- package/dist/mcp-server/resources/definitions/obsidian-tags.resource.d.ts.map +1 -1
- package/dist/mcp-server/resources/definitions/obsidian-tags.resource.js +6 -5
- package/dist/mcp-server/resources/definitions/obsidian-tags.resource.js.map +1 -1
- package/dist/mcp-server/resources/definitions/obsidian-vault-note.resource.d.ts +2 -2
- package/dist/mcp-server/resources/definitions/obsidian-vault-note.resource.js +2 -2
- package/dist/mcp-server/resources/definitions/obsidian-vault-note.resource.js.map +1 -1
- package/dist/mcp-server/tools/definitions/_shared/markdown.d.ts +14 -0
- package/dist/mcp-server/tools/definitions/_shared/markdown.d.ts.map +1 -0
- package/dist/mcp-server/tools/definitions/_shared/markdown.js +17 -0
- package/dist/mcp-server/tools/definitions/_shared/markdown.js.map +1 -0
- package/dist/mcp-server/tools/definitions/_shared/suggest-paths.d.ts +8 -1
- package/dist/mcp-server/tools/definitions/_shared/suggest-paths.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/_shared/suggest-paths.js +17 -6
- package/dist/mcp-server/tools/definitions/_shared/suggest-paths.js.map +1 -1
- package/dist/mcp-server/tools/definitions/index.d.ts +18 -18
- package/dist/mcp-server/tools/definitions/obsidian-append-to-note.tool.d.ts +2 -2
- package/dist/mcp-server/tools/definitions/obsidian-append-to-note.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/obsidian-append-to-note.tool.js +5 -4
- package/dist/mcp-server/tools/definitions/obsidian-append-to-note.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/obsidian-delete-note.tool.d.ts +7 -6
- package/dist/mcp-server/tools/definitions/obsidian-delete-note.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/obsidian-delete-note.tool.js +9 -8
- package/dist/mcp-server/tools/definitions/obsidian-delete-note.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/obsidian-execute-command.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/obsidian-execute-command.tool.js +2 -1
- package/dist/mcp-server/tools/definitions/obsidian-execute-command.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/obsidian-get-note.tool.d.ts +2 -2
- package/dist/mcp-server/tools/definitions/obsidian-get-note.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/obsidian-get-note.tool.js +38 -28
- package/dist/mcp-server/tools/definitions/obsidian-get-note.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/obsidian-list-commands.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/obsidian-list-commands.tool.js +7 -4
- package/dist/mcp-server/tools/definitions/obsidian-list-commands.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/obsidian-list-notes.tool.d.ts +6 -6
- package/dist/mcp-server/tools/definitions/obsidian-list-notes.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/obsidian-list-notes.tool.js +15 -14
- package/dist/mcp-server/tools/definitions/obsidian-list-notes.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/obsidian-list-tags.tool.d.ts +4 -4
- package/dist/mcp-server/tools/definitions/obsidian-list-tags.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/obsidian-list-tags.tool.js +9 -8
- package/dist/mcp-server/tools/definitions/obsidian-list-tags.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/obsidian-manage-frontmatter.tool.d.ts +2 -2
- package/dist/mcp-server/tools/definitions/obsidian-manage-frontmatter.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/obsidian-manage-frontmatter.tool.js +9 -7
- package/dist/mcp-server/tools/definitions/obsidian-manage-frontmatter.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/obsidian-manage-tags.tool.d.ts +2 -2
- package/dist/mcp-server/tools/definitions/obsidian-manage-tags.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/obsidian-manage-tags.tool.js +7 -5
- package/dist/mcp-server/tools/definitions/obsidian-manage-tags.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/obsidian-open-in-ui.tool.d.ts +2 -2
- package/dist/mcp-server/tools/definitions/obsidian-open-in-ui.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/obsidian-open-in-ui.tool.js +4 -3
- package/dist/mcp-server/tools/definitions/obsidian-open-in-ui.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/obsidian-patch-note.tool.d.ts +2 -2
- package/dist/mcp-server/tools/definitions/obsidian-patch-note.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/obsidian-patch-note.tool.js +5 -4
- package/dist/mcp-server/tools/definitions/obsidian-patch-note.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/obsidian-replace-in-note.tool.d.ts +2 -2
- package/dist/mcp-server/tools/definitions/obsidian-replace-in-note.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/obsidian-replace-in-note.tool.js +10 -4
- package/dist/mcp-server/tools/definitions/obsidian-replace-in-note.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/obsidian-search-notes.tool.d.ts +46 -12
- package/dist/mcp-server/tools/definitions/obsidian-search-notes.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/obsidian-search-notes.tool.js +93 -43
- package/dist/mcp-server/tools/definitions/obsidian-search-notes.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/obsidian-write-note.tool.d.ts +2 -2
- package/dist/mcp-server/tools/definitions/obsidian-write-note.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/obsidian-write-note.tool.js +5 -4
- package/dist/mcp-server/tools/definitions/obsidian-write-note.tool.js.map +1 -1
- package/dist/services/obsidian/jsonlogic-evaluator.d.ts +33 -8
- package/dist/services/obsidian/jsonlogic-evaluator.d.ts.map +1 -1
- package/dist/services/obsidian/jsonlogic-evaluator.js +171 -32
- package/dist/services/obsidian/jsonlogic-evaluator.js.map +1 -1
- package/dist/services/obsidian/jsonlogic-worker.d.ts +6 -5
- package/dist/services/obsidian/jsonlogic-worker.d.ts.map +1 -1
- package/dist/services/obsidian/jsonlogic-worker.js +7 -5
- package/dist/services/obsidian/jsonlogic-worker.js.map +1 -1
- package/dist/services/obsidian/obsidian-service.d.ts +38 -22
- package/dist/services/obsidian/obsidian-service.d.ts.map +1 -1
- package/dist/services/obsidian/obsidian-service.js +293 -99
- package/dist/services/obsidian/obsidian-service.js.map +1 -1
- package/dist/services/obsidian/patch-instruction.d.ts +4 -1
- package/dist/services/obsidian/patch-instruction.d.ts.map +1 -1
- package/dist/services/obsidian/patch-instruction.js +5 -2
- package/dist/services/obsidian/patch-instruction.js.map +1 -1
- package/dist/services/obsidian/path-policy.d.ts +67 -23
- package/dist/services/obsidian/path-policy.d.ts.map +1 -1
- package/dist/services/obsidian/path-policy.js +183 -80
- package/dist/services/obsidian/path-policy.js.map +1 -1
- package/dist/services/obsidian/section-extractor.d.ts +6 -1
- package/dist/services/obsidian/section-extractor.d.ts.map +1 -1
- package/dist/services/obsidian/section-extractor.js +106 -11
- package/dist/services/obsidian/section-extractor.js.map +1 -1
- package/dist/services/obsidian/simple-search.d.ts +62 -0
- package/dist/services/obsidian/simple-search.d.ts.map +1 -0
- package/dist/services/obsidian/simple-search.js +264 -0
- package/dist/services/obsidian/simple-search.js.map +1 -0
- package/manifest.json +10 -2
- package/package.json +8 -7
- package/server.json +19 -7
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
<div align="center">
|
|
9
9
|
|
|
10
|
-
[](./CHANGELOG.md) [](./LICENSE) [](https://github.com/users/cyanheads/packages/container/package/obsidian-mcp-server) [](https://modelcontextprotocol.io/) [](https://www.npmjs.com/package/obsidian-mcp-server) [](https://www.typescriptlang.org/) [](https://bun.sh/)
|
|
11
11
|
|
|
12
12
|
</div>
|
|
13
13
|
|
|
@@ -68,7 +68,7 @@ Note and tag data are also reachable through tools (`obsidian_get_note`, `obsidi
|
|
|
68
68
|
|
|
69
69
|
- Walks from `path` (default vault root) to `depth` 1–20 (default 2), filtered by `extension` and `nameRegex` (≤256 chars); a folder that fails `nameRegex` is not walked
|
|
70
70
|
- Returns `entries[]` (`file` / `directory`), `totals`, and `appliedFilters`; the walk stops at 1,000 entries with `excluded.reason: "entry_cap"`, and a folder the depth limit stopped carries `truncated: true`
|
|
71
|
-
- With `OBSIDIAN_READ_PATHS` set, a listing holds only readable entries and the folders leading to the scope; those folders are walked and accepted as `path
|
|
71
|
+
- With `OBSIDIAN_READ_PATHS` or `OBSIDIAN_DENY_PATHS` set, a listing holds only readable entries and the folders leading to the scope; those folders are walked and accepted as `path`, and a denied folder is absent at every depth
|
|
72
72
|
|
|
73
73
|
---
|
|
74
74
|
|
|
@@ -76,15 +76,15 @@ Note and tag data are also reachable through tools (`obsidian_get_note`, `obsidi
|
|
|
76
76
|
|
|
77
77
|
- `nameRegex` (≤256 chars) and `minCount` narrow the set, then tags are ranked by count and capped at `limit` (default 200, max 10000); hierarchical parents count (`work/tasks` adds to `work`)
|
|
78
78
|
- When the cap withholds tags, the response carries `truncated`, `shown`, and `cap`
|
|
79
|
-
- With `OBSIDIAN_READ_PATHS` set, only tags from readable notes are listed, and `count` is the number of readable notes carrying the tag or a tag nested under it
|
|
79
|
+
- With `OBSIDIAN_READ_PATHS` or `OBSIDIAN_DENY_PATHS` set, only tags from readable notes are listed, and `count` is the number of readable notes carrying the tag or a tag nested under it
|
|
80
80
|
|
|
81
81
|
---
|
|
82
82
|
|
|
83
83
|
### `obsidian_search_notes` <sub>tool</sub>
|
|
84
84
|
|
|
85
|
-
- `mode: "text"` requires every
|
|
85
|
+
- `mode: "text"` requires every space-separated token of `query` as a case-insensitive substring of the filename or body (quotes are literal), shaped by `contextLength` (default 100), `pathPrefix`, and `maxMatchesPerHit` (default 10); `mode: "jsonlogic"` evaluates a `logic` tree over `path`, `content`, `frontmatter.<key>`, `tags`, and `stat` (plus `links`, `backlinks`, and `unresolvedLinks` on plugin versions that provide them, and not under `OBSIDIAN_READ_PATHS` or `OBSIDIAN_DENY_PATHS`), with `glob` / `regexp` taking `[PATTERN, VALUE]`
|
|
86
86
|
- `result.mode` discriminates the payload; every mode reports `totalCount` and pages via `nextCursor`, and a text hit clipped to `maxMatchesPerHit` carries `truncated` and `totalMatches`
|
|
87
|
-
- `mode: "omnisearch"` (BM25 ranking, quoted phrases, `-exclusion`, `path:` / `ext:` filters) is offered only when the Omnisearch plugin answered at startup
|
|
87
|
+
- `mode: "omnisearch"` (BM25 ranking, quoted phrases, `-exclusion`, `path:` / `ext:` filters) is offered only when the Omnisearch plugin answered at startup. It keeps its top 50 hits; `path:`, `ext:`, and plain terms apply before that cap, quoted phrases and `-exclusion` after it. `truncated: true` marks a result the cap may have cut, with `truncatedReason` `cap_reached`, `filtered_after_cap`, or `read_scope`; under `OBSIDIAN_READ_PATHS` or `OBSIDIAN_DENY_PATHS` it is always `true`, and a hit's membership and `score` still depend on the whole vault
|
|
88
88
|
|
|
89
89
|
---
|
|
90
90
|
|
|
@@ -138,7 +138,7 @@ Note and tag data are also reachable through tools (`obsidian_get_note`, `obsidi
|
|
|
138
138
|
|
|
139
139
|
- Takes a `target` (path, active file, or periodic note) and checks it against the write scope first; a folder path fails with `path_is_directory`. There is no API-level undo, only Obsidian's local trash
|
|
140
140
|
- The path must match exactly, letter case included, even on a case-insensitive filesystem. A different-case or extension-less spelling fails `note_missing` before anything is deleted, naming the near matches in `suggestions`
|
|
141
|
-
- By default the note is deleted on the first call, with no confirmation; `OBSIDIAN_WRITE_PATHS` and `OBSIDIAN_READ_ONLY` bound what it can reach
|
|
141
|
+
- By default the note is deleted on the first call, with no confirmation; `OBSIDIAN_WRITE_PATHS`, `OBSIDIAN_DENY_PATHS`, and `OBSIDIAN_READ_ONLY` bound what it can reach
|
|
142
142
|
- With `OBSIDIAN_DELETE_ELICITATION=true`, the first call answers with a confirmation request naming the path and byte size, and the note is deleted only after the user accepts. Declining fails with `cancelled`, and a client without elicitation support cannot delete
|
|
143
143
|
- In that mode an answer counts only against the single-use consent record stored when the prompt was shown, bound to the caller, the path, and the note's content then. A pre-supplied or replayed answer, or one given after the note changed, gets a fresh prompt instead
|
|
144
144
|
- Consent records live in the server's storage provider (`STORAGE_PROVIDER_TYPE`, default `in-memory`, process-local). That works for stdio or a single HTTP instance; several instances behind one endpoint need a shared provider (`filesystem`, `supabase`, or `cloudflare-d1`, never `cloudflare-kv`)
|
|
@@ -178,7 +178,7 @@ Note and tag data are also reachable through tools (`obsidian_get_note`, `obsidi
|
|
|
178
178
|
### `obsidian://tags` <sub>resource</sub>
|
|
179
179
|
|
|
180
180
|
- Every tag with its `count`, uncapped and unsorted, hierarchical parents included
|
|
181
|
-
- With `OBSIDIAN_READ_PATHS` set, only tags from readable notes, counted the same way as `obsidian_list_tags`
|
|
181
|
+
- With `OBSIDIAN_READ_PATHS` or `OBSIDIAN_DENY_PATHS` set, only tags from readable notes, counted the same way as `obsidian_list_tags`
|
|
182
182
|
- No ranking or filters; `obsidian_list_tags` gives the count-ranked, capped view
|
|
183
183
|
|
|
184
184
|
---
|
|
@@ -196,7 +196,7 @@ Obsidian-specific:
|
|
|
196
196
|
|
|
197
197
|
- Typed client for the [Obsidian Local REST API](https://github.com/coddingtonbear/obsidian-local-rest-api) plugin; section writes speak markdown-patch 2.0 to plugin v5.0 and later and 1.x to v4.x, chosen from the reported plugin version
|
|
198
198
|
- Heading targets take a full `Parent::Child` path or a bare leaf name; writes reject an ambiguous one with `ambiguous_section` and its `candidates`, unless exactly one match is a top-level heading. On plugin v5.0 and later, content added to a heading is set off by a blank line (a list item continues an adjacent list), and a heading inside it must sit below the section's level (`heading_outside_section`)
|
|
199
|
-
- Folder-scoped read/write permissions, a read-only switch, and an opt-in command-palette pair (see [Path policy](#path-policy)); server-level `instructions` on `initialize` report the active policy
|
|
199
|
+
- Folder-scoped read/write permissions, a deny list that withholds paths from both, a read-only switch, and an opt-in command-palette pair (see [Path policy](#path-policy)); server-level `instructions` on `initialize` report the active policy
|
|
200
200
|
- `obsidian_get_note` and `obsidian_open_in_ui` retry a case-mismatched path against the real filename, disclosed as `requestedPath` plus a `notice`, and add `Did you mean` suggestions to a miss; writes and deletes match the exact path
|
|
201
201
|
- Regex inputs are capped (`nameRegex` at 256 chars, `useRegex` at 1024) and rejected with `regex_unsafe` when they nest quantifiers
|
|
202
202
|
- Backlinks have no dedicated tool; `obsidian_search_notes` in `jsonlogic` mode finds them with `{"regexp": ["\\[\\[Target Note(\\\\?\\||#|\\]\\])", {"var": "content"}]}`, which also matches the table-safe `[[Target Note\|Alias]]`
|
|
@@ -210,110 +210,37 @@ Agent-friendly output:
|
|
|
210
210
|
|
|
211
211
|
## Getting started
|
|
212
212
|
|
|
213
|
-
Add
|
|
213
|
+
Add it to Claude Code:
|
|
214
214
|
|
|
215
|
-
```
|
|
216
|
-
|
|
217
|
-
"mcpServers": {
|
|
218
|
-
"obsidian-mcp-server": {
|
|
219
|
-
"type": "stdio",
|
|
220
|
-
"command": "bunx",
|
|
221
|
-
"args": ["obsidian-mcp-server@latest"],
|
|
222
|
-
"env": {
|
|
223
|
-
"MCP_TRANSPORT_TYPE": "stdio",
|
|
224
|
-
"MCP_LOG_LEVEL": "info",
|
|
225
|
-
"OBSIDIAN_API_KEY": "your-local-rest-api-key"
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
}
|
|
215
|
+
```sh
|
|
216
|
+
claude mcp add obsidian-mcp-server -e OBSIDIAN_API_KEY=your-api-key -- npx -y obsidian-mcp-server
|
|
230
217
|
```
|
|
231
218
|
|
|
232
|
-
|
|
219
|
+
For other clients, add this to the MCP config file:
|
|
233
220
|
|
|
234
221
|
```json
|
|
235
222
|
{
|
|
236
223
|
"mcpServers": {
|
|
237
224
|
"obsidian-mcp-server": {
|
|
238
|
-
"type": "stdio",
|
|
239
225
|
"command": "npx",
|
|
240
|
-
"args": ["-y", "obsidian-mcp-server
|
|
241
|
-
"env": {
|
|
242
|
-
"MCP_TRANSPORT_TYPE": "stdio",
|
|
243
|
-
"MCP_LOG_LEVEL": "info",
|
|
244
|
-
"OBSIDIAN_API_KEY": "your-local-rest-api-key"
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
```
|
|
250
|
-
|
|
251
|
-
Or with Docker:
|
|
252
|
-
|
|
253
|
-
```json
|
|
254
|
-
{
|
|
255
|
-
"mcpServers": {
|
|
256
|
-
"obsidian-mcp-server": {
|
|
257
|
-
"type": "stdio",
|
|
258
|
-
"command": "docker",
|
|
259
|
-
"args": [
|
|
260
|
-
"run", "-i", "--rm",
|
|
261
|
-
"-e", "MCP_TRANSPORT_TYPE=stdio",
|
|
262
|
-
"-e", "MCP_LOG_LEVEL=info",
|
|
263
|
-
"-e", "OBSIDIAN_API_KEY=your-local-rest-api-key",
|
|
264
|
-
"ghcr.io/cyanheads/obsidian-mcp-server:latest"
|
|
265
|
-
]
|
|
226
|
+
"args": ["-y", "obsidian-mcp-server"],
|
|
227
|
+
"env": { "OBSIDIAN_API_KEY": "your-api-key" }
|
|
266
228
|
}
|
|
267
229
|
}
|
|
268
230
|
}
|
|
269
231
|
```
|
|
270
232
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
For Streamable HTTP, set the transport and start the server:
|
|
274
|
-
|
|
275
|
-
```sh
|
|
276
|
-
MCP_TRANSPORT_TYPE=http OBSIDIAN_API_KEY=... bun run start:http
|
|
277
|
-
# Server listens at http://127.0.0.1:3010/mcp by default
|
|
278
|
-
```
|
|
233
|
+
Generate the key in Obsidian under **Settings → Community Plugins → Local REST API**, with the plugin set up as [Prerequisites](#prerequisites) describes. For Docker or a source build, see [Running the server](#running-the-server).
|
|
279
234
|
|
|
280
235
|
### Prerequisites
|
|
281
236
|
|
|
282
|
-
- [
|
|
283
|
-
- The [Obsidian Local REST API](https://github.com/coddingtonbear/obsidian-local-rest-api) plugin, v4.0.0 or later, enabled in your vault. Generate an API key under **Settings → Community Plugins → Local REST API** and set it as `OBSIDIAN_API_KEY`.
|
|
237
|
+
- The [Obsidian Local REST API](https://github.com/coddingtonbear/obsidian-local-rest-api) plugin, v4.0.0 or later, enabled in your vault. Its API key is `OBSIDIAN_API_KEY`.
|
|
284
238
|
- The server defaults to `http://127.0.0.1:27123`, so enable **"Non-encrypted (HTTP) Server"** in the plugin settings, or set `OBSIDIAN_BASE_URL=https://127.0.0.1:27124` for the always-on HTTPS port (its self-signed cert is accepted while `OBSIDIAN_VERIFY_SSL=false`, the default).
|
|
285
239
|
- Periodic-note targets work natively on plugin v5.0.1 and earlier. From v5.0.2 they need the [periodic-notes API extension](https://github.com/coddingtonbear/obsidian-local-rest-api-periodic-notes); without it they fail with `periodic_unsupported`.
|
|
286
240
|
- On plugin v4.x, heading section writes to a note with CRLF (Windows) line endings land mid-line: the plugin's markdown-patch 1.x engine misplaces them, and the server can't correct it. Plugin v5.0 and later handle CRLF notes.
|
|
287
241
|
- Plugin v6.0 drops markdown-patch 1.x, which two table-row writes (`contentType: "json"`) still use: rows under a heading, and rows through a block ID on its own line below the table. On v6.0, target the table by an ID on its last row.
|
|
288
242
|
- With `OBSIDIAN_DELETE_ELICITATION=true`, an MCP client that supports elicitation and renders its form, to use `obsidian_delete_note`. Every other tool, and the default delete, works without it.
|
|
289
243
|
|
|
290
|
-
### Installation
|
|
291
|
-
|
|
292
|
-
1. **Clone the repository:**
|
|
293
|
-
|
|
294
|
-
```sh
|
|
295
|
-
git clone https://github.com/cyanheads/obsidian-mcp-server.git
|
|
296
|
-
```
|
|
297
|
-
|
|
298
|
-
2. **Navigate into the directory:**
|
|
299
|
-
|
|
300
|
-
```sh
|
|
301
|
-
cd obsidian-mcp-server
|
|
302
|
-
```
|
|
303
|
-
|
|
304
|
-
3. **Install dependencies:**
|
|
305
|
-
|
|
306
|
-
```sh
|
|
307
|
-
bun install
|
|
308
|
-
```
|
|
309
|
-
|
|
310
|
-
4. **Configure environment:**
|
|
311
|
-
|
|
312
|
-
```sh
|
|
313
|
-
cp .env.example .env
|
|
314
|
-
# edit .env and set OBSIDIAN_API_KEY
|
|
315
|
-
```
|
|
316
|
-
|
|
317
244
|
## Configuration
|
|
318
245
|
|
|
319
246
|
| Variable | Description | Default |
|
|
@@ -325,6 +252,7 @@ MCP_TRANSPORT_TYPE=http OBSIDIAN_API_KEY=... bun run start:http
|
|
|
325
252
|
| `OBSIDIAN_ENABLE_COMMANDS` | Enable `obsidian_list_commands` and `obsidian_execute_command`. Commands are opaque and can be destructive. | `false` |
|
|
326
253
|
| `OBSIDIAN_READ_PATHS` | Comma-separated folder allowlist for reads. See [Path policy](#path-policy). | unset (full vault) |
|
|
327
254
|
| `OBSIDIAN_WRITE_PATHS` | Comma-separated folder allowlist for writes. See [Path policy](#path-policy). | unset (the read scope, or the full vault without one) |
|
|
255
|
+
| `OBSIDIAN_DENY_PATHS` | Comma-separated paths withheld from reads and writes alike, overriding both allowlists. See [Path policy](#path-policy). | unset (nothing denied) |
|
|
328
256
|
| `OBSIDIAN_READ_ONLY` | Deny every write and disable the command-palette pair. | `false` |
|
|
329
257
|
| `OBSIDIAN_DELETE_ELICITATION` | Ask the user to confirm each `obsidian_delete_note` call through an elicitation form before deleting. Needs a client that renders elicitation, and a stateful session over HTTP (not required when `OBSIDIAN_READ_ONLY=true` disables the tool). Off, the delete runs on the first call. | `false` |
|
|
330
258
|
| `OBSIDIAN_OMNISEARCH_URL` | [Omnisearch](https://github.com/scambier/obsidian-omnisearch) HTTP server URL. Unset derives from the `OBSIDIAN_BASE_URL` host on port `51361`. Probed once at startup; the `omnisearch` search mode appears only if it answers. | derived |
|
|
@@ -340,25 +268,42 @@ See [`.env.example`](./.env.example) for the full list of optional overrides.
|
|
|
340
268
|
|
|
341
269
|
### Path policy
|
|
342
270
|
|
|
343
|
-
|
|
271
|
+
Four optional env vars limit which vault paths the tools can touch. Unset, reads and writes cover the full vault.
|
|
344
272
|
|
|
345
273
|
| Goal | Config |
|
|
346
274
|
|:---|:---|
|
|
347
275
|
| Read everywhere, write only in `projects/` and `scratch/` | `OBSIDIAN_WRITE_PATHS=projects/,scratch/` |
|
|
348
276
|
| Read only `public/`, write only `public/inbox/` | `OBSIDIAN_READ_PATHS=public/`, `OBSIDIAN_WRITE_PATHS=public/inbox/` |
|
|
277
|
+
| Read and write everywhere except `private/` | `OBSIDIAN_DENY_PATHS=private/` |
|
|
278
|
+
| Read and write only `projects/`, except `projects/secret/` | `OBSIDIAN_READ_PATHS=projects/`, `OBSIDIAN_DENY_PATHS=projects/secret/` |
|
|
349
279
|
| No writes anywhere | `OBSIDIAN_READ_ONLY=true` |
|
|
350
280
|
|
|
351
|
-
- Matching is by prefix, recursive, and case-insensitive
|
|
281
|
+
- Matching is by prefix, recursive, and case-insensitive. Entries are normalized the way a path reaches the plugin: `\` becomes `/`, repeated and trailing slashes collapse, `.` segments drop, and names are compared in Unicode NFC, so `journal//therapy` and `./private` cover the folders they name. Request paths get the same slash, case, and NFC folding, so `notes//private/x.md` or an NFD spelling of a denied name is refused like the plain one. An entry naming the vault root (`.`, `./`) fails startup. Write paths are also readable.
|
|
282
|
+
- `OBSIDIAN_DENY_PATHS` wins over both allowlists, write-implies-read included: a denied path is neither readable nor writable, and it is left out of listings, search hits, and tag counts. An allow entry inside a deny entry grants nothing, and the startup log warns about it.
|
|
352
283
|
- `OBSIDIAN_READ_ONLY=true` removes every write tool and the command-palette pair from `tools/list`, including `obsidian_manage_frontmatter` and `obsidian_manage_tags` (so their `get` / `list` go too). `obsidian_open_in_ui` still opens existing files but won't create one.
|
|
353
|
-
- A denial fails with `path_forbidden`, echoing the active scope in `data.activeScope` and the recovery hint. A denial on an `active` or `periodic` target names the target (`(active file)`, `daily note for 2026-10-01`), not the note it resolved to. `obsidian_list_notes` lists only readable entries plus the folders leading to the scope, so a nested scope can be browsed to from the root.
|
|
354
|
-
- Under `OBSIDIAN_READ_PATHS`, searches return readable notes only
|
|
355
|
-
-
|
|
356
|
-
-
|
|
284
|
+
- A denial fails with `path_forbidden`, echoing the active scope in `data.activeScope` and the recovery hint. A path the deny list withholds fails with subreason `denied_path` instead, which carries no `activeScope` and names `OBSIDIAN_DENY_PATHS` but none of its entries; a path outside an allowlist reports `outside_read_paths` or `outside_write_paths` even when it is also denied. A denial on an `active` or `periodic` target names the target (`(active file)`, `daily note for 2026-10-01`), not the note it resolved to. `obsidian_list_notes` lists only readable entries plus the folders leading to the scope, so a nested scope can be browsed to from the root; a denied folder never appears, even on the way to an allow entry.
|
|
285
|
+
- Under `OBSIDIAN_READ_PATHS` or `OBSIDIAN_DENY_PATHS`, searches return readable notes only. With neither set, a JSONLogic tree goes to the plugin unchanged; with either:
|
|
286
|
+
- JSONLogic: the server evaluates the tree over the readable notes, so it never reads a note outside the scope. Notes carry no `links`, `backlinks`, or `unresolvedLinks`, since Obsidian resolves a link across the whole vault: a tree naming one by a fixed `var`, `missing`, or `missing_some` key fails with `link_fields_unavailable`, and a computed key reads `null`. Match link text in `content` instead. A tree that names `content` reads each readable note with one request per note, at most 32 in flight across the server.
|
|
287
|
+
- Evaluation is bounded (1,000 levels of nesting, a work budget per note and per search, 10 s of evaluation, 45 s for the whole search); a tree past a bound fails with `logic_too_costly`. A readable note nested too deep for the evaluator (about 3,500 levels under Node) fails the search with `note_too_deep`.
|
|
288
|
+
- Text: the server never calls the plugin's text search. It asks the plugin which readable notes may hold every token, reads each candidate note, and applies Obsidian's search rule itself, so hits and context windows match the unscoped search. Past 45 s it fails with `text_search_too_costly`, and `context_length_too_large` fails where the plugin's response for the readable matches alone would pass its size ceiling.
|
|
289
|
+
- Omnisearch ranks the whole vault before the scope filters it (see `obsidian_search_notes`).
|
|
290
|
+
- Tag listings (`obsidian_list_tags`, `obsidian://tags`) under `OBSIDIAN_READ_PATHS` or `OBSIDIAN_DENY_PATHS` cover readable notes only, counting notes rather than occurrences. Write paths and read-only alone leave them vault-wide.
|
|
291
|
+
- The server `instructions` state the effective scope: readable is the read paths plus the write paths, and writable is the write paths, or the read scope when only reads are restricted. With a deny list set, each scope reads "…, except paths the operator withholds", naming none of them, and an empty scope reads `none`. The startup log prints the configured lists, the deny list included.
|
|
357
292
|
|
|
358
293
|
## Running the server
|
|
359
294
|
|
|
360
295
|
### Local development
|
|
361
296
|
|
|
297
|
+
Requires [Bun v1.4.0](https://bun.sh/) or higher (or Node.js v24+).
|
|
298
|
+
|
|
299
|
+
- **Clone and install:**
|
|
300
|
+
|
|
301
|
+
```sh
|
|
302
|
+
git clone https://github.com/cyanheads/obsidian-mcp-server.git
|
|
303
|
+
cd obsidian-mcp-server && bun install
|
|
304
|
+
cp .env.example .env # set OBSIDIAN_API_KEY
|
|
305
|
+
```
|
|
306
|
+
|
|
362
307
|
- **Build and run the production version:**
|
|
363
308
|
|
|
364
309
|
```sh
|
|
@@ -367,7 +312,7 @@ Three optional env vars limit which vault paths the tools can touch. Unset, read
|
|
|
367
312
|
|
|
368
313
|
# Run the built server
|
|
369
314
|
bun run start:stdio
|
|
370
|
-
# or
|
|
315
|
+
# or, listening at http://127.0.0.1:3010/mcp
|
|
371
316
|
bun run start:http
|
|
372
317
|
```
|
|
373
318
|
|
|
@@ -382,11 +327,10 @@ Three optional env vars limit which vault paths the tools can touch. Unset, read
|
|
|
382
327
|
### Docker
|
|
383
328
|
|
|
384
329
|
```sh
|
|
385
|
-
docker
|
|
386
|
-
docker run --rm -e OBSIDIAN_API_KEY=your-key -p 3010:3010 obsidian-mcp-server
|
|
330
|
+
docker run --rm -e OBSIDIAN_API_KEY=your-key -p 3010:3010 ghcr.io/cyanheads/obsidian-mcp-server:latest
|
|
387
331
|
```
|
|
388
332
|
|
|
389
|
-
The image defaults to HTTP transport on `0.0.0.0`, stateful sessions, and logs in `/var/log/obsidian-mcp-server`.
|
|
333
|
+
The image defaults to HTTP transport on `0.0.0.0`, stateful sessions, and logs in `/var/log/obsidian-mcp-server`. Inside a container, the default `OBSIDIAN_BASE_URL` (`http://127.0.0.1:27123`) is the container's own loopback: point it at `http://host.docker.internal:27123` (Docker Desktop) or use `--network host` (Linux) to reach the plugin on your host. To run it over stdio from an MCP client, use `"command": "docker"` with `"args": ["run", "-i", "--rm", "-e", "MCP_TRANSPORT_TYPE=stdio", "-e", "OBSIDIAN_API_KEY=your-key", "-e", "OBSIDIAN_BASE_URL=http://host.docker.internal:27123", "ghcr.io/cyanheads/obsidian-mcp-server:latest"]`. To build locally, `docker build -t obsidian-mcp-server .`; OpenTelemetry peer dependencies are installed by default — build with `--build-arg OTEL_ENABLED=false` to omit them.
|
|
390
334
|
|
|
391
335
|
If the port is reachable from other machines, set `MCP_AUTH_MODE=jwt` (with `MCP_AUTH_SECRET_KEY`) or `oauth`. With the default `none`, every caller acts on your vault with your `OBSIDIAN_API_KEY`.
|
|
392
336
|
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
---
|
|
2
|
+
summary: "Vault text in content[] (paths, tags, headings, frontmatter keys and values, command names, search terms) reads back as written under a CommonMark parse. Adopts mcp-ts-core ^0.13.18, which escapes error text and renders enrichment trailer strings as code spans."
|
|
3
|
+
breaking: false
|
|
4
|
+
security: false
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# 3.7.2 — 2026-10-25
|
|
8
|
+
|
|
9
|
+
## Changed
|
|
10
|
+
|
|
11
|
+
- **`obsidian_get_note` `format: "full"` renders a non-string frontmatter value as compact JSON in a code span** (before: pretty-printed multi-line JSON inside the list item).
|
|
12
|
+
- **`obsidian_get_note` `format: "section"` on a `frontmatter` target renders a string value escaped and a non-string value as a fenced `json` block** (before: raw). Heading and block section values and note bodies still render as raw Markdown.
|
|
13
|
+
- **`obsidian_search_notes` empty-result notices show the query and path prefix as code spans** (before: in double quotes, raw). The `obsidian_list_notes` tree fence and the search result fences are sized by the framework's code-block builder.
|
|
14
|
+
- **Framework `@cyanheads/mcp-ts-core` ^0.13.15 → ^0.13.18:**
|
|
15
|
+
- Tool error `content[]` escapes the message, recovery hint, and reason; `structuredContent` is unchanged (0.13.16, cyanheads/mcp-ts-core#760).
|
|
16
|
+
- Enrichment trailer strings such as `requestedPath` and the search `echo` render as code spans, or a fenced block when they hold a line break (0.13.18, cyanheads/mcp-ts-core#785); every line of a multi-line notice stays inside the trailer blockquote (0.13.17, cyanheads/mcp-ts-core#765).
|
|
17
|
+
- A call whose handler returns after the request was cancelled settles as `RequestCancelled` (`-32011`) (0.13.16, cyanheads/mcp-ts-core#753).
|
|
18
|
+
- HTTP shutdown answers `503` with `Retry-After: 1` while draining and aborts in-flight requests instead of waiting out the drain (0.13.16, cyanheads/mcp-ts-core#749, cyanheads/mcp-ts-core#757).
|
|
19
|
+
- Out-of-range framework environment values (a non-integer count, an out-of-range timer) fail startup, and the error names the variable (0.13.16, cyanheads/mcp-ts-core#747, cyanheads/mcp-ts-core#746).
|
|
20
|
+
- CORS exposes `WWW-Authenticate` and `Retry-After` (0.13.16, cyanheads/mcp-ts-core#687); log redaction catches secret names run into another word (0.13.16, cyanheads/mcp-ts-core#702).
|
|
21
|
+
- The `.mcpb` bundle drops the framework's build-only files (its `changelog/`, `templates/`, `scripts/`, `CLAUDE.md`, and similar) (0.13.18, cyanheads/mcp-ts-core#791).
|
|
22
|
+
- MCP SDK `@modelcontextprotocol/server` ^2.3.1 → ^2.4.0 (0.13.17).
|
|
23
|
+
- **Packaging and repo:** root Agent Plugins 1.0.0 `plugin.json` and `mcp.json`; `.mcpbignore` anchors `/changelog/`, `/plugin.json`, `/mcp.json`; `package.json` `files` and the Dockerfile carry a root `skills/` directory per the framework template (this server publishes no skills, so nothing new ships); the Docker image sets `MCP_SESSION_MODE="stateful"` explicitly (already the default from `createApp`, so behavior is unchanged); README Getting started is condensed to a `claude mcp add` line and one `npx` config; issue forms use the template's Environment table; `bunfig.toml` `minimumReleaseAge` 3 days → 1 day; framework skills and scripts synced (new `add-skill`).
|
|
24
|
+
|
|
25
|
+
## Fixed
|
|
26
|
+
|
|
27
|
+
- **Every tool's `content[]` renders text it did not author through `escapeMarkdownInline` or a code span sized past the value's longest backtick run.** Paths, tags, headings, block IDs, frontmatter keys and values, link targets, command IDs and names, regexes, search terms, and cursors read back under a CommonMark parse as the value `structuredContent` carries; before, `_`, `*`, `[`, `]`, `|`, and backticks in vault names could render as emphasis, links, or a broken code span. `structuredContent`, `tools/list`, and error contracts are unchanged.
|
|
28
|
+
|
|
29
|
+
## Dependencies
|
|
30
|
+
|
|
31
|
+
- `@cyanheads/mcp-ts-core` ^0.13.15 → ^0.13.18
|
|
32
|
+
- `@biomejs/biome` ^2.6.2 → ^2.6.3 (dev)
|
|
33
|
+
- `ignore` ^7.0.13 → ^7.0.14 (dev)
|
|
34
|
+
- `tsc-alias` ^1.9.8 → ^1.9.9 (dev)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
---
|
|
2
|
+
summary: "OBSIDIAN_DENY_PATHS withholds paths from reads and writes. Under a read scope, text and JSONLogic searches now read only readable notes, so out-of-scope notes no longer change their errors or results; a scoped JSONLogic tree naming link fields by a fixed var, missing, or missing_some key is rejected. Fixes the .mcpb bundle failing to boot."
|
|
3
|
+
breaking: true
|
|
4
|
+
security: true
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# 3.8.0 — 2026-10-30
|
|
8
|
+
|
|
9
|
+
## Added
|
|
10
|
+
|
|
11
|
+
- **`OBSIDIAN_DENY_PATHS`** ([#76](https://github.com/cyanheads/obsidian-mcp-server/issues/76)): a deny list checked before the allow lists on reads and writes, so it wins over `OBSIDIAN_WRITE_PATHS`' write-implies-read. Denied paths drop out of listings, search results, and tag counts; the `path_forbidden` `denied_path` denial and the server instructions never echo the entries.
|
|
12
|
+
- **`obsidian_search_notes` omnisearch output field `truncatedReason`** (`cap_reached | filtered_after_cap | read_scope`), present when `truncated` is true ([#172](https://github.com/cyanheads/obsidian-mcp-server/issues/172)).
|
|
13
|
+
|
|
14
|
+
## Changed
|
|
15
|
+
|
|
16
|
+
- **`obsidian_search_notes` `jsonlogic` under `OBSIDIAN_READ_PATHS` or `OBSIDIAN_DENY_PATHS` rejects a tree that names `links`, `backlinks`, or `unresolvedLinks` by a fixed `var`, `missing`, or `missing_some` key**, with `link_fields_unavailable` and no upstream request: scoped note contexts no longer carry link fields, because Obsidian resolves them against the whole vault. A computed key reads `null`. Unscoped searches are unchanged. ([#165](https://github.com/cyanheads/obsidian-mcp-server/issues/165))
|
|
17
|
+
- **Omnisearch `truncated` is also true when quoted or `-exclusion` terms filter after the 50-hit cap**, and always true under a read scope ([#172](https://github.com/cyanheads/obsidian-mcp-server/issues/172)).
|
|
18
|
+
- **Unscoped `text` queries split on spaces, as Obsidian does.** A tab or newline stays inside its token, so a query carrying whitespace-only tokens can answer differently.
|
|
19
|
+
- **`OBSIDIAN_READ_PATHS` / `OBSIDIAN_WRITE_PATHS` entries are normalized** (backslashes as slashes, repeated slashes and `.` segments dropped, Unicode NFC), and `.` or `./` entries now fail startup ([#76](https://github.com/cyanheads/obsidian-mcp-server/issues/76)).
|
|
20
|
+
- **Scoped `jsonlogic` that reads note content issues one `GET` per readable note**, at most 32 in flight across the server, in batches of at most 32 MiB. Each search gets one evaluation worker and a 45 s deadline (`logic_too_costly`). ([#167](https://github.com/cyanheads/obsidian-mcp-server/issues/167))
|
|
21
|
+
- **Scoped `text` search runs under a 45 s deadline (`text_search_too_costly`)**, and its `context_length_too_large` fails exactly where the plugin's response for the readable matches alone would pass V8's string ceiling. Each match costs its window plus about 150 characters of framing, so a very common token fails even at `contextLength` 0.
|
|
22
|
+
|
|
23
|
+
## Fixed
|
|
24
|
+
|
|
25
|
+
- **The section scan no longer overflows the stack, or goes quadratic, on deeply nested lists or lazy blockquotes** ([#164](https://github.com/cyanheads/obsidian-mcp-server/issues/164)).
|
|
26
|
+
- **A duplicate link reference definition no longer shifts later heading offsets in section reads** ([#171](https://github.com/cyanheads/obsidian-mcp-server/issues/171)).
|
|
27
|
+
- **`obsidian_get_note` puts a blank line between the `**Content**` / `**Value:**` label and the raw body** ([#169](https://github.com/cyanheads/obsidian-mcp-server/issues/169)).
|
|
28
|
+
- **End-anchored trims in `canonicalContent`, `extractHeading`, the path slash trims, and the not-found suggestion builder run in linear time** ([#174](https://github.com/cyanheads/obsidian-mcp-server/issues/174)).
|
|
29
|
+
- **The `.mcpb` bundle boots again.** `scripts/clean-mcpb.ts` no longer strips the MCP SDK's `dist/ext/skills/`, which the server imports at startup, and `scripts/lint-packaging.ts` no longer flags it; the published 3.7.2 asset fails to boot without it.
|
|
30
|
+
|
|
31
|
+
## Security
|
|
32
|
+
|
|
33
|
+
- **Under `OBSIDIAN_READ_PATHS`, text-mode errors, omnisearch truncation, and JSONLogic link fields varied with notes outside the scope** ([#163](https://github.com/cyanheads/obsidian-mcp-server/issues/163), [#165](https://github.com/cyanheads/obsidian-mcp-server/issues/165)). Scoped text search now runs in-server (`src/services/obsidian/simple-search.ts`, a port of the plugin's matching) with no `/search/simple/` request; scoped omnisearch is always `truncated`, and scoped JSONLogic notes carry no link fields.
|
|
34
|
+
|
|
35
|
+
## Dependencies
|
|
36
|
+
|
|
37
|
+
- `@biomejs/biome` ^2.6.3 → ^2.6.4 (dev)
|
|
38
|
+
- `@types/node` ^26.6.5 → ^26.6.6 (dev)
|
|
@@ -12,6 +12,7 @@ declare const ServerConfigSchema: z.ZodObject<{
|
|
|
12
12
|
enableCommands: z.ZodDefault<z.ZodUnion<readonly [z.ZodBoolean, z.ZodCodec<z.ZodString, z.ZodBoolean>]>>;
|
|
13
13
|
readPaths: z.ZodPreprocess<z.ZodPipe<z.ZodOptional<z.ZodArray<z.ZodString>>, z.ZodTransform<string[] | undefined, string[] | undefined>>, unknown>;
|
|
14
14
|
writePaths: z.ZodPreprocess<z.ZodPipe<z.ZodOptional<z.ZodArray<z.ZodString>>, z.ZodTransform<string[] | undefined, string[] | undefined>>, unknown>;
|
|
15
|
+
denyPaths: z.ZodPreprocess<z.ZodPipe<z.ZodOptional<z.ZodArray<z.ZodString>>, z.ZodTransform<string[] | undefined, string[] | undefined>>, unknown>;
|
|
15
16
|
readOnly: z.ZodDefault<z.ZodUnion<readonly [z.ZodBoolean, z.ZodCodec<z.ZodString, z.ZodBoolean>]>>;
|
|
16
17
|
deleteElicitation: z.ZodDefault<z.ZodUnion<readonly [z.ZodBoolean, z.ZodCodec<z.ZodString, z.ZodBoolean>]>>;
|
|
17
18
|
omnisearchUrl: z.ZodOptional<z.ZodString>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server-config.d.ts","sourceRoot":"","sources":["../../src/config/server-config.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"server-config.d.ts","sourceRoot":"","sources":["../../src/config/server-config.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,wBAAwB,CAAC;AAwF3C,QAAA,MAAM,kBAAkB;;;;;;;;;;;;iBA+DtB,CAAC;AAEH,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAI9D,wBAAgB,eAAe,IAAI,YAAY,CAe9C;AAED,iEAAiE;AACjE,wBAAgB,iBAAiB,IAAI,IAAI,CAExC"}
|
|
@@ -12,18 +12,20 @@ import { parseEnvConfig } from '@cyanheads/mcp-ts-core/config';
|
|
|
12
12
|
*/
|
|
13
13
|
const envBoolean = z.union([z.boolean(), z.stringbool()]);
|
|
14
14
|
/**
|
|
15
|
-
* Comma-separated path-list preprocessor
|
|
16
|
-
*
|
|
15
|
+
* Comma-separated path-list preprocessor, shared by OBSIDIAN_READ_PATHS,
|
|
16
|
+
* OBSIDIAN_WRITE_PATHS, and OBSIDIAN_DENY_PATHS. Semantics (see issues #40, #76):
|
|
17
|
+
* - undefined / `''` / whitespace-only → undefined (treat as unset)
|
|
17
18
|
* - `,` / `,,,` (separators only, no path content) → throw ZodError → ConfigurationError
|
|
18
19
|
* - mixed empties (`a,,b`) → drop empties → `['a', 'b']`
|
|
19
|
-
* - absolute path / `..` traversal → throw
|
|
20
|
-
* - valid: `\` →
|
|
20
|
+
* - absolute path / `..` traversal / an entry naming the vault root (`.`, `./`) → throw
|
|
21
|
+
* - valid: `\` → `/`, empty and `.` segments dropped, lower-case, NFC, dedupe
|
|
22
|
+
* (preserves first occurrence order)
|
|
21
23
|
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
24
|
+
* The stored form is `PathPolicy.normalize()`'s — the form a vault path
|
|
25
|
+
* reaches the plugin in — so a prefix and a candidate compare directly. A
|
|
26
|
+
* prefix kept as written (`foo\bar`, `./private`, `journal//therapy`) would
|
|
27
|
+
* match no vault path: a refusal for an allowlist, and a deny entry that
|
|
28
|
+
* silently denies nothing.
|
|
27
29
|
*/
|
|
28
30
|
const envPathList = z
|
|
29
31
|
.preprocess((val) => {
|
|
@@ -63,7 +65,7 @@ const envPathList = z
|
|
|
63
65
|
});
|
|
64
66
|
return z.NEVER;
|
|
65
67
|
}
|
|
66
|
-
const segments = raw.split(/[\\/]/);
|
|
68
|
+
const segments = raw.split(/[\\/]/).filter((s) => s !== '' && s !== '.');
|
|
67
69
|
if (segments.includes('..')) {
|
|
68
70
|
ctx.addIssue({
|
|
69
71
|
code: 'custom',
|
|
@@ -71,9 +73,14 @@ const envPathList = z
|
|
|
71
73
|
});
|
|
72
74
|
return z.NEVER;
|
|
73
75
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
76
|
+
if (segments.length === 0) {
|
|
77
|
+
ctx.addIssue({
|
|
78
|
+
code: 'custom',
|
|
79
|
+
message: `must name a folder or note, not the vault root; got '${raw}'`,
|
|
80
|
+
});
|
|
81
|
+
return z.NEVER;
|
|
82
|
+
}
|
|
83
|
+
const normalized = segments.join('/').toLowerCase().normalize('NFC');
|
|
77
84
|
if (seen.has(normalized))
|
|
78
85
|
continue;
|
|
79
86
|
seen.add(normalized);
|
|
@@ -111,8 +118,9 @@ const ServerConfigSchema = z.object({
|
|
|
111
118
|
enableCommands: envBoolean
|
|
112
119
|
.default(false)
|
|
113
120
|
.describe('Opt-in flag for the command-palette pair (`obsidian_list_commands` + `obsidian_execute_command`). Off by default — Obsidian commands are opaque and can be destructive.'),
|
|
114
|
-
readPaths: envPathList.describe('Optional vault-relative folder allowlist for read operations. Comma-separated; prefix-based with implicit recursion; case-insensitive; trailing slashes normalized. Unset = full vault.'),
|
|
115
|
-
writePaths: envPathList.describe('Optional vault-relative folder allowlist for write operations. Same syntax as OBSIDIAN_READ_PATHS. Write paths are implicitly readable. Unset = full vault, or the read scope when reads are restricted.'),
|
|
121
|
+
readPaths: envPathList.describe('Optional vault-relative folder allowlist for read operations. Comma-separated; prefix-based with implicit recursion; case-insensitive; backslashes read as slashes, repeated and trailing slashes and `.` segments dropped, and names normalized to Unicode NFC; an entry naming the vault root (`.`) is rejected at startup. Unset = full vault.'),
|
|
122
|
+
writePaths: envPathList.describe('Optional vault-relative folder allowlist for write operations. Same syntax as OBSIDIAN_READ_PATHS. Write paths are implicitly readable. Unset = full vault, or the read scope when reads are restricted; paths in OBSIDIAN_DENY_PATHS stay denied.'),
|
|
123
|
+
denyPaths: envPathList.describe('Optional vault-relative blocklist for reads and writes alike. Same syntax as OBSIDIAN_READ_PATHS. Wins over both allowlists: a denied path is unreadable and unwritable, and absent from listings, search results, and tag counts. Unset = nothing denied.'),
|
|
116
124
|
readOnly: envBoolean
|
|
117
125
|
.default(false)
|
|
118
126
|
.describe('Global kill switch. When true, denies every write regardless of OBSIDIAN_WRITE_PATHS, and suppresses the OBSIDIAN_ENABLE_COMMANDS pair (commands can mutate). Defaults to false.'),
|
|
@@ -135,6 +143,7 @@ export function getServerConfig() {
|
|
|
135
143
|
enableCommands: 'OBSIDIAN_ENABLE_COMMANDS',
|
|
136
144
|
readPaths: 'OBSIDIAN_READ_PATHS',
|
|
137
145
|
writePaths: 'OBSIDIAN_WRITE_PATHS',
|
|
146
|
+
denyPaths: 'OBSIDIAN_DENY_PATHS',
|
|
138
147
|
readOnly: 'OBSIDIAN_READ_ONLY',
|
|
139
148
|
deleteElicitation: 'OBSIDIAN_DELETE_ELICITATION',
|
|
140
149
|
omnisearchUrl: 'OBSIDIAN_OMNISEARCH_URL',
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server-config.js","sourceRoot":"","sources":["../../src/config/server-config.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,wBAAwB,CAAC;AAC3C,OAAO,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AAE/D;;;;GAIG;AACH,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC;AAE1D
|
|
1
|
+
{"version":3,"file":"server-config.js","sourceRoot":"","sources":["../../src/config/server-config.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -92,7 +92,9 @@ const { services } = await createApp({
|
|
|
92
92
|
* the tool is disabled there). The server `instructions` and the active scope
|
|
93
93
|
* on `path_forbidden` errors state the effective scope instead (write paths
|
|
94
94
|
* are readable; a write needs read access), so the LLM (or operator) can
|
|
95
|
-
* self-correct without scrolling the log.
|
|
95
|
+
* self-correct without scrolling the log. The deny list (`denyPaths`, present
|
|
96
|
+
* when set) appears only here: the agent-facing surfaces say paths are
|
|
97
|
+
* withheld without naming them.
|
|
96
98
|
*/
|
|
97
99
|
const bannerCtx = requestContextService.createRequestContext({
|
|
98
100
|
operation: 'startup',
|
|
@@ -111,4 +113,7 @@ services.logger.info(omnisearchReachable
|
|
|
111
113
|
if (policy.readOnlyShadowsWritePaths) {
|
|
112
114
|
services.logger.warning('OBSIDIAN_WRITE_PATHS is set but ignored because OBSIDIAN_READ_ONLY=true. Unset one of the two to remove the conflict.', bannerCtx);
|
|
113
115
|
}
|
|
116
|
+
for (const { variable, prefix } of policy.denyShadowedPaths) {
|
|
117
|
+
services.logger.warning(`${variable} entry '${prefix}' lies inside OBSIDIAN_DENY_PATHS and grants nothing. Remove it, or narrow the deny entry that covers it.`, bannerCtx);
|
|
118
|
+
}
|
|
114
119
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"instructions.d.ts","sourceRoot":"","sources":["../src/instructions.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oCAAoC,CAAC;
|
|
1
|
+
{"version":3,"file":"instructions.d.ts","sourceRoot":"","sources":["../src/instructions.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oCAAoC,CAAC;AAsBrE;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE;IACtC,cAAc,EAAE,OAAO,CAAC;IACxB,mBAAmB,EAAE,OAAO,CAAC;IAC7B,MAAM,EAAE,UAAU,CAAC;IACnB,QAAQ,EAAE,OAAO,CAAC;CACnB,GAAG,MAAM,CAmCT"}
|
package/dist/instructions.js
CHANGED
|
@@ -7,9 +7,18 @@
|
|
|
7
7
|
*/
|
|
8
8
|
const BASELINE = 'Use the `obsidian_*` tools to access the Obsidian vault via the Local REST API plugin: search, read, write, and patch notes, including targeted edits to headings, blocks, and YAML frontmatter. Notes are addressed by vault-relative path including the file extension (e.g. `Folder/Note.md`); tags support hierarchical `parent/child` notation, and counts roll up to parents.';
|
|
9
9
|
const SCOPE_RECOVERY = 'Paths outside scope reject with `path_forbidden` — error data carries the active scope so you can self-correct.';
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
const WITHHELD_RECOVERY = 'Withheld paths reject with subreason `denied_path` and carry no scope.';
|
|
11
|
+
/**
|
|
12
|
+
* Prefixes as the instructions quote them; `undefined` is an unrestricted op,
|
|
13
|
+
* `[]` an op that reaches nothing. `withheld` notes that OBSIDIAN_DENY_PATHS
|
|
14
|
+
* carves paths out of the scope, without naming them: the deny prefixes name
|
|
15
|
+
* exactly what the list hides.
|
|
16
|
+
*/
|
|
17
|
+
function renderScope(scope, withheld) {
|
|
18
|
+
if (scope?.length === 0)
|
|
19
|
+
return 'none';
|
|
20
|
+
const reach = scope === undefined ? 'full vault' : scope.map((p) => `'${p}'`).join(', ');
|
|
21
|
+
return withheld ? `${reach}, except paths the operator withholds` : reach;
|
|
13
22
|
}
|
|
14
23
|
/**
|
|
15
24
|
* Build the instructions. The scope lines state what `path_forbidden`
|
|
@@ -19,21 +28,26 @@ function renderScope(scope) {
|
|
|
19
28
|
export function buildInstructions(opts) {
|
|
20
29
|
const { enableCommands, omnisearchReachable, policy, readOnly } = opts;
|
|
21
30
|
const { readable, writable } = policy.effectiveScope();
|
|
31
|
+
const withheld = policy.withholdsPaths;
|
|
32
|
+
const recovery = withheld ? `${SCOPE_RECOVERY} ${WITHHELD_RECOVERY}` : SCOPE_RECOVERY;
|
|
22
33
|
const sections = [BASELINE];
|
|
23
34
|
if (readOnly) {
|
|
24
35
|
sections.push('Read-only mode is active (OBSIDIAN_READ_ONLY=true): write tools are unavailable. `obsidian_open_in_ui` still opens notes that exist, but rejects an open against a missing path — Obsidian would create the file.');
|
|
25
|
-
if (readable !== undefined) {
|
|
26
|
-
sections.push(`Vault path policy is enforced. Readable: ${renderScope(readable)}. ${
|
|
36
|
+
if (readable !== undefined || withheld) {
|
|
37
|
+
sections.push(`Vault path policy is enforced. Readable: ${renderScope(readable, withheld)}. ${recovery}`);
|
|
27
38
|
}
|
|
28
39
|
}
|
|
29
40
|
else if (!policy.isUnrestricted) {
|
|
30
|
-
sections.push(`Vault path policy is enforced. Readable: ${renderScope(readable)}. Writable: ${renderScope(writable)}. ${
|
|
41
|
+
sections.push(`Vault path policy is enforced. Readable: ${renderScope(readable, withheld)}. Writable: ${renderScope(writable, withheld)}. ${recovery}`);
|
|
31
42
|
}
|
|
32
43
|
if (enableCommands && !readOnly) {
|
|
33
44
|
sections.push('Command-palette tools (`obsidian_list_commands`, `obsidian_execute_command`) are enabled and can fire any Obsidian command. Commands are opaque and may be destructive — prefer dedicated tools when one fits.');
|
|
34
45
|
}
|
|
35
46
|
if (omnisearchReachable) {
|
|
36
|
-
|
|
47
|
+
const omnisearch = '`obsidian_search_notes` includes an `omnisearch` mode: BM25-ranked, typo-tolerant, with PDF/OCR coverage (via the Text Extractor plugin). Omnisearch keeps only its top 50 matches: narrow with `path:` / `ext:` filters or more plain terms, which apply before that cap. Quoted phrases and `-exclusion` filter only those 50, so a result using them is marked `truncated`.';
|
|
48
|
+
sections.push(policy.restrictsReads
|
|
49
|
+
? `${omnisearch} Under the read scope the cap ranks the whole vault before the scope filters it, so \`truncated\` is always true; a \`path:\` term naming a readable folder ranks within that folder.`
|
|
50
|
+
: omnisearch);
|
|
37
51
|
}
|
|
38
52
|
return sections.join('\n\n');
|
|
39
53
|
}
|
package/dist/instructions.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"instructions.js","sourceRoot":"","sources":["../src/instructions.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,MAAM,QAAQ,GACZ,qXAAqX,CAAC;AAExX,MAAM,cAAc,GAClB,iHAAiH,CAAC;AAEpH,
|
|
1
|
+
{"version":3,"file":"instructions.js","sourceRoot":"","sources":["../src/instructions.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,MAAM,QAAQ,GACZ,qXAAqX,CAAC;AAExX,MAAM,cAAc,GAClB,iHAAiH,CAAC;AAEpH,MAAM,iBAAiB,GAAG,wEAAwE,CAAC;AAEnG;;;;;GAKG;AACH,SAAS,WAAW,CAAC,KAAoC,EAAE,QAAiB;IAC1E,IAAI,KAAK,EAAE,MAAM,KAAK,CAAC;QAAE,OAAO,MAAM,CAAC;IACvC,MAAM,KAAK,GAAG,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACzF,OAAO,QAAQ,CAAC,CAAC,CAAC,GAAG,KAAK,uCAAuC,CAAC,CAAC,CAAC,KAAK,CAAC;AAC5E,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAAC,IAKjC;IACC,MAAM,EAAE,cAAc,EAAE,mBAAmB,EAAE,MAAM,EAAE,QAAQ,EAAE,GAAG,IAAI,CAAC;IACvE,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,GAAG,MAAM,CAAC,cAAc,EAAE,CAAC;IACvD,MAAM,QAAQ,GAAG,MAAM,CAAC,cAAc,CAAC;IACvC,MAAM,QAAQ,GAAG,QAAQ,CAAC,CAAC,CAAC,GAAG,cAAc,IAAI,iBAAiB,EAAE,CAAC,CAAC,CAAC,cAAc,CAAC;IACtF,MAAM,QAAQ,GAAG,CAAC,QAAQ,CAAC,CAAC;IAC5B,IAAI,QAAQ,EAAE,CAAC;QACb,QAAQ,CAAC,IAAI,CACX,mNAAmN,CACpN,CAAC;QACF,IAAI,QAAQ,KAAK,SAAS,IAAI,QAAQ,EAAE,CAAC;YACvC,QAAQ,CAAC,IAAI,CACX,4CAA4C,WAAW,CAAC,QAAQ,EAAE,QAAQ,CAAC,KAAK,QAAQ,EAAE,CAC3F,CAAC;QACJ,CAAC;IACH,CAAC;SAAM,IAAI,CAAC,MAAM,CAAC,cAAc,EAAE,CAAC;QAClC,QAAQ,CAAC,IAAI,CACX,4CAA4C,WAAW,CAAC,QAAQ,EAAE,QAAQ,CAAC,eAAe,WAAW,CAAC,QAAQ,EAAE,QAAQ,CAAC,KAAK,QAAQ,EAAE,CACzI,CAAC;IACJ,CAAC;IACD,IAAI,cAAc,IAAI,CAAC,QAAQ,EAAE,CAAC;QAChC,QAAQ,CAAC,IAAI,CACX,gNAAgN,CACjN,CAAC;IACJ,CAAC;IACD,IAAI,mBAAmB,EAAE,CAAC;QACxB,MAAM,UAAU,GACd,gXAAgX,CAAC;QACnX,QAAQ,CAAC,IAAI,CACX,MAAM,CAAC,cAAc;YACnB,CAAC,CAAC,GAAG,UAAU,uLAAuL;YACtM,CAAC,CAAC,UAAU,CACf,CAAC;IACJ,CAAC;IACD,OAAO,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;AAC/B,CAAC"}
|
|
@@ -17,8 +17,8 @@ export declare const allResourceDefinitions: (import("@cyanheads/mcp-ts-core").R
|
|
|
17
17
|
}, import("zod/v4/core").$strip>, readonly [{
|
|
18
18
|
readonly reason: "path_forbidden";
|
|
19
19
|
readonly code: import("@cyanheads/mcp-ts-core/errors").JsonRpcErrorCode.Forbidden;
|
|
20
|
-
readonly when: "The requested path is outside OBSIDIAN_READ_PATHS (and OBSIDIAN_WRITE_PATHS, since write paths imply read access).";
|
|
21
|
-
readonly recovery: "Use a path inside the configured read scope. The error data echoes the active scope.";
|
|
20
|
+
readonly when: "The requested path is outside OBSIDIAN_READ_PATHS (and OBSIDIAN_WRITE_PATHS, since write paths imply read access), or inside OBSIDIAN_DENY_PATHS.";
|
|
21
|
+
readonly recovery: "Use a path inside the configured read scope. The error data echoes the active scope; a `denied_path` denial carries none, since the operator withholds that path.";
|
|
22
22
|
}, {
|
|
23
23
|
readonly reason: "note_missing";
|
|
24
24
|
readonly code: import("@cyanheads/mcp-ts-core/errors").JsonRpcErrorCode.NotFound;
|
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Not a mirror of `obsidian_list_tags`: this is a snapshot of
|
|
6
6
|
* `ObsidianService.listTags`, returned whole and unsorted — the upstream
|
|
7
|
-
* `/tags/` payload in upstream order, or under OBSIDIAN_READ_PATHS
|
|
8
|
-
* readable notes in order of first
|
|
7
|
+
* `/tags/` payload in upstream order, or under OBSIDIAN_READ_PATHS or
|
|
8
|
+
* OBSIDIAN_DENY_PATHS the tags of readable notes in order of first
|
|
9
|
+
* appearance. The tool shapes the same
|
|
9
10
|
* payload for an LLM caller — count-descending, capped, filterable — and that
|
|
10
11
|
* shaping deliberately lives in the tool handler rather than the shared
|
|
11
12
|
* service call both make.
|