UNPKG

ms365-mcp-server

Version:

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

338 lines (337 loc) β€’ 12.8 kB
import { logger } from './api.js'; export class CircuitBreaker { constructor(failureThreshold = 5, recoveryTimeout = 30000, // 30 seconds successThreshold = 2) { this.failureThreshold = failureThreshold; this.recoveryTimeout = recoveryTimeout; this.successThreshold = successThreshold; this.failures = 0; this.lastFailureTime = 0; this.state = 'closed'; } async execute(operation, operationName) { if (this.state === 'open') { if (Date.now() - this.lastFailureTime > this.recoveryTimeout) { this.state = 'half-open'; logger.log(`πŸ”„ Circuit breaker entering half-open state for ${operationName}`); } else { throw new Error(`Circuit breaker is open for ${operationName}. Retry after ${Math.ceil((this.recoveryTimeout - (Date.now() - this.lastFailureTime)) / 1000)} seconds.`); } } try { const result = await operation(); this.onSuccess(operationName); return result; } catch (error) { this.onFailure(operationName); throw error; } } onSuccess(operationName) { if (this.state === 'half-open') { this.failures = 0; this.state = 'closed'; logger.log(`βœ… Circuit breaker closed for ${operationName}`); } } onFailure(operationName) { this.failures++; this.lastFailureTime = Date.now(); if (this.failures >= this.failureThreshold) { this.state = 'open'; logger.log(`🚨 Circuit breaker opened for ${operationName} after ${this.failures} failures`); } } getState() { return { state: this.state, failures: this.failures, lastFailureTime: this.lastFailureTime }; } } export class EnhancedErrorHandler { /** * Handle errors with comprehensive analysis and user-friendly messages */ static handleError(error, context) { const errorType = this.classifyError(error); const userMessage = this.generateUserMessage(error, errorType, context); const suggestedActions = this.generateSuggestedActions(error, errorType, context); const recoverable = this.isRecoverable(error, errorType); // Track error frequency this.trackError(context.operation, errorType); // Log structured error information logger.error(`🚨 Error in ${context.operation}:`, { type: errorType, message: error.message, context, stack: error.stack, recoverable }); return { success: false, error: { type: errorType, message: error.message, userMessage, recoverable, retryAfter: this.getRetryAfter(error, errorType), suggestedActions }, context }; } /** * Execute operation with circuit breaker protection */ static async executeWithCircuitBreaker(operation, operationName, context) { try { const circuitBreaker = this.getCircuitBreaker(operationName); const result = await circuitBreaker.execute(operation, operationName); return { success: true, data: result }; } catch (error) { return this.handleError(error, context); } } /** * Execute operation with automatic retry and exponential backoff */ static async executeWithRetry(operation, context, maxRetries = 3, baseDelay = 1000) { let lastError; for (let attempt = 1; attempt <= maxRetries; attempt++) { try { const result = await operation(); return { success: true, data: result }; } catch (error) { lastError = error; const errorType = this.classifyError(error); // Don't retry on non-recoverable errors if (!this.isRecoverable(error, errorType)) { break; } if (attempt === maxRetries) { break; } // Exponential backoff with jitter const delay = baseDelay * Math.pow(2, attempt - 1) + Math.random() * 1000; logger.log(`πŸ”„ Retrying ${context.operation} (attempt ${attempt + 1}/${maxRetries}) after ${delay}ms`); await new Promise(resolve => setTimeout(resolve, delay)); } } return this.handleError(lastError, context); } /** * Classify error into specific types */ static classifyError(error) { if (!error) return 'unknown'; // Authentication errors if (error.code === 'InvalidAuthenticationToken' || error.code === 'Unauthorized' || error.status === 401 || error.message?.includes('authentication') || error.message?.includes('token')) { return 'authentication'; } // Rate limiting errors if (error.status === 429 || error.code === 'TooManyRequests' || error.message?.includes('throttle') || error.message?.includes('rate limit')) { return 'rate_limit'; } // Permission errors if (error.status === 403 || error.code === 'Forbidden' || error.message?.includes('permission') || error.message?.includes('access denied')) { return 'permission'; } // Network errors if (error.code === 'ECONNRESET' || error.code === 'ENOTFOUND' || error.code === 'ETIMEDOUT' || error.message?.includes('network') || error.message?.includes('timeout')) { return 'network'; } // Validation errors if (error.status === 400 || error.code === 'InvalidRequest' || error.message?.includes('validation') || error.message?.includes('invalid')) { return 'validation'; } // Server errors if (error.status >= 500 || error.code === 'InternalServerError' || error.message?.includes('server error')) { return 'server'; } return 'unknown'; } /** * Generate user-friendly error messages */ static generateUserMessage(error, errorType, context) { const operation = context.operation; switch (errorType) { case 'authentication': return `πŸ” Authentication expired. Please re-authenticate with Microsoft 365 to continue using ${operation}.`; case 'rate_limit': return `⏸️ Microsoft 365 is temporarily limiting requests. Please wait a moment and try again.`; case 'permission': return `🚫 You don't have permission to perform this action. Please check your Microsoft 365 permissions.`; case 'network': return `🌐 Network connection issue. Please check your internet connection and try again.`; case 'validation': return `πŸ“‹ Invalid input provided. Please check your parameters and try again.`; case 'server': return `πŸ”§ Microsoft 365 service is temporarily unavailable. Please try again in a few minutes.`; default: return `❌ An unexpected error occurred while ${operation}. Please try again or contact support if the problem persists.`; } } /** * Generate suggested actions for error recovery */ static generateSuggestedActions(error, errorType, context) { const actions = []; switch (errorType) { case 'authentication': actions.push('Use the "authenticate" tool to re-authenticate with Microsoft 365'); actions.push('Check if your Microsoft 365 account is still active'); break; case 'rate_limit': actions.push('Wait 1-2 minutes before trying again'); actions.push('Use more specific search terms to reduce API calls'); actions.push('Consider using the AI assistant with large mailbox strategy'); break; case 'permission': actions.push('Contact your Microsoft 365 administrator'); actions.push('Check if you have the required permissions for this operation'); break; case 'network': actions.push('Check your internet connection'); actions.push('Try again in a few seconds'); actions.push('If using VPN, try disconnecting and reconnecting'); break; case 'validation': actions.push('Check all required parameters are provided'); actions.push('Verify date formats (use YYYY-MM-DD)'); actions.push('Ensure email addresses are valid'); break; case 'server': actions.push('Wait 2-3 minutes and try again'); actions.push('Check Microsoft 365 service status'); actions.push('Try a simpler operation first'); break; default: actions.push('Try the operation again'); actions.push('If the problem persists, restart the MCP server'); actions.push('Check the debug logs for more details'); break; } return actions; } /** * Determine if error is recoverable */ static isRecoverable(error, errorType) { switch (errorType) { case 'authentication': case 'permission': case 'validation': return false; // These require user intervention case 'rate_limit': case 'network': case 'server': return true; // These can be retried default: return true; // Default to recoverable } } /** * Get retry delay from error */ static getRetryAfter(error, errorType) { if (errorType === 'rate_limit') { // Check for Retry-After header if (error.headers?.['retry-after']) { return parseInt(error.headers['retry-after']) * 1000; } return 60000; // Default 60 seconds for rate limits } if (errorType === 'server') { return 30000; // 30 seconds for server errors } return undefined; } /** * Get or create circuit breaker for operation */ static getCircuitBreaker(operationName) { if (!this.circuitBreakers.has(operationName)) { this.circuitBreakers.set(operationName, new CircuitBreaker()); } return this.circuitBreakers.get(operationName); } /** * Track error frequency */ static trackError(operation, errorType) { const key = `${operation}:${errorType}`; const now = Date.now(); const hourMs = 60 * 60 * 1000; if (!this.errorCounts.has(key)) { this.errorCounts.set(key, { count: 0, lastReset: now }); } const errorData = this.errorCounts.get(key); // Reset counter if it's been more than an hour if (now - errorData.lastReset > hourMs) { errorData.count = 0; errorData.lastReset = now; } errorData.count++; // Log warning if error rate is high if (errorData.count > 10) { logger.error(`⚠️ High error rate detected for ${operation}:${errorType} (${errorData.count} errors in the last hour)`); } } /** * Get error statistics */ static getErrorStats() { return Object.fromEntries(this.errorCounts); } /** * Get circuit breaker status */ static getCircuitBreakerStatus() { const status = {}; for (const [name, breaker] of this.circuitBreakers) { status[name] = breaker.getState(); } return status; } } EnhancedErrorHandler.circuitBreakers = new Map(); EnhancedErrorHandler.errorCounts = new Map(); // Helper function to wrap results export function wrapResult(data) { return { success: true, data }; } // Helper function to check if result is success export function isSuccess(result) { return result.success; } // Helper function to check if result is error export function isError(result) { return !result.success; }