obsidian-mcp-server 1.5.0 → 1.5.2

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,8 +1,8 @@
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.7.0-green.svg)](https://modelcontextprotocol.io/)
5
- [![Version](https://img.shields.io/badge/Version-1.5.0-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.2-blue.svg)]()
6
6
  [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
7
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)
@@ -57,8 +57,14 @@ export function setupToolListingHandler(server, toolHandlers) {
57
57
  // Log failure with timing information
58
58
  const elapsedMs = logger.endTimer('list_tools', 'Failed to list tools');
59
59
  logger.logOperationResult(false, 'list_tools', elapsedMs);
60
- logger.error('Failed to list available tools', error instanceof Error ? error : undefined);
61
- throw error;
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 });
62
68
  }
63
69
  });
64
70
  }
@@ -107,8 +113,10 @@ export function setupToolCallingHandler(server, toolHandlers) {
107
113
  }
108
114
  // Add timeout handling
109
115
  const timeoutMs = DEFAULT_TIMEOUT_CONFIG.toolExecutionMs;
116
+ let timeoutId = null; // Variable to hold the timeout ID
110
117
  const timeoutPromise = new Promise((_, reject) => {
111
- setTimeout(() => {
118
+ timeoutId = setTimeout(() => {
119
+ timeoutId = null; // Clear the ID reference once the timeout executes
112
120
  const errorInfo = {
113
121
  toolName: name,
114
122
  timeoutMs,
@@ -199,6 +207,13 @@ export function setupToolCallingHandler(server, toolHandlers) {
199
207
  logger.logOperationResult(false, 'call_tool', elapsedMs, errorInfo);
200
208
  throw new ObsidianError(`Tool '${name}' execution failed: ${errorMessage}`, McpErrorCode.INTERNAL_SERVER_ERROR, { originalError: errorMessage, stack: errorStack });
201
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 });
215
+ }
216
+ }
202
217
  });
203
218
  }
204
219
  /**
@@ -224,8 +239,14 @@ export function setupResourceListingHandler(server, resources) {
224
239
  // Log failure with timing information
225
240
  const elapsedMs = logger.endTimer('list_resources', 'Failed to list resources');
226
241
  logger.logOperationResult(false, 'list_resources', elapsedMs);
227
- logger.error('Failed to list available resources', error instanceof Error ? error : undefined);
228
- throw error;
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 });
229
250
  }
230
251
  });
231
252
  }
@@ -4,6 +4,7 @@
4
4
  import { config } from "dotenv";
5
5
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
6
6
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
7
+ import { ObsidianError } from "../utils/errors.js"; // Import ObsidianError
7
8
  import { createLogger, ErrorCategoryType } from "../utils/logging.js";
8
9
  import { rateLimiter } from "../utils/rate-limiting.js";
9
10
  import { createTagResource } from "../resources/index.js";
@@ -29,7 +30,8 @@ export async function initializeServer() {
29
30
  errorCategory: ErrorCategoryType.CATEGORY_AUTHENTICATION,
30
31
  errorCode: McpErrorCode.UNAUTHORIZED
31
32
  });
32
- throw new Error("OBSIDIAN_API_KEY environment variable is required");
33
+ // Use ObsidianError for consistency
34
+ throw new ObsidianError("OBSIDIAN_API_KEY environment variable is required", McpErrorCode.UNAUTHORIZED);
33
35
  }
34
36
  // Initialize Obsidian client with environment configuration
35
37
  logger.info('Initializing Obsidian client', {
@@ -141,20 +143,11 @@ export function setupShutdownHandling(server, cleanupHandlers = []) {
141
143
  logger.startTimer('server_shutdown');
142
144
  logger.info('Shutting down server...', { signal });
143
145
  try {
144
- // Run cleanup handlers
146
+ // Run cleanup handlers sequentially, stopping on error
145
147
  logger.debug('Running cleanup handlers', { handlersCount: cleanupHandlers.length });
146
148
  for (const handler of cleanupHandlers) {
147
- try {
148
- await handler();
149
- }
150
- catch (error) {
151
- const errorMessage = error instanceof Error ? error.message : String(error);
152
- logger.error('Error during cleanup handler execution', {
153
- error: errorMessage,
154
- stack: error instanceof Error ? error.stack : undefined,
155
- errorCategory: ErrorCategoryType.CATEGORY_SYSTEM
156
- });
157
- }
149
+ // No try-catch here; let errors propagate to the main catch block
150
+ await handler();
158
151
  }
159
152
  // Dispose rate limiter
160
153
  logger.debug('Disposing rate limiter');
@@ -1,3 +1,4 @@
1
+ import pLimit from 'p-limit'; // Import p-limit
1
2
  import { PropertyManager } from "../tools/properties/manager.js";
2
3
  import { sep } from "path";
3
4
  import { createLogger, ErrorCategoryType } from "../utils/logging.js";
@@ -33,6 +34,7 @@ export class TagResource {
33
34
  tagCache = new Map();
34
35
  propertyManager;
35
36
  isInitialized = false;
37
+ isUpdating = false; // Flag to prevent concurrent updates
36
38
  lastUpdate = 0;
37
39
  updateInterval = 5000; // 5 seconds
38
40
  constructor(client) {
@@ -64,24 +66,45 @@ export class TagResource {
64
66
  };
65
67
  const results = await this.client.searchJson(query);
66
68
  this.tagCache.clear();
67
- // Process each file
68
- for (const result of results) {
69
- if (!('filename' in result))
70
- continue;
69
+ // Create a limiter with a concurrency of 3 (reduced from 10)
70
+ const limit = pLimit(3);
71
+ // Create promises for processing each file with concurrency limiting
72
+ const processingPromises = results
73
+ .filter(result => 'filename' in result) // Ensure filename exists
74
+ .map((result) => limit(async () => {
75
+ const filename = result.filename;
71
76
  try {
72
- const content = await this.client.getFileContents(result.filename);
77
+ // This call is now rate-limited
78
+ const content = await this.client.getFileContents(filename);
73
79
  // Only extract tags from frontmatter YAML
74
80
  const properties = this.propertyManager.parseProperties(content);
75
- if (properties.tags) {
76
- properties.tags.forEach((tag) => {
77
- this.addTag(tag, result.filename);
81
+ return { filename, tags: properties.tags || [] };
82
+ }
83
+ catch (error) {
84
+ logger.error(`Failed to process file ${filename}:`, errorToObject(error));
85
+ return { filename, error: true }; // Mark as failed
86
+ }
87
+ })); // Close the limiter wrapper
88
+ // Execute promises in parallel and wait for all to settle
89
+ const processedResults = await Promise.allSettled(processingPromises);
90
+ // Populate the cache from settled results
91
+ processedResults.forEach(settledResult => {
92
+ // Check if the promise was fulfilled and didn't encounter a processing error
93
+ if (settledResult.status === 'fulfilled' && !settledResult.value.error) {
94
+ const { filename, tags } = settledResult.value;
95
+ // Ensure tags is an array before iterating
96
+ if (tags && Array.isArray(tags)) {
97
+ tags.forEach((tag) => {
98
+ this.addTag(tag, filename);
78
99
  });
79
100
  }
80
101
  }
81
- catch (error) {
82
- logger.error(`Failed to process file ${result.filename}:`, errorToObject(error));
102
+ else if (settledResult.status === 'rejected') {
103
+ // Log unexpected rejections from the async map function itself
104
+ logger.error(`Unexpected error during file processing setup:`, errorToObject(settledResult.reason));
83
105
  }
84
- }
106
+ // Errors during getFileContents/parseProperties are already logged within the map function
107
+ });
85
108
  this.isInitialized = true;
86
109
  this.lastUpdate = Date.now();
87
110
  const elapsedMs = logger.endTimer('init_tag_cache');
@@ -108,13 +131,47 @@ export class TagResource {
108
131
  this.tagCache.get(tag).add(filepath);
109
132
  }
110
133
  /**
111
- * Update the cache if needed
134
+ * Update the cache if needed, preventing race conditions.
112
135
  */
113
136
  async updateCacheIfNeeded() {
114
137
  const now = Date.now();
115
- if (now - this.lastUpdate > this.updateInterval) {
116
- logger.debug('Tag cache needs update, refreshing...');
117
- await this.initializeCache();
138
+ // Check if cache is fresh enough
139
+ if (now - this.lastUpdate <= this.updateInterval) {
140
+ return; // Cache is up-to-date
141
+ }
142
+ // Check if an update is already in progress
143
+ if (this.isUpdating) {
144
+ logger.debug('Cache update already in progress, skipping redundant update.');
145
+ // Optionally, wait for the ongoing update instead of returning immediately
146
+ // For now, we return to avoid complexity, potentially serving slightly stale data.
147
+ return;
148
+ }
149
+ // Acquire the update lock
150
+ this.isUpdating = true;
151
+ logger.debug('Acquired cache update lock.');
152
+ try {
153
+ // Double-check the update condition after acquiring the lock
154
+ // to handle cases where another process finished updating
155
+ // while this one was waiting for the lock (though less likely with a simple flag).
156
+ const nowAfterLock = Date.now();
157
+ if (nowAfterLock - this.lastUpdate > this.updateInterval) {
158
+ logger.info('Tag cache needs update, refreshing...');
159
+ await this.initializeCache(); // This method updates this.lastUpdate internally
160
+ }
161
+ else {
162
+ logger.debug('Cache was updated by another process while waiting for lock.');
163
+ }
164
+ }
165
+ catch (error) {
166
+ // Log error during update, but don't necessarily block other operations
167
+ logger.error('Error during cache update:', errorToObject(error));
168
+ // Decide if the error should be re-thrown or handled gracefully
169
+ // For now, we log and continue, allowing the lock to be released.
170
+ }
171
+ finally {
172
+ // Release the update lock
173
+ this.isUpdating = false;
174
+ logger.debug('Released cache update lock.');
118
175
  }
119
176
  }
120
177
  /**
@@ -29,19 +29,26 @@ export class PropertyManager {
29
29
  return {};
30
30
  }
31
31
  const frontmatter = match[1];
32
- const properties = parse(frontmatter);
33
- // Handle tags - don't add # prefix in frontmatter
34
- if (properties.tags && Array.isArray(properties.tags)) {
35
- properties.tags = properties.tags.map((tag) => tag.startsWith('#') ? tag.substring(1) : tag);
32
+ // Parse YAML first
33
+ const rawProperties = parse(frontmatter);
34
+ // Validate the raw parsed object against the schema
35
+ const validationResult = ObsidianPropertiesSchema.safeParse(rawProperties);
36
+ if (!validationResult.success) {
37
+ // Log validation errors and return empty if invalid
38
+ logger.warn('Frontmatter validation failed:', {
39
+ validationError: validationResult.error.flatten()
40
+ });
41
+ return {}; // Return empty object for invalid frontmatter
36
42
  }
37
- // Validate against schema
38
- const result = ObsidianPropertiesSchema.safeParse(properties);
39
- if (!result.success) {
40
- logger.warn('Property validation warnings:', { validationError: result.error });
41
- // Return the properties with fixed tags
42
- return properties;
43
+ // Use the validated data from now on
44
+ const validatedProperties = validationResult.data;
45
+ // Handle tags transformation on validated data
46
+ if (validatedProperties.tags && Array.isArray(validatedProperties.tags)) {
47
+ // Create a new array to avoid modifying the validated data directly if needed elsewhere
48
+ validatedProperties.tags = validatedProperties.tags.map((tag) => tag.startsWith('#') ? tag.substring(1) : tag);
43
49
  }
44
- return result.data;
50
+ // Return the validated (and potentially transformed) properties
51
+ return validatedProperties;
45
52
  }
46
53
  catch (error) {
47
54
  logger.error('Error parsing properties:', error instanceof Error ? error : { error: String(error) });
@@ -0,0 +1,21 @@
1
+ import { nanoid } from 'nanoid';
2
+ /**
3
+ * Generates a unique, URL-friendly, 6-character alphanumeric ID.
4
+ * Uses nanoid's default alphabet (A-Za-z0-9_-).
5
+ *
6
+ * @returns A 6-character string ID.
7
+ */
8
+ export function generateShortId() {
9
+ return nanoid(6);
10
+ }
11
+ /**
12
+ * Generates a unique ID with a specified prefix and length.
13
+ *
14
+ * @param prefix - The prefix for the ID (e.g., 'prj', 'tsk', 'knw').
15
+ * @param length - The desired length of the random part of the ID (default: 10).
16
+ * @returns A prefixed ID string (e.g., 'prj_aBcDeFgHiJ').
17
+ */
18
+ export function generatePrefixedId(prefix, length = 10) {
19
+ return `${prefix}_${nanoid(length)}`;
20
+ }
21
+ //# sourceMappingURL=idGenerator.js.map
@@ -11,20 +11,27 @@ export const TRUNCATION_MESSAGE = "\n\n[Response truncated due to length]";
11
11
  export class TokenCounter {
12
12
  tokenizer = encoding_for_model("gpt-4"); // This is strictly for token counting, not for LLM inference
13
13
  isShuttingDown = false;
14
+ cleanupListener;
14
15
  constructor() {
15
- // Clean up tokenizer when process exits
16
- const cleanup = () => {
16
+ // Define the cleanup logic
17
+ this.cleanupListener = () => {
17
18
  if (!this.isShuttingDown) {
18
19
  this.isShuttingDown = true;
19
20
  if (this.tokenizer) {
20
21
  this.tokenizer.free();
22
+ // No need to explicitly set this.tokenizer to null,
23
+ // but ensure it's not used after free()
21
24
  }
25
+ // Remove listeners after execution to prevent multiple calls
26
+ // and potential leaks if cleanup is called manually before exit
27
+ this.removeListeners();
22
28
  }
23
29
  };
24
- process.on('exit', cleanup);
25
- process.on('SIGINT', cleanup);
26
- process.on('SIGTERM', cleanup);
27
- process.on('uncaughtException', cleanup);
30
+ // Attach listeners
31
+ process.on('exit', this.cleanupListener);
32
+ process.on('SIGINT', this.cleanupListener);
33
+ process.on('SIGTERM', this.cleanupListener);
34
+ process.on('uncaughtException', this.cleanupListener);
28
35
  }
29
36
  /**
30
37
  * Count the number of tokens in a string
@@ -48,13 +55,19 @@ export class TokenCounter {
48
55
  return truncatedText + TRUNCATION_MESSAGE;
49
56
  }
50
57
  /**
51
- * Clean up resources
58
+ * Clean up resources and remove listeners
52
59
  */
53
60
  cleanup() {
54
- if (this.tokenizer && !this.isShuttingDown) {
55
- this.isShuttingDown = true;
56
- this.tokenizer.free();
57
- }
61
+ this.cleanupListener(); // Call the main cleanup logic
62
+ }
63
+ /**
64
+ * Remove process event listeners
65
+ */
66
+ removeListeners() {
67
+ process.off('exit', this.cleanupListener);
68
+ process.off('SIGINT', this.cleanupListener);
69
+ process.off('SIGTERM', this.cleanupListener);
70
+ process.off('uncaughtException', this.cleanupListener);
58
71
  }
59
72
  }
60
73
  // Export a singleton instance
@@ -1,6 +1,8 @@
1
1
  /**
2
2
  * Validation utilities for the Obsidian MCP Server
3
3
  */
4
+ import { McpErrorCode } from "../mcp/types.js";
5
+ import { ObsidianError } from "./errors.js";
4
6
  /**
5
7
  * Validates a file path to prevent path traversal attacks and other security issues
6
8
  * @param filepath The path to validate
@@ -10,11 +12,13 @@ export function validateFilePath(filepath) {
10
12
  // Prevent path traversal attacks
11
13
  const normalizedPath = filepath.replace(/\\/g, '/');
12
14
  if (normalizedPath.includes('../') || normalizedPath.includes('..\\')) {
13
- throw new Error('Invalid file path: Path traversal not allowed');
15
+ // Use ObsidianError for consistency
16
+ throw new ObsidianError('Invalid file path: Path traversal not allowed', McpErrorCode.BAD_REQUEST);
14
17
  }
15
- // Additional path validations
18
+ // Additional path validations (check for absolute paths Unix or Windows style)
16
19
  if (normalizedPath.startsWith('/') || /^[a-zA-Z]:/.test(normalizedPath)) {
17
- throw new Error('Invalid file path: Absolute paths not allowed');
20
+ // Use ObsidianError for consistency
21
+ throw new ObsidianError('Invalid file path: Absolute paths not allowed', McpErrorCode.BAD_REQUEST);
18
22
  }
19
23
  }
20
24
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "obsidian-mcp-server",
3
- "version": "1.5.0",
3
+ "version": "1.5.2",
4
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",
@@ -19,21 +19,23 @@
19
19
  "format": "prettier --write \"src/**/*.ts\""
20
20
  },
21
21
  "dependencies": {
22
- "@modelcontextprotocol/sdk": "^1.7.0",
23
- "axios": "^1.8.3",
22
+ "@modelcontextprotocol/sdk": "^1.8.0",
23
+ "@types/node": "^22.14.0",
24
+ "@typescript-eslint/eslint-plugin": "^8.29.0",
25
+ "@typescript-eslint/parser": "^8.29.0",
26
+ "axios": "^1.8.4",
24
27
  "dotenv": "^16.4.7",
25
- "tiktoken": "^1.0.20",
26
- "yaml": "^2.7.0",
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",
28
+ "eslint": "^9.24.0",
33
29
  "eslint-config-prettier": "^10.1.1",
34
- "eslint-plugin-prettier": "^5.2.3",
30
+ "eslint-plugin-prettier": "^5.2.6",
31
+ "nanoid": "^5.1.5",
32
+ "p-limit": "^6.2.0",
35
33
  "prettier": "^3.5.3",
36
- "typescript": "^5.8.2"
34
+ "tiktoken": "^1.0.20",
35
+ "typescript": "^5.8.3",
36
+ "winston": "^3.17.0",
37
+ "yaml": "^2.7.1",
38
+ "zod": "^3.24.2"
37
39
  },
38
40
  "keywords": [
39
41
  "mcp",
@@ -0,0 +1,20 @@
1
+ {
2
+ "output": {
3
+ "filePath": "repomix-output.xml",
4
+ "style": "xml",
5
+ "removeComments": false,
6
+ "removeEmptyLines": false,
7
+ "topFilesLength": 5,
8
+ "showLineNumbers": false,
9
+ "copyToClipboard": false
10
+ },
11
+ "include": [],
12
+ "ignore": {
13
+ "useGitignore": true,
14
+ "useDefaultPatterns": true,
15
+ "customPatterns": []
16
+ },
17
+ "security": {
18
+ "enableSecurityCheck": true
19
+ }
20
+ }
@@ -85,8 +85,18 @@ export function setupToolListingHandler(
85
85
  // Log failure with timing information
86
86
  const elapsedMs = logger.endTimer('list_tools', 'Failed to list tools');
87
87
  logger.logOperationResult(false, 'list_tools', elapsedMs);
88
- logger.error('Failed to list available tools', error instanceof Error ? error : undefined);
89
- throw error;
88
+ const errorMessage = error instanceof Error ? error.message : String(error);
89
+ logger.error('Failed to list available tools', {
90
+ error: errorMessage,
91
+ stack: error instanceof Error ? error.stack : undefined,
92
+ errorCategory: ErrorCategoryType.CATEGORY_SYSTEM
93
+ });
94
+ // Wrap in ObsidianError
95
+ throw new ObsidianError(
96
+ `Failed to list tools: ${errorMessage}`,
97
+ McpErrorCode.INTERNAL_SERVER_ERROR,
98
+ { originalError: errorMessage }
99
+ );
90
100
  }
91
101
  });
92
102
  }
