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