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/LICENSE CHANGED
@@ -1,7 +1,8 @@
1
1
 
2
2
  Apache License
3
3
  Version 2.0, January 2004
4
- http://www.apache.org/licenses/
4
+ Copyright 2025 Casey Hand @cyanheads
5
+ http://www.apache.org/licenses/
5
6
 
6
7
  TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7
8
 
package/README.md CHANGED
@@ -1,10 +1,10 @@
1
1
  # Obsidian MCP Server
2
2
 
3
- [![TypeScript](https://img.shields.io/badge/TypeScript-5.3-blue.svg)](https://www.typescriptlang.org/)
4
- [![Model Context Protocol](https://img.shields.io/badge/MCP-1.4.0-green.svg)](https://modelcontextprotocol.io/)
5
- [![Version](https://img.shields.io/badge/Version-1.2.3-blue.svg)]()
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.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,28 +16,36 @@ 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
22
+ - Error handling and graceful failure
21
23
 
22
24
  ### Search System
25
+
23
26
  - Full-text search with configurable context
24
27
  - Advanced JsonLogic queries for files, tags, and metadata
25
28
  - Support for glob patterns and frontmatter fields
26
29
 
27
30
  ### Property Management
31
+
28
32
  - YAML frontmatter parsing and intelligent merging
29
33
  - Automatic timestamps (created by Obsidian, modified by server)
30
34
  - Custom field support
31
35
 
32
36
  ### Security & Performance
37
+
33
38
  - API key auth with rate limiting and SSL options
34
39
  - Resource monitoring and health checks
40
+ - Graceful shutdown handling
35
41
 
36
42
  ## Installation
37
43
 
38
- 1. Install Node.js (LTS recommended)
39
- 2. Enable Local REST API plugin in Obsidian
40
- 3. Clone and build:
44
+ Note: Requires Node.js
45
+
46
+ 1. Enable Local REST API plugin in Obsidian
47
+ 2. Clone and build:
48
+
41
49
  ```bash
42
50
  git clone git@github.com:cyanheads/obsidian-mcp-server.git
43
51
  cd obsidian-mcp-server
@@ -46,6 +54,7 @@ npm run build
46
54
  ```
47
55
 
48
56
  Or install from npm:
57
+
49
58
  ```bash
50
59
  npm install obsidian-mcp-server
51
60
  ```
@@ -81,86 +90,70 @@ Add to your MCP client settings (e.g., `claude_desktop_config.json` or `cline_mc
81
90
  Environment Variables:
82
91
 
83
92
  Required:
93
+
84
94
  - `OBSIDIAN_API_KEY`: Your API key from Obsidian's Local REST API plugin settings
85
95
 
86
96
  Connection Settings:
87
- - `VERIFY_SSL`: Enable SSL certificate verification (default: false in development)
97
+
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.
88
99
  - `OBSIDIAN_PROTOCOL`: Protocol to use (default: "https")
89
100
  - `OBSIDIAN_HOST`: Host address (default: "127.0.0.1")
90
101
  - `OBSIDIAN_PORT`: Port number (default: 27124)
91
102
 
92
103
  Request Limits:
104
+
93
105
  - `REQUEST_TIMEOUT`: Request timeout in milliseconds (default: 5000)
94
106
  - `MAX_CONTENT_LENGTH`: Maximum response content length in bytes (default: 52428800 [50MB])
95
107
  - `MAX_BODY_LENGTH`: Maximum request body length in bytes (default: 52428800 [50MB])
96
108
 
97
109
  Rate Limiting:
110
+
98
111
  - `RATE_LIMIT_WINDOW_MS`: Rate limit window in milliseconds (default: 900000 [15 minutes])
99
112
  - `RATE_LIMIT_MAX_REQUESTS`: Maximum requests per window (default: 200)
100
113
 
101
114
  Tool Execution:
102
- - `TOOL_TIMEOUT_MS`: Tool execution timeout in milliseconds (default: 60000 [1 minute])
103
115
 
104
- SSL Certificate Setup:
105
-
106
- For Windows Users:
107
- 1. Development Setup (Not Recommended for Production):
108
- - Set `VERIFY_SSL` to "false"
109
- - Set `OBSIDIAN_PROTOCOL` to "http"
110
- - Enable "Non-encrypted (HTTP) Server" in Obsidian's Local REST API settings
116
+ - `TOOL_TIMEOUT_MS`: Tool execution timeout in milliseconds (default: 60000 [1 minute])
111
117
 
112
- 2. Production Setup (Recommended):
113
- - Set `VERIFY_SSL` to "true"
114
- - Get the certificate from Obsidian Settings > Local REST API > 'How to Access'
115
- - Open Windows Certificate Manager (certmgr.msc)
116
- - Navigate to "Trusted Root Certification Authorities" > "Certificates"
117
- - Right-click > "All Tasks" > "Import" and select the certificate file
118
+ ## Project Structure
118
119
 
119
- For Other Systems:
120
- - macOS: Add certificate to Keychain Access
121
- - Linux: Add to ca-certificates
120
+ The project follows a modular architecture with clear separation of concerns:
122
121
 
123
- Additional configuration options:
124
- ```typescript
125
- interface ObsidianConfig {
126
- apiKey: string; // Required: API key for authentication
127
- verifySSL?: boolean; // Optional: Enable SSL verification
128
- timeout?: number; // Optional: Request timeout in ms
129
- maxContentLength?: number;// Optional: Max response content length
130
- maxBodyLength?: number; // Optional: Max request body length
131
- }
132
-
133
- interface RateLimitConfig {
134
- windowMs: number; // Time window for rate limiting
135
- maxRequests: number; // Max requests per window
136
- }
137
122
  ```
138
-
139
- Error Handling:
140
- - All errors include a 5-digit error code
141
- - HTTP status codes are automatically converted (e.g., 404 -> 40400)
142
- - Default server error code: 50000
143
- - Detailed error messages include original error stack traces in development
123
+ src/
124
+ ├── index.ts # Main entry point
125
+ ├── mcp/ # MCP server implementation
126
+ ├── obsidian/ # Obsidian API client and types
127
+ ├── resources/ # MCP resource implementations
128
+ ├── tools/ # MCP tool implementations
129
+ │ ├── files/ # File operations tools
130
+ │ ├── search/ # Search tools
131
+ │ └── properties/ # Property management tools
132
+ └── utils/ # Shared utilities
133
+ ```
144
134
 
145
135
  ## Tools
146
136
 
147
137
  ### File Management
138
+
148
139
  ```typescript
149
140
  // List vault contents
150
- obsidian_list_files_in_vault: {}
141
+ obsidian_list_files_in_vault: {
142
+ }
151
143
 
152
144
  // List directory contents
153
145
  obsidian_list_files_in_dir: {
154
- dirpath: string // Path relative to vault root
146
+ dirpath: string; // Path relative to vault root
155
147
  }
156
148
 
157
149
  // Get file contents
158
150
  obsidian_get_file_contents: {
159
- filepath: string // Path relative to vault root
151
+ filepath: string; // Path relative to vault root
160
152
  }
161
153
  ```
162
154
 
163
155
  ### Search Operations
156
+
164
157
  ```typescript
165
158
  // Text search with context
166
159
  obsidian_find_in_file: {
@@ -184,9 +177,15 @@ obsidian_complex_search: {
184
177
  // {"in": ["#mytag", {"var": "frontmatter.tags"}]}
185
178
  // ]}
186
179
  }
180
+
181
+ // Get all tags in vault or directory
182
+ obsidian_get_tags: {
183
+ path?: string // Optional: limit to specific directory
184
+ }
187
185
  ```
188
186
 
189
187
  ### Content Modification
188
+
190
189
  ```typescript
191
190
  // Append to file
192
191
  obsidian_append_content: {
@@ -201,41 +200,9 @@ obsidian_patch_content: {
201
200
  }
202
201
  ```
203
202
 
204
- ### Command Management
205
- ```typescript
206
- // List available commands
207
- obsidian_list_commands: {}
208
-
209
- // Execute a command
210
- obsidian_execute_command: {
211
- commandId: string // Command ID to execute
212
- }
213
- ```
214
-
215
- ### File Navigation
216
- ```typescript
217
- // Open a file in Obsidian
218
- obsidian_open_file: {
219
- filepath: string, // Path relative to vault root
220
- newLeaf?: boolean // Open in new leaf (default: false)
221
- }
222
-
223
- // Get active file content
224
- obsidian_get_active_file: {}
225
-
226
- // Get periodic note content
227
- obsidian_get_periodic_note: {
228
- period: "daily" | "weekly" | "monthly" | "quarterly" | "yearly"
229
- }
230
- ```
231
-
232
203
  ### Property Management
233
- ```typescript
234
- // Get all tags in vault or directory
235
- obsidian_get_tags: {
236
- path?: string // Optional: limit to specific directory
237
- }
238
204
 
205
+ ```typescript
239
206
  // Get note properties
240
207
  obsidian_get_properties: {
241
208
  filepath: string // Path relative to vault root
@@ -248,8 +215,8 @@ obsidian_update_properties: {
248
215
  title?: string,
249
216
  author?: string,
250
217
  // Note: created/modified timestamps are managed automatically
251
- type?: Array<"concept" | "architecture" | "specification" |
252
- "protocol" | "api" | "research" | "implementation" |
218
+ type?: Array<"concept" | "architecture" | "specification" |
219
+ "protocol" | "api" | "research" | "implementation" |
253
220
  "guide" | "reference">,
254
221
  tags?: string[], // Must start with #
255
222
  status?: Array<"draft" | "in-progress" | "review" | "complete">,
@@ -268,24 +235,36 @@ obsidian_update_properties: {
268
235
  ## Best Practices
269
236
 
270
237
  ### File Operations
238
+
271
239
  - Use atomic operations with validation
272
240
  - Handle errors and monitor performance
273
241
 
274
242
  ### Search Implementation
243
+
275
244
  - Use appropriate search tool for the task:
276
245
  - obsidian_find_in_file for text search
277
246
  - obsidian_complex_search for metadata/tag filtering
278
247
  - Keep context size reasonable (default: 10 chars)
279
248
 
280
249
  ### Property Management
250
+
281
251
  - Use appropriate types and validate updates
282
252
  - Handle arrays and custom fields properly
283
253
  - Never set timestamps (managed automatically)
284
254
 
285
255
  ### Error Prevention
256
+
286
257
  - Validate inputs and handle errors gracefully
287
258
  - Monitor patterns and respect rate limits
288
259
 
260
+ ## Resources
261
+
262
+ The MCP server exposes the following resources:
263
+
264
+ ```
265
+ obsidian://tags # List of all tags used across the vault
266
+ ```
267
+
289
268
  ## Contributing
290
269
 
291
270
  1. Fork the repository
@@ -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