ms365-mcp-server 1.1.17 → 1.1.19

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
@@ -12,6 +12,12 @@ A powerful **Model Context Protocol (MCP) server** that enables seamless Microso
12
12
  - **📁 Smart Contact Management** - Search and retrieve your Microsoft 365 contacts
13
13
  - **🌐 Cross-Platform** - Works on macOS, Linux, and Windows
14
14
 
15
+ ## 📚 Documentation
16
+
17
+ For detailed technical documentation, enhancement reports, and guides, see the **[docs/](./docs/)** directory:
18
+ - **[Enhancement Reports](./docs/MS365-MCP-Server-Enhancement-Report.md)** - Recent fixes and improvements
19
+ - **[Technical Guides](./docs/)** - Batch operations and Graph API implementation guides
20
+
15
21
  ## 🛠️ Available Tools (6 Total)
16
22
 
17
23
  ### **📧 Email Management**
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.19"
87
87
  }, {
88
88
  capabilities: {
89
89
  resources: {
@@ -7,7 +7,7 @@ export class MS365Operations {
7
7
  constructor() {
8
8
  this.graphClient = null;
9
9
  this.searchCache = new Map();
10
- this.CACHE_DURATION = 60 * 1000; // 1 minute cache
10
+ this.CACHE_DURATION = 300 * 1000; // 5 minute cache for better performance
11
11
  this.MAX_RETRIES = 3;
12
12
  this.BASE_DELAY = 1000; // 1 second
13
13
  }
@@ -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}`);
@@ -1622,7 +1624,18 @@ ${originalBodyContent}
1622
1624
  // Other filters remain the same but are more robust
1623
1625
  if (criteria.to && !message.toRecipients.some(r => r.address.toLowerCase().includes(criteria.to.toLowerCase())))
1624
1626
  return false;
1625
- if (criteria.cc && (!message.ccRecipients || !message.ccRecipients.some(r => r.address.toLowerCase().includes(criteria.cc.toLowerCase()))))
1627
+ if (criteria.cc && (!message.ccRecipients || !message.ccRecipients.some(r => {
1628
+ const searchTerm = criteria.cc.toLowerCase().trim();
1629
+ const recipientName = r.name.toLowerCase();
1630
+ const recipientAddress = r.address.toLowerCase();
1631
+ // Multiple matching strategies for robust CC filtering
1632
+ return (recipientAddress === searchTerm ||
1633
+ recipientAddress.includes(searchTerm) ||
1634
+ recipientName === searchTerm ||
1635
+ recipientName.includes(searchTerm) ||
1636
+ searchTerm.split(/\s+/).every(part => recipientName.includes(part)) ||
1637
+ new RegExp(`\\b${searchTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`, 'i').test(recipientName));
1638
+ })))
1626
1639
  return false;
1627
1640
  if (criteria.subject && !message.subject.toLowerCase().includes(criteria.subject.toLowerCase()))
1628
1641
  return false;
@@ -2080,11 +2093,11 @@ ${originalBodyContent}
2080
2093
  const filters = [];
2081
2094
  if (additionalCriteria.after) {
2082
2095
  const afterDate = this.formatDateForOData(additionalCriteria.after);
2083
- filters.push(`receivedDateTime ge '${afterDate}'`);
2096
+ filters.push(`receivedDateTime ge ${afterDate}`);
2084
2097
  }
2085
2098
  if (additionalCriteria.before) {
2086
2099
  const beforeDate = this.formatDateForOData(additionalCriteria.before);
2087
- filters.push(`receivedDateTime le '${beforeDate}'`);
2100
+ filters.push(`receivedDateTime le ${beforeDate}`);
2088
2101
  }
2089
2102
  if (additionalCriteria.isUnread !== undefined) {
2090
2103
  filters.push(`isRead eq ${!additionalCriteria.isUnread}`);
@@ -2377,13 +2390,49 @@ ${originalBodyContent}
2377
2390
  return false;
2378
2391
  }
2379
2392
  if (criteria.to) {
2380
- const toMatch = message.toRecipients.some(recipient => recipient.address.toLowerCase() === criteria.to.toLowerCase());
2393
+ const searchTerm = criteria.to.toLowerCase().trim();
2394
+ const toMatch = message.toRecipients.some(recipient => {
2395
+ const recipientName = recipient.name.toLowerCase();
2396
+ const recipientAddress = recipient.address.toLowerCase();
2397
+ // Multiple matching strategies for robust TO filtering
2398
+ return (
2399
+ // Exact email match
2400
+ recipientAddress === searchTerm ||
2401
+ // Email contains search term
2402
+ recipientAddress.includes(searchTerm) ||
2403
+ // Full name match
2404
+ recipientName === searchTerm ||
2405
+ // Name contains search term
2406
+ recipientName.includes(searchTerm) ||
2407
+ // Split name matching (for "first last" searches)
2408
+ searchTerm.split(/\s+/).every(part => recipientName.includes(part)) ||
2409
+ // Word boundary matching
2410
+ new RegExp(`\\b${searchTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`, 'i').test(recipientName));
2411
+ });
2381
2412
  if (!toMatch)
2382
2413
  return false;
2383
2414
  }
2384
2415
  if (criteria.cc) {
2416
+ const searchTerm = criteria.cc.toLowerCase().trim();
2385
2417
  const ccMatch = message.ccRecipients && message.ccRecipients.length > 0 &&
2386
- message.ccRecipients.some(recipient => recipient.address.toLowerCase() === criteria.cc.toLowerCase());
2418
+ message.ccRecipients.some(recipient => {
2419
+ const recipientName = recipient.name.toLowerCase();
2420
+ const recipientAddress = recipient.address.toLowerCase();
2421
+ // Multiple matching strategies for robust CC filtering (same as TO)
2422
+ return (
2423
+ // Exact email match
2424
+ recipientAddress === searchTerm ||
2425
+ // Email contains search term
2426
+ recipientAddress.includes(searchTerm) ||
2427
+ // Full name match
2428
+ recipientName === searchTerm ||
2429
+ // Name contains search term
2430
+ recipientName.includes(searchTerm) ||
2431
+ // Split name matching (for "first last" searches)
2432
+ searchTerm.split(/\s+/).every(part => recipientName.includes(part)) ||
2433
+ // Word boundary matching
2434
+ new RegExp(`\\b${searchTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`, 'i').test(recipientName));
2435
+ });
2387
2436
  if (!ccMatch)
2388
2437
  return false;
2389
2438
  }
@@ -2646,14 +2695,14 @@ ${originalBodyContent}
2646
2695
  filters.push(`contains(subject,'${escapedSubject}')`);
2647
2696
  }
2648
2697
  if (criteria.after) {
2649
- // Fix date formatting - use proper ISO format with quotes
2698
+ // Fix date formatting - use proper DateTimeOffset format without quotes
2650
2699
  const afterDate = this.formatDateForOData(criteria.after);
2651
- filters.push(`receivedDateTime ge '${afterDate}'`);
2700
+ filters.push(`receivedDateTime ge ${afterDate}`);
2652
2701
  }
2653
2702
  if (criteria.before) {
2654
- // Fix date formatting - use proper ISO format with quotes
2703
+ // Fix date formatting - use proper DateTimeOffset format without quotes
2655
2704
  const beforeDate = this.formatDateForOData(criteria.before);
2656
- filters.push(`receivedDateTime le '${beforeDate}'`);
2705
+ filters.push(`receivedDateTime le ${beforeDate}`);
2657
2706
  }
2658
2707
  if (criteria.isUnread !== undefined) {
2659
2708
  filters.push(`isRead eq ${!criteria.isUnread}`);
@@ -3000,10 +3049,8 @@ ${originalBodyContent}
3000
3049
  const escapedFrom = this.escapeODataValue(criteria.from);
3001
3050
  filters.push(`contains(from/emailAddress/address,'${escapedFrom}') or contains(from/emailAddress/name,'${escapedFrom}')`);
3002
3051
  }
3003
- if (criteria.to) {
3004
- const escapedTo = this.escapeODataValue(criteria.to);
3005
- filters.push(`toRecipients/any(r:contains(r/emailAddress/address,'${escapedTo}'))`);
3006
- }
3052
+ // NOTE: toRecipients filtering not supported by Microsoft Graph OData API
3053
+ // Manual filtering will be applied after retrieval
3007
3054
  if (criteria.cc) {
3008
3055
  const escapedCc = this.escapeODataValue(criteria.cc);
3009
3056
  filters.push(`ccRecipients/any(r:contains(r/emailAddress/address,'${escapedCc}'))`);
@@ -3014,11 +3061,11 @@ ${originalBodyContent}
3014
3061
  }
3015
3062
  if (criteria.after) {
3016
3063
  const afterDate = this.formatDateForOData(criteria.after);
3017
- filters.push(`receivedDateTime ge '${afterDate}'`);
3064
+ filters.push(`receivedDateTime ge ${afterDate}`);
3018
3065
  }
3019
3066
  if (criteria.before) {
3020
3067
  const beforeDate = this.formatDateForOData(criteria.before);
3021
- filters.push(`receivedDateTime le '${beforeDate}'`);
3068
+ filters.push(`receivedDateTime le ${beforeDate}`);
3022
3069
  }
3023
3070
  if (criteria.isUnread !== undefined) {
3024
3071
  filters.push(`isRead eq ${!criteria.isUnread}`);
@@ -3418,9 +3465,9 @@ ${originalBodyContent}
3418
3465
  .api(`/me/mailFolders/${folder.id}/messages`)
3419
3466
  .select('id,subject,from,toRecipients,ccRecipients,receivedDateTime,sentDateTime,bodyPreview,isRead,hasAttachments,importance,conversationId,parentFolderId,webLink')
3420
3467
  .orderby('receivedDateTime desc')
3421
- .top(Math.min(maxResults * 2, 500)); // Get more for filtering
3468
+ .top(Math.min(maxResults * 10, 1000)); // Increased limit for TO searches to find more recipients
3422
3469
  // Build filters for better performance
3423
- const filters = [`receivedDateTime ge '${recentDateStr}T00:00:00Z'`];
3470
+ const filters = [`receivedDateTime ge ${recentDateStr}T00:00:00Z`];
3424
3471
  if (criteria.from) {
3425
3472
  const escapedFrom = this.escapeODataValue(criteria.from);
3426
3473
  if (criteria.from.includes('@')) {
@@ -3434,6 +3481,8 @@ ${originalBodyContent}
3434
3481
  const escapedSubject = this.escapeODataValue(criteria.subject);
3435
3482
  filters.push(`contains(subject,'${escapedSubject}')`);
3436
3483
  }
3484
+ // NOTE: toRecipients filtering is not supported by Microsoft Graph OData API
3485
+ // Manual filtering will be applied after retrieving results
3437
3486
  if (criteria.isUnread !== undefined) {
3438
3487
  filters.push(`isRead eq ${!criteria.isUnread}`);
3439
3488
  }
@@ -3453,6 +3502,14 @@ ${originalBodyContent}
3453
3502
  return messageText.includes(searchText);
3454
3503
  });
3455
3504
  }
3505
+ // Apply manual TO filtering for names that might not be caught by OData filter
3506
+ if (criteria.to && !criteria.to.includes('@')) {
3507
+ messages = messages.filter(message => {
3508
+ const toSearchTerm = criteria.to.toLowerCase();
3509
+ return message.toRecipients.some(recipient => recipient.name.toLowerCase().includes(toSearchTerm) ||
3510
+ recipient.address.toLowerCase().includes(toSearchTerm));
3511
+ });
3512
+ }
3456
3513
  logger.log(`📧 Large folder search Phase 1 result: ${messages.length} emails found`);
3457
3514
  // If we have enough results or found good matches, return
3458
3515
  if (messages.length >= maxResults || messages.length > 5) {
@@ -3470,8 +3527,8 @@ ${originalBodyContent}
3470
3527
  .top(300);
3471
3528
  // More restrictive filters for older search
3472
3529
  const olderFilters = [
3473
- `receivedDateTime ge '${olderDateStr}T00:00:00Z'`,
3474
- `receivedDateTime lt '${recentDateStr}T00:00:00Z'`
3530
+ `receivedDateTime ge ${olderDateStr}T00:00:00Z`,
3531
+ `receivedDateTime lt ${recentDateStr}T00:00:00Z`
3475
3532
  ];
3476
3533
  // Must have at least one specific criteria for older search
3477
3534
  if (criteria.from) {
@@ -3483,13 +3540,17 @@ ${originalBodyContent}
3483
3540
  olderFilters.push(`contains(from/emailAddress/name,'${escapedFrom}')`);
3484
3541
  }
3485
3542
  }
3543
+ else if (criteria.to) {
3544
+ // NOTE: toRecipients filtering is not supported by Microsoft Graph OData API
3545
+ // Skip OData filtering, manual filtering will be applied later
3546
+ }
3486
3547
  else if (criteria.subject) {
3487
3548
  const escapedSubject = this.escapeODataValue(criteria.subject);
3488
3549
  olderFilters.push(`contains(subject,'${escapedSubject}')`);
3489
3550
  }
3490
3551
  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.`);
3552
+ // If no specific sender/subject/to, skip older search to avoid timeout
3553
+ logger.log(`⚠️ No specific sender/subject/to for older search. Returning recent results only.`);
3493
3554
  return messages.slice(0, maxResults);
3494
3555
  }
3495
3556
  if (criteria.importance) {
@@ -3509,6 +3570,14 @@ ${originalBodyContent}
3509
3570
  return messageText.includes(searchText);
3510
3571
  });
3511
3572
  }
3573
+ // Apply manual TO filtering for names to older results
3574
+ if (criteria.to && !criteria.to.includes('@')) {
3575
+ filteredOlderMessages = filteredOlderMessages.filter(message => {
3576
+ const toSearchTerm = criteria.to.toLowerCase();
3577
+ return message.toRecipients.some(recipient => recipient.name.toLowerCase().includes(toSearchTerm) ||
3578
+ recipient.address.toLowerCase().includes(toSearchTerm));
3579
+ });
3580
+ }
3512
3581
  logger.log(`📧 Large folder search Phase 2 result: ${filteredOlderMessages.length} additional emails found`);
3513
3582
  // Combine results and avoid duplicates
3514
3583
  const allMessages = [...messages];
@@ -3528,7 +3597,7 @@ ${originalBodyContent}
3528
3597
  .api(`/me/mailFolders/${folder.id}/messages`)
3529
3598
  .select('id,subject,from,toRecipients,ccRecipients,receivedDateTime,sentDateTime,bodyPreview,isRead,hasAttachments,importance,conversationId,parentFolderId,webLink')
3530
3599
  .orderby('receivedDateTime desc')
3531
- .top(Math.min(maxResults * 2, 999));
3600
+ .top(Math.min(maxResults * 10, 999)); // Increased limit for TO searches to find more recipients
3532
3601
  // Build filters
3533
3602
  const filters = [];
3534
3603
  if (criteria.from) {
@@ -3540,13 +3609,11 @@ ${originalBodyContent}
3540
3609
  filters.push(`contains(from/emailAddress/name,'${escapedFrom}')`);
3541
3610
  }
3542
3611
  }
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
- }
3612
+ // NOTE: toRecipients filtering not supported by Microsoft Graph OData API
3613
+ // Manual filtering will be applied after retrieval
3547
3614
  if (criteria.cc && criteria.cc.includes('@')) {
3548
3615
  const escapedCc = this.escapeODataValue(criteria.cc);
3549
- filters.push(`ccRecipients/any(r: r/emailAddress/address eq '${escapedCc}')`);
3616
+ filters.push(`ccRecipients/any(c: c/emailAddress/address eq '${escapedCc}')`);
3550
3617
  }
