obsidian-mcp-server 1.2.6 → 1.4.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 (79) hide show
  1. package/README.md +54 -10
  2. package/build/index.js +12 -3
  3. package/build/mcp/handlers.js +138 -0
  4. package/build/mcp/index.js +7 -0
  5. package/build/mcp/server.js +131 -0
  6. package/build/mcp/types.js +7 -0
  7. package/build/{obsidian.js → obsidian/client.js} +156 -104
  8. package/build/obsidian/errors.js +75 -0
  9. package/build/obsidian/index.js +7 -0
  10. package/build/obsidian/types.js +12 -0
  11. package/build/resources/index.js +15 -0
  12. package/build/{resources.js → resources/tags.js} +35 -14
  13. package/build/resources/types.js +5 -0
  14. package/build/tools/base.js +78 -0
  15. package/build/tools/files/content.js +171 -0
  16. package/build/tools/files/index.js +22 -0
  17. package/build/tools/files/list.js +133 -0
  18. package/build/tools/index.js +31 -0
  19. package/build/tools/properties/index.js +19 -0
  20. package/build/{properties.js → tools/properties/manager.js} +50 -13
  21. package/build/{propertyTools.js → tools/properties/tools.js} +22 -6
  22. package/build/{propertyTypes.js → tools/properties/types.js} +19 -7
  23. package/build/tools/search/complex.js +203 -0
  24. package/build/tools/search/index.js +20 -0
  25. package/build/tools/search/simple.js +127 -0
  26. package/build/utils/errors.js +59 -0
  27. package/build/utils/index.js +9 -0
  28. package/build/utils/logging.js +119 -0
  29. package/build/utils/rate-limiting.js +94 -0
  30. package/build/utils/tokenization.js +62 -0
  31. package/build/utils/validation.js +88 -0
  32. package/examples/README.md +48 -0
  33. package/examples/append-content.md +63 -0
  34. package/examples/complex-search.md +117 -0
  35. package/examples/find-in-file.md +94 -0
  36. package/examples/get-file-contents.md +72 -0
  37. package/examples/get-properties.md +89 -0
  38. package/examples/list-files-in-dir.md +55 -0
  39. package/examples/list-files-in-vault.md +53 -0
  40. package/examples/patch-content.md +60 -0
  41. package/examples/update-properties.md +126 -0
  42. package/mcp-client-config.example.json +23 -0
  43. package/package.json +4 -3
  44. package/src/index.ts +13 -3
  45. package/src/mcp/handlers.ts +183 -0
  46. package/src/mcp/index.ts +6 -0
  47. package/src/mcp/server.ts +162 -0
  48. package/src/mcp/types.ts +46 -0
  49. package/src/{obsidian.ts → obsidian/client.ts} +174 -134
  50. package/src/obsidian/errors.ts +105 -0
  51. package/src/obsidian/index.ts +6 -0
  52. package/src/obsidian/types.ts +124 -0
  53. package/src/resources/index.ts +17 -0
  54. package/src/{resources.ts → resources/tags.ts} +42 -16
  55. package/src/resources/types.ts +30 -0
  56. package/src/tools/base.ts +112 -0
  57. package/src/tools/files/content.ts +206 -0
  58. package/src/tools/files/index.ts +31 -0
  59. package/src/tools/files/list.ts +150 -0
  60. package/src/tools/index.ts +38 -0
  61. package/src/tools/properties/index.ts +21 -0
  62. package/src/{properties.ts → tools/properties/manager.ts} +54 -13
  63. package/src/{propertyTools.ts → tools/properties/tools.ts} +33 -7
  64. package/src/{propertyTypes.ts → tools/properties/types.ts} +31 -7
  65. package/src/tools/search/complex.ts +231 -0
  66. package/src/tools/search/index.ts +22 -0
  67. package/src/tools/search/simple.ts +147 -0
  68. package/src/utils/errors.ts +69 -0
  69. package/src/utils/index.ts +8 -0
  70. package/src/utils/logging.ts +146 -0
  71. package/src/utils/rate-limiting.ts +114 -0
  72. package/src/utils/tokenization.ts +71 -0
  73. package/src/utils/validation.ts +95 -0
  74. package/build/server.js +0 -238
  75. package/build/tools.js +0 -863
  76. package/build/types.js +0 -37
  77. package/src/server.ts +0 -308
  78. package/src/tools.ts +0 -946
  79. package/src/types.ts +0 -184
