obsidian-mcp-server 1.2.3 → 1.2.5

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,111 @@
1
+ import { PropertyManager } from "./properties.js";
2
+ export class TagResource {
3
+ client;
4
+ static TAG_PATTERN = /#[a-zA-Z0-9_-]+/g;
5
+ tagCache = new Map();
6
+ propertyManager;
7
+ isInitialized = false;
8
+ lastUpdate = 0;
9
+ updateInterval = 5000; // 5 seconds
10
+ constructor(client) {
11
+ this.client = client;
12
+ this.propertyManager = new PropertyManager(client);
13
+ this.initializeCache();
14
+ }
15
+ getResourceDescription() {
16
+ return {
17
+ uri: "obsidian://tags",
18
+ name: "Obsidian Tags",
19
+ description: "List of all tags used across the Obsidian vault with their usage counts",
20
+ mimeType: "application/json"
21
+ };
22
+ }
23
+ async initializeCache() {
24
+ try {
25
+ // Get all markdown files
26
+ const query = {
27
+ "glob": ["**/*.md", { "var": "path" }]
28
+ };
29
+ const results = await this.client.searchJson(query);
30
+ this.tagCache.clear();
31
+ // Process each file
32
+ for (const result of results) {
33
+ if (!('filename' in result))
34
+ continue;
35
+ try {
36
+ const content = await this.client.getFileContents(result.filename);
37
+ // Extract tags from frontmatter
38
+ const properties = this.propertyManager.parseProperties(content);
39
+ if (properties.tags) {
40
+ properties.tags.forEach((tag) => {
41
+ this.addTag(tag, result.filename);
42
+ });
43
+ }
44
+ // Extract inline tags
45
+ const inlineTags = content.match(TagResource.TAG_PATTERN) || [];
46
+ inlineTags.forEach(tag => {
47
+ this.addTag(tag, result.filename);
48
+ });
49
+ }
50
+ catch (error) {
51
+ console.error(`Failed to process file ${result.filename}:`, error);
52
+ }
53
+ }
54
+ this.isInitialized = true;
55
+ this.lastUpdate = Date.now();
56
+ }
57
+ catch (error) {
58
+ console.error("Failed to initialize tag cache:", error);
59
+ throw error;
60
+ }
61
+ }
62
+ addTag(tag, filepath) {
63
+ if (!this.tagCache.has(tag)) {
64
+ this.tagCache.set(tag, new Set());
65
+ }
66
+ this.tagCache.get(tag).add(filepath);
67
+ }
68
+ async updateCacheIfNeeded() {
69
+ const now = Date.now();
70
+ if (now - this.lastUpdate > this.updateInterval) {
71
+ await this.initializeCache();
72
+ }
73
+ }
74
+ async getContent() {
75
+ try {
76
+ if (!this.isInitialized) {
77
+ await this.initializeCache();
78
+ }
79
+ else {
80
+ await this.updateCacheIfNeeded();
81
+ }
82
+ const response = {
83
+ tags: Array.from(this.tagCache.entries())
84
+ .map(([name, files]) => ({
85
+ name,
86
+ count: files.size,
87
+ files: Array.from(files).sort()
88
+ }))
89
+ .sort((a, b) => b.count - a.count || a.name.localeCompare(b.name)),
90
+ metadata: {
91
+ totalOccurrences: Array.from(this.tagCache.values())
92
+ .reduce((sum, files) => sum + files.size, 0),
93
+ uniqueTags: this.tagCache.size,
94
+ scannedFiles: new Set(Array.from(this.tagCache.values())
95
+ .flatMap(files => Array.from(files))).size,
96
+ lastUpdate: this.lastUpdate
97
+ }
98
+ };
99
+ return [{
100
+ type: "text",
101
+ text: JSON.stringify(response, null, 2),
102
+ uri: this.getResourceDescription().uri
103
+ }];
104
+ }
105
+ catch (error) {
106
+ console.error("Failed to get tags:", error);
107
+ throw error;
108
+ }
109
+ }
110
+ }
111
+ //# sourceMappingURL=resources.js.map
package/build/server.js CHANGED
@@ -1,10 +1,11 @@
1
1
  import { config } from "dotenv";
2
2
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
- import { ListToolsRequestSchema, CallToolRequestSchema } from "@modelcontextprotocol/sdk/types.js";
4
+ import { ListToolsRequestSchema, CallToolRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema } from "@modelcontextprotocol/sdk/types.js";
5
5
  import { ObsidianClient } from "./obsidian.js";
6
6
  import { ObsidianError, DEFAULT_RATE_LIMIT_CONFIG } from "./types.js";
7
- import { ListFilesInVaultToolHandler, ListFilesInDirToolHandler, GetFileContentsToolHandler, FindInFileToolHandler, AppendContentToolHandler, PatchContentToolHandler, ComplexSearchToolHandler } from "./tools.js";
7
+ import { TagResource } from "./resources.js";
8
+ import { ListFilesInVaultToolHandler, ListFilesInDirToolHandler, GetFileContentsToolHandler, FindInFileToolHandler, AppendContentToolHandler, PatchContentToolHandler, ComplexSearchToolHandler, GetTagsToolHandler } from "./tools.js";
8
9
  import { GetPropertiesToolHandler, UpdatePropertiesToolHandler } from "./propertyTools.js";
