index.js 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722
  1. #!/usr/bin/env node
  2. /**
  3. * Apple Mail MCP Server
  4. *
  5. * A Model Context Protocol (MCP) server that provides AI assistants
  6. * with the ability to interact with Apple Mail on macOS.
  7. *
  8. * This server exposes tools for:
  9. * - Reading and searching emails
  10. * - Sending emails
  11. * - Managing mailboxes
  12. * - Managing multiple accounts (iCloud, Gmail, Exchange, etc.)
  13. *
  14. * Architecture:
  15. * - Tool definitions are declarative (schema + handler)
  16. * - The AppleMailManager class handles all AppleScript operations
  17. * - Error handling is consistent across all tools
  18. *
  19. * @module apple-mail-mcp
  20. * @see https://modelcontextprotocol.io
  21. */
  22. import { createRequire } from "module";
  23. import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
  24. import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
  25. import { z } from "zod";
  26. import { AppleMailManager } from "./services/appleMailManager.js";
  27. // =============================================================================
  28. // Shared Validation Schemas
  29. // =============================================================================
  30. /** Message IDs in Apple Mail are always numeric. Enforce this at the schema level
  31. * to prevent AppleScript injection via the `whose id is ${id}` interpolation. */
  32. const MESSAGE_ID_SCHEMA = z.string().regex(/^\d+$/, "Message ID must be numeric");
  33. /** Batch operations are capped to prevent unbounded loops / DoS. */
  34. const BATCH_IDS_SCHEMA = z
  35. .array(MESSAGE_ID_SCHEMA)
  36. .min(1, "At least one message ID is required")
  37. .max(100, "Cannot process more than 100 messages in a single batch");
  38. /** Date filter strings must look like natural-language dates (e.g. "March 1, 2026").
  39. * Block characters that could escape an AppleScript `date "..."` literal. */
  40. const DATE_FILTER_SCHEMA = z
  41. .string()
  42. .regex(/^[a-zA-Z0-9 ,/\-:]+$/, "Date must contain only alphanumeric characters, spaces, commas, slashes, hyphens, and colons")
  43. .refine((val) => !isNaN(new Date(val).getTime()), {
  44. message: "Date string must be a valid date (e.g., 'January 1, 2026' or '2026-03-15')",
  45. })
  46. .optional();
  47. // Read version from package.json to keep it in sync
  48. const require = createRequire(import.meta.url);
  49. const { version } = require("../package.json");
  50. // =============================================================================
  51. // Server Initialization
  52. // =============================================================================
  53. /**
  54. * MCP server instance configured for Apple Mail operations.
  55. */
  56. const server = new McpServer({
  57. name: "apple-mail",
  58. version,
  59. description: "MCP server for managing Apple Mail - read, search, send, and organize emails",
  60. });
  61. /**
  62. * Singleton instance of the Apple Mail manager.
  63. * Handles all AppleScript execution and mail operations.
  64. */
  65. const mailManager = new AppleMailManager();
  66. // =============================================================================
  67. // Response Helpers
  68. // =============================================================================
  69. /**
  70. * Creates a successful MCP tool response.
  71. */
  72. function successResponse(message) {
  73. return {
  74. content: [{ type: "text", text: message }],
  75. };
  76. }
  77. /**
  78. * Creates an error MCP tool response.
  79. */
  80. function errorResponse(message) {
  81. return {
  82. content: [{ type: "text", text: message }],
  83. isError: true,
  84. };
  85. }
  86. /**
  87. * Wraps a tool handler with consistent error handling.
  88. */
  89. function withErrorHandling(handler, errorPrefix) {
  90. return async (params) => {
  91. try {
  92. return handler(params);
  93. }
  94. catch (error) {
  95. const message = error instanceof Error ? error.message : "Unknown error";
  96. return errorResponse(`${errorPrefix}: ${message}`);
  97. }
  98. };
  99. }
  100. // =============================================================================
  101. // Message Tools
  102. // =============================================================================
  103. // --- search-messages ---
  104. server.tool("search-messages", {
  105. query: z.string().optional().describe("Text to search for in subject, sender, or content"),
  106. from: z.string().optional().describe("Filter by sender email address"),
  107. subject: z.string().optional().describe("Filter by subject line"),
  108. mailbox: z
  109. .string()
  110. .optional()
  111. .describe("Mailbox to search in (e.g., 'INBOX'). Omit to search all mailboxes."),
  112. account: z.string().optional().describe("Account to search in (omit to search all accounts)"),
  113. isRead: z.boolean().optional().describe("Filter by read status"),
  114. isFlagged: z.boolean().optional().describe("Filter by flagged status"),
  115. dateFrom: DATE_FILTER_SCHEMA.describe("Start date filter (e.g., 'January 1, 2026')"),
  116. dateTo: DATE_FILTER_SCHEMA.describe("End date filter (e.g., 'March 1, 2026')"),
  117. limit: z.number().optional().describe("Maximum number of results (default: 50)"),
  118. }, withErrorHandling(({ query, mailbox, account, limit = 50, dateFrom, dateTo }) => {
  119. const messages = mailManager.searchMessages(query, mailbox, account, limit, dateFrom, dateTo);
  120. if (messages.length === 0) {
  121. return successResponse("No messages found matching criteria");
  122. }
  123. const messageList = messages
  124. .map((m) => ` - ID: ${m.id} | ${m.dateReceived.toLocaleDateString()} | ${m.subject} (from: ${m.sender}) [${m.isRead ? "read" : "unread"}]`)
  125. .join("\n");
  126. return successResponse(`Found ${messages.length} message(s):\n${messageList}`);
  127. }, "Error searching messages"));
  128. // --- get-message ---
  129. server.tool("get-message", {
  130. id: MESSAGE_ID_SCHEMA,
  131. preferHtml: z.boolean().optional().describe("Return HTML source instead of plain text"),
  132. }, withErrorHandling(({ id, preferHtml }) => {
  133. const content = mailManager.getMessageContent(id);
  134. if (!content) {
  135. return errorResponse(`Message with ID "${id}" not found`);
  136. }
  137. if (preferHtml && content.htmlContent) {
  138. return successResponse(`Subject: ${content.subject}\n\n${content.htmlContent}`);
  139. }
  140. return successResponse(`Subject: ${content.subject}\n\n${content.plainText}`);
  141. }, "Error retrieving message"));
  142. // --- list-messages ---
  143. server.tool("list-messages", {
  144. mailbox: z
  145. .string()
  146. .optional()
  147. .describe("Mailbox to list messages from. Omit to list from all mailboxes."),
  148. account: z.string().optional().describe("Account to list messages from"),
  149. limit: z.number().optional().describe("Maximum number of messages (default: 50)"),
  150. offset: z.number().optional().describe("Number of messages to skip (for pagination)"),
  151. from: z.string().optional().describe("Filter by sender email address or name"),
  152. unreadOnly: z.boolean().optional().describe("Only show unread messages"),
  153. }, withErrorHandling(({ mailbox, account, limit = 50, offset = 0, from }) => {
  154. const messages = mailManager.listMessages(mailbox, account, limit, from, offset);
  155. if (messages.length === 0) {
  156. return successResponse("No messages found");
  157. }
  158. const messageList = messages
  159. .map((m) => ` - ID: ${m.id} | ${m.dateReceived.toLocaleDateString()} | ${m.subject} (from: ${m.sender})`)
  160. .join("\n");
  161. return successResponse(`Found ${messages.length} message(s):\n${messageList}`);
  162. }, "Error listing messages"));
  163. // --- send-email ---
  164. server.tool("send-email", {
  165. to: z.array(z.string()).min(1, "At least one recipient is required"),
  166. subject: z.string().min(1, "Subject is required"),
  167. body: z.string().min(1, "Body is required"),
  168. cc: z.array(z.string()).optional().describe("CC recipients"),
  169. bcc: z.array(z.string()).optional().describe("BCC recipients"),
  170. account: z.string().optional().describe("Account to send from"),
  171. attachments: z
  172. .array(z.string())
  173. .max(20, "Cannot attach more than 20 files")
  174. .optional()
  175. .describe("Absolute file paths to attach (e.g., ['/Users/me/report.pdf'])"),
  176. }, withErrorHandling(({ to, subject, body, cc, bcc, account, attachments }) => {
  177. const success = mailManager.sendEmail(to, subject, body, cc, bcc, account, attachments);
  178. if (!success) {
  179. return errorResponse("Failed to send email. Check Mail.app configuration.");
  180. }
  181. const attachInfo = attachments?.length ? ` with ${attachments.length} attachment(s)` : "";
  182. return successResponse(`Email sent to ${to.join(", ")}${attachInfo}`);
  183. }, "Error sending email"));
  184. // --- send-serial-email ---
  185. server.tool("send-serial-email", {
  186. recipients: z
  187. .array(z.object({
  188. email: z.string().min(1, "Recipient email is required"),
  189. variables: z
  190. .record(z.string())
  191. .describe("Placeholder values, e.g. { Name: 'Alice', Company: 'Acme' }"),
  192. }))
  193. .min(1, "At least one recipient is required")
  194. .max(100, "Cannot send to more than 100 recipients in a single batch")
  195. .describe("List of recipients with personalization variables (max 100)"),
  196. subject: z
  197. .string()
  198. .min(1, "Subject is required")
  199. .describe("Subject line — use {{Key}} for placeholders"),
  200. body: z
  201. .string()
  202. .min(1, "Body is required")
  203. .describe("Email body — use {{Key}} for placeholders"),
  204. account: z.string().optional().describe("Account to send from"),
  205. delayMs: z
  206. .number()
  207. .min(0)
  208. .max(10000)
  209. .optional()
  210. .describe("Delay between sends in ms (default: 500, max: 10000)"),
  211. }, withErrorHandling(({ recipients, subject, body, account, delayMs }) => {
  212. const results = mailManager.sendSerialEmail(recipients, subject, body, account, delayMs);
  213. const successCount = results.filter((r) => r.success).length;
  214. const failCount = results.length - successCount;
  215. const details = results
  216. .map((r) => ` - ${r.email}: ${r.success ? "sent" : `FAILED (${r.error})`}`)
  217. .join("\n");
  218. if (failCount === 0) {
  219. return successResponse(`Successfully sent ${successCount} email(s):\n${details}`);
  220. }
  221. else if (successCount === 0) {
  222. return errorResponse(`Failed to send all ${failCount} email(s):\n${details}`);
  223. }
  224. else {
  225. return successResponse(`Sent ${successCount} of ${results.length} email(s), ${failCount} failed:\n${details}`);
  226. }
  227. }, "Error sending serial emails"));
  228. // --- create-draft ---
  229. server.tool("create-draft", {
  230. to: z.array(z.string()).min(1, "At least one recipient is required"),
  231. subject: z.string().min(1, "Subject is required"),
  232. body: z.string().min(1, "Body is required"),
  233. cc: z.array(z.string()).optional().describe("CC recipients"),
  234. bcc: z.array(z.string()).optional().describe("BCC recipients"),
  235. account: z.string().optional().describe("Account to create draft in"),
  236. attachments: z
  237. .array(z.string())
  238. .max(20, "Cannot attach more than 20 files")
  239. .optional()
  240. .describe("Absolute file paths to attach (e.g., ['/Users/me/report.pdf'])"),
  241. }, withErrorHandling(({ to, subject, body, cc, bcc, account, attachments }) => {
  242. const success = mailManager.createDraft(to, subject, body, cc, bcc, account, attachments);
  243. if (!success) {
  244. return errorResponse("Failed to create draft. Check Mail.app configuration.");
  245. }
  246. const attachInfo = attachments?.length ? ` with ${attachments.length} attachment(s)` : "";
  247. return successResponse(`Draft created for ${to.join(", ")}${attachInfo}`);
  248. }, "Error creating draft"));
  249. // --- reply-to-message ---
  250. server.tool("reply-to-message", {
  251. id: MESSAGE_ID_SCHEMA,
  252. body: z.string().min(1, "Reply body is required"),
  253. replyAll: z.boolean().optional().default(false).describe("Reply to all recipients"),
  254. send: z.boolean().optional().default(true).describe("Send immediately (false = save as draft)"),
  255. }, withErrorHandling(({ id, body, replyAll, send }) => {
  256. const success = mailManager.replyToMessage(id, body, replyAll, send);
  257. if (!success) {
  258. return errorResponse(`Failed to reply to message "${id}"`);
  259. }
  260. return successResponse(send ? "Reply sent" : "Reply saved as draft");
  261. }, "Error replying to message"));
  262. // --- forward-message ---
  263. server.tool("forward-message", {
  264. id: MESSAGE_ID_SCHEMA,
  265. to: z.array(z.string()).min(1, "At least one recipient is required"),
  266. body: z.string().optional().describe("Optional message to prepend"),
  267. send: z.boolean().optional().default(true).describe("Send immediately (false = save as draft)"),
  268. }, withErrorHandling(({ id, to, body, send }) => {
  269. const success = mailManager.forwardMessage(id, to, body, send);
  270. if (!success) {
  271. return errorResponse(`Failed to forward message "${id}"`);
  272. }
  273. return successResponse(send ? `Message forwarded to ${to.join(", ")}` : "Forward saved as draft");
  274. }, "Error forwarding message"));
  275. // --- mark-as-read ---
  276. server.tool("mark-as-read", {
  277. id: MESSAGE_ID_SCHEMA,
  278. }, withErrorHandling(({ id }) => {
  279. const success = mailManager.markAsRead(id);
  280. if (!success) {
  281. return errorResponse(`Failed to mark message "${id}" as read`);
  282. }
  283. return successResponse("Message marked as read");
  284. }, "Error marking message as read"));
  285. // --- mark-as-unread ---
  286. server.tool("mark-as-unread", {
  287. id: MESSAGE_ID_SCHEMA,
  288. }, withErrorHandling(({ id }) => {
  289. const success = mailManager.markAsUnread(id);
  290. if (!success) {
  291. return errorResponse(`Failed to mark message "${id}" as unread`);
  292. }
  293. return successResponse("Message marked as unread");
  294. }, "Error marking message as unread"));
  295. // --- flag-message ---
  296. server.tool("flag-message", {
  297. id: MESSAGE_ID_SCHEMA,
  298. }, withErrorHandling(({ id }) => {
  299. const success = mailManager.flagMessage(id);
  300. if (!success) {
  301. return errorResponse(`Failed to flag message "${id}"`);
  302. }
  303. return successResponse("Message flagged");
  304. }, "Error flagging message"));
  305. // --- unflag-message ---
  306. server.tool("unflag-message", {
  307. id: MESSAGE_ID_SCHEMA,
  308. }, withErrorHandling(({ id }) => {
  309. const success = mailManager.unflagMessage(id);
  310. if (!success) {
  311. return errorResponse(`Failed to unflag message "${id}"`);
  312. }
  313. return successResponse("Message unflagged");
  314. }, "Error unflagging message"));
  315. // --- delete-message ---
  316. server.tool("delete-message", {
  317. id: MESSAGE_ID_SCHEMA,
  318. }, withErrorHandling(({ id }) => {
  319. const success = mailManager.deleteMessage(id);
  320. if (!success) {
  321. return errorResponse(`Failed to delete message "${id}"`);
  322. }
  323. return successResponse("Message deleted");
  324. }, "Error deleting message"));
  325. // --- move-message ---
  326. server.tool("move-message", {
  327. id: MESSAGE_ID_SCHEMA,
  328. mailbox: z.string().min(1, "Destination mailbox is required"),
  329. account: z.string().optional().describe("Account containing the destination mailbox"),
  330. }, withErrorHandling(({ id, mailbox, account }) => {
  331. const success = mailManager.moveMessage(id, mailbox, account);
  332. if (!success) {
  333. return errorResponse(`Failed to move message to "${mailbox}"`);
  334. }
  335. return successResponse(`Message moved to "${mailbox}"`);
  336. }, "Error moving message"));
  337. // --- batch-delete-messages ---
  338. server.tool("batch-delete-messages", {
  339. ids: BATCH_IDS_SCHEMA,
  340. }, withErrorHandling(({ ids }) => {
  341. const results = mailManager.batchDeleteMessages(ids);
  342. const successCount = results.filter((r) => r.success).length;
  343. const failCount = results.length - successCount;
  344. if (failCount === 0) {
  345. return successResponse(`Successfully deleted ${successCount} message(s)`);
  346. }
  347. else if (successCount === 0) {
  348. return errorResponse(`Failed to delete all ${failCount} message(s)`);
  349. }
  350. else {
  351. return successResponse(`Deleted ${successCount} message(s), ${failCount} failed`);
  352. }
  353. }, "Error batch deleting messages"));
  354. // --- batch-move-messages ---
  355. server.tool("batch-move-messages", {
  356. ids: BATCH_IDS_SCHEMA,
  357. mailbox: z.string().min(1, "Destination mailbox is required"),
  358. account: z.string().optional().describe("Account containing the destination mailbox"),
  359. }, withErrorHandling(({ ids, mailbox, account }) => {
  360. const results = mailManager.batchMoveMessages(ids, mailbox, account);
  361. const successCount = results.filter((r) => r.success).length;
  362. const failCount = results.length - successCount;
  363. if (failCount === 0) {
  364. return successResponse(`Successfully moved ${successCount} message(s) to "${mailbox}"`);
  365. }
  366. else if (successCount === 0) {
  367. return errorResponse(`Failed to move all ${failCount} message(s)`);
  368. }
  369. else {
  370. return successResponse(`Moved ${successCount} message(s) to "${mailbox}", ${failCount} failed`);
  371. }
  372. }, "Error batch moving messages"));
  373. // --- batch-mark-as-read ---
  374. server.tool("batch-mark-as-read", {
  375. ids: BATCH_IDS_SCHEMA,
  376. }, withErrorHandling(({ ids }) => {
  377. const results = mailManager.batchMarkAsRead(ids);
  378. const successCount = results.filter((r) => r.success).length;
  379. const failCount = results.length - successCount;
  380. if (failCount === 0) {
  381. return successResponse(`Successfully marked ${successCount} message(s) as read`);
  382. }
  383. else if (successCount === 0) {
  384. return errorResponse(`Failed to mark all ${failCount} message(s) as read`);
  385. }
  386. else {
  387. return successResponse(`Marked ${successCount} message(s) as read, ${failCount} failed`);
  388. }
  389. }, "Error batch marking messages as read"));
  390. // --- batch-mark-as-unread ---
  391. server.tool("batch-mark-as-unread", {
  392. ids: BATCH_IDS_SCHEMA,
  393. }, withErrorHandling(({ ids }) => {
  394. const results = mailManager.batchMarkAsUnread(ids);
  395. const successCount = results.filter((r) => r.success).length;
  396. const failCount = results.length - successCount;
  397. if (failCount === 0) {
  398. return successResponse(`Successfully marked ${successCount} message(s) as unread`);
  399. }
  400. else if (successCount === 0) {
  401. return errorResponse(`Failed to mark all ${failCount} message(s) as unread`);
  402. }
  403. else {
  404. return successResponse(`Marked ${successCount} message(s) as unread, ${failCount} failed`);
  405. }
  406. }, "Error batch marking messages as unread"));
  407. // --- batch-flag-messages ---
  408. server.tool("batch-flag-messages", {
  409. ids: BATCH_IDS_SCHEMA,
  410. }, withErrorHandling(({ ids }) => {
  411. const results = mailManager.batchFlagMessages(ids);
  412. const successCount = results.filter((r) => r.success).length;
  413. const failCount = results.length - successCount;
  414. if (failCount === 0) {
  415. return successResponse(`Successfully flagged ${successCount} message(s)`);
  416. }
  417. else if (successCount === 0) {
  418. return errorResponse(`Failed to flag all ${failCount} message(s)`);
  419. }
  420. else {
  421. return successResponse(`Flagged ${successCount} message(s), ${failCount} failed`);
  422. }
  423. }, "Error batch flagging messages"));
  424. // --- batch-unflag-messages ---
  425. server.tool("batch-unflag-messages", {
  426. ids: BATCH_IDS_SCHEMA,
  427. }, withErrorHandling(({ ids }) => {
  428. const results = mailManager.batchUnflagMessages(ids);
  429. const successCount = results.filter((r) => r.success).length;
  430. const failCount = results.length - successCount;
  431. if (failCount === 0) {
  432. return successResponse(`Successfully unflagged ${successCount} message(s)`);
  433. }
  434. else if (successCount === 0) {
  435. return errorResponse(`Failed to unflag all ${failCount} message(s)`);
  436. }
  437. else {
  438. return successResponse(`Unflagged ${successCount} message(s), ${failCount} failed`);
  439. }
  440. }, "Error batch unflagging messages"));
  441. // --- list-attachments ---
  442. server.tool("list-attachments", {
  443. id: MESSAGE_ID_SCHEMA,
  444. }, withErrorHandling(({ id }) => {
  445. const attachments = mailManager.listAttachments(id);
  446. if (attachments.length === 0) {
  447. return successResponse("No attachments found");
  448. }
  449. const attachmentList = attachments
  450. .map((a) => {
  451. const sizeKb = Math.round(a.size / 1024);
  452. return ` - ${a.name} (${a.mimeType}, ${sizeKb} KB)`;
  453. })
  454. .join("\n");
  455. return successResponse(`Found ${attachments.length} attachment(s):\n${attachmentList}`);
  456. }, "Error listing attachments"));
  457. // --- save-attachment ---
  458. server.tool("save-attachment", {
  459. id: MESSAGE_ID_SCHEMA,
  460. attachmentName: z.string().min(1, "Attachment name is required"),
  461. savePath: z.string().min(1, "Save directory path is required"),
  462. }, withErrorHandling(({ id, attachmentName, savePath }) => {
  463. const success = mailManager.saveAttachment(id, attachmentName, savePath);
  464. if (!success) {
  465. return errorResponse(`Failed to save attachment "${attachmentName}"`);
  466. }
  467. return successResponse(`Attachment "${attachmentName}" saved to ${savePath}`);
  468. }, "Error saving attachment"));
  469. // =============================================================================
  470. // Mailbox Tools
  471. // =============================================================================
  472. // --- list-mailboxes ---
  473. server.tool("list-mailboxes", {
  474. account: z.string().optional().describe("Account to list mailboxes from"),
  475. }, withErrorHandling(({ account }) => {
  476. const mailboxes = mailManager.listMailboxes(account);
  477. if (mailboxes.length === 0) {
  478. return successResponse("No mailboxes found");
  479. }
  480. const mailboxList = mailboxes.map((m) => ` - ${m.name} (${m.unreadCount} unread)`).join("\n");
  481. return successResponse(`Found ${mailboxes.length} mailbox(es):\n${mailboxList}`);
  482. }, "Error listing mailboxes"));
  483. // --- get-unread-count ---
  484. server.tool("get-unread-count", {
  485. mailbox: z.string().optional().describe("Mailbox to check (default: all)"),
  486. account: z.string().optional().describe("Account to check"),
  487. }, withErrorHandling(({ mailbox, account }) => {
  488. const count = mailManager.getUnreadCount(mailbox, account);
  489. const location = mailbox ? ` in "${mailbox}"` : "";
  490. return successResponse(`${count} unread message(s)${location}`);
  491. }, "Error getting unread count"));
  492. // --- create-mailbox ---
  493. server.tool("create-mailbox", {
  494. name: z.string().min(1, "Mailbox name is required"),
  495. account: z.string().optional().describe("Account to create the mailbox in"),
  496. }, withErrorHandling(({ name, account }) => {
  497. const success = mailManager.createMailbox(name, account);
  498. if (!success) {
  499. return errorResponse(`Failed to create mailbox "${name}"`);
  500. }
  501. return successResponse(`Mailbox "${name}" created`);
  502. }, "Error creating mailbox"));
  503. // --- delete-mailbox ---
  504. server.tool("delete-mailbox", {
  505. name: z.string().min(1, "Mailbox name is required"),
  506. account: z.string().optional().describe("Account containing the mailbox"),
  507. }, withErrorHandling(({ name, account }) => {
  508. const success = mailManager.deleteMailbox(name, account);
  509. if (!success) {
  510. return errorResponse(`Failed to delete mailbox "${name}"`);
  511. }
  512. return successResponse(`Mailbox "${name}" deleted`);
  513. }, "Error deleting mailbox"));
  514. // --- rename-mailbox ---
  515. server.tool("rename-mailbox", {
  516. oldName: z.string().min(1, "Current mailbox name is required"),
  517. newName: z.string().min(1, "New mailbox name is required"),
  518. account: z.string().optional().describe("Account containing the mailbox"),
  519. }, withErrorHandling(({ oldName, newName, account }) => {
  520. const success = mailManager.renameMailbox(oldName, newName, account);
  521. if (!success) {
  522. return errorResponse(`Failed to rename mailbox "${oldName}" to "${newName}"`);
  523. }
  524. return successResponse(`Mailbox renamed from "${oldName}" to "${newName}"`);
  525. }, "Error renaming mailbox"));
  526. // =============================================================================
  527. // Account Tools
  528. // =============================================================================
  529. // --- list-accounts ---
  530. server.tool("list-accounts", {}, withErrorHandling(() => {
  531. const accounts = mailManager.listAccounts();
  532. if (accounts.length === 0) {
  533. return successResponse("No Mail accounts found");
  534. }
  535. const accountList = accounts.map((a) => ` - ${a.name}`).join("\n");
  536. return successResponse(`Found ${accounts.length} account(s):\n${accountList}`);
  537. }, "Error listing accounts"));
  538. // =============================================================================
  539. // Mail Rules Tools
  540. // =============================================================================
  541. // --- list-rules ---
  542. server.tool("list-rules", {}, withErrorHandling(() => {
  543. const rules = mailManager.listRules();
  544. if (rules.length === 0) {
  545. return successResponse("No mail rules found");
  546. }
  547. const ruleList = rules
  548. .map((r) => ` - ${r.name} [${r.enabled ? "enabled" : "disabled"}]`)
  549. .join("\n");
  550. return successResponse(`Found ${rules.length} rule(s):\n${ruleList}`);
  551. }, "Error listing rules"));
  552. // --- enable-rule ---
  553. server.tool("enable-rule", {
  554. name: z.string().min(1, "Rule name is required"),
  555. }, withErrorHandling(({ name }) => {
  556. const success = mailManager.setRuleEnabled(name, true);
  557. if (!success) {
  558. return errorResponse(`Failed to enable rule "${name}"`);
  559. }
  560. return successResponse(`Rule "${name}" enabled`);
  561. }, "Error enabling rule"));
  562. // --- disable-rule ---
  563. server.tool("disable-rule", {
  564. name: z.string().min(1, "Rule name is required"),
  565. }, withErrorHandling(({ name }) => {
  566. const success = mailManager.setRuleEnabled(name, false);
  567. if (!success) {
  568. return errorResponse(`Failed to disable rule "${name}"`);
  569. }
  570. return successResponse(`Rule "${name}" disabled`);
  571. }, "Error disabling rule"));
  572. // =============================================================================
  573. // Contacts Tools
  574. // =============================================================================
  575. // --- search-contacts ---
  576. server.tool("search-contacts", {
  577. query: z.string().min(1, "Search query is required"),
  578. }, withErrorHandling(({ query }) => {
  579. const contacts = mailManager.searchContacts(query);
  580. if (contacts.length === 0) {
  581. return successResponse("No contacts found");
  582. }
  583. const contactList = contacts
  584. .map((c) => {
  585. const emails = c.emails.length > 0 ? c.emails.join(", ") : "no email";
  586. return ` - ${c.name} (${emails})`;
  587. })
  588. .join("\n");
  589. return successResponse(`Found ${contacts.length} contact(s):\n${contactList}`);
  590. }, "Error searching contacts"));
  591. // =============================================================================
  592. // Email Template Tools
  593. // =============================================================================
  594. // --- save-template ---
  595. server.tool("save-template", {
  596. name: z.string().min(1, "Template name is required"),
  597. subject: z.string().min(1, "Subject is required"),
  598. body: z.string().min(1, "Body is required"),
  599. to: z.array(z.string()).optional().describe("Default recipients"),
  600. cc: z.array(z.string()).optional().describe("Default CC recipients"),
  601. id: z.string().optional().describe("Template ID (for updating existing template)"),
  602. }, withErrorHandling(({ name, subject, body, to, cc, id }) => {
  603. const template = mailManager.saveTemplate(name, subject, body, to, cc, id);
  604. return successResponse(`Template "${template.name}" saved with ID: ${template.id}`);
  605. }, "Error saving template"));
  606. // --- list-templates ---
  607. server.tool("list-templates", {}, withErrorHandling(() => {
  608. const templates = mailManager.listTemplates();
  609. if (templates.length === 0) {
  610. return successResponse("No templates saved");
  611. }
  612. const templateList = templates
  613. .map((t) => ` - [${t.id}] ${t.name} — "${t.subject}"`)
  614. .join("\n");
  615. return successResponse(`Found ${templates.length} template(s):\n${templateList}`);
  616. }, "Error listing templates"));
  617. // --- get-template ---
  618. server.tool("get-template", {
  619. id: z.string().min(1, "Template ID is required"),
  620. }, withErrorHandling(({ id }) => {
  621. const template = mailManager.getTemplate(id);
  622. if (!template) {
  623. return errorResponse(`Template "${id}" not found`);
  624. }
  625. const lines = [
  626. `Name: ${template.name}`,
  627. `Subject: ${template.subject}`,
  628. template.to ? `To: ${template.to.join(", ")}` : null,
  629. template.cc ? `CC: ${template.cc.join(", ")}` : null,
  630. `\n${template.body}`,
  631. ]
  632. .filter(Boolean)
  633. .join("\n");
  634. return successResponse(lines);
  635. }, "Error getting template"));
  636. // --- delete-template ---
  637. server.tool("delete-template", {
  638. id: z.string().min(1, "Template ID is required"),
  639. }, withErrorHandling(({ id }) => {
  640. const success = mailManager.deleteTemplate(id);
  641. if (!success) {
  642. return errorResponse(`Template "${id}" not found`);
  643. }
  644. return successResponse(`Template "${id}" deleted`);
  645. }, "Error deleting template"));
  646. // --- use-template ---
  647. server.tool("use-template", {
  648. id: z.string().min(1, "Template ID is required"),
  649. to: z.array(z.string()).optional().describe("Override recipients"),
  650. cc: z.array(z.string()).optional().describe("Override CC recipients"),
  651. subject: z.string().optional().describe("Override subject"),
  652. body: z.string().optional().describe("Override body"),
  653. }, withErrorHandling(({ id, to, cc, subject, body }) => {
  654. const success = mailManager.useTemplate(id, { to, cc, subject, body });
  655. if (!success) {
  656. return errorResponse(`Failed to use template "${id}". Template not found or no recipients.`);
  657. }
  658. return successResponse(`Draft created from template "${id}"`);
  659. }, "Error using template"));
  660. // =============================================================================
  661. // Diagnostics Tools
  662. // =============================================================================
  663. // --- health-check ---
  664. server.tool("health-check", {}, withErrorHandling(() => {
  665. const result = mailManager.healthCheck();
  666. const statusIcon = result.healthy ? "✓" : "✗";
  667. const statusText = result.healthy ? "All checks passed" : "Issues detected";
  668. const checkLines = result.checks
  669. .map((c) => {
  670. const icon = c.passed ? "✓" : "✗";
  671. return ` ${icon} ${c.name}: ${c.message}`;
  672. })
  673. .join("\n");
  674. return successResponse(`${statusIcon} ${statusText}\n\n${checkLines}`);
  675. }, "Error running health check"));
  676. // --- get-mail-stats ---
  677. server.tool("get-mail-stats", {}, withErrorHandling(() => {
  678. const stats = mailManager.getMailStats();
  679. const lines = [];
  680. lines.push(`📊 Mail Statistics`);
  681. lines.push(`══════════════════`);
  682. lines.push(`Total messages: ${stats.totalMessages}`);
  683. lines.push(`Unread messages: ${stats.totalUnread}`);
  684. lines.push(``);
  685. if (stats.recentlyReceived) {
  686. lines.push(`📥 Recently Received:`);
  687. lines.push(` Last 24 hours: ${stats.recentlyReceived.last24h}`);
  688. lines.push(` Last 7 days: ${stats.recentlyReceived.last7d}`);
  689. lines.push(` Last 30 days: ${stats.recentlyReceived.last30d}`);
  690. lines.push(``);
  691. }
  692. if (stats.accounts.length > 0) {
  693. lines.push(`📁 By Account:`);
  694. for (const account of stats.accounts) {
  695. lines.push(` ${account.name}: ${account.totalMessages} messages (${account.unreadMessages} unread)`);
  696. }
  697. }
  698. return successResponse(lines.join("\n"));
  699. }, "Error getting mail statistics"));
  700. // --- get-sync-status ---
  701. server.tool("get-sync-status", {}, withErrorHandling(() => {
  702. const status = mailManager.getSyncStatus();
  703. const lines = [];
  704. lines.push(`🔄 Mail Sync Status`);
  705. lines.push(`═══════════════════`);
  706. if (status.error) {
  707. lines.push(`Status: ⚠️ ${status.error}`);
  708. }
  709. else {
  710. lines.push(`Mail.app: ${status.recentActivity ? "Running" : "Not running"}`);
  711. lines.push(`Sync active: ${status.syncDetected ? "Yes" : "No"}`);
  712. }
  713. return successResponse(lines.join("\n"));
  714. }, "Error getting sync status"));
  715. // =============================================================================
  716. // Server Startup
  717. // =============================================================================
  718. /**
  719. * Initialize and start the MCP server.
  720. */
  721. const transport = new StdioServerTransport();
  722. await server.connect(transport);