ms365-mcp-server 1.0.4 ā 1.0.5
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.
- package/dist/index.js +36 -3
- package/dist/utils/ms365-auth-enhanced.js +54 -5
- package/dist/utils/ms365-operations.js +200 -43
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -55,7 +55,7 @@ function parseArgs() {
|
|
|
55
55
|
}
|
|
56
56
|
const server = new Server({
|
|
57
57
|
name: "ms365-mcp-server",
|
|
58
|
-
version: "1.0.
|
|
58
|
+
version: "1.0.5"
|
|
59
59
|
}, {
|
|
60
60
|
capabilities: {
|
|
61
61
|
resources: {
|
|
@@ -770,11 +770,22 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
770
770
|
const isAuthenticated = await enhancedMS365Auth.isAuthenticated();
|
|
771
771
|
const currentUser = await enhancedMS365Auth.getCurrentUser();
|
|
772
772
|
const storageInfo = enhancedMS365Auth.getStorageInfo();
|
|
773
|
+
const tokenInfo = await enhancedMS365Auth.getTokenExpirationInfo();
|
|
774
|
+
let statusText = `š Microsoft 365 Authentication Status\n\nš Authentication: ${isAuthenticated ? 'ā
Valid' : 'ā Not authenticated'}\nš¤ Current User: ${currentUser || 'None'}\nš¾ Storage method: ${storageInfo.method}\nš Storage location: ${storageInfo.location}`;
|
|
775
|
+
if (isAuthenticated) {
|
|
776
|
+
statusText += `\nā° Token expires in: ${tokenInfo.expiresInMinutes} minutes`;
|
|
777
|
+
if (tokenInfo.needsRefresh) {
|
|
778
|
+
statusText += `\nā ļø Token will be refreshed automatically on next operation`;
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
else {
|
|
782
|
+
statusText += `\nš” Use the "authenticate_with_device_code" tool to sign in`;
|
|
783
|
+
}
|
|
773
784
|
return {
|
|
774
785
|
content: [
|
|
775
786
|
{
|
|
776
787
|
type: "text",
|
|
777
|
-
text:
|
|
788
|
+
text: statusText
|
|
778
789
|
}
|
|
779
790
|
]
|
|
780
791
|
};
|
|
@@ -881,11 +892,33 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
881
892
|
ms365Ops.setGraphClient(graphClient);
|
|
882
893
|
}
|
|
883
894
|
const searchResults = await ms365Ops.searchEmails(args);
|
|
895
|
+
// Enhanced feedback for search results
|
|
896
|
+
let responseText = `š Email Search Results (${searchResults.messages.length} found)`;
|
|
897
|
+
if (searchResults.messages.length === 0) {
|
|
898
|
+
responseText = `š No emails found matching your criteria.\n\nš” Search Tips:\n`;
|
|
899
|
+
if (args?.from) {
|
|
900
|
+
responseText += `⢠Try partial names: "${args.from.split(' ')[0]}" or "${args.from.split(' ').pop()}"\n`;
|
|
901
|
+
responseText += `⢠Check spelling of sender name\n`;
|
|
902
|
+
}
|
|
903
|
+
if (args?.subject) {
|
|
904
|
+
responseText += `⢠Try broader subject terms\n`;
|
|
905
|
+
}
|
|
906
|
+
if (args?.after || args?.before) {
|
|
907
|
+
responseText += `⢠Try expanding date range\n`;
|
|
908
|
+
}
|
|
909
|
+
responseText += `⢠Remove some search criteria to get broader results`;
|
|
910
|
+
}
|
|
911
|
+
else {
|
|
912
|
+
responseText += `\n\n${searchResults.messages.map((email, index) => `${index + 1}. š§ ${email.subject}\n š¤ From: ${email.from.name} <${email.from.address}>\n š
${new Date(email.receivedDateTime).toLocaleDateString()}\n ${email.isRead ? 'š Read' : 'š© Unread'}\n š ID: ${email.id}\n`).join('\n')}`;
|
|
913
|
+
if (searchResults.hasMore) {
|
|
914
|
+
responseText += `\nš” There are more results available. Use maxResults parameter to get more emails.`;
|
|
915
|
+
}
|
|
916
|
+
}
|
|
884
917
|
return {
|
|
885
918
|
content: [
|
|
886
919
|
{
|
|
887
920
|
type: "text",
|
|
888
|
-
text:
|
|
921
|
+
text: responseText
|
|
889
922
|
}
|
|
890
923
|
]
|
|
891
924
|
};
|
|
@@ -347,12 +347,52 @@ export class EnhancedMS365Auth {
|
|
|
347
347
|
return client;
|
|
348
348
|
}
|
|
349
349
|
/**
|
|
350
|
-
*
|
|
350
|
+
* Get token expiration information for proactive refresh
|
|
351
|
+
*/
|
|
352
|
+
async getTokenExpirationInfo() {
|
|
353
|
+
try {
|
|
354
|
+
const storedToken = await this.loadStoredToken();
|
|
355
|
+
if (!storedToken) {
|
|
356
|
+
return { expiresInMinutes: 0, needsRefresh: true };
|
|
357
|
+
}
|
|
358
|
+
const now = Date.now();
|
|
359
|
+
const expiresInMs = storedToken.expiresOn - now;
|
|
360
|
+
const expiresInMinutes = Math.floor(expiresInMs / (1000 * 60));
|
|
361
|
+
return {
|
|
362
|
+
expiresInMinutes: Math.max(0, expiresInMinutes),
|
|
363
|
+
needsRefresh: expiresInMinutes < 5 // Refresh if expiring within 5 minutes
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
catch (error) {
|
|
367
|
+
logger.error('Error getting token expiration info:', error);
|
|
368
|
+
return { expiresInMinutes: 0, needsRefresh: true };
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Refresh token if needed (proactive refresh)
|
|
373
|
+
*/
|
|
374
|
+
async refreshTokenIfNeeded() {
|
|
375
|
+
try {
|
|
376
|
+
const tokenInfo = await this.getTokenExpirationInfo();
|
|
377
|
+
if (tokenInfo.needsRefresh) {
|
|
378
|
+
logger.log('Proactively refreshing token to prevent interruption...');
|
|
379
|
+
await this.refreshToken();
|
|
380
|
+
return true;
|
|
381
|
+
}
|
|
382
|
+
return false;
|
|
383
|
+
}
|
|
384
|
+
catch (error) {
|
|
385
|
+
logger.error('Proactive token refresh failed:', error);
|
|
386
|
+
return false;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Enhanced refresh token with better error handling
|
|
351
391
|
*/
|
|
352
392
|
async refreshToken() {
|
|
353
393
|
const storedToken = await this.loadStoredToken();
|
|
354
394
|
if (!storedToken?.account) {
|
|
355
|
-
throw new Error('No account information available. Please re-authenticate
|
|
395
|
+
throw new Error('No account information available. Please re-authenticate using: authenticate_with_device_code');
|
|
356
396
|
}
|
|
357
397
|
if (!await this.loadCredentials()) {
|
|
358
398
|
throw new Error('MS365 credentials not configured');
|
|
@@ -364,14 +404,23 @@ export class EnhancedMS365Auth {
|
|
|
364
404
|
account: storedToken.account
|
|
365
405
|
});
|
|
366
406
|
if (!tokenResponse) {
|
|
367
|
-
throw new Error('Failed to refresh token');
|
|
407
|
+
throw new Error('Failed to refresh token - please re-authenticate using: authenticate_with_device_code');
|
|
368
408
|
}
|
|
369
409
|
await this.saveToken(tokenResponse, storedToken.authType);
|
|
370
410
|
logger.log('MS365 token refreshed successfully');
|
|
371
411
|
}
|
|
372
412
|
catch (error) {
|
|
373
|
-
|
|
374
|
-
|
|
413
|
+
// Enhanced error handling with user-friendly messages
|
|
414
|
+
if (error.errorCode === 'invalid_grant' || error.errorCode === 'interaction_required') {
|
|
415
|
+
throw new Error('Authentication has expired. Please re-authenticate using the "authenticate_with_device_code" tool.');
|
|
416
|
+
}
|
|
417
|
+
else if (error.errorCode === 'consent_required') {
|
|
418
|
+
throw new Error('Additional consent required. Please re-authenticate using the "authenticate_with_device_code" tool.');
|
|
419
|
+
}
|
|
420
|
+
else {
|
|
421
|
+
logger.error('Token refresh failed:', error);
|
|
422
|
+
throw new Error(`Token refresh failed: ${error.message}. Please re-authenticate using the "authenticate_with_device_code" tool.`);
|
|
423
|
+
}
|
|
375
424
|
}
|
|
376
425
|
}
|
|
377
426
|
/**
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { ms365Auth } from './ms365-auth.js';
|
|
2
1
|
import { logger } from './api.js';
|
|
3
2
|
/**
|
|
4
3
|
* Microsoft 365 operations manager class
|
|
@@ -6,6 +5,38 @@ import { logger } from './api.js';
|
|
|
6
5
|
export class MS365Operations {
|
|
7
6
|
constructor() {
|
|
8
7
|
this.graphClient = null;
|
|
8
|
+
this.searchCache = new Map();
|
|
9
|
+
this.CACHE_DURATION = 60 * 1000; // 1 minute cache
|
|
10
|
+
this.MAX_RETRIES = 3;
|
|
11
|
+
this.BASE_DELAY = 1000; // 1 second
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Execute API call with retry logic and exponential backoff
|
|
15
|
+
*/
|
|
16
|
+
async executeWithRetry(operation, context = 'API call') {
|
|
17
|
+
let lastError;
|
|
18
|
+
for (let attempt = 1; attempt <= this.MAX_RETRIES; attempt++) {
|
|
19
|
+
try {
|
|
20
|
+
return await operation();
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
lastError = error;
|
|
24
|
+
// Don't retry on authentication errors or client errors (4xx)
|
|
25
|
+
if (error.code === 'InvalidAuthenticationToken' ||
|
|
26
|
+
(error.status >= 400 && error.status < 500)) {
|
|
27
|
+
throw error;
|
|
28
|
+
}
|
|
29
|
+
if (attempt === this.MAX_RETRIES) {
|
|
30
|
+
logger.error(`${context} failed after ${this.MAX_RETRIES} attempts:`, error);
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
33
|
+
// Exponential backoff with jitter
|
|
34
|
+
const delay = this.BASE_DELAY * Math.pow(2, attempt - 1) + Math.random() * 1000;
|
|
35
|
+
logger.log(`${context} failed (attempt ${attempt}), retrying in ${delay}ms...`);
|
|
36
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
throw lastError;
|
|
9
40
|
}
|
|
10
41
|
/**
|
|
11
42
|
* Set the Microsoft Graph client externally
|
|
@@ -14,14 +45,52 @@ export class MS365Operations {
|
|
|
14
45
|
this.graphClient = client;
|
|
15
46
|
}
|
|
16
47
|
/**
|
|
17
|
-
* Get authenticated Microsoft Graph client
|
|
48
|
+
* Get authenticated Microsoft Graph client with proactive token refresh
|
|
18
49
|
*/
|
|
19
50
|
async getGraphClient() {
|
|
20
51
|
if (!this.graphClient) {
|
|
21
|
-
|
|
52
|
+
// Import the enhanced auth module dynamically to avoid circular imports
|
|
53
|
+
const { enhancedMS365Auth } = await import('./ms365-auth-enhanced.js');
|
|
54
|
+
// Proactive token check - refresh if expiring within 5 minutes
|
|
55
|
+
const tokenInfo = await enhancedMS365Auth.getTokenExpirationInfo();
|
|
56
|
+
if (tokenInfo.expiresInMinutes < 5) {
|
|
57
|
+
logger.log(`Token expires in ${tokenInfo.expiresInMinutes} minutes, refreshing proactively...`);
|
|
58
|
+
await enhancedMS365Auth.refreshTokenIfNeeded();
|
|
59
|
+
}
|
|
60
|
+
this.graphClient = await enhancedMS365Auth.getGraphClient();
|
|
22
61
|
}
|
|
23
62
|
return this.graphClient;
|
|
24
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* Clear expired cache entries
|
|
66
|
+
*/
|
|
67
|
+
clearExpiredCache() {
|
|
68
|
+
const now = Date.now();
|
|
69
|
+
for (const [key, value] of this.searchCache.entries()) {
|
|
70
|
+
if (now - value.timestamp > this.CACHE_DURATION) {
|
|
71
|
+
this.searchCache.delete(key);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Get cached search results if available and not expired
|
|
77
|
+
*/
|
|
78
|
+
getCachedResults(cacheKey) {
|
|
79
|
+
this.clearExpiredCache();
|
|
80
|
+
const cached = this.searchCache.get(cacheKey);
|
|
81
|
+
if (cached && Date.now() - cached.timestamp < this.CACHE_DURATION) {
|
|
82
|
+
logger.log(`Cache hit for search: ${cacheKey}`);
|
|
83
|
+
return cached.results;
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Cache search results
|
|
89
|
+
*/
|
|
90
|
+
setCachedResults(cacheKey, results) {
|
|
91
|
+
this.searchCache.set(cacheKey, { results, timestamp: Date.now() });
|
|
92
|
+
logger.log(`Cached results for search: ${cacheKey}`);
|
|
93
|
+
}
|
|
25
94
|
/**
|
|
26
95
|
* Build filter query for Microsoft Graph API
|
|
27
96
|
*/
|
|
@@ -235,54 +304,57 @@ export class MS365Operations {
|
|
|
235
304
|
* Search emails with criteria
|
|
236
305
|
*/
|
|
237
306
|
async searchEmails(criteria = {}) {
|
|
238
|
-
|
|
307
|
+
return await this.executeWithAuth(async () => {
|
|
239
308
|
const graphClient = await this.getGraphClient();
|
|
309
|
+
// Create cache key from criteria
|
|
310
|
+
const cacheKey = JSON.stringify(criteria);
|
|
311
|
+
const cachedResults = this.getCachedResults(cacheKey);
|
|
312
|
+
if (cachedResults) {
|
|
313
|
+
return cachedResults;
|
|
314
|
+
}
|
|
240
315
|
// For name-based searches, use enhanced approach for better partial matching
|
|
241
316
|
if (criteria.from && !criteria.from.includes('@')) {
|
|
242
317
|
logger.log(`Using enhanced name matching for: "${criteria.from}"`);
|
|
243
318
|
// Get more emails and filter manually for better name matching
|
|
244
319
|
const maxResults = criteria.maxResults || 100;
|
|
245
320
|
try {
|
|
246
|
-
//
|
|
321
|
+
// Optimized field selection for name search - only get essential fields first
|
|
322
|
+
const essentialFields = 'id,subject,from,receivedDateTime,isRead,hasAttachments,importance';
|
|
247
323
|
const apiCall = graphClient.api('/me/messages')
|
|
248
|
-
.select(
|
|
324
|
+
.select(essentialFields)
|
|
249
325
|
.orderby('receivedDateTime desc')
|
|
250
|
-
.top(Math.min(maxResults *
|
|
326
|
+
.top(Math.min(maxResults * 3, 300)); // Get more emails for better filtering
|
|
251
327
|
const result = await apiCall.get();
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
from
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
conversationId: email.conversationId || '',
|
|
274
|
-
parentFolderId: email.parentFolderId || '',
|
|
275
|
-
webLink: email.webLink || ''
|
|
276
|
-
})) || [];
|
|
277
|
-
// Apply manual filtering with enhanced name matching
|
|
278
|
-
messages = this.applyManualFiltering(messages, criteria);
|
|
328
|
+
// First pass: filter by name using minimal data
|
|
329
|
+
const nameMatches = result.value?.filter((email) => {
|
|
330
|
+
const fromName = email.from?.emailAddress?.name?.toLowerCase() || '';
|
|
331
|
+
const fromAddress = email.from?.emailAddress?.address?.toLowerCase() || '';
|
|
332
|
+
const searchTerm = criteria.from.toLowerCase().trim();
|
|
333
|
+
// Quick name matching (subset of the full logic for performance)
|
|
334
|
+
return fromName.includes(searchTerm) ||
|
|
335
|
+
fromAddress.includes(searchTerm) ||
|
|
336
|
+
fromName.split(/\s+/).some((part) => part.startsWith(searchTerm)) ||
|
|
337
|
+
searchTerm.split(/\s+/).every((part) => fromName.includes(part));
|
|
338
|
+
}) || [];
|
|
339
|
+
if (nameMatches.length === 0) {
|
|
340
|
+
const emptyResult = { messages: [], hasMore: false };
|
|
341
|
+
this.setCachedResults(cacheKey, emptyResult);
|
|
342
|
+
return emptyResult;
|
|
343
|
+
}
|
|
344
|
+
// Second pass: get full details for matched emails
|
|
345
|
+
const messageIds = nameMatches.slice(0, maxResults).map((email) => email.id);
|
|
346
|
+
const fullMessages = await this.getEmailsByIds(messageIds);
|
|
347
|
+
// Apply remaining criteria filtering
|
|
348
|
+
let messages = this.applyManualFiltering(fullMessages, criteria);
|
|
279
349
|
// Apply maxResults limit
|
|
280
350
|
const limitedMessages = messages.slice(0, maxResults);
|
|
281
351
|
logger.log(`Enhanced name search found ${limitedMessages.length} emails matching "${criteria.from}"`);
|
|
282
|
-
|
|
352
|
+
const result_final = {
|
|
283
353
|
messages: limitedMessages,
|
|
284
|
-
hasMore: messages.length > maxResults ||
|
|
354
|
+
hasMore: messages.length > maxResults || nameMatches.length > maxResults
|
|
285
355
|
};
|
|
356
|
+
this.setCachedResults(cacheKey, result_final);
|
|
357
|
+
return result_final;
|
|
286
358
|
}
|
|
287
359
|
catch (error) {
|
|
288
360
|
logger.error('Error in enhanced name search, falling back to standard search:', error);
|
|
@@ -375,10 +447,12 @@ export class MS365Operations {
|
|
|
375
447
|
if (useSearchAPI && (criteria.folder || filterQuery)) {
|
|
376
448
|
messages = this.applyManualFiltering(messages, criteria);
|
|
377
449
|
}
|
|
378
|
-
|
|
450
|
+
const standardResult = {
|
|
379
451
|
messages,
|
|
380
452
|
hasMore: !!result['@odata.nextLink']
|
|
381
453
|
};
|
|
454
|
+
this.setCachedResults(cacheKey, standardResult);
|
|
455
|
+
return standardResult;
|
|
382
456
|
}
|
|
383
457
|
catch (searchError) {
|
|
384
458
|
// If search API fails due to syntax error, fall back to filter-only approach
|
|
@@ -431,21 +505,19 @@ export class MS365Operations {
|
|
|
431
505
|
// Apply manual filtering for any criteria that couldn't be handled by the filter
|
|
432
506
|
// This includes subject, to, cc, and query filters that need manual processing
|
|
433
507
|
messages = this.applyManualFiltering(messages, criteria);
|
|
434
|
-
|
|
508
|
+
const fallbackResult = {
|
|
435
509
|
messages,
|
|
436
510
|
hasMore: !!result['@odata.nextLink']
|
|
437
511
|
};
|
|
512
|
+
this.setCachedResults(cacheKey, fallbackResult);
|
|
513
|
+
return fallbackResult;
|
|
438
514
|
}
|
|
439
515
|
else {
|
|
440
516
|
// Re-throw other errors
|
|
441
517
|
throw searchError;
|
|
442
518
|
}
|
|
443
519
|
}
|
|
444
|
-
}
|
|
445
|
-
catch (error) {
|
|
446
|
-
logger.error('Error searching emails:', error);
|
|
447
|
-
throw error;
|
|
448
|
-
}
|
|
520
|
+
}, 'searchEmails');
|
|
449
521
|
}
|
|
450
522
|
/**
|
|
451
523
|
* Apply manual filtering to search results (used when $filter can't be used with $search)
|
|
@@ -791,5 +863,90 @@ export class MS365Operations {
|
|
|
791
863
|
throw error;
|
|
792
864
|
}
|
|
793
865
|
}
|
|
866
|
+
/**
|
|
867
|
+
* Get multiple emails by their IDs efficiently
|
|
868
|
+
*/
|
|
869
|
+
async getEmailsByIds(messageIds) {
|
|
870
|
+
try {
|
|
871
|
+
const graphClient = await this.getGraphClient();
|
|
872
|
+
// Batch request for better performance when getting multiple emails
|
|
873
|
+
const emails = [];
|
|
874
|
+
// Process in batches of 20 to stay within Graph API limits
|
|
875
|
+
const batchSize = 20;
|
|
876
|
+
for (let i = 0; i < messageIds.length; i += batchSize) {
|
|
877
|
+
const batch = messageIds.slice(i, i + batchSize);
|
|
878
|
+
// Get full details for this batch
|
|
879
|
+
const promises = batch.map(id => graphClient
|
|
880
|
+
.api(`/me/messages/${id}`)
|
|
881
|
+
.select('id,subject,from,toRecipients,ccRecipients,receivedDateTime,sentDateTime,bodyPreview,isRead,hasAttachments,importance,conversationId,parentFolderId,webLink')
|
|
882
|
+
.get());
|
|
883
|
+
const results = await Promise.all(promises);
|
|
884
|
+
const batchEmails = results.map((email) => ({
|
|
885
|
+
id: email.id,
|
|
886
|
+
subject: email.subject || '',
|
|
887
|
+
from: {
|
|
888
|
+
name: email.from?.emailAddress?.name || '',
|
|
889
|
+
address: email.from?.emailAddress?.address || ''
|
|
890
|
+
},
|
|
891
|
+
toRecipients: email.toRecipients?.map((recipient) => ({
|
|
892
|
+
name: recipient.emailAddress?.name || '',
|
|
893
|
+
address: recipient.emailAddress?.address || ''
|
|
894
|
+
})) || [],
|
|
895
|
+
ccRecipients: email.ccRecipients?.map((recipient) => ({
|
|
896
|
+
name: recipient.emailAddress?.name || '',
|
|
897
|
+
address: recipient.emailAddress?.address || ''
|
|
898
|
+
})) || [],
|
|
899
|
+
receivedDateTime: email.receivedDateTime,
|
|
900
|
+
sentDateTime: email.sentDateTime,
|
|
901
|
+
bodyPreview: email.bodyPreview || '',
|
|
902
|
+
isRead: email.isRead || false,
|
|
903
|
+
hasAttachments: email.hasAttachments || false,
|
|
904
|
+
importance: email.importance || 'normal',
|
|
905
|
+
conversationId: email.conversationId || '',
|
|
906
|
+
parentFolderId: email.parentFolderId || '',
|
|
907
|
+
webLink: email.webLink || ''
|
|
908
|
+
}));
|
|
909
|
+
emails.push(...batchEmails);
|
|
910
|
+
}
|
|
911
|
+
return emails;
|
|
912
|
+
}
|
|
913
|
+
catch (error) {
|
|
914
|
+
logger.error('Error getting emails by IDs:', error);
|
|
915
|
+
throw error;
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
/**
|
|
919
|
+
* Clear cached graph client (used when authentication fails)
|
|
920
|
+
*/
|
|
921
|
+
clearGraphClient() {
|
|
922
|
+
this.graphClient = null;
|
|
923
|
+
logger.log('Cleared cached graph client due to authentication failure');
|
|
924
|
+
}
|
|
925
|
+
/**
|
|
926
|
+
* Execute operation with authentication retry
|
|
927
|
+
*/
|
|
928
|
+
async executeWithAuth(operation, operationName) {
|
|
929
|
+
try {
|
|
930
|
+
return await this.executeWithRetry(operation, operationName);
|
|
931
|
+
}
|
|
932
|
+
catch (error) {
|
|
933
|
+
// Check if it's an authentication error
|
|
934
|
+
if (error.code === 'InvalidAuthenticationToken' ||
|
|
935
|
+
error.code === 'Unauthorized' ||
|
|
936
|
+
error.status === 401) {
|
|
937
|
+
logger.log(`Authentication failed for ${operationName}, clearing cache and retrying...`);
|
|
938
|
+
this.clearGraphClient();
|
|
939
|
+
// Retry once with fresh authentication
|
|
940
|
+
try {
|
|
941
|
+
return await this.executeWithRetry(operation, `${operationName} (retry after auth failure)`);
|
|
942
|
+
}
|
|
943
|
+
catch (retryError) {
|
|
944
|
+
logger.error(`${operationName} failed even after authentication retry:`, retryError);
|
|
945
|
+
throw new Error(`Authentication failed. Please re-authenticate using the "authenticate_with_device_code" tool. Details: ${retryError.message}`);
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
throw error;
|
|
949
|
+
}
|
|
950
|
+
}
|
|
794
951
|
}
|
|
795
952
|
export const ms365Operations = new MS365Operations();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ms365-mcp-server",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.5",
|
|
4
4
|
"description": "Microsoft 365 MCP Server for managing Microsoft 365 email through natural language interactions with full OAuth2 authentication support",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|