UNPKG

ms365-mcp-server

Version:

Microsoft 365 MCP Server for managing Microsoft 365 email through natural language interactions with full OAuth2 authentication support

285 lines (284 loc) 9.9 kB
import { logger } from './api.js'; export class RateLimiter { constructor(config) { this.config = config; this.requests = []; this.throttleUntil = 0; this.adaptiveDelay = 0; this.consecutiveThrottles = 0; } /** * Check if request is allowed */ checkLimit(endpoint) { const now = Date.now(); const windowStart = now - this.config.windowMs; // Remove old requests outside the window this.requests = this.requests.filter(req => req.timestamp > windowStart); // Check if we're still throttled if (this.throttleUntil > now) { return { allowed: false, remainingRequests: 0, resetTime: this.throttleUntil, retryAfter: this.throttleUntil - now, isThrottled: true }; } // Check burst allowance const burstLimit = this.config.burstAllowance || Math.floor(this.config.maxRequests * 0.5); const recentRequests = this.requests.filter(req => req.timestamp > now - 10000); // Last 10 seconds if (recentRequests.length >= burstLimit) { const retryAfter = 10000 - (now - recentRequests[0].timestamp); return { allowed: false, remainingRequests: this.config.maxRequests - this.requests.length, resetTime: windowStart + this.config.windowMs, retryAfter, isThrottled: false }; } // Check window limit if (this.requests.length >= this.config.maxRequests) { const oldestRequest = this.requests[0]; const retryAfter = (oldestRequest.timestamp + this.config.windowMs) - now; return { allowed: false, remainingRequests: 0, resetTime: oldestRequest.timestamp + this.config.windowMs, retryAfter, isThrottled: false }; } // Request is allowed - add to tracking this.requests.push({ timestamp: now, endpoint }); return { allowed: true, remainingRequests: this.config.maxRequests - this.requests.length, resetTime: windowStart + this.config.windowMs, isThrottled: false }; } /** * Record a throttling event from the API */ recordThrottle(retryAfterMs) { const now = Date.now(); this.throttleUntil = now + retryAfterMs; this.consecutiveThrottles++; // Adaptive throttling - increase delay with consecutive throttles if (this.config.adaptiveThrottling) { this.adaptiveDelay = Math.min(this.consecutiveThrottles * 1000, 30000); // Max 30 seconds logger.log(`🚨 Throttled by API. Consecutive throttles: ${this.consecutiveThrottles}, adaptive delay: ${this.adaptiveDelay}ms`); } } /** * Record a successful request */ recordSuccess() { // Reset consecutive throttles on success if (this.consecutiveThrottles > 0) { this.consecutiveThrottles = 0; this.adaptiveDelay = 0; logger.log('✅ API throttling recovered'); } } /** * Get current status */ getStatus() { const now = Date.now(); const windowStart = now - this.config.windowMs; const activeRequests = this.requests.filter(req => req.timestamp > windowStart); return { requestsInWindow: activeRequests.length, maxRequests: this.config.maxRequests, isThrottled: this.throttleUntil > now, throttleUntil: this.throttleUntil, consecutiveThrottles: this.consecutiveThrottles, adaptiveDelay: this.adaptiveDelay }; } } export class MS365RateLimitManager { constructor() { this.rateLimiters = new Map(); } static getInstance() { if (!this.instance) { this.instance = new MS365RateLimitManager(); } return this.instance; } /** * Get rate limiter for endpoint */ getRateLimiter(endpoint) { if (!this.rateLimiters.has(endpoint)) { const config = MS365RateLimitManager.ENDPOINT_CONFIGS[endpoint] || MS365RateLimitManager.ENDPOINT_CONFIGS['default']; this.rateLimiters.set(endpoint, new RateLimiter(config)); } return this.rateLimiters.get(endpoint); } /** * Check if request is allowed */ checkRequest(endpoint, operation) { const limiter = this.getRateLimiter(endpoint); const result = limiter.checkLimit(operation); if (!result.allowed) { logger.log(`🚫 Rate limit exceeded for ${endpoint}:${operation}. Retry after ${result.retryAfter}ms`); } return result; } /** * Record a throttling event */ recordThrottle(endpoint, retryAfterMs) { const limiter = this.getRateLimiter(endpoint); limiter.recordThrottle(retryAfterMs); } /** * Record a successful request */ recordSuccess(endpoint) { const limiter = this.getRateLimiter(endpoint); limiter.recordSuccess(); } /** * Execute operation with rate limiting */ async executeWithRateLimit(operation, endpoint, operationName) { const rateLimitResult = this.checkRequest(endpoint, operationName); if (!rateLimitResult.allowed) { const retryAfter = rateLimitResult.retryAfter || 1000; logger.log(`⏳ Rate limited, waiting ${retryAfter}ms before ${endpoint}:${operationName}`); await new Promise(resolve => setTimeout(resolve, retryAfter)); // Retry after waiting return this.executeWithRateLimit(operation, endpoint, operationName); } try { const result = await operation(); this.recordSuccess(endpoint); return result; } catch (error) { // Check if error is due to rate limiting if (error.status === 429 || error.code === 'TooManyRequests') { const retryAfter = this.extractRetryAfter(error); this.recordThrottle(endpoint, retryAfter); logger.log(`🚨 API throttled ${endpoint}:${operationName}, waiting ${retryAfter}ms`); await new Promise(resolve => setTimeout(resolve, retryAfter)); // Retry after throttling return this.executeWithRateLimit(operation, endpoint, operationName); } throw error; } } /** * Extract retry-after value from error */ extractRetryAfter(error) { // Check for Retry-After header if (error.headers?.['retry-after']) { return parseInt(error.headers['retry-after']) * 1000; } // Check for retry-after in error message const retryMatch = error.message?.match(/retry after (\d+)/i); if (retryMatch) { return parseInt(retryMatch[1]) * 1000; } // Default retry after 60 seconds return 60000; } /** * Get status of all rate limiters */ getStatus() { const status = {}; for (const [endpoint, limiter] of this.rateLimiters) { status[endpoint] = limiter.getStatus(); } return status; } /** * Reset rate limiters (for testing or recovery) */ reset() { this.rateLimiters.clear(); logger.log('🔄 Rate limiters reset'); } /** * Get intelligent delay based on current state */ getIntelligentDelay(endpoint) { const limiter = this.getRateLimiter(endpoint); const status = limiter.getStatus(); // Base delay increases with request density const requestDensity = status.requestsInWindow / status.maxRequests; let baseDelay = 0; if (requestDensity > 0.8) { baseDelay = 2000; // 2 seconds when near limit } else if (requestDensity > 0.6) { baseDelay = 1000; // 1 second when getting busy } else if (requestDensity > 0.4) { baseDelay = 500; // 0.5 seconds when moderately busy } // Add adaptive delay if we've been throttled const totalDelay = baseDelay + status.adaptiveDelay; if (totalDelay > 0) { logger.log(`🕐 Intelligent delay: ${totalDelay}ms for ${endpoint} (density: ${(requestDensity * 100).toFixed(1)}%)`); } return totalDelay; } } // Rate limit configurations for different MS365 endpoints MS365RateLimitManager.ENDPOINT_CONFIGS = { 'search': { maxRequests: 10, windowMs: 60000, // 1 minute burstAllowance: 5, adaptiveThrottling: true }, 'email': { maxRequests: 100, windowMs: 60000, // 1 minute burstAllowance: 20, adaptiveThrottling: true }, 'send': { maxRequests: 30, windowMs: 60000, // 1 minute burstAllowance: 10, adaptiveThrottling: true }, 'attachment': { maxRequests: 50, windowMs: 60000, // 1 minute burstAllowance: 10, adaptiveThrottling: true }, 'calendar': { maxRequests: 60, windowMs: 60000, // 1 minute burstAllowance: 15, adaptiveThrottling: true }, 'contacts': { maxRequests: 60, windowMs: 60000, // 1 minute burstAllowance: 15, adaptiveThrottling: true }, 'default': { maxRequests: 50, windowMs: 60000, // 1 minute burstAllowance: 10, adaptiveThrottling: true } }; // Export singleton instance export const ms365RateLimit = MS365RateLimitManager.getInstance();