ms365-mcp-server 1.1.3 → 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.
Files changed (3) hide show
  1. package/README.md +40 -0
  2. package/dist/index.js +182 -21
  3. 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.3"
63
+ version: "1.1.4"
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);
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) {
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.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
  },