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.
- package/.github/workflows/ci.yml +66 -38
- package/.github/workflows/release.yml +152 -0
- package/.husky/commit-msg +18 -0
- package/.nvmrc +1 -0
- package/CHANGELOG.md +3 -3
- package/DEVELOPER.md +158 -106
- package/README.md +137 -67
- package/dist/audit/log.js +99 -0
- package/dist/audit/log.js.map +1 -0
- package/dist/cache/file-cache.js +66 -0
- package/dist/cache/file-cache.js.map +1 -0
- package/dist/cli/index.js +139 -32
- package/dist/cli/index.js.map +1 -1
- package/dist/fs/io.js +68 -0
- package/dist/fs/io.js.map +1 -0
- package/dist/fs/paths.js +41 -0
- package/dist/fs/paths.js.map +1 -0
- package/dist/fs/trash.js +20 -0
- package/dist/fs/trash.js.map +1 -0
- package/dist/fs/walk.js +73 -0
- package/dist/fs/walk.js.map +1 -0
- package/dist/handlers/args.js +97 -0
- package/dist/handlers/args.js.map +1 -0
- package/dist/handlers/registry.js +54 -0
- package/dist/handlers/registry.js.map +1 -0
- package/dist/markdown/blocks.js +95 -0
- package/dist/markdown/blocks.js.map +1 -0
- package/dist/markdown/fingerprint.js +99 -0
- package/dist/markdown/fingerprint.js.map +1 -0
- package/dist/markdown/frontmatter.js +153 -0
- package/dist/markdown/frontmatter.js.map +1 -0
- package/dist/markdown/headings.js +104 -0
- package/dist/markdown/headings.js.map +1 -0
- package/dist/markdown/links.js +93 -0
- package/dist/markdown/links.js.map +1 -0
- package/dist/markdown/outline.js +12 -0
- package/dist/markdown/outline.js.map +1 -0
- package/dist/markdown/parse-file.js +41 -0
- package/dist/markdown/parse-file.js.map +1 -0
- package/dist/markdown/parse.js +49 -0
- package/dist/markdown/parse.js.map +1 -0
- package/dist/markdown/tags.js +40 -0
- package/dist/markdown/tags.js.map +1 -0
- package/dist/mcp/framing.js +56 -0
- package/dist/mcp/framing.js.map +1 -0
- package/dist/mcp/http.js +240 -0
- package/dist/mcp/http.js.map +1 -0
- package/dist/mcp/protocol.js +16 -56
- package/dist/mcp/protocol.js.map +1 -1
- package/dist/mcp/server.js +105 -268
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/stdio.js +38 -0
- package/dist/mcp/stdio.js.map +1 -0
- package/dist/mcp/transport.js +4 -2
- package/dist/mcp/transport.js.map +1 -1
- package/dist/plugin/main.js +21956 -1109
- package/dist/plugin/main.js.map +1 -1
- package/dist/plugin/manifest.json +1 -1
- package/dist/plugin/settings.js +149 -41
- package/dist/plugin/settings.js.map +1 -1
- package/dist/prompts/provider.js +96 -0
- package/dist/prompts/provider.js.map +1 -0
- package/dist/tools/common.js +84 -0
- package/dist/tools/common.js.map +1 -0
- package/dist/tools/index.js +41 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/read.js +631 -0
- package/dist/tools/read.js.map +1 -0
- package/dist/tools/write-basic.js +319 -0
- package/dist/tools/write-basic.js.map +1 -0
- package/dist/tools/write-bulk.js +331 -0
- package/dist/tools/write-bulk.js.map +1 -0
- package/dist/tools/write-patch.js +152 -0
- package/dist/tools/write-patch.js.map +1 -0
- package/dist/tools/write-structural.js +389 -0
- package/dist/tools/write-structural.js.map +1 -0
- package/dist/tools/write-surgical.js +349 -0
- package/dist/tools/write-surgical.js.map +1 -0
- package/dist/utils/log.js +2 -6
- package/dist/utils/log.js.map +1 -1
- package/dist/utils/types.js +19 -0
- package/dist/utils/types.js.map +1 -0
- package/dist/vault/permissions.js +73 -0
- package/dist/vault/permissions.js.map +1 -0
- package/dist/vault/registry.js +164 -0
- package/dist/vault/registry.js.map +1 -0
- package/eslint.config.mjs +2 -2
- package/manifest.json +1 -1
- package/package.json +18 -2
- package/renovate.json +6 -0
- package/scripts/start-mcp.sh +6 -0
- package/src/audit/log.ts +110 -0
- package/src/cache/file-cache.ts +92 -0
- package/src/cli/index.ts +155 -31
- package/src/fs/io.ts +69 -0
- package/src/fs/paths.ts +45 -0
- package/src/fs/trash.ts +22 -0
- package/src/fs/walk.ts +95 -0
- package/src/handlers/args.ts +129 -0
- package/src/handlers/registry.ts +100 -0
- package/src/markdown/blocks.ts +108 -0
- package/src/markdown/fingerprint.ts +112 -0
- package/src/markdown/frontmatter.ts +173 -0
- package/src/markdown/headings.ts +116 -0
- package/src/markdown/links.ts +99 -0
- package/src/markdown/outline.ts +18 -0
- package/src/markdown/parse-file.ts +51 -0
- package/src/markdown/parse.ts +53 -0
- package/src/markdown/tags.ts +42 -0
- package/src/mcp/framing.ts +59 -0
- package/src/mcp/http.ts +275 -0
- package/src/mcp/protocol.ts +41 -81
- package/src/mcp/server.ts +150 -278
- package/src/mcp/stdio.ts +41 -0
- package/src/mcp/transport.ts +12 -5
- package/src/plugin/main.ts +104 -86
- package/src/plugin/settings.ts +176 -44
- package/src/prompts/provider.ts +98 -0
- package/src/tools/common.ts +106 -0
- package/src/tools/index.ts +60 -0
- package/src/tools/read.ts +662 -0
- package/src/tools/write-basic.ts +330 -0
- package/src/tools/write-bulk.ts +355 -0
- package/src/tools/write-patch.ts +166 -0
- package/src/tools/write-structural.ts +409 -0
- package/src/tools/write-surgical.ts +378 -0
- package/src/utils/types.ts +147 -0
- package/src/vault/permissions.ts +94 -0
- package/src/vault/registry.ts +191 -0
- package/tests/fixtures/vaults/agents/AGENTS.md +16 -0
- package/tests/fixtures/vaults/code-heavy/code-heavy.md +46 -0
- package/tests/fixtures/vaults/daily-note/Daily/2026-05-21.md +21 -0
- package/tests/fixtures/vaults/dup-headings/dup-headings.md +17 -0
- package/tests/fixtures/vaults/frontmatter-stress/fm.md +17 -0
- package/tests/fixtures/vaults/large-kb/big.md +5501 -0
- package/tests/fixtures/vaults/links-zoo/Target.md +3 -0
- package/tests/fixtures/vaults/links-zoo/source.md +13 -0
- package/tests/fixtures/vaults/tiny/tiny.md +3 -0
- package/tests/helpers/sandbox.ts +107 -0
- package/tests/integration/apply-edits.test.ts +56 -0
- package/tests/integration/audit.test.ts +66 -0
- package/tests/integration/blocks.test.ts +71 -0
- package/tests/integration/file-ops.test.ts +78 -0
- package/tests/integration/links.test.ts +65 -0
- package/tests/integration/permissions.test.ts +72 -0
- package/tests/scenarios/S1-mark-tasks-done.test.ts +69 -0
- package/tests/scenarios/S10-code-fence-safety.test.ts +78 -0
- package/tests/scenarios/S11-no-fabrication.test.ts +72 -0
- package/tests/scenarios/S12-apply-patch.test.ts +92 -0
- package/tests/scenarios/S13-search-pagination.test.ts +35 -0
- package/tests/scenarios/S14-read-write-roundtrip.test.ts +76 -0
- package/tests/scenarios/S15-byte-budget.test.ts +77 -0
- package/tests/scenarios/S2-refactor-section.test.ts +87 -0
- package/tests/scenarios/S4-S5-concurrent.test.ts +85 -0
- package/tests/scenarios/S6-S7-large-file.test.ts +77 -0
- package/tests/scenarios/S8-bulk-atomic.test.ts +105 -0
- package/tests/scenarios/S9-frontmatter-nested.test.ts +63 -0
- package/tests/unit/fingerprint.test.ts +85 -0
- package/tests/unit/frontmatter.test.ts +78 -0
- package/tests/unit/headings.test.ts +66 -0
- package/tsconfig.json +5 -3
- package/dist/handlers/prompts.js +0 -127
- package/dist/handlers/prompts.js.map +0 -1
- package/dist/handlers/tools.js +0 -113
- package/dist/handlers/tools.js.map +0 -1
- package/dist/mcp/http-transport.js +0 -142
- package/dist/mcp/http-transport.js.map +0 -1
- package/dist/mcp/stdio-transport.js +0 -49
- package/dist/mcp/stdio-transport.js.map +0 -1
- package/dist/utils/fs-utils.js +0 -268
- package/dist/utils/fs-utils.js.map +0 -1
- package/dist/utils/search.js +0 -62
- package/dist/utils/search.js.map +0 -1
- package/dist/utils/vaults.js +0 -179
- package/dist/utils/vaults.js.map +0 -1
- package/src/handlers/prompts.ts +0 -148
- package/src/handlers/tools.ts +0 -146
- package/src/mcp/http-transport.ts +0 -159
- package/src/mcp/stdio-transport.ts +0 -54
- package/src/utils/fs-utils.ts +0 -358
- package/src/utils/search.ts +0 -84
- package/src/utils/vaults.ts +0 -198
- package/tests/http-transport.test.ts +0 -111
- 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
|
-
**
|
|
6
|
-
|
|
5
|
+
**LLM-optimized MCP server for Obsidian vaults**
|
|
6
|
+
Surgical edits, hash-based concurrency safety, no whole-file rewrites.
|
|
7
7
|
|
|
8
8
|
[](https://github.com/usrivastava92/obsidian-native-mcp/actions)
|
|
9
9
|
[](https://github.com/usrivastava92/obsidian-native-mcp/releases)
|
|
10
10
|
[](https://www.npmjs.com/package/obsidian-native-mcp)
|
|
11
|
-
[](https://www.npmjs.com/package/obsidian-native-mcp)
|
|
12
11
|
[](LICENSE)
|
|
13
|
-
[](CONTRIBUTING.md)
|
|
14
12
|
|
|
15
13
|
</div>
|
|
16
14
|
|
|
17
|
-
|
|
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**
|
|
22
|
-
- **CLI** — standalone
|
|
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
|
-
|
|
22
|
+
## Why Obsidian Native MCP
|
|
25
23
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
|
29
|
-
|
|
|
30
|
-
|
|
|
31
|
-
|
|
|
32
|
-
|
|
|
33
|
-
|
|
|
34
|
-
|
|
|
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
|
|
43
|
-
4.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
|
130
|
-
|
|
|
131
|
-
| `
|
|
132
|
-
| `
|
|
133
|
-
| `
|
|
134
|
-
| `
|
|
135
|
-
| `
|
|
136
|
-
|
|
137
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
28
|
-
|
|
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()
|
|
37
|
-
|
|
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
|