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.
- package/README.md +75 -5
- package/build/obsidian.js +181 -32
- package/build/properties.js +7 -7
- package/build/propertyTools.js +11 -21
- package/build/propertyTypes.js +4 -13
- package/build/resources.js +111 -0
- package/build/server.js +36 -12
- package/build/tools.js +425 -48
- package/build/types.js +21 -4
- package/package.json +3 -3
- package/src/obsidian.ts +260 -38
- package/src/properties.ts +9 -7
- package/src/propertyTools.ts +13 -21
- package/src/propertyTypes.ts +5 -13
- package/src/resources.ts +123 -0
- package/src/server.ts +39 -14
- package/src/tools.ts +461 -49
- package/src/types.ts +89 -4
|
@@ -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 {
|
|
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
|
|
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}`,
|
|
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.`,
|
|
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`,
|
|
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(', ')}`,
|
|
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.
|
|
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}`,
|
|
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",
|
|
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
|