obsidian-mcp-server 1.4.0 → 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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "obsidian-mcp-server",
3
- "version": "1.4.0",
4
- "description": "Model Context Protocol server for Obsidian integration with token-aware response handling",
3
+ "version": "1.5.0",
4
+ "description": "Model Context Protocol (MCP) server designed for LLMs to interact with Obsidian vaults. Provides secure, token-aware tools for seamless knowledge base management through a standardized interface.",
5
5
  "main": "build/index.js",
6
6
  "type": "module",
7
7
  "engines": {
@@ -19,22 +19,21 @@
19
19
  "format": "prettier --write \"src/**/*.ts\""
20
20
  },
21
21
  "dependencies": {
22
- "@modelcontextprotocol/sdk": "^1.4.1",
23
- "axios": "^1.7.9",
22
+ "@modelcontextprotocol/sdk": "^1.7.0",
23
+ "axios": "^1.8.3",
24
24
  "dotenv": "^16.4.7",
25
- "tiktoken": "^1.0.18",
25
+ "tiktoken": "^1.0.20",
26
26
  "yaml": "^2.7.0",
27
- "zod": "^3.24.1"
28
- },
29
- "devDependencies": {
30
- "@types/node": "^22.10.10",
31
- "@typescript-eslint/eslint-plugin": "^8.21.0",
32
- "@typescript-eslint/parser": "^8.21.0",
33
- "eslint": "^9.19.0",
34
- "eslint-config-prettier": "^10.0.1",
27
+ "zod": "^3.24.2",
28
+ "winston": "^3.17.0",
29
+ "@types/node": "^22.13.10",
30
+ "@typescript-eslint/eslint-plugin": "^8.26.1",
31
+ "@typescript-eslint/parser": "^8.26.1",
32
+ "eslint": "^9.22.0",
33
+ "eslint-config-prettier": "^10.1.1",
35
34
  "eslint-plugin-prettier": "^5.2.3",
36
- "prettier": "^3.4.2",
37
- "typescript": "^5.7.3"
35
+ "prettier": "^3.5.3",
36
+ "typescript": "^5.8.2"
38
37
  },
