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
@@ -2,74 +2,102 @@ name: CI
2
2
 
3
3
  on:
4
4
  push:
5
- branches: [main]
5
+ branches:
6
+ - main
7
+ - master
8
+ - develop
9
+ - "feature/**"
10
+ - "renovate/**"
6
11
  pull_request:
7
- branches: [main]
12
+ workflow_dispatch:
8
13
 
9
14
  permissions:
10
15
  contents: read
11
16
 
12
17
  jobs:
13
- lint:
14
- name: Lint and Type Check
18
+ setup:
19
+ name: Setup
15
20
  runs-on: ubuntu-latest
21
+ if: github.event_name != 'push' || !contains(github.event.head_commit.message, '[skip ci]')
16
22
  steps:
17
23
  - uses: actions/checkout@v4
18
24
  - uses: actions/setup-node@v4
19
25
  with:
20
- node-version: "22"
26
+ node-version-file: .nvmrc
27
+ cache: npm
28
+ - run: npm ci
29
+
30
+ format:
31
+ name: Format
32
+ needs: setup
33
+ runs-on: ubuntu-latest
34
+ if: github.event_name != 'push' || !contains(github.event.head_commit.message, '[skip ci]')
35
+ steps:
36
+ - uses: actions/checkout@v4
37
+ - uses: actions/setup-node@v4
38
+ with:
39
+ node-version-file: .nvmrc
21
40
  cache: npm
22
41
  - run: npm ci
23
- - run: npm run lint
24
42
  - run: npm run format:check
25
- - run: npm test
26
- - run: npm run check
27
43
 
28
- build:
29
- name: Build
30
- needs: lint
44
+ lint:
45
+ name: Lint
46
+ needs: setup
31
47
  runs-on: ubuntu-latest
48
+ if: github.event_name != 'push' || !contains(github.event.head_commit.message, '[skip ci]')
32
49
  steps:
33
50
  - uses: actions/checkout@v4
34
51
  - uses: actions/setup-node@v4
35
52
  with:
36
- node-version: "22"
53
+ node-version-file: .nvmrc
37
54
  cache: npm
38
55
  - run: npm ci
39
- - run: npm run build
40
- - run: npm run build:plugin
41
- - uses: actions/upload-artifact@v4
56
+ - run: npm run lint
57
+
58
+ typecheck:
59
+ name: Typecheck
60
+ needs: setup
61
+ runs-on: ubuntu-latest
62
+ if: github.event_name != 'push' || !contains(github.event.head_commit.message, '[skip ci]')
63
+ steps:
64
+ - uses: actions/checkout@v4
65
+ - uses: actions/setup-node@v4
42
66
  with:
43
- name: dist
44
- path: dist/
67
+ node-version-file: .nvmrc
68
+ cache: npm
69
+ - run: npm ci
70
+ - run: npm run check
45
71
 
46
- release:
47
- name: Release
48
- if: github.event_name == 'push' && github.ref == 'refs/heads/main'
49
- needs: build
72
+ test:
73
+ name: Test
74
+ needs: setup
50
75
  runs-on: ubuntu-latest
51
- permissions:
52
- contents: write
53
- issues: write
54
- pull-requests: write
55
- id-token: write
76
+ if: github.event_name != 'push' || !contains(github.event.head_commit.message, '[skip ci]')
56
77
  steps:
57
78
  - uses: actions/checkout@v4
79
+ - uses: actions/setup-node@v4
58
80
  with:
59
- fetch-depth: 0
60
- persist-credentials: false
81
+ node-version-file: .nvmrc
82
+ cache: npm
83
+ - run: npm ci
84
+ - run: npm test
85
+
86
+ build:
87
+ name: Build
88
+ needs: [format, lint, typecheck, test]
89
+ runs-on: ubuntu-latest
90
+ if: github.event_name != 'push' || !contains(github.event.head_commit.message, '[skip ci]')
91
+ steps:
92
+ - uses: actions/checkout@v4
61
93
  - uses: actions/setup-node@v4