9
10
  // Load environment variables
10
11
  config();
@@ -60,9 +61,12 @@ const handlers = [
60
61
  new PatchContentToolHandler(client),
61
62
  new ComplexSearchToolHandler(client),
62
63
  new GetPropertiesToolHandler(client),
63
- new UpdatePropertiesToolHandler(client)
64
+ new UpdatePropertiesToolHandler(client),
65
+ new GetTagsToolHandler(client)
64
66
  ];
65
67
  handlers.forEach(handler => toolHandlers.set(handler.name, handler));
68
+ // Initialize resources
69
+ const tagResource = new TagResource(client);
66
70
  // Create MCP server
67
71
  const server = new Server({
68
72
  name: "obsidian-mcp-server",
@@ -70,10 +74,26 @@ const server = new Server({
70
74
  }, {
71
75
  capabilities: {
72
76
  tools: {},
73
- resources: {}
77
+ resources: {
78
+ [tagResource.getResourceDescription().uri]: tagResource
79
+ }
80
+ }
81
+ });
82
+ // Set up resource handlers
83
+ server.setRequestHandler(ListResourcesRequestSchema, async () => {
84
+ return {
85
+ resources: [tagResource.getResourceDescription()]
86
+ };
87
+ });
88
+ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
89
+ if (request.params.uri === tagResource.getResourceDescription().uri) {
90
+ return {
91
+ contents: await tagResource.getContent()
92
+ };
74
93
  }
94
+ throw new ObsidianError(`Resource not found: ${request.params.uri}`, 40400); // 40400 = Not found
75
95
  });
76
- // Set up request handlers
96
+ // Set up tool handlers
77
97
  server.setRequestHandler(ListToolsRequestSchema, async () => {
78
98
  const tools = [];
79
99
  for (const handler of toolHandlers.values()) {
@@ -141,17 +161,18 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
141
161
  const { name, arguments: args } = request.params;
142
162
  const handler = toolHandlers.get(name);
143
163
  if (!handler) {
144
- throw new ObsidianError(`Unknown tool: ${name}`, 404);
164
+ throw new ObsidianError(`Unknown tool: ${name}`, 40400); // 40400 = Not found
145
165
  }
146
166
  // Check rate limit
147
167
  if (!checkRateLimit(name)) {
148
- throw new ObsidianError(`Rate limit exceeded for tool: ${name}. Please try again later.`, 429);
168
+ throw new ObsidianError(`Rate limit exceeded for tool: ${name}. Please try again later.`, 42900 // 42900 = Rate limit exceeded
169
+ );
149
170
  }
150
171
  // Add timeout handling
151
172
  const timeoutMs = parseInt(process.env.TOOL_TIMEOUT_MS ?? '60000'); // 60 second default timeout
152
173
  const timeoutPromise = new Promise((_, reject) => {
153
174
  setTimeout(() => {
154
- reject(new ObsidianError(`Tool execution timed out after ${timeoutMs}ms`, 408));
175
+ reject(new ObsidianError(`Tool execution timed out after ${timeoutMs}ms`, 40800)); // 40800 = Request timeout
155
176
  }, timeoutMs);
156
177
  });
157
178
  try {
@@ -159,7 +180,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
159
180
  const toolDescription = handler.getToolDescription();
160
181
  const validationResult = validateToolArguments(args, toolDescription.inputSchema);
161
182
  if (!validationResult.valid) {
162
- throw new ObsidianError(`Invalid tool arguments: ${validationResult.errors.join(', ')}`, 400);
183
+ throw new ObsidianError(`Invalid tool arguments: ${validationResult.errors.join(', ')}`, 40000 // 40000 = Bad request
184
+ );
163
185
  }
164
186
  // Race between tool execution and timeout
165
187
  const content = await Promise.race([
@@ -171,7 +193,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
171
193
  catch (error) {
172
194
  if (error instanceof ObsidianError) {
173
195
  // Check if the operation actually succeeded despite the error
174
- if (error.code === 204) {
196
+ if (error.errorCode === 20400) { // 20400 = Success with no content
175
197
  return {
176
198
  content: [{
177
199
  type: "text",
@@ -190,9 +212,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
190
212
  args
191
213
  });
192
214
  if (error instanceof Error) {
193
- throw new ObsidianError(`Tool '${name}' execution failed: ${error.message}`, 500, { originalError: error.stack });
215
+ throw new ObsidianError(`Tool '${name}' execution failed: ${error.message}`, 50000, // 50000 = Internal server error
216
+ { originalError: error.stack });
194
217
  }
195
- throw new ObsidianError("Tool execution failed with unknown error", 500, { error });
218
+ throw new ObsidianError("Tool execution failed with unknown error", 50000, // 50000 = Internal server error
219
+ { error });
196
220
  }
197
221
  });
198
222
  // Error handler