@@ -152,8 +162,10 @@ export function setupToolCallingHandler(
152
162
 
153
163
  // Add timeout handling
154
164
  const timeoutMs = DEFAULT_TIMEOUT_CONFIG.toolExecutionMs;
165
+ let timeoutId: NodeJS.Timeout | null = null; // Variable to hold the timeout ID
155
166
  const timeoutPromise = new Promise<never>((_, reject) => {
156
- setTimeout(() => {
167
+ timeoutId = setTimeout(() => {
168
+ timeoutId = null; // Clear the ID reference once the timeout executes
157
169
  const errorInfo = {
158
170
  toolName: name,
159
171
  timeoutMs,
@@ -269,6 +281,12 @@ export function setupToolCallingHandler(
269
281
  McpErrorCode.INTERNAL_SERVER_ERROR,
270
282
  { originalError: errorMessage, stack: errorStack }
271
283
  );
284
+ } finally {
285
+ // Ensure the timeout is cleared regardless of the outcome
286
+ if (timeoutId) {
287
+ clearTimeout(timeoutId);
288
+ logger.debug(`Cleared timeout for tool: ${name}`, { operationId });
289
+ }
272
290
  }
273
291
  });
274
292
  }
@@ -302,8 +320,18 @@ export function setupResourceListingHandler(
302
320
  // Log failure with timing information
303
321
  const elapsedMs = logger.endTimer('list_resources', 'Failed to list resources');
304
322
  logger.logOperationResult(false, 'list_resources', elapsedMs);
305
- logger.error('Failed to list available resources', error instanceof Error ? error : undefined);
306
- throw error;
323
+ const errorMessage = error instanceof Error ? error.message : String(error);
324
+ logger.error('Failed to list available resources', {
325
+ error: errorMessage,
326
+ stack: error instanceof Error ? error.stack : undefined,
327
+ errorCategory: ErrorCategoryType.CATEGORY_SYSTEM
328
+ });
329
+ // Wrap in ObsidianError
330
+ throw new ObsidianError(
331
+ `Failed to list resources: ${errorMessage}`,
332
+ McpErrorCode.INTERNAL_SERVER_ERROR,
333
+ { originalError: errorMessage }
334
+ );
307
335
  }
308
336
  });
309
337
  }
@@ -382,4 +410,4 @@ export function setupResourceReadingHandler(
382
410
  );
383
411
  }
