| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722 |
- #!/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")
- .refine((val) => !isNaN(new Date(val).getTime()), {
- message: "Date string must be a valid date (e.g., 'January 1, 2026' or '2026-03-15')",
- })
- .optional();
- // Read version from package.json to keep it in sync
- const require = createRequire(import.meta.url);
- const { version } = require("../package.json");
- // =============================================================================
- // 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) {
- return {
- content: [{ type: "text", text: message }],
- };
- }
- /**
- * Creates an error MCP tool response.
- */
- function errorResponse(message) {
- return {
- content: [{ type: "text", text: message }],
- isError: true,
- };
- }
- /**
- * Wraps a tool handler with consistent error handling.
- */
- function withErrorHandling(handler, errorPrefix) {
- return async (params) => {
- 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'). Omit to search all mailboxes."),
- 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. Omit to list from all mailboxes."),
- 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 = [];
- 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 = [];
- 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);
|