obsidian-mcp-server 1.3.0 → 1.4.1

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.
Files changed (79) hide show
  1. package/LICENSE +2 -1
  2. package/README.md +36 -77
  3. package/build/index.js +12 -3
  4. package/build/mcp/handlers.js +138 -0
  5. package/build/mcp/index.js +7 -0
  6. package/build/mcp/server.js +131 -0
  7. package/build/mcp/types.js +7 -0
  8. package/build/{obsidian.js → obsidian/client.js} +116 -96
  9. package/build/obsidian/errors.js +75 -0
  10. package/build/obsidian/index.js +7 -0
  11. package/build/obsidian/types.js +12 -0
  12. package/build/resources/index.js +15 -0
  13. package/build/{resources.js → resources/tags.js} +31 -5
  14. package/build/resources/types.js +5 -0
  15. package/build/tools/base.js +78 -0
  16. package/build/tools/files/content.js +171 -0
  17. package/build/tools/files/index.js +22 -0
  18. package/build/tools/files/list.js +133 -0
  19. package/build/tools/index.js +31 -0
  20. package/build/tools/properties/index.js +19 -0
  21. package/build/{properties.js → tools/properties/manager.js} +40 -8
  22. package/build/{propertyTools.js → tools/properties/tools.js} +22 -6
  23. package/build/{propertyTypes.js → tools/properties/types.js} +17 -5
  24. package/build/tools/search/complex.js +203 -0
  25. package/build/tools/search/index.js +20 -0
  26. package/build/tools/search/simple.js +127 -0
  27. package/build/utils/errors.js +59 -0
  28. package/build/utils/index.js +9 -0
  29. package/build/utils/logging.js +119 -0
  30. package/build/utils/rate-limiting.js +94 -0
  31. package/build/utils/tokenization.js +62 -0
  32. package/build/utils/validation.js +88 -0
  33. package/examples/README.md +48 -0
  34. package/examples/append-content.md +63 -0
  35. package/examples/complex-search.md +117 -0
  36. package/examples/find-in-file.md +94 -0
  37. package/examples/get-file-contents.md +72 -0
  38. package/examples/get-properties.md +89 -0
  39. package/examples/list-files-in-dir.md +55 -0
  40. package/examples/list-files-in-vault.md +53 -0
  41. package/examples/patch-content.md +60 -0
  42. package/examples/update-properties.md +126 -0
  43. package/package.json +12 -14
  44. package/src/index.ts +13 -3
  45. package/src/mcp/handlers.ts +183 -0
  46. package/src/mcp/index.ts +6 -0
  47. package/src/mcp/server.ts +162 -0
  48. package/src/mcp/types.ts +46 -0
  49. package/src/{obsidian.ts → obsidian/client.ts} +132 -125
  50. package/src/obsidian/errors.ts +105 -0
  51. package/src/obsidian/index.ts +6 -0
  52. package/src/obsidian/types.ts +124 -0
  53. package/src/resources/index.ts +17 -0
  54. package/src/{resources.ts → resources/tags.ts} +38 -6
  55. package/src/resources/types.ts +30 -0
  56. package/src/tools/base.ts +112 -0
  57. package/src/tools/files/content.ts +206 -0
  58. package/src/tools/files/index.ts +31 -0
  59. package/src/tools/files/list.ts +150 -0
  60. package/src/tools/index.ts +38 -0
  61. package/src/tools/properties/index.ts +21 -0
  62. package/src/{properties.ts → tools/properties/manager.ts} +41 -8
  63. package/src/{propertyTools.ts → tools/properties/tools.ts} +33 -7
  64. package/src/{propertyTypes.ts → tools/properties/types.ts} +29 -5
  65. package/src/tools/search/complex.ts +231 -0
  66. package/src/tools/search/index.ts +22 -0
  67. package/src/tools/search/simple.ts +147 -0
  68. package/src/utils/errors.ts +69 -0
  69. package/src/utils/index.ts +8 -0
  70. package/src/utils/logging.ts +146 -0
  71. package/src/utils/rate-limiting.ts +114 -0
  72. package/src/utils/tokenization.ts +71 -0
  73. package/src/utils/validation.ts +95 -0
  74. package/build/server.js +0 -263
  75. package/build/tools.js +0 -845
  76. package/build/types.js +0 -37
  77. package/src/server.ts +0 -335
  78. package/src/tools.ts +0 -926
  79. package/src/types.ts +0 -184
