obsidian-mcp-server 1.2.6 → 1.4.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.
Files changed (79) hide show
  1. package/README.md +54 -10
  2. package/build/index.js +12 -3
  3. package/build/mcp/handlers.js +138 -0
  4. package/build/mcp/index.js +7 -0
  5. package/build/mcp/server.js +131 -0
  6. package/build/mcp/types.js +7 -0
  7. package/build/{obsidian.js → obsidian/client.js} +156 -104
  8. package/build/obsidian/errors.js +75 -0
  9. package/build/obsidian/index.js +7 -0
  10. package/build/obsidian/types.js +12 -0
  11. package/build/resources/index.js +15 -0
  12. package/build/{resources.js → resources/tags.js} +35 -14
  13. package/build/resources/types.js +5 -0
  14. package/build/tools/base.js +78 -0
  15. package/build/tools/files/content.js +171 -0
  16. package/build/tools/files/index.js +22 -0
  17. package/build/tools/files/list.js +133 -0
  18. package/build/tools/index.js +31 -0
  19. package/build/tools/properties/index.js +19 -0
  20. package/build/{properties.js → tools/properties/manager.js} +50 -13
  21. package/build/{propertyTools.js → tools/properties/tools.js} +22 -6
  22. package/build/{propertyTypes.js → tools/properties/types.js} +19 -7
  23. package/build/tools/search/complex.js +203 -0
  24. package/build/tools/search/index.js +20 -0
  25. package/build/tools/search/simple.js +127 -0
  26. package/build/utils/errors.js +59 -0
  27. package/build/utils/index.js +9 -0
  28. package/build/utils/logging.js +119 -0
  29. package/build/utils/rate-limiting.js +94 -0
  30. package/build/utils/tokenization.js +62 -0
  31. package/build/utils/validation.js +88 -0
  32. package/examples/README.md +48 -0
  33. package/examples/append-content.md +63 -0
  34. package/examples/complex-search.md +117 -0
  35. package/examples/find-in-file.md +94 -0
  36. package/examples/get-file-contents.md +72 -0
  37. package/examples/get-properties.md +89 -0
  38. package/examples/list-files-in-dir.md +55 -0
  39. package/examples/list-files-in-vault.md +53 -0
  40. package/examples/patch-content.md +60 -0
  41. package/examples/update-properties.md +126 -0
  42. package/mcp-client-config.example.json +23 -0
  43. package/package.json +4 -3
  44. package/src/index.ts +13 -3
  45. package/src/mcp/handlers.ts +183 -0
  46. package/src/mcp/index.ts +6 -0
  47. package/src/mcp/server.ts +162 -0
  48. package/src/mcp/types.ts +46 -0
  49. package/src/{obsidian.ts → obsidian/client.ts} +174 -134
  50. package/src/obsidian/errors.ts +105 -0
  51. package/src/obsidian/index.ts +6 -0
  52. package/src/obsidian/types.ts +124 -0
  53. package/src/resources/index.ts +17 -0
  54. package/src/{resources.ts → resources/tags.ts} +42 -16
  55. package/src/resources/types.ts +30 -0
  56. package/src/tools/base.ts +112 -0
  57. package/src/tools/files/content.ts +206 -0
  58. package/src/tools/files/index.ts +31 -0
  59. package/src/tools/files/list.ts +150 -0
  60. package/src/tools/index.ts +38 -0
  61. package/src/tools/properties/index.ts +21 -0
  62. package/src/{properties.ts → tools/properties/manager.ts} +54 -13
  63. package/src/{propertyTools.ts → tools/properties/tools.ts} +33 -7
  64. package/src/{propertyTypes.ts → tools/properties/types.ts} +31 -7
  65. package/src/tools/search/complex.ts +231 -0
  66. package/src/tools/search/index.ts +22 -0
  67. package/src/tools/search/simple.ts +147 -0
  68. package/src/utils/errors.ts +69 -0
  69. package/src/utils/index.ts +8 -0
  70. package/src/utils/logging.ts +146 -0
  71. package/src/utils/rate-limiting.ts +114 -0
  72. package/src/utils/tokenization.ts +71 -0
  73. package/src/utils/validation.ts +95 -0
  74. package/build/server.js +0 -238
  75. package/build/tools.js +0 -863
  76. package/build/types.js +0 -37
  77. package/src/server.ts +0 -308
  78. package/src/tools.ts +0 -946
  79. package/src/types.ts +0 -184
