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.
- package/LICENSE +2 -1
- package/README.md +36 -77
- package/build/index.js +12 -3
- package/build/mcp/handlers.js +138 -0
- package/build/mcp/index.js +7 -0
- package/build/mcp/server.js +131 -0
- package/build/mcp/types.js +7 -0
- package/build/{obsidian.js → obsidian/client.js} +116 -96
- package/build/obsidian/errors.js +75 -0
- package/build/obsidian/index.js +7 -0
- package/build/obsidian/types.js +12 -0
- package/build/resources/index.js +15 -0
- package/build/{resources.js → resources/tags.js} +31 -5
- package/build/resources/types.js +5 -0
- package/build/tools/base.js +78 -0
- package/build/tools/files/content.js +171 -0
- package/build/tools/files/index.js +22 -0
- package/build/tools/files/list.js +133 -0
- package/build/tools/index.js +31 -0
- package/build/tools/properties/index.js +19 -0
- package/build/{properties.js → tools/properties/manager.js} +40 -8
- package/build/{propertyTools.js → tools/properties/tools.js} +22 -6
- package/build/{propertyTypes.js → tools/properties/types.js} +17 -5
- package/build/tools/search/complex.js +203 -0
- package/build/tools/search/index.js +20 -0
- package/build/tools/search/simple.js +127 -0
- package/build/utils/errors.js +59 -0
- package/build/utils/index.js +9 -0
- package/build/utils/logging.js +119 -0
- package/build/utils/rate-limiting.js +94 -0
- package/build/utils/tokenization.js +62 -0
- package/build/utils/validation.js +88 -0
- package/examples/README.md +48 -0
- package/examples/append-content.md +63 -0
- package/examples/complex-search.md +117 -0
- package/examples/find-in-file.md +94 -0
- package/examples/get-file-contents.md +72 -0
- package/examples/get-properties.md +89 -0
- package/examples/list-files-in-dir.md +55 -0
- package/examples/list-files-in-vault.md +53 -0
- package/examples/patch-content.md +60 -0
- package/examples/update-properties.md +126 -0
- package/package.json +12 -14
- package/src/index.ts +13 -3
- package/src/mcp/handlers.ts +183 -0
- package/src/mcp/index.ts +6 -0
- package/src/mcp/server.ts +162 -0
- package/src/mcp/types.ts +46 -0
- package/src/{obsidian.ts → obsidian/client.ts} +132 -125
- package/src/obsidian/errors.ts +105 -0
- package/src/obsidian/index.ts +6 -0
- package/src/obsidian/types.ts +124 -0
- package/src/resources/index.ts +17 -0
- package/src/{resources.ts → resources/tags.ts} +38 -6
- package/src/resources/types.ts +30 -0
- package/src/tools/base.ts +112 -0
- package/src/tools/files/content.ts +206 -0
- package/src/tools/files/index.ts +31 -0
- package/src/tools/files/list.ts +150 -0
- package/src/tools/index.ts +38 -0
- package/src/tools/properties/index.ts +21 -0
- package/src/{properties.ts → tools/properties/manager.ts} +41 -8
- package/src/{propertyTools.ts → tools/properties/tools.ts} +33 -7
- package/src/{propertyTypes.ts → tools/properties/types.ts} +29 -5
- package/src/tools/search/complex.ts +231 -0
- package/src/tools/search/index.ts +22 -0
- package/src/tools/search/simple.ts +147 -0
- package/src/utils/errors.ts +69 -0
- package/src/utils/index.ts +8 -0
- package/src/utils/logging.ts +146 -0
- package/src/utils/rate-limiting.ts +114 -0
- package/src/utils/tokenization.ts +71 -0
- package/src/utils/validation.ts +95 -0
- package/build/server.js +0 -263
- package/build/tools.js +0 -845
- package/build/types.js +0 -37
- package/src/server.ts +0 -335
- package/src/tools.ts +0 -926
- 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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
137
|
+
sanitizeHeader(value)
|
|
120
138
|
]));
|
|
121
139
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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=
|
|
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,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 "
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
127
|
+
logger.error("Failed to get tags:", error);
|
|
102
128
|
throw error;
|
|
103
129
|
}
|
|
104
130
|
}
|
|
105
131
|
}
|
|
106
|
-
//# sourceMappingURL=
|
|
132
|
+
//# sourceMappingURL=tags.js.map
|