@@ -1,42 +1,54 @@
1
+ /**
2
+ * Obsidian REST API client implementation
3
+ */
1
4
  import axios from "axios";
2
- import { ObsidianError, DEFAULT_OBSIDIAN_CONFIG } from "./types.js";
3
5
  import { Agent } from "node:https";
4
6
  import { readFileSync } from "fs";
5
7
  import { fileURLToPath } from 'url';
6
8
  import { dirname, join } from "path";
9
+ import { createLogger } from '../utils/logging.js';
10
+ import { ObsidianError } from '../utils/errors.js';
11
+ import { validateFilePath, sanitizeHeader } from '../utils/validation.js';
12
+ import { DEFAULT_OBSIDIAN_CONFIG } from './types.js';
13
+ import { createMissingAPIKeyMessage, handleAxiosError } from './errors.js';
14
+ // Logger for the ObsidianClient
15
+ const logger = createLogger('ObsidianClient');
7
16
  // Get package version for user agent
8
17
  const __filename = fileURLToPath(import.meta.url);
9
18
  const __dirname = dirname(__filename);
10
19
  const VERSION = (() => {
11
20
  try {
12
21
  // Look for package.json in the same directory as the built files
13
- const packagePath = join(__dirname, '..', 'package.json');
22
+ const packagePath = join(__dirname, '..', '..', 'package.json');
14
23
  const pkg = JSON.parse(readFileSync(packagePath, 'utf-8'));
15
24
  return pkg.version;
16
25
  }
17
26
  catch (error) {
18
27
  // Try alternative location for development
19
28
  try {
20
- const devPackagePath = join(__dirname, '..', '..', 'package.json');
29
+ const devPackagePath = join(__dirname, '..', '..', '..', 'package.json');
21
30
  const pkg = JSON.parse(readFileSync(devPackagePath, 'utf-8'));
22
31
  return pkg.version;
23
32
  }
24
33
  catch (devError) {
25
- console.warn('Could not read package.json version, using fallback');
34
+ logger.warn('Could not read package.json version, using fallback');
26
35
  return '1.1.0'; // Fallback version
27
36
  }
28
37
  }
29
38
  })();
