obsidian-native-mcp 1.0.3 → 1.2.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 (184) hide show
  1. package/.github/workflows/ci.yml +66 -38
  2. package/.github/workflows/release.yml +152 -0
  3. package/.husky/commit-msg +18 -0
  4. package/.nvmrc +1 -0
  5. package/CHANGELOG.md +3 -3
  6. package/DEVELOPER.md +158 -106
  7. package/README.md +137 -67
  8. package/dist/audit/log.js +99 -0
  9. package/dist/audit/log.js.map +1 -0
  10. package/dist/cache/file-cache.js +66 -0
  11. package/dist/cache/file-cache.js.map +1 -0
  12. package/dist/cli/index.js +139 -32
  13. package/dist/cli/index.js.map +1 -1
  14. package/dist/fs/io.js +68 -0
  15. package/dist/fs/io.js.map +1 -0
  16. package/dist/fs/paths.js +41 -0
  17. package/dist/fs/paths.js.map +1 -0
  18. package/dist/fs/trash.js +20 -0
  19. package/dist/fs/trash.js.map +1 -0
  20. package/dist/fs/walk.js +73 -0
  21. package/dist/fs/walk.js.map +1 -0
  22. package/dist/handlers/args.js +97 -0
  23. package/dist/handlers/args.js.map +1 -0
  24. package/dist/handlers/registry.js +54 -0
  25. package/dist/handlers/registry.js.map +1 -0
  26. package/dist/markdown/blocks.js +95 -0
  27. package/dist/markdown/blocks.js.map +1 -0
  28. package/dist/markdown/fingerprint.js +99 -0
  29. package/dist/markdown/fingerprint.js.map +1 -0
  30. package/dist/markdown/frontmatter.js +153 -0
  31. package/dist/markdown/frontmatter.js.map +1 -0
  32. package/dist/markdown/headings.js +104 -0
  33. package/dist/markdown/headings.js.map +1 -0
  34. package/dist/markdown/links.js +93 -0
  35. package/dist/markdown/links.js.map +1 -0
  36. package/dist/markdown/outline.js +12 -0
  37. package/dist/markdown/outline.js.map +1 -0
  38. package/dist/markdown/parse-file.js +41 -0
  39. package/dist/markdown/parse-file.js.map +1 -0
  40. package/dist/markdown/parse.js +49 -0
  41. package/dist/markdown/parse.js.map +1 -0
  42. package/dist/markdown/tags.js +40 -0
  43. package/dist/markdown/tags.js.map +1 -0
  44. package/dist/mcp/framing.js +56 -0
  45. package/dist/mcp/framing.js.map +1 -0
  46. package/dist/mcp/http.js +240 -0
  47. package/dist/mcp/http.js.map +1 -0
  48. package/dist/mcp/protocol.js +16 -56
  49. package/dist/mcp/protocol.js.map +1 -1
  50. package/dist/mcp/server.js +105 -268
  51. package/dist/mcp/server.js.map +1 -1
  52. package/dist/mcp/stdio.js +38 -0
  53. package/dist/mcp/stdio.js.map +1 -0
  54. package/dist/mcp/transport.js +4 -2
  55. package/dist/mcp/transport.js.map +1 -1
  56. package/dist/plugin/main.js +21956 -1109
  57. package/dist/plugin/main.js.map +1 -1
  58. package/dist/plugin/manifest.json +1 -1
  59. package/dist/plugin/settings.js +149 -41
  60. package/dist/plugin/settings.js.map +1 -1
  61. package/dist/prompts/provider.js +96 -0
  62. package/dist/prompts/provider.js.map +1 -0
  63. package/dist/tools/common.js +84 -0
  64. package/dist/tools/common.js.map +1 -0
  65. package/dist/tools/index.js +41 -0
  66. package/dist/tools/index.js.map +1 -0
  67. package/dist/tools/read.js +631 -0
  68. package/dist/tools/read.js.map +1 -0
  69. package/dist/tools/write-basic.js +319 -0
  70. package/dist/tools/write-basic.js.map +1 -0
  71. package/dist/tools/write-bulk.js +331 -0
  72. package/dist/tools/write-bulk.js.map +1 -0
  73. package/dist/tools/write-patch.js +152 -0
  74. package/dist/tools/write-patch.js.map +1 -0
  75. package/dist/tools/write-structural.js +389 -0
  76. package/dist/tools/write-structural.js.map +1 -0
  77. package/dist/tools/write-surgical.js +349 -0
  78. package/dist/tools/write-surgical.js.map +1 -0
  79. package/dist/utils/log.js +2 -6
  80. package/dist/utils/log.js.map +1 -1
  81. package/dist/utils/types.js +19 -0
  82. package/dist/utils/types.js.map +1 -0
  83. package/dist/vault/permissions.js +73 -0
  84. package/dist/vault/permissions.js.map +1 -0
  85. package/dist/vault/registry.js +164 -0
  86. package/dist/vault/registry.js.map +1 -0
  87. package/eslint.config.mjs +2 -2
  88. package/manifest.json +1 -1
  89. package/package.json +18 -2
  90. package/renovate.json +6 -0
  91. package/scripts/start-mcp.sh +6 -0
  92. package/src/audit/log.ts +110 -0
  93. package/src/cache/file-cache.ts +92 -0
  94. package/src/cli/index.ts +155 -31
  95. package/src/fs/io.ts +69 -0
  96. package/src/fs/paths.ts +45 -0
  97. package/src/fs/trash.ts +22 -0
  98. package/src/fs/walk.ts +95 -0
  99. package/src/handlers/args.ts +129 -0
  100. package/src/handlers/registry.ts +100 -0
  101. package/src/markdown/blocks.ts +108 -0
  102. package/src/markdown/fingerprint.ts +112 -0
  103. package/src/markdown/frontmatter.ts +173 -0
  104. package/src/markdown/headings.ts +116 -0
  105. package/src/markdown/links.ts +99 -0
  106. package/src/markdown/outline.ts +18 -0
  107. package/src/markdown/parse-file.ts +51 -0
  108. package/src/markdown/parse.ts +53 -0
  109. package/src/markdown/tags.ts +42 -0
  110. package/src/mcp/framing.ts +59 -0
  111. package/src/mcp/http.ts +275 -0
  112. package/src/mcp/protocol.ts +41 -81
  113. package/src/mcp/server.ts +150 -278
  114. package/src/mcp/stdio.ts +41 -0
  115. package/src/mcp/transport.ts +12 -5
  116. package/src/plugin/main.ts +104 -86
  117. package/src/plugin/settings.ts +176 -44
  118. package/src/prompts/provider.ts +98 -0
  119. package/src/tools/common.ts +106 -0
  120. package/src/tools/index.ts +60 -0
  121. package/src/tools/read.ts +662 -0
  122. package/src/tools/write-basic.ts +330 -0
  123. package/src/tools/write-bulk.ts +355 -0
  124. package/src/tools/write-patch.ts +166 -0
  125. package/src/tools/write-structural.ts +409 -0
  126. package/src/tools/write-surgical.ts +378 -0
  127. package/src/utils/types.ts +147 -0
  128. package/src/vault/permissions.ts +94 -0
  129. package/src/vault/registry.ts +191 -0
  130. package/tests/fixtures/vaults/agents/AGENTS.md +16 -0
  131. package/tests/fixtures/vaults/code-heavy/code-heavy.md +46 -0
  132. package/tests/fixtures/vaults/daily-note/Daily/2026-05-21.md +21 -0
  133. package/tests/fixtures/vaults/dup-headings/dup-headings.md +17 -0
  134. package/tests/fixtures/vaults/frontmatter-stress/fm.md +17 -0
  135. package/tests/fixtures/vaults/large-kb/big.md +5501 -0
  136. package/tests/fixtures/vaults/links-zoo/Target.md +3 -0
  137. package/tests/fixtures/vaults/links-zoo/source.md +13 -0
  138. package/tests/fixtures/vaults/tiny/tiny.md +3 -0
  139. package/tests/helpers/sandbox.ts +107 -0
  140. package/tests/integration/apply-edits.test.ts +56 -0
  141. package/tests/integration/audit.test.ts +66 -0
  142. package/tests/integration/blocks.test.ts +71 -0
  143. package/tests/integration/file-ops.test.ts +78 -0
  144. package/tests/integration/links.test.ts +65 -0
  145. package/tests/integration/permissions.test.ts +72 -0
  146. package/tests/scenarios/S1-mark-tasks-done.test.ts +69 -0
  147. package/tests/scenarios/S10-code-fence-safety.test.ts +78 -0
  148. package/tests/scenarios/S11-no-fabrication.test.ts +72 -0
  149. package/tests/scenarios/S12-apply-patch.test.ts +92 -0
  150. package/tests/scenarios/S13-search-pagination.test.ts +35 -0
  151. package/tests/scenarios/S14-read-write-roundtrip.test.ts +76 -0
  152. package/tests/scenarios/S15-byte-budget.test.ts +77 -0
  153. package/tests/scenarios/S2-refactor-section.test.ts +87 -0
  154. package/tests/scenarios/S4-S5-concurrent.test.ts +85 -0
  155. package/tests/scenarios/S6-S7-large-file.test.ts +77 -0
  156. package/tests/scenarios/S8-bulk-atomic.test.ts +105 -0
  157. package/tests/scenarios/S9-frontmatter-nested.test.ts +63 -0
  158. package/tests/unit/fingerprint.test.ts +85 -0
  159. package/tests/unit/frontmatter.test.ts +78 -0
  160. package/tests/unit/headings.test.ts +66 -0
  161. package/tsconfig.json +5 -3
  162. package/dist/handlers/prompts.js +0 -127
  163. package/dist/handlers/prompts.js.map +0 -1
  164. package/dist/handlers/tools.js +0 -113
  165. package/dist/handlers/tools.js.map +0 -1
  166. package/dist/mcp/http-transport.js +0 -142
  167. package/dist/mcp/http-transport.js.map +0 -1
  168. package/dist/mcp/stdio-transport.js +0 -49
  169. package/dist/mcp/stdio-transport.js.map +0 -1
  170. package/dist/utils/fs-utils.js +0 -268
  171. package/dist/utils/fs-utils.js.map +0 -1
  172. package/dist/utils/search.js +0 -62
  173. package/dist/utils/search.js.map +0 -1
  174. package/dist/utils/vaults.js +0 -179
  175. package/dist/utils/vaults.js.map +0 -1
  176. package/src/handlers/prompts.ts +0 -148
  177. package/src/handlers/tools.ts +0 -146
  178. package/src/mcp/http-transport.ts +0 -159
  179. package/src/mcp/stdio-transport.ts +0 -54
  180. package/src/utils/fs-utils.ts +0 -358
  181. package/src/utils/search.ts +0 -84
  182. package/src/utils/vaults.ts +0 -198
  183. package/tests/http-transport.test.ts +0 -111
  184. package/tests/protocol.test.ts +0 -36
