ms365-mcp-server 1.1.2 ā 1.1.4
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 +40 -0
- package/dist/index.js +221 -10
- package/dist/utils/ms365-operations.js +145 -231
- package/package.json +8 -6
package/README.md
CHANGED
|
@@ -252,6 +252,46 @@ ms365-mcp-server --login
|
|
|
252
252
|
}
|
|
253
253
|
```
|
|
254
254
|
|
|
255
|
+
### Attachment Management
|
|
256
|
+
```javascript
|
|
257
|
+
// Download a specific attachment
|
|
258
|
+
{
|
|
259
|
+
"tool": "get_attachment",
|
|
260
|
+
"arguments": {
|
|
261
|
+
"messageId": "email_id",
|
|
262
|
+
"attachmentId": "attachment_id"
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Download all attachments from an email
|
|
267
|
+
{
|
|
268
|
+
"tool": "get_attachment",
|
|
269
|
+
"arguments": {
|
|
270
|
+
"messageId": "email_id"
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
### File Serving
|
|
276
|
+
The server automatically serves downloaded attachments. By default, files are served at `http://localhost:55000/attachments/`. For production environments, configure the server URL using the `SERVER_URL` environment variable:
|
|
277
|
+
|
|
278
|
+
```bash
|
|
279
|
+
# Local development (default)
|
|
280
|
+
SERVER_URL=http://localhost:55000 ms365-mcp-server
|
|
281
|
+
|
|
282
|
+
# Production environment
|
|
283
|
+
SERVER_URL=https://your-domain.com ms365-mcp-server
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
Files are:
|
|
287
|
+
- Stored with unique names to prevent collisions
|
|
288
|
+
- Available for 24 hours
|
|
289
|
+
- Served with proper content types
|
|
290
|
+
- Accessible via the configured server URL
|
|
291
|
+
|
|
292
|
+
### Cleanup
|
|
293
|
+
Attachments are automatically cleaned up after 24 hours. You can also manually delete files from the `public/attachments` directory.
|
|
294
|
+
|
|
255
295
|
## šÆ Command Line Options
|
|
256
296
|
|
|
257
297
|
```bash
|
package/dist/index.js
CHANGED
|
@@ -4,6 +4,11 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
6
6
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
7
|
+
import express from 'express';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import { fileURLToPath } from 'url';
|
|
10
|
+
import fs from 'fs/promises';
|
|
11
|
+
import crypto from 'crypto';
|
|
7
12
|
import { CallToolRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ListPromptsRequestSchema, ReadResourceRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
8
13
|
import { logger } from './utils/api.js';
|
|
9
14
|
import { MS365Operations } from './utils/ms365-operations.js';
|
|
@@ -55,7 +60,7 @@ function parseArgs() {
|
|
|
55
60
|
}
|
|
56
61
|
const server = new Server({
|
|
57
62
|
name: "ms365-mcp-server",
|
|
58
|
-
version: "1.1.
|
|
63
|
+
version: "1.1.4"
|
|
59
64
|
}, {
|
|
60
65
|
capabilities: {
|
|
61
66
|
resources: {
|
|
@@ -612,11 +617,32 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
612
617
|
throw new Error("messageId is required for read action");
|
|
613
618
|
}
|
|
614
619
|
const email = await ms365Ops.getEmail(args.messageId, args?.includeAttachments);
|
|
620
|
+
let emailDetailsText = `š§ Email Details\n\nš Subject: ${email.subject}\nš¤ From: ${email.from.name} <${email.from.address}>\nš
Date: ${email.receivedDateTime}\n`;
|
|
621
|
+
if (email.attachments && email.attachments.length > 0) {
|
|
622
|
+
emailDetailsText += `\nš Attachments (${email.attachments.length}):\n`;
|
|
623
|
+
email.attachments.forEach((attachment, index) => {
|
|
624
|
+
const sizeInMB = (attachment.size / (1024 * 1024)).toFixed(2);
|
|
625
|
+
emailDetailsText += `\n${index + 1}. ${attachment.name}\n`;
|
|
626
|
+
emailDetailsText += ` š¦ Size: ${sizeInMB} MB\n`;
|
|
627
|
+
emailDetailsText += ` š Type: ${attachment.contentType}\n`;
|
|
628
|
+
emailDetailsText += ` š ID: ${attachment.id}\n`;
|
|
629
|
+
if (attachment.isInline) {
|
|
630
|
+
emailDetailsText += ` š Inline: Yes\n`;
|
|
631
|
+
}
|
|
632
|
+
});
|
|
633
|
+
emailDetailsText += `\nš” To download an attachment, use the get_attachment tool with:\n`;
|
|
634
|
+
emailDetailsText += ` ⢠messageId: ${email.id}\n`;
|
|
635
|
+
emailDetailsText += ` ⢠attachmentId: (use the ID from above)\n`;
|
|
636
|
+
}
|
|
637
|
+
else {
|
|
638
|
+
emailDetailsText += `\nš No attachments`;
|
|
639
|
+
}
|
|
640
|
+
emailDetailsText += `\n\nš¬ Body:\n${email.body || email.bodyPreview}`;
|
|
615
641
|
return {
|
|
616
642
|
content: [
|
|
617
643
|
{
|
|
618
644
|
type: "text",
|
|
619
|
-
text:
|
|
645
|
+
text: emailDetailsText
|
|
620
646
|
}
|
|
621
647
|
]
|
|
622
648
|
};
|
|
@@ -814,15 +840,66 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
814
840
|
const graphClient = await enhancedMS365Auth.getGraphClient();
|
|
815
841
|
ms365Ops.setGraphClient(graphClient);
|
|
816
842
|
}
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
843
|
+
if (!args?.messageId) {
|
|
844
|
+
throw new Error("messageId is required");
|
|
845
|
+
}
|
|
846
|
+
try {
|
|
847
|
+
// First verify the email exists and has attachments
|
|
848
|
+
const email = await ms365Ops.getEmail(args.messageId, true);
|
|
849
|
+
if (!email.hasAttachments) {
|
|
850
|
+
throw new Error("This email has no attachments");
|
|
851
|
+
}
|
|
852
|
+
if (!email.attachments || email.attachments.length === 0) {
|
|
853
|
+
throw new Error("No attachment information available for this email");
|
|
854
|
+
}
|
|
855
|
+
// If specific attachmentId is provided, get that attachment
|
|
856
|
+
if (args?.attachmentId) {
|
|
857
|
+
const attachmentExists = email.attachments.some(att => att.id === args.attachmentId);
|
|
858
|
+
if (!attachmentExists) {
|
|
859
|
+
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')}`);
|
|
823
860
|
}
|
|
824
|
-
|
|
825
|
-
|
|
861
|
+
const attachment = await ms365Ops.getAttachment(args.messageId, args.attachmentId);
|
|
862
|
+
// Save the attachment to file
|
|
863
|
+
const savedFilename = await saveBase64ToFile(attachment.contentBytes, attachment.name);
|
|
864
|
+
const fileUrl = `${SERVER_URL}/attachments/${savedFilename}`;
|
|
865
|
+
attachment.fileUrl = fileUrl;
|
|
866
|
+
const artifacts = getListOfArtifacts("get_attachment", [attachment]);
|
|
867
|
+
const content = {
|
|
868
|
+
type: "text",
|
|
869
|
+
text: `š Attachment Downloaded\n\nš Name: ${attachment.name}\nš¾ Size: ${attachment.size} bytes\nšļø Type: ${attachment.contentType}\n\nš Download URL: ${fileUrl}\n\nš” The file will be available for 24 hours.`
|
|
870
|
+
};
|
|
871
|
+
return {
|
|
872
|
+
content: [content, ...artifacts]
|
|
873
|
+
};
|
|
874
|
+
}
|
|
875
|
+
else {
|
|
876
|
+
// Get all attachments
|
|
877
|
+
const attachments = await Promise.all(email.attachments.map(async (att) => {
|
|
878
|
+
const attachment = await ms365Ops.getAttachment(args.messageId, att.id);
|
|
879
|
+
const savedFilename = await saveBase64ToFile(attachment.contentBytes, attachment.name);
|
|
880
|
+
return {
|
|
881
|
+
name: attachment.name,
|
|
882
|
+
contentType: attachment.contentType,
|
|
883
|
+
size: attachment.size,
|
|
884
|
+
fileUrl: `${SERVER_URL}/attachments/${savedFilename}`
|
|
885
|
+
};
|
|
886
|
+
}));
|
|
887
|
+
const artifacts = getListOfArtifacts("get_attachment", attachments);
|
|
888
|
+
const content = {
|
|
889
|
+
type: "text",
|
|
890
|
+
text: `š Downloaded ${attachments.length} Attachments\n\n${attachments.map((att, index) => `${index + 1}. š ${att.name}\n š¾ Size: ${att.size} bytes\n šļø Type: ${att.contentType}\n š Download URL: ${att.fileUrl}\n`).join('\n')}\n\nš” Files will be available for 24 hours.`
|
|
891
|
+
};
|
|
892
|
+
return {
|
|
893
|
+
content: [content, ...artifacts]
|
|
894
|
+
};
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
catch (error) {
|
|
898
|
+
if (error.message.includes("not found in the store")) {
|
|
899
|
+
throw new Error(`Email or attachment not found. Please verify:\n1. The email ID is correct\n2. The attachment ID is correct\n3. You have permission to access this email/attachment`);
|
|
900
|
+
}
|
|
901
|
+
throw error;
|
|
902
|
+
}
|
|
826
903
|
case "list_folders":
|
|
827
904
|
if (ms365Config.multiUser) {
|
|
828
905
|
const userId = args?.userId;
|
|
@@ -892,6 +969,140 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
892
969
|
};
|
|
893
970
|
}
|
|
894
971
|
});
|
|
972
|
+
function generateAlphaNumericId(alphaSize = 3, numericSize = 3) {
|
|
973
|
+
const letters = 'abcdefghijklmnopqrstuvwxyz';
|
|
974
|
+
const digits = '0123456789';
|
|
975
|
+
let alphaPart = '';
|
|
976
|
+
let numericPart = '';
|
|
977
|
+
for (let i = 0; i < alphaSize; i++) {
|
|
978
|
+
alphaPart += letters.charAt(Math.floor(Math.random() * letters.length));
|
|
979
|
+
}
|
|
980
|
+
for (let i = 0; i < numericSize; i++) {
|
|
981
|
+
numericPart += digits.charAt(Math.floor(Math.random() * digits.length));
|
|
982
|
+
}
|
|
983
|
+
return alphaPart + numericPart;
|
|
984
|
+
}
|
|
985
|
+
function getListOfArtifacts(functionName, attachments) {
|
|
986
|
+
const artifacts = [];
|
|
987
|
+
attachments.forEach((attachment, i) => {
|
|
988
|
+
const url = attachment?.fileUrl || '';
|
|
989
|
+
const fileName = attachment?.name || '';
|
|
990
|
+
if (url) {
|
|
991
|
+
const timestamp = Math.floor(Date.now() / 1000); // seconds
|
|
992
|
+
const visitTimestamp = Date.now(); // milliseconds
|
|
993
|
+
const artifactData = {
|
|
994
|
+
id: `msg_browser_${generateAlphaNumericId()}`,
|
|
995
|
+
parentTaskId: `task_attachment_${generateAlphaNumericId()}`,
|
|
996
|
+
timestamp: timestamp,
|
|
997
|
+
agent: {
|
|
998
|
+
id: "agent_siya_browser",
|
|
999
|
+
name: "SIYA",
|
|
1000
|
+
type: "qna"
|
|
1001
|
+
},
|
|
1002
|
+
messageType: "action",
|
|
1003
|
+
action: {
|
|
1004
|
+
tool: "browser",
|
|
1005
|
+
operation: "browsing",
|
|
1006
|
+
params: {
|
|
1007
|
+
url: `Filename: ${fileName}`,
|
|
1008
|
+
pageTitle: `Tool response for ${functionName}`,
|
|
1009
|
+
visual: {
|
|
1010
|
+
icon: "browser",
|
|
1011
|
+
color: "#2D8CFF"
|
|
1012
|
+
},
|
|
1013
|
+
stream: {
|
|
1014
|
+
type: "vnc",
|
|
1015
|
+
streamId: "stream_browser_1",
|
|
1016
|
+
target: "browser"
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
},
|
|
1020
|
+
content: `Viewed page: ${functionName}`,
|
|
1021
|
+
artifacts: [
|
|
1022
|
+
{
|
|
1023
|
+
id: "artifact_webpage_1746018877304_994",
|
|
1024
|
+
type: "browser_view",
|
|
1025
|
+
content: {
|
|
1026
|
+
url: url,
|
|
1027
|
+
title: functionName,
|
|
1028
|
+
screenshot: "",
|
|
1029
|
+
textContent: `Observed output of cmd \`${functionName}\` executed:`,
|
|
1030
|
+
extractedInfo: {}
|
|
1031
|
+
},
|
|
1032
|
+
metadata: {
|
|
1033
|
+
domainName: "example.com",
|
|
1034
|
+
visitTimestamp: visitTimestamp,
|
|
1035
|
+
category: "web_page"
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
],
|
|
1039
|
+
status: "completed"
|
|
1040
|
+
};
|
|
1041
|
+
const artifact = {
|
|
1042
|
+
type: "text",
|
|
1043
|
+
text: JSON.stringify(artifactData, null, 2),
|
|
1044
|
+
title: `Filename: ${fileName}`,
|
|
1045
|
+
format: "json"
|
|
1046
|
+
};
|
|
1047
|
+
artifacts.push(artifact);
|
|
1048
|
+
}
|
|
1049
|
+
});
|
|
1050
|
+
return artifacts;
|
|
1051
|
+
}
|
|
1052
|
+
// Set up Express server for file serving
|
|
1053
|
+
const app = express();
|
|
1054
|
+
const PORT = process.env.PORT || 55000;
|
|
1055
|
+
const SERVER_URL = process.env.SERVER_URL || `http://localhost:${PORT}`;
|
|
1056
|
+
// Get the directory name in ESM
|
|
1057
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
1058
|
+
const __dirname = path.dirname(__filename);
|
|
1059
|
+
// Serve static files from public directory
|
|
1060
|
+
app.use('/attachments', express.static(path.join(__dirname, '../public/attachments')));
|
|
1061
|
+
// Start Express server
|
|
1062
|
+
app.listen(PORT, () => {
|
|
1063
|
+
logger.log(`File server running on ${SERVER_URL}`);
|
|
1064
|
+
});
|
|
1065
|
+
// Helper function to generate unique filenames
|
|
1066
|
+
function generateUniqueFilename(originalName) {
|
|
1067
|
+
const timestamp = Date.now();
|
|
1068
|
+
const randomString = crypto.randomBytes(8).toString('hex');
|
|
1069
|
+
const extension = path.extname(originalName);
|
|
1070
|
+
const baseName = path.basename(originalName, extension);
|
|
1071
|
+
return `${baseName}-${timestamp}-${randomString}${extension}`;
|
|
1072
|
+
}
|
|
1073
|
+
// Helper function to save base64 content to file
|
|
1074
|
+
async function saveBase64ToFile(base64Content, filename) {
|
|
1075
|
+
const uniqueFilename = generateUniqueFilename(filename);
|
|
1076
|
+
const filePath = path.join(__dirname, '../public/attachments', uniqueFilename);
|
|
1077
|
+
const buffer = Buffer.from(base64Content, 'base64');
|
|
1078
|
+
await fs.writeFile(filePath, buffer);
|
|
1079
|
+
return uniqueFilename;
|
|
1080
|
+
}
|
|
1081
|
+
// Helper function to clean up old attachments
|
|
1082
|
+
async function cleanupOldAttachments() {
|
|
1083
|
+
const attachmentsDir = path.join(__dirname, '../public/attachments');
|
|
1084
|
+
try {
|
|
1085
|
+
const files = await fs.readdir(attachmentsDir);
|
|
1086
|
+
const now = Date.now();
|
|
1087
|
+
for (const file of files) {
|
|
1088
|
+
const filePath = path.join(attachmentsDir, file);
|
|
1089
|
+
const stats = await fs.stat(filePath);
|
|
1090
|
+
const fileAge = now - stats.mtimeMs;
|
|
1091
|
+
// Remove files older than 24 hours
|
|
1092
|
+
if (fileAge > 24 * 60 * 60 * 1000) {
|
|
1093
|
+
await fs.unlink(filePath);
|
|
1094
|
+
logger.log(`Cleaned up old attachment: ${file}`);
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
catch (error) {
|
|
1099
|
+
logger.error('Error cleaning up attachments:', error);
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
// Set up cleanup interval (run every hour)
|
|
1103
|
+
setInterval(cleanupOldAttachments, 60 * 60 * 1000);
|
|
1104
|
+
// Run initial cleanup
|
|
1105
|
+
cleanupOldAttachments();
|
|
895
1106
|
async function main() {
|
|
896
1107
|
ms365Config = parseArgs();
|
|
897
1108
|
if (ms365Config.setupAuth) {
|
|
@@ -318,14 +318,16 @@ export class MS365Operations {
|
|
|
318
318
|
async getEmail(messageId, includeAttachments = false) {
|
|
319
319
|
try {
|
|
320
320
|
const graphClient = await this.getGraphClient();
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
selectFields += ',body';
|
|
324
|
-
}
|
|
321
|
+
logger.log('Fetching email details...');
|
|
322
|
+
// First get the basic email info with attachments expanded
|
|
325
323
|
const email = await graphClient
|
|
326
324
|
.api(`/me/messages/${messageId}`)
|
|
327
|
-
.select(
|
|
325
|
+
.select('id,subject,from,toRecipients,ccRecipients,receivedDateTime,sentDateTime,bodyPreview,isRead,hasAttachments,importance,conversationId,parentFolderId,webLink,body,attachments')
|
|
326
|
+
.expand('attachments')
|
|
328
327
|
.get();
|
|
328
|
+
logger.log(`Email details retrieved. hasAttachments flag: ${email.hasAttachments}`);
|
|
329
|
+
logger.log(`Email subject: ${email.subject}`);
|
|
330
|
+
logger.log(`Direct attachments count: ${email.attachments?.length || 0}`);
|
|
329
331
|
const emailInfo = {
|
|
330
332
|
id: email.id,
|
|
331
333
|
subject: email.subject || '',
|
|
@@ -349,22 +351,75 @@ export class MS365Operations {
|
|
|
349
351
|
importance: email.importance || 'normal',
|
|
350
352
|
conversationId: email.conversationId || '',
|
|
351
353
|
parentFolderId: email.parentFolderId || '',
|
|
352
|
-
webLink: email.webLink || ''
|
|
354
|
+
webLink: email.webLink || '',
|
|
355
|
+
attachments: email.attachments?.map((attachment) => ({
|
|
356
|
+
id: attachment.id,
|
|
357
|
+
name: attachment.name,
|
|
358
|
+
contentType: attachment.contentType,
|
|
359
|
+
size: attachment.size,
|
|
360
|
+
isInline: attachment.isInline,
|
|
361
|
+
contentId: attachment.contentId
|
|
362
|
+
})) || []
|
|
353
363
|
};
|
|
354
|
-
if (
|
|
364
|
+
if (email.body) {
|
|
355
365
|
emailInfo.body = email.body.content || '';
|
|
356
366
|
}
|
|
357
|
-
//
|
|
358
|
-
if (includeAttachments
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
367
|
+
// Always try to get attachments if requested
|
|
368
|
+
if (includeAttachments) {
|
|
369
|
+
logger.log('Attempting to fetch attachments...');
|
|
370
|
+
try {
|
|
371
|
+
// First check if we got attachments from the expanded query
|
|
372
|
+
if (email.attachments && email.attachments.length > 0) {
|
|
373
|
+
logger.log(`Found ${email.attachments.length} attachments from expanded query`);
|
|
374
|
+
emailInfo.attachments = email.attachments.map((attachment) => ({
|
|
375
|
+
id: attachment.id,
|
|
376
|
+
name: attachment.name,
|
|
377
|
+
contentType: attachment.contentType,
|
|
378
|
+
size: attachment.size,
|
|
379
|
+
isInline: attachment.isInline,
|
|
380
|
+
contentId: attachment.contentId
|
|
381
|
+
}));
|
|
382
|
+
emailInfo.hasAttachments = true;
|
|
383
|
+
}
|
|
384
|
+
else {
|
|
385
|
+
// Try getting attachments directly
|
|
386
|
+
logger.log('Method 1: Direct attachment query...');
|
|
387
|
+
const attachments = await graphClient
|
|
388
|
+
.api(`/me/messages/${messageId}/attachments`)
|
|
389
|
+
.select('id,name,contentType,size,isInline,contentId')
|
|
390
|
+
.get();
|
|
391
|
+
logger.log(`Method 1 results: Found ${attachments.value?.length || 0} attachments`);
|
|
392
|
+
if (attachments && attachments.value && attachments.value.length > 0) {
|
|
393
|
+
emailInfo.attachments = attachments.value.map((attachment) => ({
|
|
394
|
+
id: attachment.id,
|
|
395
|
+
name: attachment.name,
|
|
396
|
+
contentType: attachment.contentType,
|
|
397
|
+
size: attachment.size,
|
|
398
|
+
isInline: attachment.isInline,
|
|
399
|
+
contentId: attachment.contentId
|
|
400
|
+
}));
|
|
401
|
+
emailInfo.hasAttachments = true;
|
|
402
|
+
logger.log(`Successfully retrieved ${emailInfo.attachments.length} attachments`);
|
|
403
|
+
}
|
|
404
|
+
else {
|
|
405
|
+
logger.log('No attachments found with either method');
|
|
406
|
+
emailInfo.attachments = [];
|
|
407
|
+
emailInfo.hasAttachments = false;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
catch (attachmentError) {
|
|
412
|
+
logger.error('Error getting attachments:', attachmentError);
|
|
413
|
+
logger.error('Error details:', JSON.stringify(attachmentError, null, 2));
|
|
414
|
+
emailInfo.attachments = [];
|
|
415
|
+
emailInfo.hasAttachments = false;
|
|
416
|
+
}
|
|
363
417
|
}
|
|
364
418
|
return emailInfo;
|
|
365
419
|
}
|
|
366
420
|
catch (error) {
|
|
367
421
|
logger.error('Error getting email:', error);
|
|
422
|
+
logger.error('Error details:', JSON.stringify(error, null, 2));
|
|
368
423
|
throw error;
|
|
369
424
|
}
|
|
370
425
|
}
|
|
@@ -380,111 +435,13 @@ export class MS365Operations {
|
|
|
380
435
|
if (cachedResults) {
|
|
381
436
|
return cachedResults;
|
|
382
437
|
}
|
|
383
|
-
// For
|
|
384
|
-
if (criteria.from && !criteria.from.includes('@')) {
|
|
385
|
-
logger.log(`Using enhanced name matching for: "${criteria.from}"`);
|
|
386
|
-
// Get more emails and filter manually for better name matching
|
|
387
|
-
const maxResults = criteria.maxResults || 100;
|
|
388
|
-
try {
|
|
389
|
-
// Optimized field selection for name search - only get essential fields first
|
|
390
|
-
const essentialFields = 'id,subject,from,receivedDateTime,isRead,hasAttachments,importance';
|
|
391
|
-
const apiCall = graphClient.api('/me/messages')
|
|
392
|
-
.select(essentialFields)
|
|
393
|
-
.orderby('receivedDateTime desc')
|
|
394
|
-
.top(Math.min(maxResults * 3, 300)); // Get more emails for better filtering
|
|
395
|
-
const result = await apiCall.get();
|
|
396
|
-
// First pass: filter by name using minimal data
|
|
397
|
-
const nameMatches = result.value?.filter((email) => {
|
|
398
|
-
const fromName = email.from?.emailAddress?.name?.toLowerCase() || '';
|
|
399
|
-
const fromAddress = email.from?.emailAddress?.address?.toLowerCase() || '';
|
|
400
|
-
const searchTerm = criteria.from.toLowerCase().trim();
|
|
401
|
-
// Quick name matching (subset of the full logic for performance)
|
|
402
|
-
return fromName.includes(searchTerm) ||
|
|
403
|
-
fromAddress.includes(searchTerm) ||
|
|
404
|
-
fromName.split(/\s+/).some((part) => part.startsWith(searchTerm)) ||
|
|
405
|
-
searchTerm.split(/\s+/).every((part) => fromName.includes(part));
|
|
406
|
-
}) || [];
|
|
407
|
-
if (nameMatches.length === 0) {
|
|
408
|
-
const emptyResult = { messages: [], hasMore: false };
|
|
409
|
-
this.setCachedResults(cacheKey, emptyResult);
|
|
410
|
-
return emptyResult;
|
|
411
|
-
}
|
|
412
|
-
// Second pass: get full details for matched emails
|
|
413
|
-
const messageIds = nameMatches.slice(0, maxResults).map((email) => email.id);
|
|
414
|
-
const fullMessages = await this.getEmailsByIds(messageIds);
|
|
415
|
-
// Apply remaining criteria filtering
|
|
416
|
-
let messages = this.applyManualFiltering(fullMessages, criteria);
|
|
417
|
-
// Apply maxResults limit
|
|
418
|
-
const limitedMessages = messages.slice(0, maxResults);
|
|
419
|
-
logger.log(`Enhanced name search found ${limitedMessages.length} emails matching "${criteria.from}"`);
|
|
420
|
-
const result_final = {
|
|
421
|
-
messages: limitedMessages,
|
|
422
|
-
hasMore: messages.length > maxResults || nameMatches.length > maxResults
|
|
423
|
-
};
|
|
424
|
-
this.setCachedResults(cacheKey, result_final);
|
|
425
|
-
return result_final;
|
|
426
|
-
}
|
|
427
|
-
catch (error) {
|
|
428
|
-
logger.error('Error in enhanced name search, falling back to standard search:', error);
|
|
429
|
-
// Fall through to standard search logic
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
// Standard search logic (for email addresses and other criteria)
|
|
433
|
-
const searchQuery = this.buildSearchQuery(criteria);
|
|
434
|
-
const filterQuery = this.buildFilterQuery(criteria);
|
|
435
|
-
// Debug logging
|
|
436
|
-
if (searchQuery) {
|
|
437
|
-
logger.log(`Search query: ${searchQuery}`);
|
|
438
|
-
}
|
|
439
|
-
if (filterQuery) {
|
|
440
|
-
logger.log(`Filter query: ${filterQuery}`);
|
|
441
|
-
}
|
|
442
|
-
let apiCall;
|
|
443
|
-
let useSearchAPI = !!searchQuery;
|
|
438
|
+
// For complex searches, use a simpler approach
|
|
444
439
|
try {
|
|
445
|
-
//
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
.select('id,subject,from,toRecipients,ccRecipients,receivedDateTime,sentDateTime,bodyPreview,isRead,hasAttachments,importance,conversationId,parentFolderId,webLink')
|
|
451
|
-
.header('ConsistencyLevel', 'eventual')
|
|
452
|
-
.search(searchQuery)
|
|
453
|
-
.top(criteria.maxResults || 50);
|
|
454
|
-
// Can't use filter with search, so we'll need to filter results manually if needed
|
|
455
|
-
}
|
|
456
|
-
else {
|
|
457
|
-
// Use filter API when we don't have search terms (orderby allowed)
|
|
458
|
-
apiCall = graphClient.api('/me/messages')
|
|
459
|
-
.select('id,subject,from,toRecipients,ccRecipients,receivedDateTime,sentDateTime,bodyPreview,isRead,hasAttachments,importance,conversationId,parentFolderId,webLink')
|
|
460
|
-
.orderby('receivedDateTime desc')
|
|
461
|
-
.top(criteria.maxResults || 50);
|
|
462
|
-
if (filterQuery) {
|
|
463
|
-
apiCall = apiCall.filter(filterQuery);
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
// Handle folder-specific searches
|
|
467
|
-
if (criteria.folder && criteria.folder !== 'inbox') {
|
|
468
|
-
if (searchQuery) {
|
|
469
|
-
// For folder + search, we need to use a different approach
|
|
470
|
-
apiCall = graphClient.api(`/me/mailFolders/${criteria.folder}/messages`)
|
|
471
|
-
.select('id,subject,from,toRecipients,ccRecipients,receivedDateTime,sentDateTime,bodyPreview,isRead,hasAttachments,importance,conversationId,parentFolderId,webLink')
|
|
472
|
-
.top(criteria.maxResults || 50);
|
|
473
|
-
if (filterQuery) {
|
|
474
|
-
apiCall = apiCall.filter(filterQuery);
|
|
475
|
-
}
|
|
476
|
-
// Note: Folder-specific search is limited, we'll do text filtering on results
|
|
477
|
-
}
|
|
478
|
-
else {
|
|
479
|
-
apiCall = graphClient.api(`/me/mailFolders/${criteria.folder}/messages`)
|
|
480
|
-
.select('id,subject,from,toRecipients,ccRecipients,receivedDateTime,sentDateTime,bodyPreview,isRead,hasAttachments,importance,conversationId,parentFolderId,webLink')
|
|
481
|
-
.orderby('receivedDateTime desc')
|
|
482
|
-
.top(criteria.maxResults || 50);
|
|
483
|
-
if (filterQuery) {
|
|
484
|
-
apiCall = apiCall.filter(filterQuery);
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
}
|
|
440
|
+
// Start with a basic query to get recent emails
|
|
441
|
+
const apiCall = graphClient.api('/me/messages')
|
|
442
|
+
.select('id,subject,from,toRecipients,ccRecipients,receivedDateTime,sentDateTime,bodyPreview,isRead,hasAttachments,importance,conversationId,parentFolderId,webLink')
|
|
443
|
+
.orderby('receivedDateTime desc')
|
|
444
|
+
.top(criteria.maxResults || 100);
|
|
488
445
|
const result = await apiCall.get();
|
|
489
446
|
let messages = result.value?.map((email) => ({
|
|
490
447
|
id: email.id,
|
|
@@ -511,79 +468,34 @@ export class MS365Operations {
|
|
|
511
468
|
parentFolderId: email.parentFolderId || '',
|
|
512
469
|
webLink: email.webLink || ''
|
|
513
470
|
})) || [];
|
|
514
|
-
// Apply manual filtering
|
|
515
|
-
|
|
516
|
-
|
|
471
|
+
// Apply manual filtering for all criteria
|
|
472
|
+
messages = this.applyManualFiltering(messages, criteria);
|
|
473
|
+
// For emails with attachments, get attachment counts
|
|
474
|
+
for (const message of messages) {
|
|
475
|
+
if (message.hasAttachments) {
|
|
476
|
+
try {
|
|
477
|
+
const attachments = await graphClient
|
|
478
|
+
.api(`/me/messages/${message.id}/attachments`)
|
|
479
|
+
.select('id')
|
|
480
|
+
.get();
|
|
481
|
+
message.attachments = new Array(attachments.value?.length || 0);
|
|
482
|
+
}
|
|
483
|
+
catch (error) {
|
|
484
|
+
logger.error(`Error getting attachment count for message ${message.id}:`, error);
|
|
485
|
+
message.attachments = [];
|
|
486
|
+
}
|
|
487
|
+
}
|
|
517
488
|
}
|
|
518
|
-
const
|
|
489
|
+
const searchResult = {
|
|
519
490
|
messages,
|
|
520
491
|
hasMore: !!result['@odata.nextLink']
|
|
521
492
|
};
|
|
522
|
-
this.setCachedResults(cacheKey,
|
|
523
|
-
return
|
|
493
|
+
this.setCachedResults(cacheKey, searchResult);
|
|
494
|
+
return searchResult;
|
|
524
495
|
}
|
|
525
|
-
catch (
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
logger.log('Search API failed with syntax error, falling back to filter-only query');
|
|
529
|
-
// Fallback: Use only filter-based query without search
|
|
530
|
-
apiCall = graphClient.api('/me/messages')
|
|
531
|
-
.select('id,subject,from,toRecipients,ccRecipients,receivedDateTime,sentDateTime,bodyPreview,isRead,hasAttachments,importance,conversationId,parentFolderId,webLink')
|
|
532
|
-
.orderby('receivedDateTime desc')
|
|
533
|
-
.top(criteria.maxResults || 50);
|
|
534
|
-
if (filterQuery) {
|
|
535
|
-
apiCall = apiCall.filter(filterQuery);
|
|
536
|
-
}
|
|
537
|
-
// Handle folder-specific queries
|
|
538
|
-
if (criteria.folder && criteria.folder !== 'inbox') {
|
|
539
|
-
apiCall = graphClient.api(`/me/mailFolders/${criteria.folder}/messages`)
|
|
540
|
-
.select('id,subject,from,toRecipients,ccRecipients,receivedDateTime,sentDateTime,bodyPreview,isRead,hasAttachments,importance,conversationId,parentFolderId,webLink')
|
|
541
|
-
.orderby('receivedDateTime desc')
|
|
542
|
-
.top(criteria.maxResults || 50);
|
|
543
|
-
if (filterQuery) {
|
|
544
|
-
apiCall = apiCall.filter(filterQuery);
|
|
545
|
-
}
|
|
546
|
-
}
|
|
547
|
-
const result = await apiCall.get();
|
|
548
|
-
let messages = result.value?.map((email) => ({
|
|
549
|
-
id: email.id,
|
|
550
|
-
subject: email.subject || '',
|
|
551
|
-
from: {
|
|
552
|
-
name: email.from?.emailAddress?.name || '',
|
|
553
|
-
address: email.from?.emailAddress?.address || ''
|
|
554
|
-
},
|
|
555
|
-
toRecipients: email.toRecipients?.map((recipient) => ({
|
|
556
|
-
name: recipient.emailAddress?.name || '',
|
|
557
|
-
address: recipient.emailAddress?.address || ''
|
|
558
|
-
})) || [],
|
|
559
|
-
ccRecipients: email.ccRecipients?.map((recipient) => ({
|
|
560
|
-
name: recipient.emailAddress?.name || '',
|
|
561
|
-
address: recipient.emailAddress?.address || ''
|
|
562
|
-
})) || [],
|
|
563
|
-
receivedDateTime: email.receivedDateTime,
|
|
564
|
-
sentDateTime: email.sentDateTime,
|
|
565
|
-
bodyPreview: email.bodyPreview || '',
|
|
566
|
-
isRead: email.isRead || false,
|
|
567
|
-
hasAttachments: email.hasAttachments || false,
|
|
568
|
-
importance: email.importance || 'normal',
|
|
569
|
-
conversationId: email.conversationId || '',
|
|
570
|
-
parentFolderId: email.parentFolderId || '',
|
|
571
|
-
webLink: email.webLink || ''
|
|
572
|
-
})) || [];
|
|
573
|
-
// Apply manual filtering for any criteria that couldn't be handled by the filter
|
|
574
|
-
// This includes subject, to, cc, and query filters that need manual processing
|
|
575
|
-
messages = this.applyManualFiltering(messages, criteria);
|
|
576
|
-
const fallbackResult = {
|
|
577
|
-
messages,
|
|
578
|
-
hasMore: !!result['@odata.nextLink']
|
|
579
|
-
};
|
|
580
|
-
this.setCachedResults(cacheKey, fallbackResult);
|
|
581
|
-
return fallbackResult;
|
|
582
|
-
}
|
|
583
|
-
else {
|
|
584
|
-
// Re-throw other errors
|
|
585
|
-
throw searchError;
|
|
586
|
-
}
|
|
496
|
+
catch (error) {
|
|
497
|
+
logger.error('Error in email search:', error);
|
|
498
|
+
throw error;
|
|
587
499
|
}
|
|
588
500
|
}, 'searchEmails');
|
|
589
501
|
}
|
|
@@ -935,53 +847,55 @@ export class MS365Operations {
|
|
|
935
847
|
* Get multiple emails by their IDs efficiently
|
|
936
848
|
*/
|
|
937
849
|
async getEmailsByIds(messageIds) {
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
const
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
}
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
850
|
+
const emails = [];
|
|
851
|
+
const batchSize = 20;
|
|
852
|
+
for (let i = 0; i < messageIds.length; i += batchSize) {
|
|
853
|
+
const batch = messageIds.slice(i, i + batchSize);
|
|
854
|
+
const batchPromises = batch.map(async (id) => {
|
|
855
|
+
try {
|
|
856
|
+
const graphClient = await this.getGraphClient();
|
|
857
|
+
const email = await graphClient
|
|
858
|
+
.api(`/me/messages/${id}`)
|
|
859
|
+
.select('id,subject,from,toRecipients,ccRecipients,receivedDateTime,sentDateTime,bodyPreview,isRead,hasAttachments,importance,conversationId,parentFolderId,webLink')
|
|
860
|
+
.get();
|
|
861
|
+
const emailInfo = {
|
|
862
|
+
id: email.id,
|
|
863
|
+
subject: email.subject || '',
|
|
864
|
+
from: {
|
|
865
|
+
name: email.from?.emailAddress?.name || '',
|
|
866
|
+
address: email.from?.emailAddress?.address || ''
|
|
867
|
+
},
|
|
868
|
+
toRecipients: email.toRecipients?.map((recipient) => ({
|
|
869
|
+
name: recipient.emailAddress?.name || '',
|
|
870
|
+
address: recipient.emailAddress?.address || ''
|
|
871
|
+
})) || [],
|
|
872
|
+
ccRecipients: email.ccRecipients?.map((recipient) => ({
|
|
873
|
+
name: recipient.emailAddress?.name || '',
|
|
874
|
+
address: recipient.emailAddress?.address || ''
|
|
875
|
+
})) || [],
|
|
876
|
+
receivedDateTime: email.receivedDateTime,
|
|
877
|
+
sentDateTime: email.sentDateTime,
|
|
878
|
+
bodyPreview: email.bodyPreview || '',
|
|
879
|
+
isRead: email.isRead || false,
|
|
880
|
+
hasAttachments: email.hasAttachments || false,
|
|
881
|
+
importance: email.importance || 'normal',
|
|
882
|
+
conversationId: email.conversationId || '',
|
|
883
|
+
parentFolderId: email.parentFolderId || '',
|
|
884
|
+
webLink: email.webLink || '',
|
|
885
|
+
attachments: [] // Initialize empty attachments array
|
|
886
|
+
};
|
|
887
|
+
return emailInfo;
|
|
888
|
+
}
|
|
889
|
+
catch (error) {
|
|
890
|
+
logger.error(`Error getting email ${id}:`, error);
|
|
891
|
+
return null;
|
|
892
|
+
}
|
|
893
|
+
});
|
|
894
|
+
const batchResults = await Promise.all(batchPromises);
|
|
895
|
+
const batchEmails = batchResults.filter((email) => email !== null);
|
|
896
|
+
emails.push(...batchEmails);
|
|
984
897
|
}
|
|
898
|
+
return emails;
|
|
985
899
|
}
|
|
986
900
|
/**
|
|
987
901
|
* Clear cached graph client (used when authentication fails)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ms365-mcp-server",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.4",
|
|
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",
|
|
@@ -30,19 +30,21 @@
|
|
|
30
30
|
],
|
|
31
31
|
"license": "MIT",
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@modelcontextprotocol/sdk": "^1.10.1",
|
|
34
33
|
"@azure/msal-node": "^2.6.6",
|
|
35
34
|
"@microsoft/microsoft-graph-client": "^3.0.7",
|
|
35
|
+
"@modelcontextprotocol/sdk": "^1.10.1",
|
|
36
|
+
"express": "^5.1.0",
|
|
36
37
|
"isomorphic-fetch": "^3.0.0",
|
|
37
|
-
"open": "^10.1.0",
|
|
38
|
-
"mime-types": "^2.1.35",
|
|
39
38
|
"keytar": "^7.9.0",
|
|
39
|
+
"mime-types": "^2.1.35",
|
|
40
|
+
"open": "^10.1.0",
|
|
40
41
|
"typescript": "^5.0.4"
|
|
41
42
|
},
|
|
42
43
|
"devDependencies": {
|
|
43
|
-
"@types/
|
|
44
|
-
"@types/mime-types": "^2.1.4",
|
|
44
|
+
"@types/express": "^5.0.3",
|
|
45
45
|
"@types/isomorphic-fetch": "^0.0.36",
|
|
46
|
+
"@types/mime-types": "^2.1.4",
|
|
47
|
+
"@types/node": "^20.2.3",
|
|
46
48
|
"ts-node": "^10.9.1",
|
|
47
49
|
"tsx": "^4.19.4"
|
|
48
50
|
},
|