obsidian-mcp-server 1.2.2 → 1.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +76 -6
- package/build/obsidian.js +185 -16
- package/build/properties.js +12 -11
- package/build/propertyTools.js +11 -22
- package/build/propertyTypes.js +28 -14
- package/build/resources.js +69 -0
- package/build/server.js +36 -12
- package/build/tools.js +424 -48
- package/build/types.js +20 -3
- package/package.json +3 -3
- package/src/obsidian.ts +244 -21
- package/src/properties.ts +19 -15
- package/src/propertyTools.ts +13 -22
- package/src/propertyTypes.ts +36 -14
- package/src/resources.ts +76 -0
- package/src/server.ts +39 -14
- package/src/tools.ts +460 -49
- package/src/types.ts +87 -3
package/src/resources.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { Resource, TextContent } from "@modelcontextprotocol/sdk/types.js";
|
|
2
|
+
import { ObsidianClient } from "./obsidian.js";
|
|
3
|
+
import { TagResponse, SearchMatch, SimpleSearchResult, SearchResponse } from "./types.js";
|
|
4
|
+
|
|
5
|
+
export class TagResource {
|
|
6
|
+
private static readonly TAG_PATTERN = /#[a-zA-Z0-9_-]+/g;
|
|
7
|
+
|
|
8
|
+
constructor(private client: ObsidianClient) {}
|
|
9
|
+
|
|
10
|
+
getResourceDescription(): Resource {
|
|
11
|
+
return {
|
|
12
|
+
uri: "obsidian://tags",
|
|
13
|
+
name: "Obsidian Tags",
|
|
14
|
+
description: "List of all tags used across the Obsidian vault with their usage counts",
|
|
15
|
+
mimeType: "application/json"
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async getContent(): Promise<TextContent[]> {
|
|
20
|
+
try {
|
|
21
|
+
// Search for files containing #
|
|
22
|
+
const query = {
|
|
23
|
+
"contains": [{ "var": "content" }, "#"]
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const results = await this.client.searchJson(query);
|
|
27
|
+
|
|
28
|
+
// Process results to extract tags
|
|
29
|
+
const tagMap = new Map<string, Set<string>>();
|
|
30
|
+
let totalOccurrences = 0;
|
|
31
|
+
let scannedFiles = 0;
|
|
32
|
+
|
|
33
|
+
results.forEach((result: SearchResponse) => {
|
|
34
|
+
scannedFiles++;
|
|
35
|
+
if ('matches' in result) {
|
|
36
|
+
result.matches.forEach((match: SearchMatch) => {
|
|
37
|
+
const tags = match.context.match(TagResource.TAG_PATTERN);
|
|
38
|
+
if (tags) {
|
|
39
|
+
tags.forEach((tag: string) => {
|
|
40
|
+
if (!tagMap.has(tag)) {
|
|
41
|
+
tagMap.set(tag, new Set());
|
|
42
|
+
}
|
|
43
|
+
tagMap.get(tag)!.add(result.filename);
|
|
44
|
+
totalOccurrences++;
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// Convert to sorted response format
|
|
52
|
+
const response: TagResponse = {
|
|
53
|
+
tags: Array.from(tagMap.entries())
|
|
54
|
+
.map(([name, files]) => ({
|
|
55
|
+
name,
|
|
56
|
+
count: files.size,
|
|
57
|
+
files: Array.from(files).sort()
|
|
58
|
+
}))
|
|
59
|
+
.sort((a, b) => b.count - a.count || a.name.localeCompare(b.name)),
|
|
60
|
+
metadata: {
|
|
61
|
+
totalOccurrences,
|
|
62
|
+
uniqueTags: tagMap.size,
|
|
63
|
+
scannedFiles
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
return [{
|
|
68
|
+
type: "text",
|
|
69
|
+
text: JSON.stringify(response, null, 2)
|
|
70
|
+
}];
|
|
71
|
+
} catch (error) {
|
|
72
|
+
console.error("Failed to fetch tags:", error);
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
package/src/server.ts
CHANGED
|
@@ -7,11 +7,14 @@ import {
|
|
|
7
7
|
ImageContent,
|
|
8
8
|
EmbeddedResource,
|
|
9
9
|
ListToolsRequestSchema,
|
|
10
|
-
CallToolRequestSchema
|
|
10
|
+
CallToolRequestSchema,
|
|
11
|
+
ListResourcesRequestSchema,
|
|
12
|
+
ReadResourceRequestSchema
|
|
11
13
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
12
14
|
import { ObsidianClient } from "./obsidian.js";
|
|
13
15
|
import { ObsidianError, DEFAULT_RATE_LIMIT_CONFIG, RateLimitConfig } from "./types.js";
|
|
14
16
|
import type { ToolHandler } from "./types.js";
|
|
17
|
+
import { TagResource } from "./resources.js";
|
|
15
18
|
import {
|
|
16
19
|
ListFilesInVaultToolHandler,
|
|
17
20
|
ListFilesInDirToolHandler,
|
|
@@ -19,7 +22,8 @@ import {
|
|
|
19
22
|
FindInFileToolHandler,
|
|
20
23
|
AppendContentToolHandler,
|
|
21
24
|
PatchContentToolHandler,
|
|
22
|
-
ComplexSearchToolHandler
|
|
25
|
+
ComplexSearchToolHandler,
|
|
26
|
+
GetTagsToolHandler
|
|
23
27
|
} from "./tools.js";
|
|
24
28
|
import {
|
|
25
29
|
GetPropertiesToolHandler,
|
|
@@ -75,7 +79,7 @@ const cleanupInterval = setInterval(() => {
|
|
|
75
79
|
}, 60000); // Clean up every minute
|
|
76
80
|
|
|
77
81
|
// Initialize Obsidian client
|
|
78
|
-
const client = new ObsidianClient({
|
|
82
|
+
const client = new ObsidianClient({
|
|
79
83
|
apiKey: API_KEY,
|
|
80
84
|
verifySSL: process.env.NODE_ENV === 'production' // Enable SSL verification in production
|
|
81
85
|
});
|
|
@@ -83,7 +87,6 @@ const client = new ObsidianClient({
|
|
|
83
87
|
// Initialize tool handlers
|
|
84
88
|
type AnyToolHandler = ToolHandler<any>;
|
|
85
89
|
const toolHandlers = new Map<string, AnyToolHandler>();
|
|
86
|
-
|
|
87
90
|
const handlers: AnyToolHandler[] = [
|
|
88
91
|
new ListFilesInVaultToolHandler(client),
|
|
89
92
|
new ListFilesInDirToolHandler(client),
|
|
@@ -93,11 +96,15 @@ const handlers: AnyToolHandler[] = [
|
|
|
93
96
|
new PatchContentToolHandler(client),
|
|
94
97
|
new ComplexSearchToolHandler(client),
|
|
95
98
|
new GetPropertiesToolHandler(client),
|
|
96
|
-
new UpdatePropertiesToolHandler(client)
|
|
99
|
+
new UpdatePropertiesToolHandler(client),
|
|
100
|
+
new GetTagsToolHandler(client)
|
|
97
101
|
];
|
|
98
102
|
|
|
99
103
|
handlers.forEach(handler => toolHandlers.set(handler.name, handler));
|
|
100
104
|
|
|
105
|
+
// Initialize resources
|
|
106
|
+
const tagResource = new TagResource(client);
|
|
107
|
+
|
|
101
108
|
// Create MCP server
|
|
102
109
|
const server = new Server(
|
|
103
110
|
{
|
|
@@ -107,12 +114,30 @@ const server = new Server(
|
|
|
107
114
|
{
|
|
108
115
|
capabilities: {
|
|
109
116
|
tools: {},
|
|
110
|
-
resources: {
|
|
117
|
+
resources: {
|
|
118
|
+
[tagResource.getResourceDescription().uri]: tagResource
|
|
119
|
+
}
|
|
111
120
|
}
|
|
112
121
|
}
|
|
113
122
|
);
|
|
114
123
|
|
|
115
|
-
// Set up
|
|
124
|
+
// Set up resource handlers
|
|
125
|
+
server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
126
|
+
return {
|
|
127
|
+
resources: [tagResource.getResourceDescription()]
|
|
128
|
+
};
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
132
|
+
if (request.params.uri === tagResource.getResourceDescription().uri) {
|
|
133
|
+
return {
|
|
134
|
+
contents: await tagResource.getContent()
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
throw new ObsidianError(`Resource not found: ${request.params.uri}`, 40400); // 40400 = Not found
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// Set up tool handlers
|
|
116
141
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
117
142
|
const tools: Tool[] = [];
|
|
118
143
|
for (const handler of toolHandlers.values()) {
|
|
@@ -188,14 +213,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
188
213
|
|
|
189
214
|
const handler = toolHandlers.get(name);
|
|
190
215
|
if (!handler) {
|
|
191
|
-
throw new ObsidianError(`Unknown tool: ${name}`,
|
|
216
|
+
throw new ObsidianError(`Unknown tool: ${name}`, 40400); // 40400 = Not found
|
|
192
217
|
}
|
|
193
218
|
|
|
194
219
|
// Check rate limit
|
|
195
220
|
if (!checkRateLimit(name)) {
|
|
196
221
|
throw new ObsidianError(
|
|
197
222
|
`Rate limit exceeded for tool: ${name}. Please try again later.`,
|
|
198
|
-
|
|
223
|
+
42900 // 42900 = Rate limit exceeded
|
|
199
224
|
);
|
|
200
225
|
}
|
|
201
226
|
|
|
@@ -203,7 +228,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
203
228
|
const timeoutMs = parseInt(process.env.TOOL_TIMEOUT_MS ?? '60000'); // 60 second default timeout
|
|
204
229
|
const timeoutPromise = new Promise((_, reject) => {
|
|
205
230
|
setTimeout(() => {
|
|
206
|
-
reject(new ObsidianError(`Tool execution timed out after ${timeoutMs}ms`,
|
|
231
|
+
reject(new ObsidianError(`Tool execution timed out after ${timeoutMs}ms`, 40800)); // 40800 = Request timeout
|
|
207
232
|
}, timeoutMs);
|
|
208
233
|
});
|
|
209
234
|
|
|
@@ -214,7 +239,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
214
239
|
if (!validationResult.valid) {
|
|
215
240
|
throw new ObsidianError(
|
|
216
241
|
`Invalid tool arguments: ${validationResult.errors.join(', ')}`,
|
|
217
|
-
|
|
242
|
+
40000 // 40000 = Bad request
|
|
218
243
|
);
|
|
219
244
|
}
|
|
220
245
|
|
|
@@ -227,7 +252,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
227
252
|
} catch (error) {
|
|
228
253
|
if (error instanceof ObsidianError) {
|
|
229
254
|
// Check if the operation actually succeeded despite the error
|
|
230
|
-
if (error.
|
|
255
|
+
if (error.errorCode === 20400) { // 20400 = Success with no content
|
|
231
256
|
return {
|
|
232
257
|
content: [{
|
|
233
258
|
type: "text",
|
|
@@ -250,14 +275,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
250
275
|
if (error instanceof Error) {
|
|
251
276
|
throw new ObsidianError(
|
|
252
277
|
`Tool '${name}' execution failed: ${error.message}`,
|
|
253
|
-
|
|
278
|
+
50000, // 50000 = Internal server error
|
|
254
279
|
{ originalError: error.stack }
|
|
255
280
|
);
|
|
256
281
|
}
|
|
257
282
|
|
|
258
283
|
throw new ObsidianError(
|
|
259
284
|
"Tool execution failed with unknown error",
|
|
260
|
-
|
|
285
|
+
50000, // 50000 = Internal server error
|
|
261
286
|
{ error }
|
|
262
287
|
);
|
|
263
288
|
}
|