ntfy-mcp-server 1.0.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 (59) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +423 -0
  3. package/dist/config/index.d.ts +23 -0
  4. package/dist/config/index.js +111 -0
  5. package/dist/index.d.ts +2 -0
  6. package/dist/index.js +108 -0
  7. package/dist/mcp-server/resources/ntfyResource/getNtfyTopic.d.ts +2 -0
  8. package/dist/mcp-server/resources/ntfyResource/getNtfyTopic.js +111 -0
  9. package/dist/mcp-server/resources/ntfyResource/index.d.ts +12 -0
  10. package/dist/mcp-server/resources/ntfyResource/index.js +72 -0
  11. package/dist/mcp-server/resources/ntfyResource/types.d.ts +27 -0
  12. package/dist/mcp-server/resources/ntfyResource/types.js +8 -0
  13. package/dist/mcp-server/server.d.ts +40 -0
  14. package/dist/mcp-server/server.js +245 -0
  15. package/dist/mcp-server/tools/ntfyTool/index.d.ts +11 -0
  16. package/dist/mcp-server/tools/ntfyTool/index.js +110 -0
  17. package/dist/mcp-server/tools/ntfyTool/ntfyMessage.d.ts +9 -0
  18. package/dist/mcp-server/tools/ntfyTool/ntfyMessage.js +289 -0
  19. package/dist/mcp-server/tools/ntfyTool/types.d.ts +252 -0
  20. package/dist/mcp-server/tools/ntfyTool/types.js +144 -0
  21. package/dist/mcp-server/utils/registrationHelper.d.ts +48 -0
  22. package/dist/mcp-server/utils/registrationHelper.js +63 -0
  23. package/dist/services/ntfy/constants.d.ts +37 -0
  24. package/dist/services/ntfy/constants.js +37 -0
  25. package/dist/services/ntfy/errors.d.ts +79 -0
  26. package/dist/services/ntfy/errors.js +134 -0
  27. package/dist/services/ntfy/index.d.ts +33 -0
  28. package/dist/services/ntfy/index.js +56 -0
  29. package/dist/services/ntfy/publisher.d.ts +66 -0
  30. package/dist/services/ntfy/publisher.js +229 -0
  31. package/dist/services/ntfy/subscriber.d.ts +81 -0
  32. package/dist/services/ntfy/subscriber.js +502 -0
  33. package/dist/services/ntfy/types.d.ts +161 -0
  34. package/dist/services/ntfy/types.js +4 -0
  35. package/dist/services/ntfy/utils.d.ts +85 -0
  36. package/dist/services/ntfy/utils.js +410 -0
  37. package/dist/types-global/errors.d.ts +35 -0
  38. package/dist/types-global/errors.js +39 -0
  39. package/dist/types-global/mcp.d.ts +30 -0
  40. package/dist/types-global/mcp.js +25 -0
  41. package/dist/types-global/tool.d.ts +61 -0
  42. package/dist/types-global/tool.js +99 -0
  43. package/dist/utils/errorHandler.d.ts +98 -0
  44. package/dist/utils/errorHandler.js +271 -0
  45. package/dist/utils/idGenerator.d.ts +94 -0
  46. package/dist/utils/idGenerator.js +149 -0
  47. package/dist/utils/index.d.ts +13 -0
  48. package/dist/utils/index.js +16 -0
  49. package/dist/utils/logger.d.ts +36 -0
  50. package/dist/utils/logger.js +92 -0
  51. package/dist/utils/rateLimiter.d.ts +115 -0
  52. package/dist/utils/rateLimiter.js +180 -0
  53. package/dist/utils/requestContext.d.ts +68 -0
  54. package/dist/utils/requestContext.js +91 -0
  55. package/dist/utils/sanitization.d.ts +224 -0
  56. package/dist/utils/sanitization.js +367 -0
  57. package/dist/utils/security.d.ts +26 -0
  58. package/dist/utils/security.js +27 -0
  59. package/package.json +47 -0
