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.
- package/LICENSE +201 -0
- package/README.md +233 -0
- package/dist/config/index.d.ts +73 -0
- package/dist/config/index.js +125 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +162 -0
- package/dist/mcp-client/client.d.ts +36 -0
- package/dist/mcp-client/client.js +276 -0
- package/dist/mcp-client/configLoader.d.ts +75 -0
- package/dist/mcp-client/configLoader.js +203 -0
- package/dist/mcp-client/index.d.ts +10 -0
- package/dist/mcp-client/index.js +14 -0
- package/dist/mcp-client/transport.d.ts +34 -0
- package/dist/mcp-client/transport.js +183 -0
- package/dist/mcp-server/resources/echoResource/echoResourceLogic.d.ts +38 -0
- package/dist/mcp-server/resources/echoResource/echoResourceLogic.js +40 -0
- package/dist/mcp-server/resources/echoResource/index.d.ts +5 -0
- package/dist/mcp-server/resources/echoResource/index.js +5 -0
- package/dist/mcp-server/resources/echoResource/registration.d.ts +12 -0
- package/dist/mcp-server/resources/echoResource/registration.js +122 -0
- package/dist/mcp-server/server.d.ts +27 -0
- package/dist/mcp-server/server.js +176 -0
- package/dist/mcp-server/tools/echoTool/echoToolLogic.d.ts +68 -0
- package/dist/mcp-server/tools/echoTool/echoToolLogic.js +73 -0
- package/dist/mcp-server/tools/echoTool/index.d.ts +5 -0
- package/dist/mcp-server/tools/echoTool/index.js +5 -0
- package/dist/mcp-server/tools/echoTool/registration.d.ts +12 -0
- package/dist/mcp-server/tools/echoTool/registration.js +86 -0
- package/dist/mcp-server/transports/authentication/authMiddleware.d.ts +57 -0
- package/dist/mcp-server/transports/authentication/authMiddleware.js +145 -0
- package/dist/mcp-server/transports/httpTransport.d.ts +23 -0
- package/dist/mcp-server/transports/httpTransport.js +411 -0
- package/dist/mcp-server/transports/stdioTransport.d.ts +40 -0
- package/dist/mcp-server/transports/stdioTransport.js +70 -0
- package/dist/types-global/errors.d.ts +73 -0
- package/dist/types-global/errors.js +66 -0
- package/dist/utils/index.d.ts +4 -0
- package/dist/utils/index.js +12 -0
- package/dist/utils/internal/errorHandler.d.ts +90 -0
- package/dist/utils/internal/errorHandler.js +247 -0
- package/dist/utils/internal/index.d.ts +3 -0
- package/dist/utils/internal/index.js +3 -0
- package/dist/utils/internal/logger.d.ts +50 -0
- package/dist/utils/internal/logger.js +267 -0
- package/dist/utils/internal/requestContext.d.ts +47 -0
- package/dist/utils/internal/requestContext.js +48 -0
- package/dist/utils/metrics/index.d.ts +1 -0
- package/dist/utils/metrics/index.js +1 -0
- package/dist/utils/metrics/tokenCounter.d.ts +27 -0
- package/dist/utils/metrics/tokenCounter.js +124 -0
- package/dist/utils/parsing/dateParser.d.ts +27 -0
- package/dist/utils/parsing/dateParser.js +62 -0
- package/dist/utils/parsing/index.d.ts +2 -0
- package/dist/utils/parsing/index.js +2 -0
- package/dist/utils/parsing/jsonParser.d.ts +46 -0
- package/dist/utils/parsing/jsonParser.js +79 -0
- package/dist/utils/security/idGenerator.d.ts +93 -0
- package/dist/utils/security/idGenerator.js +147 -0
- package/dist/utils/security/index.d.ts +3 -0
- package/dist/utils/security/index.js +3 -0
- package/dist/utils/security/rateLimiter.d.ts +92 -0
- package/dist/utils/security/rateLimiter.js +171 -0
- package/dist/utils/security/sanitization.d.ts +180 -0
- package/dist/utils/security/sanitization.js +372 -0
- 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,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
|
+
});
|