39
+ /**
40
+ * Client for interacting with the Obsidian Local REST API
41
+ */
30
42
  export class ObsidianClient {
31
43
  client;
32
44
  config;
45
+ /**
46
+ * Create a new ObsidianClient
47
+ * @param config Configuration for the client
48
+ */
33
49
  constructor(config) {
34
50
  if (!config.apiKey) {
35
- throw new ObsidianError("Missing API key. To fix this:\n" +
36
- "1. Install the 'Local REST API' plugin in Obsidian\n" +
37
- "2. Enable the plugin in Obsidian Settings\n" +
38
- "3. Copy your API key from Obsidian Settings > Local REST API\n" +
39
- "4. Provide the API key in your configuration", 40100 // Unauthorized
51
+ throw new ObsidianError(createMissingAPIKeyMessage(), 40100 // Unauthorized
40
52
  );
41
53
  }
42
54
  // Determine if we're in a development environment
@@ -89,7 +101,7 @@ export class ObsidianClient {
89
101
  decompress: true
90
102
  };
91
103
  if (!this.config.verifySSL) {
92
- console.warn("WARNING: SSL verification is disabled. While this works for development, it's not recommended for production.\n" +
104
+ logger.warn("WARNING: SSL verification is disabled. While this works for development, it's not recommended for production.\n" +
93
105
  "To properly configure SSL certificates:\n" +
94
106
  "1. Go to Obsidian Settings > Local REST API\n" +
95
107
  "2. Under 'How to Access', copy the certificate\n" +
@@ -104,9 +116,15 @@ export class ObsidianClient {
104
116
  }
105
117
  this.client = axios.create(axiosConfig);
106
118
  }
119
+ /**
120
+ * Get the base URL for the Obsidian API
121
+ */
107
122
  getBaseUrl() {
108
123
  return `${this.config.protocol}://${this.config.host}:${this.config.port}`;
109
124
  }
125
+ /**
126
+ * Get headers for requests to the Obsidian API
127
+ */
110
128
  getHeaders() {
111
129
  const headers = {
112
130
  Authorization: `Bearer ${this.config.apiKey}`,
@@ -116,93 +134,19 @@ export class ObsidianClient {
116
134
  // Sanitize headers
117
135
  return Object.fromEntries(Object.entries(headers).map(([key, value]) => [
118
136
  key,
119
- this.sanitizeHeader(value)
137
+ sanitizeHeader(value)
120
138
  ]));
121
139
  }
122
- sanitizeHeader(value) {
123
- // Remove any potentially harmful characters from header values
124
- return value.replace(/[^\w\s\-\._~:/?#\[\]@!$&'()*+,;=]/g, '');
125
- }
126
- validateFilePath(filepath) {
127
- // Prevent path traversal attacks
128
- const normalizedPath = filepath.replace(/\\/g, '/');
129
- if (normalizedPath.includes('../') || normalizedPath.includes('..\\')) {
130
- throw new ObsidianError('Invalid file path: Path traversal not allowed', 40001);
131
- }
132
- // Additional path validations
133
- if (normalizedPath.startsWith('/') || /^[a-zA-Z]:/.test(normalizedPath)) {
134
- throw new ObsidianError('Invalid file path: Absolute paths not allowed', 40002);
135
- }
136
- }
137
- getErrorCode(status) {
138
- switch (status) {
139
- case 400: return 40000; // Bad request
140
- case 401: return 40100; // Unauthorized
141
- case 403: return 40300; // Forbidden
142
- case 404: return 40400; // Not found
143
- case 405: return 40500; // Method not allowed
144
- case 409: return 40900; // Conflict
145
- case 429: return 42900; // Too many requests
146
- case 500: return 50000; // Internal server error
147
- case 501: return 50100; // Not implemented
148
- case 502: return 50200; // Bad gateway
149
- case 503: return 50300; // Service unavailable
150
- case 504: return 50400; // Gateway timeout
151
- default:
152
- if (status >= 400 && status < 500)
153
- return 40000 + (status - 400) * 100;
154
- if (status >= 500 && status < 600)
155
- return 50000 + (status - 500) * 100;
156
- return 50000;
157
- }
158
- }
140
+ /**
141
+ * Safely execute an API request with error handling
142
+ */
159
143
  async safeRequest(operation) {
160
144
  try {
161
145
  return await operation();
162
146
  }
163
147
  catch (error) {
164
148
  if (axios.isAxiosError(error)) {
165
- const axiosError = error;
166
- const response = axiosError.response;
167
- const errorData = response?.data;
168
- // Handle common connection errors with helpful messages
169
- if (error.code === 'DEPTH_ZERO_SELF_SIGNED_CERT' || error.code === 'UNABLE_TO_VERIFY_LEAF_SIGNATURE') {
170
- throw new ObsidianError(`SSL certificate verification failed. You have two options:\n\n` +
171
- `Option 1 - Enable HTTP (not recommended for production):\n` +
172
- `1. Go to Obsidian Settings > Local REST API\n` +
173
- `2. Enable "Enable Non-encrypted (HTTP) Server"\n` +
174
- `3. Update your client config to use "http" protocol\n\n` +
175
- `Option 2 - Configure HTTPS (recommended):\n` +
176
- `1. Go to Obsidian Settings > Local REST API\n` +
177
- `2. Under 'How to Access', copy the certificate\n` +
178
- `3. Add the certificate to your system's trusted certificates:\n` +
179
- ` - On macOS: Add to Keychain Access\n` +
180
- ` - On Windows: Add to Certificate Manager\n` +
181
- ` - On Linux: Add to ca-certificates\n` +
182
- ` For development only: Set verifySSL: false in client config\n\n` +
183
- `Original error: ${error.message}`, 50001, // SSL error code
184
- { code: error.code, config: { verifySSL: this.config.verifySSL } });
185
- }
186
- if (error.code === 'ECONNREFUSED') {
187
- throw new ObsidianError(`Connection refused. To fix this:\n` +
188
- `1. Ensure Obsidian is running\n` +
189
- `2. Verify the 'Local REST API' plugin is enabled in Obsidian Settings\n` +
190
- `3. Check that you're using the correct host (${this.config.host}) and port (${this.config.port})\n` +
191
- `4. Make sure HTTPS is enabled in the plugin settings`, 50002, // Connection refused
192
- { code: error.code });
193
- }
194
- if (response?.status === 401) {
195
- throw new ObsidianError(`Authentication failed. To fix this:\n` +
196
- `1. Go to Obsidian Settings > Local REST API\n` +
197
- `2. Copy your API key from the settings\n` +
198
- `3. Update your configuration with the new API key\n` +
199
- `Note: The API key changes when you regenerate certificates`, 40100, // Unauthorized
200
- { code: error.code });
201
- }
202
- // For other errors, use API error code if available
203
- const errorCode = errorData?.errorCode ?? this.getErrorCode(response?.status ?? 500);
204
- const message = errorData?.message ?? axiosError.message ?? "Unknown error";
205
- throw new ObsidianError(message, errorCode, errorData);
149
+ throw handleAxiosError(error, this.config.host, this.config.port);
206
150
  }
207
151
  if (error instanceof Error) {
208
152
  throw new ObsidianError(error.message, 50000, error);
@@ -210,38 +154,58 @@ export class ObsidianClient {
210
154
  throw new ObsidianError("Unknown error occurred", 50000, error);
211
155
  }
212
156
  }
157
+ /**
158
+ * List all files in the vault
159
+ */
213
160
  async listFilesInVault() {
214
161
  return this.safeRequest(async () => {
162
+ logger.debug('Listing all files in vault');
215
163
  const response = await this.client.get("/vault/");
216
164
  return response.data.files;
217
165
  });
218
166
  }
167
+ /**
168
+ * List files in a specific directory
169
+ */
219
170
  async listFilesInDir(dirpath) {
220
- this.validateFilePath(dirpath);
171
+ validateFilePath(dirpath);
221
172
  return this.safeRequest(async () => {
173
+ logger.debug(`Listing files in directory: ${dirpath}`);
222
174
  const response = await this.client.get(`/vault/${dirpath}/`);
223
175
  return response.data.files;
224
176
  });
225
177
  }
178
+ /**
179
+ * Get the contents of a file
180
+ */
226
181
  async getFileContents(filepath) {
227
- this.validateFilePath(filepath);
182
+ validateFilePath(filepath);
228
183
  return this.safeRequest(async () => {
184
+ logger.debug(`Getting contents of file: ${filepath}`);
229
185
  const response = await this.client.get(`/vault/${filepath}`);
230
186
  return response.data;
231
187
  });
232
188
  }
189
+ /**
190
+ * Search for a string across all files
191
+ */
233
192
  async search(query, contextLength = 100) {
234
193
  return this.safeRequest(async () => {
194
+ logger.debug(`Searching for: ${query} with context length: ${contextLength}`);
235
195
  const response = await this.client.post("/search/simple/", null, { params: { query, contextLength } });
236
196
  return response.data;
237
197
  });
238
198
  }
199
+ /**
200
+ * Append content to a file
201
+ */
239
202
  async appendContent(filepath, content) {
240
- this.validateFilePath(filepath);
203
+ validateFilePath(filepath);
241
204
  if (!content || typeof content !== 'string') {
242
205
  throw new ObsidianError('Invalid content: Content must be a non-empty string', 40003);
243
206
  }
244
207
  return this.safeRequest(async () => {
208
+ logger.debug(`Appending content to file: ${filepath}`);
245
209
  await this.client.post(`/vault/${filepath}`, content, {
246
210
  headers: {
247
211
  "Content-Type": "text/markdown"
@@ -249,12 +213,16 @@ export class ObsidianClient {
249
213
  });
250
214
  });
251
215
  }
216
+ /**
217
+ * Update the entire content of a file
218
+ */
252
219
  async updateContent(filepath, content) {
253
- this.validateFilePath(filepath);
220
+ validateFilePath(filepath);
254
221
  if (!content || typeof content !== 'string') {
255
222
  throw new ObsidianError('Invalid content: Content must be a non-empty string', 40003);
256
223
  }
257
224
  return this.safeRequest(async () => {
225
+ logger.debug(`Updating content of file: ${filepath}`);
258
226
  await this.client.put(`/vault/${filepath}`, content, {
259
227
  headers: {
260
228
  "Content-Type": "text/markdown"
@@ -262,8 +230,12 @@ export class ObsidianClient {
262
230
  });
263
231
  });
264
232
  }
233
+ /**
234
+ * Execute a complex search using JsonLogic query
235
+ */
265
236
  async searchJson(query) {
266
237
  return this.safeRequest(async () => {
238
+ logger.debug(`Executing JSON search with query: ${JSON.stringify(query)}`);
267
239
  const isTagSearch = JSON.stringify(query).includes('"contains"') &&
268
240
  JSON.stringify(query).includes('"#"');
269
241
  const response = await this.client.post("/search/", query, {
@@ -272,36 +244,56 @@ export class ObsidianClient {
272
244
  "Accept": "application/vnd.olrapi.note+json"
273
245
  }
274
246
  });
275
- return isTagSearch ? response.data : response.data;
247
+ return response.data;
276
248
  });
277
249
  }
250
+ /**
251
+ * Get server status
252
+ */
278
253
  async getStatus() {
279
254
  return this.safeRequest(async () => {
255
+ logger.debug('Getting server status');
280
256
  const response = await this.client.get("/");
281
257
  return response.data;
282
258
  });
283
259
  }
260
+ /**
261
+ * List available commands
262
+ */
284
263
  async listCommands() {
285
264
  return this.safeRequest(async () => {
265
+ logger.debug('Listing commands');
286
266
  const response = await this.client.get("/commands/");
287
267
  return response.data.commands;
288
268
  });
289
269
  }
270
+ /**
271
+ * Execute a command by ID
272
+ */
290
273
  async executeCommand(commandId) {
291
274
  return this.safeRequest(async () => {
275
+ logger.debug(`Executing command: ${commandId}`);
292
276
  await this.client.post(`/commands/${commandId}/`);
293
277
  });
294
278
  }
279
+ /**
280
+ * Open a file in Obsidian
281
+ */
295
282
  async openFile(filepath, newLeaf = false) {
296
- this.validateFilePath(filepath);
283
+ validateFilePath(filepath);
297
284
  return this.safeRequest(async () => {
285
+ logger.debug(`Opening file: ${filepath}, newLeaf: ${newLeaf}`);
298
286
  await this.client.post(`/open/${filepath}`, null, {
299
287
  params: { newLeaf }
300
288
  });
301
289
  });
302
290
  }
291
+ /**
292
+ * Get the currently active file
293
+ */
303
294
  async getActiveFile() {
304
295
  return this.safeRequest(async () => {
296
+ logger.debug('Getting active file');
305
297
  const response = await this.client.get("/active/", {
306
298
  headers: {
307
299
  "Accept": "application/vnd.olrapi.note+json"
@@ -310,8 +302,12 @@ export class ObsidianClient {
310
302
  return response.data;
311
303
  });
312
304
  }
305
+ /**
306
+ * Update the active file
307
+ */
313
308
  async updateActiveFile(content) {
314
309
  return this.safeRequest(async () => {
310
+ logger.debug('Updating active file');
315
311
  await this.client.put("/active/", content, {
316
312
  headers: {
317
313
  "Content-Type": "text/markdown"
@@ -319,13 +315,21 @@ export class ObsidianClient {
319
315
  });
320
316
  });
321
317
  }
318
+ /**
319
+ * Delete the active file
320
+ */
322
321
  async deleteActiveFile() {
323
322
  return this.safeRequest(async () => {
323
+ logger.debug('Deleting active file');
324
324
  await this.client.delete("/active/");
325
325
  });
326
326
  }
327
+ /**
328
+ * Patch the active file
329
+ */
327
330
  async patchActiveFile(operation, targetType, target, content, options) {
328
331
  return this.safeRequest(async () => {
332
+ logger.debug(`Patching active file: ${operation} ${targetType} "${target}"`);
329
333
  const headers = {
330
334
  "Operation": operation,
331
335
  "Target-Type": targetType,
@@ -341,8 +345,12 @@ export class ObsidianClient {
341
345
  await this.client.patch("/active/", content, { headers });
342
346
  });
343
347
  }
348
+ /**
349
+ * Get a periodic note (e.g., daily, weekly)
350
+ */
344
351
  async getPeriodicNote(period) {
345
352
  return this.safeRequest(async () => {
353
+ logger.debug(`Getting ${period} periodic note`);
346
354
  const response = await this.client.get(`/periodic/${period}/`, {
347
355
  headers: {
348
356
  "Accept": "application/vnd.olrapi.note+json"
@@ -351,8 +359,12 @@ export class ObsidianClient {
351
359
  return response.data;
352
360
  });
353
361
  }
362
+ /**
363
+ * Update a periodic note
364
+ */
354
365
  async updatePeriodicNote(period, content) {
355
366
  return this.safeRequest(async () => {
367
+ logger.debug(`Updating ${period} periodic note`);
356
368
  await this.client.put(`/periodic/${period}/`, content, {
357
369
  headers: {
358
370
  "Content-Type": "text/markdown"
@@ -360,13 +372,21 @@ export class ObsidianClient {
360
372
  });
361
373
  });
362
374
  }
375
+ /**
376
+ * Delete a periodic note
377
+ */
363
378
  async deletePeriodicNote(period) {
364
379
  return this.safeRequest(async () => {
380
+ logger.debug(`Deleting ${period} periodic note`);
365
381
  await this.client.delete(`/periodic/${period}/`);
366
382
  });
367
383
  }
384
+ /**
385
+ * Patch a periodic note
386
+ */
368
387
  async patchPeriodicNote(period, operation, targetType, target, content, options) {
369
388
  return this.safeRequest(async () => {
389
+ logger.debug(`Patching ${period} periodic note: ${operation} ${targetType} "${target}"`);
370
390
  const headers = {
371
391
  "Operation": operation,
372
392
  "Target-Type": targetType,
@@ -383,4 +403,4 @@ export class ObsidianClient {
383
403
  });
384
404
  }
385
405
  }
386
- //# sourceMappingURL=obsidian.js.map
406
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1,75 @@
1
+ import { ObsidianError, getErrorCodeFromStatus } from '../utils/errors.js';
2
+ /**
3
+ * Helper function to create a descriptive error message for SSL certificate issues
4
+ */
5
+ export function createSSLErrorMessage(error, config) {
6
+ return (`SSL certificate verification failed. You have two options:\n\n` +
7
+ `Option 1 - Enable HTTP (not recommended for production):\n` +
8
+ `1. Go to Obsidian Settings > Local REST API\n` +
9
+ `2. Enable "Enable Non-encrypted (HTTP) Server"\n` +
10
+ `3. Update your client config to use "http" protocol\n\n` +
11
+ `Option 2 - Configure HTTPS (recommended):\n` +
12
+ `1. Go to Obsidian Settings > Local REST API\n` +
13
+ `2. Under 'How to Access', copy the certificate\n` +
14
+ `3. Add the certificate to your system's trusted certificates:\n` +
15
+ ` - On macOS: Add to Keychain Access\n` +
16
+ ` - On Windows: Add to Certificate Manager\n` +
17
+ ` - On Linux: Add to ca-certificates\n` +
18
+ ` For development only: Set verifySSL: false in client config\n\n` +
19
+ `Original error: ${error.message}`);
20
+ }
21
+ /**
22
+ * Helper function to create a descriptive error message for connection refused issues
23
+ */
24
+ export function createConnectionRefusedMessage(host, port) {
25
+ return (`Connection refused. To fix this:\n` +
26
+ `1. Ensure Obsidian is running\n` +
27
+ `2. Verify the 'Local REST API' plugin is enabled in Obsidian Settings\n` +
28
+ `3. Check that you're using the correct host (${host}) and port (${port})\n` +
29
+ `4. Make sure HTTPS is enabled in the plugin settings`);
30
+ }
31
+ /**
32
+ * Helper function to create a descriptive error message for authentication failures
33
+ */
34
+ export function createAuthFailedMessage() {
35
+ return (`Authentication failed. To fix this:\n` +
36
+ `1. Go to Obsidian Settings > Local REST API\n` +
37
+ `2. Copy your API key from the settings\n` +
38
+ `3. Update your configuration with the new API key\n` +
39
+ `Note: The API key changes when you regenerate certificates`);
40
+ }
41
+ /**
42
+ * Helper function to create a descriptive error message for missing API key
43
+ */
44
+ export function createMissingAPIKeyMessage() {
45
+ return (`Missing API key. To fix this:\n` +
46
+ `1. Install the 'Local REST API' plugin in Obsidian\n` +
47
+ `2. Enable the plugin in Obsidian Settings\n` +
48
+ `3. Copy your API key from Obsidian Settings > Local REST API\n` +
49
+ `4. Provide the API key in your configuration`);
50
+ }
51
+ /**
52
+ * Helper function to handle Axios errors consistently
53
+ */
54
+ export function handleAxiosError(error, host, port) {
55
+ const response = error.response;
56
+ const errorData = response?.data;
57
+ // Handle common connection errors with helpful messages
58
+ if (error.code === 'DEPTH_ZERO_SELF_SIGNED_CERT' || error.code === 'UNABLE_TO_VERIFY_LEAF_SIGNATURE') {
59
+ return new ObsidianError(createSSLErrorMessage(error, { verifySSL: true }), 50001, // SSL error code
60
+ { code: error.code });
61
+ }
62
+ if (error.code === 'ECONNREFUSED') {
63
+ return new ObsidianError(createConnectionRefusedMessage(host, port), 50002, // Connection refused
64
+ { code: error.code });
65
+ }
66
+ if (response?.status === 401) {
67
+ return new ObsidianError(createAuthFailedMessage(), 40100, // Unauthorized
68
+ { code: error.code });
69
+ }
70
+ // For other errors, use API error code if available
71
+ const errorCode = errorData?.errorCode ?? getErrorCodeFromStatus(response?.status ?? 500);
72
+ const message = errorData?.message ?? error.message ?? "Unknown error";
73
+ return new ObsidianError(message, errorCode, errorData);
74
+ }
75
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Exports for the Obsidian module
3
+ */
4
+ export * from './client.js';
5
+ export * from './types.js';
6
+ export * from './errors.js';
7
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Type definitions for Obsidian client
3
+ */
4
+ /**
5
+ * Default configuration for Obsidian server
6
+ */
7
+ export const DEFAULT_OBSIDIAN_CONFIG = {
8
+ protocol: "https", // HTTPS required by default in Obsidian REST API plugin
9
+ host: "127.0.0.1",
10
+ port: 27124
11
+ };
12
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Resources module exports
3
+ */
4
+ export * from './types.js';
5
+ export * from './tags.js';
6
+ import { TagResource } from './tags.js';
7
+ /**
8
+ * Create and return the tag resource
9
+ * @param client The ObsidianClient instance
10
+ * @returns The tag resource
11
+ */
12
+ export function createTagResource(client) {
13
+ return new TagResource(client);
14
+ }
15
+ //# sourceMappingURL=index.js.map
@@ -1,5 +1,11 @@
1
- import { PropertyManager } from "./properties.js";
1
+ import { PropertyManager } from "../tools/properties/manager.js";
2
2
  import { sep } from "path";
3
+ import { createLogger } from "../utils/logging.js";
4
+ // Create a logger for tag resources
5
+ const logger = createLogger('TagResource');
6
+ /**
7
+ * Resource for providing tags used in the Obsidian vault
8
+ */
3
9
  export class TagResource {
4
10
  client;
5
11
  tagCache = new Map();
@@ -12,6 +18,9 @@ export class TagResource {
12
18
  this.propertyManager = new PropertyManager(client);
13
19
  this.initializeCache();
14
20
  }
21
+ /**
22
+ * Get resource description for the MCP server
23
+ */
15
24
  getResourceDescription() {
16
25
  return {
17
26
  uri: "obsidian://tags",
@@ -20,8 +29,12 @@ export class TagResource {
20
29
  mimeType: "application/json"
21
30
  };
22
31
  }
32
+ /**
33
+ * Initialize the tag cache
34
+ */
23
35
  async initializeCache() {
24
36
  try {
37
+ logger.info('Initializing tag cache');
25
38
  // Get all markdown files using platform-agnostic path pattern
26
39
  const query = {
27
40
  "glob": [`**${sep}*.md`.replace(/\\/g, '/'), { "var": "path" }]
@@ -43,32 +56,44 @@ export class TagResource {
43
56
  }
44
57
  }
45
58
  catch (error) {
46
- console.error(`Failed to process file ${result.filename}:`, error);
59
+ logger.error(`Failed to process file ${result.filename}:`, error);
47
60
  }
48
61
  }
49
62
  this.isInitialized = true;
50
63
  this.lastUpdate = Date.now();
64
+ logger.info(`Tag cache initialized with ${this.tagCache.size} unique tags`);
51
65
  }
52
66
  catch (error) {
53
- console.error("Failed to initialize tag cache:", error);
67
+ logger.error("Failed to initialize tag cache:", error);
54
68
  throw error;
55
69
  }
56
70
  }
71
+ /**
72
+ * Add a tag to the cache
73
+ */
57
74
  addTag(tag, filepath) {
58
75
  if (!this.tagCache.has(tag)) {
59
76
  this.tagCache.set(tag, new Set());
60
77
  }
61
78
  this.tagCache.get(tag).add(filepath);
62
79
  }
80
+ /**
81
+ * Update the cache if needed
82
+ */
63
83
  async updateCacheIfNeeded() {
64
84
  const now = Date.now();
65
85
  if (now - this.lastUpdate > this.updateInterval) {
86
+ logger.debug('Tag cache needs update, refreshing...');
66
87
  await this.initializeCache();
67
88
  }
68
89
  }
90
+ /**
91
+ * Get the content for the resource
92
+ */
69
93
  async getContent() {
70
94
  try {
71
95
  if (!this.isInitialized) {
96
+ logger.info('Tag cache not initialized, initializing now');
72
97
  await this.initializeCache();
73
98
  }
74
99
  else {
@@ -91,6 +116,7 @@ export class TagResource {
91
116
  lastUpdate: this.lastUpdate
92
117
  }
93
118
  };
119
+ logger.debug(`Returning tag resource with ${response.tags.length} tags`);
94
120
  return [{
95
121
  type: "text",
96
122
  text: JSON.stringify(response, null, 2),
@@ -98,9 +124,9 @@ export class TagResource {
98
124
  }];
99
125
  }
100
126
  catch (error) {
101
- console.error("Failed to get tags:", error);
127
+ logger.error("Failed to get tags:", error);
102
128
  throw error;
103
129
  }
104
130
  }
105
131
  }
106
- //# sourceMappingURL=resources.js.map
132
+ //# sourceMappingURL=tags.js.map
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Resource types for the MCP server
3
+ */
4
+ export {};
5
+ //# sourceMappingURL=types.js.map