obsidian-mcp-server 1.2.3 → 1.2.5

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/build/tools.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { encoding_for_model } from "tiktoken";
2
2
  import { ObsidianError } from "./types.js";
3
+ import { PropertyManager } from "./properties.js";
3
4
  const TOOL_NAMES = {
4
5
  LIST_FILES_IN_VAULT: "obsidian_list_files_in_vault",
5
6
  LIST_FILES_IN_DIR: "obsidian_list_files_in_dir",
@@ -7,7 +8,8 @@ const TOOL_NAMES = {
7
8
  FIND_IN_FILE: "obsidian_find_in_file",
8
9
  APPEND_CONTENT: "obsidian_append_content",
9
10
  PATCH_CONTENT: "obsidian_patch_content",
10
- COMPLEX_SEARCH: "obsidian_complex_search"
11
+ COMPLEX_SEARCH: "obsidian_complex_search",
12
+ GET_TAGS: "obsidian_get_tags"
11
13
  };
12
14
  // Load token limits from environment or use defaults
13
15
  const MAX_TOKENS = parseInt(process.env.MAX_TOKENS ?? '20000');
@@ -259,7 +261,7 @@ export class FindInFileToolHandler extends BaseToolHandler {
259
261
  getToolDescription() {
260
262
  return {
261
263
  name: this.name,
262
- description: "Full-text search across all files in the vault. Returns matching files with surrounding context for each match. Useful for finding specific content, references, or patterns across notes.",
264
+ description: "Full-text search across all files in the vault. Returns matching files with surrounding context for each match. For results with more than 5 matching files, returns only file names and match counts to prevent overwhelming responses. Useful for finding specific content, references, or patterns across notes.",
263
265
  examples: [
264
266
  {
265
267
  description: "Search for a specific term",
@@ -273,17 +275,39 @@ export class FindInFileToolHandler extends BaseToolHandler {
273
275
  args: {
274
276
  query: "#todo"
275
277
  },
276
- response: [
277
- {
278
- "filename": "Projects/AI.md",
279
- "matches": [
280
- {
281
- "context": "Research needed:\n#todo Implement transformer architecture\nDeadline: Next week",
282
- "match": { "start": 15, "end": 45 }
283
- }
284
- ]
285
- }
286
- ]
278
+ response: {
279
+ "message": "Found 1 file with matches:",
280
+ "results": [
281
+ {
282
+ "filename": "Projects/AI.md",
283
+ "matches": [
284
+ {
285
+ "context": "Research needed:\n#todo Implement transformer architecture\nDeadline: Next week",
286
+ "match": { "start": 15, "end": 45 }
287
+ }
288
+ ]
289
+ }
290
+ ]
291
+ }
292
+ },
293
+ {
294
+ description: "Example response with many matches (file-only format)",
295
+ args: {
296
+ query: "API"
297
+ },
298
+ response: {
299
+ "message": "Found 92 files with matches. Showing file names only:",
300
+ "results": [
301
+ {
302
+ "filename": "Developer/Documentation/API.md",
303
+ "matchCount": 43
304
+ },
305
+ {
306
+ "filename": "Projects/API_Design.md",
307
+ "matchCount": 34
308
+ }
309
+ ]
310
+ }
287
311
  }
288
312
  ],
289
313
  inputSchema: {
@@ -305,10 +329,34 @@ export class FindInFileToolHandler extends BaseToolHandler {
305
329
  }
306
330
  async runTool(args) {
307
331
  try {
308
- const results = await this.client.search(args.query, args.contextLength);
309
- // Extract only unique filenames from search results
310
- const filenames = [...new Set(results.map(result => result.filename))].sort();
311
- return this.createResponse(filenames);
332
+ const results = await this.client.search(args.query, args.contextLength ?? 100);
333
+ // If more than 5 results, only return filenames
334
+ if (results.length > 5) {
335
+ const fileOnlyResults = results.map(result => ({
336
+ filename: result.filename,
337
+ matchCount: result.matches.length
338
+ }));
339
+ return this.createResponse({
340
+ message: `Found ${results.length} files with matches. Showing file names only:`,
341
+ results: fileOnlyResults
342
+ });
343
+ }
344
+ // Otherwise return full context as before
345
+ const formattedResults = results.map(result => ({
346
+ filename: result.filename,
347
+ matches: result.matches.map(match => ({
348
+ context: match.context,
349
+ match: {
350
+ text: match.context.substring(match.match.start, match.match.end),
351
+ position: {
352
+ start: match.match.start,
353
+ end: match.match.end
354
+ }
355
+ }
356
+ })),
357
+ score: result.score
358
+ }));
359
+ return this.createResponse(formattedResults);
312
360
  }
313
361
  catch (error) {
314
362
  return this.handleError(error);
@@ -417,46 +465,21 @@ export class ComplexSearchToolHandler extends BaseToolHandler {
417
465
  getToolDescription() {
418
466
  return {
419
467
  name: this.name,
420
- description: "Advanced search functionality using JsonLogic queries. Enables complex file filtering based on paths, metadata, modification times, and content patterns. Supports logical operations, date comparisons, and pattern matching.",
468
+ description: "File path pattern matching using JsonLogic queries. Supported operations:\n- glob: Pattern matching for paths (e.g., \"*.md\")\n- Variable access: {\"var\": \"path\"}\n\nNote: For full-text content search, date-based searches, or other advanced queries, use obsidian_find_in_file instead.",
421
469
  examples: [
422
470
  {
423
- description: "Find markdown files in a specific folder",
471
+ description: "Find markdown files in Projects folder",
424
472
  args: {
425
473
  query: {
426
- "and": [
427
- { "glob": ["Projects/*.md", { "var": "path" }] },
428
- { "contains": [{ "var": "content" }, "#active"] }
429
- ]
474
+ "glob": ["Projects/*.md", { "var": "path" }]
430
475
  }
431
476
  }
432
477
  },
433
478
  {
434
- description: "Find recently modified documentation",
479
+ description: "Find files in a specific subfolder",
435
480
  args: {
436
481
  query: {
437
- "and": [
438
- { "glob": ["docs/*.md", { "var": "path" }] },
439
- { ">=": [
440
- { "var": "mtime" },
441
- { "date": "-7 days" }
442
- ] },
443
- { "!=": [{ "var": "size" }, 0] }
444
- ]
445
- }
446
- }
447
- },
448
- {
449
- description: "Find files by multiple criteria",
450
- args: {
451
- query: {
452
- "and": [
453
- { "or": [
454
- { "glob": ["*.md", { "var": "path" }] },
455
- { "glob": ["*.txt", { "var": "path" }] }
456
- ] },
457
- { "contains": [{ "var": "content" }, "TODO"] },
458
- { "<": [{ "var": "size" }, 10000] }
459
- ]
482
+ "glob": ["**/Test/*.md", { "var": "path" }]
460
483
  }
461
484
  }
462
485
  }
@@ -475,12 +498,366 @@ export class ComplexSearchToolHandler extends BaseToolHandler {
475
498
  }
476
499
  async runTool(args) {
477
500
  try {
501
+ // Perform search
478
502
  const results = await this.client.searchJson(args.query);
479
- return this.createResponse(results);
503
+ console.debug('Search results:', results);
504
+ // Format response based on result type
505
+ const formattedResults = results.map(result => {
506
+ if ('matches' in result) {
507
+ // SimpleSearchResult
508
+ return {
509
+ filename: result.filename,
510
+ matches: result.matches,
511
+ score: result.score
512
+ };
513
+ }
514
+ else {
515
+ // SearchResult
516
+ return {
517
+ filename: result.filename,
518
+ result: result.result
519
+ };
520
+ }
521
+ });
522
+ return this.createResponse(formattedResults);
523
+ }
524
+ catch (error) {
525
+ console.error('Complex search error:', error);
526
+ return this.handleError(error);
527
+ }
528
+ }
529
+ }
530
+ export class GetTagsToolHandler extends BaseToolHandler {
531
+ static TAG_PATTERN = /#[a-zA-Z0-9_-]+/g;
532
+ propertyManager;
533
+ constructor(client) {
534
+ super(TOOL_NAMES.GET_TAGS, client);
535
+ this.propertyManager = new PropertyManager(client);
536
+ }
537
+ getToolDescription() {
538
+ return {
539
+ name: this.name,
540
+ description: "Get all tags used across the Obsidian vault with their usage counts. Optionally filter tags within a specific folder.",
541
+ examples: [
542
+ {
543
+ description: "Get all tags in vault",
544
+ args: {}
545
+ },
546
+ {
547
+ description: "Get tags in Projects folder",
548
+ args: {
549
+ path: "Projects"
550
+ }
551
+ },
552
+ {
553
+ description: "Example response",
554
+ args: {},
555
+ response: {
556
+ "tags": [
557
+ {
558
+ "name": "#project",
559
+ "count": 15,
560
+ "files": [
561
+ "Projects/ProjectA.md",
562
+ "Projects/ProjectB.md"
563
+ ]
564
+ }
565
+ ],
566
+ "metadata": {
567
+ "totalOccurrences": 45,
568
+ "uniqueTags": 12,
569
+ "scannedFiles": 30
570
+ }
571
+ }
572
+ }
573
+ ],
574
+ inputSchema: {
575
+ type: "object",
576
+ properties: {
577
+ path: {
578
+ type: "string",
579
+ description: "Optional path to limit tag search to specific folder",
580
+ format: "path"
581
+ }
582
+ }
583
+ }
584
+ };
585
+ }
586
+ async processFiles(files, basePath, tagMap) {
587
+ let scannedFiles = 0;
588
+ for (const file of files) {
589
+ const fullPath = basePath ? `${basePath}/${file.path}` : file.path;
590
+ if (file.type === "folder" && file.children) {
591
+ // Recursively process subdirectories
592
+ scannedFiles += await this.processFiles(file.children, fullPath, tagMap);
593
+ }
594
+ else if (file.type === "file" && file.path.endsWith('.md')) {
595
+ // Process markdown files
596
+ scannedFiles++;
597
+ const content = await this.client.getFileContents(fullPath);
598
+ // Extract tags from frontmatter
599
+ const properties = this.propertyManager.parseProperties(content);
600
+ if (properties.tags) {
601
+ properties.tags.forEach((tag) => {
602
+ if (!tagMap.has(tag)) {
603
+ tagMap.set(tag, new Set());
604
+ }
605
+ tagMap.get(tag).add(fullPath);
606
+ });
607
+ }
608
+ // Extract inline tags using regex
609
+ const inlineTags = content.match(GetTagsToolHandler.TAG_PATTERN) || [];
610
+ inlineTags.forEach(tag => {
611
+ if (!tagMap.has(tag)) {
612
+ tagMap.set(tag, new Set());
613
+ }
614
+ tagMap.get(tag).add(fullPath);
615
+ });
616
+ }
617
+ }
618
+ return scannedFiles;
619
+ }
620
+ async runTool(args) {
621
+ try {
622
+ const tagMap = new Map();
623
+ const basePath = args.path || '';
624
+ // Get files from vault or specific directory
625
+ const files = args.path
626
+ ? await this.client.listFilesInDir(args.path)
627
+ : await this.client.listFilesInVault();
628
+ // Process files recursively
629
+ const scannedFiles = await this.processFiles(files, basePath, tagMap);
630
+ // Calculate total occurrences
631
+ const totalOccurrences = Array.from(tagMap.values())
632
+ .reduce((sum, files) => sum + files.size, 0);
633
+ const response = {
634
+ tags: Array.from(tagMap.entries())
635
+ .map(([name, files]) => ({
636
+ name,
637
+ count: files.size,
638
+ files: Array.from(files).sort()
639
+ }))
640
+ .sort((a, b) => b.count - a.count || a.name.localeCompare(b.name)),
641
+ metadata: {
642
+ totalOccurrences,
643
+ uniqueTags: tagMap.size,
644
+ scannedFiles,
645
+ lastUpdate: Date.now()
646
+ }
647
+ };
648
+ return this.createResponse(response);
649
+ }
650
+ catch (error) {
651
+ return this.handleError(error);
652
+ }
653
+ }
654
+ }
655
+ // Export all handlers
656
+ export class ListCommandsToolHandler extends BaseToolHandler {
657
+ constructor(client) {
658
+ super("obsidian_list_commands", client);
659
+ }
660
+ getToolDescription() {
661
+ return {
662
+ name: this.name,
663
+ description: "Get a list of available commands that can be executed in Obsidian.",
664
+ examples: [
665
+ {
666
+ description: "List all available commands",
667
+ args: {}
668
+ }
669
+ ],
670
+ inputSchema: {
671
+ type: "object",
672
+ properties: {},
673
+ required: []
674
+ }
675
+ };
676
+ }
677
+ async runTool() {
678
+ try {
679
+ const commands = await this.client.listCommands();
680
+ return this.createResponse(commands);
681
+ }
682
+ catch (error) {
683
+ return this.handleError(error);
684
+ }
685
+ }
686
+ }
687
+ export class ExecuteCommandToolHandler extends BaseToolHandler {
688
+ constructor(client) {
689
+ super("obsidian_execute_command", client);
690
+ }
691
+ getToolDescription() {
692
+ return {
693
+ name: this.name,
694
+ description: "Execute a specific command in Obsidian by its ID.",
695
+ examples: [
696
+ {
697
+ description: "Execute the graph view command",
698
+ args: {
699
+ commandId: "graph:open"
700
+ }
701
+ }
702
+ ],
703
+ inputSchema: {
704
+ type: "object",
705
+ properties: {
706
+ commandId: {
707
+ type: "string",
708
+ description: "The ID of the command to execute"
709
+ }
710
+ },
711
+ required: ["commandId"]
712
+ }
713
+ };
714
+ }
715
+ async runTool(args) {
716
+ try {
717
+ await this.client.executeCommand(args.commandId);
718
+ return this.createResponse({ message: `Successfully executed command: ${args.commandId}` });
719
+ }
720
+ catch (error) {
721
+ return this.handleError(error);
722
+ }
723
+ }
724
+ }
725
+ export class OpenFileToolHandler extends BaseToolHandler {
726
+ constructor(client) {
727
+ super("obsidian_open_file", client);
728
+ }
729
+ getToolDescription() {
730
+ return {
731
+ name: this.name,
732
+ description: "Open a specific file in Obsidian, optionally in a new leaf.",
733
+ examples: [
734
+ {
735
+ description: "Open a file in the current leaf",
736
+ args: {
737
+ filepath: "Projects/research.md"
738
+ }
739
+ },
740
+ {
741
+ description: "Open a file in a new leaf",
742
+ args: {
743
+ filepath: "Projects/research.md",
744
+ newLeaf: true
745
+ }
746
+ }
747
+ ],
748
+ inputSchema: {
749
+ type: "object",
750
+ properties: {
751
+ filepath: {
752
+ type: "string",
753
+ description: "Path to the file to open (relative to vault root)",
754
+ format: "path"
755
+ },
756
+ newLeaf: {
757
+ type: "boolean",
758
+ description: "Whether to open the file in a new leaf",
759
+ default: false
760
+ }
761
+ },
762
+ required: ["filepath"]
763
+ }
764
+ };
765
+ }
766
+ async runTool(args) {
767
+ try {
768
+ await this.client.openFile(args.filepath, args.newLeaf);
769
+ return this.createResponse({
770
+ message: `Successfully opened ${args.filepath}${args.newLeaf ? ' in new leaf' : ''}`
771
+ });
772
+ }
773
+ catch (error) {
774
+ return this.handleError(error);
775
+ }
776
+ }
777
+ }
778
+ export class GetActiveFileToolHandler extends BaseToolHandler {
779
+ constructor(client) {
780
+ super("obsidian_get_active_file", client);
781
+ }
782
+ getToolDescription() {
783
+ return {
784
+ name: this.name,
785
+ description: "Get the content and metadata of the currently active file in Obsidian.",
786
+ examples: [
787
+ {
788
+ description: "Get active file content",
789
+ args: {}
790
+ }
791
+ ],
792
+ inputSchema: {
793
+ type: "object",
794
+ properties: {},
795
+ required: []
796
+ }
797
+ };
798
+ }
799
+ async runTool() {
800
+ try {
801
+ const activeFile = await this.client.getActiveFile();
802
+ return this.createResponse(activeFile);
803
+ }
804
+ catch (error) {
805
+ return this.handleError(error);
806
+ }
807
+ }
808
+ }
809
+ export class GetPeriodicNoteToolHandler extends BaseToolHandler {
810
+ constructor(client) {
811
+ super("obsidian_get_periodic_note", client);
812
+ }
813
+ getToolDescription() {
814
+ return {
815
+ name: this.name,
816
+ description: "Get the content and metadata of a periodic note (daily, weekly, monthly, quarterly, or yearly).",
817
+ examples: [
818
+ {
819
+ description: "Get today's daily note",
820
+ args: {
821
+ period: "daily"
822
+ }
823
+ }
824
+ ],
825
+ inputSchema: {
826
+ type: "object",
827
+ properties: {
828
+ period: {
829
+ type: "string",
830
+ enum: ["daily", "weekly", "monthly", "quarterly", "yearly"],
831
+ description: "The type of periodic note to retrieve"
832
+ }
833
+ },
834
+ required: ["period"]
835
+ }
836
+ };
837
+ }
838
+ async runTool(args) {
839
+ try {
840
+ const note = await this.client.getPeriodicNote(args.period);
841
+ return this.createResponse(note);
480
842
  }
481
843
  catch (error) {
482
844
  return this.handleError(error);
483
845
  }
484
846
  }
485
847
  }
848
+ export const handlers = [
849
+ ListFilesInVaultToolHandler,
850
+ ListFilesInDirToolHandler,
851
+ GetFileContentsToolHandler,
852
+ FindInFileToolHandler,
853
+ AppendContentToolHandler,
854
+ PatchContentToolHandler,
855
+ ComplexSearchToolHandler,
856
+ GetTagsToolHandler,
857
+ ListCommandsToolHandler,
858
+ ExecuteCommandToolHandler,
859
+ OpenFileToolHandler,
860
+ GetActiveFileToolHandler,
861
+ GetPeriodicNoteToolHandler
862
+ ];
486
863
  //# sourceMappingURL=tools.js.map
package/build/types.js CHANGED
@@ -1,5 +1,5 @@
1
1
  export const DEFAULT_OBSIDIAN_CONFIG = {
2
- protocol: "https",
2
+ protocol: "https", // HTTPS required by default in Obsidian REST API plugin
3
3
  host: "127.0.0.1",
4
4
  port: 27124
5
5
  };
@@ -8,13 +8,30 @@ export const DEFAULT_RATE_LIMIT_CONFIG = {
8
8
  maxRequests: 200
9
9
  };
10
10
  export class ObsidianError extends Error {
11
- code;
12
11
  details;
13
- constructor(message, code, details) {
12
+ errorCode;
13
+ constructor(message, errorCode = 50000, // Default server error code
14
+ details) {
14
15
  super(message);
15
- this.code = code;
16
16
  this.details = details;
17
17
  this.name = "ObsidianError";
18
+ // Ensure 5-digit error code
19
+ if (errorCode < 10000 || errorCode > 99999) {
20
+ // Convert HTTP status codes to 5-digit codes
21
+ // 4xx -> 4xxxx
22
+ // 5xx -> 5xxxx
23
+ this.errorCode = errorCode < 1000 ? errorCode * 100 : 50000;
24
+ }
25
+ else {
26
+ this.errorCode = errorCode;
27
+ }
28
+ }
29
+ // Convert to API error format
30
+ toApiError() {
31
+ return {
32
+ errorCode: this.errorCode,
33
+ message: this.message
34
+ };
18
35
  }
19
36
  }
20
37
  //# sourceMappingURL=types.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "obsidian-mcp-server",
3
- "version": "1.2.3",
3
+ "version": "1.2.5",
4
4
  "description": "Model Context Protocol server for Obsidian integration with token-aware response handling",
5
5
  "main": "build/index.js",
6
6
  "type": "module",
@@ -18,7 +18,7 @@
18
18
  "format": "prettier --write \"src/**/*.ts\""
19
19
  },
20
20
  "dependencies": {
21
- "@modelcontextprotocol/sdk": "^1.4.0",
21
+ "@modelcontextprotocol/sdk": "^1.4.1",
22
22
  "axios": "^1.7.9",
23
23
  "dotenv": "^16.4.7",
24
24
  "tiktoken": "^1.0.18",
@@ -29,7 +29,7 @@
29
29
  "@types/node": "^22.10.10",
30
30
  "@typescript-eslint/eslint-plugin": "^8.21.0",
31
31
  "@typescript-eslint/parser": "^8.21.0",
32
- "eslint": "^9.18.0",
32
+ "eslint": "^9.19.0",
33
33
  "eslint-config-prettier": "^10.0.1",
34
34
  "eslint-plugin-prettier": "^5.2.3",
35
35
  "prettier": "^3.4.2",