package/README.md CHANGED
@@ -2,36 +2,37 @@
2
2
 
3
3
  # Obsidian Native MCP
4
4
 
5
- **Zero-dependency MCP server for Obsidian vaults**
6
- Direct filesystem access no Obsidian process, no REST API plugin required.
5
+ **LLM-optimized MCP server for Obsidian vaults**
6
+ Surgical edits, hash-based concurrency safety, no whole-file rewrites.
7
7
 
8
8
  [![Build](https://img.shields.io/github/actions/workflow/status/usrivastava92/obsidian-native-mcp/ci.yml?branch=main&label=CI&logo=github)](https://github.com/usrivastava92/obsidian-native-mcp/actions)
9
9
  [![Release](https://img.shields.io/github/v/release/usrivastava92/obsidian-native-mcp?logo=semanticrelease)](https://github.com/usrivastava92/obsidian-native-mcp/releases)
10
10
  [![npm](https://img.shields.io/npm/v/obsidian-native-mcp?logo=npm)](https://www.npmjs.com/package/obsidian-native-mcp)
11
- [![npm downloads](https://img.shields.io/npm/dm/obsidian-native-mcp?logo=npm)](https://www.npmjs.com/package/obsidian-native-mcp)
12
11
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue)](LICENSE)
13
- [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen)](CONTRIBUTING.md)
14
12
 
15
13
  </div>
16
14
 
17
- Obsidian Native MCP is a [Model Context Protocol](https://modelcontextprotocol.io) server that gives AI assistants (Claude Desktop, etc.) direct, safe access to your Obsidian vaults.
15
+ A [Model Context Protocol](https://modelcontextprotocol.io) server that gives AI assistants (Claude Desktop, Cursor, Rovo Dev, etc.) direct, safe, **context-efficient** access to your Obsidian vaults.
18
16
 
19
17
  **Two ways to use it:**
20
18
 
21
- - **Obsidian plugin** (recommended) — 1-click install, auto-discovers vaults, settings UI, runs inside Obsidian
22
- - **CLI** — standalone, works without Obsidian running, configured via env var or config file
19
+ - **Obsidian plugin** — 1-click install, auto-discovers vaults, settings UI for per-tool toggles, runs inside Obsidian over HTTP/SSE with a bearer token.
20
+ - **CLI** — standalone Node binary, configured via env var or config file, speaks JSON-RPC over stdio with Content-Length framing.
23
21
 
24
- **Why Obsidian Native MCP over other solutions?**
22
+ ## Why Obsidian Native MCP
25
23
 
26
- | Feature | Obsidian Native MCP | obsidian-mcp-tools (archived) |
27
- | ------------------ | -------------------------------------- | ----------------------------------------- |
28
- | Obsidian required? | **Plugin: no. CLI: no.** | Yes must be running with Local REST API |
29
- | Dependencies | **Zero** — uses only Node.js stdlib | MCP SDK, arktype, zod, radash, turndown… |
30
- | Distribution | Obsidian plugin + npm CLI | Bun-compiled binary |
31
- | Multi-vault | **Built-in** one server, many vaults | No |
32
- | Cross-platform | **1 codebase, runs everywhere (WORA)** | Platform-specific binaries |
33
- | File patching | Headings, blocks, frontmatter | Via REST API |
34
- | Setup effort | Plugin: 1 click. CLI: one command. | Manual download + config |
24
+ The defining design goal is **minimize how many bytes the LLM has to push around per edit.** Every read returns content **plus cryptographic hashes**; every write declares the precondition hash it expects. The result: most edits become tiny `str_replace`s or unified-diff patches instead of full file rewrites.
25
+
26
+ | Feature | Obsidian Native MCP | Typical Obsidian MCP server |
27
+ | ------------------------ | ----------------------------------------------------------------- | ------------------------------------ |
28
+ | **Edit model** | `str_replace`, `apply_patch`, `apply_edits` surgical by default | Read whole file → write whole file |
29
+ | **Concurrency safety** | Cryptographic preconditions (`expected_*_hash`) on every write | None — silent clobbering |
30
+ | **Structural awareness** | mdast-AST: code-fenced "headings" never treated as headings | Regex hacks that corrupt code blocks |
31
+ | **Frontmatter** | Real YAML parser with nested key paths | Hand-rolled line matching |
32
+ | **Atomicity** | Multi-file `bulk.apply` with rollback | None |
33
+ | **Permissions** | Read-only mode + per-tool toggle + per-vault subdir allow/deny | All-or-nothing |
34
+ | **Audit trail** | JSONL log with content hashes before/after every mutation | None |
35
+ | **Multi-vault** | First-class | Usually one vault |
35
36
 
36
37
  ## Installation
37
38
 
@@ -39,8 +40,8 @@ Obsidian Native MCP is a [Model Context Protocol](https://modelcontextprotocol.i
39
40
 
40
41
  1. Open Obsidian → Settings → Community Plugins → Browse
41
42
  2. Search for "Obsidian Native MCP" and install
42
- 3. Enable the plugin in Community Plugins list
43
- 4. Go to plugin settings toggle which vaults to expose copy the MCP URL
43
+ 3. Enable in Community Plugins
44
+ 4. Open plugin settings: select which vaults to expose, optionally toggle per-tool permissions, copy the MCP URL
44
45
 
45
46
  ### CLI (standalone)
46
47
 
@@ -61,11 +62,11 @@ npm run build
61
62
 
62
63
  ### Plugin
63
64
 
64
- Plugin auto-discovers all your Obsidian vaults from Obsidian's own config. Open plugin settings to select which vaults to expose.
65
+ Auto-discovers all your Obsidian vaults from Obsidian's own config. Pick which to expose in plugin settings. Plugin also surfaces a bearer token and the MCP URL.
65
66
 
66
67
  ### CLI
67
68
 
68
- Configure vault(s) via environment variable or config file.
69
+ Either an env var or a config file.
69
70
 
70
71
  ```bash
71
72
  # Single vault
@@ -73,9 +74,6 @@ export OBSIDIAN_VAULT_PATHS=/Users/me/my-obsidian-vault
73
74
 
74
75
  # Multiple vaults (semicolons on all platforms)
75
76
  export OBSIDIAN_VAULT_PATHS=/Users/me/personal;/Users/me/work
76
-
77
- # Windows
78
- set OBSIDIAN_VAULT_PATHS=C:\Users\me\personal;C:\Users\me\work
79
77
  ```
80
78
 
81
79
  Config file at `~/.config/obsidian-native-mcp/vaults.json`:
@@ -89,17 +87,25 @@ Config file at `~/.config/obsidian-native-mcp/vaults.json`:
89
87
  }
90
88
  ```
91
89
 
90
+ Optional flags:
91
+
92
+ ```bash
93
+ obsidian-native-mcp --read-only # all write tools disabled
94
+ obsidian-native-mcp --vault notes=/path # ad-hoc named vault
95
+ obsidian-native-mcp --config ./my.json # explicit config file
96
+ ```
97
+
92
98
  ## Usage
93
99
 
94
100
  ### Obsidian plugin
95
101
 
96
- After enabling the plugin, open its settings tab. You'll see a URL like `http://127.0.0.1:9789/sse`. Add it to your `claude_desktop_config.json`:
102
+ Add the URL from plugin settings to your `claude_desktop_config.json`:
97
103
 
98
104
  ```json
99
105
  {
100
106
  "mcpServers": {
101
107
  "obsidian-native-mcp": {
102
- "url": "http://127.0.0.1:9789/sse"
108
+ "url": "http://127.0.0.1:9789/sse?token=YOUR_TOKEN"
103
109
  }
104
110
  }
105
111
  }
@@ -122,57 +128,121 @@ After enabling the plugin, open its settings tab. You'll see a URL like `http://
122
128
 
123
129
  ## Tools
124
130
 
125
- | Tool | Description |
126
- | ---------------- | ------------------------------------------------------- |
127
- | `list_vaults` | List all configured vaults with paths |
128
- | `get_vault_info` | Stats per vault (file count, etc.) |
129
- | `list_files` | List files/dirs in a vault directory |
130
- | `get_file` | Read file content (markdown or json with frontmatter) |
131
- | `create_file` | Create or overwrite a file |
132
- | `append_to_file` | Append content to a file |
133
- | `patch_file` | Patch by heading, block reference, or frontmatter field |
134
- | `delete_file` | Delete a file |
135
- | `search` | Full-text search across markdown files |
136
-
137
- All file tools accept an optional `vault` parameter. When only one vault is configured, it's inferred automatically.
131
+ All tools accept an optional `vault` parameter; with a single vault configured, it's inferred. Every read returns hashes used by writes as preconditions.
132
+
133
+ ### Read tools
134
+
135
+ | Tool | What it returns | Notes |
136
+ | ----------------- | ------------------------------------------------ | ------------------------------------------------ |
137
+ | `vault.list` | All configured vaults | |
138
+ | `vault.info` | Stats per vault | |
139
+ | `file.list` | Paged file listing | `recursive`, `pattern` (glob), `limit`, `offset` |
140
+ | `file.find` | Find files by name | exact / substring / glob / regex |
141
+ | `file.read` | Full file content + `contentHash` + `totalLines` | Use freely — guidelines/AGENTS.md/etc. |
142
+ | `file.read_range` | Line range + `rangeHash` | Cheaper for big files |
143
+ | `outline` | Heading skeleton + `sectionHash` per heading | Sub-KB even for 5000-line files |
144
+ | `heading.find` | All matches (line, level, `sectionHash`) | Returns all — caller disambiguates |
145
+ | `block.find` | Block ref location + `blockHash` | Structural-type aware (list/table/paragraph) |
146
+ | `frontmatter.get` | Whole frontmatter or single nested key | YAML-aware |
147
+ | `tags.list` | Tags from frontmatter + body | Code-fence aware |
148
+ | `links.get` | Outlinks, backlinks, or both | Typed: wiki/embed/header/block/markdown |
149
+ | `metadata.read` | Frontmatter + headings + tags + links + hashes | One-shot context dump |
150
+ | `search.content` | Paged full-text matches with per-line hashes | DSL: `tag:`, `path:`, `\"phrase\"`, AND/OR/NOT |
151
+
152
+ ### Write tools — surgical primaries
153
+
154
+ | Tool | Shape | Why |
155
+ | ------------- | ------------------------------------------------------------ | ---------------------------------------------------------------- |
156
+ | `str_replace` | `{file, find, replace, occurrence?, expected_content_hash?}` | The default editing verb — quote what you see |
157
+ | `apply_patch` | Unified diff | Multi-hunk edits in one shot; context lines act as preconditions |
158
+ | `apply_edits` | `[{find, replace, occurrence?}, ...]` | Multi-edit, atomic per file |
159
+
160
+ ### Write tools — structural (when you have the address)
161
+
162
+ | Tool | Notes |
163
+ | ---------------------- | ----------------------------------------------------------- |
164
+ | `heading.replace_body` | Requires `expected_section_hash` |
165
+ | `heading.rename` | Optionally update wiki-link references |
166
+ | `block.replace` | Requires `expected_block_hash`; preserves list/table prefix |
167
+ | `block.rename` | Renames a `^id` and updates references |
168
+ | `frontmatter.set` | Nested key path; YAML-safe round-trip |
169
+ | `frontmatter.delete` | Nested key path |
170
+ | `lines.replace` | Requires `expected_range_hash` |
171
+ | `lines.insert` | Insert at line N |
172
+
173
+ ### Write tools — whole-file & metadata
174
+
175
+ | Tool | Notes |
176
+ | -------------- | --------------------------------------------------------------------------- |
177
+ | `file.create` | Create-only — errors if file exists |
178
+ | `file.replace` | Whole-file rewrite — heavy, requires `expected_content_hash` |
179
+ | `file.append` | Cheap, no read needed |
180
+ | `file.move` | Default `on_conflict: error`; alternatives: `overwrite`, `rename` |
181
+ | `file.delete` | Defaults to `.obsidian/trash`; hard delete requires `expected_content_hash` |
182
+
183
+ ### Power & batch
184
+
185
+ | Tool | Notes |
186
+ | --------------- | ------------------------------------------------------------------------- |
187
+ | `bulk.apply` | Multi-file, multi-op batch. `atomic: true` → snapshot + rollback on error |
188
+ | `regex.replace` | Two-step: server returns proposal token + diff → caller confirms |
189
+ | `file.diff` | Diff between two `contentHash` versions (when cache has them) |
138
190
 
139
191
  ### Prompts
140
192
 
141
- Place markdown files in a `Prompts/` folder in any vault, tagged with `mcp-tools-prompt` in frontmatter:
193
+ Place markdown in any vault's `Prompts/` folder with `mcp-tools-prompt` in the frontmatter; Templater-style `<% tp.mcpTools.prompt(name, hint) %>` placeholders become MCP prompt arguments automatically.
194
+
195
+ ## Concurrency safety
142
196
 
143
- ```markdown
144
- ---
145
- tags: [mcp-tools-prompt]
146
- description: Summarize the daily note
147
- ---
197
+ Every read returns one or more hashes. Every write that operates on an existing range requires the matching `expected_*_hash`. If the file changed underneath you (a human edit in Obsidian, a parallel tool call, etc.), the write returns:
148
198
 
149
- Summarize what happened on <% tp.mcpTools.prompt("date", "Date to summarize") %>.
199
+ ```json
200
+ {
201
+ "ok": false,
202
+ "error": {
203
+ "code": "STALE_PRECONDITION",
204
+ "current_content_hash": "sha256:…",
205
+ "current_section_hash": "sha256:…"
206
+ }
207
+ }
150
208
  ```
151
209
 
152
- Prompts appear automatically in your MCP client's prompt selector.
153
-
154
- ## Roadmap
155
-
156
- - [x] Zero-dependency MCP protocol implementation
157
- - [x] Full vault CRUD (list, read, create, append, patch, delete)
158
- - [x] Full-text search across vault
159
- - [x] Multi-vault support
160
- - [x] Cross-platform (runs anywhere Node.js runs)
161
- - [x] Prompt templates from vault's Prompts folder
162
- - [x] Config file support (`~/.config/obsidian-native-mcp/vaults.json`)
163
- - [x] Obsidian community plugin with settings UI
164
- - [x] Vault auto-discovery from Obsidian config
165
- - [x] HTTP/SSE transport for plugin
166
- - [x] Two distribution methods (plugin + CLI)
167
- - [ ] Smart Connections-like semantic search (local embeddings)
168
- - [ ] Vault change watching (file system events)
169
- - [ ] npm publish workflow
210
+ The model refreshes from the new hash and retries. No silent clobbering.
170
211
 
171
- ## Security
212
+ ## Permissions
213
+
214
+ - **Read-only mode** — plugin toggle or CLI `--read-only` flag disables every write tool.
215
+ - **Per-tool toggle** — disable individual tools (e.g., turn off `file.delete` for less-trusted clients).
216
+ - **Per-vault subdir allow/deny** — limit a client to a vault subtree.
217
+
218
+ ## Audit log
172
219
 
173
- obsidian-native-mcp runs locally on your machine. The plugin exposes vaults over localhost only. The CLI communicates over local stdio. No data is sent to external services. Only vaults you explicitly select/configure are accessible.
220
+ Every mutating call appends one JSONL line to `<vault>/.obsidian/plugins/obsidian-native-mcp/audit.log`:
221
+
222
+ ```json
223
+ {
224
+ "ts": "2026-05-21T13:00:00Z",
225
+ "tool": "str_replace",
226
+ "vault": "notes",
227
+ "file": "Daily/2026-05-21.md",
228
+ "args_hash": "sha256:…",
229
+ "before_hash": "sha256:…",
230
+ "after_hash": "sha256:…",
231
+ "dry_run": false,
232
+ "ok": true
233
+ }
234
+ ```
235
+
236
+ Rotates at 5 MB by default.
237
+
238
+ ## Security
174
239
 
175
- For security concerns, please open an issue or see [SECURITY.md](SECURITY.md).
240
+ - Runs locally only loopback (`127.0.0.1`) for HTTP, stdio for CLI.
241
+ - HTTP transport requires a startup-generated bearer token in the SSE URL.
242
+ - Origin header allowlist enforced; CORS is not `*`.
243
+ - Request bodies capped at 5 MB; max-sessions and idle TTL applied.
244
+ - Path-traversal protection on every vault-relative path.
245
+ - Only vaults you explicitly select are accessible.
176
246
 
177
247
  ## License
178
248
 
@@ -0,0 +1,99 @@
1
+ /**
2
+ * JSONL audit log for mutating tool calls.
3
+ *
4
+ * <vault>/.obsidian/plugins/obsidian-native-mcp/audit.log
5
+ *
6
+ * Each entry contains: ts, tool, vault, file, args_hash, before_hash,
7
+ * after_hash, dry_run, client_id. We don't log raw args (could leak note
8
+ * bodies); we hash them.
9
+ *
10
+ * Rotation: when the file exceeds 10MB, rename to `audit.log.1` (keep up to
11
+ * 5 rotations).
12
+ */
13
+ import * as fs from "node:fs/promises";
14
+ import * as path from "node:path";
15
+ import { createHash } from "node:crypto";
16
+ import { ensureDir } from "../fs/io.js";
17
+ const REL = ".obsidian/plugins/obsidian-native-mcp/audit.log";
18
+ const MAX_BYTES = 10 * 1024 * 1024;
19
+ const MAX_ROTATIONS = 5;
20
+ export class AuditLog {
21
+ vaultRoot;
22
+ constructor(vaultRoot) {
23
+ this.vaultRoot = vaultRoot;
24
+ }
25
+ async append(entry) {
26
+ const filePath = path.join(this.vaultRoot, REL);
27
+ await ensureDir(path.dirname(filePath));
28
+ await this.rotateIfNeeded(filePath);
29
+ const line = JSON.stringify({ ts: new Date().toISOString(), ...entry }) + "\n";
30
+ await fs.appendFile(filePath, line, "utf-8");
31
+ }
32
+ async tail(n = 100) {
33
+ const filePath = path.join(this.vaultRoot, REL);
34
+ let text;
35
+ try {
36
+ text = await fs.readFile(filePath, "utf-8");
37
+ }
38
+ catch {
39
+ return [];
40
+ }
41
+ const lines = text.split("\n").filter((l) => l.length > 0);
42
+ const slice = lines.slice(-n);
43
+ const out = [];
44
+ for (const l of slice) {
45
+ try {
46
+ out.push(JSON.parse(l));
47
+ }
48
+ catch {
49
+ /* skip malformed line */
50
+ }
51
+ }
52
+ return out;
53
+ }
54
+ static hashArgs(args) {
55
+ const stable = stableStringify(args);
56
+ const h = createHash("sha256");
57
+ h.update(stable, "utf-8");
58
+ return `sha256:${h.digest("hex")}`;
59
+ }
60
+ async rotateIfNeeded(filePath) {
61
+ let stat;
62
+ try {
63
+ stat = await fs.stat(filePath);
64
+ }
65
+ catch {
66
+ return;
67
+ }
68
+ if (stat.size < MAX_BYTES)
69
+ return;
70
+ // Shift .N → .(N+1)
71
+ for (let i = MAX_ROTATIONS - 1; i >= 1; i--) {
72
+ const from = `${filePath}.${i}`;
73
+ const to = `${filePath}.${i + 1}`;
74
+ try {
75
+ await fs.rename(from, to);
76
+ }
77
+ catch {
78
+ /* ignore */
79
+ }
80
+ }
81
+ try {
82
+ await fs.rename(filePath, `${filePath}.1`);
83
+ }
84
+ catch {
85
+ /* ignore */
86
+ }
87
+ }
88
+ }
89
+ function stableStringify(value) {
90
+ if (value === null || typeof value !== "object")
91
+ return JSON.stringify(value);
92
+ if (Array.isArray(value)) {
93
+ return "[" + value.map((v) => stableStringify(v)).join(",") + "]";
94
+ }
95
+ const obj = value;
96
+ const keys = Object.keys(obj).sort();
97
+ return "{" + keys.map((k) => JSON.stringify(k) + ":" + stableStringify(obj[k])).join(",") + "}";
98
+ }
99
+ //# sourceMappingURL=log.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"log.js","sourceRoot":"","sources":["../../src/audit/log.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Simple bounded LRU file cache (Design B from DESIGN_V1.md §11).
3
+ *
4
+ * Validation: an entry is reused only if both `mtimeMs` and `size` match the
5
+ * current `fs.stat` result. On any write the server performs, we refresh the
6
+ * entry inline using the post-write text in memory (avoiding a re-parse from
7
+ * disk).
8
+ */
9
+ import * as fs from "node:fs/promises";
10
+ import { readText } from "../fs/io.js";
11
+ import { parseFile } from "../markdown/parse-file.js";
12
+ import { toPosix } from "../fs/paths.js";
13
+ import * as path from "node:path";
14
+ export class LRUFileCache {
15
+ cache = new Map();
16
+ maxEntries;
17
+ constructor(maxEntries = 256) {
18
+ this.maxEntries = maxEntries;
19
+ }
20
+ async get(vaultRoot, absPath) {
21
+ const stat = await fs.stat(absPath);
22
+ const cached = this.cache.get(absPath);
23
+ if (cached && cached.mtimeMs === stat.mtimeMs && cached.size === stat.size) {
24
+ this.touch(absPath);
25
+ return cached;
26
+ }
27
+ const rawText = await readText(absPath);
28
+ const relPath = toPosix(path.relative(vaultRoot, absPath));
29
+ const parsed = parseFile({ path: relPath, absPath, rawText, stat });
30
+ this.put(absPath, parsed);
31
+ return parsed;
32
+ }
33
+ invalidate(absPath) {
34
+ this.cache.delete(absPath);
35
+ }
36
+ refreshAfterWrite(vaultRoot, absPath, text, stat) {
37
+ const relPath = toPosix(path.relative(vaultRoot, absPath));
38
+ const parsed = parseFile({ path: relPath, absPath, rawText: text, stat });
39
+ this.put(absPath, parsed);
40
+ return parsed;
41
+ }
42
+ size() {
43
+ return this.cache.size;
44
+ }
45
+ clear() {
46
+ this.cache.clear();
47
+ }
48
+ touch(absPath) {
49
+ const v = this.cache.get(absPath);
50
+ if (v === undefined)
51
+ return;
52
+ this.cache.delete(absPath);
53
+ this.cache.set(absPath, v);
54
+ }
55
+ put(absPath, parsed) {
56
+ this.cache.delete(absPath);
57
+ this.cache.set(absPath, parsed);
58
+ while (this.cache.size > this.maxEntries) {
59
+ const firstKey = this.cache.keys().next().value;
60
+ if (firstKey === undefined)
61
+ break;
62
+ this.cache.delete(firstKey);
63
+ }
64
+ }
65
+ }
66
+ //# sourceMappingURL=file-cache.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"file-cache.js","sourceRoot":"","sources":["../../src/cache/file-cache.ts"],"names":[],"mappings":"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"}
package/dist/cli/index.js CHANGED
@@ -1,40 +1,147 @@
1
1
  #!/usr/bin/env node
2
- "use strict";
3
- Object.defineProperty(exports, "__esModule", { value: true });
4
- const server_1 = require("../mcp/server");
5
- const stdio_transport_1 = require("../mcp/stdio-transport");
6
- const log_1 = require("../utils/log");
7
- const vaults_1 = require("../utils/vaults");
8
- const log = (0, log_1.createLogger)("cli");
9
- async function main() {
10
- const registry = new vaults_1.VaultRegistry();
11
- if (process.argv.includes("--version") || process.argv.includes("-v")) {
12
- console.log("0.2.0");
13
- process.exit(0);
2
+ /**
3
+ * CLI entry point: stdio MCP server using the official @modelcontextprotocol/sdk.
4
+ *
5
+ * Usage:
6
+ * obsidian-native-mcp [--read-only] [--vault name=path]... [--config path]
7
+ *
8
+ * Vault resolution order: --vault flags → $OBSIDIAN_VAULT_PATHS env
9
+ * ~/.config/obsidian-native-mcp/vaults.json
10
+ * (Obsidian auto-discovery is skipped in CLI mode — configure vaults explicitly)
11
+ */
12
+ import { readFileSync } from "node:fs";
13
+ import { fileURLToPath } from "node:url";
14
+ import * as path from "node:path";
15
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
16
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
17
+ import { CallToolRequestSchema, ListToolsRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
18
+ import { VaultRegistry } from "../vault/registry.js";
19
+ import { Permissions, DEFAULT_PERMISSIONS } from "../vault/permissions.js";
20
+ import { LRUFileCache } from "../cache/file-cache.js";
21
+ import { AuditLog } from "../audit/log.js";
22
+ import { ToolRegistry } from "../handlers/registry.js";
23
+ import { registerAll } from "../tools/index.js";
24
+ import { FsPromptsProvider } from "../prompts/provider.js";
25
+ function parseFlags(argv) {
26
+ const flags = { readOnly: false, initialVaults: [] };
27
+ for (let i = 0; i < argv.length; i++) {
28
+ const a = argv[i];
29
+ if (a === "--read-only")
30
+ flags.readOnly = true;
31
+ else if (a === "--config")
32
+ flags.configPath = argv[++i];
33
+ else if (a === "--vault") {
34
+ const v = argv[++i];
35
+ const eq = v?.indexOf("=") ?? -1;
36
+ if (eq === -1) {
37
+ process.stderr.write(`--vault expects name=path, got: ${v}\n`);
38
+ process.exit(2);
39
+ }
40
+ flags.initialVaults.push({ name: v.slice(0, eq), root: path.resolve(v.slice(eq + 1)) });
41
+ }
42
+ else if (a === "--version" || a === "-V") {
43
+ process.stdout.write(readPkgVersion() + "\n");
44
+ process.exit(0);
45
+ }
46
+ else if (a === "--help" || a === "-h") {
47
+ process.stdout.write("obsidian-native-mcp [--read-only] [--vault name=path]... [--config path]\n");
48
+ process.exit(0);
49
+ }
14
50
  }
15
- const vaults = registry.list();
16
- const validation = registry.validate();
17
- log.info("starting obsidian-native-mcp", {
18
- transport: "stdio",
19
- vaultCount: vaults.length,
20
- source: registry.getSource(),
21
- });
22
- if (vaults.length === 0) {
23
- log.warn("no vaults configured", {
24
- hint: "Set OBSIDIAN_VAULT_PATHS or ~/.config/obsidian-native-mcp/vaults.json",
25
- });
51
+ return flags;
52
+ }
53
+ function readPkgVersion() {
54
+ try {
55
+ const here = path.dirname(fileURLToPath(import.meta.url));
56
+ const pkgPath = path.resolve(here, "..", "..", "package.json");
57
+ const raw = readFileSync(pkgPath, "utf-8");
58
+ const json = JSON.parse(raw);
59
+ return json.version ?? "0.0.0";
26
60
  }
27
- for (const vault of validation.missing) {
28
- log.warn("configured vault path does not exist", { vault: vault.name, path: vault.path });
61
+ catch {
62
+ return "0.0.0";
29
63
  }
30
- const server = (0, server_1.createServer)(registry);
31
- const transport = new stdio_transport_1.StdioTransport();
32
- transport.onRequest(async (msg) => server.handleRequest(msg));
33
- transport.start();
34
- log.info("stdio transport ready", { protocol: "jsonl" });
35
64
  }
36
- main().catch((err) => {
37
- log.error("startup failed", { error: (0, log_1.formatError)(err) });
65
+ async function main() {
66
+ const flags = parseFlags(process.argv.slice(2));
67
+ const version = readPkgVersion();
68
+ // CLI mode: skip Obsidian auto-discovery — user configures vaults via env/flags.
69
+ const registry = await VaultRegistry.discover({
70
+ initial: flags.initialVaults,
71
+ skipObsidian: true,
72
+ });
73
+ if (registry.count() === 0) {
74
+ process.stderr.write("error: no vaults configured\n");
75
+ process.stderr.write(" set OBSIDIAN_VAULT_PATHS=/path/to/vault\n");
76
+ process.stderr.write(" or pass --vault name=/path/to/vault\n");
77
+ process.exit(2);
78
+ }
79
+ const perms = new Permissions({ ...DEFAULT_PERMISSIONS, readOnly: flags.readOnly });
80
+ const cache = new LRUFileCache();
81
+ const audit = new AuditLog(registry.list()[0].root);
82
+ const toolReg = new ToolRegistry();
83
+ registerAll(toolReg);
84
+ const promptsProvider = new FsPromptsProvider(registry);
85
+ // ------------------------------------------------------------------
86
+ // Wire everything into the official MCP SDK Server
87
+ // ------------------------------------------------------------------
88
+ const server = new Server({ name: "obsidian-native-mcp", version }, { capabilities: { tools: {}, prompts: {} } });
89
+ // tools/list
90
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
91
+ const tools = toolReg
92
+ .list((name) => perms.isToolEnabled(name))
93
+ .map((t) => ({
94
+ name: t.name,
95
+ description: t.summary,
96
+ inputSchema: t.schema,
97
+ }));
98
+ return { tools };
99
+ });
100
+ // tools/call
101
+ server.setRequestHandler(CallToolRequestSchema, async (req) => {
102
+ const name = req.params.name;
103
+ const args = (req.params.arguments ?? {});
104
+ const vaultName = typeof args.vault === "string" ? args.vault : undefined;
105
+ let vault;
106
+ try {
107
+ vault = registry.resolve(vaultName);
108
+ }
109
+ catch (e) {
110
+ return {
111
+ isError: true,
112
+ content: [{ type: "text", text: e.message }],
113
+ };
114
+ }
115
+ const ctx = { vault, perms, cache, audit, registry, clientId: "stdio" };
116
+ const result = await toolReg.invoke(name, args, ctx);
117
+ if (!result.ok) {
118
+ return {
119
+ isError: true,
120
+ content: [{ type: "text", text: JSON.stringify(result.error) }],
121
+ };
122
+ }
123
+ return {
124
+ content: [{ type: "text", text: JSON.stringify(result.result) }],
125
+ };
126
+ });
127
+ // prompts/list
128
+ server.setRequestHandler(ListPromptsRequestSchema, async () => {
129
+ const prompts = await promptsProvider.list();
130
+ return { prompts };
131
+ });
132
+ // prompts/get
133
+ server.setRequestHandler(GetPromptRequestSchema, async (req) => {
134
+ const out = await promptsProvider.get(req.params.name, req.params.arguments);
135
+ return out;
136
+ });
137
+ // ------------------------------------------------------------------
138
+ // Start with the official stdio transport
139
+ // ------------------------------------------------------------------
140
+ const transport = new StdioServerTransport();
141
+ await server.connect(transport);
142
+ }
143
+ main().catch((e) => {
144
+ process.stderr.write(`fatal: ${e.message}\n`);
38
145
  process.exit(1);
39
146
  });
40
147
  //# sourceMappingURL=index.js.map