384
412
  });
385
- }
413
+ }
package/src/mcp/server.ts CHANGED
@@ -4,6 +4,7 @@
4
4
  import { config } from "dotenv";
5
5
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
6
6
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
7
+ import { ObsidianError } from "../utils/errors.js"; // Import ObsidianError
7
8
  import { createLogger, ErrorCategoryType } from "../utils/logging.js";
8
9
  import { rateLimiter } from "../utils/rate-limiting.js";
9
10
  import { createTagResource } from "../resources/index.js";
@@ -40,11 +41,15 @@ export async function initializeServer(): Promise<Server> {
40
41
  if (!API_KEY) {
41
42
  logger.error("Missing API key", {
42
43
  errorCategory: ErrorCategoryType.CATEGORY_AUTHENTICATION,
43
- errorCode: McpErrorCode.UNAUTHORIZED
44
- });
45
- throw new Error("OBSIDIAN_API_KEY environment variable is required");
46
- }
47
-
44
+ errorCode: McpErrorCode.UNAUTHORIZED
45
+ });
46
+ // Use ObsidianError for consistency
47
+ throw new ObsidianError(
48
+ "OBSIDIAN_API_KEY environment variable is required",
49
+ McpErrorCode.UNAUTHORIZED
50
+ );
51
+ }
52
+
48
53
  // Initialize Obsidian client with environment configuration
49
54
  logger.info('Initializing Obsidian client', {
50
55
  verifySSL: process.env.VERIFY_SSL === 'true',
@@ -174,19 +179,11 @@ export function setupShutdownHandling(
174
179
  logger.info('Shutting down server...', { signal });
175
180
 
176
181
  try {
177
- // Run cleanup handlers
182
+ // Run cleanup handlers sequentially, stopping on error
178
183
  logger.debug('Running cleanup handlers', { handlersCount: cleanupHandlers.length });
179
184
  for (const handler of cleanupHandlers) {
180
- try {
181
- await handler();
182
- } catch (error) {
183
- const errorMessage = error instanceof Error ? error.message : String(error);
184
- logger.error('Error during cleanup handler execution', {
185
- error: errorMessage,
186
- stack: error instanceof Error ? error.stack : undefined,
187
- errorCategory: ErrorCategoryType.CATEGORY_SYSTEM
188
- });
189
- }
185
+ // No try-catch here; let errors propagate to the main catch block
186
+ await handler();
190
187
  }
191
188
 
192
189
  // Dispose rate limiter
@@ -288,4 +285,4 @@ export async function run(): Promise<void> {
288
285
 
289
286
  process.exit(1);
290
287
  }
291
- }
288
+ }