Read unread emails, search email content, and send messages directly from the Apple Mail app. Manage mailboxes and accounts through automated operations.
Instructions
Interact with Apple Mail app - read unread emails, search emails, and send emails
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| operation | Yes | Operation to perform: 'unread', 'search', 'send', 'mailboxes', 'accounts', or 'latest' | |
| account | No | Email account to use (optional - if not provided, searches across all accounts) | |
| mailbox | No | Mailbox to use (optional - if not provided, uses inbox or searches across all mailboxes) | |
| limit | No | Number of emails to retrieve (optional, for unread, search, and latest operations) | |
| searchTerm | No | Text to search for in emails (required for search operation) | |
| to | No | Recipient email address (required for send operation) | |
| subject | No | Email subject (required for send operation) | |
| body | No | Email body content (required for send operation) | |
| cc | No | CC email address (optional for send operation) | |
| bcc | No | BCC email address (optional for send operation) |
Implementation Reference
- index.ts:443-671 (handler)Main execution handler for the 'mail' MCP tool. Loads utils/mail module and handles operations: unread (getUnreadMails), search (searchMails), send (sendMail), mailboxes/accounts (getMailboxes etc.). Includes custom AppleScript for account-specific unread emails.case "mail": { if (!isMailArgs(args)) { throw new Error("Invalid arguments for mail tool"); } try { const mailModule = await loadModule('mail'); switch (args.operation) { case "unread": { // If an account is specified, we'll try to search specifically in that account let emails; if (args.account) { console.error(`Getting unread emails for account: ${args.account}`); // Use AppleScript to get unread emails from specific account const script = ` tell application "Mail" set resultList to {} try set targetAccount to first account whose name is "${args.account.replace(/"/g, '\\"')}" -- Get mailboxes for this account set acctMailboxes to every mailbox of targetAccount -- If mailbox is specified, only search in that mailbox set mailboxesToSearch to acctMailboxes ${args.mailbox ? ` set mailboxesToSearch to {} repeat with mb in acctMailboxes if name of mb is "${args.mailbox.replace(/"/g, '\\"')}" then set mailboxesToSearch to {mb} exit repeat end if end repeat ` : ''} -- Search specified mailboxes repeat with mb in mailboxesToSearch try set unreadMessages to (messages of mb whose read status is false) if (count of unreadMessages) > 0 then set msgLimit to ${args.limit || 10} if (count of unreadMessages) < msgLimit then set msgLimit to (count of unreadMessages) end if repeat with i from 1 to msgLimit try set currentMsg to item i of unreadMessages set msgData to {subject:(subject of currentMsg), sender:(sender of currentMsg), ¬ date:(date sent of currentMsg) as string, mailbox:(name of mb)} -- Try to get content if possible try set msgContent to content of currentMsg 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 resultList to msgData on error -- Skip problematic messages end try end repeat if (count of resultList) ≥ ${args.limit || 10} then exit repeat end if on error -- Skip problematic mailboxes end try end repeat on error errMsg return "Error: " & errMsg end try return resultList end tell`; try { const asResult = await runAppleScript(script); if (asResult && asResult.startsWith('Error:')) { throw new Error(asResult); } // Parse the results - similar to general getUnreadMails const emailData = []; const matches = asResult.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) { emailData.push({ subject: email.subject || "No subject", sender: email.sender || "Unknown sender", dateSent: email.date || new Date().toString(), content: email.content || "[Content not available]", isRead: false, mailbox: `${args.account} - ${email.mailbox || "Unknown"}` }); } } catch (parseError) { console.error('Error parsing email match:', parseError); } } } emails = emailData; } catch (error) { console.error('Error getting account-specific emails:', error); // Fallback to general method if specific account fails emails = await mailModule.getUnreadMails(args.limit); } } else { // No account specified, use the general method emails = await mailModule.getUnreadMails(args.limit); } return { content: [{ type: "text", text: emails.length > 0 ? `Found ${emails.length} unread email(s)${args.account ? ` in account "${args.account}"` : ''}${args.mailbox ? ` and mailbox "${args.mailbox}"` : ''}:\n\n` + emails.map((email: any) => `[${email.dateSent}] From: ${email.sender}\nMailbox: ${email.mailbox}\nSubject: ${email.subject}\n${email.content.substring(0, 500)}${email.content.length > 500 ? '...' : ''}` ).join("\n\n") : `No unread emails found${args.account ? ` in account "${args.account}"` : ''}${args.mailbox ? ` and mailbox "${args.mailbox}"` : ''}` }], isError: false }; } case "search": { if (!args.searchTerm) { throw new Error("Search term is required for search operation"); } const emails = await mailModule.searchMails(args.searchTerm, args.limit); return { content: [{ type: "text", text: emails.length > 0 ? `Found ${emails.length} email(s) for "${args.searchTerm}"${args.account ? ` in account "${args.account}"` : ''}${args.mailbox ? ` and mailbox "${args.mailbox}"` : ''}:\n\n` + emails.map((email: any) => `[${email.dateSent}] From: ${email.sender}\nMailbox: ${email.mailbox}\nSubject: ${email.subject}\n${email.content.substring(0, 200)}${email.content.length > 200 ? '...' : ''}` ).join("\n\n") : `No emails found for "${args.searchTerm}"${args.account ? ` in account "${args.account}"` : ''}${args.mailbox ? ` and mailbox "${args.mailbox}"` : ''}` }], isError: false }; } case "send": { if (!args.to || !args.subject || !args.body) { throw new Error("Recipient (to), subject, and body are required for send operation"); } const result = await mailModule.sendMail(args.to, args.subject, args.body, args.cc, args.bcc); return { content: [{ type: "text", text: result }], isError: false }; } case "mailboxes": { if (args.account) { const mailboxes = await mailModule.getMailboxesForAccount(args.account); return { content: [{ type: "text", text: mailboxes.length > 0 ? `Found ${mailboxes.length} mailboxes for account "${args.account}":\n\n${mailboxes.join("\n")}` : `No mailboxes found for account "${args.account}". Make sure the account name is correct.` }], isError: false }; } else { const mailboxes = await mailModule.getMailboxes(); return { content: [{ type: "text", text: mailboxes.length > 0 ? `Found ${mailboxes.length} mailboxes:\n\n${mailboxes.join("\n")}` : "No mailboxes found. Make sure Mail app is running and properly configured." }], isError: false }; } } case "accounts": { const accounts = await mailModule.getAccounts(); return { content: [{ type: "text", text: accounts.length > 0 ? `Found ${accounts.length} email accounts:\n\n${accounts.join("\n")}` : "No email accounts found. Make sure Mail app is configured with at least one account." }], isError: false }; } default: throw new Error(`Unknown operation: ${args.operation}`); } } catch (error) { return { content: [{ type: "text", text: `Error with mail operation: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } }
- tools.ts:81-131 (schema)Input schema and metadata for the 'mail' tool, defining parameters and operations.const MAIL_TOOL: Tool = { name: "mail", description: "Interact with Apple Mail app - read unread emails, search emails, and send emails", inputSchema: { type: "object", properties: { operation: { type: "string", description: "Operation to perform: 'unread', 'search', 'send', 'mailboxes', or 'accounts'", enum: ["unread", "search", "send", "mailboxes", "accounts"] }, account: { type: "string", description: "Email account to use (optional - if not provided, searches across all accounts)" }, mailbox: { type: "string", description: "Mailbox to use (optional - if not provided, uses inbox or searches across all mailboxes)" }, limit: { type: "number", description: "Number of emails to retrieve (optional, for unread 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)" }, cc: { type: "string", description: "CC email address (optional for send operation)" }, bcc: { type: "string", description: "BCC email address (optional for send operation)" } }, required: ["operation"] } };
- tools.ts:307-309 (registration)Registers the MAIL_TOOL in the exported tools array for MCP server list tools response.const tools = [CONTACTS_TOOL, NOTES_TOOL, MESSAGES_TOOL, MAIL_TOOL, REMINDERS_TOOL, WEB_SEARCH_TOOL, CALENDAR_TOOL, MAPS_TOOL];
- utils/mail.ts:748-755 (helper)Default export of mail helper functions invoked by the 'mail' tool handler.export default { getUnreadMails, searchMails, sendMail, getMailboxes, getAccounts, getMailboxesForAccount, };
- index.ts:1178-1223 (schema)Runtime input argument validation for 'mail' tool calls, enforcing schema constraints.function isMailArgs(args: unknown): args is { operation: "unread" | "search" | "send" | "mailboxes" | "accounts"; account?: string; mailbox?: string; limit?: number; searchTerm?: string; to?: string; subject?: string; body?: string; cc?: string; bcc?: string; } { if (typeof args !== "object" || args === null) return false; const { operation, account, mailbox, limit, searchTerm, to, subject, body, cc, bcc } = args as any; if (!operation || !["unread", "search", "send", "mailboxes", "accounts"].includes(operation)) { return false; } // Validate required fields based on operation switch (operation) { case "search": if (!searchTerm || typeof searchTerm !== "string") return false; break; case "send": if (!to || typeof to !== "string" || !subject || typeof subject !== "string" || !body || typeof body !== "string") return false; break; case "unread": case "mailboxes": case "accounts": // No additional required fields break; } // Validate field types if present if (account && typeof account !== "string") return false; if (mailbox && typeof mailbox !== "string") return false; if (limit && typeof limit !== "number") return false; if (cc && typeof cc !== "string") return false; if (bcc && typeof bcc !== "string") return false; return true; }