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 +3 -3
- package/build/mcp/handlers.js +26 -5
- package/build/mcp/server.js +6 -13
- package/build/resources/tags.js +72 -15
- package/build/tools/properties/manager.js +18 -11
- package/build/utils/idGenerator.js +21 -0
- package/build/utils/tokenization.js +24 -11
- package/build/utils/validation.js +7 -3
- package/package.json +15 -13
- package/repomix.config.json +20 -0
- package/src/mcp/handlers.ts +34 -6
- package/src/mcp/server.ts +14 -17
- package/src/resources/tags.ts +79 -21
- package/src/tools/properties/manager.ts +24 -15
- package/src/utils/idGenerator.ts +22 -0
- package/src/utils/tokenization.ts +26 -12
- package/src/utils/validation.ts +14 -4
- package/examples/README.md +0 -48
- package/examples/append-content.md +0 -63
- package/examples/complex-search.md +0 -117
- package/examples/find-in-file.md +0 -94
- package/examples/get-file-contents.md +0 -72
- package/examples/get-properties.md +0 -89
- package/examples/list-files-in-dir.md +0 -55
- package/examples/list-files-in-vault.md +0 -53
- package/examples/patch-content.md +0 -60
- package/examples/update-properties.md +0 -126
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Obsidian MCP Server
|
|
2
2
|
|
|
3
|
-
[](https://www.typescriptlang.org/)
|
|
4
|
+
[](https://modelcontextprotocol.io/)
|
|
5
|
+
[]()
|
|
6
6
|
[](https://opensource.org/licenses/Apache-2.0)
|
|
7
7
|
[]()
|
|
8
8
|
[](https://github.com/cyanheads/obsidian-mcp-server)
|
package/build/mcp/handlers.js
CHANGED
|
@@ -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
|
-
|
|
61
|
-
|
|
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
|
-
|
|
228
|
-
|
|
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
|
}
|
package/build/mcp/server.js
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";
|
|
@@ -29,7 +30,8 @@ export async function initializeServer() {
|
|
|
29
30
|
errorCategory: ErrorCategoryType.CATEGORY_AUTHENTICATION,
|
|
30
31
|
errorCode: McpErrorCode.UNAUTHORIZED
|
|
31
32
|
});
|
|
32
|
-
|
|
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
|
-
|
|
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');
|
package/build/resources/tags.js
CHANGED
|
@@ -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
|
-
//
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
82
|
-
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
//
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
//
|
|
42
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
16
|
-
|
|
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
|
-
|
|
25
|
-
process.on('
|
|
26
|
-
process.on('
|
|
27
|
-
process.on('
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
23
|
-
"
|
|
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
|
-
"
|
|
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.
|
|
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
|
-
"
|
|
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
|
+
}
|
package/src/mcp/handlers.ts
CHANGED
|
@@ -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
|
-
|
|
89
|
-
|
|
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
|
-
|
|
306
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
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
|
+
}
|