| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066 |
- #!/usr/bin/env node
- /**
- * Apple Mail MCP Server
- *
- * A Model Context Protocol (MCP) server that provides AI assistants
- * with the ability to interact with Apple Mail on macOS.
- *
- * This server exposes tools for:
- * - Reading and searching emails
- * - Sending emails
- * - Managing mailboxes
- * - Managing multiple accounts (iCloud, Gmail, Exchange, etc.)
- *
- * Architecture:
- * - Tool definitions are declarative (schema + handler)
- * - The AppleMailManager class handles all AppleScript operations
- * - Error handling is consistent across all tools
- *
- * @module apple-mail-mcp
- * @see https://modelcontextprotocol.io
- */
- import { createRequire } from "module";
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
- import { z } from "zod";
- import { AppleMailManager } from "@/services/appleMailManager.js";
- // =============================================================================
- // Shared Validation Schemas
- // =============================================================================
- /** Message IDs in Apple Mail are always numeric. Enforce this at the schema level
- * to prevent AppleScript injection via the `whose id is ${id}` interpolation. */
- const MESSAGE_ID_SCHEMA = z.string().regex(/^\d+$/, "Message ID must be numeric");
- /** Batch operations are capped to prevent unbounded loops / DoS. */
- const BATCH_IDS_SCHEMA = z
- .array(MESSAGE_ID_SCHEMA)
- .min(1, "At least one message ID is required")
- .max(100, "Cannot process more than 100 messages in a single batch");
- /** Date filter strings must look like natural-language dates (e.g. "March 1, 2026").
- * Block characters that could escape an AppleScript `date "..."` literal. */
- const DATE_FILTER_SCHEMA = z
- .string()
- .regex(
- /^[a-zA-Z0-9 ,/\-:]+$/,
- "Date must contain only alphanumeric characters, spaces, commas, slashes, hyphens, and colons"
- )
- .optional();
- // Read version from package.json to keep it in sync
- const require = createRequire(import.meta.url);
- const { version } = require("../package.json") as { version: string };
- // =============================================================================
- // Server Initialization
- // =============================================================================
- /**
- * MCP server instance configured for Apple Mail operations.
- */
- const server = new McpServer({
- name: "apple-mail",
- version,
- description: "MCP server for managing Apple Mail - read, search, send, and organize emails",
- });
- /**
- * Singleton instance of the Apple Mail manager.
- * Handles all AppleScript execution and mail operations.
- */
- const mailManager = new AppleMailManager();
- // =============================================================================
- // Response Helpers
- // =============================================================================
- /**
- * Creates a successful MCP tool response.
- */
- function successResponse(message: string) {
- return {
- content: [{ type: "text" as const, text: message }],
- };
- }
- /**
- * Creates an error MCP tool response.
- */
- function errorResponse(message: string) {
- return {
- content: [{ type: "text" as const, text: message }],
- isError: true,
- };
- }
- /**
- * Wraps a tool handler with consistent error handling.
- */
- function withErrorHandling<T extends Record<string, unknown>>(
- handler: (params: T) => ReturnType<typeof successResponse>,
- errorPrefix: string
- ) {
- return async (params: T) => {
- try {
- return handler(params);
- } catch (error) {
- const message = error instanceof Error ? error.message : "Unknown error";
- return errorResponse(`${errorPrefix}: ${message}`);
- }
- };
- }
- // =============================================================================
- // Message Tools
- // =============================================================================
- // --- search-messages ---
- server.tool(
- "search-messages",
- {
- query: z.string().optional().describe("Text to search for in subject, sender, or content"),
- from: z.string().optional().describe("Filter by sender email address"),
- subject: z.string().optional().describe("Filter by subject line"),
- mailbox: z.string().optional().describe("Mailbox to search in (e.g., 'INBOX')"),
- account: z.string().optional().describe("Account to search in (omit to search all accounts)"),
- isRead: z.boolean().optional().describe("Filter by read status"),
- isFlagged: z.boolean().optional().describe("Filter by flagged status"),
- dateFrom: DATE_FILTER_SCHEMA.describe("Start date filter (e.g., 'January 1, 2026')"),
- dateTo: DATE_FILTER_SCHEMA.describe("End date filter (e.g., 'March 1, 2026')"),
- limit: z.number().optional().describe("Maximum number of results (default: 50)"),
- },
- withErrorHandling(({ query, mailbox, account, limit = 50, dateFrom, dateTo }) => {
- const messages = mailManager.searchMessages(query, mailbox, account, limit, dateFrom, dateTo);
- if (messages.length === 0) {
- return successResponse("No messages found matching criteria");
- }
- const messageList = messages
- .map(
- (m) =>
- ` - ID: ${m.id} | ${m.dateReceived.toLocaleDateString()} | ${m.subject} (from: ${m.sender}) [${m.isRead ? "read" : "unread"}]`
- )
- .join("\n");
- return successResponse(`Found ${messages.length} message(s):\n${messageList}`);
- }, "Error searching messages")
- );
- // --- get-message ---
- server.tool(
- "get-message",
- {
- id: MESSAGE_ID_SCHEMA,
- preferHtml: z.boolean().optional().describe("Return HTML source instead of plain text"),
- },
- withErrorHandling(({ id, preferHtml }) => {
- const content = mailManager.getMessageContent(id);
- if (!content) {
- return errorResponse(`Message with ID "${id}" not found`);
- }
- if (preferHtml && content.htmlContent) {
- return successResponse(`Subject: ${content.subject}\n\n${content.htmlContent}`);
- }
- return successResponse(`Subject: ${content.subject}\n\n${content.plainText}`);
- }, "Error retrieving message")
- );
- // --- list-messages ---
- server.tool(
- "list-messages",
- {
- mailbox: z.string().optional().describe("Mailbox to list messages from (default: INBOX)"),
- account: z.string().optional().describe("Account to list messages from"),
- limit: z.number().optional().describe("Maximum number of messages (default: 50)"),
- offset: z.number().optional().describe("Number of messages to skip (for pagination)"),
- from: z.string().optional().describe("Filter by sender email address or name"),
- unreadOnly: z.boolean().optional().describe("Only show unread messages"),
- },
- withErrorHandling(({ mailbox, account, limit = 50, offset = 0, from }) => {
- const messages = mailManager.listMessages(mailbox, account, limit, from, offset);
- if (messages.length === 0) {
- return successResponse("No messages found");
- }
- const messageList = messages
- .map(
- (m) =>
- ` - ID: ${m.id} | ${m.dateReceived.toLocaleDateString()} | ${m.subject} (from: ${m.sender})`
- )
- .join("\n");
- return successResponse(`Found ${messages.length} message(s):\n${messageList}`);
- }, "Error listing messages")
- );
- // --- send-email ---
- server.tool(
- "send-email",
- {
- to: z.array(z.string()).min(1, "At least one recipient is required"),
- subject: z.string().min(1, "Subject is required"),
- body: z.string().min(1, "Body is required"),
- cc: z.array(z.string()).optional().describe("CC recipients"),
- bcc: z.array(z.string()).optional().describe("BCC recipients"),
- account: z.string().optional().describe("Account to send from"),
- attachments: z
- .array(z.string())
- .max(20, "Cannot attach more than 20 files")
- .optional()
- .describe("Absolute file paths to attach (e.g., ['/Users/me/report.pdf'])"),
- },
- withErrorHandling(({ to, subject, body, cc, bcc, account, attachments }) => {
- const success = mailManager.sendEmail(to, subject, body, cc, bcc, account, attachments);
- if (!success) {
- return errorResponse("Failed to send email. Check Mail.app configuration.");
- }
- const attachInfo = attachments?.length ? ` with ${attachments.length} attachment(s)` : "";
- return successResponse(`Email sent to ${to.join(", ")}${attachInfo}`);
- }, "Error sending email")
- );
- // --- send-serial-email ---
- server.tool(
- "send-serial-email",
- {
- recipients: z
- .array(
- z.object({
- email: z.string().min(1, "Recipient email is required"),
- variables: z
- .record(z.string())
- .describe("Placeholder values, e.g. { Name: 'Alice', Company: 'Acme' }"),
- })
- )
- .min(1, "At least one recipient is required")
- .max(100, "Cannot send to more than 100 recipients in a single batch")
- .describe("List of recipients with personalization variables (max 100)"),
- subject: z
- .string()
- .min(1, "Subject is required")
- .describe("Subject line — use {{Key}} for placeholders"),
- body: z
- .string()
- .min(1, "Body is required")
- .describe("Email body — use {{Key}} for placeholders"),
- account: z.string().optional().describe("Account to send from"),
- delayMs: z
- .number()
- .min(0)
- .max(10000)
- .optional()
- .describe("Delay between sends in ms (default: 500, max: 10000)"),
- },
- withErrorHandling(({ recipients, subject, body, account, delayMs }) => {
- const results = mailManager.sendSerialEmail(recipients, subject, body, account, delayMs);
- const successCount = results.filter((r) => r.success).length;
- const failCount = results.length - successCount;
- const details = results
- .map((r) => ` - ${r.email}: ${r.success ? "sent" : `FAILED (${r.error})`}`)
- .join("\n");
- if (failCount === 0) {
- return successResponse(`Successfully sent ${successCount} email(s):\n${details}`);
- } else if (successCount === 0) {
- return errorResponse(`Failed to send all ${failCount} email(s):\n${details}`);
- } else {
- return successResponse(
- `Sent ${successCount} of ${results.length} email(s), ${failCount} failed:\n${details}`
- );
- }
- }, "Error sending serial emails")
- );
- // --- create-draft ---
- server.tool(
- "create-draft",
- {
- to: z.array(z.string()).min(1, "At least one recipient is required"),
- subject: z.string().min(1, "Subject is required"),
- body: z.string().min(1, "Body is required"),
- cc: z.array(z.string()).optional().describe("CC recipients"),
- bcc: z.array(z.string()).optional().describe("BCC recipients"),
- account: z.string().optional().describe("Account to create draft in"),
- attachments: z
- .array(z.string())
- .max(20, "Cannot attach more than 20 files")
- .optional()
- .describe("Absolute file paths to attach (e.g., ['/Users/me/report.pdf'])"),
- },
- withErrorHandling(({ to, subject, body, cc, bcc, account, attachments }) => {
- const success = mailManager.createDraft(to, subject, body, cc, bcc, account, attachments);
- if (!success) {
- return errorResponse("Failed to create draft. Check Mail.app configuration.");
- }
- const attachInfo = attachments?.length ? ` with ${attachments.length} attachment(s)` : "";
- return successResponse(`Draft created for ${to.join(", ")}${attachInfo}`);
- }, "Error creating draft")
- );
- // --- reply-to-message ---
- server.tool(
- "reply-to-message",
- {
- id: MESSAGE_ID_SCHEMA,
- body: z.string().min(1, "Reply body is required"),
- replyAll: z.boolean().optional().default(false).describe("Reply to all recipients"),
- send: z.boolean().optional().default(true).describe("Send immediately (false = save as draft)"),
- },
- withErrorHandling(({ id, body, replyAll, send }) => {
- const success = mailManager.replyToMessage(id, body, replyAll, send);
- if (!success) {
- return errorResponse(`Failed to reply to message "${id}"`);
- }
- return successResponse(send ? "Reply sent" : "Reply saved as draft");
- }, "Error replying to message")
- );
- // --- forward-message ---
- server.tool(
- "forward-message",
- {
- id: MESSAGE_ID_SCHEMA,
- to: z.array(z.string()).min(1, "At least one recipient is required"),
- body: z.string().optional().describe("Optional message to prepend"),
- send: z.boolean().optional().default(true).describe("Send immediately (false = save as draft)"),
- },
- withErrorHandling(({ id, to, body, send }) => {
- const success = mailManager.forwardMessage(id, to, body, send);
- if (!success) {
- return errorResponse(`Failed to forward message "${id}"`);
- }
- return successResponse(
- send ? `Message forwarded to ${to.join(", ")}` : "Forward saved as draft"
- );
- }, "Error forwarding message")
- );
- // --- mark-as-read ---
- server.tool(
- "mark-as-read",
- {
- id: MESSAGE_ID_SCHEMA,
- },
- withErrorHandling(({ id }) => {
- const success = mailManager.markAsRead(id);
- if (!success) {
- return errorResponse(`Failed to mark message "${id}" as read`);
- }
- return successResponse("Message marked as read");
- }, "Error marking message as read")
- );
- // --- mark-as-unread ---
- server.tool(
- "mark-as-unread",
- {
- id: MESSAGE_ID_SCHEMA,
- },
- withErrorHandling(({ id }) => {
- const success = mailManager.markAsUnread(id);
- if (!success) {
- return errorResponse(`Failed to mark message "${id}" as unread`);
- }
- return successResponse("Message marked as unread");
- }, "Error marking message as unread")
- );
- // --- flag-message ---
- server.tool(
- "flag-message",
- {
- id: MESSAGE_ID_SCHEMA,
- },
- withErrorHandling(({ id }) => {
- const success = mailManager.flagMessage(id);
- if (!success) {
- return errorResponse(`Failed to flag message "${id}"`);
- }
- return successResponse("Message flagged");
- }, "Error flagging message")
- );
- // --- unflag-message ---
- server.tool(
- "unflag-message",
- {
- id: MESSAGE_ID_SCHEMA,
- },
- withErrorHandling(({ id }) => {
- const success = mailManager.unflagMessage(id);
- if (!success) {
- return errorResponse(`Failed to unflag message "${id}"`);
- }
- return successResponse("Message unflagged");
- }, "Error unflagging message")
- );
- // --- delete-message ---
- server.tool(
- "delete-message",
- {
- id: MESSAGE_ID_SCHEMA,
- },
- withErrorHandling(({ id }) => {
- const success = mailManager.deleteMessage(id);
- if (!success) {
- return errorResponse(`Failed to delete message "${id}"`);
- }
- return successResponse("Message deleted");
- }, "Error deleting message")
- );
- // --- move-message ---
- server.tool(
- "move-message",
- {
- id: MESSAGE_ID_SCHEMA,
- mailbox: z.string().min(1, "Destination mailbox is required"),
- account: z.string().optional().describe("Account containing the destination mailbox"),
- },
- withErrorHandling(({ id, mailbox, account }) => {
- const success = mailManager.moveMessage(id, mailbox, account);
- if (!success) {
- return errorResponse(`Failed to move message to "${mailbox}"`);
- }
- return successResponse(`Message moved to "${mailbox}"`);
- }, "Error moving message")
- );
- // --- batch-delete-messages ---
- server.tool(
- "batch-delete-messages",
- {
- ids: BATCH_IDS_SCHEMA,
- },
- withErrorHandling(({ ids }) => {
- const results = mailManager.batchDeleteMessages(ids);
- const successCount = results.filter((r) => r.success).length;
- const failCount = results.length - successCount;
- if (failCount === 0) {
- return successResponse(`Successfully deleted ${successCount} message(s)`);
- } else if (successCount === 0) {
- return errorResponse(`Failed to delete all ${failCount} message(s)`);
- } else {
- return successResponse(`Deleted ${successCount} message(s), ${failCount} failed`);
- }
- }, "Error batch deleting messages")
- );
- // --- batch-move-messages ---
- server.tool(
- "batch-move-messages",
- {
- ids: BATCH_IDS_SCHEMA,
- mailbox: z.string().min(1, "Destination mailbox is required"),
- account: z.string().optional().describe("Account containing the destination mailbox"),
- },
- withErrorHandling(({ ids, mailbox, account }) => {
- const results = mailManager.batchMoveMessages(ids, mailbox, account);
- const successCount = results.filter((r) => r.success).length;
- const failCount = results.length - successCount;
- if (failCount === 0) {
- return successResponse(`Successfully moved ${successCount} message(s) to "${mailbox}"`);
- } else if (successCount === 0) {
- return errorResponse(`Failed to move all ${failCount} message(s)`);
- } else {
- return successResponse(
- `Moved ${successCount} message(s) to "${mailbox}", ${failCount} failed`
- );
- }
- }, "Error batch moving messages")
- );
- // --- batch-mark-as-read ---
- server.tool(
- "batch-mark-as-read",
- {
- ids: BATCH_IDS_SCHEMA,
- },
- withErrorHandling(({ ids }) => {
- const results = mailManager.batchMarkAsRead(ids);
- const successCount = results.filter((r) => r.success).length;
- const failCount = results.length - successCount;
- if (failCount === 0) {
- return successResponse(`Successfully marked ${successCount} message(s) as read`);
- } else if (successCount === 0) {
- return errorResponse(`Failed to mark all ${failCount} message(s) as read`);
- } else {
- return successResponse(`Marked ${successCount} message(s) as read, ${failCount} failed`);
- }
- }, "Error batch marking messages as read")
- );
- // --- batch-mark-as-unread ---
- server.tool(
- "batch-mark-as-unread",
- {
- ids: BATCH_IDS_SCHEMA,
- },
- withErrorHandling(({ ids }) => {
- const results = mailManager.batchMarkAsUnread(ids);
- const successCount = results.filter((r) => r.success).length;
- const failCount = results.length - successCount;
- if (failCount === 0) {
- return successResponse(`Successfully marked ${successCount} message(s) as unread`);
- } else if (successCount === 0) {
- return errorResponse(`Failed to mark all ${failCount} message(s) as unread`);
- } else {
- return successResponse(`Marked ${successCount} message(s) as unread, ${failCount} failed`);
- }
- }, "Error batch marking messages as unread")
- );
- // --- batch-flag-messages ---
- server.tool(
- "batch-flag-messages",
- {
- ids: BATCH_IDS_SCHEMA,
- },
- withErrorHandling(({ ids }) => {
- const results = mailManager.batchFlagMessages(ids);
- const successCount = results.filter((r) => r.success).length;
- const failCount = results.length - successCount;
- if (failCount === 0) {
- return successResponse(`Successfully flagged ${successCount} message(s)`);
- } else if (successCount === 0) {
- return errorResponse(`Failed to flag all ${failCount} message(s)`);
- } else {
- return successResponse(`Flagged ${successCount} message(s), ${failCount} failed`);
- }
- }, "Error batch flagging messages")
- );
- // --- batch-unflag-messages ---
- server.tool(
- "batch-unflag-messages",
- {
- ids: BATCH_IDS_SCHEMA,
- },
- withErrorHandling(({ ids }) => {
- const results = mailManager.batchUnflagMessages(ids);
- const successCount = results.filter((r) => r.success).length;
- const failCount = results.length - successCount;
- if (failCount === 0) {
- return successResponse(`Successfully unflagged ${successCount} message(s)`);
- } else if (successCount === 0) {
- return errorResponse(`Failed to unflag all ${failCount} message(s)`);
- } else {
- return successResponse(`Unflagged ${successCount} message(s), ${failCount} failed`);
- }
- }, "Error batch unflagging messages")
- );
- // --- list-attachments ---
- server.tool(
- "list-attachments",
- {
- id: MESSAGE_ID_SCHEMA,
- },
- withErrorHandling(({ id }) => {
- const attachments = mailManager.listAttachments(id);
- if (attachments.length === 0) {
- return successResponse("No attachments found");
- }
- const attachmentList = attachments
- .map((a) => {
- const sizeKb = Math.round(a.size / 1024);
- return ` - ${a.name} (${a.mimeType}, ${sizeKb} KB)`;
- })
- .join("\n");
- return successResponse(`Found ${attachments.length} attachment(s):\n${attachmentList}`);
- }, "Error listing attachments")
- );
- // --- save-attachment ---
- server.tool(
- "save-attachment",
- {
- id: MESSAGE_ID_SCHEMA,
- attachmentName: z.string().min(1, "Attachment name is required"),
- savePath: z.string().min(1, "Save directory path is required"),
- },
- withErrorHandling(({ id, attachmentName, savePath }) => {
- const success = mailManager.saveAttachment(id, attachmentName, savePath);
- if (!success) {
- return errorResponse(`Failed to save attachment "${attachmentName}"`);
- }
- return successResponse(`Attachment "${attachmentName}" saved to ${savePath}`);
- }, "Error saving attachment")
- );
- // =============================================================================
- // Mailbox Tools
- // =============================================================================
- // --- list-mailboxes ---
- server.tool(
- "list-mailboxes",
- {
- account: z.string().optional().describe("Account to list mailboxes from"),
- },
- withErrorHandling(({ account }) => {
- const mailboxes = mailManager.listMailboxes(account);
- if (mailboxes.length === 0) {
- return successResponse("No mailboxes found");
- }
- const mailboxList = mailboxes.map((m) => ` - ${m.name} (${m.unreadCount} unread)`).join("\n");
- return successResponse(`Found ${mailboxes.length} mailbox(es):\n${mailboxList}`);
- }, "Error listing mailboxes")
- );
- // --- get-unread-count ---
- server.tool(
- "get-unread-count",
- {
- mailbox: z.string().optional().describe("Mailbox to check (default: all)"),
- account: z.string().optional().describe("Account to check"),
- },
- withErrorHandling(({ mailbox, account }) => {
- const count = mailManager.getUnreadCount(mailbox, account);
- const location = mailbox ? ` in "${mailbox}"` : "";
- return successResponse(`${count} unread message(s)${location}`);
- }, "Error getting unread count")
- );
- // --- create-mailbox ---
- server.tool(
- "create-mailbox",
- {
- name: z.string().min(1, "Mailbox name is required"),
- account: z.string().optional().describe("Account to create the mailbox in"),
- },
- withErrorHandling(({ name, account }) => {
- const success = mailManager.createMailbox(name, account);
- if (!success) {
- return errorResponse(`Failed to create mailbox "${name}"`);
- }
- return successResponse(`Mailbox "${name}" created`);
- }, "Error creating mailbox")
- );
- // --- delete-mailbox ---
- server.tool(
- "delete-mailbox",
- {
- name: z.string().min(1, "Mailbox name is required"),
- account: z.string().optional().describe("Account containing the mailbox"),
- },
- withErrorHandling(({ name, account }) => {
- const success = mailManager.deleteMailbox(name, account);
- if (!success) {
- return errorResponse(`Failed to delete mailbox "${name}"`);
- }
- return successResponse(`Mailbox "${name}" deleted`);
- }, "Error deleting mailbox")
- );
- // --- rename-mailbox ---
- server.tool(
- "rename-mailbox",
- {
- oldName: z.string().min(1, "Current mailbox name is required"),
- newName: z.string().min(1, "New mailbox name is required"),
- account: z.string().optional().describe("Account containing the mailbox"),
- },
- withErrorHandling(({ oldName, newName, account }) => {
- const success = mailManager.renameMailbox(oldName, newName, account);
- if (!success) {
- return errorResponse(`Failed to rename mailbox "${oldName}" to "${newName}"`);
- }
- return successResponse(`Mailbox renamed from "${oldName}" to "${newName}"`);
- }, "Error renaming mailbox")
- );
- // =============================================================================
- // Account Tools
- // =============================================================================
- // --- list-accounts ---
- server.tool(
- "list-accounts",
- {},
- withErrorHandling(() => {
- const accounts = mailManager.listAccounts();
- if (accounts.length === 0) {
- return successResponse("No Mail accounts found");
- }
- const accountList = accounts.map((a) => ` - ${a.name}`).join("\n");
- return successResponse(`Found ${accounts.length} account(s):\n${accountList}`);
- }, "Error listing accounts")
- );
- // =============================================================================
- // Mail Rules Tools
- // =============================================================================
- // --- list-rules ---
- server.tool(
- "list-rules",
- {},
- withErrorHandling(() => {
- const rules = mailManager.listRules();
- if (rules.length === 0) {
- return successResponse("No mail rules found");
- }
- const ruleList = rules
- .map((r) => ` - ${r.name} [${r.enabled ? "enabled" : "disabled"}]`)
- .join("\n");
- return successResponse(`Found ${rules.length} rule(s):\n${ruleList}`);
- }, "Error listing rules")
- );
- // --- enable-rule ---
- server.tool(
- "enable-rule",
- {
- name: z.string().min(1, "Rule name is required"),
- },
- withErrorHandling(({ name }) => {
- const success = mailManager.setRuleEnabled(name, true);
- if (!success) {
- return errorResponse(`Failed to enable rule "${name}"`);
- }
- return successResponse(`Rule "${name}" enabled`);
- }, "Error enabling rule")
- );
- // --- disable-rule ---
- server.tool(
- "disable-rule",
- {
- name: z.string().min(1, "Rule name is required"),
- },
- withErrorHandling(({ name }) => {
- const success = mailManager.setRuleEnabled(name, false);
- if (!success) {
- return errorResponse(`Failed to disable rule "${name}"`);
- }
- return successResponse(`Rule "${name}" disabled`);
- }, "Error disabling rule")
- );
- // =============================================================================
- // Contacts Tools
- // =============================================================================
- // --- search-contacts ---
- server.tool(
- "search-contacts",
- {
- query: z.string().min(1, "Search query is required"),
- },
- withErrorHandling(({ query }) => {
- const contacts = mailManager.searchContacts(query);
- if (contacts.length === 0) {
- return successResponse("No contacts found");
- }
- const contactList = contacts
- .map((c) => {
- const emails = c.emails.length > 0 ? c.emails.join(", ") : "no email";
- return ` - ${c.name} (${emails})`;
- })
- .join("\n");
- return successResponse(`Found ${contacts.length} contact(s):\n${contactList}`);
- }, "Error searching contacts")
- );
- // =============================================================================
- // Email Template Tools
- // =============================================================================
- // --- save-template ---
- server.tool(
- "save-template",
- {
- name: z.string().min(1, "Template name is required"),
- subject: z.string().min(1, "Subject is required"),
- body: z.string().min(1, "Body is required"),
- to: z.array(z.string()).optional().describe("Default recipients"),
- cc: z.array(z.string()).optional().describe("Default CC recipients"),
- id: z.string().optional().describe("Template ID (for updating existing template)"),
- },
- withErrorHandling(({ name, subject, body, to, cc, id }) => {
- const template = mailManager.saveTemplate(name, subject, body, to, cc, id);
- return successResponse(`Template "${template.name}" saved with ID: ${template.id}`);
- }, "Error saving template")
- );
- // --- list-templates ---
- server.tool(
- "list-templates",
- {},
- withErrorHandling(() => {
- const templates = mailManager.listTemplates();
- if (templates.length === 0) {
- return successResponse("No templates saved");
- }
- const templateList = templates
- .map((t) => ` - [${t.id}] ${t.name} — "${t.subject}"`)
- .join("\n");
- return successResponse(`Found ${templates.length} template(s):\n${templateList}`);
- }, "Error listing templates")
- );
- // --- get-template ---
- server.tool(
- "get-template",
- {
- id: z.string().min(1, "Template ID is required"),
- },
- withErrorHandling(({ id }) => {
- const template = mailManager.getTemplate(id);
- if (!template) {
- return errorResponse(`Template "${id}" not found`);
- }
- const lines = [
- `Name: ${template.name}`,
- `Subject: ${template.subject}`,
- template.to ? `To: ${template.to.join(", ")}` : null,
- template.cc ? `CC: ${template.cc.join(", ")}` : null,
- `\n${template.body}`,
- ]
- .filter(Boolean)
- .join("\n");
- return successResponse(lines);
- }, "Error getting template")
- );
- // --- delete-template ---
- server.tool(
- "delete-template",
- {
- id: z.string().min(1, "Template ID is required"),
- },
- withErrorHandling(({ id }) => {
- const success = mailManager.deleteTemplate(id);
- if (!success) {
- return errorResponse(`Template "${id}" not found`);
- }
- return successResponse(`Template "${id}" deleted`);
- }, "Error deleting template")
- );
- // --- use-template ---
- server.tool(
- "use-template",
- {
- id: z.string().min(1, "Template ID is required"),
- to: z.array(z.string()).optional().describe("Override recipients"),
- cc: z.array(z.string()).optional().describe("Override CC recipients"),
- subject: z.string().optional().describe("Override subject"),
- body: z.string().optional().describe("Override body"),
- },
- withErrorHandling(({ id, to, cc, subject, body }) => {
- const success = mailManager.useTemplate(id, { to, cc, subject, body });
- if (!success) {
- return errorResponse(`Failed to use template "${id}". Template not found or no recipients.`);
- }
- return successResponse(`Draft created from template "${id}"`);
- }, "Error using template")
- );
- // =============================================================================
- // Diagnostics Tools
- // =============================================================================
- // --- health-check ---
- server.tool(
- "health-check",
- {},
- withErrorHandling(() => {
- const result = mailManager.healthCheck();
- const statusIcon = result.healthy ? "✓" : "✗";
- const statusText = result.healthy ? "All checks passed" : "Issues detected";
- const checkLines = result.checks
- .map((c) => {
- const icon = c.passed ? "✓" : "✗";
- return ` ${icon} ${c.name}: ${c.message}`;
- })
- .join("\n");
- return successResponse(`${statusIcon} ${statusText}\n\n${checkLines}`);
- }, "Error running health check")
- );
- // --- get-mail-stats ---
- server.tool(
- "get-mail-stats",
- {},
- withErrorHandling(() => {
- const stats = mailManager.getMailStats();
- const lines: string[] = [];
- lines.push(`📊 Mail Statistics`);
- lines.push(`══════════════════`);
- lines.push(`Total messages: ${stats.totalMessages}`);
- lines.push(`Unread messages: ${stats.totalUnread}`);
- lines.push(``);
- if (stats.recentlyReceived) {
- lines.push(`📥 Recently Received:`);
- lines.push(` Last 24 hours: ${stats.recentlyReceived.last24h}`);
- lines.push(` Last 7 days: ${stats.recentlyReceived.last7d}`);
- lines.push(` Last 30 days: ${stats.recentlyReceived.last30d}`);
- lines.push(``);
- }
- if (stats.accounts.length > 0) {
- lines.push(`📁 By Account:`);
- for (const account of stats.accounts) {
- lines.push(
- ` ${account.name}: ${account.totalMessages} messages (${account.unreadMessages} unread)`
- );
- }
- }
- return successResponse(lines.join("\n"));
- }, "Error getting mail statistics")
- );
- // --- get-sync-status ---
- server.tool(
- "get-sync-status",
- {},
- withErrorHandling(() => {
- const status = mailManager.getSyncStatus();
- const lines: string[] = [];
- lines.push(`🔄 Mail Sync Status`);
- lines.push(`═══════════════════`);
- if (status.error) {
- lines.push(`Status: ⚠️ ${status.error}`);
- } else {
- lines.push(`Mail.app: ${status.recentActivity ? "Running" : "Not running"}`);
- lines.push(`Sync active: ${status.syncDetected ? "Yes" : "No"}`);
- }
- return successResponse(lines.join("\n"));
- }, "Error getting sync status")
- );
- // =============================================================================
- // Server Startup
- // =============================================================================
- /**
- * Initialize and start the MCP server.
- */
- const transport = new StdioServerTransport();
- await server.connect(transport);
|