Claude Outlook MCP Tool

#!/usr/bin/env bun import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ListToolsRequestSchema, type Tool, } from "@modelcontextprotocol/sdk/types.js"; import { runAppleScript } from 'run-applescript'; // ==================================================== // 1. Tool Definitions // ==================================================== // Define Outlook Mail tool const OUTLOOK_MAIL_TOOL: Tool = { name: "outlook_mail", description: "Interact with Microsoft Outlook for macOS - read, search, send, and manage emails", inputSchema: { type: "object", properties: { operation: { type: "string", description: "Operation to perform: 'unread', 'search', 'send', 'folders', or 'read'", enum: ["unread", "search", "send", "folders", "read"] }, folder: { type: "string", description: "Email folder to use (optional - if not provided, uses inbox or searches across all folders)" }, limit: { type: "number", description: "Number of emails to retrieve (optional, for unread, read, and search operations)" }, searchTerm: { type: "string", description: "Text to search for in emails (required for search operation)" }, to: { type: "string", description: "Recipient email address (required for send operation)" }, subject: { type: "string", description: "Email subject (required for send operation)" }, body: { type: "string", description: "Email body content (required for send operation)" }, isHtml: { type: "boolean", description: "Whether the body content is HTML (optional for send operation, default: false)" }, cc: { type: "string", description: "CC email address (optional for send operation)" }, bcc: { type: "string", description: "BCC email address (optional for send operation)" }, attachments: { type: "array", description: "File paths to attach to the email (optional for send operation)", items: { type: "string" } } }, required: ["operation"] } }; // Define Outlook Calendar tool const OUTLOOK_CALENDAR_TOOL: Tool = { name: "outlook_calendar", description: "Interact with Microsoft Outlook for macOS calendar - view, create, and manage events", inputSchema: { type: "object", properties: { operation: { type: "string", description: "Operation to perform: 'today', 'upcoming', 'search', or 'create'", enum: ["today", "upcoming", "search", "create"] }, searchTerm: { type: "string", description: "Text to search for in events (required for search operation)" }, limit: { type: "number", description: "Number of events to retrieve (optional, for today and upcoming operations)" }, days: { type: "number", description: "Number of days to look ahead (optional, for upcoming operation, default: 7)" }, subject: { type: "string", description: "Event subject/title (required for create operation)" }, start: { type: "string", description: "Start time in ISO format (required for create operation)" }, end: { type: "string", description: "End time in ISO format (required for create operation)" }, location: { type: "string", description: "Event location (optional for create operation)" }, body: { type: "string", description: "Event description/body (optional for create operation)" }, attendees: { type: "string", description: "Comma-separated list of attendee email addresses (optional for create operation)" } }, required: ["operation"] } }; // Define Outlook Contacts tool const OUTLOOK_CONTACTS_TOOL: Tool = { name: "outlook_contacts", description: "Search and retrieve contacts from Microsoft Outlook for macOS", inputSchema: { type: "object", properties: { operation: { type: "string", description: "Operation to perform: 'list' or 'search'", enum: ["list", "search"] }, searchTerm: { type: "string", description: "Text to search for in contacts (required for search operation)" }, limit: { type: "number", description: "Number of contacts to retrieve (optional)" } }, required: ["operation"] } }; // ==================================================== // 2. Server Setup // ==================================================== console.error("Starting Outlook MCP server..."); const server = new Server( { name: "Outlook MCP Tool", version: "1.0.0", }, { capabilities: { tools: {}, }, } ); // ==================================================== // 3. Core Functions // ==================================================== // Check if Outlook is installed and running async function checkOutlookAccess(): Promise<boolean> { console.error("[checkOutlookAccess] Checking if Outlook is accessible..."); try { const isInstalled = await runAppleScript(` tell application "System Events" set outlookExists to exists application process "Microsoft Outlook" return outlookExists end tell `); if (isInstalled !== "true") { console.error("[checkOutlookAccess] Microsoft Outlook is not installed or running"); throw new Error("Microsoft Outlook is not installed or running on this system"); } const isRunning = await runAppleScript(` tell application "System Events" set outlookRunning to application process "Microsoft Outlook" exists return outlookRunning end tell `); if (isRunning !== "true") { console.error("[checkOutlookAccess] Microsoft Outlook is not running, attempting to launch..."); try { await runAppleScript(` tell application "Microsoft Outlook" to activate delay 2 `); console.error("[checkOutlookAccess] Launched Outlook successfully"); } catch (activateError) { console.error("[checkOutlookAccess] Error activating Microsoft Outlook:", activateError); throw new Error("Could not activate Microsoft Outlook. Please start it manually."); } } else { console.error("[checkOutlookAccess] Microsoft Outlook is already running"); } return true; } catch (error) { console.error("[checkOutlookAccess] Outlook access check failed:", error); throw new Error( `Cannot access Microsoft Outlook. Please make sure Outlook is installed and properly configured. Error: ${error instanceof Error ? error.message : String(error)}` ); } } // ==================================================== // 4. EMAIL FUNCTIONS // ==================================================== // Function to get unread emails async function getUnreadEmails(folder: string = "Inbox", limit: number = 10): Promise<any[]> { console.error(`[getUnreadEmails] Getting unread emails from folder: ${folder}, limit: ${limit}`); await checkOutlookAccess(); const folderPath = folder === "Inbox" ? "inbox" : folder; const script = ` tell application "Microsoft Outlook" try set theFolder to ${folderPath} -- Use the specified folder or default to inbox set unreadMessages to {} set allMessages to messages of theFolder set i to 0 repeat with theMessage in allMessages if read status of theMessage is false then set i to i + 1 set msgData to {subject:subject of theMessage, sender:sender of theMessage, ¬ date:time sent of theMessage, id:id of theMessage} -- Try to get content try set msgContent to content of theMessage if length of msgContent > 500 then set msgContent to (text 1 thru 500 of msgContent) & "..." end if set msgData to msgData & {content:msgContent} on error set msgData to msgData & {content:"[Content not available]"} end try set end of unreadMessages to msgData -- Stop if we've reached the limit if i >= ${limit} then exit repeat end if end if end repeat return unreadMessages on error errMsg return "Error: " & errMsg end try end tell `; try { const result = await runAppleScript(script); console.error(`[getUnreadEmails] Raw result length: ${result.length}`); // Parse the results (AppleScript returns records as text) if (result.startsWith("Error:")) { throw new Error(result); } // Simple parsing for demonstration // In a production environment, you'd want more robust parsing const emails = []; const matches = result.match(/\{([^}]+)\}/g); if (matches && matches.length > 0) { for (const match of matches) { try { const props = match.substring(1, match.length - 1).split(','); const email: any = {}; props.forEach(prop => { const parts = prop.split(':'); if (parts.length >= 2) { const key = parts[0].trim(); const value = parts.slice(1).join(':').trim(); email[key] = value; } }); if (email.subject || email.sender) { emails.push({ subject: email.subject || "No subject", sender: email.sender || "Unknown sender", dateSent: email.date || new Date().toString(), content: email.content || "[Content not available]", id: email.id || "" }); } } catch (parseError) { console.error('[getUnreadEmails] Error parsing email match:', parseError); } } } console.error(`[getUnreadEmails] Found ${emails.length} unread emails`); return emails; } catch (error) { console.error("[getUnreadEmails] Error getting unread emails:", error); throw error; } } // Function to search emails async function searchEmails(searchTerm: string, folder: string = "Inbox", limit: number = 10): Promise<any[]> { console.error(`[searchEmails] Searching for "${searchTerm}" in folder: ${folder}, limit: ${limit}`); await checkOutlookAccess(); const folderPath = folder === "Inbox" ? "inbox" : folder; const script = ` tell application "Microsoft Outlook" try set theFolder to ${folderPath} set searchResults to {} set allMessages to messages of theFolder set i to 0 set searchString to "${searchTerm.replace(/"/g, '\\"')}" repeat with theMessage in allMessages if (subject of theMessage contains searchString) or (content of theMessage contains searchString) then set i to i + 1 set msgData to {subject:subject of theMessage, sender:sender of theMessage, ¬ date:time sent of theMessage, id:id of theMessage} -- Try to get content try set msgContent to content of theMessage if length of msgContent > 500 then set msgContent to (text 1 thru 500 of msgContent) & "..." end if set msgData to msgData & {content:msgContent} on error set msgData to msgData & {content:"[Content not available]"} end try set end of searchResults to msgData -- Stop if we've reached the limit if i >= ${limit} then exit repeat end if end if end repeat return searchResults on error errMsg return "Error: " & errMsg end try end tell `; try { const result = await runAppleScript(script); console.error(`[searchEmails] Raw result length: ${result.length}`); // Parse the results if (result.startsWith("Error:")) { throw new Error(result); } // Parse the emails similar to unread emails const emails = []; const matches = result.match(/\{([^}]+)\}/g); if (matches && matches.length > 0) { for (const match of matches) { try { const props = match.substring(1, match.length - 1).split(','); const email: any = {}; props.forEach(prop => { const parts = prop.split(':'); if (parts.length >= 2) { const key = parts[0].trim(); const value = parts.slice(1).join(':').trim(); email[key] = value; } }); if (email.subject || email.sender) { emails.push({ subject: email.subject || "No subject", sender: email.sender || "Unknown sender", dateSent: email.date || new Date().toString(), content: email.content || "[Content not available]", id: email.id || "" }); } } catch (parseError) { console.error('[searchEmails] Error parsing email match:', parseError); } } } console.error(`[searchEmails] Found ${emails.length} matching emails`); return emails; } catch (error) { console.error("[searchEmails] Error searching emails:", error); throw error; } } async function checkAttachmentPath(filePath: string): Promise<string> { try { // Convert to absolute path if relative let fullPath = filePath; if (!filePath.startsWith('/')) { const cwd = process.cwd(); fullPath = `${cwd}/${filePath}`; } // Check if the file exists and is readable const fs = require('fs'); const { promisify } = require('util'); const access = promisify(fs.access); const stat = promisify(fs.stat); try { await access(fullPath, fs.constants.R_OK); const stats = await stat(fullPath); return `File exists and is readable: ${fullPath}\nSize: ${stats.size} bytes\nPermissions: ${stats.mode.toString(8)}\nLast modified: ${stats.mtime}`; } catch (err) { return `ERROR: Cannot access file: ${fullPath}\nError details: ${err.message}`; } } catch (error) { return `Failed to check attachment path: ${error.message}`; } } // Add a debug version of sending email with attachment to test if files are accessible async function debugSendEmailWithAttachment( to: string, subject: string, body: string, attachmentPath: string ): Promise<string> { // First check if the file exists and is readable const fileStatus = await checkAttachmentPath(attachmentPath); console.error(`[debugSendEmail] Attachment status: ${fileStatus}`); // Create a simple AppleScript that just attempts to open the file const script = ` set theFile to POSIX file "${attachmentPath.replace(/"/g, '\\"')}" try tell application "Finder" set fileExists to exists file theFile set fileInfo to info for file theFile return "File exists: " & fileExists & ", size: " & (size of fileInfo) end tell on error errMsg return "Error accessing file: " & errMsg end try `; try { const result = await runAppleScript(script); console.error(`[debugSendEmail] AppleScript file check: ${result}`); // Now try to actually create a draft with the attachment const emailScript = ` tell application "Microsoft Outlook" try set newMessage to make new outgoing message with properties {subject:"DEBUG: ${subject.replace(/"/g, '\\"')}", visible:true} set content of newMessage to "${body.replace(/"/g, '\\"')}" set to recipients of newMessage to {"${to}"} try set attachmentFile to POSIX file "${attachmentPath.replace(/"/g, '\\"')}" make new attachment at newMessage with properties {file:attachmentFile} set attachResult to "Successfully attached file" on error attachErrMsg set attachResult to "Failed to attach file: " & attachErrMsg end try return attachResult on error errMsg return "Error creating email: " & errMsg end try end tell `; const attachResult = await runAppleScript(emailScript); console.error(`[debugSendEmail] Attachment result: ${attachResult}`); return `File check: ${fileStatus}\n\nAttachment test: ${attachResult}`; } catch (error) { console.error("[debugSendEmail] Error during debug:", error); return `Debugging error: ${error.message}\n\nFile check: ${fileStatus}`; } } // Update the sendEmail function to handle attachments and HTML content async function sendEmail( to: string, subject: string, body: string, cc?: string, bcc?: string, isHtml: boolean = false, attachments?: string[] ): Promise<string> { console.error(`[sendEmail] Sending email to: ${to}, subject: "${subject}"`); console.error(`[sendEmail] Attachments: ${attachments ? JSON.stringify(attachments) : 'none'}`); await checkOutlookAccess(); // Extract name from email if possible (for display name) const extractNameFromEmail = (email: string): string => { const namePart = email.split('@')[0]; return namePart .split('.') .map(part => part.charAt(0).toUpperCase() + part.slice(1)) .join(' '); }; // Get name for display const toName = extractNameFromEmail(to); const ccName = cc ? extractNameFromEmail(cc) : ""; const bccName = bcc ? extractNameFromEmail(bcc) : ""; // Escape special characters const escapedSubject = subject.replace(/"/g, '\\"'); const escapedBody = body.replace(/"/g, '\\"').replace(/\n/g, '\\n'); // Process attachments: Convert to absolute paths if they are relative let processedAttachments: string[] = []; if (attachments && attachments.length > 0) { processedAttachments = attachments.map(path => { // Check if path is absolute (starts with /) if (path.startsWith('/')) { return path; } // Get current working directory and join with relative path const cwd = process.cwd(); return `${cwd}/${path}`; }); console.error(`[sendEmail] Processed attachments: ${JSON.stringify(processedAttachments)}`); } // Create attachment script part with better error handling const attachmentScript = processedAttachments.length > 0 ? processedAttachments.map(filePath => { const escapedPath = filePath.replace(/"/g, '\\"'); return ` try set attachmentFile to POSIX file "${escapedPath}" make new attachment at msg with properties {file:attachmentFile} log "Successfully attached file: ${escapedPath}" on error errMsg log "Failed to attach file: ${escapedPath} - Error: " & errMsg end try `; }).join('\n') : ''; // Try approach 1: Using specific syntax for creating a message with attachments try { const script1 = ` tell application "Microsoft Outlook" try set msg to make new outgoing message with properties {subject:"${escapedSubject}"} ${isHtml ? `set content type of msg to HTML set content of msg to "${escapedBody}"` : `set content of msg to "${escapedBody}"` } tell msg set recipTo to make new to recipient with properties {email address:{name:"${toName}", address:"${to}"}} ${cc ? `set recipCc to make new cc recipient with properties {email address:{name:"${ccName}", address:"${cc}"}}` : ''} ${bcc ? `set recipBcc to make new bcc recipient with properties {email address:{name:"${bccName}", address:"${bcc}"}}` : ''} ${attachmentScript} end tell -- Delay to allow attachments to be processed delay 1 send msg return "Email sent successfully with attachments" on error errMsg return "Error: " & errMsg end try end tell `; console.error("[sendEmail] Executing AppleScript method 1"); const result = await runAppleScript(script1); console.error(`[sendEmail] Result (method 1): ${result}`); if (result.startsWith("Error:")) { throw new Error(result); } return result; } catch (error1) { console.error("[sendEmail] Method 1 failed:", error1); // Try approach 2: Using AppleScript's draft window method try { const script2 = ` tell application "Microsoft Outlook" try set newDraft to make new draft window set theMessage to item 1 of mail items of newDraft set subject of theMessage to "${escapedSubject}" ${isHtml ? `set content type of theMessage to HTML set content of theMessage to "${escapedBody}"` : `set content of theMessage to "${escapedBody}"` } set to recipients of theMessage to {"${to}"} ${cc ? `set cc recipients of theMessage to {"${cc}"}` : ''} ${bcc ? `set bcc recipients of theMessage to {"${bcc}"}` : ''} ${processedAttachments.map(filePath => { const escapedPath = filePath.replace(/"/g, '\\"'); return ` try set attachmentFile to POSIX file "${escapedPath}" make new attachment at theMessage with properties {file:attachmentFile} log "Successfully attached file: ${escapedPath}" on error attachErrMsg log "Failed to attach file: ${escapedPath} - Error: " & attachErrMsg end try `; }).join('\n')} -- Delay to allow attachments to be processed delay 1 send theMessage return "Email sent successfully with method 2" on error errMsg return "Error: " & errMsg end try end tell `; console.error("[sendEmail] Executing AppleScript method 2"); const result = await runAppleScript(script2); console.error(`[sendEmail] Result (method 2): ${result}`); if (result.startsWith("Error:")) { throw new Error(result); } return result; } catch (error2) { console.error("[sendEmail] Method 2 failed:", error2); // Try approach 3: Create a draft for the user to manually send try { const script3 = ` tell application "Microsoft Outlook" try set newMessage to make new outgoing message with properties {subject:"${escapedSubject}", visible:true} ${isHtml ? `set content type of newMessage to HTML set content of newMessage to "${escapedBody}"` : `set content of newMessage to "${escapedBody}"` } set to recipients of newMessage to {"${to}"} ${cc ? `set cc recipients of newMessage to {"${cc}"}` : ''} ${bcc ? `set bcc recipients of newMessage to {"${bcc}"}` : ''} ${processedAttachments.map(filePath => { const escapedPath = filePath.replace(/"/g, '\\"'); return ` try set attachmentFile to POSIX file "${escapedPath}" make new attachment at newMessage with properties {file:attachmentFile} log "Successfully attached file: ${escapedPath}" on error attachErrMsg log "Failed to attach file: ${escapedPath} - Error: " & attachErrMsg end try `; }).join('\n')} -- Display the message activate return "Email draft created with attachments. Please review and send manually." on error errMsg return "Error: " & errMsg end try end tell `; console.error("[sendEmail] Executing AppleScript method 3"); const result = await runAppleScript(script3); console.error(`[sendEmail] Result (method 3): ${result}`); if (result.startsWith("Error:")) { throw new Error(result); } return "A draft has been created in Outlook with the content and attachments. Please review and send it manually."; } catch (error3) { console.error("[sendEmail] All methods failed:", error3); throw new Error(`Could not send or create email. Please check if Outlook is properly configured and that you have granted necessary permissions. Error details: ${error3}`); } } } } // Function to get mail folders - this works based on your logs async function getMailFolders(): Promise<string[]> { console.error("[getMailFolders] Getting mail folders"); await checkOutlookAccess(); const script = ` tell application "Microsoft Outlook" set folderNames to {} set allFolders to mail folders repeat with theFolder in allFolders set end of folderNames to name of theFolder end repeat return folderNames end tell `; try { const result = await runAppleScript(script); console.error(`[getMailFolders] Result: ${result}`); return result.split(", "); } catch (error) { console.error("[getMailFolders] Error getting mail folders:", error); throw error; } } // Function to read emails in a folder that uses simple AppleScript async function readEmails(folder: string = "Inbox", limit: number = 10): Promise<any[]> { console.error(`[readEmails] Reading emails from folder: ${folder}, limit: ${limit}`); await checkOutlookAccess(); // Use a simplified approach that should be more compatible const script = ` tell application "Microsoft Outlook" try -- Get the folder by name safely set targetFolder to null set allFolders to mail folders repeat with mailFolder in allFolders if name of mailFolder is "${folder}" then set targetFolder to mailFolder exit repeat end if end repeat if targetFolder is null then set targetFolder to inbox -- Get messages set messageList to {} set msgCount to 0 set allMsgs to messages of targetFolder repeat with i from 1 to (count of allMsgs) if msgCount >= ${limit} then exit repeat try set theMsg to item i of allMsgs set msgSubject to subject of theMsg set msgSender to sender of theMsg set msgDate to time sent of theMsg -- Create a simple text representation for the message set msgInfo to msgSubject & " | " & msgSender & " | " & msgDate set end of messageList to msgInfo set msgCount to msgCount + 1 on error -- Skip problematic messages end try end repeat return messageList on error errMsg return "Error: " & errMsg end try end tell `; try { const result = await runAppleScript(script); if (result.startsWith("Error:")) { throw new Error(result); } // Parse the results in a simple format const emails = result.split(", ").map(msgInfo => { const parts = msgInfo.split(" | "); return { subject: parts[0] || "No subject", sender: parts[1] || "Unknown sender", dateSent: parts[2] || new Date().toString(), content: "Content not retrieved in simple mode" }; }); console.error(`[readEmails] Found ${emails.length} emails using simplified approach`); return emails; } catch (error) { console.error("[readEmails] Error reading emails:", error); throw error; } } // ==================================================== // 5. CALENDAR FUNCTIONS // ==================================================== // Function to get today's calendar events async function getTodayEvents(limit: number = 10): Promise<any[]> { console.error(`[getTodayEvents] Getting today's events, limit: ${limit}`); await checkOutlookAccess(); const script = ` tell application "Microsoft Outlook" set todayEvents to {} set theCalendar to default calendar set todayDate to current date set startOfDay to todayDate - (time of todayDate) set endOfDay to startOfDay + 1 * days set eventList to events of theCalendar whose start time is greater than or equal to startOfDay and start time is less than endOfDay set eventCount to count of eventList set limitCount to ${limit} if eventCount < limitCount then set limitCount to eventCount end if repeat with i from 1 to limitCount set theEvent to item i of eventList set eventData to {subject:subject of theEvent, ¬ start:start time of theEvent, ¬ end:end time of theEvent, ¬ location:location of theEvent, ¬ id:id of theEvent} set end of todayEvents to eventData end repeat return todayEvents end tell `; try { const result = await runAppleScript(script); console.error(`[getTodayEvents] Raw result length: ${result.length}`); // Parse the results const events = []; const matches = result.match(/\{([^}]+)\}/g); if (matches && matches.length > 0) { for (const match of matches) { try { const props = match.substring(1, match.length - 1).split(','); const event: any = {}; props.forEach(prop => { const parts = prop.split(':'); if (parts.length >= 2) { const key = parts[0].trim(); const value = parts.slice(1).join(':').trim(); event[key] = value; } }); if (event.subject) { events.push({ subject: event.subject, start: event.start, end: event.end, location: event.location || "No location", id: event.id }); } } catch (parseError) { console.error('[getTodayEvents] Error parsing event match:', parseError); } } } console.error(`[getTodayEvents] Found ${events.length} events for today`); return events; } catch (error) { console.error("[getTodayEvents] Error getting today's events:", error); throw error; } } // Function to get upcoming calendar events async function getUpcomingEvents(days: number = 7, limit: number = 10): Promise<any[]> { console.error(`[getUpcomingEvents] Getting upcoming events for next ${days} days, limit: ${limit}`); await checkOutlookAccess(); const script = ` tell application "Microsoft Outlook" set upcomingEvents to {} set theCalendar to default calendar set todayDate to current date set startOfToday to todayDate - (time of todayDate) set endDate to startOfToday + ${days} * days set eventList to events of theCalendar whose start time is greater than or equal to todayDate and start time is less than endDate set eventCount to count of eventList set limitCount to ${limit} if eventCount < limitCount then set limitCount to eventCount end if repeat with i from 1 to limitCount set theEvent to item i of eventList set eventData to {subject:subject of theEvent, ¬ start:start time of theEvent, ¬ end:end time of theEvent, ¬ location:location of theEvent, ¬ id:id of theEvent} set end of upcomingEvents to eventData end repeat return upcomingEvents end tell `; try { const result = await runAppleScript(script); console.error(`[getUpcomingEvents] Raw result length: ${result.length}`); // Parse the results const events = []; const matches = result.match(/\{([^}]+)\}/g); if (matches && matches.length > 0) { for (const match of matches) { try { const props = match.substring(1, match.length - 1).split(','); const event: any = {}; props.forEach(prop => { const parts = prop.split(':'); if (parts.length >= 2) { const key = parts[0].trim(); const value = parts.slice(1).join(':').trim(); event[key] = value; } }); if (event.subject) { events.push({ subject: event.subject, start: event.start, end: event.end, location: event.location || "No location", id: event.id }); } } catch (parseError) { console.error('[getUpcomingEvents] Error parsing event match:', parseError); } } } console.error(`[getUpcomingEvents] Found ${events.length} upcoming events`); return events; } catch (error) { console.error("[getUpcomingEvents] Error getting upcoming events:", error); throw error; } } // Function to search calendar events async function searchEvents(searchTerm: string, limit: number = 10): Promise<any[]> { console.error(`[searchEvents] Searching for events with term: "${searchTerm}", limit: ${limit}`); await checkOutlookAccess(); const script = ` tell application "Microsoft Outlook" set searchResults to {} set theCalendar to default calendar set allEvents to events of theCalendar set i to 0 set searchString to "${searchTerm.replace(/"/g, '\\"')}" repeat with theEvent in allEvents if (subject of theEvent contains searchString) or (location of theEvent contains searchString) then set i to i + 1 set eventData to {subject:subject of theEvent, ¬ start:start time of theEvent, ¬ end:end time of theEvent, ¬ location:location of theEvent, ¬ id:id of theEvent} set end of searchResults to eventData -- Stop if we've reached the limit if i >= ${limit} then exit repeat end if end if end repeat return searchResults end tell `; try { const result = await runAppleScript(script); console.error(`[searchEvents] Raw result length: ${result.length}`); // Parse the results const events = []; const matches = result.match(/\{([^}]+)\}/g); if (matches && matches.length > 0) { for (const match of matches) { try { const props = match.substring(1, match.length - 1).split(','); const event: any = {}; props.forEach(prop => { const parts = prop.split(':'); if (parts.length >= 2) { const key = parts[0].trim(); const value = parts.slice(1).join(':').trim(); event[key] = value; } }); if (event.subject) { events.push({ subject: event.subject, start: event.start, end: event.end, location: event.location || "No location", id: event.id }); } } catch (parseError) { console.error('[searchEvents] Error parsing event match:', parseError); } } } console.error(`[searchEvents] Found ${events.length} matching events`); return events; } catch (error) { console.error("[searchEvents] Error searching events:", error); throw error; } } // Function to create a calendar event async function createEvent(subject: string, start: string, end: string, location?: string, body?: string, attendees?: string): Promise<string> { console.error(`[createEvent] Creating event: "${subject}", start: ${start}, end: ${end}`); await checkOutlookAccess(); // Parse the ISO date strings to a format AppleScript can understand const startDate = new Date(start); const endDate = new Date(end); // Format for AppleScript (month/day/year hour:minute:second) const formattedStart = `date "${startDate.getMonth() + 1}/${startDate.getDate()}/${startDate.getFullYear()} ${startDate.getHours()}:${startDate.getMinutes()}:${startDate.getSeconds()}"`; const formattedEnd = `date "${endDate.getMonth() + 1}/${endDate.getDate()}/${endDate.getFullYear()} ${endDate.getHours()}:${endDate.getMinutes()}:${endDate.getSeconds()}"`; // Escape strings for AppleScript const escapedSubject = subject.replace(/"/g, '\\"'); const escapedLocation = location ? location.replace(/"/g, '\\"') : ""; const escapedBody = body ? body.replace(/"/g, '\\"') : ""; let script = ` tell application "Microsoft Outlook" set theCalendar to default calendar set newEvent to make new calendar event at theCalendar with properties {subject:"${escapedSubject}", start time:${formattedStart}, end time:${formattedEnd} `; if (location) { script += `, location:"${escapedLocation}"`; } if (body) { script += `, content:"${escapedBody}"`; } script += `} `; // Add attendees if provided if (attendees) { const attendeeList = attendees.split(',').map(email => email.trim()); for (const attendee of attendeeList) { const escapedAttendee = attendee.replace(/"/g, '\\"'); script += ` make new attendee at newEvent with properties {email address:"${escapedAttendee}"} `; } } script += ` save newEvent return "Event created successfully" end tell `; try { const result = await runAppleScript(script); console.error(`[createEvent] Result: ${result}`); return result; } catch (error) { console.error("[createEvent] Error creating event:", error); throw error; } } // ==================================================== // 6. CONTACTS FUNCTIONS // ==================================================== // Function to list contacts with improved AppleScript syntax async function listContacts(limit: number = 20): Promise<any[]> { console.error(`[listContacts] Listing contacts, limit: ${limit}`); await checkOutlookAccess(); const script = ` tell application "Microsoft Outlook" set contactList to {} set allContactsList to contacts set contactCount to count of allContactsList set limitCount to ${limit} if contactCount < limitCount then set limitCount to contactCount end if repeat with i from 1 to limitCount try set theContact to item i of allContactsList set contactName to full name of theContact -- Create a basic object with name set contactData to {name:contactName} -- Try to get email try set emailList to email addresses of theContact if (count of emailList) > 0 then set emailAddr to address of item 1 of emailList set contactData to contactData & {email:emailAddr} else set contactData to contactData & {email:"No email"} end if on error set contactData to contactData & {email:"No email"} end try -- Try to get phone try set phoneList to phones of theContact if (count of phoneList) > 0 then set phoneNum to formatted dial string of item 1 of phoneList set contactData to contactData & {phone:phoneNum} else set contactData to contactData & {phone:"No phone"} end if on error set contactData to contactData & {phone:"No phone"} end try set end of contactList to contactData on error -- Skip contacts that can't be processed end try end repeat return contactList end tell `; try { const result = await runAppleScript(script); console.error(`[listContacts] Raw result length: ${result.length}`); // Parse the results const contacts = []; const matches = result.match(/\{([^}]+)\}/g); if (matches && matches.length > 0) { for (const match of matches) { try { const props = match.substring(1, match.length - 1).split(','); const contact: any = {}; props.forEach(prop => { const parts = prop.split(':'); if (parts.length >= 2) { const key = parts[0].trim(); const value = parts.slice(1).join(':').trim(); contact[key] = value; } }); if (contact.name) { contacts.push({ name: contact.name, email: contact.email || "No email", phone: contact.phone || "No phone" }); } } catch (parseError) { console.error('[listContacts] Error parsing contact match:', parseError); } } } console.error(`[listContacts] Found ${contacts.length} contacts`); return contacts; } catch (error) { console.error("[listContacts] Error listing contacts:", error); // Try an alternative approach using a simpler script try { const alternativeScript = ` tell application "Microsoft Outlook" set contactList to {} set contactCount to count of contacts set limitCount to ${limit} if contactCount < limitCount then set limitCount to contactCount end if repeat with i from 1 to limitCount try set theContact to item i of contacts set contactName to full name of theContact set end of contactList to contactName end try end repeat return contactList end tell `; const result = await runAppleScript(alternativeScript); // Parse the simpler result format (just names) const simplifiedContacts = result.split(", ").map(name => ({ name: name, email: "Not available with simplified method", phone: "Not available with simplified method" })); console.error(`[listContacts] Found ${simplifiedContacts.length} contacts using alternative method`); return simplifiedContacts; } catch (altError) { console.error("[listContacts] Alternative method also failed:", altError); throw new Error(`Error accessing contacts. The error might be related to Outlook permissions or configuration: ${error instanceof Error ? error.message : String(error)}`); } } } // Function to search contacts // Function to search contacts with improved AppleScript syntax async function searchContacts(searchTerm: string, limit: number = 10): Promise<any[]> { console.error(`[searchContacts] Searching for contacts with term: "${searchTerm}", limit: ${limit}`); await checkOutlookAccess(); const script = ` tell application "Microsoft Outlook" set searchResults to {} set allContacts to contacts set i to 0 set searchString to "${searchTerm.replace(/"/g, '\\"')}" repeat with theContact in allContacts try set contactName to full name of theContact if contactName contains searchString then set i to i + 1 -- Create basic contact info set contactData to {name:contactName} -- Try to get email try set emailList to email addresses of theContact if (count of emailList) > 0 then set emailAddr to address of item 1 of emailList set contactData to contactData & {email:emailAddr} else set contactData to contactData & {email:"No email"} end if on error set contactData to contactData & {email:"No email"} end try -- Try to get phone try set phoneList to phones of theContact if (count of phoneList) > 0 then set phoneNum to formatted dial string of item 1 of phoneList set contactData to contactData & {phone:phoneNum} else set contactData to contactData & {phone:"No phone"} end if on error set contactData to contactData & {phone:"No phone"} end try set end of searchResults to contactData -- Stop if we've reached the limit if i >= ${limit} then exit repeat end if end if on error -- Skip contacts that can't be processed end try end repeat return searchResults end tell `; try { const result = await runAppleScript(script); console.error(`[searchContacts] Raw result length: ${result.length}`); // Parse the results const contacts = []; const matches = result.match(/\{([^}]+)\}/g); if (matches && matches.length > 0) { for (const match of matches) { try { const props = match.substring(1, match.length - 1).split(','); const contact: any = {}; props.forEach(prop => { const parts = prop.split(':'); if (parts.length >= 2) { const key = parts[0].trim(); const value = parts.slice(1).join(':').trim(); contact[key] = value; } }); if (contact.name) { contacts.push({ name: contact.name, email: contact.email || "No email", phone: contact.phone || "No phone" }); } } catch (parseError) { console.error('[searchContacts] Error parsing contact match:', parseError); } } } console.error(`[searchContacts] Found ${contacts.length} matching contacts`); return contacts; } catch (error) { console.error("[searchContacts] Error searching contacts:", error); // Try an alternative approach with a simpler script that just returns names try { const alternativeScript = ` tell application "Microsoft Outlook" set matchingContacts to {} set searchString to "${searchTerm.replace(/"/g, '\\"')}" set i to 0 repeat with theContact in contacts try set contactName to full name of theContact if contactName contains searchString then set i to i + 1 set end of matchingContacts to contactName if i >= ${limit} then exit repeat end if end try end repeat return matchingContacts end tell `; const result = await runAppleScript(alternativeScript); // Parse the simpler result format (just names) const simplifiedContacts = result.split(", ").map(name => ({ name: name, email: "Not available with simplified method", phone: "Not available with simplified method" })); console.error(`[searchContacts] Found ${simplifiedContacts.length} contacts using alternative method`); return simplifiedContacts; } catch (altError) { console.error("[searchContacts] Alternative method also failed:", altError); throw new Error(`Error searching contacts. The error might be related to Outlook permissions or configuration: ${error instanceof Error ? error.message : String(error)}`); } } } // ==================================================== // 7. TYPE GUARDS // ==================================================== // Type guards for arguments function isMailArgs(args: unknown): args is { operation: "unread" | "search" | "send" | "folders" | "read"; folder?: string; limit?: number; searchTerm?: string; to?: string; subject?: string; body?: string; isHtml?: boolean; cc?: string; bcc?: string; attachments?: string[]; } { if (typeof args !== "object" || args === null) return false; const { operation } = args as any; if (!operation || !["unread", "search", "send", "folders", "read"].includes(operation)) { return false; } // Check required fields based on operation switch (operation) { case "search": if (!(args as any).searchTerm) return false; break; case "send": if (!(args as any).to || !(args as any).subject || !(args as any).body) return false; break; } return true; } function isCalendarArgs(args: unknown): args is { operation: "today" | "upcoming" | "search" | "create"; searchTerm?: string; limit?: number; days?: number; subject?: string; start?: string; end?: string; location?: string; body?: string; attendees?: string; } { if (typeof args !== "object" || args === null) return false; const { operation } = args as any; if (!operation || !["today", "upcoming", "search", "create"].includes(operation)) { return false; } // Check required fields based on operation switch (operation) { case "search": if (!(args as any).searchTerm) return false; break; case "create": if (!(args as any).subject || !(args as any).start || !(args as any).end) return false; break; } return true; } function isContactsArgs(args: unknown): args is { operation: "list" | "search"; searchTerm?: string; limit?: number; } { if (typeof args !== "object" || args === null) return false; const { operation } = args as any; if (!operation || !["list", "search"].includes(operation)) { return false; } // Check required fields based on operation if (operation === "search" && !(args as any).searchTerm) { return false; } return true; } // ==================================================== // 8. MCP REQUEST HANDLERS // ==================================================== // Set up request handlers server.setRequestHandler(ListToolsRequestSchema, async () => { console.error("[ListToolsRequest] Returning available tools"); return { tools: [OUTLOOK_MAIL_TOOL, OUTLOOK_CALENDAR_TOOL, OUTLOOK_CONTACTS_TOOL], }; }); server.setRequestHandler(CallToolRequestSchema, async (request) => { try { const { name, arguments: args } = request.params; console.error(`[CallToolRequest] Received request for tool: ${name}`); if (!args) { throw new Error("No arguments provided"); } switch (name) { case "outlook_mail": { if (!isMailArgs(args)) { throw new Error("Invalid arguments for outlook_mail tool"); } const { operation } = args; console.error(`[CallToolRequest] Mail operation: ${operation}`); switch (operation) { case "unread": { const emails = await getUnreadEmails(args.folder, args.limit); return { content: [{ type: "text", text: emails.length > 0 ? `Found ${emails.length} unread email(s)${args.folder ? ` in folder "${args.folder}"` : ''}\n\n` + emails.map(email => `[${email.dateSent}] From: ${email.sender}\nSubject: ${email.subject}\n${email.content.substring(0, 200)}${email.content.length > 200 ? '...' : ''}` ).join("\n\n") : `No unread emails found${args.folder ? ` in folder "${args.folder}"` : ''}` }], isError: false }; } case "search": { if (!args.searchTerm) { throw new Error("Search term is required for search operation"); } const emails = await searchEmails(args.searchTerm, args.folder, args.limit); return { content: [{ type: "text", text: emails.length > 0 ? `Found ${emails.length} email(s) for "${args.searchTerm}"${args.folder ? ` in folder "${args.folder}"` : ''}\n\n` + emails.map(email => `[${email.dateSent}] From: ${email.sender}\nSubject: ${email.subject}\n${email.content.substring(0, 200)}${email.content.length > 200 ? '...' : ''}` ).join("\n\n") : `No emails found for "${args.searchTerm}"${args.folder ? ` in folder "${args.folder}"` : ''}` }], isError: false }; } // Update the handler in CallToolRequestSchema case "send": { if (!args.to || !args.subject || !args.body) { throw new Error("Recipient (to), subject, and body are required for send operation"); } // Validate attachments if provided if (args.attachments && !Array.isArray(args.attachments)) { throw new Error("Attachments must be an array of file paths"); } // Log attachment information for debugging console.error(`[CallTool] Send email with attachments: ${args.attachments ? JSON.stringify(args.attachments) : 'none'}`); const result = await sendEmail( args.to, args.subject, args.body, args.cc, args.bcc, args.isHtml || false, args.attachments ); return { content: [{ type: "text", text: result }], isError: false }; } case "folders": { const folders = await getMailFolders(); return { content: [{ type: "text", text: folders.length > 0 ? `Found ${folders.length} mail folders:\n\n${folders.join("\n")}` : "No mail folders found. Make sure Outlook is running and properly configured." }], isError: false }; } case "read": { const emails = await readEmails(args.folder, args.limit); return { content: [{ type: "text", text: emails.length > 0 ? `Found ${emails.length} email(s)${args.folder ? ` in folder "${args.folder}"` : ''}\n\n` + emails.map(email => `[${email.dateSent}] From: ${email.sender}\nSubject: ${email.subject}\n${email.content.substring(0, 200)}${email.content.length > 200 ? '...' : ''}` ).join("\n\n") : `No emails found${args.folder ? ` in folder "${args.folder}"` : ''}` }], isError: false }; } default: throw new Error(`Unknown mail operation: ${operation}`); } } case "outlook_calendar": { if (!isCalendarArgs(args)) { throw new Error("Invalid arguments for outlook_calendar tool"); } const { operation } = args; console.error(`[CallToolRequest] Calendar operation: ${operation}`); switch (operation) { case "today": { const events = await getTodayEvents(args.limit); return { content: [{ type: "text", text: events.length > 0 ? `Found ${events.length} event(s) for today:\n\n` + events.map(event => `${event.subject}\nTime: ${event.start} - ${event.end}\nLocation: ${event.location}` ).join("\n\n") : "No events found for today" }], isError: false }; } case "upcoming": { const days = args.days || 7; const events = await getUpcomingEvents(days, args.limit); return { content: [{ type: "text", text: events.length > 0 ? `Found ${events.length} upcoming event(s) for the next ${days} days:\n\n` + events.map(event => `${event.subject}\nTime: ${event.start} - ${event.end}\nLocation: ${event.location}` ).join("\n\n") : `No upcoming events found for the next ${days} days` }], isError: false }; } case "search": { if (!args.searchTerm) { throw new Error("Search term is required for search operation"); } const events = await searchEvents(args.searchTerm, args.limit); return { content: [{ type: "text", text: events.length > 0 ? `Found ${events.length} event(s) matching "${args.searchTerm}":\n\n` + events.map(event => `${event.subject}\nTime: ${event.start} - ${event.end}\nLocation: ${event.location}` ).join("\n\n") : `No events found matching "${args.searchTerm}"` }], isError: false }; } case "create": { if (!args.subject || !args.start || !args.end) { throw new Error("Subject, start time, and end time are required for create operation"); } const result = await createEvent(args.subject, args.start, args.end, args.location, args.body, args.attendees); return { content: [{ type: "text", text: result }], isError: false }; } default: throw new Error(`Unknown calendar operation: ${operation}`); } } case "outlook_contacts": { if (!isContactsArgs(args)) { throw new Error("Invalid arguments for outlook_contacts tool"); } const { operation } = args; console.error(`[CallToolRequest] Contacts operation: ${operation}`); switch (operation) { case "list": { const contacts = await listContacts(args.limit); return { content: [{ type: "text", text: contacts.length > 0 ? `Found ${contacts.length} contact(s):\n\n` + contacts.map(contact => `Name: ${contact.name}\nEmail: ${contact.email}\nPhone: ${contact.phone}` ).join("\n\n") : "No contacts found" }], isError: false }; } case "search": { if (!args.searchTerm) { throw new Error("Search term is required for search operation"); } const contacts = await searchContacts(args.searchTerm, args.limit); return { content: [{ type: "text", text: contacts.length > 0 ? `Found ${contacts.length} contact(s) matching "${args.searchTerm}":\n\n` + contacts.map(contact => `Name: ${contact.name}\nEmail: ${contact.email}\nPhone: ${contact.phone}` ).join("\n\n") : `No contacts found matching "${args.searchTerm}"` }], isError: false }; } default: throw new Error(`Unknown contacts operation: ${operation}`); } } default: return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true, }; } } catch (error) { console.error("[CallToolRequest] Error:", error); return { content: [ { type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}`, }, ], isError: true, }; } }); // ==================================================== // 9. START SERVER // ==================================================== // Start the MCP server console.error("Initializing Outlook MCP server transport..."); const transport = new StdioServerTransport(); (async () => { try { console.error("Connecting to transport..."); await server.connect(transport); console.error("Outlook MCP Server running on stdio"); } catch (error) { console.error("Failed to initialize MCP server:", error); process.exit(1); } })();