@mauricio.wolff/mcp-obsidian 0.4.1 → 0.5.1

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/README.md CHANGED
@@ -6,16 +6,17 @@ A lightweight Model Context Protocol (MCP) server for safe Obsidian vault access
6
6
 
7
7
  ## Quick Start (5 minutes)
8
8
 
9
- 1. **Install Bun runtime:**
9
+ 1. **Install Node.js runtime:**
10
10
  ```bash
11
- curl -fsSL https://bun.sh/install | bash
11
+ # Download from https://nodejs.org (v18.0.0 or later)
12
+ # or use a package manager like nvm, brew, apt, etc.
12
13
  ```
13
14
 
14
15
  2. **Test the server:**
15
16
 
16
17
  If using the published package:
17
18
  ```bash
18
- bunx @modelcontextprotocol/inspector bunx @mauricio.wolff/mcp-obsidian /path/to/your/vault
19
+ npx @modelcontextprotocol/inspector npx @mauricio.wolff/mcp-obsidian /path/to/your/vault
19
20
  ```
20
21
 
21
22
  3. **Configure your AI client:**
@@ -25,7 +26,7 @@ A lightweight Model Context Protocol (MCP) server for safe Obsidian vault access
25
26
  {
26
27
  "mcpServers": {
27
28
  "obsidian": {
28
- "command": "bunx",
29
+ "command": "npx",
29
30
  "args": ["@mauricio.wolff/mcp-obsidian", "/path/to/your/vault"]
30
31
  }
31
32
  }
@@ -37,7 +38,7 @@ A lightweight Model Context Protocol (MCP) server for safe Obsidian vault access
37
38
  {
38
39
  "mcpServers": {
39
40
  "obsidian": {
40
- "command": "bunx",
41
+ "command": "npx",
41
42
  "args": ["@mauricio.wolff/mcp-obsidian", "/path/to/your/vault"],
42
43
  "env": {}
43
44
  }
@@ -71,12 +72,12 @@ A lightweight Model Context Protocol (MCP) server for safe Obsidian vault access
71
72
  - ✅ **NEW:** Tag management: add, remove, and list tags in notes
72
73
  - ✅ Safe deletion with confirmation requirement to prevent accidents
73
74
  - ✅ Automatic path trimming to handle whitespace in inputs
74
- - ✅ TypeScript support with Bun runtime (no compilation needed)
75
+ - ✅ TypeScript support with Node.js runtime (using tsx for execution)
75
76
  - ✅ Comprehensive error handling and validation
76
77
 
77
78
  ## Prerequisites
78
79
 
79
- - [Bun](https://bun.sh) runtime (v1.0.0 or later)
80
+ - [Node.js](https://nodejs.org) runtime (v18.0.0 or later)
80
81
  - An Obsidian vault (local directory with `.md` files)
81
82
  - MCP-compatible AI client (Claude Desktop, ChatGPT Desktop, Claude Code, etc.)
82
83
 
@@ -84,23 +85,23 @@ A lightweight Model Context Protocol (MCP) server for safe Obsidian vault access
84
85
 
85
86
  ### For End Users (Recommended)
86
87
 
87
- No installation needed! Use `bunx` to run directly:
88
+ No installation needed! Use `npx` to run directly:
88
89
 
89
90
  ```bash
90
- bunx @mauricio.wolff/mcp-obsidian /path/to/your/obsidian/vault
91
+ npx @mauricio.wolff/mcp-obsidian /path/to/your/obsidian/vault
91
92
  ```
92
93
 
93
94
  ### For Developers
94
95
 
95
96
  1. Clone this repository
96
- 2. Install dependencies with Bun:
97
+ 2. Install dependencies with npm:
97
98
  ```bash
98
- bun install
99
+ npm install
99
100
  ```
100
101
 
101
102
  3. Test locally with MCP inspector:
102
103
  ```bash
103
- bunx @modelcontextprotocol/inspector bun server.ts /path/to/your/vault
104
+ npx @modelcontextprotocol/inspector npm start /path/to/your/vault
104
105
  ```
105
106
 
106
107
  ## Usage
@@ -109,12 +110,12 @@ bunx @modelcontextprotocol/inspector bun server.ts /path/to/your/vault
109
110
 
110
111
  **End users:**
111
112
  ```bash
112
- bunx @mauricio.wolff/mcp-obsidian /path/to/your/obsidian/vault
113
+ npx @mauricio.wolff/mcp-obsidian /path/to/your/obsidian/vault
113
114
  ```
114
115
 
115
116
  **Developers:**
116
117
  ```bash
117
- bun server.ts /path/to/your/obsidian/vault
118
+ npm start /path/to/your/obsidian/vault
118
119
  ```
119
120
 
120
121
  ### AI Client Configuration
@@ -128,7 +129,7 @@ Add to your Claude Desktop configuration file:
128
129
  {
129
130
  "mcpServers": {
130
131
  "obsidian": {
131
- "command": "bunx",
132
+ "command": "npx",
132
133
  "args": ["@mauricio.wolff/mcp-obsidian", "/Users/yourname/Documents/MyVault"]
133
134
  }
134
135
  }
@@ -140,11 +141,11 @@ Add to your Claude Desktop configuration file:
140
141
  {
141
142
  "mcpServers": {
142
143
  "obsidian-personal": {
143
- "command": "bunx",
144
+ "command": "npx",
144
145
  "args": ["@mauricio.wolff/mcp-obsidian", "/Users/yourname/Documents/PersonalVault"]
145
146
  },
146
147
  "obsidian-work": {
147
- "command": "bunx",
148
+ "command": "npx",
148
149
  "args": ["@mauricio.wolff/mcp-obsidian", "/Users/yourname/Documents/WorkVault"]
149
150
  }
150
151
  }
@@ -180,7 +181,7 @@ Edit `~/.claude.json`:
180
181
  {
181
182
  "mcpServers": {
182
183
  "obsidian": {
183
- "command": "bunx",
184
+ "command": "npx",
184
185
  "args": ["@mauricio.wolff/mcp-obsidian", "/path/to/your/vault"],
185
186
  "env": {}
186
187
  }
@@ -196,7 +197,7 @@ Edit `.claude.json` in your project or add to the projects section:
196
197
  "/path/to/your/project": {
197
198
  "mcpServers": {
198
199
  "obsidian": {
199
- "command": "bunx",
200
+ "command": "npx",
200
201
  "args": ["@mauricio.wolff/mcp-obsidian", "/path/to/your/vault"]
201
202
  }
202
203
  }
@@ -207,7 +208,7 @@ Edit `.claude.json` in your project or add to the projects section:
207
208
 
208
209
  **Using Claude Code CLI:**
209
210
  ```bash
210
- claude mcp add obsidian --scope user bunx @mauricio.wolff/mcp-obsidian /path/to/your/vault
211
+ claude mcp add obsidian --scope user npx @mauricio.wolff/mcp-obsidian /path/to/your/vault
211
212
  ```
212
213
 
213
214
  #### Other MCP-Compatible Clients (2025)
@@ -241,11 +242,11 @@ Most modern MCP clients use similar JSON configuration patterns. Refer to your s
241
242
 
242
243
  ### Common Issues
243
244
 
244
- #### "command not found: bunx"
245
- - **Solution:** Install Bun runtime from [bun.sh](https://bun.sh)
246
- - **Alternative:** Use npm: `npx @mauricio.wolff/mcp-obsidian /path/to/vault`
245
+ #### "command not found: npx"
246
+ - **Solution:** Install Node.js runtime from [nodejs.org](https://nodejs.org)
247
+ - **Alternative:** Use global install: `npm install -g @mauricio.wolff/mcp-obsidian`
247
248
 
248
- #### "Usage: bun server.ts /path/to/vault"
249
+ #### "Usage: node server.ts /path/to/vault"
249
250
  - **Cause:** No vault path provided
250
251
  - **Solution:** Specify the full path to your Obsidian vault directory
251
252
 
@@ -272,20 +273,20 @@ Most modern MCP clients use similar JSON configuration patterns. Refer to your s
272
273
 
273
274
  Run with error logging:
274
275
  ```bash
275
- bunx @mauricio.wolff/mcp-obsidian /path/to/vault 2>debug.log
276
+ npx @mauricio.wolff/mcp-obsidian /path/to/vault 2>debug.log
276
277
  ```
277
278
 
278
279
  ### Getting Help
279
280
 
280
281
  - [Open an issue](https://github.com/bitbonsai/mcp-obsidian/issues) on GitHub
281
- - Include your OS, Bun version, and error messages
282
+ - Include your OS, Node.js version, and error messages
282
283
  - Provide the vault directory structure (without sensitive content)
283
284
 
284
285
  ## Testing
285
286
 
286
287
  Run the test suite:
287
288
  ```bash
288
- bun test
289
+ npm test
289
290
  ```
290
291
 
291
292
  ## API Methods
@@ -691,7 +692,7 @@ This MCP server implements several security measures to protect your Obsidian va
691
692
  1. Fork the repository
692
693
  2. Create a feature branch: `git checkout -b feature-name`
693
694
  3. Make your changes and add tests
694
- 4. Ensure all tests pass: `bun test`
695
+ 4. Ensure all tests pass: `npm test`
695
696
  5. Submit a pull request
696
697
 
697
698
  ## License
package/dist/server.js ADDED
@@ -0,0 +1,489 @@
1
+ #!/usr/bin/env node
2
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
5
+ import { FileSystemService } from "./src/filesystem.js";
6
+ import { FrontmatterHandler } from "./src/frontmatter.js";
7
+ import { PathFilter } from "./src/pathfilter.js";
8
+ import { SearchService } from "./src/search.js";
9
+ const vaultPath = process.argv[2];
10
+ if (!vaultPath) {
11
+ console.error("Usage: npx @mauricio.wolff/mcp-obsidian /path/to/vault");
12
+ process.exit(1);
13
+ }
14
+ // Initialize services
15
+ const pathFilter = new PathFilter();
16
+ const frontmatterHandler = new FrontmatterHandler();
17
+ const fileSystem = new FileSystemService(vaultPath, pathFilter, frontmatterHandler);
18
+ const searchService = new SearchService(vaultPath, pathFilter);
19
+ const server = new Server({
20
+ name: "mcp-obsidian",
21
+ version: "0.5.1"
22
+ }, {
23
+ capabilities: {
24
+ tools: {},
25
+ },
26
+ });
27
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
28
+ return {
29
+ tools: [
30
+ {
31
+ name: "read_note",
32
+ description: "Read a note from the Obsidian vault",
33
+ inputSchema: {
34
+ type: "object",
35
+ properties: {
36
+ path: {
37
+ type: "string",
38
+ description: "Path to the note relative to vault root"
39
+ }
40
+ },
41
+ required: ["path"]
42
+ }
43
+ },
44
+ {
45
+ name: "write_note",
46
+ description: "Write a note to the Obsidian vault",
47
+ inputSchema: {
48
+ type: "object",
49
+ properties: {
50
+ path: {
51
+ type: "string",
52
+ description: "Path to the note relative to vault root"
53
+ },
54
+ content: {
55
+ type: "string",
56
+ description: "Content of the note"
57
+ },
58
+ frontmatter: {
59
+ type: "object",
60
+ description: "Frontmatter object (optional)"
61
+ },
62
+ mode: {
63
+ type: "string",
64
+ enum: ["overwrite", "append", "prepend"],
65
+ description: "Write mode: 'overwrite' (default), 'append', or 'prepend'",
66
+ default: "overwrite"
67
+ }
68
+ },
69
+ required: ["path", "content"]
70
+ }
71
+ },
72
+ {
73
+ name: "list_directory",
74
+ description: "List files and directories in the vault",
75
+ inputSchema: {
76
+ type: "object",
77
+ properties: {
78
+ path: {
79
+ type: "string",
80
+ description: "Path relative to vault root (default: '/')",
81
+ default: "/"
82
+ }
83
+ }
84
+ }
85
+ },
86
+ {
87
+ name: "delete_note",
88
+ description: "Delete a note from the Obsidian vault (requires confirmation)",
89
+ inputSchema: {
90
+ type: "object",
91
+ properties: {
92
+ path: {
93
+ type: "string",
94
+ description: "Path to the note relative to vault root"
95
+ },
96
+ confirmPath: {
97
+ type: "string",
98
+ description: "Confirmation: must exactly match the path parameter to proceed with deletion"
99
+ }
100
+ },
101
+ required: ["path", "confirmPath"]
102
+ }
103
+ },
104
+ {
105
+ name: "search_notes",
106
+ description: "Search for notes in the vault by content or frontmatter",
107
+ inputSchema: {
108
+ type: "object",
109
+ properties: {
110
+ query: {
111
+ type: "string",
112
+ description: "Search query text"
113
+ },
114
+ limit: {
115
+ type: "number",
116
+ description: "Maximum number of results (default: 5, max: 20)",
117
+ default: 5
118
+ },
119
+ searchContent: {
120
+ type: "boolean",
121
+ description: "Search in note content (default: true)",
122
+ default: true
123
+ },
124
+ searchFrontmatter: {
125
+ type: "boolean",
126
+ description: "Search in frontmatter (default: false)",
127
+ default: false
128
+ },
129
+ caseSensitive: {
130
+ type: "boolean",
131
+ description: "Case sensitive search (default: false)",
132
+ default: false
133
+ }
134
+ },
135
+ required: ["query"]
136
+ }
137
+ },
138
+ {
139
+ name: "move_note",
140
+ description: "Move or rename a note in the vault",
141
+ inputSchema: {
142
+ type: "object",
143
+ properties: {
144
+ oldPath: {
145
+ type: "string",
146
+ description: "Current path of the note"
147
+ },
148
+ newPath: {
149
+ type: "string",
150
+ description: "New path for the note"
151
+ },
152
+ overwrite: {
153
+ type: "boolean",
154
+ description: "Allow overwriting existing file (default: false)",
155
+ default: false
156
+ }
157
+ },
158
+ required: ["oldPath", "newPath"]
159
+ }
160
+ },
161
+ {
162
+ name: "read_multiple_notes",
163
+ description: "Read multiple notes in a batch (max 10 files)",
164
+ inputSchema: {
165
+ type: "object",
166
+ properties: {
167
+ paths: {
168
+ type: "array",
169
+ items: { type: "string" },
170
+ description: "Array of note paths to read",
171
+ maxItems: 10
172
+ },
173
+ includeContent: {
174
+ type: "boolean",
175
+ description: "Include note content (default: true)",
176
+ default: true
177
+ },
178
+ includeFrontmatter: {
179
+ type: "boolean",
180
+ description: "Include frontmatter (default: true)",
181
+ default: true
182
+ }
183
+ },
184
+ required: ["paths"]
185
+ }
186
+ },
187
+ {
188
+ name: "update_frontmatter",
189
+ description: "Update frontmatter of a note without changing content",
190
+ inputSchema: {
191
+ type: "object",
192
+ properties: {
193
+ path: {
194
+ type: "string",
195
+ description: "Path to the note"
196
+ },
197
+ frontmatter: {
198
+ type: "object",
199
+ description: "Frontmatter object to update"
200
+ },
201
+ merge: {
202
+ type: "boolean",
203
+ description: "Merge with existing frontmatter (default: true)",
204
+ default: true
205
+ }
206
+ },
207
+ required: ["path", "frontmatter"]
208
+ }
209
+ },
210
+ {
211
+ name: "get_notes_info",
212
+ description: "Get metadata for notes without reading full content",
213
+ inputSchema: {
214
+ type: "object",
215
+ properties: {
216
+ paths: {
217
+ type: "array",
218
+ items: { type: "string" },
219
+ description: "Array of note paths to get info for"
220
+ }
221
+ },
222
+ required: ["paths"]
223
+ }
224
+ },
225
+ {
226
+ name: "get_frontmatter",
227
+ description: "Extract frontmatter from a note without reading the content",
228
+ inputSchema: {
229
+ type: "object",
230
+ properties: {
231
+ path: {
232
+ type: "string",
233
+ description: "Path to the note relative to vault root"
234
+ }
235
+ },
236
+ required: ["path"]
237
+ }
238
+ },
239
+ {
240
+ name: "manage_tags",
241
+ description: "Add, remove, or list tags in a note",
242
+ inputSchema: {
243
+ type: "object",
244
+ properties: {
245
+ path: {
246
+ type: "string",
247
+ description: "Path to the note relative to vault root"
248
+ },
249
+ operation: {
250
+ type: "string",
251
+ enum: ["add", "remove", "list"],
252
+ description: "Operation to perform: 'add', 'remove', or 'list'"
253
+ },
254
+ tags: {
255
+ type: "array",
256
+ items: { type: "string" },
257
+ description: "Array of tags (required for 'add' and 'remove' operations)"
258
+ }
259
+ },
260
+ required: ["path", "operation"]
261
+ }
262
+ }
263
+ ]
264
+ };
265
+ });
266
+ // Helper function to trim path arguments
267
+ function trimPaths(args) {
268
+ const trimmed = { ...args };
269
+ // Trim single path properties
270
+ if (trimmed.path && typeof trimmed.path === 'string') {
271
+ trimmed.path = trimmed.path.trim();
272
+ }
273
+ if (trimmed.oldPath && typeof trimmed.oldPath === 'string') {
274
+ trimmed.oldPath = trimmed.oldPath.trim();
275
+ }
276
+ if (trimmed.newPath && typeof trimmed.newPath === 'string') {
277
+ trimmed.newPath = trimmed.newPath.trim();
278
+ }
279
+ if (trimmed.confirmPath && typeof trimmed.confirmPath === 'string') {
280
+ trimmed.confirmPath = trimmed.confirmPath.trim();
281
+ }
282
+ // Trim path arrays
283
+ if (trimmed.paths && Array.isArray(trimmed.paths)) {
284
+ trimmed.paths = trimmed.paths.map((p) => typeof p === 'string' ? p.trim() : p);
285
+ }
286
+ return trimmed;
287
+ }
288
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
289
+ const { name, arguments: args } = request.params;
290
+ const trimmedArgs = trimPaths(args);
291
+ try {
292
+ switch (name) {
293
+ case "read_note": {
294
+ const note = await fileSystem.readNote(trimmedArgs.path);
295
+ return {
296
+ content: [
297
+ {
298
+ type: "text",
299
+ text: JSON.stringify({
300
+ path: trimmedArgs.path,
301
+ frontmatter: note.frontmatter,
302
+ content: note.content
303
+ }, null, 2)
304
+ }
305
+ ]
306
+ };
307
+ }
308
+ case "write_note": {
309
+ await fileSystem.writeNote({
310
+ path: trimmedArgs.path,
311
+ content: trimmedArgs.content,
312
+ frontmatter: trimmedArgs.frontmatter,
313
+ mode: trimmedArgs.mode || 'overwrite'
314
+ });
315
+ return {
316
+ content: [
317
+ {
318
+ type: "text",
319
+ text: `Successfully wrote note: ${trimmedArgs.path} (mode: ${trimmedArgs.mode || 'overwrite'})`
320
+ }
321
+ ]
322
+ };
323
+ }
324
+ case "list_directory": {
325
+ const listing = await fileSystem.listDirectory(trimmedArgs.path || '');
326
+ return {
327
+ content: [
328
+ {
329
+ type: "text",
330
+ text: JSON.stringify({
331
+ path: trimmedArgs.path || '/',
332
+ directories: listing.directories,
333
+ files: listing.files
334
+ }, null, 2)
335
+ }
336
+ ]
337
+ };
338
+ }
339
+ case "delete_note": {
340
+ const result = await fileSystem.deleteNote({
341
+ path: trimmedArgs.path,
342
+ confirmPath: trimmedArgs.confirmPath
343
+ });
344
+ return {
345
+ content: [
346
+ {
347
+ type: "text",
348
+ text: JSON.stringify(result, null, 2)
349
+ }
350
+ ],
351
+ isError: !result.success
352
+ };
353
+ }
354
+ case "search_notes": {
355
+ const results = await searchService.search({
356
+ query: trimmedArgs.query,
357
+ limit: trimmedArgs.limit,
358
+ searchContent: trimmedArgs.searchContent,
359
+ searchFrontmatter: trimmedArgs.searchFrontmatter,
360
+ caseSensitive: trimmedArgs.caseSensitive
361
+ });
362
+ return {
363
+ content: [
364
+ {
365
+ type: "text",
366
+ text: JSON.stringify({
367
+ query: trimmedArgs.query,
368
+ resultCount: results.length,
369
+ results: results
370
+ }, null, 2)
371
+ }
372
+ ]
373
+ };
374
+ }
375
+ case "move_note": {
376
+ const result = await fileSystem.moveNote({
377
+ oldPath: trimmedArgs.oldPath,
378
+ newPath: trimmedArgs.newPath,
379
+ overwrite: trimmedArgs.overwrite
380
+ });
381
+ return {
382
+ content: [
383
+ {
384
+ type: "text",
385
+ text: JSON.stringify(result, null, 2)
386
+ }
387
+ ],
388
+ isError: !result.success
389
+ };
390
+ }
391
+ case "read_multiple_notes": {
392
+ const result = await fileSystem.readMultipleNotes({
393
+ paths: trimmedArgs.paths,
394
+ includeContent: trimmedArgs.includeContent,
395
+ includeFrontmatter: trimmedArgs.includeFrontmatter
396
+ });
397
+ return {
398
+ content: [
399
+ {
400
+ type: "text",
401
+ text: JSON.stringify({
402
+ successful: result.successful,
403
+ failed: result.failed,
404
+ summary: {
405
+ successCount: result.successful.length,
406
+ failureCount: result.failed.length
407
+ }
408
+ }, null, 2)
409
+ }
410
+ ]
411
+ };
412
+ }
413
+ case "update_frontmatter": {
414
+ await fileSystem.updateFrontmatter({
415
+ path: trimmedArgs.path,
416
+ frontmatter: trimmedArgs.frontmatter,
417
+ merge: trimmedArgs.merge
418
+ });
419
+ return {
420
+ content: [
421
+ {
422
+ type: "text",
423
+ text: `Successfully updated frontmatter for: ${trimmedArgs.path}`
424
+ }
425
+ ]
426
+ };
427
+ }
428
+ case "get_notes_info": {
429
+ const result = await fileSystem.getNotesInfo(trimmedArgs.paths);
430
+ return {
431
+ content: [
432
+ {
433
+ type: "text",
434
+ text: JSON.stringify({
435
+ notes: result,
436
+ count: result.length
437
+ }, null, 2)
438
+ }
439
+ ]
440
+ };
441
+ }
442
+ case "get_frontmatter": {
443
+ const note = await fileSystem.readNote(trimmedArgs.path);
444
+ return {
445
+ content: [
446
+ {
447
+ type: "text",
448
+ text: JSON.stringify({
449
+ path: trimmedArgs.path,
450
+ frontmatter: note.frontmatter
451
+ }, null, 2)
452
+ }
453
+ ]
454
+ };
455
+ }
456
+ case "manage_tags": {
457
+ const result = await fileSystem.manageTags({
458
+ path: trimmedArgs.path,
459
+ operation: trimmedArgs.operation,
460
+ tags: trimmedArgs.tags
461
+ });
462
+ return {
463
+ content: [
464
+ {
465
+ type: "text",
466
+ text: JSON.stringify(result, null, 2)
467
+ }
468
+ ],
469
+ isError: !result.success
470
+ };
471
+ }
472
+ default:
473
+ throw new Error(`Unknown tool: ${name}`);
474
+ }
475
+ }
476
+ catch (error) {
477
+ return {
478
+ content: [
479
+ {
480
+ type: "text",
481
+ text: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`
482
+ }
483
+ ],
484
+ isError: true
485
+ };
486
+ }
487
+ });
488
+ const transport = new StdioServerTransport();
489
+ await server.connect(transport);