ms365-mcp-server 1.1.17 → 1.1.18

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 CHANGED
@@ -83,7 +83,7 @@ function parseArgs() {
83
83
  }
84
84
  const server = new Server({
85
85
  name: "ms365-mcp-server",
86
- version: "1.1.17"
86
+ version: "1.1.18"
87
87
  }, {
88
88
  capabilities: {
89
89
  resources: {
@@ -101,6 +101,7 @@ export class MS365Operations {
101
101
  }
102
102
  /**
103
103
  * Utility method to validate and format date for OData filters
104
+ * Microsoft Graph expects DateTimeOffset without quotes in OData filters
104
105
  */
105
106
  formatDateForOData(dateString) {
106
107
  try {
@@ -108,7 +109,8 @@ export class MS365Operations {
108
109
  if (isNaN(date.getTime())) {
109
110
  throw new Error(`Invalid date: ${dateString}`);
110
111
  }
111
- return date.toISOString();
112
+ // Remove milliseconds and format for OData DateTimeOffset (no quotes needed)
113
+ return date.toISOString().replace(/\.\d{3}Z$/, 'Z');
112
114
  }
113
115
  catch (error) {
114
116
  logger.error(`Error formatting date ${dateString}:`, error);
@@ -127,23 +129,20 @@ export class MS365Operations {
127
129
  const escapedFrom = this.escapeODataValue(criteria.from);
128
130
  filters.push(`from/emailAddress/address eq '${escapedFrom}'`);
129
131
  }
130
- // ✅ SAFE: toRecipients/ccRecipients with proper any() syntax
131
- if (criteria.to && criteria.to.includes('@')) {
132
- const escapedTo = this.escapeODataValue(criteria.to);
133
- filters.push(`toRecipients/any(r: r/emailAddress/address eq '${escapedTo}')`);
134
- }
132
+ // NOTE: toRecipients filtering is not supported by Microsoft Graph OData API
133
+ // Manual filtering will be applied after retrieving results
135
134
  if (criteria.cc && criteria.cc.includes('@')) {
136
135
  const escapedCc = this.escapeODataValue(criteria.cc);
137
- filters.push(`ccRecipients/any(r: r/emailAddress/address eq '${escapedCc}')`);
136
+ filters.push(`ccRecipients/any(c: c/emailAddress/address eq '${escapedCc}')`);
138
137
  }
139
- // ✅ SAFE: Date filters with proper ISO 8601 format
138
+ // ✅ SAFE: Date filters with proper DateTimeOffset format (no quotes)
140
139
  if (criteria.after) {
141
140
  const afterDate = this.formatDateForOData(criteria.after);
142
- filters.push(`receivedDateTime ge '${afterDate}'`);
141
+ filters.push(`receivedDateTime ge ${afterDate}`);
143
142
  }
144
143
  if (criteria.before) {
145
144
  const beforeDate = this.formatDateForOData(criteria.before);
146
- filters.push(`receivedDateTime le '${beforeDate}'`);
145
+ filters.push(`receivedDateTime le ${beforeDate}`);
147
146
  }
148
147
  // ✅ SAFE: Boolean filters
149
148
  if (criteria.hasAttachment !== undefined) {
@@ -1007,8 +1006,13 @@ ${originalBodyContent}
1007
1006
  (criteria.from && !criteria.from.includes('@')) // name searches
1008
1007
  );
1009
1008
  try {
1009
+ // STRATEGY 0: FOLDER SEARCH - Handle folder searches with optimized methods
1010
+ if (criteria.folder) {
1011
+ logger.log('🔍 Using OPTIMIZED FOLDER SEARCH strategy');
1012
+ allMessages = await this.performOptimizedFolderSearch(criteria, maxResults);
1013
+ }
1010
1014
  // STRATEGY A: Pure Filter Strategy (when no search fields present)
1011
- if (hasFilterableFields && !hasSearchableFields) {
1015
+ else if (hasFilterableFields && !hasSearchableFields) {
1012
1016
  logger.log('🔍 Using PURE FILTER strategy (structured queries only)');
1013
1017
  allMessages = await this.performPureFilterSearch(criteria, maxResults);
1014
1018
  }
@@ -1029,7 +1033,8 @@ ${originalBodyContent}
1029
1033
  allMessages = basicResult.messages;
1030
1034
  }
1031
1035
  // Apply manual filtering for unsupported fields (to/cc names, complex logic)
1032
- const filteredMessages = this.applyManualFiltering(allMessages, criteria);
1036
+ // Note: Skip manual filtering for folder searches as they handle it internally
1037
+ const filteredMessages = criteria.folder ? allMessages : this.applyManualFiltering(allMessages, criteria);
1033
1038
  // Sort by relevance and date
1034
1039
  const sortedMessages = this.sortSearchResults(filteredMessages, criteria);
1035
1040
  // Apply maxResults limit
@@ -1420,11 +1425,8 @@ ${originalBodyContent}
1420
1425
  filters.push(`contains(from/emailAddress/name,'${escapedFrom}')`);
1421
1426
  }
1422
1427
  }
1423
- if (criteria.to && criteria.to.includes('@')) {
1424
- // Properly escape single quotes in email addresses
1425
- const escapedTo = this.escapeODataValue(criteria.to);
1426
- filters.push(`toRecipients/any(r: r/emailAddress/address eq '${escapedTo}')`);
1427
- }
1428
+ // NOTE: toRecipients filtering not supported by Microsoft Graph OData API
1429
+ // Manual filtering will be applied later
1428
1430
  if (criteria.cc && criteria.cc.includes('@')) {
1429
1431
  // Properly escape single quotes in email addresses
1430
1432
  const escapedCc = this.escapeODataValue(criteria.cc);
@@ -1436,14 +1438,14 @@ ${originalBodyContent}
1436
1438
  filters.push(`contains(subject,'${escapedSubject}')`);
1437
1439
  }
1438
1440
  if (criteria.after) {
1439
- // Fix date formatting - use proper ISO format with quotes
1441
+ // Fix date formatting - use proper DateTimeOffset format without quotes
1440
1442
  const afterDate = this.formatDateForOData(criteria.after);
1441
- filters.push(`receivedDateTime ge '${afterDate}'`);
1443
+ filters.push(`receivedDateTime ge ${afterDate}`);
1442
1444
  }
1443
1445
  if (criteria.before) {
1444
- // Fix date formatting - use proper ISO format with quotes
1446
+ // Fix date formatting - use proper DateTimeOffset format without quotes
1445
1447
  const beforeDate = this.formatDateForOData(criteria.before);
1446
- filters.push(`receivedDateTime le '${beforeDate}'`);
1448
+ filters.push(`receivedDateTime le ${beforeDate}`);
1447
1449
  }
1448
1450
  if (criteria.isUnread !== undefined) {
1449
1451
  filters.push(`isRead eq ${!criteria.isUnread}`);
@@ -2080,11 +2082,11 @@ ${originalBodyContent}
2080
2082
  const filters = [];
2081
2083
  if (additionalCriteria.after) {
2082
2084
  const afterDate = this.formatDateForOData(additionalCriteria.after);
2083
- filters.push(`receivedDateTime ge '${afterDate}'`);
2085
+ filters.push(`receivedDateTime ge ${afterDate}`);
2084
2086
  }
2085
2087
  if (additionalCriteria.before) {
2086
2088
  const beforeDate = this.formatDateForOData(additionalCriteria.before);
2087
- filters.push(`receivedDateTime le '${beforeDate}'`);
2089
+ filters.push(`receivedDateTime le ${beforeDate}`);
2088
2090
  }
2089
2091
  if (additionalCriteria.isUnread !== undefined) {
2090
2092
  filters.push(`isRead eq ${!additionalCriteria.isUnread}`);
@@ -2377,7 +2379,25 @@ ${originalBodyContent}
2377
2379
  return false;
2378
2380
  }
2379
2381
  if (criteria.to) {
2380
- const toMatch = message.toRecipients.some(recipient => recipient.address.toLowerCase() === criteria.to.toLowerCase());
2382
+ const searchTerm = criteria.to.toLowerCase().trim();
2383
+ const toMatch = message.toRecipients.some(recipient => {
2384
+ const recipientName = recipient.name.toLowerCase();
2385
+ const recipientAddress = recipient.address.toLowerCase();
2386
+ // Multiple matching strategies for robust TO filtering
2387
+ return (
2388
+ // Exact email match
2389
+ recipientAddress === searchTerm ||
2390
+ // Email contains search term
2391
+ recipientAddress.includes(searchTerm) ||
2392
+ // Full name match
2393
+ recipientName === searchTerm ||
2394
+ // Name contains search term
2395
+ recipientName.includes(searchTerm) ||
2396
+ // Split name matching (for "first last" searches)
2397
+ searchTerm.split(/\s+/).every(part => recipientName.includes(part)) ||
2398
+ // Word boundary matching
2399
+ new RegExp(`\\b${searchTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`, 'i').test(recipientName));
2400
+ });
2381
2401
  if (!toMatch)
2382
2402
  return false;
2383
2403
  }
@@ -2646,14 +2666,14 @@ ${originalBodyContent}
2646
2666
  filters.push(`contains(subject,'${escapedSubject}')`);
2647
2667
  }
2648
2668
  if (criteria.after) {
2649
- // Fix date formatting - use proper ISO format with quotes
2669
+ // Fix date formatting - use proper DateTimeOffset format without quotes
2650
2670
  const afterDate = this.formatDateForOData(criteria.after);
2651
- filters.push(`receivedDateTime ge '${afterDate}'`);
2671
+ filters.push(`receivedDateTime ge ${afterDate}`);
2652
2672
  }
2653
2673
  if (criteria.before) {
2654
- // Fix date formatting - use proper ISO format with quotes
2674
+ // Fix date formatting - use proper DateTimeOffset format without quotes
2655
2675
  const beforeDate = this.formatDateForOData(criteria.before);
2656
- filters.push(`receivedDateTime le '${beforeDate}'`);
2676
+ filters.push(`receivedDateTime le ${beforeDate}`);
2657
2677
  }
2658
2678
  if (criteria.isUnread !== undefined) {
2659
2679
  filters.push(`isRead eq ${!criteria.isUnread}`);
@@ -3000,10 +3020,8 @@ ${originalBodyContent}
3000
3020
  const escapedFrom = this.escapeODataValue(criteria.from);
3001
3021
  filters.push(`contains(from/emailAddress/address,'${escapedFrom}') or contains(from/emailAddress/name,'${escapedFrom}')`);
3002
3022
  }
3003
- if (criteria.to) {
3004
- const escapedTo = this.escapeODataValue(criteria.to);
3005
- filters.push(`toRecipients/any(r:contains(r/emailAddress/address,'${escapedTo}'))`);
3006
- }
3023
+ // NOTE: toRecipients filtering not supported by Microsoft Graph OData API
3024
+ // Manual filtering will be applied after retrieval
3007
3025
  if (criteria.cc) {
3008
3026
  const escapedCc = this.escapeODataValue(criteria.cc);
3009
3027
  filters.push(`ccRecipients/any(r:contains(r/emailAddress/address,'${escapedCc}'))`);
@@ -3014,11 +3032,11 @@ ${originalBodyContent}
3014
3032
  }
3015
3033
  if (criteria.after) {
3016
3034
  const afterDate = this.formatDateForOData(criteria.after);
3017
- filters.push(`receivedDateTime ge '${afterDate}'`);
3035
+ filters.push(`receivedDateTime ge ${afterDate}`);
3018
3036
  }
3019
3037
  if (criteria.before) {
3020
3038
  const beforeDate = this.formatDateForOData(criteria.before);
3021
- filters.push(`receivedDateTime le '${beforeDate}'`);
3039
+ filters.push(`receivedDateTime le ${beforeDate}`);
3022
3040
  }
3023
3041
  if (criteria.isUnread !== undefined) {
3024
3042
  filters.push(`isRead eq ${!criteria.isUnread}`);
@@ -3418,9 +3436,9 @@ ${originalBodyContent}
3418
3436
  .api(`/me/mailFolders/${folder.id}/messages`)
3419
3437
  .select('id,subject,from,toRecipients,ccRecipients,receivedDateTime,sentDateTime,bodyPreview,isRead,hasAttachments,importance,conversationId,parentFolderId,webLink')
3420
3438
  .orderby('receivedDateTime desc')
3421
- .top(Math.min(maxResults * 2, 500)); // Get more for filtering
3439
+ .top(Math.min(maxResults * 10, 1000)); // Increased limit for TO searches to find more recipients
3422
3440
  // Build filters for better performance
3423
- const filters = [`receivedDateTime ge '${recentDateStr}T00:00:00Z'`];
3441
+ const filters = [`receivedDateTime ge ${recentDateStr}T00:00:00Z`];
3424
3442
  if (criteria.from) {
3425
3443
  const escapedFrom = this.escapeODataValue(criteria.from);
3426
3444
  if (criteria.from.includes('@')) {
@@ -3434,6 +3452,8 @@ ${originalBodyContent}
3434
3452
  const escapedSubject = this.escapeODataValue(criteria.subject);
3435
3453
  filters.push(`contains(subject,'${escapedSubject}')`);
3436
3454
  }
3455
+ // NOTE: toRecipients filtering is not supported by Microsoft Graph OData API
3456
+ // Manual filtering will be applied after retrieving results
3437
3457
  if (criteria.isUnread !== undefined) {
3438
3458
  filters.push(`isRead eq ${!criteria.isUnread}`);
3439
3459
  }
@@ -3453,6 +3473,14 @@ ${originalBodyContent}
3453
3473
  return messageText.includes(searchText);
3454
3474
  });
3455
3475
  }
3476
+ // Apply manual TO filtering for names that might not be caught by OData filter
3477
+ if (criteria.to && !criteria.to.includes('@')) {
3478
+ messages = messages.filter(message => {
3479
+ const toSearchTerm = criteria.to.toLowerCase();
3480
+ return message.toRecipients.some(recipient => recipient.name.toLowerCase().includes(toSearchTerm) ||
3481
+ recipient.address.toLowerCase().includes(toSearchTerm));
3482
+ });
3483
+ }
3456
3484
  logger.log(`📧 Large folder search Phase 1 result: ${messages.length} emails found`);
3457
3485
  // If we have enough results or found good matches, return
3458
3486
  if (messages.length >= maxResults || messages.length > 5) {
@@ -3470,8 +3498,8 @@ ${originalBodyContent}
3470
3498
  .top(300);
3471
3499
  // More restrictive filters for older search
3472
3500
  const olderFilters = [
3473
- `receivedDateTime ge '${olderDateStr}T00:00:00Z'`,
3474
- `receivedDateTime lt '${recentDateStr}T00:00:00Z'`
3501
+ `receivedDateTime ge ${olderDateStr}T00:00:00Z`,
3502
+ `receivedDateTime lt ${recentDateStr}T00:00:00Z`
3475
3503
  ];
3476
3504
  // Must have at least one specific criteria for older search
3477
3505
  if (criteria.from) {
@@ -3483,13 +3511,17 @@ ${originalBodyContent}
3483
3511
  olderFilters.push(`contains(from/emailAddress/name,'${escapedFrom}')`);
3484
3512
  }
3485
3513
  }
3514
+ else if (criteria.to) {
3515
+ // NOTE: toRecipients filtering is not supported by Microsoft Graph OData API
3516
+ // Skip OData filtering, manual filtering will be applied later
3517
+ }
3486
3518
  else if (criteria.subject) {
3487
3519
  const escapedSubject = this.escapeODataValue(criteria.subject);
3488
3520
  olderFilters.push(`contains(subject,'${escapedSubject}')`);
3489
3521
  }
3490
3522
  else {
3491
- // If no specific sender/subject, skip older search to avoid timeout
3492
- logger.log(`⚠️ No specific sender/subject for older search. Returning recent results only.`);
3523
+ // If no specific sender/subject/to, skip older search to avoid timeout
3524
+ logger.log(`⚠️ No specific sender/subject/to for older search. Returning recent results only.`);
3493
3525
  return messages.slice(0, maxResults);
3494
3526
  }
3495
3527
  if (criteria.importance) {
@@ -3509,6 +3541,14 @@ ${originalBodyContent}
3509
3541
  return messageText.includes(searchText);
3510
3542
  });
3511
3543
  }
3544
+ // Apply manual TO filtering for names to older results
3545
+ if (criteria.to && !criteria.to.includes('@')) {
3546
+ filteredOlderMessages = filteredOlderMessages.filter(message => {
3547
+ const toSearchTerm = criteria.to.toLowerCase();
3548
+ return message.toRecipients.some(recipient => recipient.name.toLowerCase().includes(toSearchTerm) ||
3549
+ recipient.address.toLowerCase().includes(toSearchTerm));
3550
+ });
3551
+ }
3512
3552
  logger.log(`📧 Large folder search Phase 2 result: ${filteredOlderMessages.length} additional emails found`);
3513
3553
  // Combine results and avoid duplicates
3514
3554
  const allMessages = [...messages];
@@ -3528,7 +3568,7 @@ ${originalBodyContent}
3528
3568
  .api(`/me/mailFolders/${folder.id}/messages`)
3529
3569
  .select('id,subject,from,toRecipients,ccRecipients,receivedDateTime,sentDateTime,bodyPreview,isRead,hasAttachments,importance,conversationId,parentFolderId,webLink')
3530
3570
  .orderby('receivedDateTime desc')
3531
- .top(Math.min(maxResults * 2, 999));
3571
+ .top(Math.min(maxResults * 10, 999)); // Increased limit for TO searches to find more recipients
3532
3572
  // Build filters
3533
3573
  const filters = [];
3534
3574
  if (criteria.from) {
@@ -3540,13 +3580,11 @@ ${originalBodyContent}
3540
3580
  filters.push(`contains(from/emailAddress/name,'${escapedFrom}')`);
3541
3581
  }
3542
3582
  }
3543
- if (criteria.to && criteria.to.includes('@')) {
3544
- const escapedTo = this.escapeODataValue(criteria.to);
3545
- filters.push(`toRecipients/any(r: r/emailAddress/address eq '${escapedTo}')`);
3546
- }
3583
+ // NOTE: toRecipients filtering not supported by Microsoft Graph OData API
3584
+ // Manual filtering will be applied after retrieval
3547
3585
  if (criteria.cc && criteria.cc.includes('@')) {
3548
3586
  const escapedCc = this.escapeODataValue(criteria.cc);
3549
- filters.push(`ccRecipients/any(r: r/emailAddress/address eq '${escapedCc}')`);
3587
+ filters.push(`ccRecipients/any(c: c/emailAddress/address eq '${escapedCc}')`);
3550
3588
  }
3551
3589
  if (criteria.subject) {
3552
3590
  const escapedSubject = this.escapeODataValue(criteria.subject);
@@ -3554,11 +3592,11 @@ ${originalBodyContent}
3554
3592
  }
3555
3593
  if (criteria.after) {
3556
3594
  const afterDate = this.formatDateForOData(criteria.after);
3557
- filters.push(`receivedDateTime ge '${afterDate}'`);
3595
+ filters.push(`receivedDateTime ge ${afterDate}`);
3558
3596
  }
3559
3597
  if (criteria.before) {
3560
3598
  const beforeDate = this.formatDateForOData(criteria.before);
3561
- filters.push(`receivedDateTime le '${beforeDate}'`);
3599
+ filters.push(`receivedDateTime le ${beforeDate}`);
3562
3600
  }
3563
3601
  if (criteria.isUnread !== undefined) {
3564
3602
  filters.push(`isRead eq ${!criteria.isUnread}`);
@@ -3584,6 +3622,14 @@ ${originalBodyContent}
3584
3622
  return messageText.includes(searchText);
3585
3623
  });
3586
3624
  }
3625
+ // Apply manual TO filtering for names that might not be caught by OData filter
3626
+ if (criteria.to && !criteria.to.includes('@')) {
3627
+ messages = messages.filter(message => {
3628
+ const toSearchTerm = criteria.to.toLowerCase();
3629
+ return message.toRecipients.some(recipient => recipient.name.toLowerCase().includes(toSearchTerm) ||
3630
+ recipient.address.toLowerCase().includes(toSearchTerm));
3631
+ });
3632
+ }
3587
3633
  logger.log(`📧 Standard folder search result: ${messages.length} emails found`);
3588
3634
  return messages.slice(0, maxResults);
3589
3635
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ms365-mcp-server",
3
- "version": "1.1.17",
3
+ "version": "1.1.18",
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",