mcp-ts-template 1.1.6

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 (65) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +233 -0
  3. package/dist/config/index.d.ts +73 -0
  4. package/dist/config/index.js +125 -0
  5. package/dist/index.d.ts +2 -0
  6. package/dist/index.js +162 -0
  7. package/dist/mcp-client/client.d.ts +36 -0
  8. package/dist/mcp-client/client.js +276 -0
  9. package/dist/mcp-client/configLoader.d.ts +75 -0
  10. package/dist/mcp-client/configLoader.js +203 -0
  11. package/dist/mcp-client/index.d.ts +10 -0
  12. package/dist/mcp-client/index.js +14 -0
  13. package/dist/mcp-client/transport.d.ts +34 -0
  14. package/dist/mcp-client/transport.js +183 -0
  15. package/dist/mcp-server/resources/echoResource/echoResourceLogic.d.ts +38 -0
  16. package/dist/mcp-server/resources/echoResource/echoResourceLogic.js +40 -0
  17. package/dist/mcp-server/resources/echoResource/index.d.ts +5 -0
  18. package/dist/mcp-server/resources/echoResource/index.js +5 -0
  19. package/dist/mcp-server/resources/echoResource/registration.d.ts +12 -0
  20. package/dist/mcp-server/resources/echoResource/registration.js +122 -0
  21. package/dist/mcp-server/server.d.ts +27 -0
  22. package/dist/mcp-server/server.js +176 -0
  23. package/dist/mcp-server/tools/echoTool/echoToolLogic.d.ts +68 -0
  24. package/dist/mcp-server/tools/echoTool/echoToolLogic.js +73 -0
  25. package/dist/mcp-server/tools/echoTool/index.d.ts +5 -0
  26. package/dist/mcp-server/tools/echoTool/index.js +5 -0
  27. package/dist/mcp-server/tools/echoTool/registration.d.ts +12 -0
  28. package/dist/mcp-server/tools/echoTool/registration.js +86 -0
  29. package/dist/mcp-server/transports/authentication/authMiddleware.d.ts +57 -0
  30. package/dist/mcp-server/transports/authentication/authMiddleware.js +145 -0
  31. package/dist/mcp-server/transports/httpTransport.d.ts +23 -0
  32. package/dist/mcp-server/transports/httpTransport.js +411 -0
  33. package/dist/mcp-server/transports/stdioTransport.d.ts +40 -0
  34. package/dist/mcp-server/transports/stdioTransport.js +70 -0
  35. package/dist/types-global/errors.d.ts +73 -0
  36. package/dist/types-global/errors.js +66 -0
  37. package/dist/utils/index.d.ts +4 -0
  38. package/dist/utils/index.js +12 -0
  39. package/dist/utils/internal/errorHandler.d.ts +90 -0
  40. package/dist/utils/internal/errorHandler.js +247 -0
  41. package/dist/utils/internal/index.d.ts +3 -0
  42. package/dist/utils/internal/index.js +3 -0
  43. package/dist/utils/internal/logger.d.ts +50 -0
  44. package/dist/utils/internal/logger.js +267 -0
  45. package/dist/utils/internal/requestContext.d.ts +47 -0
  46. package/dist/utils/internal/requestContext.js +48 -0
  47. package/dist/utils/metrics/index.d.ts +1 -0
  48. package/dist/utils/metrics/index.js +1 -0
  49. package/dist/utils/metrics/tokenCounter.d.ts +27 -0
  50. package/dist/utils/metrics/tokenCounter.js +124 -0
  51. package/dist/utils/parsing/dateParser.d.ts +27 -0
  52. package/dist/utils/parsing/dateParser.js +62 -0
  53. package/dist/utils/parsing/index.d.ts +2 -0
  54. package/dist/utils/parsing/index.js +2 -0
  55. package/dist/utils/parsing/jsonParser.d.ts +46 -0
  56. package/dist/utils/parsing/jsonParser.js +79 -0
  57. package/dist/utils/security/idGenerator.d.ts +93 -0
  58. package/dist/utils/security/idGenerator.js +147 -0
  59. package/dist/utils/security/index.d.ts +3 -0
  60. package/dist/utils/security/index.js +3 -0
  61. package/dist/utils/security/rateLimiter.d.ts +92 -0
  62. package/dist/utils/security/rateLimiter.js +171 -0
  63. package/dist/utils/security/sanitization.d.ts +180 -0
  64. package/dist/utils/security/sanitization.js +372 -0
  65. package/package.json +79 -0
