Skip to main content
Glama
RateLimiter.js•10.2 kB
/** * Rate Limiter for command execution to prevent DoS attacks * Implements token bucket algorithm with sliding window tracking */ export class RateLimiter { constructor(options = {}) { this.options = { maxRequests: options.maxRequests || 100, // Max requests per window windowMs: options.windowMs || 60000, // 1 minute window burstLimit: options.burstLimit || 10, // Max burst requests burstWindowMs: options.burstWindowMs || 1000, // 1 second burst window enableDDoSProtection: options.enableDDoSProtection !== false, suspiciousThreshold: options.suspiciousThreshold || 50, blockDurationMs: options.blockDurationMs || 300000, // 5 minutes enableGracefulDegradation: options.enableGracefulDegradation !== false, ...options }; // Token bucket for rate limiting this.buckets = new Map(); // clientId -> bucket // Sliding window tracking this.windows = new Map(); // clientId -> requests array // Blocked clients tracking this.blockedClients = new Map(); // clientId -> blockEndTime // DDoS detection patterns this.suspiciousPatterns = new Map(); // clientId -> pattern data // Cleanup intervals this.startCleanupIntervals(); } /** * Check if request is allowed for client */ async isAllowed(clientId, commandType = 'general', metadata = {}) { const now = Date.now(); // Check if client is blocked if (this.isClientBlocked(clientId, now)) { return { allowed: false, reason: 'CLIENT_BLOCKED', retryAfter: this.getBlockTimeRemaining(clientId, now), metadata: { blockReason: 'DDoS protection activated' } }; } // Check for suspicious patterns if (this.detectSuspiciousActivity(clientId, commandType, metadata, now)) { this.blockClient(clientId, now, 'SUSPICIOUS_ACTIVITY'); return { allowed: false, reason: 'SUSPICIOUS_ACTIVITY', retryAfter: this.options.blockDurationMs, metadata: { pattern: 'Automated/malicious behavior detected' } }; } // Check burst limit const burstResult = this.checkBurstLimit(clientId, now); if (!burstResult.allowed) { return burstResult; } // Check rate limit const rateResult = this.checkRateLimit(clientId, now); if (!rateResult.allowed) { return rateResult; } // Record the request this.recordRequest(clientId, commandType, metadata, now); return { allowed: true, remaining: this.getRemainingRequests(clientId, now), resetTime: this.getResetTime(clientId, now), metadata: { rateLimit: this.options.maxRequests, window: this.options.windowMs } }; } /** * Check burst limit (short-term protection) */ checkBurstLimit(clientId, now) { const window = this.windows.get(clientId) || []; const recentRequests = window.filter(req => now - req.timestamp < this.options.burstWindowMs ); if (recentRequests.length >= this.options.burstLimit) { return { allowed: false, reason: 'BURST_LIMIT_EXCEEDED', retryAfter: this.options.burstWindowMs, metadata: { burstLimit: this.options.burstLimit, window: this.options.burstWindowMs } }; } return { allowed: true }; } /** * Check rate limit (long-term protection) */ checkRateLimit(clientId, now) { const window = this.windows.get(clientId) || []; const validRequests = window.filter(req => now - req.timestamp < this.options.windowMs ); if (validRequests.length >= this.options.maxRequests) { return { allowed: false, reason: 'RATE_LIMIT_EXCEEDED', retryAfter: this.getRetryAfter(clientId, now), metadata: { rateLimit: this.options.maxRequests, window: this.options.windowMs } }; } return { allowed: true }; } /** * Detect suspicious activity patterns */ detectSuspiciousActivity(clientId, commandType, metadata, now) { if (!this.options.enableDDoSProtection) return false; const patterns = this.suspiciousPatterns.get(clientId) || { commandCounts: new Map(), lastRequest: 0, rapidFireCount: 0, identicalCommands: [], errorCount: 0 }; // Pattern 1: Rapid fire requests (< 100ms apart) if (now - patterns.lastRequest < 100) { patterns.rapidFireCount++; if (patterns.rapidFireCount > 20) return true; } else { patterns.rapidFireCount = 0; } // Pattern 2: Identical commands in succession const commandKey = `${commandType}_${JSON.stringify(metadata)}`; patterns.identicalCommands.push({ command: commandKey, timestamp: now }); patterns.identicalCommands = patterns.identicalCommands.filter(cmd => now - cmd.timestamp < 5000 // Last 5 seconds ); const identicalCount = patterns.identicalCommands.filter(cmd => cmd.command === commandKey ).length; if (identicalCount > 10) return true; // Pattern 3: High error rate commands if (metadata.isError) { patterns.errorCount++; if (patterns.errorCount > 30) return true; } // Pattern 4: Command type distribution analysis patterns.commandCounts.set(commandType, (patterns.commandCounts.get(commandType) || 0) + 1); const totalCommands = Array.from(patterns.commandCounts.values()).reduce((a, b) => a + b, 0); if (totalCommands > this.options.suspiciousThreshold) return true; patterns.lastRequest = now; this.suspiciousPatterns.set(clientId, patterns); return false; } /** * Record a successful request */ recordRequest(clientId, commandType, metadata, now) { const window = this.windows.get(clientId) || []; window.push({ timestamp: now, commandType, metadata }); // Keep only recent requests const filtered = window.filter(req => now - req.timestamp < Math.max(this.options.windowMs, this.options.burstWindowMs) ); this.windows.set(clientId, filtered); } /** * Block a client */ blockClient(clientId, now, reason) { this.blockedClients.set(clientId, { blockEndTime: now + this.options.blockDurationMs, reason, timestamp: now }); } /** * Check if client is blocked */ isClientBlocked(clientId, now) { const blockInfo = this.blockedClients.get(clientId); if (!blockInfo) return false; if (now >= blockInfo.blockEndTime) { this.blockedClients.delete(clientId); return false; } return true; } /** * Get remaining block time */ getBlockTimeRemaining(clientId, now) { const blockInfo = this.blockedClients.get(clientId); return blockInfo ? Math.max(0, blockInfo.blockEndTime - now) : 0; } /** * Get remaining requests in current window */ getRemainingRequests(clientId, now) { const window = this.windows.get(clientId) || []; const validRequests = window.filter(req => now - req.timestamp < this.options.windowMs ); return Math.max(0, this.options.maxRequests - validRequests.length); } /** * Get time when rate limit resets */ getResetTime(clientId, now) { const window = this.windows.get(clientId) || []; if (window.length === 0) return now; const oldestRequest = Math.min(...window.map(req => req.timestamp)); return oldestRequest + this.options.windowMs; } /** * Get retry after time for rate limited requests */ getRetryAfter(clientId, now) { const resetTime = this.getResetTime(clientId, now); return Math.max(1000, resetTime - now); // At least 1 second } /** * Get rate limiting statistics */ getStats() { const now = Date.now(); return { activeClients: this.windows.size, blockedClients: this.blockedClients.size, suspiciousClients: this.suspiciousPatterns.size, totalBlocked: Array.from(this.blockedClients.values()).length, blockedByReason: Array.from(this.blockedClients.values()).reduce((acc, block) => { acc[block.reason] = (acc[block.reason] || 0) + 1; return acc; }, {}), rateLimitConfig: { maxRequests: this.options.maxRequests, windowMs: this.options.windowMs, burstLimit: this.options.burstLimit, burstWindowMs: this.options.burstWindowMs } }; } /** * Start cleanup intervals */ startCleanupIntervals() { // Clean up expired windows every minute this.windowCleanupInterval = setInterval(() => { const now = Date.now(); const maxAge = Math.max(this.options.windowMs, this.options.burstWindowMs); for (const [clientId, window] of this.windows.entries()) { const filtered = window.filter(req => now - req.timestamp < maxAge); if (filtered.length === 0) { this.windows.delete(clientId); } else { this.windows.set(clientId, filtered); } } }, 60000); // Clean up expired blocks every 5 minutes this.blockCleanupInterval = setInterval(() => { const now = Date.now(); for (const [clientId, blockInfo] of this.blockedClients.entries()) { if (now >= blockInfo.blockEndTime) { this.blockedClients.delete(clientId); } } }, 300000); // Clean up old suspicious patterns every 10 minutes this.patternCleanupInterval = setInterval(() => { const now = Date.now(); for (const [clientId, patterns] of this.suspiciousPatterns.entries()) { if (now - patterns.lastRequest > 600000) { // 10 minutes inactive this.suspiciousPatterns.delete(clientId); } } }, 600000); } /** * Stop cleanup intervals */ destroy() { if (this.windowCleanupInterval) { clearInterval(this.windowCleanupInterval); } if (this.blockCleanupInterval) { clearInterval(this.blockCleanupInterval); } if (this.patternCleanupInterval) { clearInterval(this.patternCleanupInterval); } } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/moikas-code/moidvk'

If you have feedback or need assistance with the MCP directory API, please join our Discord server