obsidian-mcp-server 1.4.1 → 1.5.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/README.md CHANGED
@@ -1,10 +1,10 @@
1
1
  # Obsidian MCP Server
2
2
 
3
3
  [![TypeScript](https://img.shields.io/badge/TypeScript-5.7-blue.svg)](https://www.typescriptlang.org/)
4
- [![Model Context Protocol](https://img.shields.io/badge/MCP-1.6.1-green.svg)](https://modelcontextprotocol.io/)
5
- [![Version](https://img.shields.io/badge/Version-1.4.1-blue.svg)]()
4
+ [![Model Context Protocol](https://img.shields.io/badge/MCP-1.7.0-green.svg)](https://modelcontextprotocol.io/)
5
+ [![Version](https://img.shields.io/badge/Version-1.5.0-blue.svg)]()
6
6
  [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
7
- [![Status](https://img.shields.io/badge/Status-Stable-blue.svg)]()
7
+ [![Status](https://img.shields.io/badge/Status-Stable-green.svg)]()
8
8
  [![GitHub](https://img.shields.io/github/stars/cyanheads/obsidian-mcp-server?style=social)](https://github.com/cyanheads/obsidian-mcp-server)
9
9
 
10
10
  A Model Context Protocol server designed for LLMs to interact with Obsidian vaults. Built with TypeScript and featuring secure API communication, efficient file operations, and comprehensive search capabilities, it enables AI assistants to seamlessly manage knowledge bases through a clean, flexible tool interface.
@@ -16,21 +16,25 @@ Requires the Local REST API plugin in Obsidian.
16
16
  ## Features
17
17
 
18
18
  ### File Operations
19
+
19
20
  - Atomic file/directory operations with validation
20
21
  - Resource monitoring and cleanup
21
22
  - Error handling and graceful failure
22
23
 
23
24
  ### Search System
25
+
24
26
  - Full-text search with configurable context
25
27
  - Advanced JsonLogic queries for files, tags, and metadata
26
28
  - Support for glob patterns and frontmatter fields
27
29
 
28
30
  ### Property Management
31
+
29
32
  - YAML frontmatter parsing and intelligent merging
30
33
  - Automatic timestamps (created by Obsidian, modified by server)
31
34
  - Custom field support
32
35
 
33
36
  ### Security & Performance
37
+
34
38
  - API key auth with rate limiting and SSL options
35
39
  - Resource monitoring and health checks
36
40
  - Graceful shutdown handling
@@ -41,6 +45,7 @@ Note: Requires Node.js
41
45
 
42
46
  1. Enable Local REST API plugin in Obsidian
43
47
  2. Clone and build:
48
+
44
49
  ```bash
45
50
  git clone git@github.com:cyanheads/obsidian-mcp-server.git
46
51
  cd obsidian-mcp-server
@@ -49,6 +54,7 @@ npm run build
49
54
  ```
50
55
 
51
56
  Or install from npm:
57
+
52
58
  ```bash
53
59
  npm install obsidian-mcp-server
54
60
  ```
@@ -84,24 +90,29 @@ Add to your MCP client settings (e.g., `claude_desktop_config.json` or `cline_mc
84
90
  Environment Variables:
85
91
 
86
92
  Required:
93
+
87
94
  - `OBSIDIAN_API_KEY`: Your API key from Obsidian's Local REST API plugin settings
88
95
 
89
96
  Connection Settings:
97
+
90
98
  - `VERIFY_SSL`: Enable SSL certificate verification (default: false) # This must be set to false for self-signed certificates. If you are running locally or don't understand what this means, this should be set to false.
91
99
  - `OBSIDIAN_PROTOCOL`: Protocol to use (default: "https")
92
100
  - `OBSIDIAN_HOST`: Host address (default: "127.0.0.1")
93
101
  - `OBSIDIAN_PORT`: Port number (default: 27124)
94
102
 
95
103
  Request Limits:
104
+
96
105
  - `REQUEST_TIMEOUT`: Request timeout in milliseconds (default: 5000)
97
106
  - `MAX_CONTENT_LENGTH`: Maximum response content length in bytes (default: 52428800 [50MB])
98
107
  - `MAX_BODY_LENGTH`: Maximum request body length in bytes (default: 52428800 [50MB])
99
108
 
100
109
  Rate Limiting:
110
+
101
111
  - `RATE_LIMIT_WINDOW_MS`: Rate limit window in milliseconds (default: 900000 [15 minutes])
102
112
  - `RATE_LIMIT_MAX_REQUESTS`: Maximum requests per window (default: 200)
103
113
 
104
114
  Tool Execution:
115
+
105
116
  - `TOOL_TIMEOUT_MS`: Tool execution timeout in milliseconds (default: 60000 [1 minute])
106
117
 
107
118
  ## Project Structure
@@ -124,22 +135,25 @@ src/
124
135
  ## Tools
125
136
 
126
137
  ### File Management
138
+
127
139
  ```typescript
128
140
  // List vault contents
129
- obsidian_list_files_in_vault: {}
141
+ obsidian_list_files_in_vault: {
142
+ }
130
143
 
131
144
  // List directory contents
132
145
  obsidian_list_files_in_dir: {
133
- dirpath: string // Path relative to vault root
146
+ dirpath: string; // Path relative to vault root
134
147
  }
135
148
 
136
149
  // Get file contents
137
150
  obsidian_get_file_contents: {
138
- filepath: string // Path relative to vault root
151
+ filepath: string; // Path relative to vault root
139
152
  }
140
153
  ```
141
154
 
142
155
  ### Search Operations
156
+
143
157
  ```typescript
144
158
  // Text search with context
145
159
  obsidian_find_in_file: {
@@ -171,6 +185,7 @@ obsidian_get_tags: {
171
185
  ```
172
186
 
173
187
  ### Content Modification
188
+
174
189
  ```typescript
175
190
  // Append to file
176
191
  obsidian_append_content: {
@@ -186,6 +201,7 @@ obsidian_patch_content: {
186
201
  ```
187
202
 
188
203
  ### Property Management
204
+
189
205
  ```typescript
190
206
  // Get note properties
191
207
  obsidian_get_properties: {
@@ -199,8 +215,8 @@ obsidian_update_properties: {
199
215
  title?: string,
200
216
  author?: string,
201
217
  // Note: created/modified timestamps are managed automatically
202
- type?: Array<"concept" | "architecture" | "specification" |
203
- "protocol" | "api" | "research" | "implementation" |
218
+ type?: Array<"concept" | "architecture" | "specification" |
219
+ "protocol" | "api" | "research" | "implementation" |
204
220
  "guide" | "reference">,
205
221
  tags?: string[], // Must start with #
206
222
  status?: Array<"draft" | "in-progress" | "review" | "complete">,
@@ -219,21 +235,25 @@ obsidian_update_properties: {
219
235
  ## Best Practices
220
236
 
221
237
  ### File Operations
238
+
222
239
  - Use atomic operations with validation
223
240
  - Handle errors and monitor performance
224
241
 
225
242
  ### Search Implementation
243
+
226
244
  - Use appropriate search tool for the task:
227
245
  - obsidian_find_in_file for text search
228
246
  - obsidian_complex_search for metadata/tag filtering
229
247
  - Keep context size reasonable (default: 10 chars)
230
248
 
231
249
  ### Property Management
250
+
232
251
  - Use appropriate types and validate updates
233
252
  - Handle arrays and custom fields properly
234
253
  - Never set timestamps (managed automatically)
235
254
 
236
255
  ### Error Prevention
256
+
237
257
  - Validate inputs and handle errors gracefully
238
258
  - Monitor patterns and respect rate limits
239
259
 
@@ -5,10 +5,32 @@ import { ListToolsRequestSchema, CallToolRequestSchema, ListResourcesRequestSche
5
5
  import { ObsidianError } from "../utils/errors.js";
6
6
  import { validateToolArguments } from "../utils/validation.js";
7
7
  import { rateLimiter } from "../utils/rate-limiting.js";
8
- import { createLogger } from "../utils/logging.js";
9
- import { DEFAULT_TIMEOUT_CONFIG } from "./types.js";
8
+ import { createLogger, ErrorCategoryType } from "../utils/logging.js";
9
+ import { DEFAULT_TIMEOUT_CONFIG, McpErrorCode } from "./types.js";
10
10
  // Create a logger for request handlers
11
11
  const logger = createLogger('McpHandlers');
12
+ /**
13
+ * Helper function to safely mask sensitive data
14
+ */
15
+ function maskSensitiveData(data) {
16
+ if (!data)
17
+ return {};
18
+ const sensitiveFields = ['password', 'token', 'secret', 'key', 'auth', 'credential'];
19
+ const result = {};
20
+ for (const [key, value] of Object.entries(data)) {
21
+ const isSensitive = sensitiveFields.some(field => key.toLowerCase().includes(field.toLowerCase()));
22
+ if (isSensitive) {
23
+ result[key] = '********';
24
+ }
25
+ else if (value && typeof value === 'object' && !Array.isArray(value)) {
26
+ result[key] = maskSensitiveData(value);
27
+ }
28
+ else {
29
+ result[key] = value;
30
+ }
31
+ }
32
+ return result;
33
+ }
12
34
  /**
13
35
  * Set up tool listing handler
14
36
  * @param server The MCP server instance
@@ -17,11 +39,27 @@ const logger = createLogger('McpHandlers');
17
39
  export function setupToolListingHandler(server, toolHandlers) {
18
40
  server.setRequestHandler(ListToolsRequestSchema, async () => {
19
41
  logger.debug('Handling ListToolsRequest');
20
- const tools = [];
21
- for (const handler of toolHandlers.values()) {
22
- tools.push(handler.getToolDescription());
42
+ // Start performance timing
43
+ logger.startTimer('list_tools');
44
+ try {
45
+ const tools = [];
46
+ for (const handler of toolHandlers.values()) {
47
+ tools.push(handler.getToolDescription());
48
+ }
49
+ // Log success and timing information
50
+ const elapsedMs = logger.endTimer('list_tools', 'Listed tools');
51
+ logger.logOperationResult(true, 'list_tools', elapsedMs, {
52
+ toolCount: tools.length
53
+ });
54
+ return { tools };
55
+ }
56
+ catch (error) {
57
+ // Log failure with timing information
58
+ const elapsedMs = logger.endTimer('list_tools', 'Failed to list tools');
59
+ logger.logOperationResult(false, 'list_tools', elapsedMs);
60
+ logger.error('Failed to list available tools', error instanceof Error ? error : undefined);
61
+ throw error;
23
62
  }
24
- return { tools };
25
63
  });
26
64
  }
27
65
  /**
@@ -32,26 +70,53 @@ export function setupToolListingHandler(server, toolHandlers) {
32
70
  export function setupToolCallingHandler(server, toolHandlers) {
33
71
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
34
72
  const { name, arguments: args } = request.params;
35
- logger.debug(`Handling CallToolRequest for tool: ${name}`);
73
+ const operationId = `call_tool_${name}_${Date.now()}`;
74
+ logger.debug(`Handling CallToolRequest for tool: ${name}`, {
75
+ toolName: name,
76
+ operationId
77
+ });
78
+ // Start performance timing
79
+ logger.startTimer(operationId);
80
+ // Handle unknown tool
36
81
  const handler = toolHandlers.get(name);
37
82
  if (!handler) {
38
- logger.error(`Unknown tool requested: ${name}`);
39
- throw new ObsidianError(`Unknown tool: ${name}`, 40400); // 40400 = Not found
83
+ const errorInfo = {
84
+ toolName: name,
85
+ errorCode: McpErrorCode.NOT_FOUND,
86
+ errorCategory: ErrorCategoryType.CATEGORY_VALIDATION
87
+ };
88
+ logger.error(`Unknown tool requested: ${name}`, errorInfo);
89
+ const elapsedMs = logger.endTimer(operationId);
90
+ logger.logOperationResult(false, 'call_tool', elapsedMs, errorInfo);
91
+ throw new ObsidianError(`Unknown tool: ${name}`, McpErrorCode.NOT_FOUND);
40
92
  }
41
93
  // Check rate limit
42
94
  try {
43
95
  rateLimiter.enforceRateLimit(name);
44
96
  }
45
97
  catch (error) {
46
- // If rate limit is exceeded, log and rethrow the error
47
- logger.warn(`Rate limit exceeded for tool: ${name}`);
48
- throw error;
98
+ const errorInfo = {
99
+ toolName: name,
100
+ errorCode: McpErrorCode.RATE_LIMIT_EXCEEDED,
101
+ errorCategory: ErrorCategoryType.CATEGORY_SYSTEM
102
+ };
103
+ logger.warn(`Rate limit exceeded for tool: ${name}`, errorInfo);
104
+ const elapsedMs = logger.endTimer(operationId);
105
+ logger.logOperationResult(false, 'call_tool', elapsedMs, errorInfo);
106
+ throw new ObsidianError(`Rate limit exceeded for tool: ${name}`, McpErrorCode.RATE_LIMIT_EXCEEDED);
49
107
  }
50
108
  // Add timeout handling
51
109
  const timeoutMs = DEFAULT_TIMEOUT_CONFIG.toolExecutionMs;
52
110
  const timeoutPromise = new Promise((_, reject) => {
53
111
  setTimeout(() => {
54
- reject(new ObsidianError(`Tool execution timed out after ${timeoutMs}ms`, 40800)); // 40800 = Request timeout
112
+ const errorInfo = {
113
+ toolName: name,
114
+ timeoutMs,
115
+ errorCode: McpErrorCode.TIMEOUT,
116
+ errorCategory: ErrorCategoryType.CATEGORY_SYSTEM
117
+ };
118
+ logger.error(`Tool execution timed out after ${timeoutMs}ms`, errorInfo);
119
+ reject(new ObsidianError(`Tool execution timed out after ${timeoutMs}ms`, McpErrorCode.TIMEOUT));
55
120
  }, timeoutMs);
56
121
  });
57
122
  try {
@@ -59,24 +124,48 @@ export function setupToolCallingHandler(server, toolHandlers) {
59
124
  const toolDescription = handler.getToolDescription();
60
125
  const validationResult = validateToolArguments(args, toolDescription.inputSchema);
61
126
  if (!validationResult.valid) {
62
- logger.error(`Invalid tool arguments for ${name}:`, validationResult.errors);
63
- throw new ObsidianError(`Invalid tool arguments: ${validationResult.errors.join(', ')}`, 40000 // 40000 = Bad request
64
- );
127
+ const errorInfo = {
128
+ toolName: name,
129
+ validationErrors: validationResult.errors,
130
+ providedArgs: maskSensitiveData(args),
131
+ errorCode: McpErrorCode.BAD_REQUEST,
132
+ errorCategory: ErrorCategoryType.CATEGORY_VALIDATION
133
+ };
134
+ logger.error(`Invalid tool arguments for ${name}:`, errorInfo);
135
+ const elapsedMs = logger.endTimer(operationId);
136
+ logger.logOperationResult(false, 'call_tool', elapsedMs, errorInfo);
137
+ throw new ObsidianError(`Invalid tool arguments: ${validationResult.errors.join(', ')}`, McpErrorCode.BAD_REQUEST);
65
138
  }
66
139
  // Log the tool execution
67
- logger.info(`Executing tool: ${name}`);
140
+ logger.info(`Executing tool: ${name}`, {
141
+ toolName: name,
142
+ args: maskSensitiveData(args)
143
+ });
68
144
  // Race between tool execution and timeout
69
145
  const content = await Promise.race([
70
146
  handler.runTool(args),
71
147
  timeoutPromise
72
148
  ]);
73
- logger.debug(`Tool execution completed successfully: ${name}`);
149
+ // Log successful execution
150
+ const elapsedMs = logger.endTimer(operationId);
151
+ logger.logOperationResult(true, 'call_tool', elapsedMs, {
152
+ toolName: name,
153
+ contentLength: content.reduce((sum, item) => {
154
+ return sum + (item.type === 'text' ? item.text.length : 0);
155
+ }, 0)
156
+ });
74
157
  return { content };
75
158
  }
76
159
  catch (error) {
160
+ // Handle ObsidianError
77
161
  if (error instanceof ObsidianError) {
78
162
  // Check if the operation actually succeeded despite the error
79
- if (error.errorCode === 20400) { // 20400 = Success with no content
163
+ if (error.errorCode === McpErrorCode.SUCCESS_NO_CONTENT) {
164
+ const elapsedMs = logger.endTimer(operationId);
165
+ logger.logOperationResult(true, 'call_tool', elapsedMs, {
166
+ toolName: name,
167
+ status: 'success_no_content'
168
+ });
80
169
  return {
81
170
  content: [{
82
171
  type: "text",
@@ -84,21 +173,31 @@ export function setupToolCallingHandler(server, toolHandlers) {
84
173
  }]
85
174
  };
86
175
  }
176
+ // Log failure for other ObsidianErrors
177
+ const errorInfo = {
178
+ toolName: name,
179
+ errorMessage: error.message,
180
+ errorCode: error.errorCode,
181
+ errorCategory: ErrorCategoryType.CATEGORY_BUSINESS_LOGIC,
182
+ details: error.details ? JSON.stringify(error.details) : undefined
183
+ };
184
+ const elapsedMs = logger.endTimer(operationId);
185
+ logger.logOperationResult(false, 'call_tool', elapsedMs, errorInfo);
87
186
  throw error;
88
187
  }
89
- // Enhanced error logging
90
- logger.error("Tool execution error:", {
91
- name: error instanceof Error ? error.name : 'Unknown',
92
- message: error instanceof Error ? error.message : String(error),
93
- stack: error instanceof Error ? error.stack : undefined,
94
- toolName: name
95
- });
96
- if (error instanceof Error) {
97
- throw new ObsidianError(`Tool '${name}' execution failed: ${error.message}`, 50000, // 50000 = Internal server error
98
- { originalError: error.stack });
99
- }
100
- throw new ObsidianError("Tool execution failed with unknown error", 50000, // 50000 = Internal server error
101
- { error });
188
+ // Enhanced error logging for other errors
189
+ const errorMessage = error instanceof Error ? error.message : String(error);
190
+ const errorStack = error instanceof Error ? error.stack : undefined;
191
+ const errorInfo = {
192
+ toolName: name,
193
+ errorMessage,
194
+ errorStack,
195
+ errorCategory: ErrorCategoryType.CATEGORY_SYSTEM
196
+ };
197
+ logger.error("Tool execution error:", errorInfo);
198
+ const elapsedMs = logger.endTimer(operationId);
199
+ logger.logOperationResult(false, 'call_tool', elapsedMs, errorInfo);
200
+ throw new ObsidianError(`Tool '${name}' execution failed: ${errorMessage}`, McpErrorCode.INTERNAL_SERVER_ERROR, { originalError: errorMessage, stack: errorStack });
102
201
  }
103
202
  });
104
203
  }
@@ -110,9 +209,24 @@ export function setupToolCallingHandler(server, toolHandlers) {
110
209
  export function setupResourceListingHandler(server, resources) {
111
210
  server.setRequestHandler(ListResourcesRequestSchema, async () => {
112
211
  logger.debug('Handling ListResourcesRequest');
113
- return {
114
- resources: Object.values(resources).map(resource => resource.getResourceDescription())
115
- };
212
+ // Start performance timing
213
+ logger.startTimer('list_resources');
214
+ try {
215
+ const resourceList = Object.values(resources).map(resource => resource.getResourceDescription());
216
+ // Log success with timing information
217
+ const elapsedMs = logger.endTimer('list_resources', 'Listed resources');
218
+ logger.logOperationResult(true, 'list_resources', elapsedMs, {
219
+ resourceCount: resourceList.length
220
+ });
221
+ return { resources: resourceList };
222
+ }
223
+ catch (error) {
224
+ // Log failure with timing information
225
+ const elapsedMs = logger.endTimer('list_resources', 'Failed to list resources');
226
+ logger.logOperationResult(false, 'list_resources', elapsedMs);
227
+ logger.error('Failed to list available resources', error instanceof Error ? error : undefined);
228
+ throw error;
229
+ }
116
230
  });
117
231
  }
118
232
  /**
@@ -123,16 +237,51 @@ export function setupResourceListingHandler(server, resources) {
123
237
  export function setupResourceReadingHandler(server, resources) {
124
238
  server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
125
239
  const uri = request.params.uri;
126
- logger.debug(`Handling ReadResourceRequest for URI: ${uri}`);
240
+ const operationId = `read_resource_${Date.now()}`;
241
+ logger.debug(`Handling ReadResourceRequest for URI: ${uri}`, {
242
+ resourceUri: uri,
243
+ operationId
244
+ });
245
+ // Start performance timing
246
+ logger.startTimer(operationId);
127
247
  const resource = resources[uri];
128
- if (resource) {
248
+ if (!resource) {
249
+ const errorInfo = {
250
+ resourceUri: uri,
251
+ errorCode: McpErrorCode.NOT_FOUND,
252
+ errorCategory: ErrorCategoryType.CATEGORY_DATA_ACCESS
253
+ };
254
+ logger.error(`Resource not found: ${uri}`, errorInfo);
255
+ const elapsedMs = logger.endTimer(operationId);
256
+ logger.logOperationResult(false, 'read_resource', elapsedMs, errorInfo);
257
+ throw new ObsidianError(`Resource not found: ${uri}`, McpErrorCode.NOT_FOUND);
258
+ }
259
+ try {
129
260
  logger.debug(`Found resource for URI: ${uri}`);
130
- return {
131
- contents: await resource.getContent()
261
+ const contents = await resource.getContent();
262
+ // Log success with timing information
263
+ const elapsedMs = logger.endTimer(operationId);
264
+ logger.logOperationResult(true, 'read_resource', elapsedMs, {
265
+ resourceUri: uri,
266
+ contentItems: contents.length
267
+ });
268
+ return { contents };
269
+ }
270
+ catch (error) {
271
+ // Log failure with timing information
272
+ const errorMessage = error instanceof Error ? error.message : String(error);
273
+ const errorStack = error instanceof Error ? error.stack : undefined;
274
+ const errorInfo = {
275
+ resourceUri: uri,
276
+ errorMessage,
277
+ errorStack,
278
+ errorCategory: ErrorCategoryType.CATEGORY_DATA_ACCESS
132
279
  };
280
+ logger.error(`Error reading resource: ${uri}`, errorInfo);
281
+ const elapsedMs = logger.endTimer(operationId);
282
+ logger.logOperationResult(false, 'read_resource', elapsedMs, errorInfo);
283
+ throw new ObsidianError(`Failed to read resource: ${errorMessage}`, McpErrorCode.INTERNAL_SERVER_ERROR, { originalError: errorMessage, stack: errorStack });
133
284
  }
134
- logger.error(`Resource not found: ${uri}`);
135
- throw new ObsidianError(`Resource not found: ${uri}`, 40400); // 40400 = Not found
136
285
  });
137
286
  }
138
287
  //# sourceMappingURL=handlers.js.map