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.
@@ -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 request handlers
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}`, 404);
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
- 429
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`, 408));
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
- 400
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.code === 204) {
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
- 500,
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
- 500,
285
+ 50000, // 50000 = Internal server error
261
286
  { error }
262
287
  );
263
288
  }