mcp-obsidian-extended 1.0.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 (43) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +372 -0
  3. package/dist/cache.d.ts +181 -0
  4. package/dist/cache.d.ts.map +1 -0
  5. package/dist/cache.js +800 -0
  6. package/dist/cache.js.map +1 -0
  7. package/dist/config.d.ts +35 -0
  8. package/dist/config.d.ts.map +1 -0
  9. package/dist/config.js +323 -0
  10. package/dist/config.js.map +1 -0
  11. package/dist/errors.d.ts +26 -0
  12. package/dist/errors.d.ts.map +1 -0
  13. package/dist/errors.js +55 -0
  14. package/dist/errors.js.map +1 -0
  15. package/dist/index.d.ts +3 -0
  16. package/dist/index.d.ts.map +1 -0
  17. package/dist/index.js +277 -0
  18. package/dist/index.js.map +1 -0
  19. package/dist/obsidian.d.ts +267 -0
  20. package/dist/obsidian.d.ts.map +1 -0
  21. package/dist/obsidian.js +1072 -0
  22. package/dist/obsidian.js.map +1 -0
  23. package/dist/schemas.d.ts +101 -0
  24. package/dist/schemas.d.ts.map +1 -0
  25. package/dist/schemas.js +64 -0
  26. package/dist/schemas.js.map +1 -0
  27. package/dist/tools/consolidated.d.ts +7 -0
  28. package/dist/tools/consolidated.d.ts.map +1 -0
  29. package/dist/tools/consolidated.js +554 -0
  30. package/dist/tools/consolidated.js.map +1 -0
  31. package/dist/tools/granular.d.ts +7 -0
  32. package/dist/tools/granular.d.ts.map +1 -0
  33. package/dist/tools/granular.js +658 -0
  34. package/dist/tools/granular.js.map +1 -0
  35. package/dist/tools/shared.d.ts +91 -0
  36. package/dist/tools/shared.d.ts.map +1 -0
  37. package/dist/tools/shared.js +403 -0
  38. package/dist/tools/shared.js.map +1 -0
  39. package/dist/tools.d.ts +7 -0
  40. package/dist/tools.d.ts.map +1 -0
  41. package/dist/tools.js +113 -0
  42. package/dist/tools.js.map +1 -0
  43. package/package.json +66 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024-2026 Markus Pfundstein, adder-factory
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,372 @@
1
+ # mcp-obsidian-extended
2
+
3
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.9-blue.svg)](https://www.typescriptlang.org/)
4
+ [![MCP SDK](https://img.shields.io/badge/MCP%20SDK-1.27-green.svg)](https://modelcontextprotocol.io)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](./LICENSE)
6
+ [![Node.js](https://img.shields.io/badge/Node.js-%3E%3D22-brightgreen.svg)](https://nodejs.org/)
7
+
8
+ Full-featured MCP server for Obsidian — 38 tools covering 100% of the [Local REST API](https://github.com/coddingtonbear/obsidian-local-rest-api).
9
+
10
+ ## Prerequisites
11
+
12
+ Install and enable the [Obsidian Local REST API](https://github.com/coddingtonbear/obsidian-local-rest-api) community plugin. Copy the API key from Obsidian Settings → Local REST API.
13
+
14
+ ## Installation
15
+
16
+ ### Option 1: npx (recommended)
17
+
18
+ Add to your Claude Desktop config:
19
+
20
+ **macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json`
21
+
22
+ **Windows:** `%APPDATA%\Claude\claude_desktop_config.json`
23
+
24
+ ```json
25
+ {
26
+ "mcpServers": {
27
+ "mcp-obsidian-extended": {
28
+ "command": "npx",
29
+ "args": ["-y", "mcp-obsidian-extended"],
30
+ "env": {
31
+ "OBSIDIAN_API_KEY": "your-api-key-here"
32
+ }
33
+ }
34
+ }
35
+ }
36
+ ```
37
+
38
+ ### Option 2: Setup Wizard
39
+
40
+ ```bash
41
+ npx mcp-obsidian-extended --setup
42
+ ```
43
+
44
+ Interactive wizard that tests your connection, configures settings, and outputs the Claude Desktop JSON snippet.
45
+
46
+ ### Option 3: Desktop Extension
47
+
48
+ Download the `.mcpb` file from [Releases](https://github.com/adder-factory/mcp-obsidian-extended/releases) and open it in Claude Desktop for one-click install.
49
+
50
+ ## What's New vs the Original
51
+
52
+ This is a TypeScript rewrite of [mcp-obsidian](https://github.com/MarkusPfundstein/mcp-obsidian) with:
53
+
54
+ - **100% REST API coverage** — 38 tools vs the original 7
55
+ - **Dual tool mode** — granular (38 tools) or consolidated (11 tools, saves tokens)
56
+ - **Tool presets** — full, read-only, minimal, safe
57
+ - **Tool filtering** — INCLUDE_TOOLS / EXCLUDE_TOOLS env vars
58
+ - **Dataview DQL search** — query vault using the Dataview plugin
59
+ - **Full periodic notes** — CRUD by current period and by specific date
60
+ - **Vault cache + graph analysis** — backlinks, orphan detection, vault structure
61
+ - **Connection recovery** — auto-reconnect when Obsidian comes back
62
+ - **Self-config tool** — change settings from chat without restarting
63
+ - **Setup wizard** — interactive `--setup` for first-time configuration
64
+ - **Upstream bug fixes** — empty dir 404, search timeouts, broken periodic notes
65
+
66
+ ## Tools
67
+
68
+ ### Granular Mode (38 tools, default)
69
+
70
+ | # | Tool | Description |
71
+ |---|------|-------------|
72
+ | 1 | `list_files_in_vault` | List all files and directories in vault root |
73
+ | 2 | `list_files_in_dir` | List files in a vault directory |
74
+ | 3 | `get_file_contents` | Read a vault file as markdown, JSON, or document map |
75
+ | 4 | `put_content` | Create or overwrite a vault file (idempotent) |
76
+ | 5 | `append_content` | Append to a vault file |
77
+ | 6 | `patch_content` | Insert at a heading, block, or frontmatter target |
78
+ | 7 | `delete_file` | Delete a vault file to Obsidian trash (idempotent) |
79
+ | 8 | `search_replace` | Find and replace text in a vault file |
80
+ | 9 | `get_active_file` | Read the currently open file |
81
+ | 10 | `put_active_file` | Replace content of the currently open file |
82
+ | 11 | `append_active_file` | Append to the currently open file |
83
+ | 12 | `patch_active_file` | Patch the active file at a target |
84
+ | 13 | `delete_active_file` | Delete the currently open file |
85
+ | 14 | `list_commands` | List all Obsidian command palette commands |
86
+ | 15 | `execute_command` | Run an Obsidian command by ID |
87
+ | 16 | `open_file` | Open a file in the Obsidian UI |
88
+ | 17 | `simple_search` | Full-text search across all vault files |
89
+ | 18 | `complex_search` | Search with JsonLogic queries (glob, regexp) |
90
+ | 19 | `dataview_search` | Query vault using Dataview DQL |
91
+ | 20 | `get_periodic_note` | Get the current periodic note |
92
+ | 21 | `put_periodic_note` | Replace current periodic note content |
93
+ | 22 | `append_periodic_note` | Append to current periodic note |
94
+ | 23 | `patch_periodic_note` | Patch current periodic note at a target |
95
+ | 24 | `delete_periodic_note` | Delete current periodic note |
96
+ | 25 | `get_periodic_note_for_date` | Get periodic note for a specific date |
97
+ | 26 | `put_periodic_note_for_date` | Replace periodic note for a date |
98
+ | 27 | `append_periodic_note_for_date` | Append to periodic note for a date |
99
+ | 28 | `patch_periodic_note_for_date` | Patch periodic note for a date |
100
+ | 29 | `delete_periodic_note_for_date` | Delete periodic note for a date |
101
+ | 30 | `get_server_status` | Check Obsidian API connection and version |
102
+ | 31 | `batch_get_file_contents` | Read multiple vault files in one call |
103
+ | 32 | `get_recent_changes` | Get recently modified files sorted by date |
104
+ | 33 | `get_recent_periodic_notes` | Get recent periodic notes for a period type |
105
+ | 34 | `configure` | View or change server settings |
106
+ | 35 | `get_backlinks` | Get all notes that link to a file |
107
+ | 36 | `get_vault_structure` | Vault stats: note count, links, orphans, most connected |
108
+ | 37 | `get_note_connections` | Get backlinks and forward links for a note |
109
+ | 38 | `refresh_cache` | Force refresh vault cache and link graph |
110
+
111
+ ### Consolidated Mode (11 tools)
112
+
113
+ Combines related tools into multi-action tools. Reduces the tool list sent to the LLM, saving tokens on every request.
114
+
115
+ | # | Tool | Actions | Replaces |
116
+ |---|------|---------|----------|
117
+ | 1 | `vault` | list, list_dir, get, put, append, patch, delete, search_replace | Tools 1-8 |
118
+ | 2 | `active_file` | get, put, append, patch, delete | Tools 9-13 |
119
+ | 3 | `commands` | list, execute | Tools 14-15 |
120
+ | 4 | `open_file` | — | Tool 16 |
121
+ | 5 | `search` | simple, jsonlogic, dataview | Tools 17-19 |
122
+ | 6 | `periodic_note` | get, put, append, patch, delete | Tools 20-29 |
123
+ | 7 | `status` | — | Tool 30 |
124
+ | 8 | `batch_get` | — | Tool 31 |
125
+ | 9 | `recent` | changes, periodic_notes | Tools 32-33 |
126
+ | 10 | `configure` | show, set, reset | Tool 34 |
127
+ | 11 | `vault_analysis` | backlinks, connections, structure, refresh | Tools 35-38 |
128
+
129
+ Set `TOOL_MODE=consolidated` to enable.
130
+
131
+ ## Tool Presets
132
+
133
+ Control which tools are available. Set via `TOOL_PRESET` env var.
134
+
135
+ | Preset | Granular | Consolidated | Description |
136
+ |--------|----------|-------------|-------------|
137
+ | `full` | 38 tools | 11 tools, all actions | Everything (default) |
138
+ | `read-only` | 19 tools | 10 tools, read actions only | No writes or deletes |
139
+ | `minimal` | 7 tools | 4 tools | Essentials only |
140
+ | `safe` | 34 tools | 11 tools, no delete action | Everything except deletes |
141
+
142
+ ### Tool Filtering
143
+
144
+ Fine-tune beyond presets with `INCLUDE_TOOLS` and `EXCLUDE_TOOLS` (comma-separated tool names).
145
+
146
+ ```bash
147
+ # Only allow read + search in granular mode
148
+ INCLUDE_TOOLS=list_files_in_vault,get_file_contents,simple_search
149
+
150
+ # Allow everything except deletes in granular mode
151
+ EXCLUDE_TOOLS=delete_file,delete_active_file,delete_periodic_note,delete_periodic_note_for_date
152
+ ```
153
+
154
+ Protected tools (`configure`, `get_server_status`/`status`, `refresh_cache`/`vault_analysis`) are always registered regardless of filters.
155
+
156
+ ## Configuration
157
+
158
+ Three-tier priority: **Defaults → Config file → Env vars** (env always wins).
159
+
160
+ ### Environment Variables
161
+
162
+ | Variable | Default | Description |
163
+ |----------|---------|-------------|
164
+ | `OBSIDIAN_API_KEY` | *(required)* | Bearer token from REST API plugin |
165
+ | `OBSIDIAN_HOST` | `127.0.0.1` | Obsidian host |
166
+ | `OBSIDIAN_PORT` | `27124` | REST API port |
167
+ | `OBSIDIAN_SCHEME` | `https` | `https` or `http` |
168
+ | `OBSIDIAN_TIMEOUT` | `30000` | Request timeout ms (search gets 2x) |
169
+ | `OBSIDIAN_CERT_PATH` | — | Path to .crt for TLS verification |
170
+ | `OBSIDIAN_VERIFY_SSL` | `false` | Strict TLS verification |
171
+ | `OBSIDIAN_VERIFY_WRITES` | `false` | Read-after-write verification |
172
+ | `OBSIDIAN_MAX_RESPONSE_CHARS` | `500000` | Truncation limit (0 = disabled) |
173
+ | `OBSIDIAN_DEBUG` | `false` | HTTP debug logging to stderr |
174
+ | `OBSIDIAN_CONFIG` | — | Custom config file path |
175
+ | `TOOL_MODE` | `granular` | `granular` or `consolidated` |
176
+ | `TOOL_PRESET` | `full` | `full`, `read-only`, `minimal`, `safe` |
177
+ | `INCLUDE_TOOLS` | — | Whitelist tool names (comma-separated) |
178
+ | `EXCLUDE_TOOLS` | — | Blacklist tool names (comma-separated) |
179
+ | `OBSIDIAN_CACHE_TTL` | `600000` | Cache refresh interval ms (10 min) |
180
+ | `OBSIDIAN_ENABLE_CACHE` | `true` | Enable/disable vault cache |
181
+
182
+ ### Config File
183
+
184
+ Auto-discovered from (in order):
185
+ 1. `OBSIDIAN_CONFIG` env var
186
+ 2. `./obsidian-mcp.config.json`
187
+ 3. `~/.obsidian-mcp.config.json`
188
+ 4. `~/.config/obsidian-mcp/config.json`
189
+
190
+ See [`obsidian-mcp.config.example.json`](./obsidian-mcp.config.example.json) for the full format. The API key should be in an env var or Claude Desktop config — not in a file that might be committed.
191
+
192
+ ## CLI
193
+
194
+ ```bash
195
+ npx mcp-obsidian-extended --setup # Interactive setup wizard
196
+ npx mcp-obsidian-extended --version # Print version
197
+ npx mcp-obsidian-extended --show-config # Print active config (API key redacted)
198
+ npx mcp-obsidian-extended --validate # Test connection + auth
199
+ ```
200
+
201
+ ## Debugging
202
+
203
+ ### MCP Inspector
204
+
205
+ ```bash
206
+ npx @modelcontextprotocol/inspector node dist/index.js
207
+ ```
208
+
209
+ ### Debug Logging
210
+
211
+ Set `OBSIDIAN_DEBUG=true` to log HTTP method/path/status/timing to stderr. Never logs request bodies or auth headers.
212
+
213
+ ### Validate Connection
214
+
215
+ ```bash
216
+ OBSIDIAN_API_KEY=your-key npx mcp-obsidian-extended --validate
217
+ ```
218
+
219
+ ### Server Logs
220
+
221
+ ```bash
222
+ # macOS
223
+ tail -f ~/Library/Logs/Claude/mcp-server-mcp-obsidian-extended.log
224
+
225
+ # Windows
226
+ Get-Content "$env:APPDATA\Claude\Logs\mcp-server-mcp-obsidian-extended.log" -Wait
227
+ ```
228
+
229
+ ## Reliability
230
+
231
+ - **Connection recovery** — health check every 30s, auto-reconnect when Obsidian comes back
232
+ - **Graceful offline startup** — warns but doesn't crash if Obsidian isn't running
233
+ - **Write verification** — optional read-after-write for PUT ops (`OBSIDIAN_VERIFY_WRITES=true`)
234
+ - **Write locks** — per-file serialization prevents concurrent write races
235
+ - **Idempotent operations** — PUT and DELETE are safe to retry on timeout
236
+ - **Response truncation** — large files capped at 500K chars (configurable)
237
+ - **Vault cache** — in-memory cache with auto-refresh, serves cached reads when offline
238
+ - **Case-insensitive paths** — automatic fallback on 404 for mismatched case
239
+
240
+ ## Performance
241
+
242
+ Benchmarked against Obsidian Local REST API on macOS with mcp-test-vault.
243
+
244
+ ### Stress Test — 395K Operations
245
+
246
+ | Metric | Result |
247
+ |--------|--------|
248
+ | Duration | 307.6s |
249
+ | Total operations | 394,607 |
250
+ | Throughput | 1,282 ops/s |
251
+ | Error rate | 0.01% (33/394K — read timeouts during cache rebuild, expected) |
252
+ | Memory (heap) | 27.9MB stable (no memory leak) |
253
+ | Crashes | 0 |
254
+
255
+ ### Latency Percentiles
256
+
257
+ | Operation | Count | p50 | p95 | p99 |
258
+ |-----------|-------|-----|-----|-----|
259
+ | get | 98,750 | 0ms | 1ms | 3ms |
260
+ | put | 59,261 | 1ms | 2ms | 3ms |
261
+ | append | 39,619 | 1ms | 2ms | 4ms |
262
+ | search | 39,522 | 0ms | 1ms | 2ms |
263
+ | list | 39,406 | 0ms | 1ms | 1ms |
264
+ | get_json | 39,329 | 0ms | 1ms | 3ms |
265
+ | cache_rebuild | 19,714 | 6ms | 9ms | 12ms |
266
+ | delete_put | 19,667 | 1ms | 3ms | 5ms |
267
+ | search_replace | 19,643 | 1ms | 4ms | 6ms |
268
+
269
+ Sub-millisecond reads at p50. Stable memory after 395K operations. Write locks serialize correctly under concurrent load. Cache rebuilds don't block reads.
270
+
271
+ ### Full-Coverage Stress Test — All 55 Tools
272
+
273
+ | Metric | Result |
274
+ |--------|--------|
275
+ | Duration | 323s |
276
+ | Total operations | 379,557 |
277
+ | Throughput | 1,175 ops/s |
278
+ | Error rate | 0.24% (916/380K — all gracefully handled) |
279
+ | Tool coverage | 55/55 (35 granular + 20 consolidated actions) |
280
+ | Memory (heap) | 17.8MB stable (no memory leak) |
281
+ | Crashes | 0 |
282
+
283
+ Error breakdown:
284
+ - `patch_*` operations: 722 errors (0.19%) — heading structure race conditions under concurrent writes
285
+ - `get`/`batch_get` timeouts: 194 errors — 30s timeouts during heavy cache rebuilds
286
+
287
+ All errors are gracefully handled with structured error messages. No crashes, no data corruption.
288
+
289
+ ### Advanced Stress Tests — Edge Case Validation
290
+
291
+ 6 targeted scenarios testing reliability under extreme conditions:
292
+
293
+ | Scenario | Duration | Ops | Result | Key Finding |
294
+ |----------|----------|-----|--------|-------------|
295
+ | Heading Mismatch Recovery | 3m | 8,763 | PASS | 89.5% PATCH success under concurrent heading restructuring |
296
+ | Cache Stampede | 14ms | 42 | PASS | 20 concurrent waiters, 1 build only, zero redundant builds |
297
+ | Large Vault Scale | 385ms | 292 | PASS | 205 notes/789 links cached in 136ms |
298
+ | Write Contention Torture | 3m | 7,145 | PASS | 0% errors, file lock serialization holds |
299
+ | Periodic Notes Date Sweep | 15m | 60 | PASS | All date edge cases handled |
300
+ | Error Cascade Recovery | 59ms | 58 | PASS | 0 unhandled exceptions, auto-recovery works |
301
+
302
+ Totals: 16,360 ops | p50=2ms | p95=37ms | 19.3MB heap stable | 6/6 pass
303
+
304
+ ### Combined Benchmark Summary
305
+
306
+ | Test Suite | Operations | Key Result |
307
+ |-----------|-----------|------------|
308
+ | Stress test | 225 | 10/10 scenarios, write locks verified |
309
+ | Extended benchmark | 394,607 | 1,282 ops/s, 0.01% error rate |
310
+ | Full tool coverage | 379,557 | 55/55 tools exercised, 1,175 ops/s |
311
+ | Advanced stress tests | 16,360 | 6/6 edge case scenarios pass |
312
+ | **Grand total** | **~790,749** | **Zero crashes. Zero data corruption.** |
313
+
314
+ ## Optional Plugins
315
+
316
+ | Plugin | Required For |
317
+ |--------|-------------|
318
+ | [Local REST API](https://github.com/coddingtonbear/obsidian-local-rest-api) | **All functionality** (required) |
319
+ | [Dataview](https://github.com/blacksmithgu/obsidian-dataview) | `dataview_search` tool |
320
+ | [Periodic Notes](https://github.com/liamcain/obsidian-periodic-notes) | Periodic note tools (daily/weekly/monthly/quarterly/yearly) |
321
+
322
+ ## Comparison
323
+
324
+ | Feature | mcp-obsidian-extended | mcp-obsidian (original) | cyanheads (363★) | mcpvault (~50★) | ToKiDoO (6★) |
325
+ |---------|----------------------|------------------------|------------------|-----------------|--------------|
326
+ | Language | TypeScript | Python | TypeScript | TypeScript | TypeScript |
327
+ | Install | npx / .mcpb | uvx (requires Python) | npx | npx | npx |
328
+ | Tools | 38 granular / 11 consolidated | 7 | 8 | 14 (filesystem) | ~15 |
329
+ | REST API coverage | 100% | ~20% | ~25% | 0% (filesystem) | ~40% |
330
+ | Tool filtering | INCLUDE/EXCLUDE + presets | — | — | — | INCLUDE only |
331
+ | Dual mode | granular + consolidated | — | — | — | — |
332
+ | Dataview DQL | Yes (TABLE queries) | — | — | — | — |
333
+ | Active file ops | Full CRUD | — | — | — | — |
334
+ | Commands | list + execute | — | — | — | — |
335
+ | Periodic notes | Full CRUD + by date | — | — | — | — |
336
+ | Self-config tool | Yes (from chat) | — | — | — | — |
337
+ | Setup wizard | Yes (--setup) | — | — | — | — |
338
+ | Desktop Extension | .mcpb one-click install | — | — | — | — |
339
+ | Configurable timeouts | Yes | — | — | N/A | — |
340
+ | Vault cache + offline | REST-only cache | — | Yes | — | — |
341
+ | Graph analysis | Backlinks, orphans, connections | — | — | — | Filesystem |
342
+ | Write locks | Per-file mutex | — | — | — | — |
343
+ | Benchmarked | 395K ops, 1,282 ops/s | — | — | — | — |
344
+ | CI/CD | GitHub Actions | — | — | — | — |
345
+ | Known bugs | Fixed (7 upstream) | 50+ open issues | — | — | — |
346
+
347
+ > mcp-obsidian-extended is a TypeScript rewrite of [mcp-obsidian](https://github.com/MarkusPfundstein/mcp-obsidian) by Markus Pfundstein, which pioneered the MCP server approach for Obsidian. We fix 7 upstream bugs and expand from 7 tools to 38 with full API coverage.
348
+
349
+ ## Known Limitations
350
+
351
+ - **PATCH under concurrent writes:** When multiple writers restructure headings simultaneously, PATCH operations may fail to find their target. With automatic retry and document map refresh, success rate is 89.5% under extreme concurrent load (up from ~5% without retry). Under normal single-user usage, PATCH success is ~99%+. For heavy concurrent editing scenarios, prefer `search_replace` over `patch_content`.
352
+ - **Dataview queries:** Only `TABLE` queries are supported by the Obsidian Local REST API. `LIST` queries are not supported — this is an upstream API limitation, not a server limitation. Use `TABLE` with column selection as a workaround.
353
+ - **Cache rebuild contention:** During cache rebuilds on large vaults (500+ notes), read operations may experience brief timeouts (~0.05% of requests). The server handles this gracefully with automatic retries. Cache stampede is prevented — 20 concurrent callers share a single build with zero redundant builds.
354
+
355
+ ## Acknowledgments
356
+
357
+ This project is a TypeScript rewrite of [mcp-obsidian](https://github.com/MarkusPfundstein/mcp-obsidian) by **Markus Pfundstein**, which pioneered the MCP server approach for Obsidian.
358
+
359
+ The Obsidian integration is made possible by [obsidian-local-rest-api](https://github.com/coddingtonbear/obsidian-local-rest-api) by **Adam Coddington**.
360
+
361
+ Design inspirations from the community:
362
+ - **Case-insensitive path fallback** and **search-replace tool** — [obsidian-mcp-server](https://github.com/cyanheads/obsidian-mcp-server) by **cyanheads**
363
+ - **Tool filtering** — [mcp-obsidian-advanced](https://github.com/ToKiDoO/mcp-obsidian-advanced) by **ToKiDoO**
364
+ - **Path traversal protection** — [mcpvault](https://github.com/bitbonsai/mcpvault) by **bitbonsai**
365
+ - **Graph analysis concept** — [mcp-obsidian-advanced](https://github.com/ToKiDoO/mcp-obsidian-advanced) by **ToKiDoO** and [obsidiantools](https://github.com/mfarragher/obsidiantools) by **mfarragher**
366
+ - **Vault cache concept** — [obsidian-mcp-server](https://github.com/cyanheads/obsidian-mcp-server) by **cyanheads**
367
+
368
+ Thank you to all upstream bug reporters whose detailed issues shaped our fixes.
369
+
370
+ ## License
371
+
372
+ [MIT](./LICENSE)
@@ -0,0 +1,181 @@
1
+ import type { ObsidianClient, VaultCacheInterface } from "./obsidian.js";
2
+ /** A parsed link extracted from note content, with resolved target and context. */
3
+ export interface ParsedLink {
4
+ readonly target: string;
5
+ readonly type: "wikilink" | "markdown";
6
+ readonly context: string;
7
+ }
8
+ /** A cached vault note with parsed content, frontmatter, tags, stat, and links. */
9
+ export interface CachedNote {
10
+ readonly path: string;
11
+ readonly content: string;
12
+ readonly frontmatter: Record<string, unknown>;
13
+ readonly tags: readonly string[];
14
+ readonly stat: {
15
+ readonly ctime: number;
16
+ readonly mtime: number;
17
+ readonly size: number;
18
+ };
19
+ readonly links: readonly ParsedLink[];
20
+ readonly cachedAt: number;
21
+ }
22
+ /**
23
+ * Parses `[[wikilinks]]` and `[text](path.md)` links from note content.
24
+ * Wikilinks are stored as short names (e.g. `NoteName.md`) without directory —
25
+ * graph queries use suffix matching to resolve them to full vault paths.
26
+ *
27
+ * @param content - The markdown content to parse.
28
+ * @param currentPath - The vault path of the note containing the links.
29
+ */
30
+ export declare function parseLinks(content: string, currentPath: string): ParsedLink[];
31
+ /**
32
+ * In-memory cache of all vault markdown notes with parsed links and graph queries.
33
+ * Provides backlink resolution, orphan detection, and connectivity analysis.
34
+ */
35
+ export declare class VaultCache implements VaultCacheInterface {
36
+ /** Maximum number of retry attempts for cache initialization. */
37
+ private static readonly INIT_MAX_ATTEMPTS;
38
+ /** Backoff delay (ms) after a generation-mismatch discard. */
39
+ private static readonly DISCARD_BACKOFF_MS;
40
+ /** Base backoff delay (ms) for network/API errors, multiplied by (attempt + 1). */
41
+ private static readonly NETWORK_BACKOFF_BASE_MS;
42
+ private readonly notes;
43
+ private readonly client;
44
+ private readonly cacheTtl;
45
+ /** Cached link count to avoid O(N) iteration on every access. */
46
+ private cachedLinkCount;
47
+ private refreshTimer;
48
+ private isInitialized;
49
+ private isRefreshing;
50
+ /** Set to true during initialize() to signal that an initial build is in flight. */
51
+ private isBuilding;
52
+ /** In-flight initialize() promise — concurrent callers await the same build. */
53
+ private buildPromise;
54
+ /** Generation counter: incremented on invalidateAll(), checked after builds to discard stale results. */
55
+ private generation;
56
+ /** Maps normalised short filename (e.g. "notename.md") → Set of full vault paths. */
57
+ private readonly shortNameIndex;
58
+ /** Creates a new vault cache backed by the given Obsidian client and refresh interval. */
59
+ constructor(client: ObsidianClient, cacheTtl: number);
60
+ /**
61
+ * Performs a full cache build by fetching all markdown files from the vault.
62
+ * Builds into a fresh snapshot then swaps atomically. Discards results if
63
+ * invalidateAll() was called during the build (generation mismatch).
64
+ * @throws {ObsidianConnectionError} On network failure or after exhausting retry attempts.
65
+ * @throws {ObsidianAuthError} On authentication failure (401/403, not retried).
66
+ * Callers must catch this — refresh() already does; direct callers should handle gracefully.
67
+ */
68
+ initialize(): Promise<void>;
69
+ /**
70
+ * Internal build logic with retry on generation mismatch.
71
+ * Retries up to 3 times within the same promise if invalidateAll() discards a build,
72
+ * so concurrent callers awaiting buildPromise see the final result.
73
+ * Callers share buildPromise; see initialize() finally block for ordering invariants.
74
+ */
75
+ private doInitialize;
76
+ /** Classifies a build attempt error and logs it. Rethrows non-transient errors. */
77
+ private handleBuildAttemptError;
78
+ /** Throws the appropriate error after exhausting all retry attempts. */
79
+ private throwExhaustedError;
80
+ /** Executes a single build attempt. Throws on generation mismatch or failure. */
81
+ private executeBuildAttempt;
82
+ /** Fetches all markdown notes from the vault in batches. Aborts early if generation changes. */
83
+ private fetchAllNotes;
84
+ /** Atomically swaps the cache contents with a fresh snapshot. */
85
+ private applySnapshot;
86
+ /**
87
+ * Refreshes the cache by re-fetching all notes and only updating those
88
+ * whose mtime has changed. Note: the Obsidian REST API does not expose
89
+ * stat info on the listing endpoint, so each note must be fetched individually
90
+ * to check mtime. For large vaults this means N HTTP requests per refresh cycle.
91
+ * The comparison itself is incremental (only changed notes are re-parsed),
92
+ * but the network cost is proportional to vault size.
93
+ * Errors are caught and logged — never throws.
94
+ */
95
+ refresh(): Promise<void>;
96
+ /** Removes cached notes that no longer exist in the vault file list. */
97
+ private pruneDeletedNotes;
98
+ /** Fetches notes in batches and updates cache entries whose mtime has changed. */
99
+ private fetchChangedNotes;
100
+ /** Starts a background timer that periodically refreshes the cache. */
101
+ startAutoRefresh(): void;
102
+ /** Stops the background auto-refresh timer if running. */
103
+ stopAutoRefresh(): void;
104
+ /** Returns a cached note by exact path, falling back to filename-based lookup. */
105
+ getNote(path: string): CachedNote | undefined;
106
+ /** Returns all cached notes as an array. */
107
+ getAllNotes(): readonly CachedNote[];
108
+ /** Returns all cached file paths. */
109
+ getFileList(): readonly string[];
110
+ get noteCount(): number;
111
+ get linkCount(): number;
112
+ /** Returns whether the cache has completed its initial build. */
113
+ getIsInitialized(): boolean;
114
+ /**
115
+ * Waits for the cache to finish initializing, with a timeout.
116
+ * Returns true if initialized within the timeout, false otherwise.
117
+ * If already initialized, resolves immediately. If no build is in
118
+ * progress (isBuilding and isRefreshing are both false), returns
119
+ * false immediately — this covers the case where invalidateAll()
120
+ * cleared the cache without triggering a rebuild. The next scheduled
121
+ * auto-refresh (startAutoRefresh timer) will rebuild the cache;
122
+ * callers should not block indefinitely waiting for that.
123
+ *
124
+ * Note: in a narrow sub-millisecond window after a build completes but
125
+ * before a new refresh/rebuild sets isRefreshing/isBuilding, this may
126
+ * return false even though a rebuild is imminent. Callers getting false
127
+ * should check getIsInitialized() and retry if needed.
128
+ * For latency-sensitive paths, a brief retry (e.g. 100ms) can bridge this gap.
129
+ */
130
+ waitForInitialization(timeoutMs: number): Promise<boolean>;
131
+ /** Tracks paths invalidated during an in-flight refresh to prevent stale re-insertion. */
132
+ private readonly invalidatedDuringRefresh;
133
+ /** Removes a single note from the cache and updates the short-name index and link count. */
134
+ invalidate(path: string): void;
135
+ /**
136
+ * Clears the entire cache, index, and resets the initialised flag.
137
+ * Increments the generation counter so that any in-flight build discards its stale results.
138
+ */
139
+ invalidateAll(): void;
140
+ /** Returns all notes that link to the given file path, with surrounding context. */
141
+ getBacklinks(path: string): Array<{
142
+ source: string;
143
+ context: string;
144
+ }>;
145
+ /** Returns all outbound links from the given note. */
146
+ getForwardLinks(path: string): readonly ParsedLink[];
147
+ /** Returns paths of notes with zero inbound links (orphans). */
148
+ getOrphanNotes(): readonly string[];
149
+ /** Returns the most connected notes sorted by total link count (inbound + outbound). */
150
+ getMostConnectedNotes(limit: number): Array<{
151
+ path: string;
152
+ inbound: number;
153
+ outbound: number;
154
+ }>;
155
+ /** Returns the total number of resolved link edges without building the full graph. */
156
+ getEdgeCount(): number;
157
+ /** Returns the full vault link graph as nodes (file paths) and edges (source-target pairs). */
158
+ getVaultGraph(): {
159
+ nodes: readonly string[];
160
+ edges: ReadonlyArray<{
161
+ source: string;
162
+ target: string;
163
+ }>;
164
+ };
165
+ /** Searches for a cached note by short-name index (O(1)) with fallback for case-insensitive full-path match. */
166
+ private findByName;
167
+ /** Normalises a link target to lowercase with forward slashes and a `.md` extension. */
168
+ private normalizeLinkTarget;
169
+ /** Recalculates the cached link count from the current notes map. */
170
+ private recalcLinkCount;
171
+ /** Rebuilds the short-name index for O(1) wikilink resolution. */
172
+ private rebuildIndex;
173
+ /**
174
+ * Resolves a link target to a full vault path using the short-name index.
175
+ * Returns the first matching full path, or the original target if unresolved.
176
+ * Note: the exact-match fast-path is case-sensitive; case-insensitive resolution
177
+ * falls through to the O(1) short-name index lookups below.
178
+ */
179
+ private resolveLinkToFullPath;
180
+ }
181
+ //# sourceMappingURL=cache.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"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"}