39
38
  "keywords": [
40
39
  "mcp",
@@ -13,13 +13,47 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
13
13
  import { ObsidianError } from "../utils/errors.js";
14
14
  import { validateToolArguments } from "../utils/validation.js";
15
15
  import { rateLimiter } from "../utils/rate-limiting.js";
16
- import { createLogger } from "../utils/logging.js";
17
- import { DEFAULT_TIMEOUT_CONFIG } from "./types.js";
16
+ import {
17
+ createLogger,
18
+ ErrorCategoryType
19
+ } from "../utils/logging.js";
20
+ import {
21
+ DEFAULT_TIMEOUT_CONFIG,
22
+ McpErrorCode,
23
+ createFailureResult,
24
+ createSuccessResult
25
+ } from "./types.js";
18
26
  import { BaseToolHandler } from "../tools/base.js";
19
27
 
20
28
  // Create a logger for request handlers
21
29
  const logger = createLogger('McpHandlers');
22
30
 
31
+ /**
32
+ * Helper function to safely mask sensitive data
33
+ */
34
+ function maskSensitiveData(data: Record<string, unknown> | undefined): Record<string, unknown> {
35
+ if (!data) return {};
36
+
37
+ const sensitiveFields = ['password', 'token', 'secret', 'key', 'auth', 'credential'];
38
+ const result: Record<string, unknown> = {};
39
+
40
+ for (const [key, value] of Object.entries(data)) {
41
+ const isSensitive = sensitiveFields.some(field =>
42
+ key.toLowerCase().includes(field.toLowerCase())
43
+ );
44
+
45
+ if (isSensitive) {
46
+ result[key] = '********';
47
+ } else if (value && typeof value === 'object' && !Array.isArray(value)) {
48
+ result[key] = maskSensitiveData(value as Record<string, unknown>);
49
+ } else {
50
+ result[key] = value;
51
+ }
52
+ }
53
+
54
+ return result;
55
+ }
56
+
23
57
  /**
24
58
  * Set up tool listing handler
25
59
  * @param server The MCP server instance
@@ -31,11 +65,29 @@ export function setupToolListingHandler(
31
65
  ): void {
32
66
  server.setRequestHandler(ListToolsRequestSchema, async () => {
33
67
  logger.debug('Handling ListToolsRequest');
34
- const tools: Tool[] = [];
35
- for (const handler of toolHandlers.values()) {
36
- tools.push(handler.getToolDescription());
68
+ // Start performance timing
69
+ logger.startTimer('list_tools');
70
+
71
+ try {
72
+ const tools: Tool[] = [];
73
+ for (const handler of toolHandlers.values()) {
74
+ tools.push(handler.getToolDescription());
75
+ }
76
+
77
+ // Log success and timing information
78
+ const elapsedMs = logger.endTimer('list_tools', 'Listed tools');
79
+ logger.logOperationResult(true, 'list_tools', elapsedMs, {
80
+ toolCount: tools.length
81
+ });
82
+
83
+ return { tools };
84
+ } catch (error) {
85
+ // Log failure with timing information
86
+ const elapsedMs = logger.endTimer('list_tools', 'Failed to list tools');
87
+ logger.logOperationResult(false, 'list_tools', elapsedMs);
88
+ logger.error('Failed to list available tools', error instanceof Error ? error : undefined);
89
+ throw error;
37
90
  }
38
- return { tools };
39
91
  });
40
92
  }
41
93
 
@@ -50,28 +102,71 @@ export function setupToolCallingHandler(
50
102
  ): void {
51
103
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
52
104
  const { name, arguments: args } = request.params;
53
- logger.debug(`Handling CallToolRequest for tool: ${name}`);
54
-
105
+ const operationId = `call_tool_${name}_${Date.now()}`;
106
+ logger.debug(`Handling CallToolRequest for tool: ${name}`, {
107
+ toolName: name,
108
+ operationId
109
+ });
110
+
111
+ // Start performance timing
112
+ logger.startTimer(operationId);
113
+
114
+ // Handle unknown tool
55
115
  const handler = toolHandlers.get(name);
56
116
  if (!handler) {
57
- logger.error(`Unknown tool requested: ${name}`);
58
- throw new ObsidianError(`Unknown tool: ${name}`, 40400); // 40400 = Not found
117
+ const errorInfo = {
118
+ toolName: name,
119
+ errorCode: McpErrorCode.NOT_FOUND,
120
+ errorCategory: ErrorCategoryType.CATEGORY_VALIDATION
121
+ };
122
+
123
+ logger.error(`Unknown tool requested: ${name}`, errorInfo);
124
+ const elapsedMs = logger.endTimer(operationId);
125
+ logger.logOperationResult(false, 'call_tool', elapsedMs, errorInfo);
126
+
127
+ throw new ObsidianError(
128
+ `Unknown tool: ${name}`,
129
+ McpErrorCode.NOT_FOUND
130
+ );
59
131
  }
60
132
 
61
133
  // Check rate limit
62
134
  try {
63
135
  rateLimiter.enforceRateLimit(name);
64
136
  } catch (error) {
65
- // If rate limit is exceeded, log and rethrow the error
66
- logger.warn(`Rate limit exceeded for tool: ${name}`);
67
- throw error;
137
+ const errorInfo = {
138
+ toolName: name,
139
+ errorCode: McpErrorCode.RATE_LIMIT_EXCEEDED,
140
+ errorCategory: ErrorCategoryType.CATEGORY_SYSTEM
141
+ };
142
+
143
+ logger.warn(`Rate limit exceeded for tool: ${name}`, errorInfo);
144
+ const elapsedMs = logger.endTimer(operationId);
145
+ logger.logOperationResult(false, 'call_tool', elapsedMs, errorInfo);
146
+
147
+ throw new ObsidianError(
148
+ `Rate limit exceeded for tool: ${name}`,
149
+ McpErrorCode.RATE_LIMIT_EXCEEDED
150
+ );
68
151
  }
69
152
 
70
153
  // Add timeout handling
71
154
  const timeoutMs = DEFAULT_TIMEOUT_CONFIG.toolExecutionMs;
72
155
  const timeoutPromise = new Promise<never>((_, reject) => {
73
156
  setTimeout(() => {
74
- reject(new ObsidianError(`Tool execution timed out after ${timeoutMs}ms`, 40800)); // 40800 = Request timeout
157
+ const errorInfo = {
158
+ toolName: name,
159
+ timeoutMs,
160
+ errorCode: McpErrorCode.TIMEOUT,
161
+ errorCategory: ErrorCategoryType.CATEGORY_SYSTEM
162
+ };
163
+
164
+ logger.error(`Tool execution timed out after ${timeoutMs}ms`, errorInfo);
165
+
166
+ reject(new ObsidianError(
167
+ `Tool execution timed out after ${timeoutMs}ms`,
168
+ McpErrorCode.TIMEOUT
169
+ ));
75
170
  }, timeoutMs);
76
171
  });
77
172
 
@@ -80,15 +175,29 @@ export function setupToolCallingHandler(
80
175
  const toolDescription = handler.getToolDescription();
81
176
  const validationResult = validateToolArguments(args, toolDescription.inputSchema);
82
177
  if (!validationResult.valid) {
83
- logger.error(`Invalid tool arguments for ${name}:`, validationResult.errors);
178
+ const errorInfo = {
179
+ toolName: name,
180
+ validationErrors: validationResult.errors,
181
+ providedArgs: maskSensitiveData(args),
182
+ errorCode: McpErrorCode.BAD_REQUEST,
183
+ errorCategory: ErrorCategoryType.CATEGORY_VALIDATION
184
+ };
185
+
186
+ logger.error(`Invalid tool arguments for ${name}:`, errorInfo);
187
+ const elapsedMs = logger.endTimer(operationId);
188
+ logger.logOperationResult(false, 'call_tool', elapsedMs, errorInfo);
189
+
84
190
  throw new ObsidianError(
85
- `Invalid tool arguments: ${validationResult.errors.join(', ')}`,
86
- 40000 // 40000 = Bad request
191
+ `Invalid tool arguments: ${validationResult.errors.join(', ')}`,
192
+ McpErrorCode.BAD_REQUEST
87
193
  );
88
194
  }
89
195
 
90
196
  // Log the tool execution
91
- logger.info(`Executing tool: ${name}`);
197
+ logger.info(`Executing tool: ${name}`, {
198
+ toolName: name,
199
+ args: maskSensitiveData(args)
200
+ });
92
201
 
93
202
  // Race between tool execution and timeout
94
203
  const content = await Promise.race([
@@ -96,13 +205,27 @@ export function setupToolCallingHandler(
96
205
  timeoutPromise
97
206
  ]);
98
207
 
99
- logger.debug(`Tool execution completed successfully: ${name}`);
208
+ // Log successful execution
209
+ const elapsedMs = logger.endTimer(operationId);
210
+ logger.logOperationResult(true, 'call_tool', elapsedMs, {
211
+ toolName: name,
212
+ contentLength: content.reduce((sum, item) => {
213
+ return sum + (item.type === 'text' ? item.text.length : 0);
214
+ }, 0)
215
+ });
100
216
 
101
217
  return { content };
102
218
  } catch (error) {
219
+ // Handle ObsidianError
103
220
  if (error instanceof ObsidianError) {
104
221
  // Check if the operation actually succeeded despite the error
105
- if (error.errorCode === 20400) { // 20400 = Success with no content
222
+ if (error.errorCode === McpErrorCode.SUCCESS_NO_CONTENT) {
223
+ const elapsedMs = logger.endTimer(operationId);
224
+ logger.logOperationResult(true, 'call_tool', elapsedMs, {
225
+ toolName: name,
226
+ status: 'success_no_content'
227
+ });
228
+
106
229
  return {
107
230
  content: [{
108
231
  type: "text",
@@ -110,29 +233,41 @@ export function setupToolCallingHandler(
110
233
  }]
111
234
  };
112
235
  }
236
+
237
+ // Log failure for other ObsidianErrors
238
+ const errorInfo = {
239
+ toolName: name,
240
+ errorMessage: error.message,
241
+ errorCode: error.errorCode,
242
+ errorCategory: ErrorCategoryType.CATEGORY_BUSINESS_LOGIC,
243
+ details: error.details ? JSON.stringify(error.details) : undefined
244
+ };
245
+
246
+ const elapsedMs = logger.endTimer(operationId);
247
+ logger.logOperationResult(false, 'call_tool', elapsedMs, errorInfo);
248
+
113
249
  throw error;
114
250
  }
115
251
 
116
- // Enhanced error logging
117
- logger.error("Tool execution error:", {
118
- name: error instanceof Error ? error.name : 'Unknown',
119
- message: error instanceof Error ? error.message : String(error),
120
- stack: error instanceof Error ? error.stack : undefined,
121
- toolName: name
122
- });
123
-
124
- if (error instanceof Error) {
125
- throw new ObsidianError(
126
- `Tool '${name}' execution failed: ${error.message}`,
127
- 50000, // 50000 = Internal server error
128
- { originalError: error.stack }
129
- );
130
- }
252
+ // Enhanced error logging for other errors
253
+ const errorMessage = error instanceof Error ? error.message : String(error);
254
+ const errorStack = error instanceof Error ? error.stack : undefined;
131
255
 
256
+ const errorInfo = {
257
+ toolName: name,
258
+ errorMessage,
259
+ errorStack,
260
+ errorCategory: ErrorCategoryType.CATEGORY_SYSTEM
261
+ };
262
+
263
+ logger.error("Tool execution error:", errorInfo);
264
+ const elapsedMs = logger.endTimer(operationId);
265
+ logger.logOperationResult(false, 'call_tool', elapsedMs, errorInfo);
266
+
132
267
  throw new ObsidianError(
133
- "Tool execution failed with unknown error",
134
- 50000, // 50000 = Internal server error
135
- { error }
268
+ `Tool '${name}' execution failed: ${errorMessage}`,
269
+ McpErrorCode.INTERNAL_SERVER_ERROR,
270
+ { originalError: errorMessage, stack: errorStack }
136
271
  );
137
272
  }
138
273
  });
@@ -149,10 +284,27 @@ export function setupResourceListingHandler(
149
284
  ): void {
150
285
  server.setRequestHandler(ListResourcesRequestSchema, async () => {
151
286
  logger.debug('Handling ListResourcesRequest');
152
- return {
153
- resources: Object.values(resources).map(resource =>
154
- resource.getResourceDescription())
155
- };
287
+ // Start performance timing
288
+ logger.startTimer('list_resources');
289
+
290
+ try {
291
+ const resourceList = Object.values(resources).map(resource =>
292
+ resource.getResourceDescription());
293
+
294
+ // Log success with timing information
295
+ const elapsedMs = logger.endTimer('list_resources', 'Listed resources');
296
+ logger.logOperationResult(true, 'list_resources', elapsedMs, {
297
+ resourceCount: resourceList.length
298
+ });
299
+
300
+ return { resources: resourceList };
301
+ } catch (error) {
302
+ // Log failure with timing information
303
+ const elapsedMs = logger.endTimer('list_resources', 'Failed to list resources');
304
+ logger.logOperationResult(false, 'list_resources', elapsedMs);
305
+ logger.error('Failed to list available resources', error instanceof Error ? error : undefined);
306
+ throw error;
307
+ }
156
308
  });
157
309
  }
158
310
 
@@ -167,17 +319,67 @@ export function setupResourceReadingHandler(
167
319
  ): void {
168
320
  server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
169
321
  const uri = request.params.uri;
170
- logger.debug(`Handling ReadResourceRequest for URI: ${uri}`);
322
+ const operationId = `read_resource_${Date.now()}`;
323
+
324
+ logger.debug(`Handling ReadResourceRequest for URI: ${uri}`, {
325
+ resourceUri: uri,
326
+ operationId
327
+ });
328
+
329
+ // Start performance timing
330
+ logger.startTimer(operationId);
171
331
 
172
332
  const resource = resources[uri];
173
- if (resource) {
174
- logger.debug(`Found resource for URI: ${uri}`);
175
- return {
176
- contents: await resource.getContent()
333
+ if (!resource) {
334
+ const errorInfo = {
335
+ resourceUri: uri,
336
+ errorCode: McpErrorCode.NOT_FOUND,
337
+ errorCategory: ErrorCategoryType.CATEGORY_DATA_ACCESS
177
338
  };
339
+
340
+ logger.error(`Resource not found: ${uri}`, errorInfo);
341
+ const elapsedMs = logger.endTimer(operationId);
342
+ logger.logOperationResult(false, 'read_resource', elapsedMs, errorInfo);
343
+
344
+ throw new ObsidianError(
345
+ `Resource not found: ${uri}`,
346
+ McpErrorCode.NOT_FOUND
347
+ );
178
348
  }
179
349
 
180
- logger.error(`Resource not found: ${uri}`);
181
- throw new ObsidianError(`Resource not found: ${uri}`, 40400); // 40400 = Not found
350
+ try {
351
+ logger.debug(`Found resource for URI: ${uri}`);
352
+ const contents = await resource.getContent();
353
+
354
+ // Log success with timing information
355
+ const elapsedMs = logger.endTimer(operationId);
356
+ logger.logOperationResult(true, 'read_resource', elapsedMs, {
357
+ resourceUri: uri,
358
+ contentItems: contents.length
359
+ });
360
+
361
+ return { contents };
362
+ } catch (error) {
363
+ // Log failure with timing information
364
+ const errorMessage = error instanceof Error ? error.message : String(error);
365
+ const errorStack = error instanceof Error ? error.stack : undefined;
366
+
367
+ const errorInfo = {
368
+ resourceUri: uri,
369
+ errorMessage,
370
+ errorStack,
371
+ errorCategory: ErrorCategoryType.CATEGORY_DATA_ACCESS
372
+ };
373
+
374
+ logger.error(`Error reading resource: ${uri}`, errorInfo);
375
+ const elapsedMs = logger.endTimer(operationId);
376
+ logger.logOperationResult(false, 'read_resource', elapsedMs, errorInfo);
377
+
378
+ throw new ObsidianError(
379
+ `Failed to read resource: ${errorMessage}`,
380
+ McpErrorCode.INTERNAL_SERVER_ERROR,
381
+ { originalError: errorMessage, stack: errorStack }
382
+ );
383
+ }
182
384
  });
183
385
  }