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 +181 -58
- package/dist/utils/ms365-operations.js +36 -7
- package/package.json +5 -5
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"
|
|
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"
|
|
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 =
|
|
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
|
|
1070
|
+
logger.log('DEBUG: manage_email called with processedArgs:', JSON.stringify(processedArgs, null, 2));
|
|
1047
1071
|
if (ms365Config.multiUser) {
|
|
1048
|
-
const 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 =
|
|
1083
|
+
const emailAction = processedArgs?.action;
|
|
1060
1084
|
logger.log(`DEBUG: Email action: ${emailAction}`);
|
|
1061
1085
|
switch (emailAction) {
|
|
1062
1086
|
case "read":
|
|
1063
|
-
if (!
|
|
1087
|
+
if (!processedArgs?.messageId) {
|
|
1064
1088
|
throw new Error("messageId is required for read action");
|
|
1065
1089
|
}
|
|
1066
|
-
const email = await ms365Ops.getEmail(
|
|
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 = !!(
|
|
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 =
|
|
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(
|
|
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 (
|
|
1117
|
-
responseText += `⢠Try partial names: "${
|
|
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 (!
|
|
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 =
|
|
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(
|
|
1347
|
-
cc: normalizeDraftEmailArray(
|
|
1348
|
-
bcc: normalizeDraftEmailArray(
|
|
1349
|
-
subject:
|
|
1350
|
-
body:
|
|
1351
|
-
bodyType: normalizeBodyType(
|
|
1352
|
-
importance:
|
|
1353
|
-
attachments:
|
|
1354
|
-
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: ${
|
|
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 =
|
|
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 (!
|
|
1555
|
+
if (!processedArgs?.to) {
|
|
1520
1556
|
throw new Error("'to' field is required for sending email");
|
|
1521
1557
|
}
|
|
1522
|
-
if (!
|
|
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(
|
|
1538
|
-
cc: normalizeEmailArray(
|
|
1539
|
-
bcc: normalizeEmailArray(
|
|
1540
|
-
subject:
|
|
1541
|
-
body:
|
|
1542
|
-
bodyType: normalizeBodyType(
|
|
1543
|
-
replyTo:
|
|
1544
|
-
importance:
|
|
1545
|
-
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(
|
|
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 =
|
|
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 (!
|
|
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(
|
|
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 (
|
|
1583
|
-
const attachmentExists = email.attachments.some(att => att.id ===
|
|
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 ${
|
|
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(
|
|
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(
|
|
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 (!
|
|
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(
|
|
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 (
|
|
1999
|
-
attachmentsToProcess = email.attachments.filter(att =>
|
|
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(
|
|
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
|
|
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
|
-
//
|
|
194
|
-
|
|
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
|
-
|
|
996
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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": "^
|
|
42
|
-
"open": "^10.
|
|
43
|
-
"typescript": "^5.
|
|
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",
|