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 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.2"
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: `šŸ“§ Email Details\n\nšŸ“‹ Subject: ${email.subject}\nšŸ‘¤ From: ${email.from.name} <${email.from.address}>\nšŸ“… Date: ${email.receivedDateTime}\nšŸ“Ž Attachments: ${email.attachments?.length || 0}\n\nšŸ’¬ Body:\n${email.body || email.bodyPreview}`
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
- const attachment = await ms365Ops.getAttachment(args?.messageId, args?.attachmentId);
818
- return {
819
- content: [
820
- {
821
- type: "text",
822
- text: `šŸ“Ž Attachment Downloaded\n\nšŸ“ Name: ${attachment.name}\nšŸ’¾ Size: ${attachment.size} bytes\nšŸ—‚ļø Type: ${attachment.contentType}\n\nšŸ“„ Content available in base64 format`
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
- let selectFields = 'id,subject,from,toRecipients,ccRecipients,receivedDateTime,sentDateTime,bodyPreview,isRead,hasAttachments,importance,conversationId,parentFolderId,webLink';
322
- if (includeAttachments) {
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(selectFields)
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 (includeAttachments && email.body) {
364
+ if (email.body) {
355
365
  emailInfo.body = email.body.content || '';
356
366
  }
357
- // Get attachments if requested
358
- if (includeAttachments && email.hasAttachments) {
359
- const attachments = await graphClient
360
- .api(`/me/messages/${messageId}/attachments`)
361
- .get();
362
- emailInfo.attachments = attachments.value || [];
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 name-based searches, use enhanced approach for better partial matching
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
- // Microsoft Graph doesn't support $orderBy with $search, so we need to choose one approach
446
- if (searchQuery) {
447
- // Use search API when we have search terms (no orderby allowed)
448
- // Add ConsistencyLevel header for search queries
449
- apiCall = graphClient.api('/me/messages')
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 when using search (since we can't use $filter with $search)
515
- if (useSearchAPI && (criteria.folder || filterQuery)) {
516
- messages = this.applyManualFiltering(messages, criteria);
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 standardResult = {
489
+ const searchResult = {
519
490
  messages,
520
491
  hasMore: !!result['@odata.nextLink']
521
492
  };
522
- this.setCachedResults(cacheKey, standardResult);
523
- return standardResult;
493
+ this.setCachedResults(cacheKey, searchResult);
494
+ return searchResult;
524
495
  }
525
- catch (searchError) {
526
- // If search API fails due to syntax error, fall back to filter-only approach
527
- if (searchError.message && searchError.message.includes('Syntax error')) {
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
- try {
939
- const graphClient = await this.getGraphClient();
940
- // Batch request for better performance when getting multiple emails
941
- const emails = [];
942
- // Process in batches of 20 to stay within Graph API limits
943
- const batchSize = 20;
944
- for (let i = 0; i < messageIds.length; i += batchSize) {
945
- const batch = messageIds.slice(i, i + batchSize);
946
- // Get full details for this batch
947
- const promises = batch.map(id => graphClient
948
- .api(`/me/messages/${id}`)
949
- .select('id,subject,from,toRecipients,ccRecipients,receivedDateTime,sentDateTime,bodyPreview,isRead,hasAttachments,importance,conversationId,parentFolderId,webLink')
950
- .get());
951
- const results = await Promise.all(promises);
952
- const batchEmails = results.map((email) => ({
953
- id: email.id,
954
- subject: email.subject || '',
955
- from: {
956
- name: email.from?.emailAddress?.name || '',
957
- address: email.from?.emailAddress?.address || ''
958
- },
959
- toRecipients: email.toRecipients?.map((recipient) => ({
960
- name: recipient.emailAddress?.name || '',
961
- address: recipient.emailAddress?.address || ''
962
- })) || [],
963
- ccRecipients: email.ccRecipients?.map((recipient) => ({
964
- name: recipient.emailAddress?.name || '',
965
- address: recipient.emailAddress?.address || ''
966
- })) || [],
967
- receivedDateTime: email.receivedDateTime,
968
- sentDateTime: email.sentDateTime,
969
- bodyPreview: email.bodyPreview || '',
970
- isRead: email.isRead || false,
971
- hasAttachments: email.hasAttachments || false,
972
- importance: email.importance || 'normal',
973
- conversationId: email.conversationId || '',
974
- parentFolderId: email.parentFolderId || '',
975
- webLink: email.webLink || ''
976
- }));
977
- emails.push(...batchEmails);
978
- }
979
- return emails;
980
- }
981
- catch (error) {
982
- logger.error('Error getting emails by IDs:', error);
983
- throw error;
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.2",
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/node": "^20.2.3",
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
  },