apple-mail-mcp 2.2.0 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +51 -8
- package/build/cli.d.ts +24 -0
- package/build/cli.d.ts.map +1 -0
- package/build/cli.js +165 -0
- package/build/index.js +579 -209
- package/build/services/smtpMailer.d.ts +33 -2
- package/build/services/smtpMailer.d.ts.map +1 -1
- package/build/services/smtpMailer.js +39 -2
- package/build/tools/doctor.js +5 -5
- package/package.json +3 -2
package/build/index.js
CHANGED
|
@@ -26,7 +26,7 @@ import { z } from "zod";
|
|
|
26
26
|
import { AppleMailManager, isPathWithinAllowedRoots } from "./services/appleMailManager.js";
|
|
27
27
|
import { writeFileSync } from "fs";
|
|
28
28
|
import { resolve as resolvePath, join as joinPath } from "path";
|
|
29
|
-
import { sendViaSmtp } from "./services/smtpMailer.js";
|
|
29
|
+
import { sendViaSmtp, shouldUseSmtp } from "./services/smtpMailer.js";
|
|
30
30
|
import { isImapAccount, resolveImapConfigs, imapSearchMessages, imapListMessages, imapUnreadCount, imapListMailboxes, imapMailStats, imapListAttachments, imapFetchAttachment, imapBatchMarkRead, imapBatchMarkUnread, imapBatchFlag, imapBatchUnflag, imapBatchDelete, imapBatchMove, imapThread, imapCreateMailbox, imapDeleteMailbox, imapRenameMailbox, imapGetMessage, imapMarkRead, imapMarkUnread, imapFlagMessage, imapUnflagMessage, imapMoveMessageById, imapDeleteMessageById, } from "./services/imapClient.js";
|
|
31
31
|
import { successResponse, errorResponse, partialCoverageBlock, withErrorHandling, messageSummary, } from "./tools/respond.js";
|
|
32
32
|
import { routeMessage } from "./services/messageRouter.js";
|
|
@@ -77,6 +77,41 @@ const ATTACHMENTS_SCHEMA = z
|
|
|
77
77
|
.optional()
|
|
78
78
|
.describe("Files to attach: absolute paths (e.g. '/Users/me/report.pdf') and/or " +
|
|
79
79
|
"inline {filename, contentBase64} objects for content not on disk.");
|
|
80
|
+
// =============================================================================
|
|
81
|
+
// Shared Output Schemas (MCP outputSchema)
|
|
82
|
+
//
|
|
83
|
+
// Every tool below declares an MCP `outputSchema` so clients can know and
|
|
84
|
+
// validate the output shape. The SDK requires `structuredContent` on every
|
|
85
|
+
// success result once an outputSchema is present and validates it against the
|
|
86
|
+
// schema, so these schemas are intentionally PERMISSIVE — fields are optional
|
|
87
|
+
// unless they are provably always present on every success path, no `.strict()`
|
|
88
|
+
// is used (extra keys pass), and rows that vary across the AppleScript and IMAP
|
|
89
|
+
// backends use loose element types. Error responses are exempt from validation.
|
|
90
|
+
// =============================================================================
|
|
91
|
+
/** A message row in a list/search result. Loose: the AppleScript path emits a
|
|
92
|
+
* messageSummary while the IMAP path emits its own record, so allow any keys. */
|
|
93
|
+
const MESSAGE_ROW_SCHEMA = z.object({}).passthrough();
|
|
94
|
+
/** Shape returned by list/search style tools (messages + count + optional
|
|
95
|
+
* partial-coverage diagnostics). Diagnostics only appear on the AppleScript
|
|
96
|
+
* path, so they are optional. */
|
|
97
|
+
const LIST_OUTPUT_SCHEMA = {
|
|
98
|
+
messages: z.array(MESSAGE_ROW_SCHEMA).optional(),
|
|
99
|
+
count: z.number().optional(),
|
|
100
|
+
partial: z.boolean().optional(),
|
|
101
|
+
skippedLargeMailboxes: z.array(z.string()).optional(),
|
|
102
|
+
notSearchedMailboxes: z.array(z.string()).optional(),
|
|
103
|
+
timedOutAccounts: z.array(z.string()).optional(),
|
|
104
|
+
};
|
|
105
|
+
/** Shape returned by the batch count tools. */
|
|
106
|
+
const BATCH_COUNT_OUTPUT_SCHEMA = {
|
|
107
|
+
ok: z.boolean().optional(),
|
|
108
|
+
success: z.number().optional(),
|
|
109
|
+
failed: z.number().optional(),
|
|
110
|
+
mailbox: z.string().optional(),
|
|
111
|
+
};
|
|
112
|
+
/** A health/doctor check item — loose to accept both health-check
|
|
113
|
+
* ({name, passed, message}) and doctor ({name, status, detail}) items. */
|
|
114
|
+
const CHECK_ITEM_SCHEMA = z.object({}).passthrough();
|
|
80
115
|
// Read version from package.json to keep it in sync
|
|
81
116
|
const require = createRequire(import.meta.url);
|
|
82
117
|
const { version } = require("../package.json");
|
|
@@ -132,23 +167,27 @@ async function hybridBatchCounts(ids, appleFn, imapFn) {
|
|
|
132
167
|
// Message Tools
|
|
133
168
|
// =============================================================================
|
|
134
169
|
// --- search-messages ---
|
|
135
|
-
server.
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
.string()
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
.string()
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
170
|
+
server.registerTool("search-messages", {
|
|
171
|
+
description: "Use when: finding messages by query/sender/subject/date/read/flag filters and you need their ids for follow-up operations.\nReturns: matching messages with id, date, subject, sender, and read state (plus partial-coverage diagnostics when some mailboxes were skipped).\nDo not use when: you want a plain mailbox listing without filters (use list-messages), already have an id and want the body (use get-message), or want a whole conversation (use get-thread).\nPrefer this first to obtain the message ids that get-message/mark-as-read/delete-message/move-message and the batch tools require.",
|
|
172
|
+
inputSchema: {
|
|
173
|
+
query: z.string().optional().describe("Text to search for in subject, sender, or content"),
|
|
174
|
+
from: z
|
|
175
|
+
.string()
|
|
176
|
+
.optional()
|
|
177
|
+
.describe("Filter by sender (substring match against the full sender string, i.e. display name + address — not an exact address match)"),
|
|
178
|
+
subject: z.string().optional().describe("Filter by subject line (substring match)"),
|
|
179
|
+
mailbox: z
|
|
180
|
+
.string()
|
|
181
|
+
.optional()
|
|
182
|
+
.describe("Mailbox to search in (e.g., 'INBOX'). Omit to search all mailboxes."),
|
|
183
|
+
account: z.string().optional().describe("Account to search in (omit to search all accounts)"),
|
|
184
|
+
isRead: z.boolean().optional().describe("Filter by read status"),
|
|
185
|
+
isFlagged: z.boolean().optional().describe("Filter by flagged status"),
|
|
186
|
+
dateFrom: DATE_FILTER_SCHEMA.describe("Start date filter (e.g., 'January 1, 2026')"),
|
|
187
|
+
dateTo: DATE_FILTER_SCHEMA.describe("End date filter (e.g., 'March 1, 2026')"),
|
|
188
|
+
limit: z.number().optional().describe("Maximum number of results (default: 50)"),
|
|
189
|
+
},
|
|
190
|
+
outputSchema: LIST_OUTPUT_SCHEMA,
|
|
152
191
|
}, withErrorHandling(async ({ query, mailbox, account, limit = 50, dateFrom, dateTo, from, subject, isRead, isFlagged, }) => {
|
|
153
192
|
// IMAP backend (issue #43): server-side search when this account is
|
|
154
193
|
// explicitly configured for IMAP; otherwise fall through to AppleScript.
|
|
@@ -193,12 +232,21 @@ server.tool("search-messages", "Use when: finding messages by query/sender/subje
|
|
|
193
232
|
return successResponse(`Found ${messages.length} message(s):\n${messageList}${coverageBlock}`, structured);
|
|
194
233
|
}, "Error searching messages"));
|
|
195
234
|
// --- get-message ---
|
|
196
|
-
server.
|
|
197
|
-
id:
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
235
|
+
server.registerTool("get-message", {
|
|
236
|
+
description: "Use when: reading the full body of one message whose id you already have (numeric or imap:…); set preferHtml to get the HTML body instead of plain text.\nReturns: the message subject and body (plain text by default, HTML when preferHtml is true).\nDo not use when: you don't yet have an id (use search-messages or list-messages first), or you want the whole conversation (use get-thread).",
|
|
237
|
+
inputSchema: {
|
|
238
|
+
id: MESSAGE_ID_SCHEMA,
|
|
239
|
+
preferHtml: z
|
|
240
|
+
.boolean()
|
|
241
|
+
.optional()
|
|
242
|
+
.describe("Return the HTML body (extracted from the message source) instead of plain text"),
|
|
243
|
+
},
|
|
244
|
+
outputSchema: {
|
|
245
|
+
id: z.string().optional(),
|
|
246
|
+
subject: z.string().optional(),
|
|
247
|
+
body: z.string().optional(),
|
|
248
|
+
isHtml: z.boolean().optional(),
|
|
249
|
+
},
|
|
202
250
|
}, withErrorHandling(({ id, preferHtml }) => routeMessage(id, {
|
|
203
251
|
// IMAP id (imap:…) → fetch via IMAP (#43 Phase 3); else AppleScript.
|
|
204
252
|
imap: () => imapGetMessage(id, preferHtml === true),
|
|
@@ -233,11 +281,20 @@ server.tool("get-message", "Use when: reading the full body of one message whose
|
|
|
233
281
|
fail: `Message with ID "${id}" not found`,
|
|
234
282
|
}), "Error retrieving message"));
|
|
235
283
|
// --- get-thread ---
|
|
236
|
-
server.
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
284
|
+
server.registerTool("get-thread", {
|
|
285
|
+
description: "Use when: you have one message id and want the whole conversation it belongs to, oldest-first. With an imap: id it threads by References/Message-ID; otherwise it groups by normalized subject.\nReturns: the thread's normalized subject and its messages (id, date, subject, sender, read state).\nDo not use when: you only need the single message (use get-message) or are searching by arbitrary criteria (use search-messages).",
|
|
286
|
+
inputSchema: {
|
|
287
|
+
id: MESSAGE_ID_SCHEMA.describe("A message ID in the conversation (numeric or imap:…)"),
|
|
288
|
+
account: z.string().optional().describe("Account to search (omit to search all)"),
|
|
289
|
+
mailbox: z.string().optional().describe("Mailbox to search (omit to search all)"),
|
|
290
|
+
limit: z.number().optional().describe("Max messages in the thread (default 50)"),
|
|
291
|
+
},
|
|
292
|
+
outputSchema: {
|
|
293
|
+
subject: z.string().optional(),
|
|
294
|
+
messages: z.array(MESSAGE_ROW_SCHEMA).optional(),
|
|
295
|
+
count: z.number().optional(),
|
|
296
|
+
partial: z.boolean().optional(),
|
|
297
|
+
},
|
|
241
298
|
}, withErrorHandling(async ({ id, account, mailbox, limit = 50 }) => {
|
|
242
299
|
// True threading via References/Message-ID when we have an imap: id (I5);
|
|
243
300
|
// falls through to subject grouping if the server lacks HEADER search or
|
|
@@ -296,16 +353,20 @@ server.tool("get-thread", "Use when: you have one message id and want the whole
|
|
|
296
353
|
return successResponse(`Thread "${base}" — ${ordered.length} message(s), oldest first:\n${list}${coverageBlock}`, structured);
|
|
297
354
|
}, "Error retrieving thread"));
|
|
298
355
|
// --- list-messages ---
|
|
299
|
-
server.
|
|
300
|
-
mailbox:
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
356
|
+
server.registerTool("list-messages", {
|
|
357
|
+
description: "Use when: browsing a mailbox's recent messages (optionally filtered by sender or unread-only) with pagination via limit/offset, and you need their ids.\nReturns: messages with id, date, subject, and sender (plus partial-coverage diagnostics when some mailboxes were skipped).\nDo not use when: you have specific search criteria like subject/date/flags (use search-messages) or already have an id and want the body (use get-message).\nLike search-messages, use this to obtain the ids that read/mark/delete/move and batch tools require.",
|
|
358
|
+
inputSchema: {
|
|
359
|
+
mailbox: z
|
|
360
|
+
.string()
|
|
361
|
+
.optional()
|
|
362
|
+
.describe("Mailbox to list messages from. Omit to list from all mailboxes."),
|
|
363
|
+
account: z.string().optional().describe("Account to list messages from"),
|
|
364
|
+
limit: z.number().optional().describe("Maximum number of messages (default: 50)"),
|
|
365
|
+
offset: z.number().optional().describe("Number of messages to skip (for pagination)"),
|
|
366
|
+
from: z.string().optional().describe("Filter by sender email address or name"),
|
|
367
|
+
unreadOnly: z.boolean().optional().describe("Only show unread messages"),
|
|
368
|
+
},
|
|
369
|
+
outputSchema: LIST_OUTPUT_SCHEMA,
|
|
309
370
|
}, withErrorHandling(async ({ mailbox, account, limit = 50, offset = 0, from, unreadOnly }) => {
|
|
310
371
|
// IMAP backend (issue #43): server-side listing when this account is
|
|
311
372
|
// explicitly configured for IMAP; otherwise fall through to AppleScript.
|
|
@@ -339,25 +400,45 @@ server.tool("list-messages", "Use when: browsing a mailbox's recent messages (op
|
|
|
339
400
|
return successResponse(`Found ${messages.length} message(s):\n${messageList}${coverageBlock}`, structured);
|
|
340
401
|
}, "Error listing messages"));
|
|
341
402
|
// --- send-email ---
|
|
342
|
-
server.
|
|
343
|
-
to:
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
403
|
+
server.registerTool("send-email", {
|
|
404
|
+
description: "Use when: the user has explicitly confirmed they want to send a single email now to the given recipients (to/cc/bcc are arrays), optionally with attachments and a chosen transport.\nReturns: a confirmation naming the recipients and attachment count.\nDo not use when: the user wants to review first (use create-draft), is replying to or forwarding an existing message (use reply-to-message / forward-message), or wants per-recipient personalized copies (use send-serial-email).\nSafety: this SENDS real email immediately and it cannot be unsent — require explicit user confirmation of the exact recipients, subject, and body before calling. Prefer create-draft when there is any doubt.",
|
|
405
|
+
inputSchema: {
|
|
406
|
+
to: z.array(z.string()).min(1, "At least one recipient is required"),
|
|
407
|
+
subject: z.string().min(1, "Subject is required"),
|
|
408
|
+
body: z.string().min(1, "Body is required"),
|
|
409
|
+
cc: z.array(z.string()).optional().describe("CC recipients"),
|
|
410
|
+
bcc: z.array(z.string()).optional().describe("BCC recipients"),
|
|
411
|
+
account: z.string().optional().describe("Account to send from"),
|
|
412
|
+
attachments: ATTACHMENTS_SCHEMA,
|
|
413
|
+
transport: z
|
|
414
|
+
.enum(["applescript", "smtp"])
|
|
415
|
+
.optional()
|
|
416
|
+
.describe("Send transport. 'smtp' submits clean MIME directly via SMTP, avoiding " +
|
|
417
|
+
"the macOS 15+ Mail.app <blockquote> wrapping (issue #12); requires " +
|
|
418
|
+
"APPLE_MAIL_MCP_SMTP_* env config. 'applescript' sends through Mail.app. " +
|
|
419
|
+
"If omitted, SMTP is used automatically when APPLE_MAIL_MCP_SMTP_* is " +
|
|
420
|
+
"configured, otherwise AppleScript."),
|
|
421
|
+
},
|
|
422
|
+
outputSchema: {
|
|
423
|
+
ok: z.boolean().optional(),
|
|
424
|
+
recipients: z.array(z.string()).optional(),
|
|
425
|
+
attachmentCount: z.number().optional(),
|
|
426
|
+
transport: z.string().optional(),
|
|
427
|
+
},
|
|
356
428
|
}, withErrorHandling(async ({ to, subject, body, cc, bcc, account, attachments, transport }) => {
|
|
357
429
|
const attachInfo = attachments?.length ? ` with ${attachments.length} attachment(s)` : "";
|
|
358
430
|
const attachmentCount = attachments?.length ?? 0;
|
|
359
|
-
|
|
360
|
-
|
|
431
|
+
// Prefer SMTP when explicitly requested, or automatically when it is
|
|
432
|
+
// configured and no transport was specified — except when a non-email
|
|
433
|
+
// `account` label requests Mail.app account selection (see shouldUseSmtp).
|
|
434
|
+
// Explicit transport:"applescript" always forces the Mail.app path.
|
|
435
|
+
if (shouldUseSmtp(transport, account)) {
|
|
436
|
+
// `account` is a Mail.app account label for the AppleScript path; for SMTP
|
|
437
|
+
// it only makes sense as a From override when it is an actual address.
|
|
438
|
+
// A bare label (only possible here via explicit transport:"smtp") must not
|
|
439
|
+
// corrupt the From — fall back to the configured SMTP From in that case.
|
|
440
|
+
const smtpFrom = account?.includes("@") ? account : undefined;
|
|
441
|
+
const result = await sendViaSmtp({ to, subject, body, cc, bcc, from: smtpFrom, attachments });
|
|
361
442
|
if (!result.success) {
|
|
362
443
|
return errorResponse(result.error ?? "Failed to send email via SMTP.");
|
|
363
444
|
}
|
|
@@ -380,32 +461,49 @@ server.tool("send-email", "Use when: the user has explicitly confirmed they want
|
|
|
380
461
|
});
|
|
381
462
|
}, "Error sending email"));
|
|
382
463
|
// --- send-serial-email ---
|
|
383
|
-
server.
|
|
384
|
-
recipients:
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
.
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
464
|
+
server.registerTool("send-serial-email", {
|
|
465
|
+
description: "Use when: the user has confirmed a mail-merge — sending individually personalized copies to many recipients (max 100), with {{Key}} placeholders in subject/body replaced per-recipient from each recipient's variables. Recipients do not see each other.\nReturns: a per-recipient sent/failed report with counts.\nDo not use when: sending one message to a shared recipient list (use send-email) or saving for review (use create-draft).\nSafety: this SENDS many real emails immediately and they cannot be unsent — require explicit user confirmation of the recipient list, the subject/body template, and the placeholder substitutions before calling.",
|
|
466
|
+
inputSchema: {
|
|
467
|
+
recipients: z
|
|
468
|
+
.array(z.object({
|
|
469
|
+
email: z.string().min(1, "Recipient email is required"),
|
|
470
|
+
variables: z
|
|
471
|
+
.record(z.string())
|
|
472
|
+
.describe("Placeholder values, e.g. { Name: 'Alice', Company: 'Acme' }"),
|
|
473
|
+
}))
|
|
474
|
+
.min(1, "At least one recipient is required")
|
|
475
|
+
.max(100, "Cannot send to more than 100 recipients in a single batch")
|
|
476
|
+
.describe("List of recipients with personalization variables (max 100)"),
|
|
477
|
+
subject: z
|
|
478
|
+
.string()
|
|
479
|
+
.min(1, "Subject is required")
|
|
480
|
+
.describe("Subject line — use {{Key}} for placeholders"),
|
|
481
|
+
body: z
|
|
482
|
+
.string()
|
|
483
|
+
.min(1, "Body is required")
|
|
484
|
+
.describe("Email body — use {{Key}} for placeholders"),
|
|
485
|
+
account: z.string().optional().describe("Account to send from"),
|
|
486
|
+
delayMs: z
|
|
487
|
+
.number()
|
|
488
|
+
.min(0)
|
|
489
|
+
.max(10000)
|
|
490
|
+
.optional()
|
|
491
|
+
.describe("Delay between sends in ms (default: 500, max: 10000)"),
|
|
492
|
+
},
|
|
493
|
+
outputSchema: {
|
|
494
|
+
ok: z.boolean().optional(),
|
|
495
|
+
sent: z.number().optional(),
|
|
496
|
+
failed: z.number().optional(),
|
|
497
|
+
results: z
|
|
498
|
+
.array(z
|
|
499
|
+
.object({
|
|
500
|
+
email: z.string().optional(),
|
|
501
|
+
success: z.boolean().optional(),
|
|
502
|
+
error: z.string().optional(),
|
|
503
|
+
})
|
|
504
|
+
.passthrough())
|
|
505
|
+
.optional(),
|
|
506
|
+
},
|
|
409
507
|
}, withErrorHandling(({ recipients, subject, body, account, delayMs }) => {
|
|
410
508
|
const results = mailManager.sendSerialEmail(recipients, subject, body, account, delayMs);
|
|
411
509
|
const successCount = results.filter((r) => r.success).length;
|
|
@@ -430,14 +528,22 @@ server.tool("send-serial-email", "Use when: the user has confirmed a mail-merge
|
|
|
430
528
|
}
|
|
431
529
|
}, "Error sending serial emails"));
|
|
432
530
|
// --- create-draft ---
|
|
433
|
-
server.
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
531
|
+
server.registerTool("create-draft", {
|
|
532
|
+
description: "Use when: composing an email the user should review in Mail.app before sending — the safe default for any new message (to/cc/bcc are arrays, optional attachments).\nReturns: a confirmation that the draft was created, with recipients and attachment count.\nDo not use when: the user has already confirmed they want it sent now (use send-email).\nSafety: low risk — creates a draft only and sends nothing; the user must open Mail.app and send it themselves.",
|
|
533
|
+
inputSchema: {
|
|
534
|
+
to: z.array(z.string()).min(1, "At least one recipient is required"),
|
|
535
|
+
subject: z.string().min(1, "Subject is required"),
|
|
536
|
+
body: z.string().min(1, "Body is required"),
|
|
537
|
+
cc: z.array(z.string()).optional().describe("CC recipients"),
|
|
538
|
+
bcc: z.array(z.string()).optional().describe("BCC recipients"),
|
|
539
|
+
account: z.string().optional().describe("Account to create draft in"),
|
|
540
|
+
attachments: ATTACHMENTS_SCHEMA,
|
|
541
|
+
},
|
|
542
|
+
outputSchema: {
|
|
543
|
+
ok: z.boolean().optional(),
|
|
544
|
+
recipients: z.array(z.string()).optional(),
|
|
545
|
+
attachmentCount: z.number().optional(),
|
|
546
|
+
},
|
|
441
547
|
}, withErrorHandling(({ to, subject, body, cc, bcc, account, attachments }) => {
|
|
442
548
|
const success = mailManager.createDraft(to, subject, body, cc, bcc, account, attachments);
|
|
443
549
|
if (!success) {
|
|
@@ -452,11 +558,23 @@ server.tool("create-draft", "Use when: composing an email the user should review
|
|
|
452
558
|
});
|
|
453
559
|
}, "Error creating draft"));
|
|
454
560
|
// --- reply-to-message ---
|
|
455
|
-
server.
|
|
456
|
-
id:
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
561
|
+
server.registerTool("reply-to-message", {
|
|
562
|
+
description: "Use when: replying to an existing message by id, preserving its threading headers. Set replyAll for all recipients; set send=false to save as a draft instead of sending.\nReturns: a confirmation that the reply was sent or saved as a draft.\nDo not use when: composing a brand-new message (use send-email / create-draft) or forwarding to new recipients (use forward-message).\nSafety: with the default send=true this SENDS real email immediately and cannot be unsent — require explicit user confirmation of the recipients and body, or pass send=false to let the user review.",
|
|
563
|
+
inputSchema: {
|
|
564
|
+
id: MESSAGE_ID_SCHEMA,
|
|
565
|
+
body: z.string().min(1, "Reply body is required"),
|
|
566
|
+
replyAll: z.boolean().optional().default(false).describe("Reply to all recipients"),
|
|
567
|
+
send: z
|
|
568
|
+
.boolean()
|
|
569
|
+
.optional()
|
|
570
|
+
.default(true)
|
|
571
|
+
.describe("Send immediately (false = save as draft)"),
|
|
572
|
+
},
|
|
573
|
+
outputSchema: {
|
|
574
|
+
ok: z.boolean().optional(),
|
|
575
|
+
sent: z.boolean().optional(),
|
|
576
|
+
id: z.string().optional(),
|
|
577
|
+
},
|
|
460
578
|
}, withErrorHandling(({ id, body, replyAll, send }) => {
|
|
461
579
|
const success = mailManager.replyToMessage(id, body, replyAll, send);
|
|
462
580
|
if (!success) {
|
|
@@ -469,11 +587,24 @@ server.tool("reply-to-message", "Use when: replying to an existing message by id
|
|
|
469
587
|
});
|
|
470
588
|
}, "Error replying to message"));
|
|
471
589
|
// --- forward-message ---
|
|
472
|
-
server.
|
|
473
|
-
id:
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
590
|
+
server.registerTool("forward-message", {
|
|
591
|
+
description: "Use when: forwarding an existing message (by id) to new recipients (to is an array), with an optional body to prepend. Set send=false to save as a draft.\nReturns: a confirmation that the message was forwarded or saved as a draft.\nDo not use when: replying to the sender/recipients (use reply-to-message) or composing a new message (use send-email / create-draft).\nSafety: with the default send=true this SENDS real email immediately and cannot be unsent — require explicit user confirmation of the recipients and any prepended body, or pass send=false to let the user review.",
|
|
592
|
+
inputSchema: {
|
|
593
|
+
id: MESSAGE_ID_SCHEMA,
|
|
594
|
+
to: z.array(z.string()).min(1, "At least one recipient is required"),
|
|
595
|
+
body: z.string().optional().describe("Optional message to prepend"),
|
|
596
|
+
send: z
|
|
597
|
+
.boolean()
|
|
598
|
+
.optional()
|
|
599
|
+
.default(true)
|
|
600
|
+
.describe("Send immediately (false = save as draft)"),
|
|
601
|
+
},
|
|
602
|
+
outputSchema: {
|
|
603
|
+
ok: z.boolean().optional(),
|
|
604
|
+
sent: z.boolean().optional(),
|
|
605
|
+
recipients: z.array(z.string()).optional(),
|
|
606
|
+
id: z.string().optional(),
|
|
607
|
+
},
|
|
477
608
|
}, withErrorHandling(({ id, to, body, send }) => {
|
|
478
609
|
const success = mailManager.forwardMessage(id, to, body, send);
|
|
479
610
|
if (!success) {
|
|
@@ -482,8 +613,12 @@ server.tool("forward-message", "Use when: forwarding an existing message (by id)
|
|
|
482
613
|
return successResponse(send ? `Message forwarded to ${to.join(", ")}` : "Forward saved as draft", { ok: true, sent: send, recipients: to, id });
|
|
483
614
|
}, "Error forwarding message"));
|
|
484
615
|
// --- mark-as-read ---
|
|
485
|
-
server.
|
|
486
|
-
id:
|
|
616
|
+
server.registerTool("mark-as-read", {
|
|
617
|
+
description: "Use when: marking a single message (by id) as read.\nReturns: a confirmation that the message was marked read.\nDo not use when: marking several at once (use batch-mark-as-read) or marking unread (use mark-as-unread). Get the id from search-messages or list-messages first.",
|
|
618
|
+
inputSchema: {
|
|
619
|
+
id: MESSAGE_ID_SCHEMA,
|
|
620
|
+
},
|
|
621
|
+
outputSchema: { ok: z.boolean().optional(), id: z.string().optional() },
|
|
487
622
|
}, withErrorHandling(({ id }) => routeMessage(id, {
|
|
488
623
|
imap: () => imapMarkRead(id),
|
|
489
624
|
apple: () => mailManager.markAsRead(id)
|
|
@@ -494,8 +629,12 @@ server.tool("mark-as-read", "Use when: marking a single message (by id) as read.
|
|
|
494
629
|
structured: { ok: true, id },
|
|
495
630
|
}), "Error marking message as read"));
|
|
496
631
|
// --- mark-as-unread ---
|
|
497
|
-
server.
|
|
498
|
-
id:
|
|
632
|
+
server.registerTool("mark-as-unread", {
|
|
633
|
+
description: "Use when: marking a single message (by id) as unread.\nReturns: a confirmation that the message was marked unread.\nDo not use when: marking several at once (use batch-mark-as-unread) or marking read (use mark-as-read). Get the id from search-messages or list-messages first.",
|
|
634
|
+
inputSchema: {
|
|
635
|
+
id: MESSAGE_ID_SCHEMA,
|
|
636
|
+
},
|
|
637
|
+
outputSchema: { ok: z.boolean().optional(), id: z.string().optional() },
|
|
499
638
|
}, withErrorHandling(({ id }) => routeMessage(id, {
|
|
500
639
|
imap: () => imapMarkUnread(id),
|
|
501
640
|
apple: () => mailManager.markAsUnread(id)
|
|
@@ -506,8 +645,12 @@ server.tool("mark-as-unread", "Use when: marking a single message (by id) as unr
|
|
|
506
645
|
structured: { ok: true, id },
|
|
507
646
|
}), "Error marking message as unread"));
|
|
508
647
|
// --- flag-message ---
|
|
509
|
-
server.
|
|
510
|
-
id:
|
|
648
|
+
server.registerTool("flag-message", {
|
|
649
|
+
description: "Use when: flagging a single message (by id).\nReturns: a confirmation that the message was flagged.\nDo not use when: flagging several at once (use batch-flag-messages) or removing a flag (use unflag-message). Get the id from search-messages or list-messages first.",
|
|
650
|
+
inputSchema: {
|
|
651
|
+
id: MESSAGE_ID_SCHEMA,
|
|
652
|
+
},
|
|
653
|
+
outputSchema: { ok: z.boolean().optional(), id: z.string().optional() },
|
|
511
654
|
}, withErrorHandling(({ id }) => routeMessage(id, {
|
|
512
655
|
imap: () => imapFlagMessage(id),
|
|
513
656
|
apple: () => mailManager.flagMessage(id)
|
|
@@ -518,8 +661,12 @@ server.tool("flag-message", "Use when: flagging a single message (by id).\nRetur
|
|
|
518
661
|
structured: { ok: true, id },
|
|
519
662
|
}), "Error flagging message"));
|
|
520
663
|
// --- unflag-message ---
|
|
521
|
-
server.
|
|
522
|
-
id:
|
|
664
|
+
server.registerTool("unflag-message", {
|
|
665
|
+
description: "Use when: removing the flag from a single message (by id).\nReturns: a confirmation that the message was unflagged.\nDo not use when: unflagging several at once (use batch-unflag-messages) or adding a flag (use flag-message). Get the id from search-messages or list-messages first.",
|
|
666
|
+
inputSchema: {
|
|
667
|
+
id: MESSAGE_ID_SCHEMA,
|
|
668
|
+
},
|
|
669
|
+
outputSchema: { ok: z.boolean().optional(), id: z.string().optional() },
|
|
523
670
|
}, withErrorHandling(({ id }) => routeMessage(id, {
|
|
524
671
|
imap: () => imapUnflagMessage(id),
|
|
525
672
|
apple: () => mailManager.unflagMessage(id)
|
|
@@ -530,8 +677,12 @@ server.tool("unflag-message", "Use when: removing the flag from a single message
|
|
|
530
677
|
structured: { ok: true, id },
|
|
531
678
|
}), "Error unflagging message"));
|
|
532
679
|
// --- delete-message ---
|
|
533
|
-
server.
|
|
534
|
-
id:
|
|
680
|
+
server.registerTool("delete-message", {
|
|
681
|
+
description: "Use when: deleting a single message by id (moves it to Trash).\nReturns: a confirmation that the message was deleted.\nDo not use when: deleting several at once (use batch-delete-messages) or just filing it away (use move-message).\nSafety: destructive — require explicit user confirmation, and search-messages/list-messages first to confirm you have the right id before deleting.",
|
|
682
|
+
inputSchema: {
|
|
683
|
+
id: MESSAGE_ID_SCHEMA,
|
|
684
|
+
},
|
|
685
|
+
outputSchema: { ok: z.boolean().optional(), id: z.string().optional() },
|
|
535
686
|
}, withErrorHandling(({ id }) => routeMessage(id, {
|
|
536
687
|
imap: () => imapDeleteMessageById(id),
|
|
537
688
|
apple: () => {
|
|
@@ -545,10 +696,18 @@ server.tool("delete-message", "Use when: deleting a single message by id (moves
|
|
|
545
696
|
structured: { ok: true, id },
|
|
546
697
|
}), "Error deleting message"));
|
|
547
698
|
// --- move-message ---
|
|
548
|
-
server.
|
|
549
|
-
id:
|
|
550
|
-
|
|
551
|
-
|
|
699
|
+
server.registerTool("move-message", {
|
|
700
|
+
description: "Use when: moving a single message (by id) into another mailbox/folder, e.g. archiving or filing.\nReturns: a confirmation naming the destination mailbox.\nDo not use when: moving several at once (use batch-move-messages) or deleting (use delete-message). Use list-mailboxes to confirm the destination name exists.\nSafety: moves a real message between folders — confirm the destination mailbox, and search-messages/list-messages first to confirm the id.",
|
|
701
|
+
inputSchema: {
|
|
702
|
+
id: MESSAGE_ID_SCHEMA,
|
|
703
|
+
mailbox: z.string().min(1, "Destination mailbox is required"),
|
|
704
|
+
account: z.string().optional().describe("Account containing the destination mailbox"),
|
|
705
|
+
},
|
|
706
|
+
outputSchema: {
|
|
707
|
+
ok: z.boolean().optional(),
|
|
708
|
+
id: z.string().optional(),
|
|
709
|
+
mailbox: z.string().optional(),
|
|
710
|
+
},
|
|
552
711
|
}, withErrorHandling(({ id, mailbox, account }) => routeMessage(id, {
|
|
553
712
|
imap: () => imapMoveMessageById(id, mailbox),
|
|
554
713
|
apple: () => {
|
|
@@ -562,8 +721,12 @@ server.tool("move-message", "Use when: moving a single message (by id) into anot
|
|
|
562
721
|
structured: { ok: true, id, mailbox },
|
|
563
722
|
}), "Error moving message"));
|
|
564
723
|
// --- batch-delete-messages ---
|
|
565
|
-
server.
|
|
566
|
-
ids:
|
|
724
|
+
server.registerTool("batch-delete-messages", {
|
|
725
|
+
description: "Use when: deleting multiple messages in one call (1–100 ids; moves them to Trash).\nReturns: counts of how many were deleted and how many failed.\nDo not use when: deleting just one (use delete-message) or filing messages away (use batch-move-messages).\nSafety: destructive and applies to many messages at once — require explicit user confirmation, and search-messages/list-messages first to confirm every id is correct before deleting.",
|
|
726
|
+
inputSchema: {
|
|
727
|
+
ids: BATCH_IDS_SCHEMA,
|
|
728
|
+
},
|
|
729
|
+
outputSchema: BATCH_COUNT_OUTPUT_SCHEMA,
|
|
567
730
|
}, withErrorHandling(async ({ ids }) => {
|
|
568
731
|
const { success: successCount, fail: failCount } = await hybridBatchCounts(ids, (n) => mailManager.batchDeleteMessages(n), (im) => imapBatchDelete(im));
|
|
569
732
|
const structured = { ok: failCount === 0, success: successCount, failed: failCount };
|
|
@@ -578,10 +741,14 @@ server.tool("batch-delete-messages", "Use when: deleting multiple messages in on
|
|
|
578
741
|
}
|
|
579
742
|
}, "Error batch deleting messages"));
|
|
580
743
|
// --- batch-move-messages ---
|
|
581
|
-
server.
|
|
582
|
-
ids:
|
|
583
|
-
|
|
584
|
-
|
|
744
|
+
server.registerTool("batch-move-messages", {
|
|
745
|
+
description: "Use when: moving multiple messages (1–100 ids) into the same destination mailbox/folder in one call, e.g. bulk archiving.\nReturns: counts of how many were moved and how many failed.\nDo not use when: moving just one (use move-message) or deleting (use batch-delete-messages). Use list-mailboxes to confirm the destination name exists.\nSafety: moves many real messages at once — confirm the destination mailbox, and search-messages/list-messages first to confirm the ids.",
|
|
746
|
+
inputSchema: {
|
|
747
|
+
ids: BATCH_IDS_SCHEMA,
|
|
748
|
+
mailbox: z.string().min(1, "Destination mailbox is required"),
|
|
749
|
+
account: z.string().optional().describe("Account containing the destination mailbox"),
|
|
750
|
+
},
|
|
751
|
+
outputSchema: BATCH_COUNT_OUTPUT_SCHEMA,
|
|
585
752
|
}, withErrorHandling(async ({ ids, mailbox, account }) => {
|
|
586
753
|
const { success: successCount, fail: failCount } = await hybridBatchCounts(ids, (n) => mailManager.batchMoveMessages(n, mailbox, account), (im) => imapBatchMove(im, mailbox, { account }));
|
|
587
754
|
const structured = { ok: failCount === 0, success: successCount, failed: failCount, mailbox };
|
|
@@ -596,8 +763,12 @@ server.tool("batch-move-messages", "Use when: moving multiple messages (1–100
|
|
|
596
763
|
}
|
|
597
764
|
}, "Error batch moving messages"));
|
|
598
765
|
// --- batch-mark-as-read ---
|
|
599
|
-
server.
|
|
600
|
-
ids:
|
|
766
|
+
server.registerTool("batch-mark-as-read", {
|
|
767
|
+
description: "Use when: marking multiple messages (1–100 ids) as read in one call.\nReturns: counts of how many were marked read and how many failed.\nDo not use when: marking just one (use mark-as-read) or marking unread (use batch-mark-as-unread). Get the ids from search-messages or list-messages first.",
|
|
768
|
+
inputSchema: {
|
|
769
|
+
ids: BATCH_IDS_SCHEMA,
|
|
770
|
+
},
|
|
771
|
+
outputSchema: BATCH_COUNT_OUTPUT_SCHEMA,
|
|
601
772
|
}, withErrorHandling(async ({ ids }) => {
|
|
602
773
|
const { success: successCount, fail: failCount } = await hybridBatchCounts(ids, (n) => mailManager.batchMarkAsRead(n), (im) => imapBatchMarkRead(im));
|
|
603
774
|
const structured = { ok: failCount === 0, success: successCount, failed: failCount };
|
|
@@ -612,8 +783,12 @@ server.tool("batch-mark-as-read", "Use when: marking multiple messages (1–100
|
|
|
612
783
|
}
|
|
613
784
|
}, "Error batch marking messages as read"));
|
|
614
785
|
// --- batch-mark-as-unread ---
|
|
615
|
-
server.
|
|
616
|
-
ids:
|
|
786
|
+
server.registerTool("batch-mark-as-unread", {
|
|
787
|
+
description: "Use when: marking multiple messages (1–100 ids) as unread in one call.\nReturns: counts of how many were marked unread and how many failed.\nDo not use when: marking just one (use mark-as-unread) or marking read (use batch-mark-as-read). Get the ids from search-messages or list-messages first.",
|
|
788
|
+
inputSchema: {
|
|
789
|
+
ids: BATCH_IDS_SCHEMA,
|
|
790
|
+
},
|
|
791
|
+
outputSchema: BATCH_COUNT_OUTPUT_SCHEMA,
|
|
617
792
|
}, withErrorHandling(async ({ ids }) => {
|
|
618
793
|
const { success: successCount, fail: failCount } = await hybridBatchCounts(ids, (n) => mailManager.batchMarkAsUnread(n), (im) => imapBatchMarkUnread(im));
|
|
619
794
|
const structured = { ok: failCount === 0, success: successCount, failed: failCount };
|
|
@@ -628,8 +803,12 @@ server.tool("batch-mark-as-unread", "Use when: marking multiple messages (1–10
|
|
|
628
803
|
}
|
|
629
804
|
}, "Error batch marking messages as unread"));
|
|
630
805
|
// --- batch-flag-messages ---
|
|
631
|
-
server.
|
|
632
|
-
ids:
|
|
806
|
+
server.registerTool("batch-flag-messages", {
|
|
807
|
+
description: "Use when: flagging multiple messages (1–100 ids) in one call.\nReturns: counts of how many were flagged and how many failed.\nDo not use when: flagging just one (use flag-message) or removing flags (use batch-unflag-messages). Get the ids from search-messages or list-messages first.",
|
|
808
|
+
inputSchema: {
|
|
809
|
+
ids: BATCH_IDS_SCHEMA,
|
|
810
|
+
},
|
|
811
|
+
outputSchema: BATCH_COUNT_OUTPUT_SCHEMA,
|
|
633
812
|
}, withErrorHandling(async ({ ids }) => {
|
|
634
813
|
const { success: successCount, fail: failCount } = await hybridBatchCounts(ids, (n) => mailManager.batchFlagMessages(n), (im) => imapBatchFlag(im));
|
|
635
814
|
const structured = { ok: failCount === 0, success: successCount, failed: failCount };
|
|
@@ -644,8 +823,12 @@ server.tool("batch-flag-messages", "Use when: flagging multiple messages (1–10
|
|
|
644
823
|
}
|
|
645
824
|
}, "Error batch flagging messages"));
|
|
646
825
|
// --- batch-unflag-messages ---
|
|
647
|
-
server.
|
|
648
|
-
ids:
|
|
826
|
+
server.registerTool("batch-unflag-messages", {
|
|
827
|
+
description: "Use when: removing flags from multiple messages (1–100 ids) in one call.\nReturns: counts of how many were unflagged and how many failed.\nDo not use when: unflagging just one (use unflag-message) or adding flags (use batch-flag-messages). Get the ids from search-messages or list-messages first.",
|
|
828
|
+
inputSchema: {
|
|
829
|
+
ids: BATCH_IDS_SCHEMA,
|
|
830
|
+
},
|
|
831
|
+
outputSchema: BATCH_COUNT_OUTPUT_SCHEMA,
|
|
649
832
|
}, withErrorHandling(async ({ ids }) => {
|
|
650
833
|
const { success: successCount, fail: failCount } = await hybridBatchCounts(ids, (n) => mailManager.batchUnflagMessages(n), (im) => imapBatchUnflag(im));
|
|
651
834
|
const structured = { ok: failCount === 0, success: successCount, failed: failCount };
|
|
@@ -660,8 +843,15 @@ server.tool("batch-unflag-messages", "Use when: removing flags from multiple mes
|
|
|
660
843
|
}
|
|
661
844
|
}, "Error batch unflagging messages"));
|
|
662
845
|
// --- list-attachments ---
|
|
663
|
-
server.
|
|
664
|
-
id:
|
|
846
|
+
server.registerTool("list-attachments", {
|
|
847
|
+
description: "Use when: enumerating a message's attachments (by id) to discover their names, MIME types, and sizes — typically before saving or fetching one.\nReturns: each attachment's name, MIME type, and size, plus a count.\nDo not use when: you want the bytes (use fetch-attachment for inline base64, or save-attachment to write to disk). Get the message id from search-messages or list-messages first.",
|
|
848
|
+
inputSchema: {
|
|
849
|
+
id: MESSAGE_ID_SCHEMA,
|
|
850
|
+
},
|
|
851
|
+
outputSchema: {
|
|
852
|
+
attachments: z.array(z.object({}).passthrough()).optional(),
|
|
853
|
+
count: z.number().optional(),
|
|
854
|
+
},
|
|
665
855
|
}, withErrorHandling(async ({ id }) => {
|
|
666
856
|
// IMAP (I1): BODYSTRUCTURE enumerates parts (incl. MIME attachments
|
|
667
857
|
// AppleScript can't see) without downloading the message.
|
|
@@ -686,10 +876,18 @@ server.tool("list-attachments", "Use when: enumerating a message's attachments (
|
|
|
686
876
|
return successResponse(`Found ${attachments.length} attachment(s):\n${attachmentList}`, structured);
|
|
687
877
|
}, "Error listing attachments"));
|
|
688
878
|
// --- save-attachment ---
|
|
689
|
-
server.
|
|
690
|
-
id:
|
|
691
|
-
|
|
692
|
-
|
|
879
|
+
server.registerTool("save-attachment", {
|
|
880
|
+
description: "Use when: writing one of a message's attachments to disk, by message id and attachmentName, into the savePath directory (saved as savePath/attachmentName).\nReturns: a confirmation of the saved file path.\nDo not use when: you don't know the attachment name (use list-attachments first) or want the bytes inline rather than on disk (use fetch-attachment).\nSafety: writes a file to disk — savePath must be a directory inside the configured allowed roots, and attachmentName may not contain path separators or '..'; calls outside those constraints are rejected.",
|
|
881
|
+
inputSchema: {
|
|
882
|
+
id: MESSAGE_ID_SCHEMA,
|
|
883
|
+
attachmentName: z.string().min(1, "Attachment name is required"),
|
|
884
|
+
savePath: z.string().min(1, "Save directory path is required"),
|
|
885
|
+
},
|
|
886
|
+
outputSchema: {
|
|
887
|
+
ok: z.boolean().optional(),
|
|
888
|
+
attachmentName: z.string().optional(),
|
|
889
|
+
savedPath: z.string().optional(),
|
|
890
|
+
},
|
|
693
891
|
}, withErrorHandling(async ({ id, attachmentName, savePath }) => {
|
|
694
892
|
// IMAP (I1): fetch the part's bytes via IMAP, then write into savePath (a
|
|
695
893
|
// directory) as savePath/attachmentName — mirroring the AppleScript path,
|
|
@@ -725,9 +923,18 @@ server.tool("save-attachment", "Use when: writing one of a message's attachments
|
|
|
725
923
|
});
|
|
726
924
|
}, "Error saving attachment"));
|
|
727
925
|
// --- fetch-attachment ---
|
|
728
|
-
server.
|
|
729
|
-
id:
|
|
730
|
-
|
|
926
|
+
server.registerTool("fetch-attachment", {
|
|
927
|
+
description: "Use when: retrieving an attachment's raw bytes inline as base64 (by message id and attachmentName), e.g. to process its contents without touching disk.\nReturns: the attachment's bytes base64-encoded, with its size and (for IMAP) MIME type.\nDo not use when: you don't know the attachment name (use list-attachments first) or you just want it saved to disk (use save-attachment).",
|
|
928
|
+
inputSchema: {
|
|
929
|
+
id: MESSAGE_ID_SCHEMA,
|
|
930
|
+
attachmentName: z.string().min(1, "Attachment name is required"),
|
|
931
|
+
},
|
|
932
|
+
outputSchema: {
|
|
933
|
+
attachmentName: z.string().optional(),
|
|
934
|
+
bytes: z.number().optional(),
|
|
935
|
+
mimeType: z.string().optional(),
|
|
936
|
+
contentBase64: z.string().optional(),
|
|
937
|
+
},
|
|
731
938
|
}, withErrorHandling(async ({ id, attachmentName }) => {
|
|
732
939
|
// Returns the attachment bytes as base64 (B4) — the read counterpart to
|
|
733
940
|
// sending inline base64 content. IMAP (I1) fetches the part directly; numeric
|
|
@@ -749,8 +956,15 @@ server.tool("fetch-attachment", "Use when: retrieving an attachment's raw bytes
|
|
|
749
956
|
// Mailbox Tools
|
|
750
957
|
// =============================================================================
|
|
751
958
|
// --- list-mailboxes ---
|
|
752
|
-
server.
|
|
753
|
-
|
|
959
|
+
server.registerTool("list-mailboxes", {
|
|
960
|
+
description: "Use when: discovering the mailbox/folder names (and unread/message counts) available in an account, e.g. before moving messages or searching a specific mailbox.\nReturns: each mailbox's name with its unread (and, for IMAP, total message) count, plus a count.\nDo not use when: you want the messages inside a mailbox (use list-messages or search-messages) or the list of accounts (use list-accounts).",
|
|
961
|
+
inputSchema: {
|
|
962
|
+
account: z.string().optional().describe("Account to list mailboxes from"),
|
|
963
|
+
},
|
|
964
|
+
outputSchema: {
|
|
965
|
+
mailboxes: z.array(z.object({}).passthrough()).optional(),
|
|
966
|
+
count: z.number().optional(),
|
|
967
|
+
},
|
|
754
968
|
}, withErrorHandling(async ({ account }) => {
|
|
755
969
|
// IMAP (I6): LIST + per-mailbox STATUS — sees the true server hierarchy and
|
|
756
970
|
// authoritative counts; falls back to AppleScript for non-IMAP accounts.
|
|
@@ -778,9 +992,17 @@ server.tool("list-mailboxes", "Use when: discovering the mailbox/folder names (a
|
|
|
778
992
|
return successResponse(`Found ${mailboxes.length} mailbox(es):\n${mailboxList}`, structured);
|
|
779
993
|
}, "Error listing mailboxes"));
|
|
780
994
|
// --- get-unread-count ---
|
|
781
|
-
server.
|
|
782
|
-
|
|
783
|
-
|
|
995
|
+
server.registerTool("get-unread-count", {
|
|
996
|
+
description: "Use when: you only need the number of unread messages (optionally scoped to one mailbox and/or account), without listing the messages themselves.\nReturns: the unread count for the requested scope.\nDo not use when: you need the actual unread messages and their ids (use list-messages with unreadOnly, or search-messages with isRead=false) or broader totals (use get-mail-stats).",
|
|
997
|
+
inputSchema: {
|
|
998
|
+
mailbox: z.string().optional().describe("Mailbox to check (default: all)"),
|
|
999
|
+
account: z.string().optional().describe("Account to check"),
|
|
1000
|
+
},
|
|
1001
|
+
outputSchema: {
|
|
1002
|
+
unread: z.number().optional(),
|
|
1003
|
+
mailbox: z.string().optional(),
|
|
1004
|
+
account: z.string().optional(),
|
|
1005
|
+
},
|
|
784
1006
|
}, withErrorHandling(async ({ mailbox, account }) => {
|
|
785
1007
|
// IMAP (I4): STATUS (UNSEEN) is authoritative and fast even on huge
|
|
786
1008
|
// mailboxes; falls back to AppleScript for non-IMAP accounts.
|
|
@@ -795,9 +1017,16 @@ server.tool("get-unread-count", "Use when: you only need the number of unread me
|
|
|
795
1017
|
});
|
|
796
1018
|
}, "Error getting unread count"));
|
|
797
1019
|
// --- create-mailbox ---
|
|
798
|
-
server.
|
|
799
|
-
|
|
800
|
-
|
|
1020
|
+
server.registerTool("create-mailbox", {
|
|
1021
|
+
description: "Use when: creating a new mailbox/folder in an account.\nReturns: a confirmation that the mailbox was created.\nDo not use when: renaming an existing one (use rename-mailbox) or deleting one (use delete-mailbox). Use list-mailboxes to see what already exists.\nSafety: creates a real folder in the mail account — confirm the name and target account first.",
|
|
1022
|
+
inputSchema: {
|
|
1023
|
+
name: z.string().min(1, "Mailbox name is required"),
|
|
1024
|
+
account: z.string().optional().describe("Account to create the mailbox in"),
|
|
1025
|
+
},
|
|
1026
|
+
outputSchema: {
|
|
1027
|
+
ok: z.boolean().optional(),
|
|
1028
|
+
name: z.string().optional(),
|
|
1029
|
+
},
|
|
801
1030
|
}, withErrorHandling(async ({ name, account }) => {
|
|
802
1031
|
// IMAP backend (issue #43, Phase 2): server-side folder op when this account
|
|
803
1032
|
// is IMAP-configured; otherwise AppleScript.
|
|
@@ -814,9 +1043,16 @@ server.tool("create-mailbox", "Use when: creating a new mailbox/folder in an acc
|
|
|
814
1043
|
return successResponse(`Mailbox "${name}" created`, { ok: true, name });
|
|
815
1044
|
}, "Error creating mailbox"));
|
|
816
1045
|
// --- delete-mailbox ---
|
|
817
|
-
server.
|
|
818
|
-
|
|
819
|
-
|
|
1046
|
+
server.registerTool("delete-mailbox", {
|
|
1047
|
+
description: "Use when: deleting a mailbox/folder from an account.\nReturns: a confirmation that the mailbox was deleted.\nDo not use when: renaming it (use rename-mailbox) or deleting messages within it (use delete-message / batch-delete-messages).\nSafety: destructive — deleting a mailbox removes the folder and any messages it contains. Require explicit user confirmation and use list-mailboxes first to confirm the exact name.",
|
|
1048
|
+
inputSchema: {
|
|
1049
|
+
name: z.string().min(1, "Mailbox name is required"),
|
|
1050
|
+
account: z.string().optional().describe("Account containing the mailbox"),
|
|
1051
|
+
},
|
|
1052
|
+
outputSchema: {
|
|
1053
|
+
ok: z.boolean().optional(),
|
|
1054
|
+
name: z.string().optional(),
|
|
1055
|
+
},
|
|
820
1056
|
}, withErrorHandling(async ({ name, account }) => {
|
|
821
1057
|
if (isImapAccount(account)) {
|
|
822
1058
|
const r = await imapDeleteMailbox(name, { account });
|
|
@@ -831,10 +1067,18 @@ server.tool("delete-mailbox", "Use when: deleting a mailbox/folder from an accou
|
|
|
831
1067
|
return successResponse(`Mailbox "${name}" deleted`, { ok: true, name });
|
|
832
1068
|
}, "Error deleting mailbox"));
|
|
833
1069
|
// --- rename-mailbox ---
|
|
834
|
-
server.
|
|
835
|
-
oldName:
|
|
836
|
-
|
|
837
|
-
|
|
1070
|
+
server.registerTool("rename-mailbox", {
|
|
1071
|
+
description: "Use when: renaming an existing mailbox/folder from oldName to newName within an account.\nReturns: a confirmation naming the old and new mailbox names.\nDo not use when: creating a new folder (use create-mailbox) or deleting one (use delete-mailbox). Use list-mailboxes to confirm the current name.\nSafety: renames a real folder in the mail account — confirm oldName matches exactly (case-sensitive) before calling.",
|
|
1072
|
+
inputSchema: {
|
|
1073
|
+
oldName: z.string().min(1, "Current mailbox name is required"),
|
|
1074
|
+
newName: z.string().min(1, "New mailbox name is required"),
|
|
1075
|
+
account: z.string().optional().describe("Account containing the mailbox"),
|
|
1076
|
+
},
|
|
1077
|
+
outputSchema: {
|
|
1078
|
+
ok: z.boolean().optional(),
|
|
1079
|
+
oldName: z.string().optional(),
|
|
1080
|
+
newName: z.string().optional(),
|
|
1081
|
+
},
|
|
838
1082
|
}, withErrorHandling(async ({ oldName, newName, account }) => {
|
|
839
1083
|
if (isImapAccount(account)) {
|
|
840
1084
|
const r = await imapRenameMailbox(oldName, newName, { account });
|
|
@@ -861,7 +1105,14 @@ server.tool("rename-mailbox", "Use when: renaming an existing mailbox/folder fro
|
|
|
861
1105
|
// Account Tools
|
|
862
1106
|
// =============================================================================
|
|
863
1107
|
// --- list-accounts ---
|
|
864
|
-
server.
|
|
1108
|
+
server.registerTool("list-accounts", {
|
|
1109
|
+
description: "Use when: discovering the configured Mail accounts (e.g. iCloud, Gmail) so you can pass an exact account name to other tools.\nReturns: the account names and a count.\nDo not use when: you want the folders within an account (use list-mailboxes) or messages (use list-messages / search-messages).",
|
|
1110
|
+
inputSchema: {},
|
|
1111
|
+
outputSchema: {
|
|
1112
|
+
accounts: z.array(z.object({}).passthrough()).optional(),
|
|
1113
|
+
count: z.number().optional(),
|
|
1114
|
+
},
|
|
1115
|
+
}, withErrorHandling(() => {
|
|
865
1116
|
const accounts = mailManager.listAccounts();
|
|
866
1117
|
const structured = { accounts, count: accounts.length };
|
|
867
1118
|
if (accounts.length === 0) {
|
|
@@ -874,7 +1125,14 @@ server.tool("list-accounts", "Use when: discovering the configured Mail accounts
|
|
|
874
1125
|
// Mail Rules Tools
|
|
875
1126
|
// =============================================================================
|
|
876
1127
|
// --- list-rules ---
|
|
877
|
-
server.
|
|
1128
|
+
server.registerTool("list-rules", {
|
|
1129
|
+
description: "Use when: discovering the Mail rules that exist and whether each is enabled or disabled, e.g. before enabling/disabling/deleting one.\nReturns: each rule's name and enabled/disabled state.\nDo not use when: you want to change a rule (use enable-rule / disable-rule / create-rule / delete-rule).",
|
|
1130
|
+
inputSchema: {},
|
|
1131
|
+
outputSchema: {
|
|
1132
|
+
rules: z.array(z.object({}).passthrough()).optional(),
|
|
1133
|
+
count: z.number().optional(),
|
|
1134
|
+
},
|
|
1135
|
+
}, withErrorHandling(() => {
|
|
878
1136
|
const rules = mailManager.listRules();
|
|
879
1137
|
const structured = {
|
|
880
1138
|
rules: rules.map((r) => ({ name: r.name, enabled: r.enabled })),
|
|
@@ -889,8 +1147,16 @@ server.tool("list-rules", "Use when: discovering the Mail rules that exist and w
|
|
|
889
1147
|
return successResponse(`Found ${rules.length} rule(s):\n${ruleList}`, structured);
|
|
890
1148
|
}, "Error listing rules"));
|
|
891
1149
|
// --- enable-rule ---
|
|
892
|
-
server.
|
|
893
|
-
name:
|
|
1150
|
+
server.registerTool("enable-rule", {
|
|
1151
|
+
description: "Use when: turning on an existing Mail rule by name.\nReturns: a confirmation that the rule was enabled.\nDo not use when: turning a rule off (use disable-rule), creating one (use create-rule), or deleting one (use delete-rule). Use list-rules to confirm the exact rule name.",
|
|
1152
|
+
inputSchema: {
|
|
1153
|
+
name: z.string().min(1, "Rule name is required"),
|
|
1154
|
+
},
|
|
1155
|
+
outputSchema: {
|
|
1156
|
+
ok: z.boolean().optional(),
|
|
1157
|
+
name: z.string().optional(),
|
|
1158
|
+
enabled: z.boolean().optional(),
|
|
1159
|
+
},
|
|
894
1160
|
}, withErrorHandling(({ name }) => {
|
|
895
1161
|
const success = mailManager.setRuleEnabled(name, true);
|
|
896
1162
|
if (!success) {
|
|
@@ -899,8 +1165,16 @@ server.tool("enable-rule", "Use when: turning on an existing Mail rule by name.\
|
|
|
899
1165
|
return successResponse(`Rule "${name}" enabled`, { ok: true, name, enabled: true });
|
|
900
1166
|
}, "Error enabling rule"));
|
|
901
1167
|
// --- disable-rule ---
|
|
902
|
-
server.
|
|
903
|
-
name:
|
|
1168
|
+
server.registerTool("disable-rule", {
|
|
1169
|
+
description: "Use when: turning off an existing Mail rule by name (without deleting it).\nReturns: a confirmation that the rule was disabled.\nDo not use when: turning a rule on (use enable-rule), creating one (use create-rule), or removing it permanently (use delete-rule). Use list-rules to confirm the exact rule name.",
|
|
1170
|
+
inputSchema: {
|
|
1171
|
+
name: z.string().min(1, "Rule name is required"),
|
|
1172
|
+
},
|
|
1173
|
+
outputSchema: {
|
|
1174
|
+
ok: z.boolean().optional(),
|
|
1175
|
+
name: z.string().optional(),
|
|
1176
|
+
enabled: z.boolean().optional(),
|
|
1177
|
+
},
|
|
904
1178
|
}, withErrorHandling(({ name }) => {
|
|
905
1179
|
const success = mailManager.setRuleEnabled(name, false);
|
|
906
1180
|
if (!success) {
|
|
@@ -909,28 +1183,35 @@ server.tool("disable-rule", "Use when: turning off an existing Mail rule by name
|
|
|
909
1183
|
return successResponse(`Rule "${name}" disabled`, { ok: true, name, enabled: false });
|
|
910
1184
|
}, "Error disabling rule"));
|
|
911
1185
|
// --- create-rule ---
|
|
912
|
-
server.
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
.
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
.enum(["
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
1186
|
+
server.registerTool("create-rule", {
|
|
1187
|
+
description: "Use when: creating a new Mail rule with one or more conditions (field/operator/value) and at least one action (markRead, markFlagged, delete, or moveTo). Set matchAll to require all conditions vs. any.\nReturns: a confirmation naming the rule and its condition count.\nDo not use when: toggling an existing rule (use enable-rule / disable-rule) or removing one (use delete-rule). Use list-rules to avoid duplicating an existing rule.\nSafety: creates a rule that automatically acts on real mail (including delete/move actions) on an ongoing basis — confirm the conditions and actions with the user before calling.",
|
|
1188
|
+
inputSchema: {
|
|
1189
|
+
name: z.string().min(1, "Rule name is required"),
|
|
1190
|
+
conditions: z
|
|
1191
|
+
.array(z.object({
|
|
1192
|
+
field: z.enum(["from", "to", "cc", "subject", "content"]),
|
|
1193
|
+
operator: z
|
|
1194
|
+
.enum(["contains", "notContains", "equals", "beginsWith", "endsWith"])
|
|
1195
|
+
.default("contains"),
|
|
1196
|
+
value: z.string().min(1, "Condition value is required"),
|
|
1197
|
+
}))
|
|
1198
|
+
.min(1, "At least one condition is required"),
|
|
1199
|
+
actions: z
|
|
1200
|
+
.object({
|
|
1201
|
+
markRead: z.boolean().optional(),
|
|
1202
|
+
markFlagged: z.boolean().optional(),
|
|
1203
|
+
delete: z.boolean().optional(),
|
|
1204
|
+
moveTo: z.string().optional(),
|
|
1205
|
+
moveToAccount: z.string().optional(),
|
|
1206
|
+
})
|
|
1207
|
+
.refine((a) => a.markRead || a.markFlagged || a.delete || a.moveTo, "At least one action is required (markRead, markFlagged, delete, or moveTo)"),
|
|
1208
|
+
matchAll: z.boolean().default(true),
|
|
1209
|
+
enabled: z.boolean().default(true),
|
|
1210
|
+
},
|
|
1211
|
+
outputSchema: {
|
|
1212
|
+
name: z.string().optional(),
|
|
1213
|
+
created: z.boolean().optional(),
|
|
1214
|
+
},
|
|
934
1215
|
}, withErrorHandling((args) => {
|
|
935
1216
|
const result = mailManager.createRule(args);
|
|
936
1217
|
if (!result.success) {
|
|
@@ -939,8 +1220,15 @@ server.tool("create-rule", "Use when: creating a new Mail rule with one or more
|
|
|
939
1220
|
return successResponse(`Rule "${args.name}" created with ${args.conditions.length} condition(s).`, { name: args.name, created: true });
|
|
940
1221
|
}, "Error creating rule"));
|
|
941
1222
|
// --- delete-rule ---
|
|
942
|
-
server.
|
|
943
|
-
name:
|
|
1223
|
+
server.registerTool("delete-rule", {
|
|
1224
|
+
description: "Use when: permanently removing a Mail rule by name.\nReturns: a confirmation that the rule was deleted.\nDo not use when: you only want to pause it (use disable-rule) or create one (use create-rule).\nSafety: destructive — the rule is removed permanently. Require explicit user confirmation and use list-rules first to confirm the exact name.",
|
|
1225
|
+
inputSchema: {
|
|
1226
|
+
name: z.string().min(1, "Rule name is required"),
|
|
1227
|
+
},
|
|
1228
|
+
outputSchema: {
|
|
1229
|
+
name: z.string().optional(),
|
|
1230
|
+
deleted: z.boolean().optional(),
|
|
1231
|
+
},
|
|
944
1232
|
}, withErrorHandling(({ name }) => {
|
|
945
1233
|
const success = mailManager.deleteRule(name);
|
|
946
1234
|
if (!success) {
|
|
@@ -952,8 +1240,15 @@ server.tool("delete-rule", "Use when: permanently removing a Mail rule by name.\
|
|
|
952
1240
|
// Contacts Tools
|
|
953
1241
|
// =============================================================================
|
|
954
1242
|
// --- search-contacts ---
|
|
955
|
-
server.
|
|
956
|
-
|
|
1243
|
+
server.registerTool("search-contacts", {
|
|
1244
|
+
description: "Use when: looking up a person in Contacts.app by name to find their email address(es) before composing or sending mail.\nReturns: matching contacts with their names and email addresses.\nDo not use when: searching email messages (use search-messages) — this queries Contacts, not the mailbox.",
|
|
1245
|
+
inputSchema: {
|
|
1246
|
+
query: z.string().min(1, "Search query is required"),
|
|
1247
|
+
},
|
|
1248
|
+
outputSchema: {
|
|
1249
|
+
contacts: z.array(z.object({}).passthrough()).optional(),
|
|
1250
|
+
count: z.number().optional(),
|
|
1251
|
+
},
|
|
957
1252
|
}, withErrorHandling(({ query }) => {
|
|
958
1253
|
const contacts = mailManager.searchContacts(query);
|
|
959
1254
|
const structured = {
|
|
@@ -975,13 +1270,21 @@ server.tool("search-contacts", "Use when: looking up a person in Contacts.app by
|
|
|
975
1270
|
// Email Template Tools
|
|
976
1271
|
// =============================================================================
|
|
977
1272
|
// --- save-template ---
|
|
978
|
-
server.
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
1273
|
+
server.registerTool("save-template", {
|
|
1274
|
+
description: "Use when: creating a reusable email template (name, subject, body, optional default to/cc), or updating one by passing its existing id. Subject/body may contain placeholders for later use.\nReturns: the saved template's name and id (reuse the id with use-template / get-template / delete-template).\nDo not use when: composing a one-off message (use create-draft / send-email) or filling in a template to send (use use-template).\nSafety: writes the template to the on-disk templates store (APPLE_MAIL_MCP_TEMPLATES_FILE) and persists across restarts; passing an existing id overwrites that template.",
|
|
1275
|
+
inputSchema: {
|
|
1276
|
+
name: z.string().min(1, "Template name is required"),
|
|
1277
|
+
subject: z.string().min(1, "Subject is required"),
|
|
1278
|
+
body: z.string().min(1, "Body is required"),
|
|
1279
|
+
to: z.array(z.string()).optional().describe("Default recipients"),
|
|
1280
|
+
cc: z.array(z.string()).optional().describe("Default CC recipients"),
|
|
1281
|
+
id: z.string().optional().describe("Template ID (for updating existing template)"),
|
|
1282
|
+
},
|
|
1283
|
+
outputSchema: {
|
|
1284
|
+
ok: z.boolean().optional(),
|
|
1285
|
+
id: z.string().optional(),
|
|
1286
|
+
name: z.string().optional(),
|
|
1287
|
+
},
|
|
985
1288
|
}, withErrorHandling(({ name, subject, body, to, cc, id }) => {
|
|
986
1289
|
const template = mailManager.saveTemplate(name, subject, body, to, cc, id);
|
|
987
1290
|
return successResponse(`Template "${template.name}" saved with ID: ${template.id}`, {
|
|
@@ -991,7 +1294,14 @@ server.tool("save-template", "Use when: creating a reusable email template (name
|
|
|
991
1294
|
});
|
|
992
1295
|
}, "Error saving template"));
|
|
993
1296
|
// --- list-templates ---
|
|
994
|
-
server.
|
|
1297
|
+
server.registerTool("list-templates", {
|
|
1298
|
+
description: "Use when: discovering the saved email templates and their ids, e.g. before using or editing one.\nReturns: each template's id, name, and subject.\nDo not use when: you want a single template's full body (use get-template) or want to apply one (use use-template).",
|
|
1299
|
+
inputSchema: {},
|
|
1300
|
+
outputSchema: {
|
|
1301
|
+
templates: z.array(z.object({}).passthrough()).optional(),
|
|
1302
|
+
count: z.number().optional(),
|
|
1303
|
+
},
|
|
1304
|
+
}, withErrorHandling(() => {
|
|
995
1305
|
const templates = mailManager.listTemplates();
|
|
996
1306
|
const structured = {
|
|
997
1307
|
templates: templates.map((t) => ({ id: t.id, name: t.name, subject: t.subject })),
|
|
@@ -1006,8 +1316,19 @@ server.tool("list-templates", "Use when: discovering the saved email templates a
|
|
|
1006
1316
|
return successResponse(`Found ${templates.length} template(s):\n${templateList}`, structured);
|
|
1007
1317
|
}, "Error listing templates"));
|
|
1008
1318
|
// --- get-template ---
|
|
1009
|
-
server.
|
|
1010
|
-
id:
|
|
1319
|
+
server.registerTool("get-template", {
|
|
1320
|
+
description: "Use when: reading the full contents of one saved template by id — its name, subject, default to/cc, and body.\nReturns: the template's name, subject, default recipients, and body text.\nDo not use when: you don't have the id (use list-templates first) or want to apply the template into a draft (use use-template).",
|
|
1321
|
+
inputSchema: {
|
|
1322
|
+
id: z.string().min(1, "Template ID is required"),
|
|
1323
|
+
},
|
|
1324
|
+
outputSchema: {
|
|
1325
|
+
id: z.string().optional(),
|
|
1326
|
+
name: z.string().optional(),
|
|
1327
|
+
subject: z.string().optional(),
|
|
1328
|
+
to: z.array(z.string()).optional(),
|
|
1329
|
+
cc: z.array(z.string()).optional(),
|
|
1330
|
+
body: z.string().optional(),
|
|
1331
|
+
},
|
|
1011
1332
|
}, withErrorHandling(({ id }) => {
|
|
1012
1333
|
const template = mailManager.getTemplate(id);
|
|
1013
1334
|
if (!template) {
|
|
@@ -1032,8 +1353,15 @@ server.tool("get-template", "Use when: reading the full contents of one saved te
|
|
|
1032
1353
|
});
|
|
1033
1354
|
}, "Error getting template"));
|
|
1034
1355
|
// --- delete-template ---
|
|
1035
|
-
server.
|
|
1036
|
-
id:
|
|
1356
|
+
server.registerTool("delete-template", {
|
|
1357
|
+
description: "Use when: permanently removing a saved email template by id.\nReturns: a confirmation that the template was deleted.\nDo not use when: you only want to view it (use get-template) or update it (use save-template with the existing id).\nSafety: destructive — removes the template from the on-disk store permanently. Require explicit user confirmation and use list-templates first to confirm the id.",
|
|
1358
|
+
inputSchema: {
|
|
1359
|
+
id: z.string().min(1, "Template ID is required"),
|
|
1360
|
+
},
|
|
1361
|
+
outputSchema: {
|
|
1362
|
+
ok: z.boolean().optional(),
|
|
1363
|
+
id: z.string().optional(),
|
|
1364
|
+
},
|
|
1037
1365
|
}, withErrorHandling(({ id }) => {
|
|
1038
1366
|
const success = mailManager.deleteTemplate(id);
|
|
1039
1367
|
if (!success) {
|
|
@@ -1042,12 +1370,19 @@ server.tool("delete-template", "Use when: permanently removing a saved email tem
|
|
|
1042
1370
|
return successResponse(`Template "${id}" deleted`, { ok: true, id });
|
|
1043
1371
|
}, "Error deleting template"));
|
|
1044
1372
|
// --- use-template ---
|
|
1045
|
-
server.
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1373
|
+
server.registerTool("use-template", {
|
|
1374
|
+
description: "Use when: composing a new draft from a saved template (by id), optionally overriding the recipients, subject, or body. Creates a draft in Mail.app for the user to review and send.\nReturns: a confirmation that a draft was created from the template.\nDo not use when: you want to inspect the template without composing (use get-template) or send immediately without a draft (use send-email).",
|
|
1375
|
+
inputSchema: {
|
|
1376
|
+
id: z.string().min(1, "Template ID is required"),
|
|
1377
|
+
to: z.array(z.string()).optional().describe("Override recipients"),
|
|
1378
|
+
cc: z.array(z.string()).optional().describe("Override CC recipients"),
|
|
1379
|
+
subject: z.string().optional().describe("Override subject"),
|
|
1380
|
+
body: z.string().optional().describe("Override body"),
|
|
1381
|
+
},
|
|
1382
|
+
outputSchema: {
|
|
1383
|
+
ok: z.boolean().optional(),
|
|
1384
|
+
id: z.string().optional(),
|
|
1385
|
+
},
|
|
1051
1386
|
}, withErrorHandling(({ id, to, cc, subject, body }) => {
|
|
1052
1387
|
const success = mailManager.useTemplate(id, { to, cc, subject, body });
|
|
1053
1388
|
if (!success) {
|
|
@@ -1059,7 +1394,14 @@ server.tool("use-template", "Use when: composing a new draft from a saved templa
|
|
|
1059
1394
|
// Diagnostics Tools
|
|
1060
1395
|
// =============================================================================
|
|
1061
1396
|
// --- health-check ---
|
|
1062
|
-
server.
|
|
1397
|
+
server.registerTool("health-check", {
|
|
1398
|
+
description: "Use when: doing a quick check that Mail.app is reachable and the server's basic checks pass.\nReturns: an overall healthy/unhealthy status with a pass/fail line per check.\nDo not use when: you need detailed permission/account/IMAP/SMTP diagnostics with remediation steps (use doctor).",
|
|
1399
|
+
inputSchema: {},
|
|
1400
|
+
outputSchema: {
|
|
1401
|
+
healthy: z.boolean().optional(),
|
|
1402
|
+
checks: z.array(CHECK_ITEM_SCHEMA).optional(),
|
|
1403
|
+
},
|
|
1404
|
+
}, withErrorHandling(() => {
|
|
1063
1405
|
const result = mailManager.healthCheck();
|
|
1064
1406
|
const statusIcon = result.healthy ? "✓" : "✗";
|
|
1065
1407
|
const statusText = result.healthy ? "All checks passed" : "Issues detected";
|
|
@@ -1072,18 +1414,36 @@ server.tool("health-check", "Use when: doing a quick check that Mail.app is reac
|
|
|
1072
1414
|
return successResponse(`${statusIcon} ${statusText}\n\n${checkLines}`, { ...result });
|
|
1073
1415
|
}, "Error running health check"));
|
|
1074
1416
|
// --- doctor ---
|
|
1075
|
-
server.
|
|
1417
|
+
server.registerTool("doctor", {
|
|
1418
|
+
description: "Use when: troubleshooting setup problems — diagnoses Mail.app automation permissions, account state, and the IMAP/SMTP backends with actionable remediation messages.\nReturns: a detailed diagnostic report (formatted text plus structured checks).\nDo not use when: you just want a quick up/down status (use health-check) or message counts (use get-mail-stats).",
|
|
1419
|
+
inputSchema: {},
|
|
1420
|
+
outputSchema: {
|
|
1421
|
+
healthy: z.boolean().optional(),
|
|
1422
|
+
checks: z.array(CHECK_ITEM_SCHEMA).optional(),
|
|
1423
|
+
},
|
|
1424
|
+
}, withErrorHandling(async () => {
|
|
1076
1425
|
// Diagnoses Mail.app permission, account state, and the IMAP/SMTP backends
|
|
1077
1426
|
// with actionable messages (C3). structuredContent carries the raw checks.
|
|
1078
1427
|
const report = await runDoctor(mailManager);
|
|
1079
1428
|
return successResponse(formatDoctorReport(report), { ...report });
|
|
1080
1429
|
}, "Error running doctor"));
|
|
1081
1430
|
// --- get-mail-stats ---
|
|
1082
|
-
server.
|
|
1083
|
-
account:
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1431
|
+
server.registerTool("get-mail-stats", {
|
|
1432
|
+
description: "Use when: you want aggregate mailbox statistics — total and unread message counts, recently-received counts (last 24h/7d/30d), and (for the all-accounts path) a per-account breakdown.\nReturns: totals, unread counts, recent-activity counts, and per-account figures.\nDo not use when: you only need a single unread number (use get-unread-count) or want to list the messages themselves (use list-messages / search-messages).",
|
|
1433
|
+
inputSchema: {
|
|
1434
|
+
account: z
|
|
1435
|
+
.string()
|
|
1436
|
+
.optional()
|
|
1437
|
+
.describe("Limit to one account; uses fast IMAP STATUS if that account is IMAP-configured"),
|
|
1438
|
+
},
|
|
1439
|
+
outputSchema: {
|
|
1440
|
+
account: z.string().optional(),
|
|
1441
|
+
totalMessages: z.number().optional(),
|
|
1442
|
+
totalUnread: z.number().optional(),
|
|
1443
|
+
accounts: z.array(z.object({}).passthrough()).optional(),
|
|
1444
|
+
recentlyReceived: z.object({}).passthrough().optional(),
|
|
1445
|
+
recent: z.object({}).passthrough().optional(),
|
|
1446
|
+
},
|
|
1087
1447
|
}, withErrorHandling(async ({ account }) => {
|
|
1088
1448
|
// IMAP (I3): for a named IMAP account, STATUS gives authoritative counts and
|
|
1089
1449
|
// SEARCH SINCE gives recent activity — fast even on huge mailboxes.
|
|
@@ -1125,7 +1485,17 @@ server.tool("get-mail-stats", "Use when: you want aggregate mailbox statistics
|
|
|
1125
1485
|
return successResponse(lines.join("\n"), { ...stats });
|
|
1126
1486
|
}, "Error getting mail statistics"));
|
|
1127
1487
|
// --- get-sync-status ---
|
|
1128
|
-
server.
|
|
1488
|
+
server.registerTool("get-sync-status", {
|
|
1489
|
+
description: "Use when: checking whether Mail.app is running and actively syncing, e.g. to explain why new mail hasn't appeared yet.\nReturns: whether Mail.app is running and whether sync activity was detected.\nDo not use when: you need message counts (use get-mail-stats) or a full setup diagnosis (use doctor).",
|
|
1490
|
+
inputSchema: {},
|
|
1491
|
+
outputSchema: {
|
|
1492
|
+
syncDetected: z.boolean().optional(),
|
|
1493
|
+
pendingUpload: z.number().optional(),
|
|
1494
|
+
recentActivity: z.boolean().optional(),
|
|
1495
|
+
secondsSinceLastChange: z.number().optional(),
|
|
1496
|
+
error: z.string().optional(),
|
|
1497
|
+
},
|
|
1498
|
+
}, withErrorHandling(() => {
|
|
1129
1499
|
const status = mailManager.getSyncStatus();
|
|
1130
1500
|
const lines = [];
|
|
1131
1501
|
lines.push(`🔄 Mail Sync Status`);
|