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.
- package/LICENSE +21 -0
- package/README.md +372 -0
- package/dist/cache.d.ts +181 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/cache.js +800 -0
- package/dist/cache.js.map +1 -0
- package/dist/config.d.ts +35 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +323 -0
- package/dist/config.js.map +1 -0
- package/dist/errors.d.ts +26 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +55 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +277 -0
- package/dist/index.js.map +1 -0
- package/dist/obsidian.d.ts +267 -0
- package/dist/obsidian.d.ts.map +1 -0
- package/dist/obsidian.js +1072 -0
- package/dist/obsidian.js.map +1 -0
- package/dist/schemas.d.ts +101 -0
- package/dist/schemas.d.ts.map +1 -0
- package/dist/schemas.js +64 -0
- package/dist/schemas.js.map +1 -0
- package/dist/tools/consolidated.d.ts +7 -0
- package/dist/tools/consolidated.d.ts.map +1 -0
- package/dist/tools/consolidated.js +554 -0
- package/dist/tools/consolidated.js.map +1 -0
- package/dist/tools/granular.d.ts +7 -0
- package/dist/tools/granular.d.ts.map +1 -0
- package/dist/tools/granular.js +658 -0
- package/dist/tools/granular.js.map +1 -0
- package/dist/tools/shared.d.ts +91 -0
- package/dist/tools/shared.d.ts.map +1 -0
- package/dist/tools/shared.js +403 -0
- package/dist/tools/shared.js.map +1 -0
- package/dist/tools.d.ts +7 -0
- package/dist/tools.d.ts.map +1 -0
- package/dist/tools.js +113 -0
- package/dist/tools.js.map +1 -0
- 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
|
+
[](https://www.typescriptlang.org/)
|
|
4
|
+
[](https://modelcontextprotocol.io)
|
|
5
|
+
[](./LICENSE)
|
|
6
|
+
[](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)
|
package/dist/cache.d.ts
ADDED
|
@@ -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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"}
|