@@ -0,0 +1,183 @@
1
+ /**
2
+ * MCP server request handlers
3
+ */
4
+ import {
5
+ Tool,
6
+ TextContent,
7
+ ListToolsRequestSchema,
8
+ CallToolRequestSchema,
9
+ ListResourcesRequestSchema,
10
+ ReadResourceRequestSchema
11
+ } from "@modelcontextprotocol/sdk/types.js";
12
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
13
+ import { ObsidianError } from "../utils/errors.js";
14
+ import { validateToolArguments } from "../utils/validation.js";
15
+ import { rateLimiter } from "../utils/rate-limiting.js";
16
+ import { createLogger } from "../utils/logging.js";
17
+ import { DEFAULT_TIMEOUT_CONFIG } from "./types.js";
18
+ import { BaseToolHandler } from "../tools/base.js";
19
+
20
+ // Create a logger for request handlers
21
+ const logger = createLogger('McpHandlers');
22
+
23
+ /**
24
+ * Set up tool listing handler
25
+ * @param server The MCP server instance
26
+ * @param toolHandlers The tool handlers to register
27
+ */
28
+ export function setupToolListingHandler(
29
+ server: Server,
30
+ toolHandlers: Map<string, BaseToolHandler<any>>
31
+ ): void {
32
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
33
+ logger.debug('Handling ListToolsRequest');
34
+ const tools: Tool[] = [];
35
+ for (const handler of toolHandlers.values()) {
36
+ tools.push(handler.getToolDescription());
37
+ }
38
+ return { tools };
39
+ });
40
+ }
41
+
42
+ /**
43
+ * Set up tool calling handler
44
+ * @param server The MCP server instance
45
+ * @param toolHandlers The tool handlers to register
46
+ */
47
+ export function setupToolCallingHandler(
48
+ server: Server,
49
+ toolHandlers: Map<string, BaseToolHandler<any>>
50
+ ): void {
51
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
52
+ const { name, arguments: args } = request.params;
53
+ logger.debug(`Handling CallToolRequest for tool: ${name}`);
54
+
55
+ const handler = toolHandlers.get(name);
56
+ if (!handler) {
57
+ logger.error(`Unknown tool requested: ${name}`);
58
+ throw new ObsidianError(`Unknown tool: ${name}`, 40400); // 40400 = Not found
59
+ }
60
+
61
+ // Check rate limit
62
+ try {
63
+ rateLimiter.enforceRateLimit(name);
64
+ } catch (error) {
65
+ // If rate limit is exceeded, log and rethrow the error
66
+ logger.warn(`Rate limit exceeded for tool: ${name}`);
67
+ throw error;
68
+ }
69
+
70
+ // Add timeout handling
71
+ const timeoutMs = DEFAULT_TIMEOUT_CONFIG.toolExecutionMs;
72
+ const timeoutPromise = new Promise<never>((_, reject) => {
73
+ setTimeout(() => {
74
+ reject(new ObsidianError(`Tool execution timed out after ${timeoutMs}ms`, 40800)); // 40800 = Request timeout
75
+ }, timeoutMs);
76
+ });
77
+
78
+ try {
79
+ // Validate arguments against tool's schema
80
+ const toolDescription = handler.getToolDescription();
81
+ const validationResult = validateToolArguments(args, toolDescription.inputSchema);
82
+ if (!validationResult.valid) {
83
+ logger.error(`Invalid tool arguments for ${name}:`, validationResult.errors);
84
+ throw new ObsidianError(
85
+ `Invalid tool arguments: ${validationResult.errors.join(', ')}`,
86
+ 40000 // 40000 = Bad request
87
+ );
88
+ }
89
+
90
+ // Log the tool execution
91
+ logger.info(`Executing tool: ${name}`);
92
+
93
+ // Race between tool execution and timeout
94
+ const content = await Promise.race([
95
+ handler.runTool(args),
96
+ timeoutPromise
97
+ ]);
98
+
99
+ logger.debug(`Tool execution completed successfully: ${name}`);
100
+
101
+ return { content };
102
+ } catch (error) {
103
+ if (error instanceof ObsidianError) {
104
+ // Check if the operation actually succeeded despite the error
105
+ if (error.errorCode === 20400) { // 20400 = Success with no content
106
+ return {
107
+ content: [{
108
+ type: "text",
109
+ text: "Operation completed successfully"
110
+ }]
111
+ };
112
+ }
113
+ throw error;
114
+ }
115
+
116
+ // Enhanced error logging
117
+ logger.error("Tool execution error:", {
118
+ name: error instanceof Error ? error.name : 'Unknown',
119
+ message: error instanceof Error ? error.message : String(error),
120
+ stack: error instanceof Error ? error.stack : undefined,
121
+ toolName: name
122
+ });
123
+
124
+ if (error instanceof Error) {
125
+ throw new ObsidianError(
126
+ `Tool '${name}' execution failed: ${error.message}`,
127
+ 50000, // 50000 = Internal server error
128
+ { originalError: error.stack }
129
+ );
130
+ }
131
+
132
+ throw new ObsidianError(
133
+ "Tool execution failed with unknown error",
134
+ 50000, // 50000 = Internal server error
135
+ { error }
136
+ );
137
+ }
138
+ });
139
+ }
140
+
141
+ /**
142
+ * Set up resource listing handler
143
+ * @param server The MCP server instance
144
+ * @param resources The resources to register
145
+ */
146
+ export function setupResourceListingHandler(
147
+ server: Server,
148
+ resources: Record<string, any>
149
+ ): void {
150
+ server.setRequestHandler(ListResourcesRequestSchema, async () => {
151
+ logger.debug('Handling ListResourcesRequest');
152
+ return {
153
+ resources: Object.values(resources).map(resource =>
154
+ resource.getResourceDescription())
155
+ };
156
+ });
157
+ }
158
+
159
+ /**
160
+ * Set up resource reading handler
161
+ * @param server The MCP server instance
162
+ * @param resources The resources to register
163
+ */
164
+ export function setupResourceReadingHandler(
165
+ server: Server,
166
+ resources: Record<string, any>
167
+ ): void {
168
+ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
169
+ const uri = request.params.uri;
170
+ logger.debug(`Handling ReadResourceRequest for URI: ${uri}`);
171
+
172
+ const resource = resources[uri];
173
+ if (resource) {
174
+ logger.debug(`Found resource for URI: ${uri}`);
175
+ return {
176
+ contents: await resource.getContent()
177
+ };
178
+ }
179
+
180
+ logger.error(`Resource not found: ${uri}`);
181
+ throw new ObsidianError(`Resource not found: ${uri}`, 40400); // 40400 = Not found
182
+ });
183
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * MCP module exports
3
+ */
4
+ export * from './types.js';
5
+ export * from './handlers.js';
6
+ export * from './server.js';
@@ -0,0 +1,162 @@
1
+ /**
2
+ * MCP server implementation
3
+ */
4
+ import { config } from "dotenv";
5
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
6
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
7
+ import { createLogger } from "../utils/logging.js";
8
+ import { rateLimiter } from "../utils/rate-limiting.js";
9
+ import { createTagResource } from "../resources/index.js";
10
+ import { createToolHandlers, createToolHandlerMap } from "../tools/index.js";
11
+ import { ObsidianClient } from "../obsidian/client.js";
12
+ import {
13
+ setupToolListingHandler,
14
+ setupToolCallingHandler,
15
+ setupResourceListingHandler,
16
+ setupResourceReadingHandler
17
+ } from "./handlers.js";
18
+ import { McpServerConfig, ResourceMap } from "./types.js";
19
+
20
+ // Create a logger for the server
21
+ const logger = createLogger('McpServer');
22
+
23
+ // Load environment variables
24
+ config();
25
+
26
+ /**
27
+ * Initialize MCP server components
28
+ */
29
+ export async function initializeServer(): Promise<Server> {
30
+ // Verify API key exists
31
+ const API_KEY = process.env.OBSIDIAN_API_KEY;
32
+ if (!API_KEY) {
33
+ throw new Error("OBSIDIAN_API_KEY environment variable is required");
34
+ }
35
+
36
+ // Initialize Obsidian client with environment configuration
37
+ logger.info('Initializing Obsidian client');
38
+ const client = new ObsidianClient({
39
+ apiKey: API_KEY,
40
+ verifySSL: process.env.VERIFY_SSL === 'true',
41
+ timeout: parseInt(process.env.REQUEST_TIMEOUT || '5000'),
42
+ maxContentLength: parseInt(process.env.MAX_CONTENT_LENGTH || String(50 * 1024 * 1024)),
43
+ maxBodyLength: parseInt(process.env.MAX_BODY_LENGTH || String(50 * 1024 * 1024))
44
+ });
45
+
46
+ // Initialize tool handlers
47
+ logger.info('Initializing tool handlers');
48
+ const toolHandlers = createToolHandlers(client);
49
+ const toolHandlerMap = createToolHandlerMap(toolHandlers);
50
+
51
+ // Initialize resources
52
+ logger.info('Initializing resources');
53
+ const tagResource = createTagResource(client);
54
+ const resources: ResourceMap = {
55
+ [tagResource.getResourceDescription().uri]: tagResource
56
+ };
57
+
58
+ // Create MCP server
59
+ const serverConfig: McpServerConfig = {
60
+ name: "obsidian-mcp-server",
61
+ version: process.env.npm_package_version ?? "1.1.0" // Use version from package.json
62
+ };
63
+
64
+ logger.info(`Creating MCP server: ${serverConfig.name} v${serverConfig.version}`);
65
+ const server = new Server(
66
+ serverConfig,
67
+ {
68
+ capabilities: {
69
+ tools: {},
70
+ resources
71
+ }
72
+ }
73
+ );
74
+
75
+ // Set up request handlers
76
+ setupToolListingHandler(server, toolHandlerMap);
77
+ setupToolCallingHandler(server, toolHandlerMap);
78
+ setupResourceListingHandler(server, resources);
79
+ setupResourceReadingHandler(server, resources);
80
+
81
+ // Set up error handler
82
+ server.onerror = (error) => {
83
+ logger.error("[MCP Error]", error);
84
+ };
85
+
86
+ return server;
87
+ }
88
+
89
+ /**
90
+ * Set up graceful shutdown handling
91
+ * @param server The MCP server instance
92
+ * @param cleanupHandlers Additional cleanup handlers to run on shutdown
93
+ */
94
+ export function setupShutdownHandling(
95
+ server: Server,
96
+ cleanupHandlers: (() => Promise<void> | void)[] = []
97
+ ): void {
98
+ // Cleanup function for graceful shutdown
99
+ const cleanup = async () => {
100
+ logger.info('Shutting down server...');
101
+
102
+ // Run cleanup handlers
103
+ for (const handler of cleanupHandlers) {
104
+ try {
105
+ await handler();
106
+ } catch (error) {
107
+ logger.error('Error during cleanup:', error);
108
+ }
109
+ }
110
+
111
+ // Dispose rate limiter
112
+ rateLimiter.dispose();
113
+
114
+ // Close server
115
+ await server.close();
116
+
117
+ process.exit(0);
118
+ };
119
+
120
+ // Handle various termination signals
121
+ process.on('SIGINT', cleanup); // Ctrl+C on all platforms
122
+ process.on('SIGTERM', cleanup); // Termination request
123
+
124
+ if (process.platform === 'win32') {
125
+ // Windows-specific handling
126
+ process.on('SIGHUP', cleanup); // Terminal closed
127
+ } else {
128
+ // Unix-specific signals
129
+ process.on('SIGUSR1', cleanup);
130
+ process.on('SIGUSR2', cleanup);
131
+ }
132
+
133
+ // Handle uncaught errors
134
+ process.on('uncaughtException', async (error) => {
135
+ logger.error('Uncaught exception:', error);
136
+ await cleanup();
137
+ });
138
+
139
+ process.on('unhandledRejection', async (error) => {
140
+ logger.error('Unhandled rejection:', error);
141
+ await cleanup();
142
+ });
143
+ }
144
+
145
+ /**
146
+ * Run the MCP server
147
+ */
148
+ export async function run(): Promise<void> {
149
+ try {
150
+ const server = await initializeServer();
151
+ setupShutdownHandling(server, []);
152
+
153
+ // Connect to transport
154
+ const transport = new StdioServerTransport();
155
+ await server.connect(transport);
156
+
157
+ logger.info("Obsidian MCP server running on stdio");
158
+ } catch (error) {
159
+ logger.error("Failed to start server:", error);
160
+ process.exit(1);
161
+ }
162
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * MCP server type definitions
3
+ */
4
+ import { TextContent, Tool } from "@modelcontextprotocol/sdk/types.js";
5
+ import { BaseToolHandler } from "../tools/base.js";
6
+ import { TagResource } from "../resources/tags.js";
7
+
8
+ /**
9
+ * MCP server configuration
10
+ */
11
+ export interface McpServerConfig extends Record<string, unknown> {
12
+ name: string;
13
+ version: string;
14
+ }
15
+
16
+ /**
17
+ * Timeout configuration for tool execution
18
+ */
19
+ export interface TimeoutConfig {
20
+ toolExecutionMs: number; // Default: 60000 (60 seconds)
21
+ }
22
+
23
+ /**
24
+ * Map of resources by URI
25
+ */
26
+ export interface ResourceMap {
27
+ [uri: string]: {
28
+ getContent(): Promise<TextContent[]>;
29
+ getResourceDescription(): any;
30
+ };
31
+ }
32
+
33
+ /**
34
+ * Server capabilities configuration
35
+ */
36
+ export interface ServerCapabilities {
37
+ tools: Record<string, Tool>;
38
+ resources: ResourceMap;
39
+ }
40
+
41
+ /**
42
+ * Default timeout configuration
43
+ */
44
+ export const DEFAULT_TIMEOUT_CONFIG: TimeoutConfig = {
45
+ toolExecutionMs: parseInt(process.env.TOOL_TIMEOUT_MS ?? '60000') // 60 second default timeout
46
+ };