obsidian-mcp-server 1.3.0 → 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 (77) hide show
  1. package/build/index.js +12 -3
  2. package/build/mcp/handlers.js +138 -0
  3. package/build/mcp/index.js +7 -0
  4. package/build/mcp/server.js +131 -0
  5. package/build/mcp/types.js +7 -0
  6. package/build/{obsidian.js → obsidian/client.js} +116 -96
  7. package/build/obsidian/errors.js +75 -0
  8. package/build/obsidian/index.js +7 -0
  9. package/build/obsidian/types.js +12 -0
  10. package/build/resources/index.js +15 -0
  11. package/build/{resources.js → resources/tags.js} +31 -5
  12. package/build/resources/types.js +5 -0
  13. package/build/tools/base.js +78 -0
  14. package/build/tools/files/content.js +171 -0
  15. package/build/tools/files/index.js +22 -0
  16. package/build/tools/files/list.js +133 -0
  17. package/build/tools/index.js +31 -0
  18. package/build/tools/properties/index.js +19 -0
  19. package/build/{properties.js → tools/properties/manager.js} +40 -8
  20. package/build/{propertyTools.js → tools/properties/tools.js} +22 -6
  21. package/build/{propertyTypes.js → tools/properties/types.js} +17 -5
  22. package/build/tools/search/complex.js +203 -0
  23. package/build/tools/search/index.js +20 -0
  24. package/build/tools/search/simple.js +127 -0
  25. package/build/utils/errors.js +59 -0
  26. package/build/utils/index.js +9 -0
  27. package/build/utils/logging.js +119 -0
  28. package/build/utils/rate-limiting.js +94 -0
  29. package/build/utils/tokenization.js +62 -0
  30. package/build/utils/validation.js +88 -0
  31. package/examples/README.md +48 -0
  32. package/examples/append-content.md +63 -0
  33. package/examples/complex-search.md +117 -0
  34. package/examples/find-in-file.md +94 -0
  35. package/examples/get-file-contents.md +72 -0
  36. package/examples/get-properties.md +89 -0
  37. package/examples/list-files-in-dir.md +55 -0
  38. package/examples/list-files-in-vault.md +53 -0
  39. package/examples/patch-content.md +60 -0
  40. package/examples/update-properties.md +126 -0
  41. package/package.json +1 -1
  42. package/src/index.ts +13 -3
  43. package/src/mcp/handlers.ts +183 -0
  44. package/src/mcp/index.ts +6 -0
  45. package/src/mcp/server.ts +162 -0
  46. package/src/mcp/types.ts +46 -0
  47. package/src/{obsidian.ts → obsidian/client.ts} +132 -125
  48. package/src/obsidian/errors.ts +105 -0
  49. package/src/obsidian/index.ts +6 -0
  50. package/src/obsidian/types.ts +124 -0
  51. package/src/resources/index.ts +17 -0
  52. package/src/{resources.ts → resources/tags.ts} +38 -6
  53. package/src/resources/types.ts +30 -0
  54. package/src/tools/base.ts +112 -0
  55. package/src/tools/files/content.ts +206 -0
  56. package/src/tools/files/index.ts +31 -0
  57. package/src/tools/files/list.ts +150 -0
  58. package/src/tools/index.ts +38 -0
  59. package/src/tools/properties/index.ts +21 -0
  60. package/src/{properties.ts → tools/properties/manager.ts} +41 -8
  61. package/src/{propertyTools.ts → tools/properties/tools.ts} +33 -7
  62. package/src/{propertyTypes.ts → tools/properties/types.ts} +29 -5
  63. package/src/tools/search/complex.ts +231 -0
  64. package/src/tools/search/index.ts +22 -0
  65. package/src/tools/search/simple.ts +147 -0
  66. package/src/utils/errors.ts +69 -0
  67. package/src/utils/index.ts +8 -0
  68. package/src/utils/logging.ts +146 -0
  69. package/src/utils/rate-limiting.ts +114 -0
  70. package/src/utils/tokenization.ts +71 -0
  71. package/src/utils/validation.ts +95 -0
  72. package/build/server.js +0 -263
  73. package/build/tools.js +0 -845
  74. package/build/types.js +0 -37
  75. package/src/server.ts +0 -335
  76. package/src/tools.ts +0 -926
  77. package/src/types.ts +0 -184
