ms365-mcp-server 1.1.22 → 1.1.24

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
@@ -145,7 +145,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
145
145
  const baseTools = [
146
146
  {
147
147
  name: "send_email",
148
- description: "Send an email message with support for HTML content, attachments, and international characters. Supports both plain text and HTML emails with proper formatting.",
148
+ description: "Send an email message with support for HTML content, attachments (via file paths or Base64), and international characters. Supports both plain text and HTML emails with proper formatting. NEW: File path attachments with automatic MIME detection and Base64 conversion.",
149
149
  inputSchema: {
150
150
  type: "object",
151
151
  properties: {
@@ -209,16 +209,20 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
209
209
  },
210
210
  contentBytes: {
211
211
  type: "string",
212
- description: "Base64 encoded content of the attachment"
212
+ description: "Base64 encoded content of the attachment (required if filePath not provided)"
213
+ },
214
+ filePath: {
215
+ type: "string",
216
+ description: "ABSOLUTE PATH (full path) to the file to attach (alternative to contentBytes). MUST start with '/' - relative paths like './file.pdf' will NOT work. Supports Unicode filenames, spaces, and special characters. Example: '/Users/name/Documents/file with spaces.pdf'"
213
217
  },
214
218
  contentType: {
215
219
  type: "string",
216
- description: "MIME type of the attachment (optional, auto-detected if not provided)"
220
+ description: "MIME type of the attachment (optional, auto-detected from file extension if not provided). Supports PDF, Office docs, images, text files, etc."
217
221
  }
218
222
  },
219
- required: ["name", "contentBytes"]
223
+ required: ["name"]
220
224
  },
221
- description: "List of email attachments (optional)"
225
+ description: "List of email attachments (optional). Each attachment can use either 'filePath' (ABSOLUTE PATH - must start with '/') or 'contentBytes' (Base64 encoded data). File paths support automatic MIME detection and Unicode filenames. IMPORTANT: Relative paths like './file.pdf' will fail."
222
226
  }
223
227
  },
224
228
  required: ["to", "subject"]
@@ -379,16 +383,20 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
379
383
  },
380
384
  contentBytes: {
381
385
  type: "string",
382
- description: "Base64 encoded content of the attachment"
386
+ description: "Base64 encoded content of the attachment (required if filePath not provided)"
387
+ },
388
+ filePath: {
389
+ type: "string",
390
+ description: "ABSOLUTE PATH (full path) to the file to attach (alternative to contentBytes). MUST start with '/' - relative paths like './file.pdf' will NOT work. Supports Unicode filenames, spaces, and special characters. Example: '/Users/name/Documents/file with spaces.pdf'"
383
391
  },
384
392
  contentType: {
385
393
  type: "string",
386
- description: "MIME type of the attachment (optional, auto-detected if not provided)"
394
+ description: "MIME type of the attachment (optional, auto-detected from file extension if not provided). Supports PDF, Office docs, images, text files, etc."
387
395
  }
388
396
  },
389
- required: ["name", "contentBytes"]
397
+ required: ["name"]
390
398
  },
391
- description: "List of email attachments for draft (optional)"
399
+ description: "List of email attachments for draft (optional). Each attachment can use either 'filePath' (ABSOLUTE PATH - must start with '/') or 'contentBytes' (Base64 encoded data). File paths support automatic MIME detection and Unicode filenames. IMPORTANT: Relative paths like './file.pdf' will fail."
392
400
  },
393
401
  // Additional draft parameters
