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.
Files changed (117) hide show
  1. package/AGENTS.md +28 -13
  2. package/CLAUDE.md +28 -13
  3. package/Dockerfile +7 -0
  4. package/README.md +43 -99
  5. package/changelog/3.7.x/3.7.2.md +34 -0
  6. package/changelog/3.8.x/3.8.0.md +38 -0
  7. package/dist/config/server-config.d.ts +1 -0
  8. package/dist/config/server-config.d.ts.map +1 -1
  9. package/dist/config/server-config.js +24 -15
  10. package/dist/config/server-config.js.map +1 -1
  11. package/dist/index.js +6 -1
  12. package/dist/index.js.map +1 -1
  13. package/dist/instructions.d.ts.map +1 -1
  14. package/dist/instructions.js +21 -7
  15. package/dist/instructions.js.map +1 -1
  16. package/dist/mcp-server/resources/definitions/index.d.ts +2 -2
  17. package/dist/mcp-server/resources/definitions/obsidian-tags.resource.d.ts +3 -2
  18. package/dist/mcp-server/resources/definitions/obsidian-tags.resource.d.ts.map +1 -1
  19. package/dist/mcp-server/resources/definitions/obsidian-tags.resource.js +6 -5
  20. package/dist/mcp-server/resources/definitions/obsidian-tags.resource.js.map +1 -1
  21. package/dist/mcp-server/resources/definitions/obsidian-vault-note.resource.d.ts +2 -2
  22. package/dist/mcp-server/resources/definitions/obsidian-vault-note.resource.js +2 -2
  23. package/dist/mcp-server/resources/definitions/obsidian-vault-note.resource.js.map +1 -1
  24. package/dist/mcp-server/tools/definitions/_shared/markdown.d.ts +14 -0
  25. package/dist/mcp-server/tools/definitions/_shared/markdown.d.ts.map +1 -0
  26. package/dist/mcp-server/tools/definitions/_shared/markdown.js +17 -0
  27. package/dist/mcp-server/tools/definitions/_shared/markdown.js.map +1 -0
  28. package/dist/mcp-server/tools/definitions/_shared/suggest-paths.d.ts +8 -1
  29. package/dist/mcp-server/tools/definitions/_shared/suggest-paths.d.ts.map +1 -1
  30. package/dist/mcp-server/tools/definitions/_shared/suggest-paths.js +17 -6
  31. package/dist/mcp-server/tools/definitions/_shared/suggest-paths.js.map +1 -1
  32. package/dist/mcp-server/tools/definitions/index.d.ts +18 -18
  33. package/dist/mcp-server/tools/definitions/obsidian-append-to-note.tool.d.ts +2 -2
  34. package/dist/mcp-server/tools/definitions/obsidian-append-to-note.tool.d.ts.map +1 -1
  35. package/dist/mcp-server/tools/definitions/obsidian-append-to-note.tool.js +5 -4
  36. package/dist/mcp-server/tools/definitions/obsidian-append-to-note.tool.js.map +1 -1
  37. package/dist/mcp-server/tools/definitions/obsidian-delete-note.tool.d.ts +7 -6
  38. package/dist/mcp-server/tools/definitions/obsidian-delete-note.tool.d.ts.map +1 -1
  39. package/dist/mcp-server/tools/definitions/obsidian-delete-note.tool.js +9 -8
  40. package/dist/mcp-server/tools/definitions/obsidian-delete-note.tool.js.map +1 -1
  41. package/dist/mcp-server/tools/definitions/obsidian-execute-command.tool.d.ts.map +1 -1
  42. package/dist/mcp-server/tools/definitions/obsidian-execute-command.tool.js +2 -1
  43. package/dist/mcp-server/tools/definitions/obsidian-execute-command.tool.js.map +1 -1
  44. package/dist/mcp-server/tools/definitions/obsidian-get-note.tool.d.ts +2 -2
  45. package/dist/mcp-server/tools/definitions/obsidian-get-note.tool.d.ts.map +1 -1
  46. package/dist/mcp-server/tools/definitions/obsidian-get-note.tool.js +38 -28
  47. package/dist/mcp-server/tools/definitions/obsidian-get-note.tool.js.map +1 -1
  48. package/dist/mcp-server/tools/definitions/obsidian-list-commands.tool.d.ts.map +1 -1
  49. package/dist/mcp-server/tools/definitions/obsidian-list-commands.tool.js +7 -4
  50. package/dist/mcp-server/tools/definitions/obsidian-list-commands.tool.js.map +1 -1
  51. package/dist/mcp-server/tools/definitions/obsidian-list-notes.tool.d.ts +6 -6
  52. package/dist/mcp-server/tools/definitions/obsidian-list-notes.tool.d.ts.map +1 -1
  53. package/dist/mcp-server/tools/definitions/obsidian-list-notes.tool.js +15 -14
  54. package/dist/mcp-server/tools/definitions/obsidian-list-notes.tool.js.map +1 -1
  55. package/dist/mcp-server/tools/definitions/obsidian-list-tags.tool.d.ts +4 -4
  56. package/dist/mcp-server/tools/definitions/obsidian-list-tags.tool.d.ts.map +1 -1
  57. package/dist/mcp-server/tools/definitions/obsidian-list-tags.tool.js +9 -8
  58. package/dist/mcp-server/tools/definitions/obsidian-list-tags.tool.js.map +1 -1
  59. package/dist/mcp-server/tools/definitions/obsidian-manage-frontmatter.tool.d.ts +2 -2
  60. package/dist/mcp-server/tools/definitions/obsidian-manage-frontmatter.tool.d.ts.map +1 -1
  61. package/dist/mcp-server/tools/definitions/obsidian-manage-frontmatter.tool.js +9 -7
  62. package/dist/mcp-server/tools/definitions/obsidian-manage-frontmatter.tool.js.map +1 -1
  63. package/dist/mcp-server/tools/definitions/obsidian-manage-tags.tool.d.ts +2 -2
  64. package/dist/mcp-server/tools/definitions/obsidian-manage-tags.tool.d.ts.map +1 -1
  65. package/dist/mcp-server/tools/definitions/obsidian-manage-tags.tool.js +7 -5
  66. package/dist/mcp-server/tools/definitions/obsidian-manage-tags.tool.js.map +1 -1
  67. package/dist/mcp-server/tools/definitions/obsidian-open-in-ui.tool.d.ts +2 -2
  68. package/dist/mcp-server/tools/definitions/obsidian-open-in-ui.tool.d.ts.map +1 -1
  69. package/dist/mcp-server/tools/definitions/obsidian-open-in-ui.tool.js +4 -3
  70. package/dist/mcp-server/tools/definitions/obsidian-open-in-ui.tool.js.map +1 -1
  71. package/dist/mcp-server/tools/definitions/obsidian-patch-note.tool.d.ts +2 -2
  72. package/dist/mcp-server/tools/definitions/obsidian-patch-note.tool.d.ts.map +1 -1
  73. package/dist/mcp-server/tools/definitions/obsidian-patch-note.tool.js +5 -4
  74. package/dist/mcp-server/tools/definitions/obsidian-patch-note.tool.js.map +1 -1
  75. package/dist/mcp-server/tools/definitions/obsidian-replace-in-note.tool.d.ts +2 -2
  76. package/dist/mcp-server/tools/definitions/obsidian-replace-in-note.tool.d.ts.map +1 -1
  77. package/dist/mcp-server/tools/definitions/obsidian-replace-in-note.tool.js +10 -4
  78. package/dist/mcp-server/tools/definitions/obsidian-replace-in-note.tool.js.map +1 -1
  79. package/dist/mcp-server/tools/definitions/obsidian-search-notes.tool.d.ts +46 -12
  80. package/dist/mcp-server/tools/definitions/obsidian-search-notes.tool.d.ts.map +1 -1
  81. package/dist/mcp-server/tools/definitions/obsidian-search-notes.tool.js +93 -43
  82. package/dist/mcp-server/tools/definitions/obsidian-search-notes.tool.js.map +1 -1
  83. package/dist/mcp-server/tools/definitions/obsidian-write-note.tool.d.ts +2 -2
  84. package/dist/mcp-server/tools/definitions/obsidian-write-note.tool.d.ts.map +1 -1
  85. package/dist/mcp-server/tools/definitions/obsidian-write-note.tool.js +5 -4
  86. package/dist/mcp-server/tools/definitions/obsidian-write-note.tool.js.map +1 -1
  87. package/dist/services/obsidian/jsonlogic-evaluator.d.ts +33 -8
  88. package/dist/services/obsidian/jsonlogic-evaluator.d.ts.map +1 -1
  89. package/dist/services/obsidian/jsonlogic-evaluator.js +171 -32
  90. package/dist/services/obsidian/jsonlogic-evaluator.js.map +1 -1
  91. package/dist/services/obsidian/jsonlogic-worker.d.ts +6 -5
  92. package/dist/services/obsidian/jsonlogic-worker.d.ts.map +1 -1
  93. package/dist/services/obsidian/jsonlogic-worker.js +7 -5
  94. package/dist/services/obsidian/jsonlogic-worker.js.map +1 -1
  95. package/dist/services/obsidian/obsidian-service.d.ts +38 -22
  96. package/dist/services/obsidian/obsidian-service.d.ts.map +1 -1
  97. package/dist/services/obsidian/obsidian-service.js +293 -99
  98. package/dist/services/obsidian/obsidian-service.js.map +1 -1
  99. package/dist/services/obsidian/patch-instruction.d.ts +4 -1
  100. package/dist/services/obsidian/patch-instruction.d.ts.map +1 -1
  101. package/dist/services/obsidian/patch-instruction.js +5 -2
  102. package/dist/services/obsidian/patch-instruction.js.map +1 -1
  103. package/dist/services/obsidian/path-policy.d.ts +67 -23
  104. package/dist/services/obsidian/path-policy.d.ts.map +1 -1
  105. package/dist/services/obsidian/path-policy.js +183 -80
  106. package/dist/services/obsidian/path-policy.js.map +1 -1
  107. package/dist/services/obsidian/section-extractor.d.ts +6 -1
  108. package/dist/services/obsidian/section-extractor.d.ts.map +1 -1
  109. package/dist/services/obsidian/section-extractor.js +106 -11
  110. package/dist/services/obsidian/section-extractor.js.map +1 -1
  111. package/dist/services/obsidian/simple-search.d.ts +62 -0
  112. package/dist/services/obsidian/simple-search.d.ts.map +1 -0
  113. package/dist/services/obsidian/simple-search.js +264 -0
  114. package/dist/services/obsidian/simple-search.js.map +1 -0
  115. package/manifest.json +10 -2
  116. package/package.json +8 -7
  117. package/server.json +19 -7
