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
JavaScript
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;
}