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 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 filtering and date ranges
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`** - General search with manual recipient specification
143
- - Requires you to specify email addresses in `to` or `cc` fields
144
- - More flexible for searching any recipient combinations
145
- - Example: `{"to": "someone@company.com", "from": "boss@company.com"}`
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.3"
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: `šŸ“Š 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}`
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
- return {
885
- content: [
886
- {
887
- type: "text",
888
- text: `šŸ” Email Search Results (${searchResults.messages.length} found)\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 šŸ†” ID: ${email.id}\n`).join('\n')}`
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
- const graphClient = await multiUserMS365Auth.getGraphClientForUser(userId);
899
- ms365Ops.setGraphClient(graphClient);
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
- const graphClient = await enhancedMS365Auth.getGraphClient();
903
- ms365Ops.setGraphClient(graphClient);
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: `šŸ” 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')}`
921
+ text: responseText
911
922
  }
912
923
  ]
913
924
  };
914
- case "list_emails":
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 emailList = await ms365Ops.listEmails(args?.folderId, args?.maxResults);
938
+ const searchToMeResults = await ms365Ops.searchEmailsToMe(args);
928
939
  return {
929
940
  content: [
930
941
  {
931
942
  type: "text",
932
- 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')}`
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
- * Refresh access token
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
- logger.error('Token refresh failed:', error);
374
- throw error;
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
- this.graphClient = await ms365Auth.getGraphClient();
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
- emailSearchTerms.push(`from:${criteria.from}`);
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
- try {
307
+ return await this.executeWithAuth(async () => {
226
308
  const graphClient = await this.getGraphClient();
227
- // Build search and filter queries
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
- return {
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
- return {
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 fromMatch = message.from.address.toLowerCase().includes(criteria.from.toLowerCase()) ||
399
- message.from.name.toLowerCase().includes(criteria.from.toLowerCase());
400
- if (!fromMatch)
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",
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",