obsidian-mcp-server 1.2.6 → 1.3.0
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 +54 -10
- package/build/obsidian.js +44 -12
- package/build/properties.js +13 -8
- package/build/propertyTypes.js +2 -2
- package/build/resources.js +4 -9
- package/build/server.js +29 -4
- package/build/tools.js +19 -37
- package/mcp-client-config.example.json +23 -0
- package/package.json +4 -3
- package/src/obsidian.ts +45 -12
- package/src/properties.ts +16 -8
- package/src/propertyTypes.ts +2 -2
- package/src/resources.ts +4 -10
- package/src/server.ts +31 -4
- package/src/tools.ts +21 -41
package/README.md
CHANGED
|
@@ -52,29 +52,73 @@ npm install obsidian-mcp-server
|
|
|
52
52
|
|
|
53
53
|
## Configuration
|
|
54
54
|
|
|
55
|
-
Add to your MCP client settings:
|
|
55
|
+
Add to your MCP client settings (e.g., `claude_desktop_config.json` or `cline_mcp_settings.json`):
|
|
56
56
|
|
|
57
57
|
```json
|
|
58
58
|
{
|
|
59
59
|
"mcpServers": {
|
|
60
|
-
"obsidian": {
|
|
60
|
+
"obsidian-mcp-server": {
|
|
61
61
|
"command": "node",
|
|
62
62
|
"args": ["/path/to/obsidian-mcp-server/build/index.js"],
|
|
63
63
|
"env": {
|
|
64
|
-
"OBSIDIAN_API_KEY": "
|
|
65
|
-
"
|
|
64
|
+
"OBSIDIAN_API_KEY": "your_api_key_here",
|
|
65
|
+
"VERIFY_SSL": "false",
|
|
66
|
+
"OBSIDIAN_PROTOCOL": "https",
|
|
67
|
+
"OBSIDIAN_HOST": "127.0.0.1",
|
|
68
|
+
"OBSIDIAN_PORT": "27124",
|
|
69
|
+
"REQUEST_TIMEOUT": "5000",
|
|
70
|
+
"MAX_CONTENT_LENGTH": "52428800",
|
|
71
|
+
"MAX_BODY_LENGTH": "52428800",
|
|
72
|
+
"RATE_LIMIT_WINDOW_MS": "900000",
|
|
73
|
+
"RATE_LIMIT_MAX_REQUESTS": "200",
|
|
74
|
+
"TOOL_TIMEOUT_MS": "60000"
|
|
66
75
|
}
|
|
67
76
|
}
|
|
68
77
|
}
|
|
69
78
|
}
|
|
70
79
|
```
|
|
71
80
|
|
|
72
|
-
Environment
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
- `
|
|
76
|
-
|
|
77
|
-
|
|
81
|
+
Environment Variables:
|
|
82
|
+
|
|
83
|
+
Required:
|
|
84
|
+
- `OBSIDIAN_API_KEY`: Your API key from Obsidian's Local REST API plugin settings
|
|
85
|
+
|
|
86
|
+
Connection Settings:
|
|
87
|
+
- `VERIFY_SSL`: Enable SSL certificate verification (default: false in development)
|
|
88
|
+
- `OBSIDIAN_PROTOCOL`: Protocol to use (default: "https")
|
|
89
|
+
- `OBSIDIAN_HOST`: Host address (default: "127.0.0.1")
|
|
90
|
+
- `OBSIDIAN_PORT`: Port number (default: 27124)
|
|
91
|
+
|
|
92
|
+
Request Limits:
|
|
93
|
+
- `REQUEST_TIMEOUT`: Request timeout in milliseconds (default: 5000)
|
|
94
|
+
- `MAX_CONTENT_LENGTH`: Maximum response content length in bytes (default: 52428800 [50MB])
|
|
95
|
+
- `MAX_BODY_LENGTH`: Maximum request body length in bytes (default: 52428800 [50MB])
|
|
96
|
+
|
|
97
|
+
Rate Limiting:
|
|
98
|
+
- `RATE_LIMIT_WINDOW_MS`: Rate limit window in milliseconds (default: 900000 [15 minutes])
|
|
99
|
+
- `RATE_LIMIT_MAX_REQUESTS`: Maximum requests per window (default: 200)
|
|
100
|
+
|
|
101
|
+
Tool Execution:
|
|
102
|
+
- `TOOL_TIMEOUT_MS`: Tool execution timeout in milliseconds (default: 60000 [1 minute])
|
|
103
|
+
|
|
104
|
+
SSL Certificate Setup:
|
|
105
|
+
|
|
106
|
+
For Windows Users:
|
|
107
|
+
1. Development Setup (Not Recommended for Production):
|
|
108
|
+
- Set `VERIFY_SSL` to "false"
|
|
109
|
+
- Set `OBSIDIAN_PROTOCOL` to "http"
|
|
110
|
+
- Enable "Non-encrypted (HTTP) Server" in Obsidian's Local REST API settings
|
|
111
|
+
|
|
112
|
+
2. Production Setup (Recommended):
|
|
113
|
+
- Set `VERIFY_SSL` to "true"
|
|
114
|
+
- Get the certificate from Obsidian Settings > Local REST API > 'How to Access'
|
|
115
|
+
- Open Windows Certificate Manager (certmgr.msc)
|
|
116
|
+
- Navigate to "Trusted Root Certification Authorities" > "Certificates"
|
|
117
|
+
- Right-click > "All Tasks" > "Import" and select the certificate file
|
|
118
|
+
|
|
119
|
+
For Other Systems:
|
|
120
|
+
- macOS: Add certificate to Keychain Access
|
|
121
|
+
- Linux: Add to ca-certificates
|
|
78
122
|
|
|
79
123
|
Additional configuration options:
|
|
80
124
|
```typescript
|
package/build/obsidian.js
CHANGED
|
@@ -7,15 +7,24 @@ import { dirname, join } from "path";
|
|
|
7
7
|
// Get package version for user agent
|
|
8
8
|
const __filename = fileURLToPath(import.meta.url);
|
|
9
9
|
const __dirname = dirname(__filename);
|
|
10
|
-
const packagePath = join(__dirname, '..', '..', 'package.json');
|
|
11
10
|
const VERSION = (() => {
|
|
12
11
|
try {
|
|
12
|
+
// Look for package.json in the same directory as the built files
|
|
13
|
+
const packagePath = join(__dirname, '..', 'package.json');
|
|
13
14
|
const pkg = JSON.parse(readFileSync(packagePath, 'utf-8'));
|
|
14
15
|
return pkg.version;
|
|
15
16
|
}
|
|
16
17
|
catch (error) {
|
|
17
|
-
|
|
18
|
-
|
|
18
|
+
// Try alternative location for development
|
|
19
|
+
try {
|
|
20
|
+
const devPackagePath = join(__dirname, '..', '..', 'package.json');
|
|
21
|
+
const pkg = JSON.parse(readFileSync(devPackagePath, 'utf-8'));
|
|
22
|
+
return pkg.version;
|
|
23
|
+
}
|
|
24
|
+
catch (devError) {
|
|
25
|
+
console.warn('Could not read package.json version, using fallback');
|
|
26
|
+
return '1.1.0'; // Fallback version
|
|
27
|
+
}
|
|
19
28
|
}
|
|
20
29
|
})();
|
|
21
30
|
export class ObsidianClient {
|
|
@@ -30,14 +39,28 @@ export class ObsidianClient {
|
|
|
30
39
|
"4. Provide the API key in your configuration", 40100 // Unauthorized
|
|
31
40
|
);
|
|
32
41
|
}
|
|
33
|
-
//
|
|
42
|
+
// Determine if we're in a development environment
|
|
43
|
+
const isDev = process.env.NODE_ENV === 'development' || !process.env.NODE_ENV;
|
|
44
|
+
// Read environment variables with fallbacks
|
|
45
|
+
const envConfig = {
|
|
46
|
+
protocol: process.env.OBSIDIAN_PROTOCOL || DEFAULT_OBSIDIAN_CONFIG.protocol,
|
|
47
|
+
host: process.env.OBSIDIAN_HOST || DEFAULT_OBSIDIAN_CONFIG.host,
|
|
48
|
+
port: parseInt(process.env.OBSIDIAN_PORT || String(DEFAULT_OBSIDIAN_CONFIG.port)),
|
|
49
|
+
verifySSL: process.env.VERIFY_SSL ? process.env.VERIFY_SSL === 'true' : (isDev ? false : true),
|
|
50
|
+
timeout: parseInt(process.env.REQUEST_TIMEOUT || '5000'),
|
|
51
|
+
maxContentLength: parseInt(process.env.MAX_CONTENT_LENGTH || String(50 * 1024 * 1024)),
|
|
52
|
+
maxBodyLength: parseInt(process.env.MAX_BODY_LENGTH || String(50 * 1024 * 1024))
|
|
53
|
+
};
|
|
54
|
+
// Combine defaults with provided config and environment variables
|
|
34
55
|
this.config = {
|
|
35
|
-
|
|
36
|
-
|
|
56
|
+
protocol: envConfig.protocol,
|
|
57
|
+
host: envConfig.host,
|
|
58
|
+
port: envConfig.port,
|
|
59
|
+
verifySSL: config.verifySSL ?? envConfig.verifySSL,
|
|
37
60
|
apiKey: config.apiKey,
|
|
38
|
-
timeout: config.timeout ??
|
|
39
|
-
maxContentLength: config.maxContentLength ??
|
|
40
|
-
maxBodyLength: config.maxBodyLength ??
|
|
61
|
+
timeout: config.timeout ?? envConfig.timeout,
|
|
62
|
+
maxContentLength: config.maxContentLength ?? envConfig.maxContentLength,
|
|
63
|
+
maxBodyLength: config.maxBodyLength ?? envConfig.maxBodyLength
|
|
41
64
|
};
|
|
42
65
|
// Configure HTTPS agent
|
|
43
66
|
const httpsAgent = new Agent({
|
|
@@ -66,9 +89,18 @@ export class ObsidianClient {
|
|
|
66
89
|
decompress: true
|
|
67
90
|
};
|
|
68
91
|
if (!this.config.verifySSL) {
|
|
69
|
-
console.warn("WARNING: SSL verification is disabled.
|
|
70
|
-
"
|
|
71
|
-
"
|
|
92
|
+
console.warn("WARNING: SSL verification is disabled. While this works for development, it's not recommended for production.\n" +
|
|
93
|
+
"To properly configure SSL certificates:\n" +
|
|
94
|
+
"1. Go to Obsidian Settings > Local REST API\n" +
|
|
95
|
+
"2. Under 'How to Access', copy the certificate\n" +
|
|
96
|
+
"3. For Windows users:\n" +
|
|
97
|
+
" - Open 'certmgr.msc' (Windows Certificate Manager)\n" +
|
|
98
|
+
" - Go to 'Trusted Root Certification Authorities' > 'Certificates'\n" +
|
|
99
|
+
" - Right-click > 'All Tasks' > 'Import' and follow the wizard\n" +
|
|
100
|
+
" - Select the certificate file you copied from Obsidian\n" +
|
|
101
|
+
"4. For other systems:\n" +
|
|
102
|
+
" - macOS: Add to Keychain Access\n" +
|
|
103
|
+
" - Linux: Add to ca-certificates");
|
|
72
104
|
}
|
|
73
105
|
this.client = axios.create(axiosConfig);
|
|
74
106
|
}
|
package/build/properties.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { parse, stringify } from 'yaml';
|
|
2
|
+
import { EOL } from 'os';
|
|
2
3
|
import { ObsidianPropertiesSchema, PropertyUpdateSchema } from './propertyTypes.js';
|
|
3
4
|
export class PropertyManager {
|
|
4
5
|
client;
|
|
@@ -10,18 +11,22 @@ export class PropertyManager {
|
|
|
10
11
|
*/
|
|
11
12
|
parseProperties(content) {
|
|
12
13
|
try {
|
|
13
|
-
// Extract frontmatter between --- markers
|
|
14
|
-
const match = content.match(
|
|
14
|
+
// Extract frontmatter between --- markers (handles both \n and \r\n)
|
|
15
|
+
const match = content.match(/^---(\r?\n)([\s\S]*?)\r?\n---/);
|
|
15
16
|
if (!match) {
|
|
16
17
|
return {};
|
|
17
18
|
}
|
|
18
|
-
const frontmatter = match[
|
|
19
|
+
const frontmatter = match[2];
|
|
19
20
|
const properties = parse(frontmatter);
|
|
21
|
+
// Handle tags - don't add # prefix in frontmatter
|
|
22
|
+
if (properties.tags && Array.isArray(properties.tags)) {
|
|
23
|
+
properties.tags = properties.tags.map((tag) => tag.startsWith('#') ? tag.substring(1) : tag);
|
|
24
|
+
}
|
|
20
25
|
// Validate against schema
|
|
21
26
|
const result = ObsidianPropertiesSchema.safeParse(properties);
|
|
22
27
|
if (!result.success) {
|
|
23
28
|
console.warn('Property validation warnings:', result.error);
|
|
24
|
-
// Return
|
|
29
|
+
// Return the properties with fixed tags
|
|
25
30
|
return properties;
|
|
26
31
|
}
|
|
27
32
|
return result.data;
|
|
@@ -38,9 +43,9 @@ export class PropertyManager {
|
|
|
38
43
|
try {
|
|
39
44
|
// Remove undefined values
|
|
40
45
|
const cleanProperties = Object.fromEntries(Object.entries(properties).filter(([_, v]) => v !== undefined));
|
|
41
|
-
// Generate YAML
|
|
46
|
+
// Generate YAML with platform-specific line endings
|
|
42
47
|
const yaml = stringify(cleanProperties);
|
|
43
|
-
return
|
|
48
|
+
return `---${EOL}${yaml}---${EOL}`;
|
|
44
49
|
}
|
|
45
50
|
catch (error) {
|
|
46
51
|
console.error('Error generating properties:', error);
|
|
@@ -134,8 +139,8 @@ export class PropertyManager {
|
|
|
134
139
|
const mergedProperties = this.mergeProperties(existingProperties, newProperties, replace);
|
|
135
140
|
// Generate new frontmatter
|
|
136
141
|
const newFrontmatter = this.generateProperties(mergedProperties);
|
|
137
|
-
// Replace existing frontmatter or prepend to file
|
|
138
|
-
const newContent = content.replace(/^---[\s\S]*?---\n/, '') || '';
|
|
142
|
+
// Replace existing frontmatter or prepend to file (handles both \n and \r\n)
|
|
143
|
+
const newContent = content.replace(/^---[\s\S]*?---\r?\n/, '') || '';
|
|
139
144
|
const updatedContent = newFrontmatter + newContent;
|
|
140
145
|
// Update file
|
|
141
146
|
await this.client.updateContent(filepath, updatedContent);
|
package/build/propertyTypes.js
CHANGED
|
@@ -18,7 +18,7 @@ export const ObsidianPropertiesSchema = z.object({
|
|
|
18
18
|
// Classification
|
|
19
19
|
type: z.array(PropertyType).optional(),
|
|
20
20
|
// Organization
|
|
21
|
-
tags: z.array(z.string()
|
|
21
|
+
tags: z.array(z.string()).optional(),
|
|
22
22
|
// Technical Metadata
|
|
23
23
|
status: z.array(StatusEnum).optional(),
|
|
24
24
|
version: z.string().optional(),
|
|
@@ -40,7 +40,7 @@ export const PropertyUpdateSchema = z.object({
|
|
|
40
40
|
// Classification
|
|
41
41
|
type: z.array(PropertyType).optional(),
|
|
42
42
|
// Organization
|
|
43
|
-
tags: z.array(z.string()
|
|
43
|
+
tags: z.array(z.string()).optional(),
|
|
44
44
|
// Technical Metadata
|
|
45
45
|
status: z.array(StatusEnum).optional(),
|
|
46
46
|
version: z.string().optional(),
|
package/build/resources.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { PropertyManager } from "./properties.js";
|
|
2
|
+
import { sep } from "path";
|
|
2
3
|
export class TagResource {
|
|
3
4
|
client;
|
|
4
|
-
static TAG_PATTERN = /#[a-zA-Z0-9_-]+/g;
|
|
5
5
|
tagCache = new Map();
|
|
6
6
|
propertyManager;
|
|
7
7
|
isInitialized = false;
|
|
@@ -22,9 +22,9 @@ export class TagResource {
|
|
|
22
22
|
}
|
|
23
23
|
async initializeCache() {
|
|
24
24
|
try {
|
|
25
|
-
// Get all markdown files
|
|
25
|
+
// Get all markdown files using platform-agnostic path pattern
|
|
26
26
|
const query = {
|
|
27
|
-
"glob": [
|
|
27
|
+
"glob": [`**${sep}*.md`.replace(/\\/g, '/'), { "var": "path" }]
|
|
28
28
|
};
|
|
29
29
|
const results = await this.client.searchJson(query);
|
|
30
30
|
this.tagCache.clear();
|
|
@@ -34,18 +34,13 @@ export class TagResource {
|
|
|
34
34
|
continue;
|
|
35
35
|
try {
|
|
36
36
|
const content = await this.client.getFileContents(result.filename);
|
|
37
|
-
//
|
|
37
|
+
// Only extract tags from frontmatter YAML
|
|
38
38
|
const properties = this.propertyManager.parseProperties(content);
|
|
39
39
|
if (properties.tags) {
|
|
40
40
|
properties.tags.forEach((tag) => {
|
|
41
41
|
this.addTag(tag, result.filename);
|
|
42
42
|
});
|
|
43
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
44
|
}
|
|
50
45
|
catch (error) {
|
|
51
46
|
console.error(`Failed to process file ${result.filename}:`, error);
|
package/build/server.js
CHANGED
|
@@ -46,10 +46,13 @@ const cleanupInterval = setInterval(() => {
|
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
48
|
}, 60000); // Clean up every minute
|
|
49
|
-
// Initialize Obsidian client
|
|
49
|
+
// Initialize Obsidian client with environment configuration
|
|
50
50
|
const client = new ObsidianClient({
|
|
51
51
|
apiKey: API_KEY,
|
|
52
|
-
verifySSL: process.env.
|
|
52
|
+
verifySSL: process.env.VERIFY_SSL === 'true',
|
|
53
|
+
timeout: parseInt(process.env.REQUEST_TIMEOUT || '5000'),
|
|
54
|
+
maxContentLength: parseInt(process.env.MAX_CONTENT_LENGTH || String(50 * 1024 * 1024)),
|
|
55
|
+
maxBodyLength: parseInt(process.env.MAX_BODY_LENGTH || String(50 * 1024 * 1024))
|
|
53
56
|
});
|
|
54
57
|
const toolHandlers = new Map();
|
|
55
58
|
const handlers = [
|
|
@@ -223,11 +226,33 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
223
226
|
server.onerror = (error) => {
|
|
224
227
|
console.error("[MCP Error]", error);
|
|
225
228
|
};
|
|
226
|
-
// Handle shutdown
|
|
227
|
-
|
|
229
|
+
// Handle shutdown gracefully across platforms
|
|
230
|
+
const cleanup = async () => {
|
|
231
|
+
console.error('Shutting down server...');
|
|
228
232
|
clearInterval(cleanupInterval); // Clean up rate limit interval
|
|
229
233
|
await server.close();
|
|
230
234
|
process.exit(0);
|
|
235
|
+
};
|
|
236
|
+
// Handle various termination signals
|
|
237
|
+
process.on('SIGINT', cleanup); // Ctrl+C on all platforms
|
|
238
|
+
process.on('SIGTERM', cleanup); // Termination request
|
|
239
|
+
if (process.platform === 'win32') {
|
|
240
|
+
// Windows-specific handling
|
|
241
|
+
process.on('SIGHUP', cleanup); // Terminal closed
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
// Unix-specific signals
|
|
245
|
+
process.on('SIGUSR1', cleanup);
|
|
246
|
+
process.on('SIGUSR2', cleanup);
|
|
247
|
+
}
|
|
248
|
+
// Handle uncaught errors
|
|
249
|
+
process.on('uncaughtException', async (error) => {
|
|
250
|
+
console.error('Uncaught exception:', error);
|
|
251
|
+
await cleanup();
|
|
252
|
+
});
|
|
253
|
+
process.on('unhandledRejection', async (error) => {
|
|
254
|
+
console.error('Unhandled rejection:', error);
|
|
255
|
+
await cleanup();
|
|
231
256
|
});
|
|
232
257
|
// Export the run function
|
|
233
258
|
export async function run() {
|
package/build/tools.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { encoding_for_model } from "tiktoken";
|
|
2
|
+
import { join } from "path";
|
|
2
3
|
import { ObsidianError } from "./types.js";
|
|
3
4
|
import { PropertyManager } from "./properties.js";
|
|
4
5
|
const TOOL_NAMES = {
|
|
@@ -528,7 +529,6 @@ export class ComplexSearchToolHandler extends BaseToolHandler {
|
|
|
528
529
|
}
|
|
529
530
|
}
|
|
530
531
|
export class GetTagsToolHandler extends BaseToolHandler {
|
|
531
|
-
static TAG_PATTERN = /#[a-zA-Z0-9_-]+/g;
|
|
532
532
|
propertyManager;
|
|
533
533
|
constructor(client) {
|
|
534
534
|
super(TOOL_NAMES.GET_TAGS, client);
|
|
@@ -555,7 +555,7 @@ export class GetTagsToolHandler extends BaseToolHandler {
|
|
|
555
555
|
response: {
|
|
556
556
|
"tags": [
|
|
557
557
|
{
|
|
558
|
-
"name": "
|
|
558
|
+
"name": "project",
|
|
559
559
|
"count": 15,
|
|
560
560
|
"files": [
|
|
561
561
|
"Projects/ProjectA.md",
|
|
@@ -583,50 +583,32 @@ export class GetTagsToolHandler extends BaseToolHandler {
|
|
|
583
583
|
}
|
|
584
584
|
};
|
|
585
585
|
}
|
|
586
|
-
async
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
const
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
586
|
+
async runTool(args) {
|
|
587
|
+
try {
|
|
588
|
+
const tagMap = new Map();
|
|
589
|
+
const basePath = args.path || '';
|
|
590
|
+
// Use searchJson to find files with tags in frontmatter
|
|
591
|
+
const query = args.path
|
|
592
|
+
? { "glob": [join(args.path, "**/*.md").replace(/\\/g, '/'), { "var": "path" }] }
|
|
593
|
+
: { "glob": ["**/*.md", { "var": "path" }] };
|
|
594
|
+
const results = await this.client.searchJson(query);
|
|
595
|
+
let scannedFiles = 0;
|
|
596
|
+
// Process each file
|
|
597
|
+
for (const result of results) {
|
|
598
|
+
if (!('filename' in result))
|
|
599
|
+
continue;
|
|
600
|
+
const content = await this.client.getFileContents(result.filename);
|
|
599
601
|
const properties = this.propertyManager.parseProperties(content);
|
|
600
602
|
if (properties.tags) {
|
|
603
|
+
scannedFiles++;
|
|
601
604
|
properties.tags.forEach((tag) => {
|
|
602
605
|
if (!tagMap.has(tag)) {
|
|
603
606
|
tagMap.set(tag, new Set());
|
|
604
607
|
}
|
|
605
|
-
tagMap.get(tag).add(
|
|
608
|
+
tagMap.get(tag).add(result.filename);
|
|
606
609
|
});
|
|
607
610
|
}
|
|
608
|
-
// Extract inline tags using regex
|
|
609
|
-
const inlineTags = content.match(GetTagsToolHandler.TAG_PATTERN) || [];
|
|
610
|
-
inlineTags.forEach(tag => {
|
|
611
|
-
if (!tagMap.has(tag)) {
|
|
612
|
-
tagMap.set(tag, new Set());
|
|
613
|
-
}
|
|
614
|
-
tagMap.get(tag).add(fullPath);
|
|
615
|
-
});
|
|
616
611
|
}
|
|
617
|
-
}
|
|
618
|
-
return scannedFiles;
|
|
619
|
-
}
|
|
620
|
-
async runTool(args) {
|
|
621
|
-
try {
|
|
622
|
-
const tagMap = new Map();
|
|
623
|
-
const basePath = args.path || '';
|
|
624
|
-
// Get files from vault or specific directory
|
|
625
|
-
const files = args.path
|
|
626
|
-
? await this.client.listFilesInDir(args.path)
|
|
627
|
-
: await this.client.listFilesInVault();
|
|
628
|
-
// Process files recursively
|
|
629
|
-
const scannedFiles = await this.processFiles(files, basePath, tagMap);
|
|
630
612
|
// Calculate total occurrences
|
|
631
613
|
const totalOccurrences = Array.from(tagMap.values())
|
|
632
614
|
.reduce((sum, files) => sum + files.size, 0);
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"mcpServers": {
|
|
3
|
+
"obsidian-mcp-server": {
|
|
4
|
+
"command": "node",
|
|
5
|
+
"args": [
|
|
6
|
+
"/path/to/obsidian-mcp-server/build/index.js"
|
|
7
|
+
],
|
|
8
|
+
"env": {
|
|
9
|
+
"OBSIDIAN_API_KEY": "your_api_key_here",
|
|
10
|
+
"VERIFY_SSL": "false",
|
|
11
|
+
"OBSIDIAN_PROTOCOL": "https",
|
|
12
|
+
"OBSIDIAN_HOST": "127.0.0.1",
|
|
13
|
+
"OBSIDIAN_PORT": "27124",
|
|
14
|
+
"REQUEST_TIMEOUT": "5000",
|
|
15
|
+
"MAX_CONTENT_LENGTH": "52428800",
|
|
16
|
+
"MAX_BODY_LENGTH": "52428800",
|
|
17
|
+
"RATE_LIMIT_WINDOW_MS": "900000",
|
|
18
|
+
"RATE_LIMIT_MAX_REQUESTS": "200",
|
|
19
|
+
"TOOL_TIMEOUT_MS": "60000"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "obsidian-mcp-server",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Model Context Protocol server for Obsidian integration with token-aware response handling",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -8,10 +8,11 @@
|
|
|
8
8
|
"node": ">=18.0.0"
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
11
|
-
"build": "tsc
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"postbuild": "node -e \"if (process.platform !== 'win32') require('fs').chmodSync('build/index.js', '755')\"",
|
|
12
13
|
"start": "node build/index.js",
|
|
13
14
|
"dev": "tsc -w",
|
|
14
|
-
"clean": "
|
|
15
|
+
"clean": "node -e \"require('fs').rmSync('build', { recursive: true, force: true })\"",
|
|
15
16
|
"rebuild": "npm run clean && npm run build",
|
|
16
17
|
"test": "echo \"No tests specified yet\" && exit 0",
|
|
17
18
|
"lint": "eslint . --ext .ts",
|
package/src/obsidian.ts
CHANGED
|
@@ -24,14 +24,22 @@ import { dirname, join } from "path";
|
|
|
24
24
|
// Get package version for user agent
|
|
25
25
|
const __filename = fileURLToPath(import.meta.url);
|
|
26
26
|
const __dirname = dirname(__filename);
|
|
27
|
-
const packagePath = join(__dirname, '..', '..', 'package.json');
|
|
28
27
|
const VERSION = (() => {
|
|
29
28
|
try {
|
|
29
|
+
// Look for package.json in the same directory as the built files
|
|
30
|
+
const packagePath = join(__dirname, '..', 'package.json');
|
|
30
31
|
const pkg = JSON.parse(readFileSync(packagePath, 'utf-8'));
|
|
31
32
|
return pkg.version;
|
|
32
33
|
} catch (error) {
|
|
33
|
-
|
|
34
|
-
|
|
34
|
+
// Try alternative location for development
|
|
35
|
+
try {
|
|
36
|
+
const devPackagePath = join(__dirname, '..', '..', 'package.json');
|
|
37
|
+
const pkg = JSON.parse(readFileSync(devPackagePath, 'utf-8'));
|
|
38
|
+
return pkg.version;
|
|
39
|
+
} catch (devError) {
|
|
40
|
+
console.warn('Could not read package.json version, using fallback');
|
|
41
|
+
return '1.1.0'; // Fallback version
|
|
42
|
+
}
|
|
35
43
|
}
|
|
36
44
|
})();
|
|
37
45
|
|
|
@@ -51,14 +59,30 @@ export class ObsidianClient {
|
|
|
51
59
|
);
|
|
52
60
|
}
|
|
53
61
|
|
|
54
|
-
//
|
|
62
|
+
// Determine if we're in a development environment
|
|
63
|
+
const isDev = process.env.NODE_ENV === 'development' || !process.env.NODE_ENV;
|
|
64
|
+
|
|
65
|
+
// Read environment variables with fallbacks
|
|
66
|
+
const envConfig = {
|
|
67
|
+
protocol: process.env.OBSIDIAN_PROTOCOL as "http" | "https" || DEFAULT_OBSIDIAN_CONFIG.protocol,
|
|
68
|
+
host: process.env.OBSIDIAN_HOST || DEFAULT_OBSIDIAN_CONFIG.host,
|
|
69
|
+
port: parseInt(process.env.OBSIDIAN_PORT || String(DEFAULT_OBSIDIAN_CONFIG.port)),
|
|
70
|
+
verifySSL: process.env.VERIFY_SSL ? process.env.VERIFY_SSL === 'true' : (isDev ? false : true),
|
|
71
|
+
timeout: parseInt(process.env.REQUEST_TIMEOUT || '5000'),
|
|
72
|
+
maxContentLength: parseInt(process.env.MAX_CONTENT_LENGTH || String(50 * 1024 * 1024)),
|
|
73
|
+
maxBodyLength: parseInt(process.env.MAX_BODY_LENGTH || String(50 * 1024 * 1024))
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// Combine defaults with provided config and environment variables
|
|
55
77
|
this.config = {
|
|
56
|
-
|
|
57
|
-
|
|
78
|
+
protocol: envConfig.protocol,
|
|
79
|
+
host: envConfig.host,
|
|
80
|
+
port: envConfig.port,
|
|
81
|
+
verifySSL: config.verifySSL ?? envConfig.verifySSL,
|
|
58
82
|
apiKey: config.apiKey,
|
|
59
|
-
timeout: config.timeout ??
|
|
60
|
-
maxContentLength: config.maxContentLength ??
|
|
61
|
-
maxBodyLength: config.maxBodyLength ??
|
|
83
|
+
timeout: config.timeout ?? envConfig.timeout,
|
|
84
|
+
maxContentLength: config.maxContentLength ?? envConfig.maxContentLength,
|
|
85
|
+
maxBodyLength: config.maxBodyLength ?? envConfig.maxBodyLength
|
|
62
86
|
};
|
|
63
87
|
|
|
64
88
|
// Configure HTTPS agent
|
|
@@ -91,9 +115,18 @@ export class ObsidianClient {
|
|
|
91
115
|
|
|
92
116
|
if (!this.config.verifySSL) {
|
|
93
117
|
console.warn(
|
|
94
|
-
"WARNING: SSL verification is disabled.
|
|
95
|
-
"
|
|
96
|
-
"
|
|
118
|
+
"WARNING: SSL verification is disabled. While this works for development, it's not recommended for production.\n" +
|
|
119
|
+
"To properly configure SSL certificates:\n" +
|
|
120
|
+
"1. Go to Obsidian Settings > Local REST API\n" +
|
|
121
|
+
"2. Under 'How to Access', copy the certificate\n" +
|
|
122
|
+
"3. For Windows users:\n" +
|
|
123
|
+
" - Open 'certmgr.msc' (Windows Certificate Manager)\n" +
|
|
124
|
+
" - Go to 'Trusted Root Certification Authorities' > 'Certificates'\n" +
|
|
125
|
+
" - Right-click > 'All Tasks' > 'Import' and follow the wizard\n" +
|
|
126
|
+
" - Select the certificate file you copied from Obsidian\n" +
|
|
127
|
+
"4. For other systems:\n" +
|
|
128
|
+
" - macOS: Add to Keychain Access\n" +
|
|
129
|
+
" - Linux: Add to ca-certificates"
|
|
97
130
|
);
|
|
98
131
|
}
|
|
99
132
|
|
package/src/properties.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { parse, stringify } from 'yaml';
|
|
2
2
|
import { ObsidianClient } from './obsidian.js';
|
|
3
|
+
import { EOL } from 'os';
|
|
3
4
|
import {
|
|
4
5
|
ObsidianProperties,
|
|
5
6
|
ObsidianPropertiesSchema,
|
|
@@ -16,20 +17,27 @@ export class PropertyManager {
|
|
|
16
17
|
*/
|
|
17
18
|
parseProperties(content: string): ObsidianProperties {
|
|
18
19
|
try {
|
|
19
|
-
// Extract frontmatter between --- markers
|
|
20
|
-
const match = content.match(
|
|
20
|
+
// Extract frontmatter between --- markers (handles both \n and \r\n)
|
|
21
|
+
const match = content.match(/^---(\r?\n)([\s\S]*?)\r?\n---/);
|
|
21
22
|
if (!match) {
|
|
22
23
|
return {};
|
|
23
24
|
}
|
|
24
25
|
|
|
25
|
-
const frontmatter = match[
|
|
26
|
+
const frontmatter = match[2];
|
|
26
27
|
const properties = parse(frontmatter);
|
|
27
28
|
|
|
29
|
+
// Handle tags - don't add # prefix in frontmatter
|
|
30
|
+
if (properties.tags && Array.isArray(properties.tags)) {
|
|
31
|
+
properties.tags = properties.tags.map((tag: string) =>
|
|
32
|
+
tag.startsWith('#') ? tag.substring(1) : tag
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
28
36
|
// Validate against schema
|
|
29
37
|
const result = ObsidianPropertiesSchema.safeParse(properties);
|
|
30
38
|
if (!result.success) {
|
|
31
39
|
console.warn('Property validation warnings:', result.error);
|
|
32
|
-
// Return
|
|
40
|
+
// Return the properties with fixed tags
|
|
33
41
|
return properties;
|
|
34
42
|
}
|
|
35
43
|
|
|
@@ -50,9 +58,9 @@ export class PropertyManager {
|
|
|
50
58
|
Object.entries(properties).filter(([_, v]) => v !== undefined)
|
|
51
59
|
);
|
|
52
60
|
|
|
53
|
-
// Generate YAML
|
|
61
|
+
// Generate YAML with platform-specific line endings
|
|
54
62
|
const yaml = stringify(cleanProperties);
|
|
55
|
-
return
|
|
63
|
+
return `---${EOL}${yaml}---${EOL}`;
|
|
56
64
|
} catch (error) {
|
|
57
65
|
console.error('Error generating properties:', error);
|
|
58
66
|
throw error;
|
|
@@ -169,8 +177,8 @@ export class PropertyManager {
|
|
|
169
177
|
// Generate new frontmatter
|
|
170
178
|
const newFrontmatter = this.generateProperties(mergedProperties);
|
|
171
179
|
|
|
172
|
-
// Replace existing frontmatter or prepend to file
|
|
173
|
-
const newContent = content.replace(/^---[\s\S]*?---\n/, '') || '';
|
|
180
|
+
// Replace existing frontmatter or prepend to file (handles both \n and \r\n)
|
|
181
|
+
const newContent = content.replace(/^---[\s\S]*?---\r?\n/, '') || '';
|
|
174
182
|
const updatedContent = newFrontmatter + newContent;
|
|
175
183
|
|
|
176
184
|
// Update file
|
package/src/propertyTypes.ts
CHANGED
|
@@ -23,7 +23,7 @@ export const ObsidianPropertiesSchema = z.object({
|
|
|
23
23
|
type: z.array(PropertyType).optional(),
|
|
24
24
|
|
|
25
25
|
// Organization
|
|
26
|
-
tags: z.array(z.string()
|
|
26
|
+
tags: z.array(z.string()).optional(),
|
|
27
27
|
|
|
28
28
|
// Technical Metadata
|
|
29
29
|
status: z.array(StatusEnum).optional(),
|
|
@@ -51,7 +51,7 @@ export const PropertyUpdateSchema = z.object({
|
|
|
51
51
|
type: z.array(PropertyType).optional(),
|
|
52
52
|
|
|
53
53
|
// Organization
|
|
54
|
-
tags: z.array(z.string()
|
|
54
|
+
tags: z.array(z.string()).optional(),
|
|
55
55
|
|
|
56
56
|
// Technical Metadata
|
|
57
57
|
status: z.array(StatusEnum).optional(),
|
package/src/resources.ts
CHANGED
|
@@ -2,9 +2,9 @@ import { Resource, TextContent } from "@modelcontextprotocol/sdk/types.js";
|
|
|
2
2
|
import { ObsidianClient } from "./obsidian.js";
|
|
3
3
|
import { TagResponse, ObsidianFile, JsonLogicQuery } from "./types.js";
|
|
4
4
|
import { PropertyManager } from "./properties.js";
|
|
5
|
+
import { join, sep } from "path";
|
|
5
6
|
|
|
6
7
|
export class TagResource {
|
|
7
|
-
private static readonly TAG_PATTERN = /#[a-zA-Z0-9_-]+/g;
|
|
8
8
|
private tagCache: Map<string, Set<string>> = new Map();
|
|
9
9
|
private propertyManager: PropertyManager;
|
|
10
10
|
private isInitialized = false;
|
|
@@ -27,9 +27,9 @@ export class TagResource {
|
|
|
27
27
|
|
|
28
28
|
private async initializeCache() {
|
|
29
29
|
try {
|
|
30
|
-
// Get all markdown files
|
|
30
|
+
// Get all markdown files using platform-agnostic path pattern
|
|
31
31
|
const query: JsonLogicQuery = {
|
|
32
|
-
"glob": [
|
|
32
|
+
"glob": [`**${sep}*.md`.replace(/\\/g, '/'), { "var": "path" }]
|
|
33
33
|
};
|
|
34
34
|
|
|
35
35
|
const results = await this.client.searchJson(query);
|
|
@@ -42,19 +42,13 @@ export class TagResource {
|
|
|
42
42
|
try {
|
|
43
43
|
const content = await this.client.getFileContents(result.filename);
|
|
44
44
|
|
|
45
|
-
//
|
|
45
|
+
// Only extract tags from frontmatter YAML
|
|
46
46
|
const properties = this.propertyManager.parseProperties(content);
|
|
47
47
|
if (properties.tags) {
|
|
48
48
|
properties.tags.forEach((tag: string) => {
|
|
49
49
|
this.addTag(tag, result.filename);
|
|
50
50
|
});
|
|
51
51
|
}
|
|
52
|
-
|
|
53
|
-
// Extract inline tags
|
|
54
|
-
const inlineTags = content.match(TagResource.TAG_PATTERN) || [];
|
|
55
|
-
inlineTags.forEach(tag => {
|
|
56
|
-
this.addTag(tag, result.filename);
|
|
57
|
-
});
|
|
58
52
|
} catch (error) {
|
|
59
53
|
console.error(`Failed to process file ${result.filename}:`, error);
|
|
60
54
|
}
|
package/src/server.ts
CHANGED
|
@@ -78,10 +78,13 @@ const cleanupInterval = setInterval(() => {
|
|
|
78
78
|
}
|
|
79
79
|
}, 60000); // Clean up every minute
|
|
80
80
|
|
|
81
|
-
// Initialize Obsidian client
|
|
81
|
+
// Initialize Obsidian client with environment configuration
|
|
82
82
|
const client = new ObsidianClient({
|
|
83
83
|
apiKey: API_KEY,
|
|
84
|
-
verifySSL: process.env.
|
|
84
|
+
verifySSL: process.env.VERIFY_SSL === 'true',
|
|
85
|
+
timeout: parseInt(process.env.REQUEST_TIMEOUT || '5000'),
|
|
86
|
+
maxContentLength: parseInt(process.env.MAX_CONTENT_LENGTH || String(50 * 1024 * 1024)),
|
|
87
|
+
maxBodyLength: parseInt(process.env.MAX_BODY_LENGTH || String(50 * 1024 * 1024))
|
|
85
88
|
});
|
|
86
89
|
|
|
87
90
|
// Initialize tool handlers
|
|
@@ -293,11 +296,35 @@ server.onerror = (error) => {
|
|
|
293
296
|
console.error("[MCP Error]", error);
|
|
294
297
|
};
|
|
295
298
|
|
|
296
|
-
// Handle shutdown
|
|
297
|
-
|
|
299
|
+
// Handle shutdown gracefully across platforms
|
|
300
|
+
const cleanup = async () => {
|
|
301
|
+
console.error('Shutting down server...');
|
|
298
302
|
clearInterval(cleanupInterval); // Clean up rate limit interval
|
|
299
303
|
await server.close();
|
|
300
304
|
process.exit(0);
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
// Handle various termination signals
|
|
308
|
+
process.on('SIGINT', cleanup); // Ctrl+C on all platforms
|
|
309
|
+
process.on('SIGTERM', cleanup); // Termination request
|
|
310
|
+
if (process.platform === 'win32') {
|
|
311
|
+
// Windows-specific handling
|
|
312
|
+
process.on('SIGHUP', cleanup); // Terminal closed
|
|
313
|
+
} else {
|
|
314
|
+
// Unix-specific signals
|
|
315
|
+
process.on('SIGUSR1', cleanup);
|
|
316
|
+
process.on('SIGUSR2', cleanup);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Handle uncaught errors
|
|
320
|
+
process.on('uncaughtException', async (error) => {
|
|
321
|
+
console.error('Uncaught exception:', error);
|
|
322
|
+
await cleanup();
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
process.on('unhandledRejection', async (error) => {
|
|
326
|
+
console.error('Unhandled rejection:', error);
|
|
327
|
+
await cleanup();
|
|
301
328
|
});
|
|
302
329
|
|
|
303
330
|
// Export the run function
|
package/src/tools.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { Tool, TextContent } from "@modelcontextprotocol/sdk/types.js";
|
|
2
2
|
import { ObsidianClient } from "./obsidian.js";
|
|
3
3
|
import { encoding_for_model } from "tiktoken";
|
|
4
|
+
import { join } from "path";
|
|
5
|
+
import { EOL } from "os";
|
|
4
6
|
import {
|
|
5
7
|
ToolHandler,
|
|
6
8
|
PatchContentArgs,
|
|
@@ -589,7 +591,6 @@ export class ComplexSearchToolHandler extends BaseToolHandler<ComplexSearchArgs>
|
|
|
589
591
|
}
|
|
590
592
|
|
|
591
593
|
export class GetTagsToolHandler extends BaseToolHandler<GetTagsArgs> {
|
|
592
|
-
private static readonly TAG_PATTERN = /#[a-zA-Z0-9_-]+/g;
|
|
593
594
|
private propertyManager: PropertyManager;
|
|
594
595
|
|
|
595
596
|
constructor(client: ObsidianClient) {
|
|
@@ -618,7 +619,7 @@ export class GetTagsToolHandler extends BaseToolHandler<GetTagsArgs> {
|
|
|
618
619
|
response: {
|
|
619
620
|
"tags": [
|
|
620
621
|
{
|
|
621
|
-
"name": "
|
|
622
|
+
"name": "project",
|
|
622
623
|
"count": 15,
|
|
623
624
|
"files": [
|
|
624
625
|
"Projects/ProjectA.md",
|
|
@@ -647,57 +648,36 @@ export class GetTagsToolHandler extends BaseToolHandler<GetTagsArgs> {
|
|
|
647
648
|
};
|
|
648
649
|
}
|
|
649
650
|
|
|
650
|
-
|
|
651
|
-
|
|
651
|
+
async runTool(args: GetTagsArgs): Promise<Array<TextContent>> {
|
|
652
|
+
try {
|
|
653
|
+
const tagMap = new Map<string, Set<string>>();
|
|
654
|
+
const basePath = args.path || '';
|
|
655
|
+
|
|
656
|
+
// Use searchJson to find files with tags in frontmatter
|
|
657
|
+
const query: JsonLogicQuery = args.path
|
|
658
|
+
? { "glob": [join(args.path, "**/*.md").replace(/\\/g, '/'), { "var": "path" }] }
|
|
659
|
+
: { "glob": ["**/*.md", { "var": "path" }] };
|
|
652
660
|
|
|
653
|
-
|
|
654
|
-
|
|
661
|
+
const results = await this.client.searchJson(query);
|
|
662
|
+
let scannedFiles = 0;
|
|
655
663
|
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
} else if (file.type === "file" && file.path.endsWith('.md')) {
|
|
660
|
-
// Process markdown files
|
|
661
|
-
scannedFiles++;
|
|
662
|
-
const content = await this.client.getFileContents(fullPath);
|
|
664
|
+
// Process each file
|
|
665
|
+
for (const result of results) {
|
|
666
|
+
if (!('filename' in result)) continue;
|
|
663
667
|
|
|
664
|
-
|
|
668
|
+
const content = await this.client.getFileContents(result.filename);
|
|
665
669
|
const properties = this.propertyManager.parseProperties(content);
|
|
670
|
+
|
|
666
671
|
if (properties.tags) {
|
|
672
|
+
scannedFiles++;
|
|
667
673
|
properties.tags.forEach((tag: string) => {
|
|
668
674
|
if (!tagMap.has(tag)) {
|
|
669
675
|
tagMap.set(tag, new Set());
|
|
670
676
|
}
|
|
671
|
-
tagMap.get(tag)!.add(
|
|
677
|
+
tagMap.get(tag)!.add(result.filename);
|
|
672
678
|
});
|
|
673
679
|
}
|
|
674
|
-
|
|
675
|
-
// Extract inline tags using regex
|
|
676
|
-
const inlineTags = content.match(GetTagsToolHandler.TAG_PATTERN) || [];
|
|
677
|
-
inlineTags.forEach(tag => {
|
|
678
|
-
if (!tagMap.has(tag)) {
|
|
679
|
-
tagMap.set(tag, new Set());
|
|
680
|
-
}
|
|
681
|
-
tagMap.get(tag)!.add(fullPath);
|
|
682
|
-
});
|
|
683
680
|
}
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
return scannedFiles;
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
async runTool(args: GetTagsArgs): Promise<Array<TextContent>> {
|
|
690
|
-
try {
|
|
691
|
-
const tagMap = new Map<string, Set<string>>();
|
|
692
|
-
const basePath = args.path || '';
|
|
693
|
-
|
|
694
|
-
// Get files from vault or specific directory
|
|
695
|
-
const files = args.path
|
|
696
|
-
? await this.client.listFilesInDir(args.path)
|
|
697
|
-
: await this.client.listFilesInVault();
|
|
698
|
-
|
|
699
|
-
// Process files recursively
|
|
700
|
-
const scannedFiles = await this.processFiles(files, basePath, tagMap);
|
|
701
681
|
|
|
702
682
|
// Calculate total occurrences
|
|
703
683
|
const totalOccurrences = Array.from(tagMap.values())
|