obsidian-mcp-server 1.4.1 → 1.5.1

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 (38) hide show
  1. package/README.md +29 -9
  2. package/build/mcp/handlers.js +211 -41
  3. package/build/mcp/server.js +180 -71
  4. package/build/mcp/types.js +42 -0
  5. package/build/resources/tags.js +111 -18
  6. package/build/tools/base.js +16 -6
  7. package/build/tools/properties/manager.js +24 -17
  8. package/build/tools/properties/tools.js +51 -4
  9. package/build/tools/search/complex.js +55 -4
  10. package/build/utils/idGenerator.js +21 -0
  11. package/build/utils/logging.js +348 -50
  12. package/build/utils/tokenization.js +24 -11
  13. package/build/utils/validation.js +7 -3
  14. package/debug.js +99 -0
  15. package/package.json +15 -13
  16. package/repomix.config.json +20 -0
  17. package/src/mcp/handlers.ts +281 -51
  18. package/src/mcp/server.ts +206 -80
  19. package/src/mcp/types.ts +69 -1
  20. package/src/resources/tags.ts +122 -24
  21. package/src/tools/base.ts +20 -9
  22. package/src/tools/properties/manager.ts +31 -22
  23. package/src/tools/properties/tools.ts +59 -4
  24. package/src/tools/search/complex.ts +60 -4
  25. package/src/utils/idGenerator.ts +22 -0
  26. package/src/utils/logging.ts +466 -54
  27. package/src/utils/tokenization.ts +26 -12
  28. package/src/utils/validation.ts +14 -4
  29. package/examples/README.md +0 -48
  30. package/examples/append-content.md +0 -63
  31. package/examples/complex-search.md +0 -117
  32. package/examples/find-in-file.md +0 -94
  33. package/examples/get-file-contents.md +0 -72
  34. package/examples/get-properties.md +0 -89
  35. package/examples/list-files-in-dir.md +0 -55
  36. package/examples/list-files-in-vault.md +0 -53
  37. package/examples/patch-content.md +0 -60
  38. package/examples/update-properties.md +0 -126
package/README.md CHANGED
@@ -1,10 +1,10 @@
1
1
  # Obsidian MCP Server
