obsidian-mcp-server 1.1.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.
@@ -0,0 +1,35 @@
1
+ name: Publish Package to npm
2
+ on:
3
+ push:
4
+ tags:
5
+ - 'v*'
6
+
7
+ jobs:
8
+ build-and-publish:
9
+ runs-on: ubuntu-latest
10
+ permissions:
11
+ contents: read
12
+ packages: write
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+
16
+ - name: Setup Node.js
17
+ uses: actions/setup-node@v4
18
+ with:
19
+ node-version: '18.x'
20
+ registry-url: 'https://registry.npmjs.org'
21
+ scope: '@cyanheads'
22
+
23
+ - name: Install dependencies
24
+ run: npm ci
25
+
26
+ - name: Build
27
+ run: npm run build
28
+
29
+ - name: Publish to npm
30
+ # Requires NPM_TOKEN secret to be set in repository settings
31
+ # Generate token at https://www.npmjs.com/settings/[username]/tokens
32
+ # and add to repository secrets with read/write packages permission
33
+ run: npm publish --access public
34
+ env:
35
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
package/README.md ADDED
@@ -0,0 +1,171 @@
1
+ # Obsidian MCP Server
2
+
3
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.3-blue.svg)](https://www.typescriptlang.org/)
4
+ [![Model Context Protocol](https://img.shields.io/badge/MCP-1.4.0-green.svg)](https://modelcontextprotocol.io/)
5
+ [![Version](https://img.shields.io/badge/Version-1.1.0-blue.svg)]()
6
+ [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
7
+ [![Status](https://img.shields.io/badge/Status-Stable-blue.svg)]()
8
+ [![GitHub](https://img.shields.io/github/stars/cyanheads/obsidian-mcp-server?style=social)](https://github.com/cyanheads/obsidian-mcp-server)
9
+
10
+ A Model Context Protocol server designed for LLMs to interact with Obsidian vaults. Built with TypeScript and featuring secure API communication, efficient file operations, and comprehensive search capabilities, it enables AI assistants to seamlessly manage knowledge bases through a clean, flexible tool interface.
11
+
12
+ The Model Context Protocol (MCP) enables AI models to interact with external tools and resources through a standardized interface. This server implements MCP to provide LLMs with secure, token-aware access to Obsidian vaults.
13
+
14
+ Requires the Local REST API plugin in Obsidian.
15
+
16
+ ## Features
17
+
18
+ ### File Operations
19
+ - Path-based file/directory management with atomic updates
20
+ - Content read/write operations with validation
21
+ - Resource monitoring and cleanup
22
+
23
+ ### Search System
24
+ - Full-text and JsonLogic-based complex search
25
+ - Configurable context boundaries and token limits
26
+ - Optimized query processing
27
+
28
+ ### Security & Performance
29
+ - API key authentication and rate limiting
30
+ - SSL verification options
31
+ - Resource management and health monitoring
32
+
33
+ ## Installation
34
+
35
+ 1. Install Node.js (LTS recommended)
36
+ 2. Enable Local REST API plugin in Obsidian
37
+ 3. Clone and build:
38
+ ```bash
39
+ git clone git@github.com:cyanheads/obsidian-mcp-server.git
40
+ cd obsidian-mcp-server
41
+ npm install
42
+ npm run build
43
+ ```
44
+
45
+ Or install from npm:
46
+ ```bash
47
+ npm install obsidian-mcp-server
48
+ ```
49
+
50
+ ## Configuration
51
+
52
+ Add to your MCP client settings:
53
+
54
+ ```json
55
+ {
56
+ "mcpServers": {
57
+ "obsidian": {
58
+ "command": "node",
59
+ "args": ["/path/to/obsidian-mcp-server/build/index.js"],
60
+ "env": {
61
+ "OBSIDIAN_API_KEY": "your-api-key-here",
62
+ "NODE_ENV": "production"
63
+ }
64
+ }
65
+ }
66
+ }
67
+ ```
68
+
69
+ Environment configuration:
70
+ - `OBSIDIAN_VERIFY_SSL`: Enable SSL verification (default: false)
71
+ - `RATE_LIMIT_WINDOW_MS`: Rate limit window in ms (default: 15 minutes)
72
+ - `RATE_LIMIT_MAX_REQUESTS`: Max requests per window (default: 200)
73
+ - `MAX_TOKENS`: Maximum tokens per response (default: 20000)
74
+ - `TOOL_TIMEOUT_MS`: Tool execution timeout (default: 60000)
75
+
76
+ ## Tools
77
+
78
+ ### File Management
79
+ ```typescript
80
+ // List vault contents
81
+ obsidian_list_files_in_vault: {}
82
+
83
+ // List directory contents
84
+ obsidian_list_files_in_dir: {
85
+ dirpath: string // Path relative to vault root
86
+ }
87
+
88
+ // Get file contents
89
+ obsidian_get_file_contents: {
90
+ filepath: string // Path relative to vault root
91
+ }
92
+ ```
93
+
94
+ ### Search Operations
95
+ ```typescript
96
+ // Text search with context
97
+ obsidian_find_in_file: {
98
+ query: string,
99
+ contextLength?: number // Default: 10
100
+ }
101
+
102
+ // Advanced search with JsonLogic
103
+ obsidian_complex_search: {
104
+ query: JsonLogicQuery // Example: {"glob": ["*.md", {"var": "path"}]}
105
+ }
106
+ ```
107
+
108
+ ### Content Modification
109
+ ```typescript
110
+ // Append to file
111
+ obsidian_append_content: {
112
+ filepath: string, // Path relative to vault root
113
+ content: string // Content to append
114
+ }
115
+
116
+ // Update file content
117
+ obsidian_patch_content: {
118
+ filepath: string, // Path relative to vault root
119
+ content: string // New content (replaces existing)
120
+ }
121
+ ```
122
+
123
+ ## Best Practices
124
+
125
+ ### File Operations
126
+ - Use atomic operations
127
+ - Validate content before modifications
128
+ - Implement proper error handling
129
+ - Monitor operation performance
130
+
131
+ ### Search Implementation
132
+ - Optimize query specificity
133
+ - Control context boundaries
134
+ - Handle large result sets
135
+ - Consider token limits
136
+
137
+ ### Error Prevention
138
+ - Validate inputs thoroughly
139
+ - Handle API errors gracefully
140
+ - Monitor error patterns
141
+ - Check rate limits
142
+
143
+ ## Contributing
144
+
145
+ 1. Fork the repository
146
+ 2. Create a feature branch
147
+ 3. Submit a Pull Request
148
+
149
+ For bugs and features, create an issue at [https://github.com/cyanheads/obsidian-mcp-server/issues](https://github.com/cyanheads/obsidian-mcp-server/issues).
150
+
151
+ ## Publishing
152
+
153
+ The package is automatically published to npm when version tags are pushed:
154
+
155
+ ```bash
156
+ # Update version in package.json
157
+ npm version patch # or minor, or major
158
+ git push --follow-tags
159
+ ```
160
+
161
+ This will trigger the GitHub Action to build and publish the package.
162
+
163
+ ## License
164
+
165
+ Apache License 2.0
166
+
167
+ ---
168
+
169
+ <div align="center">
170
+ Built with the Model Context Protocol
171
+ </div>
package/build/index.js ADDED
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+ import { run } from "./server.js";
3
+ // Main entry point
4
+ if (import.meta.url === `file://${process.argv[1]}`) {
5
+ run().catch((error) => {
6
+ console.error("Failed to start server:", error);
7
+ process.exit(1);
8
+ });
9
+ }
10
+ export { run };
11
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,197 @@
1
+ import axios from "axios";
2
+ import { ObsidianError, DEFAULT_OBSIDIAN_CONFIG } from "./types.js";
3
+ import { Agent } from "node:https";
4
+ import { readFileSync } from "fs";
5
+ import { fileURLToPath } from 'url';
6
+ import { dirname, join } from "path";
7
+ // Get package version for user agent
8
+ const __filename = fileURLToPath(import.meta.url);
9
+ const __dirname = dirname(__filename);
10
+ const packagePath = join(__dirname, '..', '..', 'package.json');
11
+ const VERSION = (() => {
12
+ try {
13
+ const pkg = JSON.parse(readFileSync(packagePath, 'utf-8'));
14
+ return pkg.version;
15
+ }
16
+ catch (error) {
17
+ console.warn('Could not read package.json version:', error);
18
+ return '1.1.0'; // Fallback version
19
+ }
20
+ })();
21
+ export class ObsidianClient {
22
+ client;
23
+ config;
24
+ constructor(config) {
25
+ if (!config.apiKey) {
26
+ throw new ObsidianError("API key is required", 401);
27
+ }
28
+ // Combine defaults with provided config
29
+ this.config = {
30
+ ...DEFAULT_OBSIDIAN_CONFIG,
31
+ verifySSL: config.verifySSL ?? process.env.NODE_ENV === 'production', // Enable SSL verification in production by default
32
+ apiKey: config.apiKey,
33
+ timeout: config.timeout ?? 5000,
34
+ maxContentLength: config.maxContentLength ?? 50 * 1024 * 1024, // 50MB
35
+ maxBodyLength: config.maxBodyLength ?? 50 * 1024 * 1024 // 50MB
36
+ };
37
+ // Configure HTTPS agent
38
+ const httpsAgent = new Agent({
39
+ rejectUnauthorized: this.config.verifySSL
40
+ });
41
+ const axiosConfig = {
42
+ baseURL: this.getBaseUrl(),
43
+ headers: {
44
+ ...this.getHeaders(),
45
+ // Security headers
46
+ 'X-Content-Type-Options': 'nosniff',
47
+ 'X-Frame-Options': 'DENY',
48
+ 'X-XSS-Protection': '1; mode=block',
49
+ 'Strict-Transport-Security': 'max-age=31536000; includeSubDomains'
50
+ },
51
+ validateStatus: (status) => status >= 200 && status < 300,
52
+ timeout: this.config.timeout,
53
+ maxRedirects: 5,
54
+ maxContentLength: this.config.maxContentLength,
55
+ maxBodyLength: this.config.maxBodyLength,
56
+ httpsAgent,
57
+ // Additional security configurations
58
+ xsrfCookieName: 'XSRF-TOKEN',
59
+ xsrfHeaderName: 'X-XSRF-TOKEN',
60
+ withCredentials: true,
61
+ decompress: true
62
+ };
63
+ if (!this.config.verifySSL) {
64
+ console.warn("WARNING: SSL verification is disabled. This is not recommended for production use.", process.env.NODE_ENV === 'production' ? "Consider enabling SSL verification." : "This is acceptable for local development only.");
65
+ }
66
+ this.client = axios.create(axiosConfig);
67
+ }
68
+ getBaseUrl() {
69
+ return `${this.config.protocol}://${this.config.host}:${this.config.port}`;
70
+ }
71
+ getHeaders() {
72
+ const headers = {
73
+ Authorization: `Bearer ${this.config.apiKey}`,
74
+ 'Accept': 'application/json',
75
+ 'User-Agent': `obsidian-mcp-server/${VERSION}`
76
+ };
77
+ // Sanitize headers
78
+ return Object.fromEntries(Object.entries(headers).map(([key, value]) => [
79
+ key,
80
+ this.sanitizeHeader(value)
81
+ ]));
82
+ }
83
+ sanitizeHeader(value) {
84
+ // Remove any potentially harmful characters from header values
85
+ return value.replace(/[^\w\s\-\._~:/?#\[\]@!$&'()*+,;=]/g, '');
86
+ }
87
+ validateFilePath(filepath) {
88
+ // Prevent path traversal attacks
89
+ const normalizedPath = filepath.replace(/\\/g, '/');
90
+ if (normalizedPath.includes('../') || normalizedPath.includes('..\\')) {
91
+ throw new ObsidianError('Invalid file path: Path traversal not allowed', 400);
92
+ }
93
+ // Additional path validations
94
+ if (normalizedPath.startsWith('/') || /^[a-zA-Z]:/.test(normalizedPath)) {
95
+ throw new ObsidianError('Invalid file path: Absolute paths not allowed', 400);
96
+ }
97
+ }
98
+ async safeRequest(operation) {
99
+ try {
100
+ return await operation();
101
+ }
102
+ catch (error) {
103
+ if (axios.isAxiosError(error)) {
104
+ const axiosError = error;
105
+ const response = axiosError.response;
106
+ const errorData = response?.data;
107
+ const code = errorData?.errorCode ?? response?.status ?? 500;
108
+ const message = errorData?.message ?? axiosError.message ?? "Unknown error";
109
+ throw new ObsidianError(message, code, errorData);
110
+ }
111
+ throw error;
112
+ }
113
+ }
114
+ async listFilesInVault() {
115
+ return this.safeRequest(async () => {
116
+ const requestId = crypto.randomUUID();
117
+ console.debug(`[${requestId}] Listing vault files`);
118
+ const response = await this.client.get("/vault/");
119
+ return response.data.files;
120
+ });
121
+ }
122
+ async listFilesInDir(dirpath) {
123
+ this.validateFilePath(dirpath);
124
+ return this.safeRequest(async () => {
125
+ const requestId = crypto.randomUUID();
126
+ console.debug(`[${requestId}] Listing files in directory: ${dirpath}`);
127
+ const response = await this.client.get(`/vault/${dirpath}/`);
128
+ return response.data.files;
129
+ });
130
+ }
131
+ async getFileContents(filepath) {
132
+ this.validateFilePath(filepath);
133
+ return this.safeRequest(async () => {
134
+ const requestId = crypto.randomUUID();
135
+ console.debug(`[${requestId}] Getting file contents: ${filepath}`);
136
+ const response = await this.client.get(`/vault/${filepath}`);
137
+ return response.data;
138
+ });
139
+ }
140
+ async search(query, contextLength = 100) {
141
+ return this.safeRequest(async () => {
142
+ const requestId = crypto.randomUUID();
143
+ console.debug(`[${requestId}] Performing simple search: ${query}`);
144
+ const response = await this.client.post("/search/simple/", undefined, {
145
+ params: {
146
+ query,
147
+ contextLength
148
+ }
149
+ });
150
+ return response.data;
151
+ });
152
+ }
153
+ async appendContent(filepath, content) {
154
+ this.validateFilePath(filepath);
155
+ if (!content || typeof content !== 'string') {
156
+ throw new ObsidianError('Invalid content: Content must be a non-empty string', 400);
157
+ }
158
+ return this.safeRequest(async () => {
159
+ const requestId = crypto.randomUUID();
160
+ console.debug(`[${requestId}] Appending content to: ${filepath}`);
161
+ await this.client.post(`/vault/${filepath}`, content, {
162
+ headers: {
163
+ "Content-Type": "text/markdown"
164
+ }
165
+ });
166
+ });
167
+ }
168
+ async updateContent(filepath, content) {
169
+ this.validateFilePath(filepath);
170
+ if (!content || typeof content !== 'string') {
171
+ throw new ObsidianError('Invalid content: Content must be a non-empty string', 400);
172
+ }
173
+ return this.safeRequest(async () => {
174
+ const requestId = crypto.randomUUID();
175
+ console.debug(`[${requestId}] Updating content in: ${filepath}`);
176
+ await this.client.put(`/vault/${filepath}`, content, {
177
+ headers: {
178
+ "Content-Type": "text/markdown"
179
+ }
180
+ });
181
+ });
182
+ }
183
+ async searchJson(query) {
184
+ return this.safeRequest(async () => {
185
+ const requestId = crypto.randomUUID();
186
+ console.debug(`[${requestId}] Performing complex search with query:`, JSON.stringify(query));
187
+ const response = await this.client.post("/search/", query, {
188
+ headers: {
189
+ "Content-Type": "application/vnd.olrapi.jsonlogic+json",
190
+ "Accept": "application/json"
191
+ }
192
+ });
193
+ return response.data;
194
+ });
195
+ }
196
+ }
197
+ //# sourceMappingURL=obsidian.js.map
@@ -0,0 +1,211 @@
1
+ import { config } from "dotenv";
2
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { ListToolsRequestSchema, CallToolRequestSchema } from "@modelcontextprotocol/sdk/types.js";
5
+ import { ObsidianClient } from "./obsidian.js";
6
+ import { ObsidianError, DEFAULT_RATE_LIMIT_CONFIG } from "./types.js";
7
+ import { ListFilesInVaultToolHandler, ListFilesInDirToolHandler, GetFileContentsToolHandler, FindInFileToolHandler, AppendContentToolHandler, PatchContentToolHandler, ComplexSearchToolHandler } from "./tools.js";
8
+ // Load environment variables
9
+ config();
10
+ const API_KEY = process.env.OBSIDIAN_API_KEY;
11
+ if (!API_KEY) {
12
+ throw new Error("OBSIDIAN_API_KEY environment variable is required");
13
+ }
14
+ // Get rate limit config from environment or use defaults
15
+ const rateLimitConfig = {
16
+ windowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS ?? String(DEFAULT_RATE_LIMIT_CONFIG.windowMs)),
17
+ maxRequests: parseInt(process.env.RATE_LIMIT_MAX_REQUESTS ?? String(DEFAULT_RATE_LIMIT_CONFIG.maxRequests))
18
+ };
19
+ // Request tracking for rate limiting
20
+ const requestCounts = new Map();
21
+ function checkRateLimit(toolName) {
22
+ const now = Date.now();
23
+ const requestInfo = requestCounts.get(toolName);
24
+ if (!requestInfo || now > requestInfo.resetTime) {
25
+ // Reset counter for new window
26
+ requestCounts.set(toolName, {
27
+ count: 1,
28
+ resetTime: now + rateLimitConfig.windowMs
29
+ });
30
+ return true;
31
+ }
32
+ if (requestInfo.count >= rateLimitConfig.maxRequests) {
33
+ return false;
34
+ }
35
+ requestInfo.count++;
36
+ return true;
37
+ }
38
+ // Clean up expired rate limit entries periodically
39
+ const cleanupInterval = setInterval(() => {
40
+ const now = Date.now();
41
+ for (const [tool, info] of requestCounts.entries()) {
42
+ if (now > info.resetTime) {
43
+ requestCounts.delete(tool);
44
+ }
45
+ }
46
+ }, 60000); // Clean up every minute
47
+ // Initialize Obsidian client
48
+ const client = new ObsidianClient({
49
+ apiKey: API_KEY,
50
+ verifySSL: process.env.NODE_ENV === 'production' // Enable SSL verification in production
51
+ });
52
+ const toolHandlers = new Map();
53
+ const handlers = [
54
+ new ListFilesInVaultToolHandler(client),
55
+ new ListFilesInDirToolHandler(client),
56
+ new GetFileContentsToolHandler(client),
57
+ new FindInFileToolHandler(client),
58
+ new AppendContentToolHandler(client),
59
+ new PatchContentToolHandler(client),
60
+ new ComplexSearchToolHandler(client)
61
+ ];
62
+ handlers.forEach(handler => toolHandlers.set(handler.name, handler));
63
+ // Create MCP server
64
+ const server = new Server({
65
+ name: "obsidian-mcp-server",
66
+ version: process.env.npm_package_version ?? "1.1.0" // Use version from package.json
67
+ }, {
68
+ capabilities: {
69
+ tools: {},
70
+ resources: {}
71
+ }
72
+ });
73
+ // Set up request handlers
74
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
75
+ const tools = [];
76
+ for (const handler of toolHandlers.values()) {
77
+ tools.push(handler.getToolDescription());
78
+ }
79
+ return { tools };
80
+ });
81
+ // Add validation helper
82
+ function validateToolArguments(args, schema) {
83
+ if (typeof args !== 'object' || args === null) {
84
+ return { valid: false, errors: ['Arguments must be an object'] };
85
+ }
86
+ const errors = [];
87
+ const required = schema.required || [];
88
+ // Check required fields
89
+ for (const field of required) {
90
+ if (!(field in args)) {
91
+ errors.push(`Missing required field: ${field}`);
92
+ }
93
+ }
94
+ // Check field types
95
+ const properties = schema.properties || {};
96
+ for (const [key, value] of Object.entries(args)) {
97
+ const propSchema = properties[key];
98
+ if (!propSchema) {
99
+ errors.push(`Unknown field: ${key}`);
100
+ continue;
101
+ }
102
+ // Skip validation for undefined optional fields
103
+ if (value === undefined && !required.includes(key)) {
104
+ continue;
105
+ }
106
+ // Type validation
107
+ if (propSchema.type === 'string' && typeof value !== 'string') {
108
+ errors.push(`Field ${key} must be a string`);
109
+ }
110
+ else if (propSchema.type === 'number' && typeof value !== 'number') {
111
+ errors.push(`Field ${key} must be a number`);
112
+ }
113
+ else if (propSchema.type === 'boolean' && typeof value !== 'boolean') {
114
+ errors.push(`Field ${key} must be a boolean`);
115
+ }
116
+ else if (propSchema.type === 'array' && !Array.isArray(value)) {
117
+ errors.push(`Field ${key} must be an array`);
118
+ }
119
+ // Enum validation
120
+ if (propSchema.enum && value !== undefined && !propSchema.enum.includes(value)) {
121
+ errors.push(`Field ${key} must be one of: ${propSchema.enum.join(', ')}`);
122
+ }
123
+ // Format validation for paths
124
+ if (propSchema.format === 'path' && typeof value === 'string') {
125
+ // Prevent path traversal
126
+ if (value.includes('../') || value.includes('..\\')) {
127
+ errors.push(`Field ${key} contains invalid path traversal`);
128
+ }
129
+ // Prevent absolute paths
130
+ if (value.startsWith('/') || /^[a-zA-Z]:/.test(value)) {
131
+ errors.push(`Field ${key} must be a relative path`);
132
+ }
133
+ }
134
+ }
135
+ return { valid: errors.length === 0, errors };
136
+ }
137
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
138
+ const { name, arguments: args } = request.params;
139
+ const handler = toolHandlers.get(name);
140
+ if (!handler) {
141
+ throw new ObsidianError(`Unknown tool: ${name}`, 404);
142
+ }
143
+ // Check rate limit
144
+ if (!checkRateLimit(name)) {
145
+ throw new ObsidianError(`Rate limit exceeded for tool: ${name}. Please try again later.`, 429);
146
+ }
147
+ // Add timeout handling
148
+ const timeoutMs = parseInt(process.env.TOOL_TIMEOUT_MS ?? '60000'); // 60 second default timeout
149
+ const timeoutPromise = new Promise((_, reject) => {
150
+ setTimeout(() => {
151
+ reject(new ObsidianError(`Tool execution timed out after ${timeoutMs}ms`, 408));
152
+ }, timeoutMs);
153
+ });
154
+ try {
155
+ // Validate arguments against tool's schema
156
+ const toolDescription = handler.getToolDescription();
157
+ const validationResult = validateToolArguments(args, toolDescription.inputSchema);
158
+ if (!validationResult.valid) {
159
+ throw new ObsidianError(`Invalid tool arguments: ${validationResult.errors.join(', ')}`, 400);
160
+ }
161
+ // Race between tool execution and timeout
162
+ const content = await Promise.race([
163
+ handler.runTool(args),
164
+ timeoutPromise
165
+ ]);
166
+ return { content };
167
+ }
168
+ catch (error) {
169
+ if (error instanceof ObsidianError) {
170
+ // Check if the operation actually succeeded despite the error
171
+ if (error.code === 204) {
172
+ return {
173
+ content: [{
174
+ type: "text",
175
+ text: "Operation completed successfully"
176
+ }]
177
+ };
178
+ }
179
+ throw error;
180
+ }
181
+ // Enhanced error logging
182
+ console.error("Tool execution error:", {
183
+ name: error instanceof Error ? error.name : 'Unknown',
184
+ message: error instanceof Error ? error.message : String(error),
185
+ stack: error instanceof Error ? error.stack : undefined,
186
+ toolName: name,
187
+ args
188
+ });
189
+ if (error instanceof Error) {
190
+ throw new ObsidianError(`Tool '${name}' execution failed: ${error.message}`, 500, { originalError: error.stack });
191
+ }
192
+ throw new ObsidianError("Tool execution failed with unknown error", 500, { error });
193
+ }
194
+ });
195
+ // Error handler
196
+ server.onerror = (error) => {
197
+ console.error("[MCP Error]", error);
198
+ };
199
+ // Handle shutdown
200
+ process.on("SIGINT", async () => {
201
+ clearInterval(cleanupInterval); // Clean up rate limit interval
202
+ await server.close();
203
+ process.exit(0);
204
+ });
205
+ // Export the run function
206
+ export async function run() {
207
+ const transport = new StdioServerTransport();
208
+ await server.connect(transport);
209
+ console.error("Obsidian MCP server running on stdio");
210
+ }
211
+ //# sourceMappingURL=server.js.map