3551
3618
  if (criteria.subject) {
3552
3619
  const escapedSubject = this.escapeODataValue(criteria.subject);
@@ -3554,11 +3621,11 @@ ${originalBodyContent}
3554
3621
  }
3555
3622
  if (criteria.after) {
3556
3623
  const afterDate = this.formatDateForOData(criteria.after);
3557
- filters.push(`receivedDateTime ge '${afterDate}'`);
3624
+ filters.push(`receivedDateTime ge ${afterDate}`);
3558
3625
  }
3559
3626
  if (criteria.before) {
3560
3627
  const beforeDate = this.formatDateForOData(criteria.before);
3561
- filters.push(`receivedDateTime le '${beforeDate}'`);
3628
+ filters.push(`receivedDateTime le ${beforeDate}`);
3562
3629
  }
3563
3630
  if (criteria.isUnread !== undefined) {
3564
3631
  filters.push(`isRead eq ${!criteria.isUnread}`);
@@ -3584,6 +3651,14 @@ ${originalBodyContent}
3584
3651
  return messageText.includes(searchText);
3585
3652
  });
3586
3653
  }
3654
+ // Apply manual TO filtering for names that might not be caught by OData filter
3655
+ if (criteria.to && !criteria.to.includes('@')) {
3656
+ messages = messages.filter(message => {
3657
+ const toSearchTerm = criteria.to.toLowerCase();
3658
+ return message.toRecipients.some(recipient => recipient.name.toLowerCase().includes(toSearchTerm) ||
3659
+ recipient.address.toLowerCase().includes(toSearchTerm));
3660
+ });
3661
+ }
3587
3662
  logger.log(`📧 Standard folder search result: ${messages.length} emails found`);
3588
3663
  return messages.slice(0, maxResults);
3589
3664
  }
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.19",
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",