@@ -0,0 +1,79 @@
1
+ import { parse as parsePartialJson, Allow as PartialJsonAllow } from 'partial-json';
2
+ import { BaseErrorCode, McpError } from '../../types-global/errors.js';
3
+ // Import utils from the main barrel file (logger, RequestContext from ../internal/*)
4
+ import { logger } from '../index.js';
5
+ /**
6
+ * Enum mirroring partial-json's Allow constants for specifying
7
+ * what types of partial JSON structures are permissible during parsing.
8
+ * Use bitwise OR to combine options (e.g., Allow.STR | Allow.OBJ).
9
+ */
10
+ export const Allow = PartialJsonAllow;
11
+ // Regex to find a <think> block at the start, capturing its content and the rest of the string
12
+ const thinkBlockRegex = /^<think>([\s\S]*?)<\/think>\s*([\s\S]*)$/;
13
+ /**
14
+ * Utility class for parsing potentially partial JSON strings.
15
+ * Wraps the 'partial-json' library to provide a consistent interface
16
+ * within the atlas-mcp-agent project.
17
+ * Handles optional <think>...</think> blocks at the beginning of the input.
18
+ */
19
+ class JsonParser {
20
+ /**
21
+ * Parses a JSON string, potentially allowing for incomplete structures
22
+ * and handling optional <think> blocks at the start.
23
+ *
24
+ * @param jsonString The JSON string to parse.
25
+ * @param allowPartial A bitwise OR combination of 'Allow' constants specifying permissible partial types (defaults to Allow.ALL).
26
+ * @param context Optional RequestContext for error correlation and logging think blocks.
27
+ * @returns The parsed JavaScript value.
28
+ * @throws {McpError} Throws an McpError with BaseErrorCode.VALIDATION_ERROR if parsing fails due to malformed JSON.
29
+ */
30
+ parse(jsonString, allowPartial = Allow.ALL, context) {
31
+ let stringToParse = jsonString;
32
+ const match = jsonString.match(thinkBlockRegex);
33
+ if (match) {
34
+ const thinkContent = match[1].trim();
35
+ const restOfString = match[2];
36
+ if (thinkContent) {
37
+ logger.debug('LLM <think> block detected and logged.', { ...context, thinkContent });
38
+ }
39
+ else {
40
+ logger.debug('Empty LLM <think> block detected.', context);
41
+ }
42
+ stringToParse = restOfString; // Parse only the part after </think>
43
+ }
44
+ // Trim leading/trailing whitespace which might interfere with JSON parsing, especially if only JSON is left
45
+ stringToParse = stringToParse.trim();
46
+ if (!stringToParse) {
47
+ // If after removing think block and trimming, the string is empty, it's an error
48
+ throw new McpError(BaseErrorCode.VALIDATION_ERROR, 'JSON string is empty after removing <think> block.', context);
49
+ }
50
+ try {
51
+ // Ensure the string starts with '{' or '[' if we expect an object or array after stripping <think>
52
+ // This helps catch cases where only non-JSON text remains.
53
+ if (!stringToParse.startsWith('{') && !stringToParse.startsWith('[')) {
54
+ // Check if it might be a simple string value that partial-json could parse
55
+ // Allow simple strings only if specifically permitted or Allow.ALL is used
56
+ const allowsString = (allowPartial & Allow.STR) === Allow.STR;
57
+ if (!allowsString && !stringToParse.startsWith('"')) { // Allow quoted strings if Allow.STR is set
58
+ throw new Error('Remaining content does not appear to be valid JSON object or array.');
59
+ }
60
+ // If it starts with a quote and strings are allowed, let parsePartialJson handle it
61
+ }
62
+ return parsePartialJson(stringToParse, allowPartial);
63
+ }
64
+ catch (error) {
65
+ // Wrap the original error in an McpError for consistent error handling
66
+ // Include the original error message for better debugging context.
67
+ logger.error('Failed to parse JSON content.', { ...context, error: error.message, contentAttempted: stringToParse });
68
+ throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Failed to parse JSON: ${error.message}`, {
69
+ ...context,
70
+ originalContent: stringToParse,
71
+ rawError: error instanceof Error ? error.stack : String(error) // Include raw error info
72
+ });
73
+ }
74
+ }
75
+ }
76
+ /**
77
+ * Singleton instance of the JsonParser utility.
78
+ */
79
+ export const jsonParser = new JsonParser();
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Interface for entity prefix configuration
3
+ */
4
+ export interface EntityPrefixConfig {
5
+ [key: string]: string;
6
+ }
7
+ /**
8
+ * ID Generation Options
9
+ */
10
+ export interface IdGenerationOptions {
11
+ length?: number;
12
+ separator?: string;
13
+ charset?: string;
14
+ }
15
+ /**
16
+ * Generic ID Generator class for creating and managing unique identifiers
17
+ */
18
+ export declare class IdGenerator {
19
+ private static DEFAULT_CHARSET;
20
+ private static DEFAULT_SEPARATOR;
21
+ private static DEFAULT_LENGTH;
22
+ private entityPrefixes;
23
+ private prefixToEntityType;
24
+ /**
25
+ * Constructor that accepts entity prefix configuration
26
+ * @param entityPrefixes Map of entity types to their prefixes
27
+ */
28
+ constructor(entityPrefixes?: EntityPrefixConfig);
29
+ /**
30
+ * Set or update entity prefixes and rebuild the reverse lookup
31
+ * @param entityPrefixes Map of entity types to their prefixes
32
+ */
33
+ setEntityPrefixes(entityPrefixes: EntityPrefixConfig): void;
34
+ /**
35
+ * Get all registered entity prefixes
36
+ * @returns The entity prefix configuration
37
+ */
38
+ getEntityPrefixes(): EntityPrefixConfig;
39
+ /**
40
+ * Generates a cryptographically secure random alphanumeric string
41
+ * @param length The length of the random string to generate
42
+ * @param charset Optional custom character set
43
+ * @returns Random alphanumeric string
44
+ */
45
+ generateRandomString(length?: number, charset?: string): string;
46
+ /**
47
+ * Generates a unique ID with an optional prefix
48
+ * @param prefix Optional prefix to add to the ID
49
+ * @param options Optional generation options
50
+ * @returns A unique identifier string
51
+ */
52
+ generate(prefix?: string, options?: IdGenerationOptions): string;
53
+ /**
54
+ * Generates a custom ID for an entity with format PREFIX_XXXXXX
55
+ * @param entityType The type of entity to generate an ID for
56
+ * @param options Optional generation options
57
+ * @returns A unique identifier string (e.g., "PROJ_A6B3J0")
58
+ * @throws {McpError} If the entity type is not registered
59
+ */
60
+ generateForEntity(entityType: string, options?: IdGenerationOptions): string;
61
+ /**
62
+ * Validates if a given ID matches the expected format for an entity type
63
+ * @param id The ID to validate
64
+ * @param entityType The expected entity type
65
+ * @param options Optional validation options
66
+ * @returns boolean indicating if the ID is valid
67
+ */
68
+ isValid(id: string, entityType: string, options?: IdGenerationOptions): boolean;
69
+ /**
70
+ * Strips the prefix from an ID
71
+ * @param id The ID to strip
72
+ * @param separator Optional custom separator
73
+ * @returns The ID without the prefix
74
+ */
75
+ stripPrefix(id: string, separator?: string): string;
76
+ /**
77
+ * Determines the entity type from an ID
78
+ * @param id The ID to get the entity type for
79
+ * @param separator Optional custom separator
80
+ * @returns The entity type
81
+ * @throws {McpError} If the ID format is invalid or entity type is unknown
82
+ */
83
+ getEntityType(id: string, separator?: string): string;
84
+ /**
85
+ * Normalizes an entity ID to ensure consistent uppercase format
86
+ * @param id The ID to normalize
87
+ * @param separator Optional custom separator
88
+ * @returns The normalized ID in uppercase format
89
+ */
90
+ normalize(id: string, separator?: string): string;
91
+ }
92
+ export declare const idGenerator: IdGenerator;
93
+ export declare const generateUUID: () => string;
@@ -0,0 +1,147 @@
1
+ import { randomBytes, randomUUID as cryptoRandomUUID } from 'crypto'; // Import cryptoRandomUUID
2
+ import { BaseErrorCode, McpError } from '../../types-global/errors.js'; // Corrected path
3
+ /**
4
+ * Generic ID Generator class for creating and managing unique identifiers
5
+ */
6
+ export class IdGenerator {
7
+ /**
8
+ * Constructor that accepts entity prefix configuration
9
+ * @param entityPrefixes Map of entity types to their prefixes
10
+ */
11
+ constructor(entityPrefixes = {}) {
12
+ // Entity prefixes
13
+ this.entityPrefixes = {};
14
+ // Reverse mapping for prefix to entity type lookup
15
+ this.prefixToEntityType = {};
16
+ this.setEntityPrefixes(entityPrefixes);
17
+ }
18
+ /**
19
+ * Set or update entity prefixes and rebuild the reverse lookup
20
+ * @param entityPrefixes Map of entity types to their prefixes
21
+ */
22
+ setEntityPrefixes(entityPrefixes) {
23
+ this.entityPrefixes = { ...entityPrefixes };
24
+ // Rebuild reverse mapping
25
+ this.prefixToEntityType = Object.entries(this.entityPrefixes).reduce((acc, [type, prefix]) => {
26
+ acc[prefix] = type;
27
+ acc[prefix.toLowerCase()] = type;
28
+ return acc;
29
+ }, {});
30
+ // Removed logger call from setEntityPrefixes to prevent logging before initialization
31
+ }
32
+ /**
33
+ * Get all registered entity prefixes
34
+ * @returns The entity prefix configuration
35
+ */
36
+ getEntityPrefixes() {
37
+ return { ...this.entityPrefixes };
38
+ }
39
+ /**
40
+ * Generates a cryptographically secure random alphanumeric string
41
+ * @param length The length of the random string to generate
42
+ * @param charset Optional custom character set
43
+ * @returns Random alphanumeric string
44
+ */
45
+ generateRandomString(length = IdGenerator.DEFAULT_LENGTH, charset = IdGenerator.DEFAULT_CHARSET) {
46
+ const bytes = randomBytes(length);
47
+ let result = '';
48
+ for (let i = 0; i < length; i++) {
49
+ result += charset[bytes[i] % charset.length];
50
+ }
51
+ return result;
52
+ }
53
+ /**
54
+ * Generates a unique ID with an optional prefix
55
+ * @param prefix Optional prefix to add to the ID
56
+ * @param options Optional generation options
57
+ * @returns A unique identifier string
58
+ */
59
+ generate(prefix, options = {}) {
60
+ const { length = IdGenerator.DEFAULT_LENGTH, separator = IdGenerator.DEFAULT_SEPARATOR, charset = IdGenerator.DEFAULT_CHARSET } = options;
61
+ const randomPart = this.generateRandomString(length, charset);
62
+ return prefix
63
+ ? `${prefix}${separator}${randomPart}`
64
+ : randomPart;
65
+ }
66
+ /**
67
+ * Generates a custom ID for an entity with format PREFIX_XXXXXX
68
+ * @param entityType The type of entity to generate an ID for
69
+ * @param options Optional generation options
70
+ * @returns A unique identifier string (e.g., "PROJ_A6B3J0")
71
+ * @throws {McpError} If the entity type is not registered
72
+ */
73
+ generateForEntity(entityType, options = {}) {
74
+ const prefix = this.entityPrefixes[entityType];
75
+ if (!prefix) {
76
+ throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Unknown entity type: ${entityType}`);
77
+ }
78
+ return this.generate(prefix, options);
79
+ }
80
+ /**
81
+ * Validates if a given ID matches the expected format for an entity type
82
+ * @param id The ID to validate
83
+ * @param entityType The expected entity type
84
+ * @param options Optional validation options
85
+ * @returns boolean indicating if the ID is valid
86
+ */
87
+ isValid(id, entityType, options = {}) {
88
+ const prefix = this.entityPrefixes[entityType];
89
+ const { length = IdGenerator.DEFAULT_LENGTH, separator = IdGenerator.DEFAULT_SEPARATOR } = options;
90
+ if (!prefix) {
91
+ return false;
92
+ }
93
+ const pattern = new RegExp(`^${prefix}${separator}[A-Z0-9]{${length}}$`);
94
+ return pattern.test(id);
95
+ }
96
+ /**
97
+ * Strips the prefix from an ID
98
+ * @param id The ID to strip
99
+ * @param separator Optional custom separator
100
+ * @returns The ID without the prefix
101
+ */
102
+ stripPrefix(id, separator = IdGenerator.DEFAULT_SEPARATOR) {
103
+ return id.split(separator)[1] || id;
104
+ }
105
+ /**
106
+ * Determines the entity type from an ID
107
+ * @param id The ID to get the entity type for
108
+ * @param separator Optional custom separator
109
+ * @returns The entity type
110
+ * @throws {McpError} If the ID format is invalid or entity type is unknown
111
+ */
112
+ getEntityType(id, separator = IdGenerator.DEFAULT_SEPARATOR) {
113
+ const parts = id.split(separator);
114
+ if (parts.length !== 2 || !parts[0]) {
115
+ throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid ID format: ${id}. Expected format: PREFIX${separator}XXXXXX`);
116
+ }
117
+ const prefix = parts[0];
118
+ const entityType = this.prefixToEntityType[prefix];
119
+ if (!entityType) {
120
+ throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Unknown entity type prefix: ${prefix}`);
121
+ }
122
+ return entityType;
123
+ }
124
+ /**
125
+ * Normalizes an entity ID to ensure consistent uppercase format
126
+ * @param id The ID to normalize
127
+ * @param separator Optional custom separator
128
+ * @returns The normalized ID in uppercase format
129
+ */
130
+ normalize(id, separator = IdGenerator.DEFAULT_SEPARATOR) {
131
+ const entityType = this.getEntityType(id, separator);
132
+ const idParts = id.split(separator);
133
+ return `${this.entityPrefixes[entityType]}${separator}${idParts[1].toUpperCase()}`;
134
+ }
135
+ }
136
+ // Default charset
137
+ IdGenerator.DEFAULT_CHARSET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
138
+ // Default separator
139
+ IdGenerator.DEFAULT_SEPARATOR = '_';
140
+ // Default random part length
141
+ IdGenerator.DEFAULT_LENGTH = 6;
142
+ // Create and export a default instance with an empty entity prefix configuration
143
+ export const idGenerator = new IdGenerator();
144
+ // For standalone use as a UUID generator
145
+ export const generateUUID = () => {
146
+ return cryptoRandomUUID(); // Use imported cryptoRandomUUID
147
+ };
@@ -0,0 +1,3 @@
1
+ export * from './sanitization.js';
2
+ export * from './rateLimiter.js';
3
+ export * from './idGenerator.js';
@@ -0,0 +1,3 @@
1
+ export * from './sanitization.js';
2
+ export * from './rateLimiter.js';
3
+ export * from './idGenerator.js';
@@ -0,0 +1,92 @@
1
+ import { RequestContext } from '../index.js';
2
+ /**
3
+ * Rate limiting configuration options
4
+ */
5
+ export interface RateLimitConfig {
6
+ /** Time window in milliseconds */
7
+ windowMs: number;
8
+ /** Maximum number of requests allowed in the window */
9
+ maxRequests: number;
10
+ /** Custom error message template */
11
+ errorMessage?: string;
12
+ /** Whether to skip rate limiting in certain environments (e.g. development) */
13
+ skipInDevelopment?: boolean;
14
+ /** Custom key generator function */
15
+ keyGenerator?: (identifier: string, context?: RequestContext) => string;
16
+ /** How often to run cleanup of expired entries (in milliseconds) */
17
+ cleanupInterval?: number;
18
+ }
19
+ /**
20
+ * Individual rate limit entry
21
+ */
22
+ export interface RateLimitEntry {
23
+ /** Current request count */
24
+ count: number;
25
+ /** When the window resets (timestamp) */
26
+ resetTime: number;
27
+ }
28
+ /**
29
+ * Generic rate limiter that can be used across the application
30
+ */
31
+ export declare class RateLimiter {
32
+ private config;
33
+ /** Map storing rate limit data */
34
+ private limits;
35
+ /** Cleanup interval timer */
36
+ private cleanupTimer;
37
+ /** Default configuration */
38
+ private static DEFAULT_CONFIG;
39
+ /**
40
+ * Create a new rate limiter
41
+ * @param config Rate limiting configuration
42
+ */
43
+ constructor(config: RateLimitConfig);
44
+ /**
45
+ * Start the cleanup timer to periodically remove expired entries
46
+ */
47
+ private startCleanupTimer;
48
+ /**
49
+ * Clean up expired rate limit entries to prevent memory leaks
50
+ */
51
+ private cleanupExpiredEntries;
52
+ /**
53
+ * Update rate limiter configuration
54
+ * @param config New configuration options
55
+ */
56
+ configure(config: Partial<RateLimitConfig>): void;
57
+ /**
58
+ * Get current configuration
59
+ * @returns Current rate limit configuration
60
+ */
61
+ getConfig(): RateLimitConfig;
62
+ /**
63
+ * Reset all rate limits
64
+ */
65
+ reset(): void;
66
+ /**
67
+ * Check if a request exceeds the rate limit
68
+ * @param key Unique identifier for the request source
69
+ * @param context Optional request context
70
+ * @throws {McpError} If rate limit is exceeded
71
+ */
72
+ check(key: string, context?: RequestContext): void;
73
+ /**
74
+ * Get rate limit information for a key
75
+ * @param key The rate limit key
76
+ * @returns Current rate limit status or null if no record exists
77
+ */
78
+ getStatus(key: string): {
79
+ current: number;
80
+ limit: number;
81
+ remaining: number;
82
+ resetTime: number;
83
+ } | null;
84
+ /**
85
+ * Stop the cleanup timer when the limiter is no longer needed
86
+ */
87
+ dispose(): void;
88
+ }
89
+ /**
90
+ * Create and export a default rate limiter instance
91
+ */
92
+ export declare const rateLimiter: RateLimiter;
@@ -0,0 +1,171 @@
1
+ import { BaseErrorCode, McpError } from '../../types-global/errors.js';
2
+ // Import config and utils
3
+ import { environment } from '../../config/index.js'; // Import environment from config
4
+ import { logger } from '../index.js';
5
+ /**
6
+ * Generic rate limiter that can be used across the application
7
+ */
8
+ export class RateLimiter {
9
+ /**
10
+ * Create a new rate limiter
11
+ * @param config Rate limiting configuration
12
+ */
13
+ constructor(config) {
14
+ this.config = config;
15
+ /** Cleanup interval timer */
16
+ this.cleanupTimer = null;
17
+ this.config = { ...RateLimiter.DEFAULT_CONFIG, ...config };
18
+ this.limits = new Map();
19
+ this.startCleanupTimer();
20
+ // Removed logger call from constructor to prevent logging before initialization
21
+ }
22
+ /**
23
+ * Start the cleanup timer to periodically remove expired entries
24
+ */
25
+ startCleanupTimer() {
26
+ if (this.cleanupTimer) {
27
+ clearInterval(this.cleanupTimer);
28
+ }
29
+ const interval = this.config.cleanupInterval ?? RateLimiter.DEFAULT_CONFIG.cleanupInterval;
30
+ if (interval) {
31
+ this.cleanupTimer = setInterval(() => {
32
+ this.cleanupExpiredEntries();
33
+ }, interval);
34
+ // Ensure the timer doesn't prevent the process from exiting
35
+ if (this.cleanupTimer.unref) {
36
+ this.cleanupTimer.unref();
37
+ }
38
+ }
39
+ }
40
+ /**
41
+ * Clean up expired rate limit entries to prevent memory leaks
42
+ */
43
+ cleanupExpiredEntries() {
44
+ const now = Date.now();
45
+ let expiredCount = 0;
46
+ // Use a synchronized approach to avoid race conditions during cleanup
47
+ for (const [key, entry] of this.limits.entries()) {
48
+ if (now >= entry.resetTime) {
49
+ this.limits.delete(key);
50
+ expiredCount++;
51
+ }
52
+ }
53
+ if (expiredCount > 0) {
54
+ logger.debug(`Cleaned up ${expiredCount} expired rate limit entries`, {
55
+ totalRemaining: this.limits.size
56
+ });
57
+ }
58
+ }
59
+ /**
60
+ * Update rate limiter configuration
61
+ * @param config New configuration options
62
+ */
63
+ configure(config) {
64
+ this.config = { ...this.config, ...config };
65
+ // Restart cleanup timer if interval changed
66
+ if (config.cleanupInterval !== undefined) {
67
+ this.startCleanupTimer();
68
+ }
69
+ }
70
+ /**
71
+ * Get current configuration
72
+ * @returns Current rate limit configuration
73
+ */
74
+ getConfig() {
75
+ return { ...this.config };
76
+ }
77
+ /**
78
+ * Reset all rate limits
79
+ */
80
+ reset() {
81
+ this.limits.clear();
82
+ logger.debug('Rate limiter reset, all limits cleared');
83
+ }
84
+ /**
85
+ * Check if a request exceeds the rate limit
86
+ * @param key Unique identifier for the request source
87
+ * @param context Optional request context
88
+ * @throws {McpError} If rate limit is exceeded
89
+ */
90
+ check(key, context) {
91
+ // Skip in development if configured, using the validated environment from config
92
+ if (this.config.skipInDevelopment && environment === 'development') {
93
+ return;
94
+ }
95
+ // Generate key using custom generator if provided
96
+ const limitKey = this.config.keyGenerator
97
+ ? this.config.keyGenerator(key, context)
98
+ : key;
99
+ const now = Date.now();
100
+ // Accessing and updating the limit entry within a single function scope
101
+ // ensures atomicity in Node.js's single-threaded event loop for Map operations.
102
+ const limit = () => {
103
+ // Get current entry or create a new one if it doesn't exist or is expired
104
+ const entry = this.limits.get(limitKey);
105
+ // Create new entry or reset if expired
106
+ if (!entry || now >= entry.resetTime) {
107
+ const newEntry = {
108
+ count: 1,
109
+ resetTime: now + this.config.windowMs
110
+ };
111
+ this.limits.set(limitKey, newEntry);
112
+ return newEntry;
113
+ }
114
+ // Check if limit exceeded
115
+ if (entry.count >= this.config.maxRequests) {
116
+ const waitTime = Math.ceil((entry.resetTime - now) / 1000);
117
+ const errorMessage = this.config.errorMessage?.replace('{waitTime}', waitTime.toString()) ||
118
+ `Rate limit exceeded. Please try again in ${waitTime} seconds.`;
119
+ throw new McpError(BaseErrorCode.RATE_LIMITED, errorMessage, { waitTime, key: limitKey });
120
+ }
121
+ // Increment counter and return updated entry
122
+ entry.count++;
123
+ return entry;
124
+ };
125
+ // Execute the rate limiting logic
126
+ limit();
127
+ }
128
+ /**
129
+ * Get rate limit information for a key
130
+ * @param key The rate limit key
131
+ * @returns Current rate limit status or null if no record exists
132
+ */
133
+ getStatus(key) {
134
+ const entry = this.limits.get(key);
135
+ if (!entry) {
136
+ return null;
137
+ }
138
+ return {
139
+ current: entry.count,
140
+ limit: this.config.maxRequests,
141
+ remaining: Math.max(0, this.config.maxRequests - entry.count),
142
+ resetTime: entry.resetTime
143
+ };
144
+ }
145
+ /**
146
+ * Stop the cleanup timer when the limiter is no longer needed
147
+ */
148
+ dispose() {
149
+ if (this.cleanupTimer) {
150
+ clearInterval(this.cleanupTimer);
151
+ this.cleanupTimer = null;
152
+ }
153
+ // Clear all entries
154
+ this.limits.clear();
155
+ }
156
+ }
157
+ /** Default configuration */
158
+ RateLimiter.DEFAULT_CONFIG = {
159
+ windowMs: 15 * 60 * 1000, // 15 minutes
160
+ maxRequests: 100, // 100 requests per window
161
+ errorMessage: 'Rate limit exceeded. Please try again in {waitTime} seconds.',
162
+ skipInDevelopment: false,
163
+ cleanupInterval: 5 * 60 * 1000 // 5 minutes
164
+ };
165
+ /**
166
+ * Create and export a default rate limiter instance
167
+ */
168
+ export const rateLimiter = new RateLimiter({
169
+ windowMs: 15 * 60 * 1000, // 15 minutes
170
+ maxRequests: 100 // 100 requests per window
171
+ });