UNPKG

ms365-mcp-server

Version:

Microsoft 365 MCP Server for managing Microsoft 365 email through natural language interactions with full OAuth2 authentication support

957 lines (956 loc) • 63.1 kB
/** * Microsoft 365 MCP Server * A comprehensive server implementation using Model Context Protocol for Microsoft 365 API */ import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import express from 'express'; import cors from 'cors'; import path from 'path'; import { fileURLToPath } from 'url'; import fs from 'fs/promises'; import crypto from 'crypto'; import { CallToolRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ListPromptsRequestSchema, ReadResourceRequestSchema, } from '@modelcontextprotocol/sdk/types.js'; import { logger } from './utils/api.js'; import { MS365Operations } from './utils/ms365-operations.js'; import { multiUserMS365Auth } from './utils/multi-user-auth.js'; import { enhancedMS365Auth } from './utils/ms365-auth-enhanced.js'; // Create singleton MS365Operations instance const ms365Ops = new MS365Operations(); let ms365Config = { setupAuth: false, resetAuth: false, debug: false, nonInteractive: false, multiUser: false, login: false, logout: false, verifyLogin: false, serverUrl: process.env.SERVER_URL || 'http://localhost:55000' }; function parseArgs() { const args = process.argv.slice(2); const config = { setupAuth: false, resetAuth: false, debug: false, nonInteractive: false, multiUser: false, login: false, logout: false, verifyLogin: false, serverUrl: process.env.SERVER_URL || 'http://localhost:55000' }; for (let i = 0; i < args.length; i++) { const arg = args[i]; if (arg === '--setup-auth') config.setupAuth = true; else if (arg === '--reset-auth') config.resetAuth = true; else if (arg === '--debug') config.debug = true; else if (arg === '--non-interactive' || arg === '-n') config.nonInteractive = true; else if (arg === '--multi-user') config.multiUser = true; else if (arg === '--login') config.login = true; else if (arg === '--logout') config.logout = true; else if (arg === '--verify-login') config.verifyLogin = true; else if (arg === '--server-url' && i + 1 < args.length) { config.serverUrl = args[++i]; } } return config; } const server = new Server({ name: "ms365-mcp-server", version: "1.1.9" }, { capabilities: { resources: { read: true, list: true }, tools: { list: true, call: true }, prompts: { list: true } } }); // Set up the resource listing request handler server.setRequestHandler(ListResourcesRequestSchema, async () => { logger.log('Received list resources request'); return { resources: [] }; }); /** * Handler for reading resource information. */ server.setRequestHandler(ReadResourceRequestSchema, async (request) => { logger.log('Received read resource request: ' + JSON.stringify(request)); throw new Error("Resource reading not implemented"); }); /** * Handler for listing available prompts. */ server.setRequestHandler(ListPromptsRequestSchema, async () => { logger.log('Received list prompts request'); return { prompts: [] }; }); /** * List available tools for interacting with Microsoft 365. */ server.setRequestHandler(ListToolsRequestSchema, async () => { const baseTools = [ { name: "send_email", description: "Send an email message with support for HTML content, attachments, and international characters. Supports both plain text and HTML emails with proper formatting.", inputSchema: { type: "object", properties: { userId: { type: "string", description: "User ID for multi-user authentication (required if using multi-user mode)" }, to: { type: "array", items: { type: "string" }, description: "List of recipient email addresses" }, cc: { type: "array", items: { type: "string" }, description: "List of CC recipient email addresses (optional)" }, bcc: { type: "array", items: { type: "string" }, description: "List of BCC recipient email addresses (optional)" }, subject: { type: "string", description: "Email subject line with full support for international characters" }, body: { type: "string", description: "Email content (text or HTML based on bodyType)" }, bodyType: { type: "string", enum: ["text", "html"], description: "Content type of the email body (default: text)", default: "text" }, replyTo: { type: "string", description: "Reply-to email address (optional)" }, importance: { type: "string", enum: ["low", "normal", "high"], description: "Email importance level (default: normal)", default: "normal" }, attachments: { type: "array", items: { type: "object", properties: { name: { type: "string", description: "Name of the attachment file" }, contentBytes: { type: "string", description: "Base64 encoded content of the attachment" }, contentType: { type: "string", description: "MIME type of the attachment (optional, auto-detected if not provided)" } }, required: ["name", "contentBytes"] }, description: "List of email attachments (optional)" } }, required: ["to", "subject"], additionalProperties: false } }, { name: "manage_email", description: "UNIFIED EMAIL MANAGEMENT: Read, search, list, mark, move, or delete emails. Combines all email operations in one powerful tool. Supports intelligent partial name matching, folder management, and advanced search criteria.", inputSchema: { type: "object", properties: { userId: { type: "string", description: "User ID for multi-user authentication (required if using multi-user mode)" }, action: { type: "string", enum: ["read", "search", "list", "mark", "move", "delete", "search_to_me", "draft"], description: "Action to perform: read (get email by ID), search (find emails), list (folder contents), mark (read/unread), move (to folder), delete (permanently), search_to_me (emails addressed to you), draft (create/save draft)" }, messageId: { type: "string", description: "Email ID (required for: read, mark, move, delete)" }, includeAttachments: { type: "boolean", description: "Include attachment info when reading email", default: false }, isRead: { type: "boolean", description: "Mark as read (true) or unread (false) - used with mark action" }, destinationFolderId: { type: "string", description: "Destination folder for move action (e.g., 'archive', 'drafts', 'deleteditems')" }, folderId: { type: "string", description: "Folder to list emails from (default: inbox) - used with list action", default: "inbox" }, query: { type: "string", description: "General search query using natural language or specific terms" }, from: { type: "string", description: "Search emails from specific sender (supports partial names like 'John' or 'Smith')" }, to: { type: "string", description: "Search emails to specific recipient" }, cc: { type: "string", description: "Search emails with specific CC recipient" }, subject: { type: "string", description: "Search emails with specific subject" }, after: { type: "string", description: "Search emails after date (format: YYYY-MM-DD)" }, before: { type: "string", description: "Search emails before date (format: YYYY-MM-DD)" }, hasAttachment: { type: "boolean", description: "Filter emails that have attachments" }, folder: { type: "string", description: "Search within specific folder (default: inbox)" }, isUnread: { type: "boolean", description: "Filter for unread emails only" }, importance: { type: "string", enum: ["low", "normal", "high"], description: "Filter by email importance level" }, maxResults: { type: "number", description: "Maximum number of results to return (default: 50, max: 200)", minimum: 1, maximum: 200, default: 50 }, // Draft email parameters draftTo: { type: "array", items: { type: "string" }, description: "List of recipient email addresses (required for draft action)" }, draftCc: { type: "array", items: { type: "string" }, description: "List of CC recipient email addresses (optional for draft action)" }, draftBcc: { type: "array", items: { type: "string" }, description: "List of BCC recipient email addresses (optional for draft action)" }, draftSubject: { type: "string", description: "Email subject line (required for draft action)" }, draftBody: { type: "string", description: "Email content (required for draft action)" }, draftBodyType: { type: "string", enum: ["text", "html"], description: "Content type of the email body for draft (default: text)", default: "text" }, draftImportance: { type: "string", enum: ["low", "normal", "high"], description: "Email importance level for draft (default: normal)", default: "normal" }, draftAttachments: { type: "array", items: { type: "object", properties: { name: { type: "string", description: "Name of the attachment file" }, contentBytes: { type: "string", description: "Base64 encoded content of the attachment" }, contentType: { type: "string", description: "MIME type of the attachment (optional, auto-detected if not provided)" } }, required: ["name", "contentBytes"] }, description: "List of email attachments for draft (optional)" } }, required: ["action"], additionalProperties: false } }, { name: "get_attachment", description: "Download and retrieve attachment information from an email. Returns attachment data, filename, and metadata.", inputSchema: { type: "object", properties: { userId: { type: "string", description: "User ID for multi-user authentication (required if using multi-user mode)" }, messageId: { type: "string", description: "Microsoft 365 message ID containing the attachment" }, attachmentId: { type: "string", description: "Attachment ID to download" } }, required: ["messageId", "attachmentId"], additionalProperties: false } }, { name: "list_folders", description: "List all mail folders in the mailbox. Returns folder names, IDs, and item counts for navigation and organization.", inputSchema: { type: "object", properties: { userId: { type: "string", description: "User ID for multi-user authentication (required if using multi-user mode)" } }, additionalProperties: false } }, { name: "manage_contacts", description: "UNIFIED CONTACT MANAGEMENT: Get all contacts or search contacts by name/email. Combines contact operations in one tool.", inputSchema: { type: "object", properties: { userId: { type: "string", description: "User ID for multi-user authentication (required if using multi-user mode)" }, action: { type: "string", enum: ["list", "search"], description: "Action: list (get all contacts) or search (find specific contacts)", default: "list" }, query: { type: "string", description: "Search query for contact name or email address (required for search action)" }, maxResults: { type: "number", description: "Maximum number of contacts to return (default: 100 for list, 50 for search, max: 500)", minimum: 1, maximum: 500 } }, additionalProperties: false } } ]; // Add multi-user specific tools const multiUserTools = [ { name: "authenticate_user", description: "Start authentication flow for a new user. Returns authentication URL that user needs to visit for Microsoft 365 access.", inputSchema: { type: "object", properties: { userEmail: { type: "string", description: "User's email address (optional, for identification)" } }, additionalProperties: false } }, { name: "remove_my_session", description: "Remove your own authentication session (user-specific, secure).", inputSchema: { type: "object", properties: { userId: { type: "string", description: "Your User ID to remove" } }, required: ["userId"], additionalProperties: false } } ]; // Consolidated authentication tools const authTools = [ { name: "authenticate", description: "UNIFIED AUTHENTICATION: Handle all Microsoft 365 authentication needs. Supports device code flow, status checking, and logout. RECOMMENDED for all auth operations.", inputSchema: { type: "object", properties: { action: { type: "string", enum: ["login", "status", "logout", "device_code", "check_pending"], description: "Auth action: login (device code auth), status (check auth status), logout (clear tokens), device_code (get device code only), check_pending (check pending auth)", default: "login" }, force: { type: "boolean", description: "Force new authentication even if already authenticated (for login action)", default: false }, accountKey: { type: "string", description: "Account key for logout action (default: current user)", default: "default-user" } }, additionalProperties: false } } ]; if (ms365Config.multiUser) { return { tools: [...baseTools, ...multiUserTools] }; } // For single-user mode, always include auth tools return { tools: [...baseTools, ...authTools] }; }); /** * Handle tool execution requests */ server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { switch (name) { // ============ UNIFIED AUTHENTICATION TOOL ============ case "authenticate": const action = args?.action || 'login'; switch (action) { case "login": try { // Check if already authenticated if (await enhancedMS365Auth.isAuthenticated() && !args?.force) { return { content: [ { type: "text", text: `āœ… Already authenticated with Microsoft 365! Use force: true to re-authenticate.` } ] }; } // First, try to complete any existing pending authentication const existingResult = await enhancedMS365Auth.completeDeviceCodeAuth(); if (existingResult) { const currentUser = await enhancedMS365Auth.getCurrentUser(); return { content: [ { type: "text", text: `āœ… Device code authentication completed successfully!\n\nšŸ‘¤ User: ${currentUser || 'authenticated-user'}\nšŸ” Status: Valid\n\nšŸš€ You can now use all Microsoft 365 email features!` } ] }; } // Check if there's already a pending device code let deviceCodeInfo = await enhancedMS365Auth.getPendingDeviceCodeInfo(); if (!deviceCodeInfo || args?.force) { // No pending code or force new one, start fresh deviceCodeInfo = await enhancedMS365Auth.startDeviceCodeAuth(); } // Return device code information immediately (MCP pattern) return { content: [ { type: "text", text: `šŸ” Microsoft 365 Device Code Authentication\n\nšŸ“± Visit: ${deviceCodeInfo.verificationUri}\nšŸ”‘ Enter this code: ${deviceCodeInfo.userCode}\n\nā³ After completing authentication in your browser, call this tool again to finish the process.\n\nšŸ’” This code will expire in 15 minutes.` } ] }; } catch (error) { return { content: [ { type: "text", text: `āŒ Authentication failed: ${error.message}\n\nšŸ’” Try again or use CLI: node dist/index.js --login` } ] }; } case "status": const isAuthenticated = await enhancedMS365Auth.isAuthenticated(); const currentUser = await enhancedMS365Auth.getCurrentUser(); const storageInfo = enhancedMS365Auth.getStorageInfo(); const tokenInfo = await enhancedMS365Auth.getTokenExpirationInfo(); let statusText = `šŸ“Š Microsoft 365 Authentication Status\n\nšŸ” Authentication: ${isAuthenticated ? 'āœ… Valid' : 'āŒ Not authenticated'}\nšŸ‘¤ Current User: ${currentUser || 'None'}\nšŸ’¾ Storage method: ${storageInfo.method}\nšŸ“ Storage location: ${storageInfo.location}`; if (isAuthenticated) { statusText += `\nā° Token expires in: ${tokenInfo.expiresInMinutes} minutes`; if (tokenInfo.needsRefresh) { statusText += `\nāš ļø Token will be refreshed automatically on next operation`; } } else { statusText += `\nšŸ’” Use "authenticate" with action: login to sign in`; } return { content: [ { type: "text", text: statusText } ] }; case "logout": const accountKey = args?.accountKey; const wasAuthenticated = await enhancedMS365Auth.isAuthenticated(); await enhancedMS365Auth.resetAuth(); return { content: [ { type: "text", text: wasAuthenticated ? `āœ… Successfully logged out from Microsoft 365.` : `ā„¹ļø No active authentication found.` } ] }; case "device_code": const deviceCodeInfo = await enhancedMS365Auth.getDeviceCodeInfo(); return { content: [ { type: "text", text: `šŸ” Microsoft 365 Device Code Authentication\n\nšŸ“± Visit: ${deviceCodeInfo.verificationUri}\nšŸ”‘ Enter code: ${deviceCodeInfo.userCode}\n\nā³ This code will expire in 15 minutes.` } ] }; case "check_pending": const pendingDeviceCodeState = await enhancedMS365Auth.getPendingDeviceCodeInfo(); if (pendingDeviceCodeState) { return { content: [ { type: "text", text: `ā³ Pending Device Code Authentication\n\nšŸ“± Visit: ${pendingDeviceCodeState.verificationUri}\nšŸ”‘ Enter this code: ${pendingDeviceCodeState.userCode}\n\nšŸ’” Use "authenticate" with action: login to finish authentication after entering the code.` } ] }; } return { content: [ { type: "text", text: "ā„¹ļø No pending device code authentication found. Use 'authenticate' with action: login to start a new authentication process." } ] }; default: throw new Error(`Unknown authentication action: ${action}`); } // ============ UNIFIED EMAIL MANAGEMENT TOOL ============ case "manage_email": if (ms365Config.multiUser) { const userId = args?.userId; if (!userId) { throw new Error("User ID is required in multi-user mode"); } const graphClient = await multiUserMS365Auth.getGraphClientForUser(userId); ms365Ops.setGraphClient(graphClient); } else { const graphClient = await enhancedMS365Auth.getGraphClient(); ms365Ops.setGraphClient(graphClient); } const emailAction = args?.action; switch (emailAction) { case "read": if (!args?.messageId) { throw new Error("messageId is required for read action"); } const email = await ms365Ops.getEmail(args.messageId, args?.includeAttachments); let emailDetailsText = `šŸ“§ Email Details\n\nšŸ“‹ Subject: ${email.subject}\nšŸ‘¤ From: ${email.from.name} <${email.from.address}>\nšŸ“… Date: ${email.receivedDateTime}\n`; if (email.attachments && email.attachments.length > 0) { emailDetailsText += `\nšŸ“Ž Attachments (${email.attachments.length}):\n`; email.attachments.forEach((attachment, index) => { const sizeInMB = (attachment.size / (1024 * 1024)).toFixed(2); emailDetailsText += `\n${index + 1}. ${attachment.name}\n`; emailDetailsText += ` šŸ“¦ Size: ${sizeInMB} MB\n`; emailDetailsText += ` šŸ“„ Type: ${attachment.contentType}\n`; emailDetailsText += ` šŸ†” ID: ${attachment.id}\n`; if (attachment.isInline) { emailDetailsText += ` šŸ“Œ Inline: Yes\n`; } }); emailDetailsText += `\nšŸ’” To download an attachment, use the get_attachment tool with:\n`; emailDetailsText += ` • messageId: ${email.id}\n`; emailDetailsText += ` • attachmentId: (use the ID from above)\n`; } else { emailDetailsText += `\nšŸ“Ž No attachments`; } emailDetailsText += `\n\nšŸ’¬ Body:\n${email.body || email.bodyPreview}`; return { content: [ { type: "text", text: emailDetailsText } ] }; case "search": const searchResults = await ms365Ops.searchEmails(args); // Enhanced feedback for search results let responseText = `šŸ” Email Search Results (${searchResults.messages.length} found)`; if (searchResults.messages.length === 0) { responseText = `šŸ” No emails found matching your criteria.\n\nšŸ’” Search Tips:\n`; if (args?.from) { responseText += `• Try partial names: "${args.from.split(' ')[0]}" or "${args.from.split(' ').pop()}"\n`; responseText += `• Check spelling of sender name\n`; } if (args?.subject) { responseText += `• Try broader subject terms\n`; } if (args?.after || args?.before) { responseText += `• Try expanding date range\n`; } responseText += `• Remove some search criteria to get broader results`; } else { responseText += `\n\n${searchResults.messages.map((email, index) => `${index + 1}. šŸ“§ ${email.subject}\n šŸ‘¤ From: ${email.from.name} <${email.from.address}>\n šŸ“… ${new Date(email.receivedDateTime).toLocaleDateString()}\n ${email.isRead ? 'šŸ“– Read' : 'šŸ“© Unread'}\n šŸ†” ID: ${email.id}\n`).join('\n')}`; if (searchResults.hasMore) { responseText += `\nšŸ’” There are more results available. Use maxResults parameter to get more emails.`; } } return { content: [ { type: "text", text: responseText } ] }; case "search_to_me": const searchToMeResults = await ms365Ops.searchEmailsToMe(args); // Process attachments for each email const processedEmails = await Promise.all(searchToMeResults.messages.map(async (email) => { try { // Get full email details including body const fullEmail = await ms365Ops.getEmail(email.id, email.hasAttachments); // Update email with body content email.body = fullEmail.body || fullEmail.bodyPreview || ''; if (email.hasAttachments && fullEmail.attachments) { // Process each attachment const processedAttachments = await Promise.all(fullEmail.attachments.map(async (attachment) => { try { // Get the actual attachment content const attachmentContent = await ms365Ops.getAttachment(email.id, attachment.id); if (attachmentContent && attachmentContent.contentBytes) { const savedFilename = await saveBase64ToFile(attachmentContent.contentBytes, attachment.name); const fileUrl = `${SERVER_URL}/attachments/${savedFilename}`; return { ...attachment, fileUrl: fileUrl }; } return attachment; } catch (error) { logger.error(`Error processing attachment ${attachment.id}:`, error); return attachment; } })); email.attachments = processedAttachments; email.artifacts = getListOfArtifacts("search_to_me", processedAttachments); } } catch (error) { logger.error(`Error processing email ${email.id}:`, error); } return email; })); return { content: [ { type: "text", text: `šŸ” Emails Addressed to You (TO & CC) - ${processedEmails.length} found\n\n${processedEmails.map((email, index) => { let emailText = `${index + 1}. šŸ“§ ${email.subject}\n šŸ‘¤ From: ${email.from.name} <${email.from.address}>\n šŸ“… ${new Date(email.receivedDateTime).toLocaleDateString()}\n šŸ†” ID: ${email.id}\n`; if (email.hasAttachments && email.attachments && email.attachments.length > 0) { emailText += ` šŸ“Ž Attachments (${email.attachments.length}):\n`; email.attachments.forEach((attachment, attIndex) => { const sizeInMB = (attachment.size / (1024 * 1024)).toFixed(2); emailText += ` ${attIndex + 1}. ${attachment.name}\n`; emailText += ` šŸ“¦ Size: ${sizeInMB} MB\n`; emailText += ` šŸ“„ Type: ${attachment.contentType}\n`; emailText += ` šŸ†” ID: ${attachment.id}\n`; if (attachment.fileUrl) { emailText += ` šŸ”— Download URL: ${attachment.fileUrl}\n`; } if (attachment.isInline) { emailText += ` šŸ“Œ Inline: Yes\n`; } }); } // Add email body with proper formatting if (email.body) { emailText += `\n šŸ’¬ Content:\n${email.body.split('\n').map((line) => ` ${line}`).join('\n')}\n`; } else { emailText += `\n šŸ’¬ Content: No content available\n`; } return emailText; }).join('\n')}` }, ...processedEmails .filter((email) => email.artifacts) .flatMap((email) => email.artifacts) ] }; case "list": const emailList = await ms365Ops.listEmails(args?.folderId, args?.maxResults); return { content: [ { type: "text", text: `šŸ“¬ Email List (${emailList.messages.length} emails)\n\n${emailList.messages.map((email, index) => `${index + 1}. šŸ“§ ${email.subject}\n šŸ‘¤ From: ${email.from.name} <${email.from.address}>\n šŸ“… ${new Date(email.receivedDateTime).toLocaleDateString()}\n ${email.isRead ? 'šŸ“–' : 'šŸ“©'} ${email.isRead ? 'Read' : 'Unread'}\n šŸ†” ID: ${email.id}\n`).join('\n')}` } ] }; case "mark": if (!args?.messageId || args?.isRead === undefined) { throw new Error("messageId and isRead are required for mark action"); } await ms365Ops.markEmail(args.messageId, args.isRead); return { content: [ { type: "text", text: `āœ… Email marked as ${args.isRead ? 'read' : 'unread'}\nšŸ†” Message ID: ${args.messageId}` } ] }; case "move": if (!args?.messageId || !args?.destinationFolderId) { throw new Error("messageId and destinationFolderId are required for move action"); } await ms365Ops.moveEmail(args.messageId, args.destinationFolderId); return { content: [ { type: "text", text: `āœ… Email moved to folder: ${args.destinationFolderId}\nšŸ†” Message ID: ${args.messageId}` } ] }; case "delete": if (!args?.messageId) { throw new Error("messageId is required for delete action"); } await ms365Ops.deleteEmail(args.messageId); return { content: [ { type: "text", text: `āœ… Email deleted permanently\nšŸ†” Message ID: ${args.messageId}` } ] }; case "draft": if (!args?.draftTo || !args?.draftSubject || !args?.draftBody) { throw new Error("draftTo, draftSubject, and draftBody are required for draft action"); } const draftResult = await ms365Ops.saveDraftEmail({ to: args.draftTo, cc: args.draftCc, bcc: args.draftBcc, subject: args.draftSubject, body: args.draftBody, bodyType: args.draftBodyType || 'text', importance: args.draftImportance || 'normal', attachments: args.draftAttachments }); return { content: [ { type: "text", text: `āœ… Draft email saved successfully!\nšŸ“§ Subject: ${args.draftSubject}\nšŸ‘„ To: ${Array.isArray(args.draftTo) ? args.draftTo.join(', ') : args.draftTo}\nšŸ†” Draft ID: ${draftResult.id}` } ] }; default: throw new Error(`Unknown email action: ${emailAction}`); } // ============ UNIFIED CONTACT MANAGEMENT TOOL ============ case "manage_contacts": if (ms365Config.multiUser) { const userId = args?.userId; if (!userId) { throw new Error("User ID is required in multi-user mode"); } const graphClient = await multiUserMS365Auth.getGraphClientForUser(userId); ms365Ops.setGraphClient(graphClient); } else { const graphClient = await enhancedMS365Auth.getGraphClient(); ms365Ops.setGraphClient(graphClient); } const contactAction = args?.action || 'list'; switch (contactAction) { case "list": const contacts = await ms365Ops.getContacts(args?.maxResults || 100); return { content: [ { type: "text", text: `šŸ‘„ Contacts (${contacts.length} found)\n\n${contacts.map((contact) => `šŸ‘¤ ${contact.displayName}\n šŸ“§ ${contact.emailAddresses?.[0]?.address || 'No email'}\n šŸ“ž ${contact.businessPhones?.[0] || 'No phone'}\n`).join('\n')}` } ] }; case "search": if (!args?.query) { throw new Error("query is required for search action"); } const searchContactResults = await ms365Ops.searchContacts(args.query, args?.maxResults || 50); return { content: [ { type: "text", text: `šŸ” Contact Search Results (${searchContactResults.length} found)\n\n${searchContactResults.map((contact) => `šŸ‘¤ ${contact.displayName}\n šŸ“§ ${contact.emailAddresses?.[0]?.address || 'No email'}\n šŸ“ž ${contact.businessPhones?.[0] || 'No phone'}\n`).join('\n')}` } ] }; default: throw new Error(`Unknown contact action: ${contactAction}`); } // ============ REMAINING ORIGINAL TOOLS ============ case "send_email": if (ms365Config.multiUser) { const userId = args?.userId; if (!userId) { throw new Error("User ID is required in multi-user mode"); } const graphClient = await multiUserMS365Auth.getGraphClientForUser(userId); ms365Ops.setGraphClient(graphClient); } else { const graphClient = await enhancedMS365Auth.getGraphClient(); ms365Ops.setGraphClient(graphClient); } const emailResult = await ms365Ops.sendEmail(args); return { content: [ { type: "text", text: `āœ… Email sent successfully!\n\nšŸ“§ To: ${Array.isArray(args?.to) ? args.to.join(', ') : args?.to}\nšŸ“‹ Subject: ${args?.subject}\nšŸ†” Message ID: ${emailResult.id}` } ] }; case "get_attachment": if (ms365Config.multiUser) { const userId = args?.userId; if (!userId) { throw new Error("User ID is required in multi-user mode"); } const graphClient = await multiUserMS365Auth.getGraphClientForUser(userId); ms365Ops.setGraphClient(graphClient); } else { const graphClient = await enhancedMS365Auth.getGraphClient(); ms365Ops.setGraphClient(graphClient); } if (!args?.messageId) { throw new Error("messageId is required"); } try { // First verify the email exists and has attachments const email = await ms365Ops.getEmail(args.messageId, true); if (!email.hasAttachments) { throw new Error("This email has no attachments"); } if (!email.attachments || email.attachments.length === 0) { throw new Error("No attachment information available for this email"); } // If specific attachmentId is provided, get that attachment if (args?.attachmentId) { const attachmentExists = email.attachments.some(att => att.id === args.attachmentId); if (!attachmentExists) { 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')}`); } const attachment = await ms365Ops.getAttachment(args.messageId, args.attachmentId); // Save the attachment to file const savedFilename = await saveBase64ToFile(attachment.contentBytes, attachment.name); const fileUrl = `${SERVER_URL}/attachments/${savedFilename}`; attachment.fileUrl = fileUrl; const artifacts = getListOfArtifacts("get_attachment", [attachment]); const content = { type: "text", 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.` }; return { content: [content, ...artifacts] }; } else { // Get all attachments const attachments = await Promise.all(email.attachments.map(async (att) => { const attachment = await ms365Ops.getAttachment(args.messageId, att.id); const savedFilename = await saveBase64ToFile(attachment.contentBytes, attachment.name); return { name: attachment.name, contentType: attachment.contentType, size: attachment.size,