@@ -1,14 +1,24 @@
1
+ /**
2
+ * Property types and schemas for Obsidian notes
3
+ */
1
4
  import { z } from "zod";
2
- // Define validation schemas
3
- // Allow any string for type to be more flexible
5
+ /**
6
+ * Define validation schemas
7
+ * Allow any string for type to be more flexible
8
+ */
4
9
  export const PropertyType = z.string();
10
+ /**
11
+ * Valid status values for notes
12
+ */
5
13
  export const StatusEnum = z.enum([
6
14
  "draft",
7
15
  "in-progress",
8
16
  "review",
9
17
  "complete"
10
18
  ]);
11
- // Schema for reading properties (includes timestamps)
19
+ /**
20
+ * Schema for reading properties (includes timestamps)
21
+ */
12
22
  export const ObsidianPropertiesSchema = z.object({
13
23
  // Basic Metadata
14
24
  // Note: Timestamps are managed automatically
@@ -32,7 +42,9 @@ export const ObsidianPropertiesSchema = z.object({
32
42
  // Custom Fields
33
43
  custom: z.record(z.unknown()).optional()
34
44
  });
35
- // Schema for validating property updates (excludes timestamps)
45
+ /**
46
+ * Schema for validating property updates (excludes timestamps)
47
+ */
36
48
  export const PropertyUpdateSchema = z.object({
37
49
  // Basic Metadata
38
50
  title: z.string().optional(),
@@ -54,4 +66,4 @@ export const PropertyUpdateSchema = z.object({
54
66
  // Custom Fields
55
67
  custom: z.record(z.unknown()).optional()
56
68
  });
57
- //# sourceMappingURL=propertyTypes.js.map
69
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1,203 @@
1
+ import { PropertyManager } from "../properties/manager.js";
2
+ import { BaseToolHandler } from "../base.js";
3
+ import { createLogger } from "../../utils/logging.js";
4
+ // Create a logger for complex search operations
5
+ const logger = createLogger('ComplexSearchTools');
6
+ /**
7
+ * Tool name for complex search
8
+ */
9
+ export const COMPLEX_SEARCH_TOOL_NAME = "obsidian_complex_search";
10
+ /**
11
+ * Tool handler for complex JsonLogic searches
12
+ */
13
+ export class ComplexSearchToolHandler extends BaseToolHandler {
14
+ constructor(client) {
15
+ super(COMPLEX_SEARCH_TOOL_NAME, client);
16
+ }
17
+ getToolDescription() {
18
+ return {
19
+ name: this.name,
20
+ description: "File path pattern matching using JsonLogic queries. Supported operations:\n- glob: Pattern matching for paths (e.g., \"*.md\")\n- Variable access: {\"var\": \"path\"}\n\nNote: For full-text content search, date-based searches, or other advanced queries, use obsidian_find_in_file instead.",
21
+ examples: [
22
+ {
23
+ description: "Find markdown files in Projects folder",
24
+ args: {
25
+ query: {
26
+ "glob": ["Projects/*.md", { "var": "path" }]
27
+ }
28
+ }
29
+ },
30
+ {
31
+ description: "Find files in a specific subfolder",
32
+ args: {
33
+ query: {
34
+ "glob": ["**/Test/*.md", { "var": "path" }]
35
+ }
36
+ }
37
+ }
38
+ ],
39
+ inputSchema: {
40
+ type: "object",
41
+ properties: {
42
+ query: {
43
+ type: "object",
44
+ description: "JsonLogic query object. Example: {\"glob\": [\"*.md\", {\"var\": \"path\"}]} matches all markdown files"
45
+ }
46
+ },
47
+ required: ["query"]
48
+ }
49
+ };
50
+ }
51
+ async runTool(args) {
52
+ try {
53
+ logger.debug(`Executing complex search with query: ${JSON.stringify(args.query)}`);
54
+ // Perform search
55
+ const results = await this.client.searchJson(args.query);
56
+ // Format response based on result type
57
+ const formattedResults = results.map(result => {
58
+ if ('matches' in result) {
59
+ // SimpleSearchResult
60
+ return {
61
+ filename: result.filename,
62
+ matches: result.matches,
63
+ score: result.score
64
+ };
65
+ }
66
+ else {
67
+ // SearchResult
68
+ return {
69
+ filename: result.filename,
70
+ result: result.result
71
+ };
72
+ }
73
+ });
74
+ logger.debug(`Complex search found ${results.length} results`);
75
+ return this.createResponse({
76
+ message: `Found ${results.length} result(s)`,
77
+ results: formattedResults
78
+ });
79
+ }
80
+ catch (error) {
81
+ logger.error(`Complex search error: ${error}`);
82
+ return this.handleError(error);
83
+ }
84
+ }
85
+ }
86
+ /**
87
+ * Tool handler for getting all tags used in the vault
88
+ */
89
+ export class GetTagsToolHandler extends BaseToolHandler {
90
+ propertyManager;
91
+ constructor(client) {
92
+ super("obsidian_get_tags", client);
93
+ this.propertyManager = new PropertyManager(client);
94
+ }
95
+ getToolDescription() {
96
+ return {
97
+ name: this.name,
98
+ description: "Get all tags used across the Obsidian vault with their usage counts. Optionally filter tags within a specific folder.",
99
+ examples: [
100
+ {
101
+ description: "Get all tags in vault",
102
+ args: {}
103
+ },
104
+ {
105
+ description: "Get tags in Projects folder",
106
+ args: {
107
+ path: "Projects"
108
+ }
109
+ },
110
+ {
111
+ description: "Example response",
112
+ args: {},
113
+ response: {
114
+ "tags": [
115
+ {
116
+ "name": "project",
117
+ "count": 15,
118
+ "files": [
119
+ "Projects/ProjectA.md",
120
+ "Projects/ProjectB.md"
121
+ ]
122
+ }
123
+ ],
124
+ "metadata": {
125
+ "totalOccurrences": 45,
126
+ "uniqueTags": 12,
127
+ "scannedFiles": 30
128
+ }
129
+ }
130
+ }
131
+ ],
132
+ inputSchema: {
133
+ type: "object",
134
+ properties: {
135
+ path: {
136
+ type: "string",
137
+ description: "Optional path to limit tag search to specific folder",
138
+ format: "path"
139
+ }
140
+ }
141
+ }
142
+ };
143
+ }
144
+ async runTool(args) {
145
+ try {
146
+ logger.debug(`Getting tags${args.path ? ` in path: ${args.path}` : ' in whole vault'}`);
147
+ const tagMap = new Map();
148
+ // Use searchJson to find files with .md extension
149
+ const query = args.path
150
+ ? { "glob": [`${args.path}/**/*.md`.replace(/\\/g, '/'), { "var": "path" }] }
151
+ : { "glob": ["**/*.md", { "var": "path" }] };
152
+ const results = await this.client.searchJson(query);
153
+ let scannedFiles = 0;
154
+ // Process each file to extract tags from frontmatter
155
+ for (const result of results) {
156
+ if (!('filename' in result))
157
+ continue;
158
+ try {
159
+ const content = await this.client.getFileContents(result.filename);
160
+ scannedFiles++;
161
+ // Use PropertyManager to properly parse frontmatter
162
+ const properties = this.propertyManager.parseProperties(content);
163
+ // Process tags if they exist
164
+ if (properties.tags && Array.isArray(properties.tags)) {
165
+ // Process each tag
166
+ for (const tag of properties.tags) {
167
+ const cleanTag = tag.replace(/^#/, ''); // Remove leading # if present
168
+ if (!tagMap.has(cleanTag)) {
169
+ tagMap.set(cleanTag, new Set());
170
+ }
171
+ tagMap.get(cleanTag).add(result.filename);
172
+ }
173
+ }
174
+ }
175
+ catch (error) {
176
+ logger.error(`Failed to process file ${result.filename}:`, error);
177
+ }
178
+ }
179
+ // Calculate total occurrences
180
+ const totalOccurrences = Array.from(tagMap.values())
181
+ .reduce((sum, files) => sum + files.size, 0);
182
+ return this.createResponse({
183
+ tags: Array.from(tagMap.entries())
184
+ .map(([name, files]) => ({
185
+ name,
186
+ count: files.size,
187
+ files: Array.from(files).sort()
188
+ }))
189
+ .sort((a, b) => b.count - a.count || a.name.localeCompare(b.name)),
190
+ metadata: {
191
+ totalOccurrences,
192
+ uniqueTags: tagMap.size,
193
+ scannedFiles,
194
+ lastUpdate: Date.now()
195
+ }
196
+ });
197
+ }
198
+ catch (error) {
199
+ return this.handleError(error);
200
+ }
201
+ }
202
+ }
203
+ //# sourceMappingURL=complex.js.map
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Search tools exports
3
+ */
4
+ export * from './simple.js';
5
+ export * from './complex.js';
6
+ import { FindInFileToolHandler } from './simple.js';
7
+ import { ComplexSearchToolHandler, GetTagsToolHandler } from './complex.js';
8
+ /**
9
+ * Create all search-related tool handlers
10
+ * @param client The ObsidianClient instance
11
+ * @returns Array of search tool handlers
12
+ */
13
+ export function createSearchToolHandlers(client) {
14
+ return [
15
+ new FindInFileToolHandler(client),
16
+ new ComplexSearchToolHandler(client),
17
+ new GetTagsToolHandler(client)
18
+ ];
19
+ }
20
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,127 @@
1
+ import { BaseToolHandler } from "../base.js";
2
+ import { createLogger } from "../../utils/logging.js";
3
+ // Create a logger for search operations
4
+ const logger = createLogger('SimpleSearchTools');
5
+ /**
6
+ * Tool name for simple search
7
+ */
8
+ export const SIMPLE_SEARCH_TOOL_NAME = "obsidian_find_in_file";
9
+ /**
10
+ * Tool handler for simple text search across files
11
+ */
12
+ export class FindInFileToolHandler extends BaseToolHandler {
13
+ constructor(client) {
14
+ super(SIMPLE_SEARCH_TOOL_NAME, client);
15
+ }
16
+ getToolDescription() {
17
+ return {
18
+ name: this.name,
19
+ description: "Full-text search across all files in the vault. Returns matching files with surrounding context for each match. For results with more than 5 matching files, returns only file names and match counts to prevent overwhelming responses. Useful for finding specific content, references, or patterns across notes.",
20
+ examples: [
21
+ {
22
+ description: "Search for a specific term",
23
+ args: {
24
+ query: "neural networks",
25
+ contextLength: 20
26
+ }
27
+ },
28
+ {
29
+ description: "Search with default context",
30
+ args: {
31
+ query: "#todo"
32
+ },
33
+ response: {
34
+ "message": "Found 1 file with matches:",
35
+ "results": [
36
+ {
37
+ "filename": "Projects/AI.md",
38
+ "matches": [
39
+ {
40
+ "context": "Research needed:\n#todo Implement transformer architecture\nDeadline: Next week",
41
+ "match": { "start": 15, "end": 45 }
42
+ }
43
+ ]
44
+ }
45
+ ]
46
+ }
47
+ },
48
+ {
49
+ description: "Example response with many matches (file-only format)",
50
+ args: {
51
+ query: "API"
52
+ },
53
+ response: {
54
+ "message": "Found 92 files with matches. Showing file names only:",
55
+ "results": [
56
+ {
57
+ "filename": "Developer/Documentation/API.md",
58
+ "matchCount": 43
59
+ },
60
+ {
61
+ "filename": "Projects/API_Design.md",
62
+ "matchCount": 34
63
+ }
64
+ ]
65
+ }
66
+ }
67
+ ],
68
+ inputSchema: {
69
+ type: "object",
70
+ properties: {
71
+ query: {
72
+ type: "string",
73
+ description: "Text pattern to search for. Can include tags, keywords, or phrases."
74
+ },
75
+ contextLength: {
76
+ type: "integer",
77
+ description: "Number of characters to include before and after each match for context (default: 10)",
78
+ default: 10
79
+ }
80
+ },
81
+ required: ["query"]
82
+ }
83
+ };
84
+ }
85
+ async runTool(args) {
86
+ try {
87
+ logger.debug(`Searching for "${args.query}" with context length: ${args.contextLength || 100}`);
88
+ const results = await this.client.search(args.query, args.contextLength ?? 100);
89
+ // If more than 5 results, only return filenames and match counts to prevent overwhelming responses
90
+ if (results.length > 5) {
91
+ const fileOnlyResults = results.map(result => ({
92
+ filename: result.filename,
93
+ matchCount: result.matches.length
94
+ }));
95
+ logger.debug(`Found ${results.length} files with matches, returning file-only format`);
96
+ return this.createResponse({
97
+ message: `Found ${results.length} files with matches. Showing file names only:`,
98
+ results: fileOnlyResults
99
+ });
100
+ }
101
+ // Otherwise return full context as before
102
+ const formattedResults = results.map(result => ({
103
+ filename: result.filename,
104
+ matches: result.matches.map(match => ({
105
+ context: match.context,
106
+ match: {
107
+ text: match.context.substring(match.match.start, match.match.end),
108
+ position: {
109
+ start: match.match.start,
110
+ end: match.match.end
111
+ }
112
+ }
113
+ })),
114
+ score: result.score
115
+ }));
116
+ logger.debug(`Found ${results.length} files with matches, returning with context`);
117
+ return this.createResponse({
118
+ message: `Found ${results.length} file(s) with matches:`,
119
+ results: formattedResults
120
+ });
121
+ }
122
+ catch (error) {
123
+ return this.handleError(error);
124
+ }
125
+ }
126
+ }
127
+ //# sourceMappingURL=simple.js.map
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Error handling utilities for the Obsidian MCP Server
3
+ */
4
+ /**
5
+ * Error class for Obsidian MCP Server specific errors
6
+ */
7
+ export class ObsidianError extends Error {
8
+ details;
9
+ errorCode;
10
+ constructor(message, errorCode = 50000, // Default server error code
11
+ details) {
12
+ super(message);
13
+ this.details = details;
14
+ this.name = "ObsidianError";
15
+ // Ensure 5-digit error code
16
+ if (errorCode < 10000 || errorCode > 99999) {
17
+ // Convert HTTP status codes to 5-digit codes
18
+ // 4xx -> 4xxxx
19
+ // 5xx -> 5xxxx
20
+ this.errorCode = errorCode < 1000 ? errorCode * 100 : 50000;
21
+ }
22
+ else {
23
+ this.errorCode = errorCode;
24
+ }
25
+ }
26
+ // Convert to API error format
27
+ toApiError() {
28
+ return {
29
+ errorCode: this.errorCode,
30
+ message: this.message
31
+ };
32
+ }
33
+ }
34
+ /**
35
+ * Maps HTTP status codes to internal error codes
36
+ */
37
+ export function getErrorCodeFromStatus(status) {
38
+ switch (status) {
39
+ case 400: return 40000; // Bad request
40
+ case 401: return 40100; // Unauthorized
41
+ case 403: return 40300; // Forbidden
42
+ case 404: return 40400; // Not found
43
+ case 405: return 40500; // Method not allowed
44
+ case 409: return 40900; // Conflict
45
+ case 429: return 42900; // Too many requests
46
+ case 500: return 50000; // Internal server error
47
+ case 501: return 50100; // Not implemented
48
+ case 502: return 50200; // Bad gateway
49
+ case 503: return 50300; // Service unavailable
50
+ case 504: return 50400; // Gateway timeout
51
+ default:
52
+ if (status >= 400 && status < 500)
53
+ return 40000 + (status - 400) * 100;
54
+ if (status >= 500 && status < 600)
55
+ return 50000 + (status - 500) * 100;
56
+ return 50000;
57
+ }
58
+ }
59
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Exports all utility functions and classes
3
+ */
4
+ export * from './errors.js';
5
+ export * from './logging.js';
6
+ export * from './rate-limiting.js';
7
+ export * from './tokenization.js';
8
+ export * from './validation.js';
9
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Logging utilities for the Obsidian MCP Server
3
+ */
4
+ /**
5
+ * Log levels
6
+ */
7
+ export var LogLevel;
8
+ (function (LogLevel) {
9
+ LogLevel[LogLevel["ERROR"] = 0] = "ERROR";
10
+ LogLevel[LogLevel["WARN"] = 1] = "WARN";
11
+ LogLevel[LogLevel["INFO"] = 2] = "INFO";
12
+ LogLevel[LogLevel["DEBUG"] = 3] = "DEBUG";
13
+ LogLevel[LogLevel["TRACE"] = 4] = "TRACE";
14
+ })(LogLevel || (LogLevel = {}));
15
+ /**
16
+ * Default logger configuration
17
+ */
18
+ const DEFAULT_CONFIG = {
19
+ level: process.env.NODE_ENV === 'production'
20
+ ? LogLevel.INFO
21
+ : LogLevel.DEBUG,
22
+ includeTimestamps: true,
23
+ includeLevel: true
24
+ };
25
+ /**
26
+ * Simple logger for MCP server operations
27
+ */
28
+ export class Logger {
29
+ name;
30
+ config;
31
+ constructor(name, config = {}) {
32
+ this.name = name;
33
+ this.config = { ...DEFAULT_CONFIG, ...config };
34
+ }
35
+ /**
36
+ * Internal method to format and output a log message
37
+ */
38
+ log(level, message, ...args) {
39
+ if (level > this.config.level)
40
+ return;
41
+ const parts = [];
42
+ // Add timestamp if configured
43
+ if (this.config.includeTimestamps) {
44
+ parts.push(`[${new Date().toISOString()}]`);
45
+ }
46
+ // Add level if configured
47
+ if (this.config.includeLevel) {
48
+ const levelStr = LogLevel[level] || 'UNKNOWN';
49
+ parts.push(`[${levelStr}]`);
50
+ }
51
+ // Add component name
52
+ parts.push(`[${this.name}]`);
53
+ // Add message
54
+ parts.push(message);
55
+ // Output to console
56
+ const output = parts.join(' ');
57
+ switch (level) {
58
+ case LogLevel.ERROR:
59
+ console.error(output, ...args);
60
+ break;
61
+ case LogLevel.WARN:
62
+ console.warn(output, ...args);
63
+ break;
64
+ case LogLevel.INFO:
65
+ console.info(output, ...args);
66
+ break;
67
+ case LogLevel.DEBUG:
68
+ case LogLevel.TRACE:
69
+ default:
70
+ console.debug(output, ...args);
71
+ break;
72
+ }
73
+ }
74
+ /**
75
+ * Log an error message
76
+ */
77
+ error(message, ...args) {
78
+ this.log(LogLevel.ERROR, message, ...args);
79
+ }
80
+ /**
81
+ * Log a warning message
82
+ */
83
+ warn(message, ...args) {
84
+ this.log(LogLevel.WARN, message, ...args);
85
+ }
86
+ /**
87
+ * Log an info message
88
+ */
89
+ info(message, ...args) {
90
+ this.log(LogLevel.INFO, message, ...args);
91
+ }
92
+ /**
93
+ * Log a debug message
94
+ */
95
+ debug(message, ...args) {
96
+ this.log(LogLevel.DEBUG, message, ...args);
97
+ }
98
+ /**
99
+ * Log a trace message
100
+ */
101
+ trace(message, ...args) {
102
+ this.log(LogLevel.TRACE, message, ...args);
103
+ }
104
+ /**
105
+ * Set the log level
106
+ */
107
+ setLevel(level) {
108
+ this.config.level = level;
109
+ }
110
+ }
111
+ /**
112
+ * Create and return a namespaced logger
113
+ */
114
+ export function createLogger(name, config) {
115
+ return new Logger(name, config);
116
+ }
117
+ // Create a global root logger
118
+ export const rootLogger = createLogger('mcp-server');
119
+ //# sourceMappingURL=logging.js.map
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Rate limiting utilities for the Obsidian MCP Server
3
+ */
4
+ import { ObsidianError } from './errors.js';
5
+ /**
6
+ * Default rate limit configuration
7
+ */
8
+ export const DEFAULT_RATE_LIMIT_CONFIG = {
9
+ windowMs: 15 * 60 * 1000, // 15 minutes
10
+ maxRequests: 200
11
+ };
12
+ /**
13
+ * RateLimit manages request rate limiting for API endpoints
14
+ */
15
+ export class RateLimiter {
16
+ config;
17
+ requestCounts = new Map();
18
+ cleanupInterval;
19
+ constructor(config = DEFAULT_RATE_LIMIT_CONFIG) {
20
+ this.config = config;
21
+ // Clean up expired rate limit entries periodically
22
+ this.cleanupInterval = setInterval(() => this.cleanup(), 60000); // Clean up every minute
23
+ }
24
+ /**
25
+ * Check if a request is within rate limits
26
+ * @param key Identifier for the rate limit bucket (e.g., toolName)
27
+ * @returns Whether the request is allowed
28
+ */
29
+ checkRateLimit(key) {
30
+ const now = Date.now();
31
+ const requestInfo = this.requestCounts.get(key);
32
+ if (!requestInfo || now > requestInfo.resetTime) {
33
+ // Reset counter for new window
34
+ this.requestCounts.set(key, {
35
+ count: 1,
36
+ resetTime: now + this.config.windowMs
37
+ });
38
+ return true;
39
+ }
40
+ if (requestInfo.count >= this.config.maxRequests) {
41
+ return false;
42
+ }
43
+ requestInfo.count++;
44
+ return true;
45
+ }
46
+ /**
47
+ * Check rate limit and throw an error if exceeded
48
+ * @param key Identifier for the rate limit bucket
49
+ * @throws ObsidianError if rate limit is exceeded
50
+ */
51
+ enforceRateLimit(key) {
52
+ if (!this.checkRateLimit(key)) {
53
+ throw new ObsidianError(`Rate limit exceeded for ${key}. Please try again later.`, 42900 // 42900 = Rate limit exceeded
54
+ );
55
+ }
56
+ }
57
+ /**
58
+ * Get information about current rate limit status
59
+ * @param key Identifier for the rate limit bucket
60
+ * @returns Rate limit information or null if no requests have been made
61
+ */
62
+ getRateLimitInfo(key) {
63
+ const requestInfo = this.requestCounts.get(key);
64
+ if (!requestInfo) {
65
+ return null;
66
+ }
67
+ return {
68
+ remaining: Math.max(0, this.config.maxRequests - requestInfo.count),
69
+ resetTime: requestInfo.resetTime
70
+ };
71
+ }
72
+ /**
73
+ * Clean up expired rate limit entries
74
+ */
75
+ cleanup() {
76
+ const now = Date.now();
77
+ for (const [key, info] of this.requestCounts.entries()) {
78
+ if (now > info.resetTime) {
79
+ this.requestCounts.delete(key);
80
+ }
81
+ }
82
+ }
83
+ /**
84
+ * Clean up resources (e.g., when shutting down)
85
+ */
86
+ dispose() {
87
+ if (this.cleanupInterval) {
88
+ clearInterval(this.cleanupInterval);
89
+ }
90
+ }
91
+ }
92
+ // Export a singleton instance with default configuration
93
+ export const rateLimiter = new RateLimiter();
94
+ //# sourceMappingURL=rate-limiting.js.map