ms365-mcp-server 1.1.4 → 1.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -60,7 +60,7 @@ function parseArgs() {
60
60
  }
61
61
  const server = new Server({
62
62
  name: "ms365-mcp-server",
63
- version: "1.1.4"
63
+ version: "1.1.6"
64
64
  }, {
65
65
  capabilities: {
66
66
  resources: {
@@ -1067,7 +1067,7 @@ function generateUniqueFilename(originalName) {
1067
1067
  const timestamp = Date.now();
1068
1068
  const randomString = crypto.randomBytes(8).toString('hex');
1069
1069
  const extension = path.extname(originalName);
1070
- const baseName = path.basename(originalName, extension);
1070
+ const baseName = path.basename(originalName, extension).replace(/[^a-zA-Z0-9-_]/g, '_');
1071
1071
  return `${baseName}-${timestamp}-${randomString}${extension}`;
1072
1072
  }
1073
1073
  // Helper function to save base64 content to file
@@ -1,4 +1,5 @@
1
1
  import { logger } from './api.js';
2
+ import mimeTypes from 'mime-types';
2
3
  /**
3
4
  * Microsoft 365 operations manager class
4
5
  */
@@ -686,19 +687,48 @@ export class MS365Operations {
686
687
  async getAttachment(messageId, attachmentId) {
687
688
  try {
688
689
  const graphClient = await this.getGraphClient();
690
+ logger.log(`Fetching attachment ${attachmentId} from message ${messageId}...`);
691
+ // First get attachment metadata
689
692
  const attachment = await graphClient
690
693
  .api(`/me/messages/${messageId}/attachments/${attachmentId}`)
691
694
  .get();
692
- return {
693
- name: attachment.name || '',
694
- contentType: attachment.contentType || 'application/octet-stream',
695
- contentBytes: attachment.contentBytes || '',
695
+ logger.log(`Retrieved attachment metadata: ${attachment.name} (${attachment.contentType}, ${attachment.size} bytes)`);
696
+ // Determine content type if not provided
697
+ let contentType = attachment.contentType;
698
+ if (!contentType && attachment.name) {
699
+ contentType = mimeTypes.lookup(attachment.name) || 'application/octet-stream';
700
+ logger.log(`Inferred content type for ${attachment.name}: ${contentType}`);
701
+ }
702
+ // Validate content bytes
703
+ if (!attachment.contentBytes) {
704
+ logger.error('No content bytes found in attachment');
705
+ throw new Error('Attachment content is empty');
706
+ }
707
+ // Return the attachment data
708
+ const result = {
709
+ name: attachment.name || 'unnamed_attachment',
710
+ contentType: contentType || 'application/octet-stream',
711
+ contentBytes: attachment.contentBytes,
696
712
  size: attachment.size || 0
697
713
  };
714
+ logger.log(`Successfully processed attachment: ${result.name} (${result.contentType}, ${result.size} bytes)`);
715
+ return result;
698
716
  }
699
717
  catch (error) {
700
718
  logger.error('Error getting attachment:', error);
701
- throw error;
719
+ // Provide more specific error messages
720
+ if (error.status === 404) {
721
+ throw new Error(`Attachment not found: ${error.message}`);
722
+ }
723
+ else if (error.status === 401) {
724
+ throw new Error('Authentication failed. Please re-authenticate.');
725
+ }
726
+ else if (error.status === 403) {
727
+ throw new Error('Permission denied to access this attachment.');
728
+ }
729
+ else {
730
+ throw new Error(`Failed to get attachment: ${error.message}`);
731
+ }
702
732
  }
703
733
  }
704
734
  /**
@@ -810,33 +840,100 @@ export class MS365Operations {
810
840
  async searchEmailsToMe(additionalCriteria = {}) {
811
841
  try {
812
842
  const userEmail = await this.getCurrentUserEmail();
813
- // First search for emails where user is in TO field
814
- const toCriteria = {
815
- ...additionalCriteria,
816
- to: userEmail
817
- };
818
- // Then search for emails where user is in CC field
819
- const ccCriteria = {
820
- ...additionalCriteria,
821
- cc: userEmail
822
- };
823
- // Execute both searches in parallel
824
- const [toResults, ccResults] = await Promise.all([
825
- this.searchEmails(toCriteria),
826
- this.searchEmails(ccCriteria)
827
- ]);
828
- // Combine results and remove duplicates based on email ID
829
- const allMessages = [...toResults.messages, ...ccResults.messages];
830
- const uniqueMessages = allMessages.filter((message, index, array) => array.findIndex(m => m.id === message.id) === index);
831
- // Sort by received date (newest first)
832
- uniqueMessages.sort((a, b) => new Date(b.receivedDateTime).getTime() - new Date(a.receivedDateTime).getTime());
833
- // Apply maxResults limit if specified
834
- const maxResults = additionalCriteria.maxResults || 50;
835
- const limitedMessages = uniqueMessages.slice(0, maxResults);
836
- return {
837
- messages: limitedMessages,
838
- hasMore: uniqueMessages.length > maxResults || toResults.hasMore || ccResults.hasMore
839
- };
843
+ const graphClient = await this.getGraphClient();
844
+ // Create cache key from criteria
845
+ const cacheKey = JSON.stringify({ ...additionalCriteria, userEmail });
846
+ const cachedResults = this.getCachedResults(cacheKey);
847
+ if (cachedResults) {
848
+ return cachedResults;
849
+ }
850
+ try {
851
+ // Start with a basic query to get emails
852
+ const apiCall = graphClient.api('/me/messages')
853
+ .select('id,subject,from,toRecipients,ccRecipients,receivedDateTime,sentDateTime,bodyPreview,isRead,hasAttachments,importance,conversationId,parentFolderId,webLink');
854
+ // Add search query for user's email in TO or CC using proper syntax
855
+ apiCall.search(`"${userEmail}"`);
856
+ // Add simple filters that are supported by the API
857
+ if (additionalCriteria.after) {
858
+ apiCall.filter(`receivedDateTime ge ${new Date(additionalCriteria.after).toISOString()}`);
859
+ }
860
+ if (additionalCriteria.before) {
861
+ apiCall.filter(`receivedDateTime le ${new Date(additionalCriteria.before).toISOString()}`);
862
+ }
863
+ if (additionalCriteria.hasAttachment !== undefined) {
864
+ apiCall.filter(`hasAttachments eq ${additionalCriteria.hasAttachment}`);
865
+ }
866
+ if (additionalCriteria.isUnread !== undefined) {
867
+ apiCall.filter(`isRead eq ${!additionalCriteria.isUnread}`);
868
+ }
869
+ if (additionalCriteria.importance) {
870
+ apiCall.filter(`importance eq '${additionalCriteria.importance}'`);
871
+ }
872
+ // Set page size
873
+ const pageSize = Math.min(additionalCriteria.maxResults || 100, 100);
874
+ apiCall.top(pageSize);
875
+ const result = await apiCall.get();
876
+ const messages = result.value?.map((email) => ({
877
+ id: email.id,
878
+ subject: email.subject || '',
879
+ from: {
880
+ name: email.from?.emailAddress?.name || '',
881
+ address: email.from?.emailAddress?.address || ''
882
+ },
883
+ toRecipients: email.toRecipients?.map((recipient) => ({
884
+ name: recipient.emailAddress?.name || '',
885
+ address: recipient.emailAddress?.address || ''
886
+ })) || [],
887
+ ccRecipients: email.ccRecipients?.map((recipient) => ({
888
+ name: recipient.emailAddress?.name || '',
889
+ address: recipient.emailAddress?.address || ''
890
+ })) || [],
891
+ receivedDateTime: email.receivedDateTime,
892
+ sentDateTime: email.sentDateTime,
893
+ bodyPreview: email.bodyPreview || '',
894
+ isRead: email.isRead || false,
895
+ hasAttachments: email.hasAttachments || false,
896
+ importance: email.importance || 'normal',
897
+ conversationId: email.conversationId || '',
898
+ parentFolderId: email.parentFolderId || '',
899
+ webLink: email.webLink || '',
900
+ attachments: []
901
+ })) || [];
902
+ // Filter messages to only include those where the user is in TO or CC
903
+ const filteredMessages = messages.filter(message => {
904
+ const isInTo = message.toRecipients.some(recipient => recipient.address.toLowerCase() === userEmail.toLowerCase());
905
+ const isInCc = message.ccRecipients.some(recipient => recipient.address.toLowerCase() === userEmail.toLowerCase());
906
+ return isInTo || isInCc;
907
+ });
908
+ // Sort messages by receivedDateTime in descending order
909
+ filteredMessages.sort((a, b) => new Date(b.receivedDateTime).getTime() - new Date(a.receivedDateTime).getTime());
910
+ // For emails with attachments, get attachment counts
911
+ for (const message of filteredMessages) {
912
+ if (message.hasAttachments) {
913
+ try {
914
+ const attachments = await graphClient
915
+ .api(`/me/messages/${message.id}/attachments`)
916
+ .select('id')
917
+ .get();
918
+ message.attachments = new Array(attachments.value?.length || 0);
919
+ }
920
+ catch (error) {
921
+ logger.error(`Error getting attachment count for message ${message.id}:`, error);
922
+ message.attachments = [];
923
+ }
924
+ }
925
+ }
926
+ const searchResult = {
927
+ messages: filteredMessages,
928
+ hasMore: !!result['@odata.nextLink']
929
+ };
930
+ this.setCachedResults(cacheKey, searchResult);
931
+ return searchResult;
932
+ }
933
+ catch (error) {
934
+ logger.error('Error in email search:', error);
935
+ throw error;
936
+ }
840
937
  }
841
938
  catch (error) {
842
939
  logger.error('Error searching emails addressed to me:', error);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ms365-mcp-server",
3
- "version": "1.1.4",
3
+ "version": "1.1.6",
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",