62
94
  with:
63
- node-version: "22"
95
+ node-version-file: .nvmrc
64
96
  cache: npm
65
97
  - run: npm ci
66
- - name: Install npm with OIDC support
67
- run: npm install --no-save npm@11.5.1
68
98
  - run: npm run build
69
- - name: Semantic Release
70
- env:
71
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
72
- run: |
73
- export PATH="$PWD/node_modules/.bin:$PATH"
74
- npm --version
75
- npx semantic-release
99
+ - run: npm run build:plugin
100
+ - uses: actions/upload-artifact@v4
101
+ with:
102
+ name: dist
103
+ path: dist/
@@ -0,0 +1,152 @@
1
+ name: Release
2
+
3
+ on:
4
+ workflow_run:
5
+ workflows: ["CI"]
6
+ types:
7
+ - completed
8
+ workflow_dispatch:
9
+ inputs:
10
+ reason:
11
+ description: Optional note for this manual release.
12
+ required: false
13
+ default: ""
14
+
15
+ concurrency:
16
+ group: release-${{ github.event.workflow_run.head_branch || github.ref }}
17
+ cancel-in-progress: false
18
+
19
+ permissions:
20
+ checks: read
21
+ contents: write
22
+ issues: write
23
+ pull-requests: write
24
+ id-token: write
25
+
26
+ env:
27
+ RELEASE_ALLOWED_ACTORS: "usrivastava92"
28
+
29
+ jobs:
30
+ authorize:
31
+ name: Authorize actor
32
+ if: github.event_name == 'workflow_dispatch'
33
+ runs-on: ubuntu-latest
34
+ steps:
35
+ - name: Check triggering actor against allow-list
36
+ env:
37
+ ACTOR: ${{ github.triggering_actor }}
38
+ ALLOWED: ${{ env.RELEASE_ALLOWED_ACTORS }}
39
+ run: |
40
+ echo "Triggered by: $ACTOR"
41
+ echo "Allow-list: $ALLOWED"
42
+ if printf '%s' ",$ALLOWED," | tr -d ' ' | grep -q ",${ACTOR},"; then
43
+ echo "$ACTOR is authorized to trigger releases."
44
+ else
45
+ echo "$ACTOR is NOT in RELEASE_ALLOWED_ACTORS." >&2
46
+ echo "Update the allow-list in .github/workflows/release.yml if needed." >&2
47
+ exit 1
48
+ fi
49
+
50
+ verify-ci:
51
+ name: Verify CI passed
52
+ needs: authorize
53
+ if: github.event_name == 'workflow_dispatch'
54
+ runs-on: ubuntu-latest
55
+ steps:
56
+ - name: Ensure target commit passed CI build
57
+ uses: actions/github-script@v7
58
+ with:
59
+ script: |
60
+ const { owner, repo } = context.repo;
61
+ const ref = context.sha;
62
+ const { data } = await github.rest.checks.listForRef({
63
+ owner,
64
+ repo,
65
+ ref,
66
+ per_page: 100,
67
+ });
68
+
69
+ const buildCheck = data.check_runs.find((run) => run.name === 'Build');
70
+
71
+ if (!buildCheck) {
72
+ core.setFailed(`No 'Build' check found for ${ref}. Run CI on this commit before releasing.`);
73
+ return;
74
+ }
75
+
76
+ if (buildCheck.status !== 'completed' || buildCheck.conclusion !== 'success') {
77
+ core.setFailed(
78
+ `'Build' check for ${ref} is ${buildCheck.status}/${buildCheck.conclusion ?? 'null'}. ` +
79
+ `Release requires a successful CI run first.`,
80
+ );
81
+ }
82
+
83
+ release-auto:
84
+ name: Release npm package
85
+ if: |
86
+ github.event_name == 'workflow_run' &&
87
+ github.event.workflow_run.conclusion == 'success' &&
88
+ github.event.workflow_run.event == 'push' &&
89
+ github.event.workflow_run.head_branch == 'main'
90
+ runs-on: ubuntu-latest
91
+ steps:
92
+ - uses: actions/checkout@v4
93
+ with:
94
+ fetch-depth: 0
95
+ persist-credentials: false
96
+ ref: ${{ github.event.workflow_run.head_sha || github.sha }}
97
+
98
+ - uses: actions/setup-node@v4
99
+ with:
100
+ node-version-file: .nvmrc
101
+ cache: npm
102
+
103
+ - run: npm ci
104
+
105
+ - name: Install npm with OIDC support
106
+ run: npm install --no-save npm@11.5.1
107
+
108
+ - run: npm run build
109
+
110
+ - name: Semantic Release
111
+ env:
112
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
113
+ run: |
114
+ export PATH="$PWD/node_modules/.bin:$PATH"
115
+ npm --version
116
+ npx semantic-release
117
+
118
+ release-manual:
119
+ name: Release npm package (manual)
120
+ needs: verify-ci
121
+ if: github.event_name == 'workflow_dispatch'
122
+ runs-on: ubuntu-latest
123
+ steps:
124
+ - name: Record trigger reason
125
+ if: ${{ inputs.reason != '' }}
126
+ run: echo "Manual release reason - ${{ inputs.reason }}" >> "$GITHUB_STEP_SUMMARY"
127
+
128
+ - uses: actions/checkout@v4
129
+ with:
130
+ fetch-depth: 0
131
+ persist-credentials: false
132
+ ref: ${{ github.event.workflow_run.head_sha || github.sha }}
133
+
134
+ - uses: actions/setup-node@v4
135
+ with:
136
+ node-version-file: .nvmrc
137
+ cache: npm
138
+
139
+ - run: npm ci
140
+
141
+ - name: Install npm with OIDC support
142
+ run: npm install --no-save npm@11.5.1
143
+
144
+ - run: npm run build
145
+
146
+ - name: Semantic Release
147
+ env:
148
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
149
+ run: |
150
+ export PATH="$PWD/node_modules/.bin:$PATH"
151
+ npm --version
152
+ npx semantic-release
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env sh
2
+
3
+ message_file="$1"
4
+ subject="$(head -n 1 "$message_file")"
5
+
6
+ if printf '%s' "$subject" | grep -Eq '^(Merge|Revert )'; then
7
+ exit 0
8
+ fi
9
+
10
+ if printf '%s' "$subject" | grep -Eq '^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\([a-z0-9._/-]+\))?(!)?: .+'; then
11
+ exit 0
12
+ fi
13
+
14
+ echo "Invalid commit message format."
15
+ echo "Expected Conventional Commits, for example:"
16
+ echo " feat: add MCP file management tools"
17
+ echo " fix(cli): handle missing vault config"
18
+ exit 1
package/.nvmrc ADDED
@@ -0,0 +1 @@
1
+ 22
package/CHANGELOG.md CHANGED
@@ -1,6 +1,6 @@
1
- ## [1.0.3](https://github.com/usrivastava92/obsidian-native-mcp/compare/v1.0.2...v1.0.3) (2026-05-14)
1
+ # [1.2.0](https://github.com/usrivastava92/obsidian-native-mcp/compare/v1.1.0...v1.2.0) (2026-05-21)
2
2
 