394
402
  draftId: {
@@ -899,6 +907,22 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
899
907
  */
900
908
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
901
909
  const { name, arguments: args } = request.params;
910
+ // Debug logging for MCP protocol issues
911
+ logger.log(`šŸ”§ MCP Tool Called: ${name}`);
912
+ logger.log(`šŸ”§ Args type: ${typeof args}`);
913
+ logger.log(`šŸ”§ Args content:`, JSON.stringify(args, null, 2));
914
+ // Handle string-encoded JSON arguments (common MCP client issue)
915
+ let processedArgs = args;
916
+ if (typeof args === 'string') {
917
+ try {
918
+ processedArgs = JSON.parse(args);
919
+ logger.log(`šŸ”§ Parsed string args to object:`, JSON.stringify(processedArgs, null, 2));
920
+ }
921
+ catch (parseError) {
922
+ logger.error(`āŒ Failed to parse string arguments: ${parseError}`);
923
+ throw new Error(`Invalid JSON in arguments: ${parseError}`);
924
+ }
925
+ }
902
926
  // Helper function to validate and normalize bodyType
903
927
  const normalizeBodyType = (bodyType) => {
904
928
  if (!bodyType)
@@ -916,7 +940,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
916
940
  switch (name) {
917
941
  // ============ UNIFIED AUTHENTICATION TOOL ============
918
942
  case "authenticate":
919
- const action = args?.action || 'login';
943
+ const action = processedArgs?.action || 'login';
920
944
  switch (action) {
921
945
  case "login":
922
946
  try {
@@ -1043,9 +1067,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1043
1067
  // ============ UNIFIED EMAIL MANAGEMENT TOOL ============
1044
1068
  case "manage_email":
1045
1069
  // Debug logging for parameter validation
1046
- logger.log('DEBUG: manage_email called with args:', JSON.stringify(args, null, 2));
1070
+ logger.log('DEBUG: manage_email called with processedArgs:', JSON.stringify(processedArgs, null, 2));
1047
1071
  if (ms365Config.multiUser) {
1048
- const userId = args?.userId;
1072
+ const userId = processedArgs?.userId;
1049
1073
  if (!userId) {
1050
1074
  throw new Error("User ID is required in multi-user mode");
1051
1075
  }
@@ -1056,14 +1080,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1056
1080
  const graphClient = await enhancedMS365Auth.getGraphClient();
1057
1081
  ms365Ops.setGraphClient(graphClient);
1058
1082
  }
1059
- const emailAction = args?.action;
1083
+ const emailAction = processedArgs?.action;
1060
1084
  logger.log(`DEBUG: Email action: ${emailAction}`);
1061
1085
  switch (emailAction) {
1062
1086
  case "read":
1063
- if (!args?.messageId) {
1087
+ if (!processedArgs?.messageId) {
1064
1088
  throw new Error("messageId is required for read action");
1065
1089
  }
1066
- const email = await ms365Ops.getEmail(args.messageId, args?.includeAttachments);
1090
+ const email = await ms365Ops.getEmail(processedArgs.messageId, processedArgs?.includeAttachments);
1067
1091
  let emailDetailsText = `šŸ“§ Email Details\n\nšŸ“‹ Subject: ${email.subject}\nšŸ‘¤ From: ${email.from.name} <${email.from.address}>\nšŸ“… Date: ${email.receivedDateTime}\n`;
1068
1092
  if (email.attachments && email.attachments.length > 0) {
1069
1093
  emailDetailsText += `\nšŸ“Ž Attachments (${email.attachments.length}):\n`;
@@ -1095,12 +1119,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1095
1119
  };
1096
1120
  case "search":
1097
1121
  // Enhanced basic search with dynamic timeout based on search complexity
1098
- const hasComplexFilters = !!(args?.query || args?.from || args?.subject || args?.after || args?.before);
1122
+ const hasComplexFilters = !!(processedArgs?.query || processedArgs?.from || processedArgs?.subject || processedArgs?.after || processedArgs?.before);
1099
1123
  const baseTimeout = hasComplexFilters ? 60000 : 45000; // 60s for complex, 45s for simple
1100
- const maxResults = args?.maxResults || 50;
1124
+ const maxResults = processedArgs?.maxResults || 50;
1101
1125
  const adjustedTimeout = maxResults > 100 ? baseTimeout * 1.5 : baseTimeout; // Increase timeout for large result sets
1102
1126
  logger.log(`šŸ” Search timeout set to ${adjustedTimeout / 1000}s (complex: ${hasComplexFilters}, maxResults: ${maxResults})`);
1103
- const searchPromise = ms365Ops.searchEmails(args);
1127
+ const searchPromise = ms365Ops.searchEmails(processedArgs);
1104
1128
  const timeoutPromise = new Promise((_, reject) => {
1105
1129
  setTimeout(() => {
1106
1130
  reject(new Error(`Search timed out after ${adjustedTimeout / 1000} seconds. For large mailboxes or complex searches, try: (1) ai_email_assistant with useLargeMailboxStrategy: true, (2) More specific search terms, (3) Smaller maxResults (current: ${maxResults}), or (4) Narrower date ranges.`));
@@ -1113,8 +1137,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1113
1137
  let responseText = `šŸ” Email Search Results (${searchResults.messages.length} found)`;
1114
1138
  if (searchResults.messages.length === 0) {
1115
1139
  responseText = `šŸ” No emails found matching your criteria.\n\nšŸ’” Search Tips:\n`;
1116
- if (args?.from) {
1117
- responseText += `• Try partial names: "${args.from.split(' ')[0]}" or "${args.from.split(' ').pop()}"\n`;
1140
+ if (processedArgs?.from) {
1141
+ responseText += `• Try partial names: "${processedArgs.from.split(' ')[0]}" or "${processedArgs.from.split(' ').pop()}"\n`;
1118
1142
  responseText += `• Check spelling of sender name\n`;
1119
1143
  }
1120
1144
  if (args?.subject) {
@@ -1324,7 +1348,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1324
1348
  ]
1325
1349
  };
1326
1350
  case "draft":
1327
- if (!args?.draftTo || !args?.draftSubject || !args?.draftBody) {
1351
+ if (!processedArgs?.draftTo || !processedArgs?.draftSubject || !processedArgs?.draftBody) {
1328
1352
  throw new Error("draftTo, draftSubject, and draftBody are required for draft action");
1329
1353
  }
1330
1354
  // Helper function to normalize email arrays for drafts
@@ -1337,23 +1361,35 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1337
1361
  return field;
1338
1362
  throw new Error(`Invalid email field format. Expected string or array of strings.`);
1339
1363
  };
1364
+ // Process draft attachments (handle both filePath and contentBytes)
1365
+ let processedDraftAttachments = [];
1366
+ if (processedArgs.draftAttachments && Array.isArray(processedArgs.draftAttachments)) {
1367
+ processedDraftAttachments = await Promise.all(processedArgs.draftAttachments.map(async (attachment) => {
1368
+ try {
1369
+ return await processAttachment(attachment);
1370
+ }
1371
+ catch (error) {
1372
+ throw new Error(`Error processing draft attachment '${attachment.name}': ${error instanceof Error ? error.message : 'Unknown error'}`);
1373
+ }
1374
+ }));
1375
+ }
1340
1376
  // Check if this looks like a reply based on subject
1341
- const isLikelyReply = args.draftSubject?.toString().toLowerCase().startsWith('re:');
1377
+ const isLikelyReply = processedArgs.draftSubject?.toString().toLowerCase().startsWith('re:');
1342
1378
  if (isLikelyReply) {
1343
1379
  logger.log('šŸ”” SUGGESTION: Consider using "reply_draft" action instead of "draft" for threaded replies that appear in conversations');
1344
1380
  }
1345
1381
  const draftResult = await ms365Ops.saveDraftEmail({
1346
- to: normalizeDraftEmailArray(args.draftTo),
1347
- cc: normalizeDraftEmailArray(args.draftCc),
1348
- bcc: normalizeDraftEmailArray(args.draftBcc),
1349
- subject: args.draftSubject,
1350
- body: args.draftBody,
1351
- bodyType: normalizeBodyType(args.draftBodyType),
1352
- importance: args.draftImportance || 'normal',
1353
- attachments: args.draftAttachments,
1354
- conversationId: args.conversationId
1382
+ to: normalizeDraftEmailArray(processedArgs.draftTo),
1383
+ cc: normalizeDraftEmailArray(processedArgs.draftCc),
1384
+ bcc: normalizeDraftEmailArray(processedArgs.draftBcc),
1385
+ subject: processedArgs.draftSubject,
1386
+ body: processedArgs.draftBody,
1387
+ bodyType: normalizeBodyType(processedArgs.draftBodyType),
1388
+ importance: processedArgs.draftImportance || 'normal',
1389
+ attachments: processedDraftAttachments,
1390
+ conversationId: processedArgs.conversationId
1355
1391
  });
1356
- let draftResponseText = `āœ… Draft email saved successfully!\nšŸ“§ Subject: ${args.draftSubject}\nšŸ‘„ To: ${Array.isArray(args.draftTo) ? args.draftTo.join(', ') : args.draftTo}\nšŸ†” Draft ID: ${draftResult.id}`;
1392
+ let draftResponseText = `āœ… Draft email saved successfully!\nšŸ“§ Subject: ${processedArgs.draftSubject}\nšŸ‘„ To: ${Array.isArray(processedArgs.draftTo) ? processedArgs.draftTo.join(', ') : processedArgs.draftTo}\nšŸ†” Draft ID: ${draftResult.id}${processedDraftAttachments.length > 0 ? `\nšŸ“Ž Attachments: ${processedDraftAttachments.length} file(s)` : ''}`;
1357
1393
  // Add helpful suggestion for threaded drafts
1358
1394
  if (isLikelyReply) {
1359
1395
  draftResponseText += `\n\nšŸ’” TIP: This looks like a reply email. For drafts that appear in email threads, use "reply_draft" action with the original message ID instead of "draft".`;
@@ -1504,7 +1540,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1504
1540
  // ============ REMAINING ORIGINAL TOOLS ============
1505
1541
  case "send_email":
1506
1542
  if (ms365Config.multiUser) {
1507
- const userId = args?.userId;
1543
+ const userId = processedArgs?.userId;
1508
1544
  if (!userId) {
1509
1545
  throw new Error("User ID is required in multi-user mode");
1510
1546
  }
@@ -1516,10 +1552,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1516
1552
  ms365Ops.setGraphClient(graphClient);
1517
1553
  }
1518
1554
  // Validate and normalize email input
1519
- if (!args?.to) {
1555
+ if (!processedArgs?.to) {
1520
1556
  throw new Error("'to' field is required for sending email");
1521
1557
  }
1522
- if (!args?.subject) {
1558
+ if (!processedArgs?.subject) {
1523
1559
  throw new Error("'subject' field is required for sending email");
1524
1560
  }
1525
1561
  // Helper function to normalize email arrays
@@ -1532,30 +1568,42 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1532
1568
  return field;
1533
1569
  throw new Error(`Invalid email field format. Expected string or array of strings.`);
1534
1570
  };
1571
+ // Process attachments (handle both filePath and contentBytes)
1572
+ let processedAttachments = [];
1573
+ if (processedArgs.attachments && Array.isArray(processedArgs.attachments)) {
1574
+ processedAttachments = await Promise.all(processedArgs.attachments.map(async (attachment) => {
1575
+ try {
1576
+ return await processAttachment(attachment);
1577
+ }
1578
+ catch (error) {
1579
+ throw new Error(`Error processing attachment '${attachment.name}': ${error instanceof Error ? error.message : 'Unknown error'}`);
1580
+ }
1581
+ }));
1582
+ }
1535
1583
  // Normalize the email message
1536
1584
  const emailMessage = {
1537
- to: normalizeEmailArray(args.to),
1538
- cc: normalizeEmailArray(args.cc),
1539
- bcc: normalizeEmailArray(args.bcc),
1540
- subject: args.subject,
1541
- body: args.body || '',
1542
- bodyType: normalizeBodyType(args.bodyType),
1543
- replyTo: args.replyTo,
1544
- importance: args.importance || 'normal',
1545
- attachments: args.attachments || []
1585
+ to: normalizeEmailArray(processedArgs.to),
1586
+ cc: normalizeEmailArray(processedArgs.cc),
1587
+ bcc: normalizeEmailArray(processedArgs.bcc),
1588
+ subject: processedArgs.subject,
1589
+ body: processedArgs.body || '',
1590
+ bodyType: normalizeBodyType(processedArgs.bodyType),
1591
+ replyTo: processedArgs.replyTo,
1592
+ importance: processedArgs.importance || 'normal',
1593
+ attachments: processedAttachments
1546
1594
  };
1547
1595
  const emailResult = await ms365Ops.sendEmail(emailMessage);
1548
1596
  return {
1549
1597
  content: [
1550
1598
  {
1551
1599
  type: "text",
1552
- text: `āœ… Email sent successfully!\n\nšŸ“§ To: ${Array.isArray(args?.to) ? args.to.join(', ') : args?.to}\nšŸ“‹ Subject: ${args?.subject}\nšŸ†” Message ID: ${emailResult.id}`
1600
+ text: `āœ… Email sent successfully!\n\nšŸ“§ To: ${Array.isArray(processedArgs?.to) ? processedArgs.to.join(', ') : processedArgs?.to}\nšŸ“‹ Subject: ${processedArgs?.subject}\nšŸ†” Message ID: ${emailResult.id}${processedAttachments.length > 0 ? `\nšŸ“Ž Attachments: ${processedAttachments.length} file(s)` : ''}`
1553
1601
  }
1554
1602
  ]
1555
1603
  };
1556
1604
  case "get_attachment":
1557
1605
  if (ms365Config.multiUser) {
1558
- const userId = args?.userId;
1606
+ const userId = processedArgs?.userId;
1559
1607
  if (!userId) {
1560
1608
  throw new Error("User ID is required in multi-user mode");
1561
1609
  }
@@ -1566,12 +1614,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1566
1614
  const graphClient = await enhancedMS365Auth.getGraphClient();
1567
1615
  ms365Ops.setGraphClient(graphClient);
1568
1616
  }
1569
- if (!args?.messageId) {
1617
+ if (!processedArgs?.messageId) {
1570
1618
  throw new Error("messageId is required");
1571
1619
  }
1572
1620
  try {
1573
1621
  // First verify the email exists and has attachments
1574
- const email = await ms365Ops.getEmail(args.messageId, true);
1622
+ const email = await ms365Ops.getEmail(processedArgs.messageId, true);
1575
1623
  if (!email.hasAttachments) {
1576
1624
  throw new Error("This email has no attachments");
1577
1625
  }
@@ -1579,12 +1627,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1579
1627
  throw new Error("No attachment information available for this email");
1580
1628
  }
1581
1629
  // If specific attachmentId is provided, get that attachment
1582
- if (args?.attachmentId) {
1583
- const attachmentExists = email.attachments.some(att => att.id === args.attachmentId);
1630
+ if (processedArgs?.attachmentId) {
1631
+ const attachmentExists = email.attachments.some(att => att.id === processedArgs.attachmentId);
1584
1632
  if (!attachmentExists) {
1585
- throw new Error(`Attachment ID ${args.attachmentId} not found in this email. Available attachments:\n${email.attachments.map(att => `- ${att.name} (ID: ${att.id})`).join('\n')}`);
1633
+ throw new Error(`Attachment ID ${processedArgs.attachmentId} not found in this email. Available attachments:\n${email.attachments.map(att => `- ${att.name} (ID: ${att.id})`).join('\n')}`);
1586
1634
  }
1587
- const attachment = await ms365Ops.getAttachment(args.messageId, args.attachmentId);
1635
+ const attachment = await ms365Ops.getAttachment(processedArgs.messageId, processedArgs.attachmentId);
1588
1636
  // Save the attachment to file
1589
1637
  const savedFilename = await saveBase64ToFile(attachment.contentBytes, attachment.name);
1590
1638
  const fileUrl = `${SERVER_URL}/attachments/${savedFilename}`;
@@ -1601,7 +1649,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1601
1649
  else {
1602
1650
  // Get all attachments
1603
1651
  const attachments = await Promise.all(email.attachments.map(async (att) => {
1604
- const attachment = await ms365Ops.getAttachment(args.messageId, att.id);
1652
+ const attachment = await ms365Ops.getAttachment(processedArgs.messageId, att.id);
1605
1653
  const savedFilename = await saveBase64ToFile(attachment.contentBytes, attachment.name);
1606
1654
  return {
1607
1655
  name: attachment.name,
@@ -1985,22 +2033,22 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1985
2033
  throw new Error(`Unknown classification action: ${classifyAction}`);
1986
2034
  }
1987
2035
  case 'process_attachments':
1988
- if (!args?.messageId) {
2036
+ if (!processedArgs?.messageId) {
1989
2037
  throw new Error("Message ID is required for attachment processing");
1990
2038
  }
1991
2039
  // Get the email and its attachments
1992
- const email = await ms365Ops.getEmail(args.messageId, true);
2040
+ const email = await ms365Ops.getEmail(processedArgs?.messageId, true);
1993
2041
  if (!email.hasAttachments || !email.attachments) {
1994
2042
  throw new Error("This email has no attachments to process");
1995
2043
  }
1996
2044
  // Filter specific attachments if requested
1997
2045
  let attachmentsToProcess = email.attachments;
1998
- if (args?.attachmentIds && Array.isArray(args.attachmentIds)) {
1999
- attachmentsToProcess = email.attachments.filter(att => args.attachmentIds.includes(att.id));
2046
+ if (processedArgs?.attachmentIds && Array.isArray(processedArgs.attachmentIds)) {
2047
+ attachmentsToProcess = email.attachments.filter(att => processedArgs.attachmentIds.includes(att.id));
2000
2048
  }
2001
2049
  // Download attachments and prepare for processing
2002
2050
  const attachmentData = await Promise.all(attachmentsToProcess.map(async (att) => {
2003
- const attachment = await ms365Ops.getAttachment(args.messageId, att.id);
2051
+ const attachment = await ms365Ops.getAttachment(processedArgs?.messageId, att.id);
2004
2052
  return {
2005
2053
  id: att.id,
2006
2054
  name: att.name,
@@ -2530,6 +2578,81 @@ app.use(cors());
2530
2578
  // Get the directory name in ESM
2531
2579
  const __filename = fileURLToPath(import.meta.url);
2532
2580
  const __dirname = path.dirname(__filename);
2581
+ // Utility function to read file and convert to Base64
2582
+ async function fileToBase64(filePath) {
2583
+ try {
2584
+ // Check if path is absolute
2585
+ if (!path.isAbsolute(filePath)) {
2586
+ throw new Error(`File path must be absolute (start with '/'). Received relative path: ${filePath}. Example of correct path: '/Users/name/Documents/file.pdf'`);
2587
+ }
2588
+ // Normalize the file path to handle Unicode characters properly
2589
+ const normalizedPath = path.normalize(filePath);
2590
+ logger.log(`šŸ” Attempting to read file: ${normalizedPath}`);
2591
+ // Check if file exists and is readable
2592
+ await fs.access(normalizedPath, fs.constants.R_OK);
2593
+ // Get file stats
2594
+ const stats = await fs.stat(normalizedPath);
2595
+ if (!stats.isFile()) {
2596
+ throw new Error(`Path is not a file: ${normalizedPath}`);
2597
+ }
2598
+ // Read file content
2599
+ const buffer = await fs.readFile(normalizedPath);
2600
+ const contentBytes = buffer.toString('base64');
2601
+ // Auto-detect MIME type based on file extension
2602
+ const ext = path.extname(normalizedPath).toLowerCase();
2603
+ const mimeTypes = {
2604
+ '.pdf': 'application/pdf',
2605
+ '.doc': 'application/msword',
2606
+ '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
2607
+ '.xls': 'application/vnd.ms-excel',
2608
+ '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
2609
+ '.ppt': 'application/vnd.ms-powerpoint',
2610
+ '.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
2611
+ '.txt': 'text/plain',
2612
+ '.csv': 'text/csv',
2613
+ '.json': 'application/json',
2614
+ '.xml': 'application/xml',
2615
+ '.zip': 'application/zip',
2616
+ '.jpg': 'image/jpeg',
2617
+ '.jpeg': 'image/jpeg',
2618
+ '.png': 'image/png',
2619
+ '.gif': 'image/gif',
2620
+ '.bmp': 'image/bmp',
2621
+ '.svg': 'image/svg+xml'
2622
+ };
2623
+ const contentType = mimeTypes[ext] || 'application/octet-stream';
2624
+ return {
2625
+ contentBytes,
2626
+ contentType,
2627
+ size: stats.size
2628
+ };
2629
+ }
2630
+ catch (error) {
2631
+ logger.error(`āŒ File read error for ${filePath}:`, error);
2632
+ throw new Error(`Failed to read file ${filePath}: ${error instanceof Error ? error.message : 'Unknown error'}`);
2633
+ }
2634
+ }
2635
+ // Utility function to process attachment (handles both filePath and contentBytes)
2636
+ async function processAttachment(attachment) {
2637
+ // If contentBytes is provided, use it directly
2638
+ if (attachment.contentBytes) {
2639
+ return {
2640
+ name: attachment.name,
2641
+ contentBytes: attachment.contentBytes,
2642
+ contentType: attachment.contentType || 'application/octet-stream'
2643
+ };
2644
+ }
2645
+ // If filePath is provided, read file and convert to Base64
2646
+ if (attachment.filePath) {
2647
+ const fileData = await fileToBase64(attachment.filePath);
2648
+ return {
2649
+ name: attachment.name,
2650
+ contentBytes: fileData.contentBytes,
2651
+ contentType: attachment.contentType || fileData.contentType
2652
+ };
2653
+ }
2654
+ throw new Error(`Attachment ${attachment.name} must have either contentBytes or filePath`);
2655
+ }
2533
2656
  // Create public/attachments directory if it doesn't exist
2534
2657
  const attachmentsDir = path.join(__dirname, '../public/attachments');
2535
2658
  fs.mkdir(attachmentsDir, { recursive: true })
@@ -186,12 +186,21 @@ export class MS365Operations {
186
186
  searchTerms.push(`subject:${escapedSubject}`);
187
187
  }
188
188
  }
189
- // āœ… SAFE: From search (supported by $search for names and emails)
189
+ // āœ… SAFE: Enhanced From search with smart email handling
190
190
  if (criteria.from) {
191
191
  const escapedFrom = criteria.from.replace(/"/g, '\\"');
192
192
  if (criteria.from.includes('@')) {
193
- // Exact email search
194
- searchTerms.push(`from:${escapedFrom}`);
193
+ // ENHANCED: Smart email search - prioritize local part for better fuzzy matching
194
+ const emailParts = criteria.from.split('@');
195
+ const localPart = emailParts[0];
196
+ // Use local part (username) for fuzzy matching - more reliable than exact email
197
+ if (localPart && localPart.length > 2) {
198
+ searchTerms.push(`from:${localPart}`);
199
+ }
200
+ else {
201
+ // Fallback to exact email if local part is too short
202
+ searchTerms.push(`from:${escapedFrom}`);
203
+ }
195
204
  }
196
205
  else {
197
206
  // Name search - use quotes for multi-word names
@@ -992,18 +1001,19 @@ ${originalBodyContent}
992
1001
  const maxResults = criteria.maxResults || 50;
993
1002
  let allMessages = [];
994
1003
  // STRATEGY 1: Use $filter for structured queries (exact matches, dates, booleans)
995
- const hasFilterableFields = !!((criteria.from && criteria.from.includes('@')) ||
996
- (criteria.to && criteria.to.includes('@')) ||
1004
+ // ENHANCED: Treat 'from' fields as searchable for better reliability
1005
+ const hasFilterableFields = !!((criteria.to && criteria.to.includes('@')) ||
997
1006
  (criteria.cc && criteria.cc.includes('@')) ||
998
1007
  criteria.after ||
999
1008
  criteria.before ||
1000
1009
  criteria.hasAttachment !== undefined ||
1001
1010
  criteria.isUnread !== undefined ||
1002
1011
  criteria.importance);
1003
- // STRATEGY 2: Use $search for text searches (subject, body content, from names)
1012
+ // STRATEGY 2: Use $search for text searches (subject, body content, all from searches)
1013
+ // ENHANCED: Always use search for 'from' field for better fuzzy matching
1004
1014
  const hasSearchableFields = !!(criteria.query ||
1005
1015
  criteria.subject ||
1006
- (criteria.from && !criteria.from.includes('@')) // name searches
1016
+ criteria.from // Always use search for from field (both names and emails)
1007
1017
  );
1008
1018
  try {
1009
1019
  // STRATEGY 0: FOLDER SEARCH - Handle folder searches with optimized methods
@@ -1039,6 +1049,25 @@ ${originalBodyContent}
1039
1049
  const sortedMessages = this.sortSearchResults(filteredMessages, criteria);
1040
1050
  // Apply maxResults limit
1041
1051
  const finalMessages = sortedMessages.slice(0, maxResults);
1052
+ // ENHANCED: Smart fallback for empty results with 'from' field
1053
+ if (finalMessages.length === 0 && criteria.from && criteria.from.includes('@')) {
1054
+ logger.log('šŸ”„ No results found with exact search, trying fuzzy fallback...');
1055
+ // Extract local part for fuzzy search
1056
+ const localPart = criteria.from.split('@')[0];
1057
+ if (localPart && localPart.length > 2) {
1058
+ const fallbackCriteria = { ...criteria, from: localPart };
1059
+ const fallbackResult = await this.performPureSearchQuery(fallbackCriteria, maxResults);
1060
+ if (fallbackResult.length > 0) {
1061
+ logger.log(`šŸŽÆ Fuzzy fallback found ${fallbackResult.length} results`);
1062
+ const searchResult = {
1063
+ messages: fallbackResult.slice(0, maxResults),
1064
+ hasMore: fallbackResult.length > maxResults
1065
+ };
1066
+ this.setCachedResults(cacheKey, searchResult);
1067
+ return searchResult;
1068
+ }
1069
+ }
1070
+ }
1042
1071
  const searchResult = {
1043
1072
  messages: finalMessages,
1044
1073
  hasMore: sortedMessages.length > maxResults
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ms365-mcp-server",
3
- "version": "1.1.22",
3
+ "version": "1.1.24",
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",
@@ -32,15 +32,15 @@
32
32
  "dependencies": {
33
33
  "@azure/msal-node": "^2.6.6",
34
34
  "@microsoft/microsoft-graph-client": "^3.0.7",
35
- "@modelcontextprotocol/sdk": "^1.10.1",
35
+ "@modelcontextprotocol/sdk": "^1.18.1",
36
36
  "@types/cors": "^2.8.19",
37
37
  "cors": "^2.8.5",
38
38
  "express": "^5.1.0",
39
39
  "isomorphic-fetch": "^3.0.0",
40
40
  "keytar": "^7.9.0",
41
- "mime-types": "^2.1.35",
42
- "open": "^10.1.0",
43
- "typescript": "^5.0.4"
41
+ "mime-types": "^3.0.1",
42
+ "open": "^10.2.0",
43
+ "typescript": "^5.7.2"
44
44
  },
45
45
  "devDependencies": {
46
46
  "@types/express": "^5.0.3",