@@ -0,0 +1,149 @@
1
+ import { randomBytes } from 'crypto';
2
+ import { BaseErrorCode, McpError } from '../types-global/errors.js';
3
+ import { logger } from './logger.js';
4
+ /**
5
+ * Generic ID Generator class for creating and managing unique identifiers
6
+ */
7
+ export class IdGenerator {
8
+ /**
9
+ * Constructor that accepts entity prefix configuration
10
+ * @param entityPrefixes Map of entity types to their prefixes
11
+ */
12
+ constructor(entityPrefixes = {}) {
13
+ // Entity prefixes
14
+ this.entityPrefixes = {};
15
+ // Reverse mapping for prefix to entity type lookup
16
+ this.prefixToEntityType = {};
17
+ this.setEntityPrefixes(entityPrefixes);
18
+ }
19
+ /**
20
+ * Set or update entity prefixes and rebuild the reverse lookup
21
+ * @param entityPrefixes Map of entity types to their prefixes
22
+ */
23
+ setEntityPrefixes(entityPrefixes) {
24
+ this.entityPrefixes = { ...entityPrefixes };
25
+ // Rebuild reverse mapping
26
+ this.prefixToEntityType = Object.entries(this.entityPrefixes).reduce((acc, [type, prefix]) => {
27
+ acc[prefix] = type;
28
+ acc[prefix.toLowerCase()] = type;
29
+ return acc;
30
+ }, {});
31
+ logger.debug('Entity prefixes updated', { entityPrefixes: this.entityPrefixes });
32
+ }
33
+ /**
34
+ * Get all registered entity prefixes
35
+ * @returns The entity prefix configuration
36
+ */
37
+ getEntityPrefixes() {
38
+ return { ...this.entityPrefixes };
39
+ }
40
+ /**
41
+ * Generates a cryptographically secure random alphanumeric string
42
+ * @param length The length of the random string to generate
43
+ * @param charset Optional custom character set
44
+ * @returns Random alphanumeric string
45
+ */
46
+ generateRandomString(length = IdGenerator.DEFAULT_LENGTH, charset = IdGenerator.DEFAULT_CHARSET) {
47
+ const bytes = randomBytes(length);
48
+ let result = '';
49
+ for (let i = 0; i < length; i++) {
50
+ result += charset[bytes[i] % charset.length];
51
+ }
52
+ return result;
53
+ }
54
+ /**
55
+ * Generates a unique ID with an optional prefix
56
+ * @param prefix Optional prefix to add to the ID
57
+ * @param options Optional generation options
58
+ * @returns A unique identifier string
59
+ */
60
+ generate(prefix, options = {}) {
61
+ const { length = IdGenerator.DEFAULT_LENGTH, separator = IdGenerator.DEFAULT_SEPARATOR, charset = IdGenerator.DEFAULT_CHARSET } = options;
62
+ const randomPart = this.generateRandomString(length, charset);
63
+ return prefix
64
+ ? `${prefix}${separator}${randomPart}`
65
+ : randomPart;
66
+ }
67
+ /**
68
+ * Generates a custom ID for an entity with format PREFIX_XXXXXX
69
+ * @param entityType The type of entity to generate an ID for
70
+ * @param options Optional generation options
71
+ * @returns A unique identifier string (e.g., "PROJ_A6B3J0")
72
+ * @throws {McpError} If the entity type is not registered
73
+ */
74
+ generateForEntity(entityType, options = {}) {
75
+ const prefix = this.entityPrefixes[entityType];
76
+ if (!prefix) {
77
+ throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Unknown entity type: ${entityType}`);
78
+ }
79
+ return this.generate(prefix, options);
80
+ }
81
+ /**
82
+ * Validates if a given ID matches the expected format for an entity type
83
+ * @param id The ID to validate
84
+ * @param entityType The expected entity type
85
+ * @param options Optional validation options
86
+ * @returns boolean indicating if the ID is valid
87
+ */
88
+ isValid(id, entityType, options = {}) {
89
+ const prefix = this.entityPrefixes[entityType];
90
+ const { length = IdGenerator.DEFAULT_LENGTH, separator = IdGenerator.DEFAULT_SEPARATOR } = options;
91
+ if (!prefix) {
92
+ return false;
93
+ }
94
+ const pattern = new RegExp(`^${prefix}${separator}[A-Z0-9]{${length}}$`);
95
+ return pattern.test(id);
96
+ }
97
+ /**
98
+ * Strips the prefix from an ID
99
+ * @param id The ID to strip
100
+ * @param separator Optional custom separator
101
+ * @returns The ID without the prefix
102
+ */
103
+ stripPrefix(id, separator = IdGenerator.DEFAULT_SEPARATOR) {
104
+ return id.split(separator)[1] || id;
105
+ }
106
+ /**
107
+ * Determines the entity type from an ID
108
+ * @param id The ID to get the entity type for
109
+ * @param separator Optional custom separator
110
+ * @returns The entity type
111
+ * @throws {McpError} If the ID format is invalid or entity type is unknown
112
+ */
113
+ getEntityType(id, separator = IdGenerator.DEFAULT_SEPARATOR) {
114
+ const parts = id.split(separator);
115
+ if (parts.length !== 2 || !parts[0]) {
116
+ throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid ID format: ${id}. Expected format: PREFIX${separator}XXXXXX`);
117
+ }
118
+ const prefix = parts[0];
119
+ const entityType = this.prefixToEntityType[prefix];
120
+ if (!entityType) {
121
+ throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Unknown entity type prefix: ${prefix}`);
122
+ }
123
+ return entityType;
124
+ }
125
+ /**
126
+ * Normalizes an entity ID to ensure consistent uppercase format
127
+ * @param id The ID to normalize
128
+ * @param separator Optional custom separator
129
+ * @returns The normalized ID in uppercase format
130
+ */
131
+ normalize(id, separator = IdGenerator.DEFAULT_SEPARATOR) {
132
+ const entityType = this.getEntityType(id, separator);
133
+ const idParts = id.split(separator);
134
+ return `${this.entityPrefixes[entityType]}${separator}${idParts[1].toUpperCase()}`;
135
+ }
136
+ }
137
+ // Default charset
138
+ IdGenerator.DEFAULT_CHARSET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
139
+ // Default separator
140
+ IdGenerator.DEFAULT_SEPARATOR = '_';
141
+ // Default random part length
142
+ IdGenerator.DEFAULT_LENGTH = 6;
143
+ // Create and export a default instance with an empty entity prefix configuration
144
+ export const idGenerator = new IdGenerator();
145
+ // For standalone use as a UUID generator
146
+ export const generateUUID = () => {
147
+ return crypto.randomUUID();
148
+ };
149
+ export default idGenerator;
@@ -0,0 +1,13 @@
1
+ export * from './requestContext.js';
2
+ export * from './errorHandler.js';
3
+ export * from './idGenerator.js';
4
+ export * from './logger.js';
5
+ export * from './rateLimiter.js';
6
+ export * from './sanitization.js';
7
+ import { default as requestContext } from './requestContext.js';
8
+ import { default as errorHandler } from './errorHandler.js';
9
+ import { default as idGenerator } from './idGenerator.js';
10
+ import { logger } from './logger.js';
11
+ import { default as rateLimiter } from './rateLimiter.js';
12
+ import { default as sanitization } from './sanitization.js';
13
+ export { requestContext, errorHandler, idGenerator, logger, rateLimiter, sanitization };
@@ -0,0 +1,16 @@
1
+ // Re-export all utilities
2
+ export * from './requestContext.js';
3
+ export * from './errorHandler.js';
4
+ export * from './idGenerator.js';
5
+ export * from './logger.js';
6
+ export * from './rateLimiter.js';
7
+ export * from './sanitization.js';
8
+ // Import named exports to re-export
9
+ import { default as requestContext } from './requestContext.js';
10
+ import { default as errorHandler } from './errorHandler.js';
11
+ import { default as idGenerator } from './idGenerator.js';
12
+ import { logger } from './logger.js';
13
+ import { default as rateLimiter } from './rateLimiter.js';
14
+ import { default as sanitization } from './sanitization.js';
15
+ // Export frequently used utilities directly
16
+ export { requestContext, errorHandler, idGenerator, logger, rateLimiter, sanitization };
@@ -0,0 +1,36 @@
1
+ export type ChildLogger = {
2
+ debug: (message: string, context?: Record<string, unknown>) => void;
3
+ info: (message: string, context?: Record<string, unknown>) => void;
4
+ warn: (message: string, context?: Record<string, unknown>) => void;
5
+ error: (message: string, context?: Record<string, unknown>) => void;
6
+ };
7
+ declare class Logger {
8
+ private static instance;
9
+ private logger;
10
+ private constructor();
11
+ static getInstance(): Logger;
12
+ debug(message: string, context?: Record<string, unknown>): void;
13
+ info(message: string, context?: Record<string, unknown>): void;
14
+ warn(message: string, context?: Record<string, unknown>): void;
15
+ error(message: string, context?: Record<string, unknown>): void;
16
+ createChildLogger(metadata: {
17
+ module: string;
18
+ service?: string;
19
+ serviceId?: string;
20
+ componentName?: string;
21
+ subscriberId?: string;
22
+ component?: string;
23
+ requestId?: string;
24
+ subscriptionTime?: string;
25
+ environment?: string;
26
+ serverId?: string;
27
+ [key: string]: any;
28
+ }): {
29
+ debug: (message: string, context?: Record<string, unknown>) => void;
30
+ info: (message: string, context?: Record<string, unknown>) => void;
31
+ warn: (message: string, context?: Record<string, unknown>) => void;
32
+ error: (message: string, context?: Record<string, unknown>) => void;
33
+ };
34
+ }
35
+ export declare const logger: Logger;
36
+ export {};
@@ -0,0 +1,92 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { fileURLToPath } from "url";
4
+ import winston from "winston";
5
+ // Handle ESM module dirname
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = path.dirname(__filename);
8
+ // Resolve logs directory relative to project root (2 levels up from utils/)
9
+ const projectRoot = path.resolve(__dirname, '..', '..');
10
+ const logsDir = path.join(projectRoot, 'logs');
11
+ class Logger {
12
+ constructor() {
13
+ const logLevel = process.env.LOG_LEVEL || "info";
14
+ // Ensure logs directory exists
15
+ if (!fs.existsSync(logsDir)) {
16
+ fs.mkdirSync(logsDir, { recursive: true });
17
+ }
18
+ // Common format for all transports
19
+ const commonFormat = winston.format.combine(winston.format.timestamp(), winston.format.errors({ stack: true }), winston.format.printf(({ timestamp, level, message, context, stack }) => {
20
+ const contextStr = context ? `\n Context: ${JSON.stringify(context, null, 2)}` : "";
21
+ const stackStr = stack ? `\n Stack: ${stack}` : "";
22
+ return `[${timestamp}] ${level}: ${message}${contextStr}${stackStr}`;
23
+ }));
24
+ this.logger = winston.createLogger({
25
+ level: logLevel,
26
+ format: winston.format.json(),
27
+ transports: [
28
+ // Combined log file for all levels
29
+ new winston.transports.File({
30
+ filename: path.join(logsDir, 'combined.log'),
31
+ format: commonFormat
32
+ }),
33
+ // Separate log files for each level
34
+ new winston.transports.File({
35
+ filename: path.join(logsDir, 'error.log'),
36
+ level: 'error',
37
+ format: commonFormat
38
+ }),
39
+ new winston.transports.File({
40
+ filename: path.join(logsDir, 'warn.log'),
41
+ level: 'warn',
42
+ format: commonFormat
43
+ }),
44
+ new winston.transports.File({
45
+ filename: path.join(logsDir, 'info.log'),
46
+ level: 'info',
47
+ format: commonFormat
48
+ }),
49
+ new winston.transports.File({
50
+ filename: path.join(logsDir, 'debug.log'),
51
+ level: 'debug',
52
+ format: commonFormat
53
+ })
54
+ ]
55
+ });
56
+ }
57
+ static getInstance() {
58
+ if (!Logger.instance) {
59
+ Logger.instance = new Logger();
60
+ }
61
+ return Logger.instance;
62
+ }
63
+ debug(message, context) {
64
+ this.logger.debug(message, { context });
65
+ }
66
+ info(message, context) {
67
+ this.logger.info(message, { context });
68
+ }
69
+ warn(message, context) {
70
+ this.logger.warn(message, { context });
71
+ }
72
+ error(message, context) {
73
+ this.logger.error(message, { context });
74
+ }
75
+ createChildLogger(metadata) {
76
+ return {
77
+ debug: (message, context) => {
78
+ this.debug(`[${metadata.module}] ${message}`, context);
79
+ },
80
+ info: (message, context) => {
81
+ this.info(`[${metadata.module}] ${message}`, context);
82
+ },
83
+ warn: (message, context) => {
84
+ this.warn(`[${metadata.module}] ${message}`, context);
85
+ },
86
+ error: (message, context) => {
87
+ this.error(`[${metadata.module}] ${message}`, context);
88
+ }
89
+ };
90
+ }
91
+ }
92
+ export const logger = Logger.getInstance();
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Request context interface
3
+ */
4
+ export interface RequestContext {
5
+ /** Unique request identifier */
6
+ requestId: string;
7
+ /** Request timestamp */
8
+ timestamp: string;
9
+ /** Request path/endpoint */
10
+ path?: string;
11
+ /** HTTP method */
12
+ method?: string;
13
+ /** Request source IP */
14
+ ip?: string;
15
+ /** Custom request properties */
16
+ [key: string]: unknown;
17
+ }
18
+ /**
19
+ * Rate limiting configuration options
20
+ */
21
+ export interface RateLimitConfig {
22
+ /** Time window in milliseconds */
23
+ windowMs: number;
24
+ /** Maximum number of requests allowed in the window */
25
+ maxRequests: number;
26
+ /** Custom error message template */
27
+ errorMessage?: string;
28
+ /** Whether to skip rate limiting in certain environments (e.g. development) */
29
+ skipInDevelopment?: boolean;
30
+ /** Custom key generator function */
31
+ keyGenerator?: (identifier: string, context?: RequestContext) => string;
32
+ /** How often to run cleanup of expired entries (in milliseconds) */
33
+ cleanupInterval?: number;
34
+ }
35
+ /**
36
+ * Individual rate limit entry
37
+ */
38
+ export interface RateLimitEntry {
39
+ /** Current request count */
40
+ count: number;
41
+ /** When the window resets (timestamp) */
42
+ resetTime: number;
43
+ /** Key for this entry, stored for faster deletion */
44
+ key: string;
45
+ }
46
+ /**
47
+ * Generic rate limiter that can be used across the application
48
+ */
49
+ export declare class RateLimiter {
50
+ private config;
51
+ /** Map storing rate limit data */
52
+ private limits;
53
+ /** Cleanup interval timer */
54
+ private cleanupTimer;
55
+ /** Default configuration */
56
+ private static DEFAULT_CONFIG;
57
+ /**
58
+ * Create a new rate limiter
59
+ * @param config Rate limiting configuration
60
+ */
61
+ constructor(config: RateLimitConfig);
62
+ /**
63
+ * Start the cleanup timer to periodically remove expired entries
64
+ */
65
+ private startCleanupTimer;
66
+ /**
67
+ * Clean up expired rate limit entries to prevent memory leaks
68
+ */
69
+ private cleanupExpiredEntries;
70
+ /**
71
+ * Update rate limiter configuration
72
+ * @param config New configuration options
73
+ */
74
+ configure(config: Partial<RateLimitConfig>): void;
75
+ /**
76
+ * Get current configuration
77
+ * @returns Current rate limit configuration
78
+ */
79
+ getConfig(): RateLimitConfig;
80
+ /**
81
+ * Reset all rate limits
82
+ */
83
+ reset(): void;
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: string, context?: RequestContext): void;
91
+ /**
92
+ * Get rate limit information for a key
93
+ * @param key The rate limit key
94
+ * @returns Current rate limit status or null if no record exists
95
+ */
96
+ getStatus(key: string): {
97
+ current: number;
98
+ limit: number;
99
+ remaining: number;
100
+ resetTime: number;
101
+ } | null;
102
+ /**
103
+ * Stop the cleanup timer when the limiter is no longer needed
104
+ */
105
+ dispose(): void;
106
+ }
107
+ /**
108
+ * Create and export a default rate limiter instance
109
+ */
110
+ export declare const rateLimiter: RateLimiter;
111
+ declare const _default: {
112
+ RateLimiter: typeof RateLimiter;
113
+ rateLimiter: RateLimiter;
114
+ };
115
+ export default _default;
@@ -0,0 +1,180 @@
1
+ import { BaseErrorCode, McpError } from '../types-global/errors.js';
2
+ import { logger } from './logger.js';
3
+ /**
4
+ * Generic rate limiter that can be used across the application
5
+ */
6
+ export class RateLimiter {
7
+ /**
8
+ * Create a new rate limiter
9
+ * @param config Rate limiting configuration
10
+ */
11
+ constructor(config) {
12
+ this.config = config;
13
+ /** Cleanup interval timer */
14
+ this.cleanupTimer = null;
15
+ this.config = { ...RateLimiter.DEFAULT_CONFIG, ...config };
16
+ this.limits = new Map();
17
+ this.startCleanupTimer();
18
+ // Log initialization
19
+ logger.debug('RateLimiter initialized', {
20
+ windowMs: this.config.windowMs,
21
+ maxRequests: this.config.maxRequests,
22
+ cleanupInterval: this.config.cleanupInterval
23
+ });
24
+ }
25
+ /**
26
+ * Start the cleanup timer to periodically remove expired entries
27
+ */
28
+ startCleanupTimer() {
29
+ if (this.cleanupTimer) {
30
+ clearInterval(this.cleanupTimer);
31
+ }
32
+ const interval = this.config.cleanupInterval ?? RateLimiter.DEFAULT_CONFIG.cleanupInterval;
33
+ if (interval) {
34
+ this.cleanupTimer = setInterval(() => {
35
+ this.cleanupExpiredEntries();
36
+ }, interval);
37
+ // Ensure the timer doesn't prevent the process from exiting
38
+ if (this.cleanupTimer.unref) {
39
+ this.cleanupTimer.unref();
40
+ }
41
+ }
42
+ }
43
+ /**
44
+ * Clean up expired rate limit entries to prevent memory leaks
45
+ */
46
+ cleanupExpiredEntries() {
47
+ const now = Date.now();
48
+ let expiredCount = 0;
49
+ // Use a synchronized approach to avoid race conditions during cleanup
50
+ for (const [key, entry] of this.limits.entries()) {
51
+ if (now >= entry.resetTime) {
52
+ this.limits.delete(key);
53
+ expiredCount++;
54
+ }
55
+ }
56
+ if (expiredCount > 0) {
57
+ logger.debug(`Cleaned up ${expiredCount} expired rate limit entries`, {
58
+ totalRemaining: this.limits.size
59
+ });
60
+ }
61
+ }
62
+ /**
63
+ * Update rate limiter configuration
64
+ * @param config New configuration options
65
+ */
66
+ configure(config) {
67
+ this.config = { ...this.config, ...config };
68
+ // Restart cleanup timer if interval changed
69
+ if (config.cleanupInterval !== undefined) {
70
+ this.startCleanupTimer();
71
+ }
72
+ }
73
+ /**
74
+ * Get current configuration
75
+ * @returns Current rate limit configuration
76
+ */
77
+ getConfig() {
78
+ return { ...this.config };
79
+ }
80
+ /**
81
+ * Reset all rate limits
82
+ */
83
+ reset() {
84
+ this.limits.clear();
85
+ logger.debug('Rate limiter reset, all limits cleared');
86
+ }
87
+ /**
88
+ * Check if a request exceeds the rate limit
89
+ * @param key Unique identifier for the request source
90
+ * @param context Optional request context
91
+ * @throws {McpError} If rate limit is exceeded
92
+ */
93
+ check(key, context) {
94
+ // Skip in development if configured
95
+ if (this.config.skipInDevelopment && process.env.NODE_ENV === 'development') {
96
+ return;
97
+ }
98
+ // Generate key using custom generator if provided
99
+ const limitKey = this.config.keyGenerator
100
+ ? this.config.keyGenerator(key, context)
101
+ : key;
102
+ const now = Date.now();
103
+ // Use a mutex-like approach for thread safety by safely getting and
104
+ // manipulating the rate limit entry in an atomic operation
105
+ const limit = () => {
106
+ // Get current entry or create a new one if it doesn't exist or is expired
107
+ const entry = this.limits.get(limitKey);
108
+ // Create new entry or reset if expired
109
+ if (!entry || now >= entry.resetTime) {
110
+ const newEntry = {
111
+ count: 1,
112
+ resetTime: now + this.config.windowMs,
113
+ key: limitKey
114
+ };
115
+ this.limits.set(limitKey, newEntry);
116
+ return newEntry;
117
+ }
118
+ // Check if limit exceeded
119
+ if (entry.count >= this.config.maxRequests) {
120
+ const waitTime = Math.ceil((entry.resetTime - now) / 1000);
121
+ const errorMessage = this.config.errorMessage?.replace('{waitTime}', waitTime.toString()) ||
122
+ `Rate limit exceeded. Please try again in ${waitTime} seconds.`;
123
+ throw new McpError(BaseErrorCode.RATE_LIMITED, errorMessage, { waitTime, key: limitKey });
124
+ }
125
+ // Increment counter and return updated entry
126
+ entry.count++;
127
+ return entry;
128
+ };
129
+ // Execute the rate limiting logic
130
+ limit();
131
+ }
132
+ /**
133
+ * Get rate limit information for a key
134
+ * @param key The rate limit key
135
+ * @returns Current rate limit status or null if no record exists
136
+ */
137
+ getStatus(key) {
138
+ const entry = this.limits.get(key);
139
+ if (!entry) {
140
+ return null;
141
+ }
142
+ return {
143
+ current: entry.count,
144
+ limit: this.config.maxRequests,
145
+ remaining: Math.max(0, this.config.maxRequests - entry.count),
146
+ resetTime: entry.resetTime
147
+ };
148
+ }
149
+ /**
150
+ * Stop the cleanup timer when the limiter is no longer needed
151
+ */
152
+ dispose() {
153
+ if (this.cleanupTimer) {
154
+ clearInterval(this.cleanupTimer);
155
+ this.cleanupTimer = null;
156
+ }
157
+ // Clear all entries
158
+ this.limits.clear();
159
+ }
160
+ }
161
+ /** Default configuration */
162
+ RateLimiter.DEFAULT_CONFIG = {
163
+ windowMs: 15 * 60 * 1000, // 15 minutes
164
+ maxRequests: 100, // 100 requests per window
165
+ errorMessage: 'Rate limit exceeded. Please try again in {waitTime} seconds.',
166
+ skipInDevelopment: false,
167
+ cleanupInterval: 5 * 60 * 1000 // 5 minutes
168
+ };
169
+ /**
170
+ * Create and export a default rate limiter instance
171
+ */
172
+ export const rateLimiter = new RateLimiter({
173
+ windowMs: 15 * 60 * 1000, // 15 minutes
174
+ maxRequests: 100 // 100 requests per window
175
+ });
176
+ // Export default
177
+ export default {
178
+ RateLimiter,
179
+ rateLimiter
180
+ };
@@ -0,0 +1,68 @@
1
+ import { RequestContext } from './rateLimiter.js';
2
+ /**
3
+ * Configuration interface for request context utilities
4
+ */
5
+ export interface ContextConfig {
6
+ /** Custom configuration properties */
7
+ [key: string]: unknown;
8
+ }
9
+ /**
10
+ * Operation context with request data
11
+ */
12
+ export interface OperationContext {
13
+ /** Request context data */
14
+ requestContext?: RequestContext;
15
+ /** Custom context properties */
16
+ [key: string]: unknown;
17
+ }
18
+ /**
19
+ * Request context utilities class
20
+ */
21
+ export declare class RequestContextService {
22
+ private static instance;
23
+ private config;
24
+ /**
25
+ * Private constructor to enforce singleton pattern
26
+ */
27
+ private constructor();
28
+ /**
29
+ * Get the singleton RequestContextService instance
30
+ * @returns RequestContextService instance
31
+ */
32
+ static getInstance(): RequestContextService;
33
+ /**
34
+ * Configure service settings
35
+ * @param config New configuration
36
+ * @returns Updated configuration
37
+ */
38
+ configure(config: Partial<ContextConfig>): ContextConfig;
39
+ /**
40
+ * Get current configuration
41
+ * @returns Current configuration
42
+ */
43
+ getConfig(): ContextConfig;
44
+ /**
45
+ * Create a request context with unique ID and timestamp
46
+ * @param additionalContext Additional context properties
47
+ * @returns Request context object
48
+ */
49
+ createRequestContext(additionalContext?: Record<string, unknown>): RequestContext;
50
+ /**
51
+ * Generate a secure random string
52
+ * @param length Length of the string
53
+ * @param chars Character set to use
54
+ * @returns Random string
55
+ */
56
+ generateSecureRandomString(length?: number, chars?: string): string;
57
+ }
58
+ export declare const requestContextService: RequestContextService;
59
+ export declare const configureContext: (config: Partial<ContextConfig>) => ContextConfig;
60
+ export declare const createRequestContext: (additionalContext?: Record<string, unknown>) => RequestContext;
61
+ export declare const generateSecureRandomString: (length?: number, chars?: string) => string;
62
+ declare const _default: {
63
+ requestContextService: RequestContextService;
64
+ configureContext: (config: Partial<ContextConfig>) => ContextConfig;
65
+ createRequestContext: (additionalContext?: Record<string, unknown>) => RequestContext;
66
+ generateSecureRandomString: (length?: number, chars?: string) => string;
67
+ };
68
+ export default _default;