ms365-mcp-server 1.0.3 ā 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/README.md +7 -5
- package/dist/index.js +58 -25
- package/dist/utils/ms365-auth-enhanced.js +54 -5
- package/dist/utils/ms365-operations.js +258 -16
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@ A powerful **Model Context Protocol (MCP) server** that enables seamless Microso
|
|
|
18
18
|
### š§ Core Email Operations (12 tools)
|
|
19
19
|
- **`send_email`** - Send emails with attachments, HTML/text content, CC/BCC
|
|
20
20
|
- **`read_email`** - Read full email content including headers and attachments
|
|
21
|
-
- **`search_emails`** - Advanced search with
|
|
21
|
+
- **`search_emails`** - Advanced search with intelligent partial name matching
|
|
22
22
|
- **`search_emails_to_me`** - Find emails addressed to YOU (both TO and CC fields)
|
|
23
23
|
- **`list_emails`** - List emails in inbox, sent, or custom folders
|
|
24
24
|
- **`get_attachment`** - Download and retrieve email attachments
|
|
@@ -139,10 +139,12 @@ ms365-mcp-server --setup-auth
|
|
|
139
139
|
|
|
140
140
|
### š Email Search Options
|
|
141
141
|
|
|
142
|
-
- **`search_emails`** -
|
|
143
|
-
-
|
|
144
|
-
-
|
|
145
|
-
-
|
|
142
|
+
- **`search_emails`** - Unified search with intelligent capabilities
|
|
143
|
+
- **Smart Name Matching**: Automatically detects names vs email addresses
|
|
144
|
+
- **Partial Name Support**: Search by "Madan", "Kumar", "M Kumar", etc.
|
|
145
|
+
- **Flexible Criteria**: Combine name search with date ranges, subjects, etc.
|
|
146
|
+
- **Email Address Matching**: Use exact email addresses when needed
|
|
147
|
+
- Example: `{"from": "Madan", "after": "2024-01-01"}` or `{"from": "boss@company.com"}`
|
|
146
148
|
|
|
147
149
|
- **`search_emails_to_me`** - Automatic search for emails addressed to YOU
|
|
148
150
|
- Automatically uses your email address for both TO and CC searches
|
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: {
|
|
@@ -197,7 +197,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
197
197
|
},
|
|
198
198
|
{
|
|
199
199
|
name: "search_emails",
|
|
200
|
-
description: "Search emails with various criteria including subject, sender, TO/CC recipients, date range, and advanced filtering. Supports complex queries and filtering, including emails addressed to you directly or where you were CC'd.",
|
|
200
|
+
description: "Search emails with various criteria including subject, sender, TO/CC recipients, date range, and advanced filtering. Features intelligent partial name matching for senders - just use their first name, last name, or partial name in the 'from' field. Supports complex queries and filtering, including emails addressed to you directly or where you were CC'd.",
|
|
201
201
|
inputSchema: {
|
|
202
202
|
type: "object",
|
|
203
203
|
properties: {
|
|
@@ -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,37 +892,37 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
881
892
|
ms365Ops.setGraphClient(graphClient);
|
|
882
893
|
}
|
|
883
894
|
const searchResults = await ms365Ops.searchEmails(args);
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
}
|
|
890
|
-
|
|
891
|
-
};
|
|
892
|
-
case "search_emails_to_me":
|
|
893
|
-
if (ms365Config.multiUser) {
|
|
894
|
-
const userId = args?.userId;
|
|
895
|
-
if (!userId) {
|
|
896
|
-
throw new Error("User ID is required in multi-user mode");
|
|
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`;
|
|
897
902
|
}
|
|
898
|
-
|
|
899
|
-
|
|
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`;
|
|
900
910
|
}
|
|
901
911
|
else {
|
|
902
|
-
|
|
903
|
-
|
|
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
|
+
}
|
|
904
916
|
}
|
|
905
|
-
const searchToMeResults = await ms365Ops.searchEmailsToMe(args);
|
|
906
917
|
return {
|
|
907
918
|
content: [
|
|
908
919
|
{
|
|
909
920
|
type: "text",
|
|
910
|
-
text:
|
|
921
|
+
text: responseText
|
|
911
922
|
}
|
|
912
923
|
]
|
|
913
924
|
};
|
|
914
|
-
case "
|
|
925
|
+
case "search_emails_to_me":
|
|
915
926
|
if (ms365Config.multiUser) {
|
|
916
927
|
const userId = args?.userId;
|
|
917
928
|
if (!userId) {
|
|
@@ -924,12 +935,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
924
935
|
const graphClient = await enhancedMS365Auth.getGraphClient();
|
|
925
936
|
ms365Ops.setGraphClient(graphClient);
|
|
926
937
|
}
|
|
927
|
-
const
|
|
938
|
+
const searchToMeResults = await ms365Ops.searchEmailsToMe(args);
|
|
928
939
|
return {
|
|
929
940
|
content: [
|
|
930
941
|
{
|
|
931
942
|
type: "text",
|
|
932
|
-
text:
|
|
943
|
+
text: `š Emails Addressed to You (TO & CC) - ${searchToMeResults.messages.length} found\n\n${searchToMeResults.messages.map((email, index) => `${index + 1}. š§ ${email.subject}\n š¤ From: ${email.from.name} <${email.from.address}>\n š
${new Date(email.receivedDateTime).toLocaleDateString()}\n š ID: ${email.id}\n`).join('\n')}`
|
|
933
944
|
}
|
|
934
945
|
]
|
|
935
946
|
};
|
|
@@ -1087,6 +1098,28 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1087
1098
|
}
|
|
1088
1099
|
]
|
|
1089
1100
|
};
|
|
1101
|
+
case "list_emails":
|
|
1102
|
+
if (ms365Config.multiUser) {
|
|
1103
|
+
const userId = args?.userId;
|
|
1104
|
+
if (!userId) {
|
|
1105
|
+
throw new Error("User ID is required in multi-user mode");
|
|
1106
|
+
}
|
|
1107
|
+
const graphClient = await multiUserMS365Auth.getGraphClientForUser(userId);
|
|
1108
|
+
ms365Ops.setGraphClient(graphClient);
|
|
1109
|
+
}
|
|
1110
|
+
else {
|
|
1111
|
+
const graphClient = await enhancedMS365Auth.getGraphClient();
|
|
1112
|
+
ms365Ops.setGraphClient(graphClient);
|
|
1113
|
+
}
|
|
1114
|
+
const emailList = await ms365Ops.listEmails(args?.folderId, args?.maxResults);
|
|
1115
|
+
return {
|
|
1116
|
+
content: [
|
|
1117
|
+
{
|
|
1118
|
+
type: "text",
|
|
1119
|
+
text: `š¬ Email List (${emailList.messages.length} emails)\n\n${emailList.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 ? 'š' : 'š©'} ${email.isRead ? 'Read' : 'Unread'}\n š ID: ${email.id}\n`).join('\n')}`
|
|
1120
|
+
}
|
|
1121
|
+
]
|
|
1122
|
+
};
|
|
1090
1123
|
default:
|
|
1091
1124
|
throw new Error(`Unknown tool: ${name}`);
|
|
1092
1125
|
}
|
|
@@ -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
|
*/
|
|
@@ -65,7 +134,20 @@ export class MS365Operations {
|
|
|
65
134
|
// Build email-specific search terms using proper Microsoft Graph syntax
|
|
66
135
|
const emailSearchTerms = [];
|
|
67
136
|
if (criteria.from) {
|
|
68
|
-
|
|
137
|
+
// Check if it looks like an email address for exact matching
|
|
138
|
+
if (criteria.from.includes('@')) {
|
|
139
|
+
emailSearchTerms.push(`from:${criteria.from}`);
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
// For names, use general search which is more flexible with partial matching
|
|
143
|
+
// This will trigger manual filtering which supports partial names better
|
|
144
|
+
if (criteria.query) {
|
|
145
|
+
searchTerms.push(`${criteria.query} AND ${criteria.from}`);
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
searchTerms.push(criteria.from);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
69
151
|
}
|
|
70
152
|
if (criteria.to) {
|
|
71
153
|
emailSearchTerms.push(`to:${criteria.to}`);
|
|
@@ -222,9 +304,64 @@ export class MS365Operations {
|
|
|
222
304
|
* Search emails with criteria
|
|
223
305
|
*/
|
|
224
306
|
async searchEmails(criteria = {}) {
|
|
225
|
-
|
|
307
|
+
return await this.executeWithAuth(async () => {
|
|
226
308
|
const graphClient = await this.getGraphClient();
|
|
227
|
-
//
|
|
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
|
+
}
|
|
315
|
+
// For name-based searches, use enhanced approach for better partial matching
|
|
316
|
+
if (criteria.from && !criteria.from.includes('@')) {
|
|
317
|
+
logger.log(`Using enhanced name matching for: "${criteria.from}"`);
|
|
318
|
+
// Get more emails and filter manually for better name matching
|
|
319
|
+
const maxResults = criteria.maxResults || 100;
|
|
320
|
+
try {
|
|
321
|
+
// Optimized field selection for name search - only get essential fields first
|
|
322
|
+
const essentialFields = 'id,subject,from,receivedDateTime,isRead,hasAttachments,importance';
|
|
323
|
+
const apiCall = graphClient.api('/me/messages')
|
|
324
|
+
.select(essentialFields)
|
|
325
|
+
.orderby('receivedDateTime desc')
|
|
326
|
+
.top(Math.min(maxResults * 3, 300)); // Get more emails for better filtering
|
|
327
|
+
const result = await apiCall.get();
|
|
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);
|
|
349
|
+
// Apply maxResults limit
|
|
350
|
+
const limitedMessages = messages.slice(0, maxResults);
|
|
351
|
+
logger.log(`Enhanced name search found ${limitedMessages.length} emails matching "${criteria.from}"`);
|
|
352
|
+
const result_final = {
|
|
353
|
+
messages: limitedMessages,
|
|
354
|
+
hasMore: messages.length > maxResults || nameMatches.length > maxResults
|
|
355
|
+
};
|
|
356
|
+
this.setCachedResults(cacheKey, result_final);
|
|
357
|
+
return result_final;
|
|
358
|
+
}
|
|
359
|
+
catch (error) {
|
|
360
|
+
logger.error('Error in enhanced name search, falling back to standard search:', error);
|
|
361
|
+
// Fall through to standard search logic
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
// Standard search logic (for email addresses and other criteria)
|
|
228
365
|
const searchQuery = this.buildSearchQuery(criteria);
|
|
229
366
|
const filterQuery = this.buildFilterQuery(criteria);
|
|
230
367
|
// Debug logging
|
|
@@ -310,10 +447,12 @@ export class MS365Operations {
|
|
|
310
447
|
if (useSearchAPI && (criteria.folder || filterQuery)) {
|
|
311
448
|
messages = this.applyManualFiltering(messages, criteria);
|
|
312
449
|
}
|
|
313
|
-
|
|
450
|
+
const standardResult = {
|
|
314
451
|
messages,
|
|
315
452
|
hasMore: !!result['@odata.nextLink']
|
|
316
453
|
};
|
|
454
|
+
this.setCachedResults(cacheKey, standardResult);
|
|
455
|
+
return standardResult;
|
|
317
456
|
}
|
|
318
457
|
catch (searchError) {
|
|
319
458
|
// If search API fails due to syntax error, fall back to filter-only approach
|
|
@@ -366,21 +505,19 @@ export class MS365Operations {
|
|
|
366
505
|
// Apply manual filtering for any criteria that couldn't be handled by the filter
|
|
367
506
|
// This includes subject, to, cc, and query filters that need manual processing
|
|
368
507
|
messages = this.applyManualFiltering(messages, criteria);
|
|
369
|
-
|
|
508
|
+
const fallbackResult = {
|
|
370
509
|
messages,
|
|
371
510
|
hasMore: !!result['@odata.nextLink']
|
|
372
511
|
};
|
|
512
|
+
this.setCachedResults(cacheKey, fallbackResult);
|
|
513
|
+
return fallbackResult;
|
|
373
514
|
}
|
|
374
515
|
else {
|
|
375
516
|
// Re-throw other errors
|
|
376
517
|
throw searchError;
|
|
377
518
|
}
|
|
378
519
|
}
|
|
379
|
-
}
|
|
380
|
-
catch (error) {
|
|
381
|
-
logger.error('Error searching emails:', error);
|
|
382
|
-
throw error;
|
|
383
|
-
}
|
|
520
|
+
}, 'searchEmails');
|
|
384
521
|
}
|
|
385
522
|
/**
|
|
386
523
|
* Apply manual filtering to search results (used when $filter can't be used with $search)
|
|
@@ -395,9 +532,29 @@ export class MS365Operations {
|
|
|
395
532
|
return false;
|
|
396
533
|
}
|
|
397
534
|
if (criteria.from) {
|
|
398
|
-
const
|
|
399
|
-
|
|
400
|
-
|
|
535
|
+
const searchTerm = criteria.from.toLowerCase().trim();
|
|
536
|
+
const fromName = message.from.name.toLowerCase();
|
|
537
|
+
const fromAddress = message.from.address.toLowerCase();
|
|
538
|
+
// Multiple matching strategies for better partial name support
|
|
539
|
+
const matches = [
|
|
540
|
+
// Direct name or email match
|
|
541
|
+
fromName.includes(searchTerm),
|
|
542
|
+
fromAddress.includes(searchTerm),
|
|
543
|
+
// Split search term and check if all parts exist in name
|
|
544
|
+
searchTerm.split(/\s+/).every(part => fromName.includes(part)),
|
|
545
|
+
// Check if any word in the name starts with the search term
|
|
546
|
+
fromName.split(/\s+/).some(namePart => namePart.startsWith(searchTerm)),
|
|
547
|
+
// Check if search term matches any word in the name exactly
|
|
548
|
+
fromName.split(/\s+/).some(namePart => namePart === searchTerm),
|
|
549
|
+
// Handle "Last, First" format
|
|
550
|
+
fromName.replace(/,\s*/g, ' ').includes(searchTerm),
|
|
551
|
+
// Handle initials (e.g., "M Kumar" for "Madan Kumar")
|
|
552
|
+
searchTerm.split(/\s+/).length === 2 &&
|
|
553
|
+
fromName.split(/\s+/).length >= 2 &&
|
|
554
|
+
fromName.split(/\s+/)[0].startsWith(searchTerm.split(/\s+/)[0][0]) &&
|
|
555
|
+
fromName.includes(searchTerm.split(/\s+/)[1])
|
|
556
|
+
];
|
|
557
|
+
if (!matches.some(match => match))
|
|
401
558
|
return false;
|
|
402
559
|
}
|
|
403
560
|
if (criteria.to) {
|
|
@@ -706,5 +863,90 @@ export class MS365Operations {
|
|
|
706
863
|
throw error;
|
|
707
864
|
}
|
|
708
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
|
+
}
|
|
709
951
|
}
|
|
710
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",
|