2
2
 
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)]()
3
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.8.3-blue.svg)](https://www.typescriptlang.org/)
4
+ [![Model Context Protocol](https://img.shields.io/badge/MCP-1.8.0-green.svg)](https://modelcontextprotocol.io/)
5
+ [![Version](https://img.shields.io/badge/Version-1.5.1-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,33 @@ 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
+ const errorMessage = error instanceof Error ? error.message : String(error);
61
+ logger.error('Failed to list available tools', {
62
+ error: errorMessage,
63
+ stack: error instanceof Error ? error.stack : undefined,
64
+ errorCategory: ErrorCategoryType.CATEGORY_SYSTEM
65
+ });
66
+ // Wrap in ObsidianError
67
+ throw new ObsidianError(`Failed to list tools: ${errorMessage}`, McpErrorCode.INTERNAL_SERVER_ERROR, { originalError: errorMessage });
23
68
  }
24
- return { tools };
25
69
  });
26
70
  }
27
71
  /**
@@ -32,26 +76,55 @@ export function setupToolListingHandler(server, toolHandlers) {
32
76
  export function setupToolCallingHandler(server, toolHandlers) {
33
77
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
34
78
  const { name, arguments: args } = request.params;
35
- logger.debug(`Handling CallToolRequest for tool: ${name}`);
79
+ const operationId = `call_tool_${name}_${Date.now()}`;
80
+ logger.debug(`Handling CallToolRequest for tool: ${name}`, {
81
+ toolName: name,
82
+ operationId
83
+ });
84
+ // Start performance timing
85
+ logger.startTimer(operationId);
86
+ // Handle unknown tool
36
87
  const handler = toolHandlers.get(name);
37
88
  if (!handler) {
38
- logger.error(`Unknown tool requested: ${name}`);
39
- throw new ObsidianError(`Unknown tool: ${name}`, 40400); // 40400 = Not found
89
+ const errorInfo = {
90
+ toolName: name,
91
+ errorCode: McpErrorCode.NOT_FOUND,
92
+ errorCategory: ErrorCategoryType.CATEGORY_VALIDATION
93
+ };
94
+ logger.error(`Unknown tool requested: ${name}`, errorInfo);
95
+ const elapsedMs = logger.endTimer(operationId);
96
+ logger.logOperationResult(false, 'call_tool', elapsedMs, errorInfo);
97
+ throw new ObsidianError(`Unknown tool: ${name}`, McpErrorCode.NOT_FOUND);
40
98
  }
41
99
  // Check rate limit
42
100
  try {
43
101
  rateLimiter.enforceRateLimit(name);
44
102
  }
45
103
  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;
104
+ const errorInfo = {
105
+ toolName: name,
106
+ errorCode: McpErrorCode.RATE_LIMIT_EXCEEDED,
107
+ errorCategory: ErrorCategoryType.CATEGORY_SYSTEM
108
+ };
109
+ logger.warn(`Rate limit exceeded for tool: ${name}`, errorInfo);
110
+ const elapsedMs = logger.endTimer(operationId);
111
+ logger.logOperationResult(false, 'call_tool', elapsedMs, errorInfo);
112
+ throw new ObsidianError(`Rate limit exceeded for tool: ${name}`, McpErrorCode.RATE_LIMIT_EXCEEDED);
49
113
  }
50
114
  // Add timeout handling
51
115
  const timeoutMs = DEFAULT_TIMEOUT_CONFIG.toolExecutionMs;
116
+ let timeoutId = null; // Variable to hold the timeout ID
52
117
  const timeoutPromise = new Promise((_, reject) => {
53
- setTimeout(() => {
54
- reject(new ObsidianError(`Tool execution timed out after ${timeoutMs}ms`, 40800)); // 40800 = Request timeout
118
+ timeoutId = setTimeout(() => {
119
+ timeoutId = null; // Clear the ID reference once the timeout executes
120
+ const errorInfo = {
121
+ toolName: name,
122
+ timeoutMs,
123
+ errorCode: McpErrorCode.TIMEOUT,
124
+ errorCategory: ErrorCategoryType.CATEGORY_SYSTEM
125
+ };
126
+ logger.error(`Tool execution timed out after ${timeoutMs}ms`, errorInfo);
127
+ reject(new ObsidianError(`Tool execution timed out after ${timeoutMs}ms`, McpErrorCode.TIMEOUT));
55
128
  }, timeoutMs);
56
129
  });
57
130
  try {
@@ -59,24 +132,48 @@ export function setupToolCallingHandler(server, toolHandlers) {
59
132
  const toolDescription = handler.getToolDescription();
60
133
  const validationResult = validateToolArguments(args, toolDescription.inputSchema);
61
134
  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
- );
135
+ const errorInfo = {
136
+ toolName: name,
137
+ validationErrors: validationResult.errors,
138
+ providedArgs: maskSensitiveData(args),
139
+ errorCode: McpErrorCode.BAD_REQUEST,
140
+ errorCategory: ErrorCategoryType.CATEGORY_VALIDATION
141
+ };
142
+ logger.error(`Invalid tool arguments for ${name}:`, errorInfo);
143
+ const elapsedMs = logger.endTimer(operationId);
144
+ logger.logOperationResult(false, 'call_tool', elapsedMs, errorInfo);
145
+ throw new ObsidianError(`Invalid tool arguments: ${validationResult.errors.join(', ')}`, McpErrorCode.BAD_REQUEST);
65
146
  }
66
147
  // Log the tool execution
67
- logger.info(`Executing tool: ${name}`);
148
+ logger.info(`Executing tool: ${name}`, {
149
+ toolName: name,
150
+ args: maskSensitiveData(args)
151
+ });
68
152
  // Race between tool execution and timeout
69
153
  const content = await Promise.race([
70
154
  handler.runTool(args),
71
155
  timeoutPromise
72
156
  ]);
73
- logger.debug(`Tool execution completed successfully: ${name}`);
157
+ // Log successful execution
158
+ const elapsedMs = logger.endTimer(operationId);
159
+ logger.logOperationResult(true, 'call_tool', elapsedMs, {
160
+ toolName: name,
161
+ contentLength: content.reduce((sum, item) => {
162
+ return sum + (item.type === 'text' ? item.text.length : 0);
163
+ }, 0)
164
+ });
74
165
  return { content };
75
166
  }
76
167
  catch (error) {
168
+ // Handle ObsidianError
77
169
  if (error instanceof ObsidianError) {
78
170
  // Check if the operation actually succeeded despite the error
79
- if (error.errorCode === 20400) { // 20400 = Success with no content
171
+ if (error.errorCode === McpErrorCode.SUCCESS_NO_CONTENT) {
172
+ const elapsedMs = logger.endTimer(operationId);
173
+ logger.logOperationResult(true, 'call_tool', elapsedMs, {
174
+ toolName: name,
175
+ status: 'success_no_content'
176
+ });
80
177
  return {
81
178
  content: [{
82
179
  type: "text",
@@ -84,21 +181,38 @@ export function setupToolCallingHandler(server, toolHandlers) {
84
181
  }]
85
182
  };
86
183
  }
184
+ // Log failure for other ObsidianErrors
185
+ const errorInfo = {
186
+ toolName: name,
187
+ errorMessage: error.message,
188
+ errorCode: error.errorCode,
189
+ errorCategory: ErrorCategoryType.CATEGORY_BUSINESS_LOGIC,
190
+ details: error.details ? JSON.stringify(error.details) : undefined
191
+ };
192
+ const elapsedMs = logger.endTimer(operationId);
193
+ logger.logOperationResult(false, 'call_tool', elapsedMs, errorInfo);
87
194
  throw error;
88
195
  }
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 });
196
+ // Enhanced error logging for other errors
197
+ const errorMessage = error instanceof Error ? error.message : String(error);
198
+ const errorStack = error instanceof Error ? error.stack : undefined;
199
+ const errorInfo = {
200
+ toolName: name,
201
+ errorMessage,
202
+ errorStack,
203
+ errorCategory: ErrorCategoryType.CATEGORY_SYSTEM
204
+ };
205
+ logger.error("Tool execution error:", errorInfo);
206
+ const elapsedMs = logger.endTimer(operationId);
207
+ logger.logOperationResult(false, 'call_tool', elapsedMs, errorInfo);
208
+ throw new ObsidianError(`Tool '${name}' execution failed: ${errorMessage}`, McpErrorCode.INTERNAL_SERVER_ERROR, { originalError: errorMessage, stack: errorStack });
209
+ }
210
+ finally {
211
+ // Ensure the timeout is cleared regardless of the outcome
212
+ if (timeoutId) {
213
+ clearTimeout(timeoutId);
214
+ logger.debug(`Cleared timeout for tool: ${name}`, { operationId });
99
215
  }
100
- throw new ObsidianError("Tool execution failed with unknown error", 50000, // 50000 = Internal server error
101
- { error });
102
216
  }
103
217
  });
104
218
  }
@@ -110,9 +224,30 @@ export function setupToolCallingHandler(server, toolHandlers) {
110
224
  export function setupResourceListingHandler(server, resources) {
111
225
  server.setRequestHandler(ListResourcesRequestSchema, async () => {
112
226
  logger.debug('Handling ListResourcesRequest');
113
- return {
114
- resources: Object.values(resources).map(resource => resource.getResourceDescription())
115
- };
227
+ // Start performance timing
228
+ logger.startTimer('list_resources');
229
+ try {
230
+ const resourceList = Object.values(resources).map(resource => resource.getResourceDescription());
231
+ // Log success with timing information
232
+ const elapsedMs = logger.endTimer('list_resources', 'Listed resources');
233
+ logger.logOperationResult(true, 'list_resources', elapsedMs, {
234
+ resourceCount: resourceList.length
235
+ });
236
+ return { resources: resourceList };
237
+ }
238
+ catch (error) {
239
+ // Log failure with timing information
240
+ const elapsedMs = logger.endTimer('list_resources', 'Failed to list resources');
241
+ logger.logOperationResult(false, 'list_resources', elapsedMs);
242
+ const errorMessage = error instanceof Error ? error.message : String(error);
243
+ logger.error('Failed to list available resources', {
244
+ error: errorMessage,
245
+ stack: error instanceof Error ? error.stack : undefined,
246
+ errorCategory: ErrorCategoryType.CATEGORY_SYSTEM
247
+ });
248
+ // Wrap in ObsidianError
249
+ throw new ObsidianError(`Failed to list resources: ${errorMessage}`, McpErrorCode.INTERNAL_SERVER_ERROR, { originalError: errorMessage });
250
+ }
116
251
  });
117
252
  }
118
253
  /**
@@ -123,16 +258,51 @@ export function setupResourceListingHandler(server, resources) {
123
258
  export function setupResourceReadingHandler(server, resources) {
124
259
  server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
125
260
  const uri = request.params.uri;
126
- logger.debug(`Handling ReadResourceRequest for URI: ${uri}`);
261
+ const operationId = `read_resource_${Date.now()}`;
262
+ logger.debug(`Handling ReadResourceRequest for URI: ${uri}`, {
263
+ resourceUri: uri,
264
+ operationId
265
+ });
266
+ // Start performance timing
267
+ logger.startTimer(operationId);
127
268
  const resource = resources[uri];
128
- if (resource) {
269
+ if (!resource) {
270
+ const errorInfo = {
271
+ resourceUri: uri,
272
+ errorCode: McpErrorCode.NOT_FOUND,
273
+ errorCategory: ErrorCategoryType.CATEGORY_DATA_ACCESS
274
+ };
275
+ logger.error(`Resource not found: ${uri}`, errorInfo);
276
+ const elapsedMs = logger.endTimer(operationId);
277
+ logger.logOperationResult(false, 'read_resource', elapsedMs, errorInfo);
278
+ throw new ObsidianError(`Resource not found: ${uri}`, McpErrorCode.NOT_FOUND);
279
+ }
280
+ try {
129
281
  logger.debug(`Found resource for URI: ${uri}`);
130
- return {
131
- contents: await resource.getContent()
282
+ const contents = await resource.getContent();
283
+ // Log success with timing information
284
+ const elapsedMs = logger.endTimer(operationId);
285
+ logger.logOperationResult(true, 'read_resource', elapsedMs, {
286
+ resourceUri: uri,
287
+ contentItems: contents.length
288
+ });
289
+ return { contents };
290
+ }
291
+ catch (error) {
292
+ // Log failure with timing information
293
+ const errorMessage = error instanceof Error ? error.message : String(error);
294
+ const errorStack = error instanceof Error ? error.stack : undefined;
295
+ const errorInfo = {
296
+ resourceUri: uri,
297
+ errorMessage,
298
+ errorStack,
299
+ errorCategory: ErrorCategoryType.CATEGORY_DATA_ACCESS
132
300
  };
301
+ logger.error(`Error reading resource: ${uri}`, errorInfo);
302
+ const elapsedMs = logger.endTimer(operationId);
303
+ logger.logOperationResult(false, 'read_resource', elapsedMs, errorInfo);
304
+ throw new ObsidianError(`Failed to read resource: ${errorMessage}`, McpErrorCode.INTERNAL_SERVER_ERROR, { originalError: errorMessage, stack: errorStack });
133
305
  }
134
- logger.error(`Resource not found: ${uri}`);
135
- throw new ObsidianError(`Resource not found: ${uri}`, 40400); // 40400 = Not found
136
306
  });
137
307
  }
138
308
  //# sourceMappingURL=handlers.js.map