@mauricio.wolff/mcp-obsidian 0.3.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/server.ts ADDED
@@ -0,0 +1,516 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
4
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
+ import {
6
+ CallToolRequestSchema,
7
+ ListToolsRequestSchema,
8
+ } from "@modelcontextprotocol/sdk/types.js";
9
+ import { FileSystemService } from "./src/filesystem.js";
10
+ import { FrontmatterHandler } from "./src/frontmatter.js";
11
+ import { PathFilter } from "./src/pathfilter.js";
12
+ import { SearchService } from "./src/search.js";
13
+
14
+ const vaultPath = process.argv[2];
15
+ if (!vaultPath) {
16
+ console.error("Usage: bun server.ts /path/to/vault");
17
+ process.exit(1);
18
+ }
19
+
20
+ // Initialize services
21
+ const pathFilter = new PathFilter();
22
+ const frontmatterHandler = new FrontmatterHandler();
23
+ const fileSystem = new FileSystemService(vaultPath, pathFilter, frontmatterHandler);
24
+ const searchService = new SearchService(vaultPath, pathFilter);
25
+
26
+ const server = new Server({
27
+ name: "mcp-obsidian",
28
+ version: "0.3.0"
29
+ }, {
30
+ capabilities: {
31
+ tools: {},
32
+ },
33
+ });
34
+
35
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
36
+ return {
37
+ tools: [
38
+ {
39
+ name: "read_note",
40
+ description: "Read a note from the Obsidian vault",
41
+ inputSchema: {
42
+ type: "object",
43
+ properties: {
44
+ path: {
45
+ type: "string",
46
+ description: "Path to the note relative to vault root"
47
+ }
48
+ },
49
+ required: ["path"]
50
+ }
51
+ },
52
+ {
53
+ name: "write_note",
54
+ description: "Write a note to the Obsidian vault",
55
+ inputSchema: {
56
+ type: "object",
57
+ properties: {
58
+ path: {
59
+ type: "string",
60
+ description: "Path to the note relative to vault root"
61
+ },
62
+ content: {
63
+ type: "string",
64
+ description: "Content of the note"
65
+ },
66
+ frontmatter: {
67
+ type: "object",
68
+ description: "Frontmatter object (optional)"
69
+ },
70
+ mode: {
71
+ type: "string",
72
+ enum: ["overwrite", "append", "prepend"],
73
+ description: "Write mode: 'overwrite' (default), 'append', or 'prepend'",
74
+ default: "overwrite"
75
+ }
76
+ },
77
+ required: ["path", "content"]
78
+ }
79
+ },
80
+ {
81
+ name: "list_directory",
82
+ description: "List files and directories in the vault",
83
+ inputSchema: {
84
+ type: "object",
85
+ properties: {
86
+ path: {
87
+ type: "string",
88
+ description: "Path relative to vault root (default: '/')",
89
+ default: "/"
90
+ }
91
+ }
92
+ }
93
+ },
94
+ {
95
+ name: "delete_note",
96
+ description: "Delete a note from the Obsidian vault (requires confirmation)",
97
+ inputSchema: {
98
+ type: "object",
99
+ properties: {
100
+ path: {
101
+ type: "string",
102
+ description: "Path to the note relative to vault root"
103
+ },
104
+ confirmPath: {
105
+ type: "string",
106
+ description: "Confirmation: must exactly match the path parameter to proceed with deletion"
107
+ }
108
+ },
109
+ required: ["path", "confirmPath"]
110
+ }
111
+ },
112
+ {
113
+ name: "search_notes",
114
+ description: "Search for notes in the vault by content or frontmatter",
115
+ inputSchema: {
116
+ type: "object",
117
+ properties: {
118
+ query: {
119
+ type: "string",
120
+ description: "Search query text"
121
+ },
122
+ limit: {
123
+ type: "number",
124
+ description: "Maximum number of results (default: 5, max: 20)",
125
+ default: 5
126
+ },
127
+ searchContent: {
128
+ type: "boolean",
129
+ description: "Search in note content (default: true)",
130
+ default: true
131
+ },
132
+ searchFrontmatter: {
133
+ type: "boolean",
134
+ description: "Search in frontmatter (default: false)",
135
+ default: false
136
+ },
137
+ caseSensitive: {
138
+ type: "boolean",
139
+ description: "Case sensitive search (default: false)",
140
+ default: false
141
+ }
142
+ },
143
+ required: ["query"]
144
+ }
145
+ },
146
+ {
147
+ name: "move_note",
148
+ description: "Move or rename a note in the vault",
149
+ inputSchema: {
150
+ type: "object",
151
+ properties: {
152
+ oldPath: {
153
+ type: "string",
154
+ description: "Current path of the note"
155
+ },
156
+ newPath: {
157
+ type: "string",
158
+ description: "New path for the note"
159
+ },
160
+ overwrite: {
161
+ type: "boolean",
162
+ description: "Allow overwriting existing file (default: false)",
163
+ default: false
164
+ }
165
+ },
166
+ required: ["oldPath", "newPath"]
167
+ }
168
+ },
169
+ {
170
+ name: "read_multiple_notes",
171
+ description: "Read multiple notes in a batch (max 10 files)",
172
+ inputSchema: {
173
+ type: "object",
174
+ properties: {
175
+ paths: {
176
+ type: "array",
177
+ items: { type: "string" },
178
+ description: "Array of note paths to read",
179
+ maxItems: 10
180
+ },
181
+ includeContent: {
182
+ type: "boolean",
183
+ description: "Include note content (default: true)",
184
+ default: true
185
+ },
186
+ includeFrontmatter: {
187
+ type: "boolean",
188
+ description: "Include frontmatter (default: true)",
189
+ default: true
190
+ }
191
+ },
192
+ required: ["paths"]
193
+ }
194
+ },
195
+ {
196
+ name: "update_frontmatter",
197
+ description: "Update frontmatter of a note without changing content",
198
+ inputSchema: {
199
+ type: "object",
200
+ properties: {
201
+ path: {
202
+ type: "string",
203
+ description: "Path to the note"
204
+ },
205
+ frontmatter: {
206
+ type: "object",
207
+ description: "Frontmatter object to update"
208
+ },
209
+ merge: {
210
+ type: "boolean",
211
+ description: "Merge with existing frontmatter (default: true)",
212
+ default: true
213
+ }
214
+ },
215
+ required: ["path", "frontmatter"]
216
+ }
217
+ },
218
+ {
219
+ name: "get_notes_info",
220
+ description: "Get metadata for notes without reading full content",
221
+ inputSchema: {
222
+ type: "object",
223
+ properties: {
224
+ paths: {
225
+ type: "array",
226
+ items: { type: "string" },
227
+ description: "Array of note paths to get info for"
228
+ }
229
+ },
230
+ required: ["paths"]
231
+ }
232
+ },
233
+ {
234
+ name: "get_frontmatter",
235
+ description: "Extract frontmatter from a note without reading the content",
236
+ inputSchema: {
237
+ type: "object",
238
+ properties: {
239
+ path: {
240
+ type: "string",
241
+ description: "Path to the note relative to vault root"
242
+ }
243
+ },
244
+ required: ["path"]
245
+ }
246
+ },
247
+ {
248
+ name: "manage_tags",
249
+ description: "Add, remove, or list tags in a note",
250
+ inputSchema: {
251
+ type: "object",
252
+ properties: {
253
+ path: {
254
+ type: "string",
255
+ description: "Path to the note relative to vault root"
256
+ },
257
+ operation: {
258
+ type: "string",
259
+ enum: ["add", "remove", "list"],
260
+ description: "Operation to perform: 'add', 'remove', or 'list'"
261
+ },
262
+ tags: {
263
+ type: "array",
264
+ items: { type: "string" },
265
+ description: "Array of tags (required for 'add' and 'remove' operations)"
266
+ }
267
+ },
268
+ required: ["path", "operation"]
269
+ }
270
+ }
271
+ ]
272
+ };
273
+ });
274
+
275
+ // Helper function to trim path arguments
276
+ function trimPaths(args: any): any {
277
+ const trimmed = { ...args };
278
+
279
+ // Trim single path properties
280
+ if (trimmed.path && typeof trimmed.path === 'string') {
281
+ trimmed.path = trimmed.path.trim();
282
+ }
283
+ if (trimmed.oldPath && typeof trimmed.oldPath === 'string') {
284
+ trimmed.oldPath = trimmed.oldPath.trim();
285
+ }
286
+ if (trimmed.newPath && typeof trimmed.newPath === 'string') {
287
+ trimmed.newPath = trimmed.newPath.trim();
288
+ }
289
+ if (trimmed.confirmPath && typeof trimmed.confirmPath === 'string') {
290
+ trimmed.confirmPath = trimmed.confirmPath.trim();
291
+ }
292
+
293
+ // Trim path arrays
294
+ if (trimmed.paths && Array.isArray(trimmed.paths)) {
295
+ trimmed.paths = trimmed.paths.map((p: any) =>
296
+ typeof p === 'string' ? p.trim() : p
297
+ );
298
+ }
299
+
300
+ return trimmed;
301
+ }
302
+
303
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
304
+ const { name, arguments: args } = request.params;
305
+ const trimmedArgs = trimPaths(args);
306
+
307
+ try {
308
+ switch (name) {
309
+ case "read_note": {
310
+ const note = await fileSystem.readNote(trimmedArgs.path);
311
+ return {
312
+ content: [
313
+ {
314
+ type: "text",
315
+ text: JSON.stringify({
316
+ path: trimmedArgs.path,
317
+ frontmatter: note.frontmatter,
318
+ content: note.content
319
+ }, null, 2)
320
+ }
321
+ ]
322
+ };
323
+ }
324
+
325
+ case "write_note": {
326
+ await fileSystem.writeNote({
327
+ path: trimmedArgs.path,
328
+ content: trimmedArgs.content,
329
+ frontmatter: trimmedArgs.frontmatter,
330
+ mode: trimmedArgs.mode || 'overwrite'
331
+ });
332
+ return {
333
+ content: [
334
+ {
335
+ type: "text",
336
+ text: `Successfully wrote note: ${trimmedArgs.path} (mode: ${trimmedArgs.mode || 'overwrite'})`
337
+ }
338
+ ]
339
+ };
340
+ }
341
+
342
+ case "list_directory": {
343
+ const listing = await fileSystem.listDirectory(trimmedArgs.path || '');
344
+ return {
345
+ content: [
346
+ {
347
+ type: "text",
348
+ text: JSON.stringify({
349
+ path: trimmedArgs.path || '/',
350
+ directories: listing.directories,
351
+ files: listing.files
352
+ }, null, 2)
353
+ }
354
+ ]
355
+ };
356
+ }
357
+
358
+ case "delete_note": {
359
+ const result = await fileSystem.deleteNote({
360
+ path: trimmedArgs.path,
361
+ confirmPath: trimmedArgs.confirmPath
362
+ });
363
+ return {
364
+ content: [
365
+ {
366
+ type: "text",
367
+ text: JSON.stringify(result, null, 2)
368
+ }
369
+ ],
370
+ isError: !result.success
371
+ };
372
+ }
373
+
374
+ case "search_notes": {
375
+ const results = await searchService.search({
376
+ query: trimmedArgs.query,
377
+ limit: trimmedArgs.limit,
378
+ searchContent: trimmedArgs.searchContent,
379
+ searchFrontmatter: trimmedArgs.searchFrontmatter,
380
+ caseSensitive: trimmedArgs.caseSensitive
381
+ });
382
+ return {
383
+ content: [
384
+ {
385
+ type: "text",
386
+ text: JSON.stringify({
387
+ query: trimmedArgs.query,
388
+ resultCount: results.length,
389
+ results: results
390
+ }, null, 2)
391
+ }
392
+ ]
393
+ };
394
+ }
395
+
396
+ case "move_note": {
397
+ const result = await fileSystem.moveNote({
398
+ oldPath: trimmedArgs.oldPath,
399
+ newPath: trimmedArgs.newPath,
400
+ overwrite: trimmedArgs.overwrite
401
+ });
402
+ return {
403
+ content: [
404
+ {
405
+ type: "text",
406
+ text: JSON.stringify(result, null, 2)
407
+ }
408
+ ],
409
+ isError: !result.success
410
+ };
411
+ }
412
+
413
+ case "read_multiple_notes": {
414
+ const result = await fileSystem.readMultipleNotes({
415
+ paths: trimmedArgs.paths,
416
+ includeContent: trimmedArgs.includeContent,
417
+ includeFrontmatter: trimmedArgs.includeFrontmatter
418
+ });
419
+ return {
420
+ content: [
421
+ {
422
+ type: "text",
423
+ text: JSON.stringify({
424
+ successful: result.successful,
425
+ failed: result.failed,
426
+ summary: {
427
+ successCount: result.successful.length,
428
+ failureCount: result.failed.length
429
+ }
430
+ }, null, 2)
431
+ }
432
+ ]
433
+ };
434
+ }
435
+
436
+ case "update_frontmatter": {
437
+ await fileSystem.updateFrontmatter({
438
+ path: trimmedArgs.path,
439
+ frontmatter: trimmedArgs.frontmatter,
440
+ merge: trimmedArgs.merge
441
+ });
442
+ return {
443
+ content: [
444
+ {
445
+ type: "text",
446
+ text: `Successfully updated frontmatter for: ${trimmedArgs.path}`
447
+ }
448
+ ]
449
+ };
450
+ }
451
+
452
+ case "get_notes_info": {
453
+ const result = await fileSystem.getNotesInfo(trimmedArgs.paths);
454
+ return {
455
+ content: [
456
+ {
457
+ type: "text",
458
+ text: JSON.stringify({
459
+ notes: result,
460
+ count: result.length
461
+ }, null, 2)
462
+ }
463
+ ]
464
+ };
465
+ }
466
+
467
+ case "get_frontmatter": {
468
+ const note = await fileSystem.readNote(trimmedArgs.path);
469
+ return {
470
+ content: [
471
+ {
472
+ type: "text",
473
+ text: JSON.stringify({
474
+ path: trimmedArgs.path,
475
+ frontmatter: note.frontmatter
476
+ }, null, 2)
477
+ }
478
+ ]
479
+ };
480
+ }
481
+
482
+ case "manage_tags": {
483
+ const result = await fileSystem.manageTags({
484
+ path: trimmedArgs.path,
485
+ operation: trimmedArgs.operation,
486
+ tags: trimmedArgs.tags
487
+ });
488
+ return {
489
+ content: [
490
+ {
491
+ type: "text",
492
+ text: JSON.stringify(result, null, 2)
493
+ }
494
+ ],
495
+ isError: !result.success
496
+ };
497
+ }
498
+
499
+ default:
500
+ throw new Error(`Unknown tool: ${name}`);
501
+ }
502
+ } catch (error) {
503
+ return {
504
+ content: [
505
+ {
506
+ type: "text",
507
+ text: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`
508
+ }
509
+ ],
510
+ isError: true
511
+ };
512
+ }
513
+ });
514
+
515
+ const transport = new StdioServerTransport();
516
+ await server.connect(transport);