@@ -1,7 +1,13 @@
1
- import { PropertyManager } from "./properties.js";
1
+ import { PropertyManager } from "../tools/properties/manager.js";
2
+ import { sep } from "path";
3
+ import { createLogger } from "../utils/logging.js";
4
+ // Create a logger for tag resources
5
+ const logger = createLogger('TagResource');
6
+ /**
7
+ * Resource for providing tags used in the Obsidian vault
8
+ */
2
9
  export class TagResource {
3
10
  client;
4
- static TAG_PATTERN = /#[a-zA-Z0-9_-]+/g;
5
11
  tagCache = new Map();
6
12
  propertyManager;
7
13
  isInitialized = false;
@@ -12,6 +18,9 @@ export class TagResource {
12
18
  this.propertyManager = new PropertyManager(client);
13
19
  this.initializeCache();
14
20
  }
21
+ /**
22
+ * Get resource description for the MCP server
23
+ */
15
24
  getResourceDescription() {
16
25
  return {
17
26
  uri: "obsidian://tags",
@@ -20,11 +29,15 @@ export class TagResource {
20
29
  mimeType: "application/json"
21
30
  };
22
31
  }
32
+ /**
33
+ * Initialize the tag cache
34
+ */
23
35
  async initializeCache() {
24
36
  try {
25
- // Get all markdown files
37
+ logger.info('Initializing tag cache');
38
+ // Get all markdown files using platform-agnostic path pattern
26
39
  const query = {
27
- "glob": ["**/*.md", { "var": "path" }]
40
+ "glob": [`**${sep}*.md`.replace(/\\/g, '/'), { "var": "path" }]
28
41
  };
29
42
  const results = await this.client.searchJson(query);
30
43
  this.tagCache.clear();
@@ -34,46 +47,53 @@ export class TagResource {
34
47
  continue;
35
48
  try {
36
49
  const content = await this.client.getFileContents(result.filename);
37
- // Extract tags from frontmatter
50
+ // Only extract tags from frontmatter YAML
38
51
  const properties = this.propertyManager.parseProperties(content);
39
52
  if (properties.tags) {
40
53
  properties.tags.forEach((tag) => {
41
54
  this.addTag(tag, result.filename);
42
55
  });
43
56
  }
44
- // Extract inline tags
45
- const inlineTags = content.match(TagResource.TAG_PATTERN) || [];
46
- inlineTags.forEach(tag => {
47
- this.addTag(tag, result.filename);
48
- });
49
57
  }
50
58
  catch (error) {
51
- console.error(`Failed to process file ${result.filename}:`, error);
59
+ logger.error(`Failed to process file ${result.filename}:`, error);
52
60
  }
53
61
  }
54
62
  this.isInitialized = true;
55
63
  this.lastUpdate = Date.now();
64
+ logger.info(`Tag cache initialized with ${this.tagCache.size} unique tags`);
56
65
  }
57
66
  catch (error) {
58
- console.error("Failed to initialize tag cache:", error);
67
+ logger.error("Failed to initialize tag cache:", error);
59
68
  throw error;
60
69
  }
61
70
  }
71
+ /**
72
+ * Add a tag to the cache
73
+ */
62
74
  addTag(tag, filepath) {
63
75
  if (!this.tagCache.has(tag)) {
64
76
  this.tagCache.set(tag, new Set());
65
77
  }
66
78
  this.tagCache.get(tag).add(filepath);
67
79
  }
80
+ /**
81
+ * Update the cache if needed
82
+ */
68
83
  async updateCacheIfNeeded() {
69
84
  const now = Date.now();
70
85
  if (now - this.lastUpdate > this.updateInterval) {
86
+ logger.debug('Tag cache needs update, refreshing...');
71
87
  await this.initializeCache();
72
88
  }
73
89
  }
90
+ /**
91
+ * Get the content for the resource
92
+ */
74
93
  async getContent() {
75
94
  try {
76
95
  if (!this.isInitialized) {
96
+ logger.info('Tag cache not initialized, initializing now');
77
97
  await this.initializeCache();
78
98
  }
79
99
  else {
@@ -96,6 +116,7 @@ export class TagResource {
96
116
  lastUpdate: this.lastUpdate
97
117
  }
98
118
  };
119
+ logger.debug(`Returning tag resource with ${response.tags.length} tags`);
99
120
  return [{
100
121
  type: "text",
101
122
  text: JSON.stringify(response, null, 2),
@@ -103,9 +124,9 @@ export class TagResource {
103
124
  }];
104
125
  }
105
126
  catch (error) {
106
- console.error("Failed to get tags:", error);
127
+ logger.error("Failed to get tags:", error);
107
128
  throw error;
108
129
  }
109
130
  }
110
131
  }
111
- //# sourceMappingURL=resources.js.map
132
+ //# sourceMappingURL=tags.js.map
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Resource types for the MCP server
3
+ */
4
+ export {};
5
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1,78 @@
1
+ import { ObsidianError } from "../utils/errors.js";
2
+ import { tokenCounter } from "../utils/tokenization.js";
3
+ import { createLogger } from "../utils/logging.js";
4
+ // Create a logger for tool operations
5
+ const logger = createLogger('Tools');
6
+ /**
7
+ * Base class for all tool handlers with common functionality
8
+ */
9
+ export class BaseToolHandler {
10
+ name;
11
+ client;
12
+ /**
13
+ * Create a new tool handler
14
+ * @param name The name of the tool
15
+ * @param client The ObsidianClient instance
16
+ */
17
+ constructor(name, client) {
18
+ this.name = name;
19
+ this.client = client;
20
+ }
21
+ /**
22
+ * Create a standardized response from any content type
23
+ * @param content The content to format for response
24
+ * @returns An array of TextContent objects
25
+ */
26
+ createResponse(content) {
27
+ let text;
28
+ // Handle different content types
29
+ if (typeof content === 'string') {
30
+ text = content;
31
+ }
32
+ else if (content instanceof Buffer) {
33
+ text = content.toString('utf-8');
34
+ }
35
+ else if (Array.isArray(content) && content.every(item => typeof item === 'string')) {
36
+ text = content.join('\n');
37
+ }
38
+ else if (content instanceof Error) {
39
+ text = `Error: ${content.message}\n${content.stack || ''}`;
40
+ }
41
+ else {
42
+ try {
43
+ text = JSON.stringify(content, null, 2);
44
+ }
45
+ catch (error) {
46
+ text = String(content);
47
+ }
48
+ }
49
+ // Count tokens and truncate if necessary
50
+ const originalTokenCount = tokenCounter.countTokens(text);
51
+ const truncatedText = tokenCounter.truncateToTokenLimit(text);
52
+ const finalTokenCount = tokenCounter.countTokens(truncatedText);
53
+ if (originalTokenCount > finalTokenCount) {
54
+ logger.debug(`[${this.name}] Response truncated:`, `original tokens=${originalTokenCount}`, `truncated tokens=${finalTokenCount}`);
55
+ }
56
+ return [{
57
+ type: "text",
58
+ text: truncatedText
59
+ }];
60
+ }
61
+ /**
62
+ * Standard error handling for tool execution
63
+ * @param error The error to handle
64
+ * @throws ObsidianError
65
+ */
66
+ handleError(error) {
67
+ if (error instanceof ObsidianError) {
68
+ throw error;
69
+ }
70
+ if (error instanceof Error) {
71
+ throw new ObsidianError(`Tool '${this.name}' execution failed: ${error.message}`, 50000, // Internal server error
72
+ { originalError: error.stack });
73
+ }
74
+ throw new ObsidianError(`Tool '${this.name}' execution failed with unknown error`, 50000, // Internal server error
75
+ { error });
76
+ }
77
+ }
78
+ //# sourceMappingURL=base.js.map
@@ -0,0 +1,171 @@
1
+ import { BaseToolHandler } from "../base.js";
2
+ import { createLogger } from "../../utils/logging.js";
3
+ // Create a logger for file content operations
4
+ const logger = createLogger('FileContentTools');
5
+ /**
6
+ * Tool names for file content operations
7
+ */
8
+ export const FILE_CONTENT_TOOL_NAMES = {
9
+ GET_FILE_CONTENTS: "obsidian_get_file_contents",
10
+ APPEND_CONTENT: "obsidian_append_content",
11
+ PATCH_CONTENT: "obsidian_patch_content"
12
+ };
13
+ /**
14
+ * Tool handler for getting file contents
15
+ */
16
+ export class GetFileContentsToolHandler extends BaseToolHandler {
17
+ constructor(client) {
18
+ super(FILE_CONTENT_TOOL_NAMES.GET_FILE_CONTENTS, client);
19
+ }
20
+ getToolDescription() {
21
+ return {
22
+ name: this.name,
23
+ description: "Return the content of a single file in your vault. Supports markdown files, text files, and other readable formats. Returns the raw content including any YAML frontmatter.",
24
+ examples: [
25
+ {
26
+ description: "Get content of a markdown note",
27
+ args: {
28
+ filepath: "Projects/research.md"
29
+ }
30
+ },
31
+ {
32
+ description: "Get content of a configuration file",
33
+ args: {
34
+ filepath: "configs/settings.yml"
35
+ }
36
+ }
37
+ ],
38
+ inputSchema: {
39
+ type: "object",
40
+ properties: {
41
+ filepath: {
42
+ type: "string",
43
+ description: "Path to the relevant file (relative to your vault root).",
44
+ format: "path"
45
+ }
46
+ },
47
+ required: ["filepath"]
48
+ }
49
+ };
50
+ }
51
+ async runTool(args) {
52
+ try {
53
+ logger.debug(`Getting contents of file: ${args.filepath}`);
54
+ const content = await this.client.getFileContents(args.filepath);
55
+ return this.createResponse(content);
56
+ }
57
+ catch (error) {
58
+ return this.handleError(error);
59
+ }
60
+ }
61
+ }
62
+ /**
63
+ * Tool handler for appending content to a file
64
+ */
65
+ export class AppendContentToolHandler extends BaseToolHandler {
66
+ constructor(client) {
67
+ super(FILE_CONTENT_TOOL_NAMES.APPEND_CONTENT, client);
68
+ }
69
+ getToolDescription() {
70
+ return {
71
+ name: this.name,
72
+ description: "Append content to a new or existing file in the vault.",
73
+ examples: [
74
+ {
75
+ description: "Append a new task",
76
+ args: {
77
+ filepath: "tasks.md",
78
+ content: "- [ ] New task to complete"
79
+ }
80
+ },
81
+ {
82
+ description: "Append meeting notes",
83
+ args: {
84
+ filepath: "meetings/2025-01-23.md",
85
+ content: "## Meeting Notes\n\n- Discussed project timeline\n- Assigned tasks"
86
+ }
87
+ }
88
+ ],
89
+ inputSchema: {
90
+ type: "object",
91
+ properties: {
92
+ filepath: {
93
+ type: "string",
94
+ description: "Path to the file (relative to vault root)",
95
+ format: "path"
96
+ },
97
+ content: {
98
+ type: "string",
99
+ description: "Content to append to the file"
100
+ }
101
+ },
102
+ required: ["filepath", "content"]
103
+ }
104
+ };
105
+ }
106
+ async runTool(args) {
107
+ try {
108
+ logger.debug(`Appending content to file: ${args.filepath}`);
109
+ await this.client.appendContent(args.filepath, args.content);
110
+ return this.createResponse({
111
+ message: `Successfully appended content to ${args.filepath}`,
112
+ success: true
113
+ });
114
+ }
115
+ catch (error) {
116
+ return this.handleError(error);
117
+ }
118
+ }
119
+ }
120
+ /**
121
+ * Tool handler for updating file content
122
+ */
123
+ export class PatchContentToolHandler extends BaseToolHandler {
124
+ constructor(client) {
125
+ super(FILE_CONTENT_TOOL_NAMES.PATCH_CONTENT, client);
126
+ }
127
+ getToolDescription() {
128
+ return {
129
+ name: this.name,
130
+ description: "Update the entire content of an existing note or create a new one.",
131
+ examples: [
132
+ {
133
+ description: "Update a note's content",
134
+ args: {
135
+ filepath: "project.md",
136
+ content: "# Project Notes\n\nThis will replace the entire content of the note."
137
+ }
138
+ }
139
+ ],
140
+ inputSchema: {
141
+ type: "object",
142
+ properties: {
143
+ filepath: {
144
+ type: "string",
145
+ description: "Path to the file (relative to vault root)",
146
+ format: "path"
147
+ },
148
+ content: {
149
+ type: "string",
150
+ description: "New content for the note (replaces existing content)"
151
+ }
152
+ },
153
+ required: ["filepath", "content"]
154
+ }
155
+ };
156
+ }
157
+ async runTool(args) {
158
+ try {
159
+ logger.debug(`Updating content of file: ${args.filepath}`);
160
+ await this.client.updateContent(args.filepath, args.content);
161
+ return this.createResponse({
162
+ message: `Successfully updated content in ${args.filepath}`,
163
+ success: true
164
+ });
165
+ }
166
+ catch (error) {
167
+ return this.handleError(error);
168
+ }
169
+ }
170
+ }
171
+ //# sourceMappingURL=content.js.map
@@ -0,0 +1,22 @@
1
+ /**
2
+ * File operation tools exports
3
+ */
4
+ export * from './list.js';
5
+ export * from './content.js';
6
+ import { ListFilesInVaultToolHandler, ListFilesInDirToolHandler } from './list.js';
7
+ import { GetFileContentsToolHandler, AppendContentToolHandler, PatchContentToolHandler } from './content.js';
8
+ /**
9
+ * Create all file-related tool handlers
10
+ * @param client The ObsidianClient instance
11
+ * @returns Array of file tool handlers
12
+ */
13
+ export function createFileToolHandlers(client) {
14
+ return [
15
+ new ListFilesInVaultToolHandler(client),
16
+ new ListFilesInDirToolHandler(client),
17
+ new GetFileContentsToolHandler(client),
18
+ new AppendContentToolHandler(client),
19
+ new PatchContentToolHandler(client)
20
+ ];
21
+ }
22
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,133 @@
1
+ import { BaseToolHandler } from "../base.js";
2
+ import { createLogger } from "../../utils/logging.js";
3
+ // Create a logger for file operations
4
+ const logger = createLogger('FileTools');
5
+ /**
6
+ * Tool names for file listing operations
7
+ */
8
+ export const FILE_TOOL_NAMES = {
9
+ LIST_FILES_IN_VAULT: "obsidian_list_files_in_vault",
10
+ LIST_FILES_IN_DIR: "obsidian_list_files_in_dir"
11
+ };
12
+ /**
13
+ * Tool handler for listing all files in the vault
14
+ */
15
+ export class ListFilesInVaultToolHandler extends BaseToolHandler {
16
+ constructor(client) {
17
+ super(FILE_TOOL_NAMES.LIST_FILES_IN_VAULT, client);
18
+ }
19
+ getToolDescription() {
20
+ return {
21
+ name: this.name,
22
+ description: "Lists all files and directories in the root directory of your Obsidian vault. Returns a hierarchical structure of files and folders, including metadata like file type.",
23
+ examples: [
24
+ {
25
+ description: "List all files in vault",
26
+ args: {}
27
+ },
28
+ {
29
+ description: "Example response",
30
+ args: {},
31
+ response: [
32
+ {
33
+ "path": "Daily Notes",
34
+ "type": "folder",
35
+ "children": [
36
+ { "path": "Daily Notes/2025-01-24.md", "type": "file" }
37
+ ]
38
+ },
39
+ {
40
+ "path": "Projects",
41
+ "type": "folder",
42
+ "children": [
43
+ { "path": "Projects/MCP.md", "type": "file" }
44
+ ]
45
+ }
46
+ ]
47
+ }
48
+ ],
49
+ inputSchema: {
50
+ type: "object",
51
+ properties: {},
52
+ required: []
53
+ }
54
+ };
55
+ }
56
+ async runTool() {
57
+ try {
58
+ logger.debug('Listing all files in vault');
59
+ const files = await this.client.listFilesInVault();
60
+ return this.createResponse(files);
61
+ }
62
+ catch (error) {
63
+ return this.handleError(error);
64
+ }
65
+ }
66
+ }
67
+ /**
68
+ * Tool handler for listing files in a specific directory
69
+ */
70
+ export class ListFilesInDirToolHandler extends BaseToolHandler {
71
+ constructor(client) {
72
+ super(FILE_TOOL_NAMES.LIST_FILES_IN_DIR, client);
73
+ }
74
+ getToolDescription() {
75
+ return {
76
+ name: this.name,
77
+ description: "Lists all files and directories that exist in a specific Obsidian directory. Returns a hierarchical structure showing files, folders, and their relationships. Useful for exploring vault organization and finding specific files.",
78
+ examples: [
79
+ {
80
+ description: "List files in Documents folder",
81
+ args: {
82
+ dirpath: "Documents"
83
+ }
84
+ },
85
+ {
86
+ description: "Example response structure",
87
+ args: {
88
+ dirpath: "Projects"
89
+ },
90
+ response: [
91
+ {
92
+ "path": "Projects/Active",
93
+ "type": "folder",
94
+ "children": [
95
+ { "path": "Projects/Active/ProjectA.md", "type": "file" },
96
+ { "path": "Projects/Active/ProjectB.md", "type": "file" }
97
+ ]
98
+ },
99
+ {
100
+ "path": "Projects/Archive",
101
+ "type": "folder",
102
+ "children": [
103
+ { "path": "Projects/Archive/OldProject.md", "type": "file" }
104
+ ]
105
+ }
106
+ ]
107
+ }
108
+ ],
109
+ inputSchema: {
110
+ type: "object",
111
+ properties: {
112
+ dirpath: {
113
+ type: "string",
114
+ description: "Path to list files from (relative to your vault root). Note that empty directories will not be returned.",
115
+ format: "path"
116
+ }
117
+ },
118
+ required: ["dirpath"]
119
+ }
120
+ };
121
+ }
122
+ async runTool(args) {
123
+ try {
124
+ logger.debug(`Listing files in directory: ${args.dirpath}`);
125
+ const files = await this.client.listFilesInDir(args.dirpath);
126
+ return this.createResponse(files);
127
+ }
128
+ catch (error) {
129
+ return this.handleError(error);
130
+ }
131
+ }
132
+ }
133
+ //# sourceMappingURL=list.js.map
@@ -0,0 +1,31 @@
1
+ import { createFileToolHandlers } from './files/index.js';
2
+ import { createSearchToolHandlers } from './search/index.js';
3
+ import { createPropertyToolHandlers } from './properties/index.js';
4
+ // Export the base handler and all submodules
5
+ export * from './base.js';
6
+ export * from './files/index.js';
7
+ export * from './search/index.js';
8
+ export * from './properties/index.js';
9
+ /**
10
+ * Create all tool handlers
11
+ * @param client The ObsidianClient instance
12
+ * @returns Array of all tool handlers
13
+ */
14
+ export function createToolHandlers(client) {
15
+ return [
16
+ ...createFileToolHandlers(client),
17
+ ...createSearchToolHandlers(client),
18
+ ...createPropertyToolHandlers(client)
19
+ ];
20
+ }
21
+ /**
22
+ * Get a tool handler map by name
23
+ * @param handlers Array of tool handlers
24
+ * @returns Map of tool names to handlers
25
+ */
26
+ export function createToolHandlerMap(handlers) {
27
+ const toolHandlerMap = new Map();
28
+ handlers.forEach(handler => toolHandlerMap.set(handler.name, handler));
29
+ return toolHandlerMap;
30
+ }
31
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Properties module exports
3
+ */
4
+ export * from './types.js';
5
+ export * from './manager.js';
6
+ export * from './tools.js';
7
+ import { GetPropertiesToolHandler, UpdatePropertiesToolHandler } from './tools.js';
8
+ /**
9
+ * Create all property-related tool handlers
10
+ * @param client The ObsidianClient instance
11
+ * @returns Array of property tool handlers
12
+ */
13
+ export function createPropertyToolHandlers(client) {
14
+ return [
15
+ new GetPropertiesToolHandler(client),
16
+ new UpdatePropertiesToolHandler(client)
17
+ ];
18
+ }
19
+ //# sourceMappingURL=index.js.map