3
3
 
4
- ### Bug Fixes
4
+ ### Features
5
5
 
6
- * align MCP transports with opencode and add smoke coverage ([3d4a072](https://github.com/usrivastava92/obsidian-native-mcp/commit/3d4a07282331b7251a71ff13545fcf3ced644c99))
6
+ * v1.0 complete rewrite LLM-optimized MCP server ([3215b40](https://github.com/usrivastava92/obsidian-native-mcp/commit/3215b40a12a241dcad001532efa6021d33f12465))
package/DEVELOPER.md CHANGED
@@ -1,158 +1,210 @@
1
1
  # Developer Guide
2
2
 
3
+ Engineering reference for **obsidian-native-mcp v1.0**. For the user-facing tool surface, see `README.md`. For design rationale and invariants, see `DESIGN_V1.md`.
4
+
3
5
  ## Architecture
4
6
 
5
7
  ```
6
8
  src/
7
- cli/index.ts CLI entry — reads config, starts stdio transport
9
+ cli/index.ts # CLI entry — reads config, starts stdio transport
8
10
  plugin/
9
- main.ts Obsidian plugin entry (extends Plugin)
10
- settings.ts Settings tab with vault picker + copy URL
11
+ main.ts # Obsidian plugin entry (extends Plugin)
12
+ settings.ts # Settings tab: vault picker, token, per-tool toggles, audit viewer
11
13
  mcp/
12
- protocol.ts JSON-RPC 2.0 types + Content-Length framing
13
- transport.ts Transport interface
14
- stdio-transport.ts Stdio transport (for CLI)
15
- http-transport.ts HTTP/SSE transport (for plugin)
16
- server.ts Transport-agnostic server creates handlers + routes requests
14
+ framing.ts # Content-Length framing (encode + buffered decode)
15
+ protocol.ts # JSON-RPC 2.0 types + standard error codes
16
+ transport.ts # Transport interface
17
+ stdio.ts # Stdio transport (CLI)
18
+ http.ts # HTTP/SSE transport with bearer token, origin allowlist,
19
+ # body cap, max-sessions, idle TTL, heartbeat, /healthz
20
+ server.ts # createServer() factory — transport-agnostic request router
17
21
  handlers/
18
- tools.ts Tool implementations (9 tools)
19
- prompts.ts Prompt directory reader + template parser
22
+ registry.ts # Declarative tool registry (no boilerplate per tool)
23
+ args.ts # Typed argument parsers per tool kind
24
+ tools/
25
+ common.ts # Shared types: ToolContext, ToolResult envelope
26
+ read.ts # 14 read tools
27
+ write-basic.ts # file.create, file.replace, file.append, file.move, file.delete
28
+ write-surgical.ts # str_replace, apply_edits, lines.replace, lines.insert
29
+ write-structural.ts # heading.*, block.*, frontmatter.set/delete
30
+ write-patch.ts # apply_patch (unified diff parser)
31
+ write-bulk.ts # bulk.apply (atomic), regex.replace (proposal-token)
32
+ index.ts # Aggregate registry of all tools
33
+ prompts/
34
+ provider.ts # Vault Prompts/ scanner + Templater-style arg parser
35
+ markdown/
36
+ parse.ts # mdast parsing (gfm + frontmatter, no broken wiki-link dep)
37
+ parse-file.ts # Glue: text → ParsedFile (ast + headings + blocks + links + tags + frontmatter + hashes)
38
+ fingerprint.ts # Canonical normalization + sha256 helpers, lineOffsets
39
+ headings.ts # AST-aware heading extraction, section bounds, disambiguation
40
+ blocks.ts # ^block-id extraction with structural-type classification
41
+ links.ts # Typed link extraction (wiki/embed/header/block/markdown) — fence-blind
42
+ tags.ts # Tag extraction — fence-blind, URL-fragment-blind
43
+ frontmatter.ts # YAML-backed nested get/set/delete preserving formatting
44
+ outline.ts # Skeleton derivation from HeadingInfo
45
+ cache/
46
+ file-cache.ts # LRU<path, ParsedFile> with mtime+size invalidation, write-through
47
+ fs/
48
+ io.ts # readText, writeTextAtomic, ensureDir, uniquePath, fileExists
49
+ walk.ts # Async recursive walker with ignore patterns
50
+ trash.ts # <vault>/.obsidian/trash/ move with structure preserved
51
+ paths.ts # Path-traversal guard, posix/native normalization
52
+ vault/
53
+ registry.ts # VaultRegistry: env + config file + Obsidian auto-discovery
54
+ permissions.ts # Read-only mode, per-tool toggle, per-vault subdir rules
55
+ audit/
56
+ log.ts # JSONL audit log + sha256 args hashing + 5MB rotation
20
57
  utils/
21
- fs-utils.ts File I/O, frontmatter parsing, heading/block patching
22
- search.ts Recursive text search across .md files
23
- vaults.ts VaultRegistry — config from env, file, Obsidian auto-discovery
58
+ types.ts # Canonical types: ParsedFile, HeadingInfo, BlockInfo, ExtractedLink, ToolError, etc.
59
+ log.ts # Structured stderr logger (key=value)
24
60
  ```
25
61
 
26
- ### Key Design Decisions
62
+ ### Layering rules
27
63
 
28
- **Zero dependencies.** The MCP protocol (JSON-RPC 2.0 over stdio with Content-Length framing) is implemented from scratch. No npm packages needed at runtime — only Node.js stdlib.
64
+ 1. `markdown/` and `fs/` are leaf modules no dependencies on `tools/`, `handlers/`, or `mcp/`.
65
+ 2. `cache/` depends only on `markdown/` and `fs/`.
66
+ 3. `tools/` depends on `markdown/`, `fs/`, `cache/`, `audit/`, `vault/` — never on `mcp/` or `handlers/`.
67
+ 4. `handlers/` orchestrates `tools/` and wraps results into MCP responses.
68
+ 5. `mcp/` knows nothing about tools — just transports and JSON-RPC.
29
69
 
30
- **Node.js everywhere.** All I/O uses `fs` and `fs/promises` — runs on any platform Node.js supports. Obsidian plugin runs inside Obsidian's Electron. CLI runs standalone.
70
+ ### Key design decisions
31
71
 
32
- **Two transports, shared logic.** The `server.ts` exports a `createServer()` factory that returns tool definitions and a `handleRequest()` function. The stdio and HTTP/SSE transports both use the same factory only the I/O layer differs.
72
+ - **Surgical-first edits.** `str_replace` / `apply_patch` / `apply_edits` are the workhorses. Whole-file `file.replace` is the documented escape hatch.
73
+ - **Hash-based concurrency.** Every read returns content hashes (file, range, section, block, frontmatter, line). Every write that targets an existing range requires the matching `expected_*_hash`. Stale precondition → structured `STALE_PRECONDITION` error with current hashes, never silent clobbering.
74
+ - **Server is source of truth for hashes.** Clients echo opaque strings the server gave them. We never trust client-computed hashes.
75
+ - **AST-aware everywhere.** mdast classifies code-fenced text as `code` nodes, HTML-commented blocks as `html` nodes — so heading/block/tag/link extractors get fence-safety _for free_.
76
+ - **Real YAML for frontmatter.** We use the `yaml` package for round-trip preserving nested get/set/delete. No more hand-rolled line matching.
77
+ - **Two transports, shared core.** `createServer()` returns `{handleRequest, toolDefinitions}`. Stdio and HTTP/SSE differ only in I/O layer.
78
+ - **Process-atomic bulk writes.** `bulk.apply` with `atomic: true` snapshots originals in memory, applies all ops, writes, and restores on any failure.
33
79
 
34
- **Direct filesystem access.** Unlike `obsidian-mcp-tools` which communicates via HTTP with the Local REST API plugin, this server reads/writes files directly.
80
+ ## Development workflow
35
81
 
36
- **Multi-vault first.** The `VaultRegistry` supports env var, config file, and Obsidian auto-discovery.
82
+ ```bash
83
+ npm install
84
+ npm run dev # tsx watch on the CLI
85
+ npm run check # tsc --noEmit
86
+ npm run lint # eslint src/
87
+ npm run format # prettier --write src/
88
+ npm test # node:test runner across tests/**/*.test.ts
89
+ npm run test:coverage # c8 with --check-coverage thresholds
90
+ npm run build # tsc → dist/
91
+ npm run build:plugin # esbuild plugin bundle for Obsidian
92
+ ```
37
93
 
38
- ## Development Workflow
94
+ ### Testing the CLI manually
39
95
 
40
96
  ```bash
41
- # Install dependencies
42
- npm install
97
+ OBSIDIAN_VAULT_PATHS=/path/to/vault node dist/cli/index.js
98
+ ```
43
99
 
44
- # Run CLI in dev mode (watch + hot reload)
45
- npm run dev
100
+ Then pipe in Content-Length framed JSON-RPC requests on stdin. The `tests/helpers/sandbox.ts` helper does this programmatically for tests.
46
101
 
47
- # Type-check
48
- npm run check
102
+ ### Testing the Plugin
49
103
 
50
- # Lint + format
51
- npm run lint
52
- npm run format
104
+ 1. `npm run build:plugin`
105
+ 2. Copy `dist/plugin/` to `<your-vault>/.obsidian/plugins/obsidian-native-mcp/`
106
+ 3. Reload Obsidian, enable in Community Plugins, open settings tab.
53
107
 
54
- # Build TS to dist/
55
- npm run build
108
+ ## Adding a new tool
56
109
 
57
- # Build plugin bundle for Obsidian
58
- npm run build:plugin
110
+ 1. Decide its layer: read-only, surgical write, structural write, whole-file write, or batch.
111
+ 2. Add the implementation to the right file under `src/tools/` (`read.ts`, `write-surgical.ts`, etc.).
112
+ 3. Export it via `src/tools/index.ts` so the aggregate registry picks it up.
113
+ 4. Add an argument parser in `src/handlers/args.ts` if its shape doesn't fit an existing one.
114
+ 5. Write tests:
115
+ - **Unit** for any pure helper logic (`tests/unit/...`).
116
+ - **Integration** for the tool itself (`tests/integration/<tool>.test.ts`).
117
+ - **Scenario** if it changes a multi-step LLM flow (`tests/scenarios/...`).
118
+ 6. Update the relevant table in `README.md` and the schema list in `src/tools/index.ts`.
59
119
 
60
- # Run CLI from compiled JS
61
- npm run start
120
+ ### Tool contract checklist
62
121
 
63
- # Test via env var
64
- OBSIDIAN_VAULT_PATHS=/path/to/vault node dist/cli/index.js
65
- ```
122
+ - Reads must return hashes the corresponding write expects (`contentHash`, `rangeHash`, `sectionHash`, `blockHash`, `frontmatterHash`, `lineHash`).
123
+ - Writes that target existing ranges must accept `expected_*_hash` and return `STALE_PRECONDITION` on mismatch.
124
+ - Mutating tools must support `dry_run: true` returning the _would-be_ new hashes without touching disk.
125
+ - Errors must be `{ok: false, error: {code, message, ...details}}` — never throw across the JSON-RPC boundary.
126
+ - Every mutating tool appends one audit-log line via `AuditLog`.
66
127
 
67
- ### Testing the CLI
128
+ ## Test strategy
68
129
 
69
- ```bash
70
- OBSIDIAN_VAULT_PATHS=/path/to/vault node --input-type=module -e '
71
- const msgs = [
72
- {jsonrpc:"2.0",id:1,method:"initialize",params:{}},
73
- {jsonrpc:"2.0",id:2,method:"tools/call",params:{name:"list_vaults",arguments:{}}},
74
- {jsonrpc:"2.0",id:3,method:"tools/call",params:{name:"list_files",arguments:{vault:"default"}}},
75
- ];
76
- let input = "";
77
- for(const m of msgs){ const s = JSON.stringify(m); input += "Content-Length: "+s.length+"\r\n\r\n"+s; }
78
- const {spawn} = await import("child_process");
79
- const child = spawn("node", ["dist/cli/index.js"], {stdio:["pipe","pipe","pipe"]});
80
- let buf = "";
81
- child.stdout.on("data", c => buf += c.toString());
82
- child.on("close", () => {
83
- let remaining = buf;
84
- while(true){
85
- const m = remaining.match(/^Content-Length: (\d+)\r\n\r\n/);
86
- if(!m) break;
87
- const len = parseInt(m[1]), start = m[0].length;
88
- if(remaining.length < start + len) break;
89
- console.log(JSON.parse(remaining.slice(start, start + len)));
90
- remaining = remaining.slice(start + len);
91
- }
92
- });
93
- child.stdin.write(input);
94
- child.stdin.end();
95
- setTimeout(() => process.exit(0), 2000);
96
- '
97
- ```
130
+ | Layer | Location | What |
131
+ | ----------- | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
132
+ | Unit | `tests/unit/` | Pure-function correctness for `markdown/`, `fingerprint`, `frontmatter`, `headings` |
133
+ | Integration | `tests/integration/` | One file per tool family — permissions, blocks, apply_edits, audit, links, file-ops |
134
+ | Scenario | `tests/scenarios/` | Multi-tool LLM flows: S1 mark-tasks-done, S2 refactor section, S4/S5 concurrency, S6/S7 large file, S8 bulk rollback, S9 nested frontmatter, S10 code-fence safety, S11 no-fabrication, S12 apply_patch validation, S13 search pagination, S14 read-write roundtrip, S15 byte-budget benchmark |
135
+ | Property | (inline in unit) | Round-trip and hash-stability invariants |
98
136
 
99
- ### Testing the Plugin
137
+ The headline benchmark (`S15`) asserts that a surgical workflow uses ≤ 30 % of the byte budget of the equivalent whole-file rewrite — that's the v1.0 thesis in test form.
100
138
 
101
- 1. Build the plugin: `npm run build:plugin`
102
- 2. Copy `dist/plugin/` to `<your-vault>/.obsidian/plugins/obsidian-native-mcp/`
103
- 3. Reload Obsidian, enable the plugin in Community Plugins
104
- 4. Open plugin settings to see discovered vaults
139
+ ### Fixtures
105
140
 
106
- ## Adding a New Tool
141
+ Under `tests/fixtures/vaults/`:
107
142
 
108
- 1. Define the tool schema in `src/mcp/server.ts`add to the `toolDefinitions` array
109
- 2. Implement the handler in `src/handlers/tools.ts`add to `getHandlers()`
110
- 3. Implement the core logic in `src/utils/fs-utils.ts`, `search.ts`, or a new util
143
+ - `tiny/`smoke
144
+ - `agents/`realistic AGENTS.md shape
145
+ - `daily-note/` task lists, headings, frontmatter
146
+ - `code-heavy/` — fenced code with heading- and tag-looking content inside (the single biggest historical bug source)
147
+ - `dup-headings/` — multiple `## Tasks` plus `### Tasks` for disambiguation tests
148
+ - `frontmatter-stress/` — block scalars, nested maps, similar key names
149
+ - `links-zoo/` — wiki, embed, header-ref, block-ref, aliased, markdown
150
+ - `large-kb/` — 5,500-line generated file for outline + search perf
111
151
 
112
- ## Release Process
152
+ Add more fixtures whenever you find a new edge class — the test suite rewards breadth.
113
153
 
114
- ```bash
115
- # 1. Update version in package.json and manifest.json
116
- # 2. Build
117
- npm run build
118
- npm run build:plugin
154
+ ## Release process
119
155
 
120
- # 3. Publish to npm
156
+ ```bash
157
+ # Bump version in package.json + manifest.json (the prepare hook + sync-version.cjs keep them aligned)
158
+ npm run check && npm run lint && npm test && npm run build && npm run build:plugin
121
159
  npm publish
160
+ gh release create vX.Y.Z --title "vX.Y.Z" --generate-notes
161
+ # Plugin: submit dist/plugin/ to https://github.com/obsidianmd/obsidian-releases
162
+ ```
122
163
 
123
- # 4. Create GitHub release
124
- gh release create v0.2.0 --title "v0.2.0" --generate-notes
164
+ CI uses semantic-release on the `main` branch.
125
165
 
126
- # 5. Submit plugin to Obsidian community plugin list
127
- # https://github.com/obsidianmd/obsidian-releases
128
- ```
166
+ ## Protocol reference
129
167
 
130
- ## Protocol Reference
168
+ The server speaks standard MCP over stdio (CLI) or HTTP/SSE (plugin).
131
169
 
132
- The server speaks the standard MCP protocol over stdio (CLI) or HTTP/SSE (plugin):
170
+ ### Stdio framing
133
171
 
134
- ### Stdio
172
+ The CLI uses the official `@modelcontextprotocol/sdk` (`StdioServerTransport`), which as of v1.29.0 uses **newline-delimited JSON**:
135
173
 
136
174
  ```
137
- Client → Server: Content-Length: <N>\r\n\r\n<JSON-RPC body>
138
- Server → Client: Content-Length: <N>\r\n\r\n<JSON-RPC body>
175
+ Client → Server: {"jsonrpc":"2.0","id":1,"method":"initialize","params":{...}}\n
176
+ Server → Client: {"jsonrpc":"2.0","id":1,"result":{...}}\n
139
177
  ```
140
178
 
179
+ Note: The MCP spec originally described Content-Length framing (LSP-style). The official SDK switched to newline-delimited JSON in recent versions. We follow the SDK — not our own framing implementation. `src/mcp/framing.ts` and `src/mcp/stdio.ts` are retained for the HTTP/SSE plugin transport but are not used by the CLI.
180
+
141
181
  ### HTTP/SSE
142
182
 
143
183
  ```
144
- Client → Server: GET /sse → SSE stream with endpoint event
145
- Client → Server: POST /message?session_id=<id> → JSON-RPC request
146
- Server → Client: SSE event "message" with JSON-RPC response
184
+ Client → Server: GET /sse?token=<bearer> → SSE stream starts; first event is "endpoint" with the POST URL
185
+ Client → Server: POST /message?session_id=<id> → JSON-RPC request (body capped at 5 MB)
186
+ Server → Client: SSE event "message" with the JSON-RPC response
187
+ Server → Client: Heartbeat events keep the connection alive; idle sessions are GC'd
147
188
  ```
148
189
 
149
- ### Supported Methods
190
+ `/healthz` returns `{ok: true, sessions: N, version: "X.Y.Z"}`.
191
+
192
+ ### Supported methods
193
+
194
+ | Method | Purpose |
195
+ | --------------------------- | ------------------------------------------------------ |
196
+ | `initialize` | Protocol handshake |
197
+ | `tools/list` | List available tools (filtered by current permissions) |
198
+ | `tools/call` | Execute a tool |
199
+ | `prompts/list` | List available prompts (from vault Prompts/ folders) |
200
+ | `prompts/get` | Get prompt content with arguments substituted |
201
+ | `notifications/initialized` | Client-ready notification |
202
+
203
+ ## Logs and auditing
204
+
205
+ - **Stderr logs** are key=value structured (`component=http session=abc msg="…"`), safe to ship to journald, OpsAgent, etc.
206
+ - **Audit log** is JSONL at `<vault>/.obsidian/plugins/obsidian-native-mcp/audit.log`, one line per mutating call. Use it for forensic analysis and regression replays.
207
+
208
+ ## See also
150
209
 
151
- | Method | Purpose |
152
- | --------------------------- | ------------------------- |
153
- | `initialize` | Protocol handshake |
154
- | `tools/list` | List available tools |
155
- | `tools/call` | Execute a tool |
156
- | `prompts/list` | List available prompts |
157
- | `prompts/get` | Get prompt content |
158
- | `notifications/initialized` | Client-ready notification |
210
+ - `DESIGN_V1.md` — design rationale, invariants, tool surface, and roadmap.