ms365-mcp-server 1.1.3 → 1.1.5

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.3"
63
+ version: "1.1.5"
59
64
  }, {
60
65
  capabilities: {
61
66
  resources: {
@@ -835,8 +840,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
835
840
  const graphClient = await enhancedMS365Auth.getGraphClient();
836
841
  ms365Ops.setGraphClient(graphClient);
837
842
  }
838
- if (!args?.messageId || !args?.attachmentId) {
839
- throw new Error("Both messageId and attachmentId are required");
843
+ if (!args?.messageId) {
844
+ throw new Error("messageId is required");
840
845
  }
841
846
  try {
842
847
  // First verify the email exists and has attachments
@@ -847,25 +852,47 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
847
852
  if (!email.attachments || email.attachments.length === 0) {
848
853
  throw new Error("No attachment information available for this email");
849
854
  }
850
- // Verify the requested attachment exists
851
- const attachmentExists = email.attachments.some(att => att.id === args.attachmentId);
852
- if (!attachmentExists) {
853
- 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')}`);
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')}`);
860
+ }
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
+ };
854
895
  }
855
- // Now get the attachment
856
- const attachment = await ms365Ops.getAttachment(args.messageId, args.attachmentId);
857
- return {
858
- content: [
859
- {
860
- type: "text",
861
- text: `šŸ“Ž Attachment Downloaded\n\nšŸ“ Name: ${attachment.name}\nšŸ’¾ Size: ${attachment.size} bytes\nšŸ—‚ļø Type: ${attachment.contentType}\n\nBelow is the base64-encoded content. You can save this to a file using a script or tool that decodes base64.\n\n---BASE64 START---\n${attachment.contentBytes}\n---BASE64 END---`
862
- }
863
- ],
864
- base64Content: attachment.contentBytes,
865
- filename: attachment.name,
866
- contentType: attachment.contentType,
867
- size: attachment.size
868
- };
869
896
  }
870
897
  catch (error) {
871
898
  if (error.message.includes("not found in the store")) {
@@ -942,6 +969,140 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
942
969
  };
943
970
  }
944
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).replace(/[^a-zA-Z0-9-_]/g, '_');
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();
945
1106
  async function main() {
946
1107
  ms365Config = parseArgs();
947
1108
  if (ms365Config.setupAuth) {
@@ -810,33 +810,100 @@ export class MS365Operations {
810
810
  async searchEmailsToMe(additionalCriteria = {}) {
811
811
  try {
812
812
  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
- };
813
+ const graphClient = await this.getGraphClient();
814
+ // Create cache key from criteria
815
+ const cacheKey = JSON.stringify({ ...additionalCriteria, userEmail });
816
+ const cachedResults = this.getCachedResults(cacheKey);
817
+ if (cachedResults) {
818
+ return cachedResults;
819
+ }
820
+ try {
821
+ // Start with a basic query to get emails
822
+ const apiCall = graphClient.api('/me/messages')
823
+ .select('id,subject,from,toRecipients,ccRecipients,receivedDateTime,sentDateTime,bodyPreview,isRead,hasAttachments,importance,conversationId,parentFolderId,webLink');
824
+ // Add search query for user's email in TO or CC using proper syntax
825
+ apiCall.search(`"${userEmail}"`);
826
+ // Add simple filters that are supported by the API
827
+ if (additionalCriteria.after) {
828
+ apiCall.filter(`receivedDateTime ge ${new Date(additionalCriteria.after).toISOString()}`);
829
+ }
830
+ if (additionalCriteria.before) {
831
+ apiCall.filter(`receivedDateTime le ${new Date(additionalCriteria.before).toISOString()}`);
832
+ }
833
+ if (additionalCriteria.hasAttachment !== undefined) {
834
+ apiCall.filter(`hasAttachments eq ${additionalCriteria.hasAttachment}`);
835
+ }
836
+ if (additionalCriteria.isUnread !== undefined) {
837
+ apiCall.filter(`isRead eq ${!additionalCriteria.isUnread}`);
838
+ }
839
+ if (additionalCriteria.importance) {
840
+ apiCall.filter(`importance eq '${additionalCriteria.importance}'`);
841
+ }
842
+ // Set page size
843
+ const pageSize = Math.min(additionalCriteria.maxResults || 100, 100);
844
+ apiCall.top(pageSize);
845
+ const result = await apiCall.get();
846
+ const messages = result.value?.map((email) => ({
847
+ id: email.id,
848
+ subject: email.subject || '',
849
+ from: {
850
+ name: email.from?.emailAddress?.name || '',
851
+ address: email.from?.emailAddress?.address || ''
852
+ },
853
+ toRecipients: email.toRecipients?.map((recipient) => ({
854
+ name: recipient.emailAddress?.name || '',
855
+ address: recipient.emailAddress?.address || ''
856
+ })) || [],
857
+ ccRecipients: email.ccRecipients?.map((recipient) => ({
858
+ name: recipient.emailAddress?.name || '',
859
+ address: recipient.emailAddress?.address || ''
860
+ })) || [],
861
+ receivedDateTime: email.receivedDateTime,
862
+ sentDateTime: email.sentDateTime,
863
+ bodyPreview: email.bodyPreview || '',
864
+ isRead: email.isRead || false,
865
+ hasAttachments: email.hasAttachments || false,
866
+ importance: email.importance || 'normal',
867
+ conversationId: email.conversationId || '',
868
+ parentFolderId: email.parentFolderId || '',
869
+ webLink: email.webLink || '',
870
+ attachments: []
871
+ })) || [];
872
+ // Filter messages to only include those where the user is in TO or CC
873
+ const filteredMessages = messages.filter(message => {
874
+ const isInTo = message.toRecipients.some(recipient => recipient.address.toLowerCase() === userEmail.toLowerCase());
875
+ const isInCc = message.ccRecipients.some(recipient => recipient.address.toLowerCase() === userEmail.toLowerCase());
876
+ return isInTo || isInCc;
877
+ });
878
+ // Sort messages by receivedDateTime in descending order
879
+ filteredMessages.sort((a, b) => new Date(b.receivedDateTime).getTime() - new Date(a.receivedDateTime).getTime());
880
+ // For emails with attachments, get attachment counts
881
+ for (const message of filteredMessages) {
882
+ if (message.hasAttachments) {
883
+ try {
884
+ const attachments = await graphClient
885
+ .api(`/me/messages/${message.id}/attachments`)
886
+ .select('id')
887
+ .get();
888
+ message.attachments = new Array(attachments.value?.length || 0);
889
+ }
890
+ catch (error) {
891
+ logger.error(`Error getting attachment count for message ${message.id}:`, error);
892
+ message.attachments = [];
893
+ }
894
+ }
895
+ }
896
+ const searchResult = {
897
+ messages: filteredMessages,
898
+ hasMore: !!result['@odata.nextLink']
899
+ };
900
+ this.setCachedResults(cacheKey, searchResult);
901
+ return searchResult;
902
+ }
903
+ catch (error) {
904
+ logger.error('Error in email search:', error);
905
+ throw error;
906
+ }
840
907
  }
841
908
  catch (error) {
842
909
  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.3",
3
+ "version": "1.1.5",
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
  },