package/README.md CHANGED
@@ -7,7 +7,7 @@
7
7
 
8
8
  <div align="center">
9
9
 
10
- [![Version](https://img.shields.io/badge/Version-3.7.1-blue.svg?style=flat-square)](./CHANGELOG.md) [![License](https://img.shields.io/badge/License-Apache%202.0-orange.svg?style=flat-square)](./LICENSE) [![Docker](https://img.shields.io/badge/Docker-ghcr.io-2496ED?style=flat-square&logo=docker&logoColor=white)](https://github.com/users/cyanheads/packages/container/package/obsidian-mcp-server) [![MCP SDK](https://img.shields.io/badge/MCP%20SDK-^2.3.1-green.svg?style=flat-square)](https://modelcontextprotocol.io/) [![npm](https://img.shields.io/npm/v/obsidian-mcp-server?style=flat-square&logo=npm&logoColor=white)](https://www.npmjs.com/package/obsidian-mcp-server) [![TypeScript](https://img.shields.io/badge/TypeScript-^7.0.2-3178C6.svg?style=flat-square)](https://www.typescriptlang.org/) [![Bun](https://img.shields.io/badge/Bun-v1.4.4%2B-blueviolet.svg?style=flat-square)](https://bun.sh/)
10
+ [![Version](https://img.shields.io/badge/Version-3.8.0-blue.svg?style=flat-square)](./CHANGELOG.md) [![License](https://img.shields.io/badge/License-Apache%202.0-orange.svg?style=flat-square)](./LICENSE) [![Docker](https://img.shields.io/badge/Docker-ghcr.io-2496ED?style=flat-square&logo=docker&logoColor=white)](https://github.com/users/cyanheads/packages/container/package/obsidian-mcp-server) [![MCP SDK](https://img.shields.io/badge/MCP%20SDK-^2.4.0-green.svg?style=flat-square)](https://modelcontextprotocol.io/) [![npm](https://img.shields.io/npm/v/obsidian-mcp-server?style=flat-square&logo=npm&logoColor=white)](https://www.npmjs.com/package/obsidian-mcp-server) [![TypeScript](https://img.shields.io/badge/TypeScript-^7.0.2-3178C6.svg?style=flat-square)](https://www.typescriptlang.org/) [![Bun](https://img.shields.io/badge/Bun-v1.4.4%2B-blueviolet.svg?style=flat-square)](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 whitespace-split 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), with `glob` / `regexp` taking `[PATTERN, VALUE]`
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; its 50-hit upstream cap sets `truncated: true`
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 the following to your MCP client configuration file. The Obsidian Local REST API plugin must be installed and enabled in your vault; see [Prerequisites](#prerequisites).
213
+ Add it to Claude Code:
214
214
 
215
- ```json
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
- Or with npx (no Bun required):
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@latest"],
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
- Inside a container, the default `OBSIDIAN_BASE_URL` (`http://127.0.0.1:27123`) is the container's own loopback. Add `-e OBSIDIAN_BASE_URL=http://host.docker.internal:27123` (Docker Desktop) or run with `--network host` (Linux) to reach the plugin on your host.
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
- - [Bun v1.4.0](https://bun.sh/) or higher (or Node.js v24+).
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
- Three optional env vars limit which vault paths the tools can touch. Unset, reads and writes cover the full vault.
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; trailing slashes are normalized. Write paths are also readable.
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, and a JSONLogic tree never reads a note outside the scope: the server evaluates it over the readable notes, with `links`, `backlinks`, and `unresolvedLinks` holding readable entries only. Which note a link points to is still Obsidian's resolution across the whole vault, so those three fields can change when a note outside the scope is created or removed; match link text in `content` when that matters. That evaluation is bounded — 1,000 levels of nesting, a work budget per note for the values a tree builds and one per search for the results it keeps, 10 s — and a tree past a bound fails with `logic_too_costly`. A readable note whose data is nested too deep to hand to the evaluator (about 3,500 levels under Node) fails the search with `note_too_deep`. Without a read scope the tree goes to the plugin unchanged.
355
- - Tag listings (`obsidian_list_tags`, `obsidian://tags`) under `OBSIDIAN_READ_PATHS` cover readable notes only, counting notes rather than occurrences. Write paths and read-only alone leave them vault-wide.
356
- - 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. The startup log prints the configured lists.
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 build -t obsidian-mcp-server .
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`. Point `OBSIDIAN_BASE_URL` at `http://host.docker.internal:27123` (Docker Desktop) or use `--network host` (Linux) to reach the plugin. OpenTelemetry peer dependencies are installed by default; build with `--build-arg OTEL_ENABLED=false` to omit them.
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;AAgF3C,QAAA,MAAM,kBAAkB;;;;;;;;;;;iBA4DtB,CAAC;AAEH,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAI9D,wBAAgB,eAAe,IAAI,YAAY,CAc9C;AAED,iEAAiE;AACjE,wBAAgB,iBAAiB,IAAI,IAAI,CAExC"}
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. Semantics (see issue #40):
16
- * - undefined / `''` / whitespace-only → undefined (treat as unset; default = full vault)
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: `\` → `/` + lower-case + trim trailing slash + dedupe (preserves first occurrence order)
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
- * Separator normalization is required for parity with `PathPolicy.normalize()`,
23
- * which also collapses `\` → `/` on candidate paths. Without matching
24
- * normalization here, a prefix like `Foo\Bar` would never match a candidate
25
- * `Foo\Bar\note.md` (candidate becomes `foo/bar/note.md`, prefix stays as
26
- * `foo\bar`).
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
- const normalized = raw.replace(/\\/g, '/').toLowerCase().replace(/\/+$/, '');
75
- if (normalized.length === 0)
76
- continue;
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":"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"}
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;AAarE;;;;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,CA6BT"}
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"}
@@ -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
- /** Prefixes as the instructions quote them; `undefined` is an unrestricted op. */
11
- function renderScope(scope) {
12
- return scope === undefined ? 'full vault' : scope.map((p) => `'${p}'`).join(', ');
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)}. ${SCOPE_RECOVERY}`);
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)}. ${SCOPE_RECOVERY}`);
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
- sections.push('`obsidian_search_notes` includes an `omnisearch` mode: BM25-ranked, typo-tolerant, with PDF/OCR coverage (via the Text Extractor plugin). Results cap at 50 upstream — narrow the query (quoted phrases, `-exclusion`, `path:` / `ext:` filters) to surface more.');
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
  }
@@ -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,kFAAkF;AAClF,SAAS,WAAW,CAAC,KAAoC;IACvD,OAAO,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;AACpF,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,CAAC,QAAQ,CAAC,CAAC;IAC5B,IAAI,QAAQ,EAAE,CAAC;QACb,QAAQ,CAAC,IAAI,CACX,mNAAmN,CACpN,CAAC;QACF,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;YAC3B,QAAQ,CAAC,IAAI,CACX,4CAA4C,WAAW,CAAC,QAAQ,CAAC,KAAK,cAAc,EAAE,CACvF,CAAC;QACJ,CAAC;IACH,CAAC;SAAM,IAAI,CAAC,MAAM,CAAC,cAAc,EAAE,CAAC;QAClC,QAAQ,CAAC,IAAI,CACX,4CAA4C,WAAW,CAAC,QAAQ,CAAC,eAAe,WAAW,CAAC,QAAQ,CAAC,KAAK,cAAc,EAAE,CAC3H,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,QAAQ,CAAC,IAAI,CACX,mQAAmQ,CACpQ,CAAC;IACJ,CAAC;IACD,OAAO,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;AAC/B,CAAC"}
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 the tags of
8
- * readable notes in order of first appearance. The tool shapes the same
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.