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
JavaScript
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();