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 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": "your-api-key-here",
65
- "NODE_ENV": "production"
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 configuration:
73
- - `OBSIDIAN_VERIFY_SSL`: Enable SSL verification (default: false)
74
- - `RATE_LIMIT_WINDOW_MS`: Rate limit window in ms (default: 15 minutes)
75
- - `RATE_LIMIT_MAX_REQUESTS`: Max requests per window (default: 200)
76
- - `MAX_TOKENS`: Maximum tokens per response (default: 20000)
77
- - `TOOL_TIMEOUT_MS`: Tool execution timeout (default: 60000)
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
- console.warn('Could not read package.json version:', error);
18
- return '1.1.0'; // Fallback version
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
- // Combine defaults with provided config
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
- ...DEFAULT_OBSIDIAN_CONFIG,
36
- verifySSL: config.verifySSL ?? true, // Default to true as required by Obsidian REST API plugin
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 ?? 5000, // 5 second default timeout
39
- maxContentLength: config.maxContentLength ?? 50 * 1024 * 1024, // 50MB
40
- maxBodyLength: config.maxBodyLength ?? 50 * 1024 * 1024 // 50MB
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. The Obsidian REST API plugin requires HTTPS by default.\n" +
70
- "Make sure you have configured the certificate as a trusted certificate authority.\n" +
71
- "See Obsidian Settings > Local REST API > 'How to Access' for setup instructions.");
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
  }
@@ -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(/^---\n([\s\S]*?)\n---/);
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[1];
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 partial valid properties rather than throwing
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 `---\n${yaml}---\n`;
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);
@@ -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().startsWith("#")).optional(),
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().startsWith("#")).optional(),
43
+ tags: z.array(z.string()).optional(),
44
44
  // Technical Metadata
45
45
  status: z.array(StatusEnum).optional(),
46
46
  version: z.string().optional(),
@@ -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": ["**/*.md", { "var": "path" }]
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
- // Extract tags from frontmatter
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.NODE_ENV === 'production' // Enable SSL verification in production
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
- process.on("SIGINT", async () => {
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": "#project",
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 processFiles(files, basePath, tagMap) {
587
- let scannedFiles = 0;
588
- for (const file of files) {
589
- const fullPath = basePath ? `${basePath}/${file.path}` : file.path;
590
- if (file.type === "folder" && file.children) {
591
- // Recursively process subdirectories
592
- scannedFiles += await this.processFiles(file.children, fullPath, tagMap);
593
- }
594
- else if (file.type === "file" && file.path.endsWith('.md')) {
595
- // Process markdown files
596
- scannedFiles++;
597
- const content = await this.client.getFileContents(fullPath);
598
- // Extract tags from frontmatter
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(fullPath);
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.2.6",
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 && chmod +x build/index.js",
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": "rm -rf build",
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
- console.warn('Could not read package.json version:', error);
34
- return '1.1.0'; // Fallback version
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
- // Combine defaults with provided config
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
- ...DEFAULT_OBSIDIAN_CONFIG,
57
- verifySSL: config.verifySSL ?? true, // Default to true as required by Obsidian REST API plugin
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 ?? 5000, // 5 second default timeout
60
- maxContentLength: config.maxContentLength ?? 50 * 1024 * 1024, // 50MB
61
- maxBodyLength: config.maxBodyLength ?? 50 * 1024 * 1024 // 50MB
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. The Obsidian REST API plugin requires HTTPS by default.\n" +
95
- "Make sure you have configured the certificate as a trusted certificate authority.\n" +
96
- "See Obsidian Settings > Local REST API > 'How to Access' for setup instructions."
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(/^---\n([\s\S]*?)\n---/);
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[1];
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 partial valid properties rather than throwing
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 `---\n${yaml}---\n`;
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
@@ -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().startsWith("#")).optional(),
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().startsWith("#")).optional(),
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": ["**/*.md", { "var": "path" }]
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
- // Extract tags from frontmatter
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.NODE_ENV === 'production' // Enable SSL verification in production
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
- process.on("SIGINT", async () => {
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": "#project",
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
- private async processFiles(files: ObsidianFile[], basePath: string, tagMap: Map<string, Set<string>>): Promise<number> {
651
- let scannedFiles = 0;
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
- for (const file of files) {
654
- const fullPath = basePath ? `${basePath}/${file.path}` : file.path;
661
+ const results = await this.client.searchJson(query);
662
+ let scannedFiles = 0;
655
663
 
656
- if (file.type === "folder" && file.children) {
657
- // Recursively process subdirectories
658
- scannedFiles += await this.processFiles(file.children, fullPath, tagMap);
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
- // Extract tags from frontmatter
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(fullPath);
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())