ms365-mcp-server 1.1.16 → 1.1.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,337 @@
1
+ import { logger } from './api.js';
2
+ export class CircuitBreaker {
3
+ constructor(failureThreshold = 5, recoveryTimeout = 30000, // 30 seconds
4
+ successThreshold = 2) {
5
+ this.failureThreshold = failureThreshold;
6
+ this.recoveryTimeout = recoveryTimeout;
7
+ this.successThreshold = successThreshold;
8
+ this.failures = 0;
9
+ this.lastFailureTime = 0;
10
+ this.state = 'closed';
11
+ }
12
+ async execute(operation, operationName) {
13
+ if (this.state === 'open') {
14
+ if (Date.now() - this.lastFailureTime > this.recoveryTimeout) {
15
+ this.state = 'half-open';
16
+ logger.log(`🔄 Circuit breaker entering half-open state for ${operationName}`);
17
+ }
18
+ else {
19
+ throw new Error(`Circuit breaker is open for ${operationName}. Retry after ${Math.ceil((this.recoveryTimeout - (Date.now() - this.lastFailureTime)) / 1000)} seconds.`);
20
+ }
21
+ }
22
+ try {
23
+ const result = await operation();
24
+ this.onSuccess(operationName);
25
+ return result;
26
+ }
27
+ catch (error) {
28
+ this.onFailure(operationName);
29
+ throw error;
30
+ }
31
+ }
32
+ onSuccess(operationName) {
33
+ if (this.state === 'half-open') {
34
+ this.failures = 0;
35
+ this.state = 'closed';
36
+ logger.log(`✅ Circuit breaker closed for ${operationName}`);
37
+ }
38
+ }
39
+ onFailure(operationName) {
40
+ this.failures++;
41
+ this.lastFailureTime = Date.now();
42
+ if (this.failures >= this.failureThreshold) {
43
+ this.state = 'open';
44
+ logger.log(`🚨 Circuit breaker opened for ${operationName} after ${this.failures} failures`);
45
+ }
46
+ }
47
+ getState() {
48
+ return {
49
+ state: this.state,
50
+ failures: this.failures,
51
+ lastFailureTime: this.lastFailureTime
52
+ };
53
+ }
54
+ }
55
+ export class EnhancedErrorHandler {
56
+ /**
57
+ * Handle errors with comprehensive analysis and user-friendly messages
58
+ */
59
+ static handleError(error, context) {
60
+ const errorType = this.classifyError(error);
61
+ const userMessage = this.generateUserMessage(error, errorType, context);
62
+ const suggestedActions = this.generateSuggestedActions(error, errorType, context);
63
+ const recoverable = this.isRecoverable(error, errorType);
64
+ // Track error frequency
65
+ this.trackError(context.operation, errorType);
66
+ // Log structured error information
67
+ logger.error(`🚨 Error in ${context.operation}:`, {
68
+ type: errorType,
69
+ message: error.message,
70
+ context,
71
+ stack: error.stack,
72
+ recoverable
73
+ });
74
+ return {
75
+ success: false,
76
+ error: {
77
+ type: errorType,
78
+ message: error.message,
79
+ userMessage,
80
+ recoverable,
81
+ retryAfter: this.getRetryAfter(error, errorType),
82
+ suggestedActions
83
+ },
84
+ context
85
+ };
86
+ }
87
+ /**
88
+ * Execute operation with circuit breaker protection
89
+ */
90
+ static async executeWithCircuitBreaker(operation, operationName, context) {
91
+ try {
92
+ const circuitBreaker = this.getCircuitBreaker(operationName);
93
+ const result = await circuitBreaker.execute(operation, operationName);
94
+ return { success: true, data: result };
95
+ }
96
+ catch (error) {
97
+ return this.handleError(error, context);
98
+ }
99
+ }
100
+ /**
101
+ * Execute operation with automatic retry and exponential backoff
102
+ */
103
+ static async executeWithRetry(operation, context, maxRetries = 3, baseDelay = 1000) {
104
+ let lastError;
105
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
106
+ try {
107
+ const result = await operation();
108
+ return { success: true, data: result };
109
+ }
110
+ catch (error) {
111
+ lastError = error;
112
+ const errorType = this.classifyError(error);
113
+ // Don't retry on non-recoverable errors
114
+ if (!this.isRecoverable(error, errorType)) {
115
+ break;
116
+ }
117
+ if (attempt === maxRetries) {
118
+ break;
119
+ }
120
+ // Exponential backoff with jitter
121
+ const delay = baseDelay * Math.pow(2, attempt - 1) + Math.random() * 1000;
122
+ logger.log(`🔄 Retrying ${context.operation} (attempt ${attempt + 1}/${maxRetries}) after ${delay}ms`);
123
+ await new Promise(resolve => setTimeout(resolve, delay));
124
+ }
125
+ }
126
+ return this.handleError(lastError, context);
127
+ }
128
+ /**
129
+ * Classify error into specific types
130
+ */
131
+ static classifyError(error) {
132
+ if (!error)
133
+ return 'unknown';
134
+ // Authentication errors
135
+ if (error.code === 'InvalidAuthenticationToken' ||
136
+ error.code === 'Unauthorized' ||
137
+ error.status === 401 ||
138
+ error.message?.includes('authentication') ||
139
+ error.message?.includes('token')) {
140
+ return 'authentication';
141
+ }
142
+ // Rate limiting errors
143
+ if (error.status === 429 ||
144
+ error.code === 'TooManyRequests' ||
145
+ error.message?.includes('throttle') ||
146
+ error.message?.includes('rate limit')) {
147
+ return 'rate_limit';
148
+ }
149
+ // Permission errors
150
+ if (error.status === 403 ||
151
+ error.code === 'Forbidden' ||
152
+ error.message?.includes('permission') ||
153
+ error.message?.includes('access denied')) {
154
+ return 'permission';
155
+ }
156
+ // Network errors
157
+ if (error.code === 'ECONNRESET' ||
158
+ error.code === 'ENOTFOUND' ||
159
+ error.code === 'ETIMEDOUT' ||
160
+ error.message?.includes('network') ||
161
+ error.message?.includes('timeout')) {
162
+ return 'network';
163
+ }
164
+ // Validation errors
165
+ if (error.status === 400 ||
166
+ error.code === 'InvalidRequest' ||
167
+ error.message?.includes('validation') ||
168
+ error.message?.includes('invalid')) {
169
+ return 'validation';
170
+ }
171
+ // Server errors
172
+ if (error.status >= 500 ||
173
+ error.code === 'InternalServerError' ||
174
+ error.message?.includes('server error')) {
175
+ return 'server';
176
+ }
177
+ return 'unknown';
178
+ }
179
+ /**
180
+ * Generate user-friendly error messages
181
+ */
182
+ static generateUserMessage(error, errorType, context) {
183
+ const operation = context.operation;
184
+ switch (errorType) {
185
+ case 'authentication':
186
+ return `🔐 Authentication expired. Please re-authenticate with Microsoft 365 to continue using ${operation}.`;
187
+ case 'rate_limit':
188
+ return `⏸️ Microsoft 365 is temporarily limiting requests. Please wait a moment and try again.`;
189
+ case 'permission':
190
+ return `🚫 You don't have permission to perform this action. Please check your Microsoft 365 permissions.`;
191
+ case 'network':
192
+ return `🌐 Network connection issue. Please check your internet connection and try again.`;
193
+ case 'validation':
194
+ return `📋 Invalid input provided. Please check your parameters and try again.`;
195
+ case 'server':
196
+ return `🔧 Microsoft 365 service is temporarily unavailable. Please try again in a few minutes.`;
197
+ default:
198
+ return `❌ An unexpected error occurred while ${operation}. Please try again or contact support if the problem persists.`;
199
+ }
200
+ }
201
+ /**
202
+ * Generate suggested actions for error recovery
203
+ */
204
+ static generateSuggestedActions(error, errorType, context) {
205
+ const actions = [];
206
+ switch (errorType) {
207
+ case 'authentication':
208
+ actions.push('Use the "authenticate" tool to re-authenticate with Microsoft 365');
209
+ actions.push('Check if your Microsoft 365 account is still active');
210
+ break;
211
+ case 'rate_limit':
212
+ actions.push('Wait 1-2 minutes before trying again');
213
+ actions.push('Use more specific search terms to reduce API calls');
214
+ actions.push('Consider using the AI assistant with large mailbox strategy');
215
+ break;
216
+ case 'permission':
217
+ actions.push('Contact your Microsoft 365 administrator');
218
+ actions.push('Check if you have the required permissions for this operation');
219
+ break;
220
+ case 'network':
221
+ actions.push('Check your internet connection');
222
+ actions.push('Try again in a few seconds');
223
+ actions.push('If using VPN, try disconnecting and reconnecting');
224
+ break;
225
+ case 'validation':
226
+ actions.push('Check all required parameters are provided');
227
+ actions.push('Verify date formats (use YYYY-MM-DD)');
228
+ actions.push('Ensure email addresses are valid');
229
+ break;
230
+ case 'server':
231
+ actions.push('Wait 2-3 minutes and try again');
232
+ actions.push('Check Microsoft 365 service status');
233
+ actions.push('Try a simpler operation first');
234
+ break;
235
+ default:
236
+ actions.push('Try the operation again');
237
+ actions.push('If the problem persists, restart the MCP server');
238
+ actions.push('Check the debug logs for more details');
239
+ break;
240
+ }
241
+ return actions;
242
+ }
243
+ /**
244
+ * Determine if error is recoverable
245
+ */
246
+ static isRecoverable(error, errorType) {
247
+ switch (errorType) {
248
+ case 'authentication':
249
+ case 'permission':
250
+ case 'validation':
251
+ return false; // These require user intervention
252
+ case 'rate_limit':
253
+ case 'network':
254
+ case 'server':
255
+ return true; // These can be retried
256
+ default:
257
+ return true; // Default to recoverable
258
+ }
259
+ }
260
+ /**
261
+ * Get retry delay from error
262
+ */
263
+ static getRetryAfter(error, errorType) {
264
+ if (errorType === 'rate_limit') {
265
+ // Check for Retry-After header
266
+ if (error.headers?.['retry-after']) {
267
+ return parseInt(error.headers['retry-after']) * 1000;
268
+ }
269
+ return 60000; // Default 60 seconds for rate limits
270
+ }
271
+ if (errorType === 'server') {
272
+ return 30000; // 30 seconds for server errors
273
+ }
274
+ return undefined;
275
+ }
276
+ /**
277
+ * Get or create circuit breaker for operation
278
+ */
279
+ static getCircuitBreaker(operationName) {
280
+ if (!this.circuitBreakers.has(operationName)) {
281
+ this.circuitBreakers.set(operationName, new CircuitBreaker());
282
+ }
283
+ return this.circuitBreakers.get(operationName);
284
+ }
285
+ /**
286
+ * Track error frequency
287
+ */
288
+ static trackError(operation, errorType) {
289
+ const key = `${operation}:${errorType}`;
290
+ const now = Date.now();
291
+ const hourMs = 60 * 60 * 1000;
292
+ if (!this.errorCounts.has(key)) {
293
+ this.errorCounts.set(key, { count: 0, lastReset: now });
294
+ }
295
+ const errorData = this.errorCounts.get(key);
296
+ // Reset counter if it's been more than an hour
297
+ if (now - errorData.lastReset > hourMs) {
298
+ errorData.count = 0;
299
+ errorData.lastReset = now;
300
+ }
301
+ errorData.count++;
302
+ // Log warning if error rate is high
303
+ if (errorData.count > 10) {
304
+ logger.error(`⚠️ High error rate detected for ${operation}:${errorType} (${errorData.count} errors in the last hour)`);
305
+ }
306
+ }
307
+ /**
308
+ * Get error statistics
309
+ */
310
+ static getErrorStats() {
311
+ return Object.fromEntries(this.errorCounts);
312
+ }
313
+ /**
314
+ * Get circuit breaker status
315
+ */
316
+ static getCircuitBreakerStatus() {
317
+ const status = {};
318
+ for (const [name, breaker] of this.circuitBreakers) {
319
+ status[name] = breaker.getState();
320
+ }
321
+ return status;
322
+ }
323
+ }
324
+ EnhancedErrorHandler.circuitBreakers = new Map();
325
+ EnhancedErrorHandler.errorCounts = new Map();
326
+ // Helper function to wrap results
327
+ export function wrapResult(data) {
328
+ return { success: true, data };
329
+ }
330
+ // Helper function to check if result is success
331
+ export function isSuccess(result) {
332
+ return result.success;
333
+ }
334
+ // Helper function to check if result is error
335
+ export function isError(result) {
336
+ return !result.success;
337
+ }
@@ -0,0 +1,71 @@
1
+ import { CrossReferenceDetector } from './cross-reference-detector.js';
2
+ import { EnhancedFuzzySearch } from './enhanced-fuzzy-search.js';
3
+ import { ThreadReconstruction } from './thread-reconstruction.js';
4
+ import { logger } from './api.js';
5
+ export class IntelligenceEngine {
6
+ constructor(ms365Operations) {
7
+ this.ms365Operations = ms365Operations;
8
+ this.crossRefDetector = new CrossReferenceDetector(ms365Operations);
9
+ this.fuzzySearch = new EnhancedFuzzySearch(ms365Operations);
10
+ this.threadReconstruction = new ThreadReconstruction(ms365Operations);
11
+ }
12
+ async intelligentSearch(query, emails, options = {}) {
13
+ const startTime = Date.now();
14
+ logger.log(`🧠 Starting intelligent search for: "${query}"`);
15
+ const result = {
16
+ originalQuery: query,
17
+ results: [],
18
+ crossReferences: new Map(),
19
+ fuzzyMatches: [],
20
+ threads: new Map(),
21
+ insights: {
22
+ totalEmails: emails.length,
23
+ highPriorityEmails: 0,
24
+ threadCount: 0,
25
+ averageConfidence: 0,
26
+ processingTime: 0
27
+ }
28
+ };
29
+ // 1. Enhanced fuzzy search
30
+ if (options.enableFuzzySearch !== false) {
31
+ result.fuzzyMatches = await this.fuzzySearch.naturalLanguageSearch(query, emails);
32
+ result.results = result.fuzzyMatches.map(match => match.email);
33
+ }
34
+ // 2. Cross-reference detection
35
+ if (options.enableCrossReference !== false && result.results.length > 0) {
36
+ result.crossReferences = await this.crossRefDetector.findAllCrossReferences(result.results);
37
+ }
38
+ // 3. Thread reconstruction
39
+ if (options.enableThreadReconstruction !== false && result.results.length > 0) {
40
+ result.threads = await this.threadReconstruction.batchReconstructThreads(result.results);
41
+ }
42
+ // 4. Calculate insights
43
+ result.insights = this.calculateInsights(result, startTime);
44
+ logger.log(`🧠 Intelligent search completed in ${result.insights.processingTime}ms`);
45
+ return result;
46
+ }
47
+ calculateInsights(result, startTime) {
48
+ const highPriorityEmails = result.results.filter(email => email.importance === 'high' ||
49
+ this.isGovernmentEmail(email) ||
50
+ this.isUrgentEmail(email)).length;
51
+ const totalConfidence = result.fuzzyMatches.reduce((sum, match) => sum + match.score, 0);
52
+ const averageConfidence = result.fuzzyMatches.length > 0 ? totalConfidence / result.fuzzyMatches.length : 0;
53
+ return {
54
+ totalEmails: result.results.length,
55
+ highPriorityEmails,
56
+ threadCount: result.threads.size,
57
+ averageConfidence,
58
+ processingTime: Date.now() - startTime
59
+ };
60
+ }
61
+ isGovernmentEmail(email) {
62
+ const govPatterns = ['.gov', 'government', 'federal', 'state', 'municipal', 'irs', 'tax'];
63
+ const text = `${email.from.address} ${email.subject} ${email.bodyPreview}`.toLowerCase();
64
+ return govPatterns.some(pattern => text.includes(pattern));
65
+ }
66
+ isUrgentEmail(email) {
67
+ const urgentPatterns = ['urgent', 'asap', 'immediately', 'deadline', 'due'];
68
+ const text = `${email.subject} ${email.bodyPreview}`.toLowerCase();
69
+ return urgentPatterns.some(pattern => text.includes(pattern));
70
+ }
71
+ }