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/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
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
|
-
[]()
|
|
8
8
|
[](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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
|
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
|
|
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
|
package/build/mcp/handlers.js
CHANGED
|
@@ -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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
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
|
-
|
|
39
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
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
|
-
|
|
63
|
-
|
|
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
|
-
|
|
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 ===
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
{
|
|
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
|
-
|
|
114
|
-
|
|
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
|
-
|
|
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
|
-
|
|
131
|
-
|
|
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
|