@agenticmail/mcp 0.3.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/dist/index.js ADDED
@@ -0,0 +1,2106 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
+
7
+ // src/pending-followup.ts
8
+ var STEP_DELAYS_MS = [
9
+ 12 * 36e5,
10
+ // 0 → 12 hours
11
+ 6 * 36e5,
12
+ // 1 → 6 hours
13
+ 3 * 36e5,
14
+ // 2 → 3 hours
15
+ 1 * 36e5
16
+ // 3 → 1 hour (final before cooldown)
17
+ ];
18
+ var COOLDOWN_MS = 3 * 24 * 36e5;
19
+ var HEARTBEAT_INTERVAL_MS = 5 * 6e4;
20
+ var tracked = /* @__PURE__ */ new Map();
21
+ var queue = [];
22
+ var heartbeatTimer = null;
23
+ function scheduleFollowUp(pendingId, recipient, subject, checkFn) {
24
+ if (tracked.has(pendingId)) return;
25
+ arm(pendingId, recipient, subject, checkFn, 0, 0);
26
+ startHeartbeat();
27
+ }
28
+ function drainFollowUps() {
29
+ if (queue.length === 0) return [];
30
+ const items = [...queue];
31
+ queue.length = 0;
32
+ return items;
33
+ }
34
+ function cancelFollowUp(pendingId) {
35
+ const entry = tracked.get(pendingId);
36
+ if (entry) {
37
+ clearTimeout(entry.timer);
38
+ tracked.delete(pendingId);
39
+ }
40
+ }
41
+ function arm(pendingId, recipient, subject, checkFn, step, cycle) {
42
+ const delayMs = step < STEP_DELAYS_MS.length ? STEP_DELAYS_MS[step] : COOLDOWN_MS;
43
+ const timer = setTimeout(() => fire(pendingId), delayMs);
44
+ timer.unref();
45
+ tracked.set(pendingId, { pendingId, recipient, subject, checkFn, step, cycle, timer });
46
+ }
47
+ async function fire(pendingId) {
48
+ const entry = tracked.get(pendingId);
49
+ if (!entry) return;
50
+ try {
51
+ const stillPending = await entry.checkFn();
52
+ if (!stillPending) {
53
+ tracked.delete(pendingId);
54
+ return;
55
+ }
56
+ } catch {
57
+ }
58
+ const { recipient, subject, step, cycle, checkFn } = entry;
59
+ const isFinal = step === STEP_DELAYS_MS.length - 1;
60
+ const isPostCooldown = step >= STEP_DELAYS_MS.length;
61
+ let message;
62
+ if (isPostCooldown) {
63
+ const totalDays = 3 * (cycle + 1);
64
+ message = [
65
+ `[FOLLOW-UP REMINDER \u2014 cycle ${cycle + 2}]`,
66
+ `Your blocked email to ${recipient} (subject: "${subject}") has been pending for over ${totalDays} days.`,
67
+ `Starting a new follow-up cycle. Please remind your owner that this email still needs their review.`,
68
+ `Pending ID: ${pendingId}`
69
+ ].join("\n");
70
+ } else if (isFinal) {
71
+ message = [
72
+ `[FINAL FOLLOW-UP]`,
73
+ `Your blocked email to ${recipient} (subject: "${subject}") is STILL pending approval.`,
74
+ `This is the last reminder before a 3-day cooldown. Please urgently remind your owner.`,
75
+ `Let them know you will not follow up again for 3 days unless they respond.`,
76
+ `Pending ID: ${pendingId}`
77
+ ].join("\n");
78
+ } else {
79
+ const nextDelayH = STEP_DELAYS_MS[step + 1] / 36e5;
80
+ message = [
81
+ `[FOLLOW-UP REMINDER ${step + 1}/${STEP_DELAYS_MS.length}]`,
82
+ `Your blocked email to ${recipient} (subject: "${subject}") is still pending owner approval.`,
83
+ `Please follow up with your owner \u2014 ask if they've reviewed the notification email.`,
84
+ `Next reminder in ${nextDelayH} hour${nextDelayH !== 1 ? "s" : ""}.`,
85
+ `Pending ID: ${pendingId}`
86
+ ].join("\n");
87
+ }
88
+ queue.push({
89
+ pendingId,
90
+ recipient,
91
+ subject,
92
+ attempt: isPostCooldown ? 1 : step + 1,
93
+ isFinalBeforeCooldown: isFinal,
94
+ message
95
+ });
96
+ if (isPostCooldown) {
97
+ arm(pendingId, recipient, subject, checkFn, 0, cycle + 1);
98
+ } else {
99
+ arm(pendingId, recipient, subject, checkFn, step + 1, cycle);
100
+ }
101
+ }
102
+ function startHeartbeat() {
103
+ if (heartbeatTimer) return;
104
+ heartbeatTimer = setInterval(heartbeat, HEARTBEAT_INTERVAL_MS);
105
+ heartbeatTimer.unref();
106
+ }
107
+ function stopHeartbeat() {
108
+ if (heartbeatTimer) {
109
+ clearInterval(heartbeatTimer);
110
+ heartbeatTimer = null;
111
+ }
112
+ }
113
+ async function heartbeat() {
114
+ if (tracked.size === 0) {
115
+ stopHeartbeat();
116
+ return;
117
+ }
118
+ for (const [pendingId, entry] of tracked) {
119
+ try {
120
+ const stillPending = await entry.checkFn();
121
+ if (!stillPending) {
122
+ clearTimeout(entry.timer);
123
+ tracked.delete(pendingId);
124
+ }
125
+ } catch {
126
+ }
127
+ }
128
+ if (tracked.size === 0) stopHeartbeat();
129
+ }
130
+
131
+ // src/tools.ts
132
+ var API_URL = process.env.AGENTICMAIL_API_URL ?? "http://127.0.0.1:3100";
133
+ var API_KEY = process.env.AGENTICMAIL_API_KEY ?? "";
134
+ var MASTER_KEY = process.env.AGENTICMAIL_MASTER_KEY ?? "";
135
+ if (!API_KEY && !MASTER_KEY) {
136
+ console.error("[agenticmail-mcp] Warning: Neither AGENTICMAIL_API_KEY nor AGENTICMAIL_MASTER_KEY is set");
137
+ }
138
+ function makePendingCheck(pendingId) {
139
+ return async () => {
140
+ try {
141
+ const res = await fetch(`${API_URL}/api/agenticmail/mail/pending/${encodeURIComponent(pendingId)}`, {
142
+ headers: { "Authorization": `Bearer ${API_KEY}` },
143
+ signal: AbortSignal.timeout(1e4)
144
+ });
145
+ if (!res.ok) return false;
146
+ const data = await res.json();
147
+ return data?.status === "pending";
148
+ } catch {
149
+ return true;
150
+ }
151
+ };
152
+ }
153
+ function withReminders(text) {
154
+ const reminders = drainFollowUps();
155
+ if (reminders.length === 0) return text;
156
+ return text + "\n\n" + reminders.map((r) => r.message).join("\n\n");
157
+ }
158
+ async function apiRequest(method, path, body, useMasterKey = false, timeoutMs = 3e4) {
159
+ const key = useMasterKey && MASTER_KEY ? MASTER_KEY : API_KEY;
160
+ if (!key) {
161
+ throw new Error(useMasterKey ? "Master key is required for this operation. Set AGENTICMAIL_MASTER_KEY." : "API key is not configured. Set AGENTICMAIL_API_KEY.");
162
+ }
163
+ const headers = { "Authorization": `Bearer ${key}` };
164
+ if (body !== void 0) headers["Content-Type"] = "application/json";
165
+ const response = await fetch(`${API_URL}/api/agenticmail${path}`, {
166
+ method,
167
+ headers,
168
+ body: body ? JSON.stringify(body) : void 0,
169
+ signal: AbortSignal.timeout(timeoutMs)
170
+ });
171
+ if (!response.ok) {
172
+ let text;
173
+ try {
174
+ text = await response.text();
175
+ } catch {
176
+ text = "(could not read response body)";
177
+ }
178
+ throw new Error(`API error ${response.status}: ${text}`);
179
+ }
180
+ const contentType = response.headers.get("content-type");
181
+ if (contentType?.includes("application/json")) {
182
+ try {
183
+ return await response.json();
184
+ } catch {
185
+ throw new Error(`API returned invalid JSON from ${path}`);
186
+ }
187
+ }
188
+ return null;
189
+ }
190
+ var toolDefinitions = [
191
+ {
192
+ name: "send_email",
193
+ description: "Send an email from the agent's mailbox. External emails are scanned for sensitive content. HIGH severity detections are BLOCKED and held for owner approval. Your owner will be notified and must approve blocked emails. You CANNOT bypass the outbound guard.",
194
+ inputSchema: {
195
+ type: "object",
196
+ properties: {
197
+ to: { type: "string", description: "Recipient email address" },
198
+ subject: { type: "string", description: "Email subject line" },
199
+ text: { type: "string", description: "Plain text body" },
200
+ html: { type: "string", description: "HTML body (optional)" },
201
+ cc: { type: "string", description: "CC recipients (optional)" },
202
+ inReplyTo: { type: "string", description: "Message-ID to reply to (optional)" },
203
+ references: {
204
+ type: "array",
205
+ items: { type: "string" },
206
+ description: "Message-IDs for threading (optional)"
207
+ },
208
+ attachments: {
209
+ type: "array",
210
+ items: {
211
+ type: "object",
212
+ properties: {
213
+ filename: { type: "string", description: "Attachment filename" },
214
+ content: { type: "string", description: "File content as text string (for text files) or base64-encoded string (for binary files)" },
215
+ contentType: { type: "string", description: "MIME type (e.g. text/plain, application/pdf)" },
216
+ encoding: { type: "string", description: 'Set to "base64" only if content is base64-encoded' }
217
+ },
218
+ required: ["filename", "content"]
219
+ },
220
+ description: "File attachments"
221
+ }
222
+ },
223
+ required: ["to", "subject"]
224
+ }
225
+ },
226
+ {
227
+ name: "list_inbox",
228
+ description: "List recent emails in the agent's inbox",
229
+ inputSchema: {
230
+ type: "object",
231
+ properties: {
232
+ limit: { type: "number", description: "Maximum number of messages to return (default: 20)" },
233
+ offset: { type: "number", description: "Number of messages to skip (default: 0)" }
234
+ }
235
+ }
236
+ },
237
+ {
238
+ name: "read_email",
239
+ description: "Read the full content of a specific email by its UID",
240
+ inputSchema: {
241
+ type: "object",
242
+ properties: {
243
+ uid: { type: "number", description: "The UID of the email to read" }
244
+ },
245
+ required: ["uid"]
246
+ }
247
+ },
248
+ {
249
+ name: "delete_email",
250
+ description: "Delete an email by its UID",
251
+ inputSchema: {
252
+ type: "object",
253
+ properties: {
254
+ uid: { type: "number", description: "The UID of the email to delete" }
255
+ },
256
+ required: ["uid"]
257
+ }
258
+ },
259
+ {
260
+ name: "search_emails",
261
+ description: "Search emails by criteria (from, to, subject, text, date range). By default searches the local inbox only. Set searchRelay=true to also search the connected Gmail/Outlook account \u2014 results include relay UIDs that can be imported with import_relay_email.",
262
+ inputSchema: {
263
+ type: "object",
264
+ properties: {
265
+ from: { type: "string", description: "Filter by sender address" },
266
+ to: { type: "string", description: "Filter by recipient address" },
267
+ subject: { type: "string", description: "Filter by subject keyword" },
268
+ text: { type: "string", description: "Search body text" },
269
+ since: { type: "string", description: "Messages since date (ISO 8601)" },
270
+ before: { type: "string", description: "Messages before date (ISO 8601)" },
271
+ seen: { type: "boolean", description: "Filter by read/unread status" },
272
+ searchRelay: { type: "boolean", description: "Also search the connected Gmail/Outlook account (default: false). Use this to find past emails from the user's main inbox." }
273
+ }
274
+ }
275
+ },
276
+ {
277
+ name: "import_relay_email",
278
+ description: "Import an email from the connected Gmail/Outlook account into the agent's local inbox. This downloads the full message with all headers (Message-ID, In-Reply-To, References) so you can continue the thread using reply_email. Use search_emails with searchRelay=true first to find the relay UID.",
279
+ inputSchema: {
280
+ type: "object",
281
+ properties: {
282
+ uid: { type: "number", description: "The relay UID of the email to import (from search_emails relay results)" }
283
+ },
284
+ required: ["uid"]
285
+ }
286
+ },
287
+ {
288
+ name: "reply_email",
289
+ description: "Reply to an email. Fetches the original message, auto-fills To, Subject (Re:), In-Reply-To, and References, then sends with quoted body. Outbound guard applies \u2014 HIGH severity content is held for review.",
290
+ inputSchema: {
291
+ type: "object",
292
+ properties: {
293
+ uid: { type: "number", description: "UID of the email to reply to" },
294
+ text: { type: "string", description: "Your reply text" },
295
+ html: { type: "string", description: "HTML reply (optional)" },
296
+ replyAll: { type: "boolean", description: "Reply to all recipients (default: false)" }
297
+ },
298
+ required: ["uid", "text"]
299
+ }
300
+ },
301
+ {
302
+ name: "forward_email",
303
+ description: "Forward an email to another recipient. Outbound guard applies \u2014 HIGH severity content is held for review.",
304
+ inputSchema: {
305
+ type: "object",
306
+ properties: {
307
+ uid: { type: "number", description: "UID of the email to forward" },
308
+ to: { type: "string", description: "Recipient to forward to" },
309
+ text: { type: "string", description: "Additional message (optional)" }
310
+ },
311
+ required: ["uid", "to"]
312
+ }
313
+ },
314
+ {
315
+ name: "move_email",
316
+ description: "Move an email to another folder (e.g., Trash, Archive)",
317
+ inputSchema: {
318
+ type: "object",
319
+ properties: {
320
+ uid: { type: "number", description: "UID of the email to move" },
321
+ to: { type: "string", description: "Destination folder (e.g., Trash, Archive)" },
322
+ from: { type: "string", description: "Source folder (default: INBOX)" }
323
+ },
324
+ required: ["uid", "to"]
325
+ }
326
+ },
327
+ {
328
+ name: "mark_unread",
329
+ description: "Mark an email as unread",
330
+ inputSchema: {
331
+ type: "object",
332
+ properties: {
333
+ uid: { type: "number", description: "UID of the email" }
334
+ },
335
+ required: ["uid"]
336
+ }
337
+ },
338
+ {
339
+ name: "mark_read",
340
+ description: "Mark an email as read",
341
+ inputSchema: {
342
+ type: "object",
343
+ properties: {
344
+ uid: { type: "number", description: "UID of the email" }
345
+ },
346
+ required: ["uid"]
347
+ }
348
+ },
349
+ {
350
+ name: "list_folders",
351
+ description: "List all mail folders/mailboxes",
352
+ inputSchema: {
353
+ type: "object",
354
+ properties: {}
355
+ }
356
+ },
357
+ {
358
+ name: "list_folder",
359
+ description: "List messages in a specific folder",
360
+ inputSchema: {
361
+ type: "object",
362
+ properties: {
363
+ folder: { type: "string", description: "Folder path (e.g., INBOX, Trash, Sent)" },
364
+ limit: { type: "number", description: "Max messages (default: 20)" },
365
+ offset: { type: "number", description: "Skip messages (default: 0)" }
366
+ },
367
+ required: ["folder"]
368
+ }
369
+ },
370
+ {
371
+ name: "batch_delete",
372
+ description: "Delete multiple emails by UIDs",
373
+ inputSchema: {
374
+ type: "object",
375
+ properties: {
376
+ uids: { type: "array", items: { type: "number" }, description: "Array of UIDs to delete" },
377
+ folder: { type: "string", description: "Folder (default: INBOX)" }
378
+ },
379
+ required: ["uids"]
380
+ }
381
+ },
382
+ {
383
+ name: "batch_mark_read",
384
+ description: "Mark multiple emails as read",
385
+ inputSchema: {
386
+ type: "object",
387
+ properties: {
388
+ uids: { type: "array", items: { type: "number" }, description: "Array of UIDs to mark as read" },
389
+ folder: { type: "string", description: "Folder (default: INBOX)" }
390
+ },
391
+ required: ["uids"]
392
+ }
393
+ },
394
+ {
395
+ name: "manage_contacts",
396
+ description: "List, add, or delete contacts",
397
+ inputSchema: {
398
+ type: "object",
399
+ properties: {
400
+ action: { type: "string", enum: ["list", "add", "delete"], description: "Action to perform" },
401
+ email: { type: "string", description: "Contact email (for add)" },
402
+ name: { type: "string", description: "Contact name (for add)" },
403
+ id: { type: "string", description: "Contact ID (for delete)" }
404
+ },
405
+ required: ["action"]
406
+ }
407
+ },
408
+ {
409
+ name: "manage_drafts",
410
+ description: "List, create, update, send, or delete drafts",
411
+ inputSchema: {
412
+ type: "object",
413
+ properties: {
414
+ action: { type: "string", enum: ["list", "create", "update", "send", "delete"], description: "Action to perform" },
415
+ id: { type: "string", description: "Draft ID (for update/send/delete)" },
416
+ to: { type: "string", description: "Recipient (for create/update)" },
417
+ subject: { type: "string", description: "Subject (for create/update)" },
418
+ text: { type: "string", description: "Body text (for create/update)" }
419
+ },
420
+ required: ["action"]
421
+ }
422
+ },
423
+ {
424
+ name: "manage_scheduled",
425
+ description: 'Manage scheduled emails: create a new scheduled email, list pending ones, or cancel one. Accepts flexible time formats for create: ISO 8601, relative ("in 30 minutes"), named ("tomorrow 8am"), day-based ("next monday 9am"), or human-friendly ("02-14-2026 3:30 PM EST").',
426
+ inputSchema: {
427
+ type: "object",
428
+ properties: {
429
+ action: { type: "string", enum: ["create", "list", "cancel"], description: "Action to perform (default: create)" },
430
+ to: { type: "string", description: "Recipient email (for create)" },
431
+ subject: { type: "string", description: "Email subject (for create)" },
432
+ text: { type: "string", description: "Body text (for create)" },
433
+ sendAt: { type: "string", description: "When to send (for create)" },
434
+ id: { type: "string", description: "Scheduled email ID (for cancel)" }
435
+ },
436
+ required: ["action"]
437
+ }
438
+ },
439
+ {
440
+ name: "create_folder",
441
+ description: "Create a new mail folder for organizing emails",
442
+ inputSchema: {
443
+ type: "object",
444
+ properties: {
445
+ name: { type: "string", description: "Folder name (e.g., Projects, Clients, Newsletters)" }
446
+ },
447
+ required: ["name"]
448
+ }
449
+ },
450
+ {
451
+ name: "manage_tags",
452
+ description: "Create, list, delete tags, tag/untag messages, get messages by tag, or get all tags for a specific message.",
453
+ inputSchema: {
454
+ type: "object",
455
+ properties: {
456
+ action: { type: "string", enum: ["list", "create", "delete", "tag_message", "untag_message", "get_messages", "get_message_tags"], description: "Action to perform" },
457
+ name: { type: "string", description: "Tag name (for create)" },
458
+ color: { type: "string", description: "Tag color hex code (for create, e.g. #ff0000)" },
459
+ id: { type: "string", description: "Tag ID (for delete, tag_message, untag_message, get_messages)" },
460
+ uid: { type: "number", description: "Message UID (for tag_message, untag_message)" },
461
+ folder: { type: "string", description: "Folder the message is in (default: INBOX)" }
462
+ },
463
+ required: ["action"]
464
+ }
465
+ },
466
+ {
467
+ name: "create_account",
468
+ description: "Create a new agent email account (requires master API key)",
469
+ inputSchema: {
470
+ type: "object",
471
+ properties: {
472
+ name: { type: "string", description: "Agent name (will be used as email local part)" },
473
+ domain: { type: "string", description: "Email domain (default: localhost)" },
474
+ role: { type: "string", enum: ["secretary", "assistant", "researcher", "writer", "custom"], description: "Agent role (default: secretary)" }
475
+ },
476
+ required: ["name"]
477
+ }
478
+ },
479
+ {
480
+ name: "setup_email_relay",
481
+ description: "Configure Gmail/Outlook relay for sending real internet email (requires master API key). BEGINNER-FRIENDLY: Just needs a Gmail/Outlook email + app password. Agents send as user+agentname@gmail.com. Automatically creates a default agent (secretary) unless skipped. Best for: quick setup, personal use, no domain needed.",
482
+ inputSchema: {
483
+ type: "object",
484
+ properties: {
485
+ provider: { type: "string", enum: ["gmail", "outlook", "custom"], description: "Email provider (gmail, outlook, or custom)" },
486
+ email: { type: "string", description: "Your real email address (e.g., user@gmail.com)" },
487
+ password: { type: "string", description: "App password (not your regular password)" },
488
+ smtpHost: { type: "string", description: "SMTP host (auto-filled for gmail/outlook)" },
489
+ smtpPort: { type: "number", description: "SMTP port (auto-filled for gmail/outlook)" },
490
+ imapHost: { type: "string", description: "IMAP host (auto-filled for gmail/outlook)" },
491
+ imapPort: { type: "number", description: "IMAP port (auto-filled for gmail/outlook)" },
492
+ agentName: { type: "string", description: "Name for the default agent (default: secretary). This becomes the email sub-address, e.g., user+secretary@gmail.com" },
493
+ agentRole: { type: "string", enum: ["secretary", "assistant", "researcher", "writer", "custom"], description: "Role for the default agent (default: secretary)" },
494
+ skipDefaultAgent: { type: "boolean", description: "Skip creating the default agent (default: false)" }
495
+ },
496
+ required: ["provider", "email", "password"]
497
+ }
498
+ },
499
+ {
500
+ name: "setup_email_domain",
501
+ description: "Set up a custom domain for real internet email via Cloudflare (requires master API key). ADVANCED: Requires Cloudflare account, API token, and a domain. Emails send from agent@yourdomain.com with full DKIM/SPF/DMARC. Optionally configures Gmail SMTP as outbound relay (recommended for residential IPs). After setup with gmailRelay, use setup_gmail_alias for each agent.",
502
+ inputSchema: {
503
+ type: "object",
504
+ properties: {
505
+ cloudflareToken: { type: "string", description: "Cloudflare API token (Zone>Zone>Read, Zone>DNS>Edit, Zone>Email Routing Rules>Edit, Account>Cloudflare Tunnel>Edit, Account>Workers Scripts>Edit; optional: Account>Registrar: Domains>Edit for domain purchase)" },
506
+ cloudflareAccountId: { type: "string", description: "Cloudflare account ID" },
507
+ domain: { type: "string", description: "Domain to use (if already owned)" },
508
+ purchase: {
509
+ type: "object",
510
+ properties: {
511
+ keywords: { type: "array", items: { type: "string" }, description: "Keywords to search for available domains" },
512
+ tld: { type: "string", description: "Preferred TLD (e.g., .com, .io)" }
513
+ },
514
+ description: "Purchase a new domain (if domain not provided)"
515
+ },
516
+ gmailRelay: {
517
+ type: "object",
518
+ properties: {
519
+ email: { type: "string", description: "Gmail address for SMTP relay (e.g., you@gmail.com)" },
520
+ appPassword: { type: "string", description: "Gmail app password (from https://myaccount.google.com/apppasswords)" }
521
+ },
522
+ description: "Gmail SMTP relay for outbound delivery (recommended for residential IPs without PTR records)"
523
+ }
524
+ },
525
+ required: ["cloudflareToken", "cloudflareAccountId"]
526
+ }
527
+ },
528
+ {
529
+ name: "setup_guide",
530
+ description: "Get a comparison of email setup modes (Relay vs Domain) with difficulty levels, requirements, pros/cons, and step-by-step instructions. Show this to users who want to set up real internet email.",
531
+ inputSchema: {
532
+ type: "object",
533
+ properties: {}
534
+ }
535
+ },
536
+ {
537
+ name: "setup_gmail_alias",
538
+ description: 'Get step-by-step instructions (with exact field values) to add an agent email as a Gmail "Send mail as" alias. Returns the Gmail settings URL and all field values. Required after domain mode setup with gmailRelay to show correct From address. The agent can automate this via browser tools or present instructions to the user.',
539
+ inputSchema: {
540
+ type: "object",
541
+ properties: {
542
+ agentEmail: { type: "string", description: "Agent email to add as alias (e.g., secretary@yourdomain.com)" },
543
+ agentDisplayName: { type: "string", description: "Display name for the alias (defaults to agent name)" }
544
+ },
545
+ required: ["agentEmail"]
546
+ }
547
+ },
548
+ {
549
+ name: "setup_payment",
550
+ description: "Get instructions for adding a payment method to Cloudflare (required before purchasing domains). Returns Option A (self-service link) and Option B (browser automation steps). Card details go directly to Cloudflare \u2014 never stored by \u{1F380} AgenticMail.",
551
+ inputSchema: {
552
+ type: "object",
553
+ properties: {}
554
+ }
555
+ },
556
+ {
557
+ name: "purchase_domain",
558
+ description: "Search for available domains via Cloudflare Registrar (requires master API key). NOTE: Cloudflare API only supports READ access \u2014 domains must be purchased manually at https://dash.cloudflare.com or from another registrar (then point nameservers to Cloudflare).",
559
+ inputSchema: {
560
+ type: "object",
561
+ properties: {
562
+ keywords: { type: "array", items: { type: "string" }, description: 'Keywords to search for (e.g., ["mybot", "aimail"])' },
563
+ tld: { type: "string", description: "Preferred TLD (default: checks .com, .net, .io, .dev)" }
564
+ },
565
+ required: ["keywords"]
566
+ }
567
+ },
568
+ {
569
+ name: "check_gateway_status",
570
+ description: "Check the current email gateway status \u2014 relay mode, domain mode, or not configured",
571
+ inputSchema: {
572
+ type: "object",
573
+ properties: {}
574
+ }
575
+ },
576
+ {
577
+ name: "send_test_email",
578
+ description: "Send a test email through the gateway to verify configuration (requires master API key)",
579
+ inputSchema: {
580
+ type: "object",
581
+ properties: {
582
+ to: { type: "string", description: "Email address to send the test to" }
583
+ },
584
+ required: ["to"]
585
+ }
586
+ },
587
+ {
588
+ name: "list_agents",
589
+ description: "List all AI agents in the system with their email addresses and roles. Use this to discover which agents you can communicate with via message_agent.",
590
+ inputSchema: {
591
+ type: "object",
592
+ properties: {}
593
+ }
594
+ },
595
+ {
596
+ name: "message_agent",
597
+ description: "Send a message to another AI agent by name. The message is delivered to their email inbox. Use list_agents first to see available agents. This is the primary way for agents to coordinate and share information with each other.",
598
+ inputSchema: {
599
+ type: "object",
600
+ properties: {
601
+ agent: { type: "string", description: 'Name of the recipient agent (e.g. "researcher", "writer")' },
602
+ subject: { type: "string", description: "Message subject \u2014 describe the purpose clearly" },
603
+ text: { type: "string", description: "Message body" },
604
+ priority: { type: "string", enum: ["normal", "high", "urgent"], description: "Priority level (default: normal)" }
605
+ },
606
+ required: ["agent", "subject", "text"]
607
+ }
608
+ },
609
+ {
610
+ name: "check_messages",
611
+ description: "Check for new unread messages from other agents or external senders. Returns a summary of pending communications. Use this to stay aware of requests and coordinate with other agents.",
612
+ inputSchema: {
613
+ type: "object",
614
+ properties: {}
615
+ }
616
+ },
617
+ {
618
+ name: "delete_agent",
619
+ description: "Delete an agent account. Archives all emails and generates a deletion report before removing the account permanently. Returns the deletion summary. Requires master API key.",
620
+ inputSchema: {
621
+ type: "object",
622
+ properties: {
623
+ name: { type: "string", description: "Name of the agent to delete" },
624
+ reason: { type: "string", description: "Reason for deletion (optional)" }
625
+ },
626
+ required: ["name"]
627
+ }
628
+ },
629
+ {
630
+ name: "deletion_reports",
631
+ description: "List past agent deletion reports or retrieve a specific report by ID. Shows archived email summaries from deleted agents. Requires master API key.",
632
+ inputSchema: {
633
+ type: "object",
634
+ properties: {
635
+ id: { type: "string", description: "Deletion report ID (omit to list all reports)" }
636
+ }
637
+ }
638
+ },
639
+ {
640
+ name: "manage_signatures",
641
+ description: "List, create, or delete email signatures",
642
+ inputSchema: {
643
+ type: "object",
644
+ properties: {
645
+ action: { type: "string", enum: ["list", "create", "delete"], description: "Action to perform" },
646
+ id: { type: "string", description: "Signature ID (for delete)" },
647
+ name: { type: "string", description: "Signature name (for create)" },
648
+ text: { type: "string", description: "Signature text content (for create)" },
649
+ isDefault: { type: "boolean", description: "Set as default signature (for create)" }
650
+ },
651
+ required: ["action"]
652
+ }
653
+ },
654
+ {
655
+ name: "manage_templates",
656
+ description: "List, create, or delete email templates",
657
+ inputSchema: {
658
+ type: "object",
659
+ properties: {
660
+ action: { type: "string", enum: ["list", "create", "delete"], description: "Action to perform" },
661
+ id: { type: "string", description: "Template ID (for delete)" },
662
+ name: { type: "string", description: "Template name (for create)" },
663
+ subject: { type: "string", description: "Template subject (for create)" },
664
+ text: { type: "string", description: "Template body text (for create)" }
665
+ },
666
+ required: ["action"]
667
+ }
668
+ },
669
+ {
670
+ name: "batch_mark_unread",
671
+ description: "Mark multiple emails as unread",
672
+ inputSchema: {
673
+ type: "object",
674
+ properties: {
675
+ uids: { type: "array", items: { type: "number" }, description: "Array of UIDs to mark as unread" },
676
+ folder: { type: "string", description: "Folder (default: INBOX)" }
677
+ },
678
+ required: ["uids"]
679
+ }
680
+ },
681
+ {
682
+ name: "batch_move",
683
+ description: "Move multiple emails to another folder",
684
+ inputSchema: {
685
+ type: "object",
686
+ properties: {
687
+ uids: { type: "array", items: { type: "number" }, description: "Array of UIDs to move" },
688
+ from: { type: "string", description: "Source folder (default: INBOX)" },
689
+ to: { type: "string", description: "Destination folder (e.g., Trash, Archive)" }
690
+ },
691
+ required: ["uids", "to"]
692
+ }
693
+ },
694
+ {
695
+ name: "whoami",
696
+ description: "Get the current agent's account info \u2014 name, email, role, and metadata",
697
+ inputSchema: {
698
+ type: "object",
699
+ properties: {}
700
+ }
701
+ },
702
+ {
703
+ name: "update_metadata",
704
+ description: "Update the current agent's metadata. Merges provided keys with existing metadata.",
705
+ inputSchema: {
706
+ type: "object",
707
+ properties: {
708
+ metadata: { type: "object", description: "Metadata key-value pairs to set or update" }
709
+ },
710
+ required: ["metadata"]
711
+ }
712
+ },
713
+ {
714
+ name: "check_health",
715
+ description: "Check \u{1F380} AgenticMail server health status",
716
+ inputSchema: {
717
+ type: "object",
718
+ properties: {}
719
+ }
720
+ },
721
+ {
722
+ name: "wait_for_email",
723
+ description: "Wait for a new email or task notification using push notifications (SSE). Blocks until an email arrives, a task is assigned to you, or timeout is reached. Much more efficient than polling \u2014 use this when waiting for a reply or a task from another agent.",
724
+ inputSchema: {
725
+ type: "object",
726
+ properties: {
727
+ timeout: { type: "number", description: "Max seconds to wait (default: 120, max: 300)" }
728
+ }
729
+ }
730
+ },
731
+ {
732
+ name: "batch_read",
733
+ description: "Read multiple emails at once by UIDs. Returns full parsed content for each message in a single call.",
734
+ inputSchema: {
735
+ type: "object",
736
+ properties: {
737
+ uids: { type: "array", items: { type: "number" }, description: "Array of UIDs to read" },
738
+ folder: { type: "string", description: "Folder (default: INBOX)" }
739
+ },
740
+ required: ["uids"]
741
+ }
742
+ },
743
+ {
744
+ name: "inbox_digest",
745
+ description: "Get a compact inbox digest with subject, sender, date, flags and text preview for each message. More efficient than listing then reading individually.",
746
+ inputSchema: {
747
+ type: "object",
748
+ properties: {
749
+ limit: { type: "number", description: "Max messages (default: 20, max: 50)" },
750
+ offset: { type: "number", description: "Skip messages (default: 0)" },
751
+ folder: { type: "string", description: "Folder (default: INBOX)" },
752
+ previewLength: { type: "number", description: "Preview text length (default: 200, max: 500)" }
753
+ }
754
+ }
755
+ },
756
+ {
757
+ name: "template_send",
758
+ description: "Send an email using a saved template with variable substitution. Variables like {{name}} are replaced.",
759
+ inputSchema: {
760
+ type: "object",
761
+ properties: {
762
+ id: { type: "string", description: "Template ID" },
763
+ to: { type: "string", description: "Recipient email" },
764
+ variables: { type: "object", description: 'Variables to substitute: { name: "Alice" }' },
765
+ cc: { type: "string", description: "CC recipients" },
766
+ bcc: { type: "string", description: "BCC recipients" }
767
+ },
768
+ required: ["id", "to"]
769
+ }
770
+ },
771
+ {
772
+ name: "manage_rules",
773
+ description: "Manage server-side email rules that auto-process incoming messages (move, tag, mark read, delete).",
774
+ inputSchema: {
775
+ type: "object",
776
+ properties: {
777
+ action: { type: "string", enum: ["list", "create", "delete"], description: "Action to perform" },
778
+ id: { type: "string", description: "Rule ID (for delete)" },
779
+ name: { type: "string", description: "Rule name (for create)" },
780
+ priority: { type: "number", description: "Higher priority rules match first (for create)" },
781
+ conditions: { type: "object", description: "Match conditions: { from_contains?, subject_contains?, subject_regex?, to_contains?, has_attachment? }" },
782
+ actions: { type: "object", description: "Actions on match: { move_to?, mark_read?, delete?, add_tags? }" }
783
+ },
784
+ required: ["action"]
785
+ }
786
+ },
787
+ {
788
+ name: "cleanup_agents",
789
+ description: "List or remove inactive non-persistent agent accounts (requires master API key)",
790
+ inputSchema: {
791
+ type: "object",
792
+ properties: {
793
+ action: { type: "string", enum: ["list_inactive", "cleanup", "set_persistent"], description: "Action to perform" },
794
+ hours: { type: "number", description: "Inactivity threshold in hours (default: 24)" },
795
+ dryRun: { type: "boolean", description: "Preview without deleting (for cleanup)" },
796
+ agentId: { type: "string", description: "Agent ID (for set_persistent)" },
797
+ persistent: { type: "boolean", description: "Set persistent flag (for set_persistent)" }
798
+ },
799
+ required: ["action"]
800
+ }
801
+ },
802
+ {
803
+ name: "assign_task",
804
+ description: "Assign a task to another agent via the task queue",
805
+ inputSchema: {
806
+ type: "object",
807
+ properties: {
808
+ assignee: { type: "string", description: "Agent name to assign the task to" },
809
+ taskType: { type: "string", description: "Task category (default: generic)" },
810
+ payload: { type: "object", description: "Task data/instructions" },
811
+ expiresInSeconds: { type: "number", description: "Task expiry in seconds" }
812
+ },
813
+ required: ["assignee"]
814
+ }
815
+ },
816
+ {
817
+ name: "check_tasks",
818
+ description: "Check for pending tasks assigned to you (or a specific agent) or tasks you assigned to others",
819
+ inputSchema: {
820
+ type: "object",
821
+ properties: {
822
+ direction: { type: "string", enum: ["incoming", "outgoing"], description: "incoming (assigned to me) or outgoing (I assigned)" },
823
+ assignee: { type: "string", description: "Check tasks for a specific agent by name (only for incoming direction)" }
824
+ }
825
+ }
826
+ },
827
+ {
828
+ name: "claim_task",
829
+ description: "Claim a pending task assigned to you",
830
+ inputSchema: {
831
+ type: "object",
832
+ properties: {
833
+ id: { type: "string", description: "Task ID to claim" }
834
+ },
835
+ required: ["id"]
836
+ }
837
+ },
838
+ {
839
+ name: "submit_result",
840
+ description: "Submit the result for a claimed task, marking it as completed",
841
+ inputSchema: {
842
+ type: "object",
843
+ properties: {
844
+ id: { type: "string", description: "Task ID" },
845
+ result: { type: "object", description: "Task result data" }
846
+ },
847
+ required: ["id"]
848
+ }
849
+ },
850
+ {
851
+ name: "call_agent",
852
+ description: "Synchronous RPC: assign a task to another agent, notify them via email (wakes wait_for_email), and wait for the result. Times out after specified duration.",
853
+ inputSchema: {
854
+ type: "object",
855
+ properties: {
856
+ target: { type: "string", description: "Name of the agent to call" },
857
+ task: { type: "string", description: "Task description" },
858
+ payload: { type: "object", description: "Additional data" },
859
+ timeout: { type: "number", description: "Max seconds to wait (default: 180, max: 300)" }
860
+ },
861
+ required: ["target", "task"]
862
+ }
863
+ },
864
+ {
865
+ name: "manage_spam",
866
+ description: "Manage spam: list spam folder, report a message as spam, mark as not-spam, or get the spam score of a message. Emails are auto-scored on arrival; high-scoring messages are moved to Spam automatically.",
867
+ inputSchema: {
868
+ type: "object",
869
+ properties: {
870
+ action: { type: "string", enum: ["list", "report", "not_spam", "score"], description: "Action to perform" },
871
+ uid: { type: "number", description: "Message UID (for report, not_spam, score)" },
872
+ folder: { type: "string", description: "Source folder (for report/score, default: INBOX)" },
873
+ limit: { type: "number", description: "Max messages to list (for list, default: 20)" },
874
+ offset: { type: "number", description: "Skip messages (for list, default: 0)" }
875
+ },
876
+ required: ["action"]
877
+ }
878
+ },
879
+ {
880
+ name: "manage_pending_emails",
881
+ description: "Check the status of pending outbound emails blocked by the outbound guard. You can list all your pending emails or get details of a specific one. You CANNOT approve or reject \u2014 only the owner can do that.",
882
+ inputSchema: {
883
+ type: "object",
884
+ properties: {
885
+ action: { type: "string", enum: ["list", "get"], description: "Action to perform (list or get only \u2014 approve/reject require owner)" },
886
+ id: { type: "string", description: "Pending email ID (required for get)" }
887
+ },
888
+ required: ["action"]
889
+ }
890
+ }
891
+ ];
892
+ var MASTER_KEY_TOOLS = /* @__PURE__ */ new Set([
893
+ "create_account",
894
+ "setup_email_relay",
895
+ "setup_email_domain",
896
+ "setup_guide",
897
+ "setup_gmail_alias",
898
+ "setup_payment",
899
+ "purchase_domain",
900
+ "check_gateway_status",
901
+ "send_test_email",
902
+ "delete_agent",
903
+ "deletion_reports",
904
+ "cleanup_agents"
905
+ ]);
906
+ var MCP_EXEC_EXTS = /* @__PURE__ */ new Set([".exe", ".bat", ".cmd", ".ps1", ".sh", ".msi", ".scr", ".com", ".vbs", ".js", ".wsf", ".hta", ".cpl", ".jar", ".app", ".dmg", ".run"]);
907
+ var MCP_ARCHIVE_EXTS = /* @__PURE__ */ new Set([".zip", ".rar", ".7z", ".tar", ".gz", ".bz2", ".xz", ".cab", ".iso"]);
908
+ function mcpBuildSecuritySection(security, attachments) {
909
+ const lines = [];
910
+ if (security?.isSpam) {
911
+ lines.push(`[SPAM] Score: ${security.score}, Category: ${security.topCategory ?? security.category} \u2014 This email was flagged as spam`);
912
+ } else if (security?.isWarning) {
913
+ lines.push(`[WARNING] Score: ${security.score}, Category: ${security.topCategory ?? security.category} \u2014 Treat with caution`);
914
+ }
915
+ if (security?.sanitized && security.sanitizeDetections?.length) {
916
+ lines.push(`Content sanitized: ${security.sanitizeDetections.map((d) => d.type).join(", ")}`);
917
+ }
918
+ if (attachments?.length) {
919
+ for (const att of attachments) {
920
+ const name = att.filename ?? "unknown";
921
+ const lower = name.toLowerCase();
922
+ const ext = lower.includes(".") ? "." + lower.split(".").pop() : "";
923
+ const parts = lower.split(".");
924
+ if (parts.length > 2 && MCP_EXEC_EXTS.has("." + parts[parts.length - 1])) {
925
+ lines.push(` [CRITICAL] "${name}": DOUBLE EXTENSION \u2014 Disguised executable`);
926
+ } else if (MCP_EXEC_EXTS.has(ext)) {
927
+ lines.push(` [HIGH] "${name}": EXECUTABLE file \u2014 DO NOT open or trust`);
928
+ } else if (MCP_ARCHIVE_EXTS.has(ext)) {
929
+ lines.push(` [MEDIUM] "${name}": ARCHIVE \u2014 May contain malware`);
930
+ } else if (ext === ".html" || ext === ".htm") {
931
+ lines.push(` [HIGH] "${name}": HTML file \u2014 May contain phishing/scripts`);
932
+ }
933
+ }
934
+ }
935
+ const matches = security?.spamMatches ?? security?.matches ?? [];
936
+ for (const m of matches) {
937
+ if (m.ruleId === "ph_mismatched_display_url") lines.push(" [!] Mismatched display URL \u2014 PHISHING");
938
+ else if (m.ruleId === "ph_data_uri") lines.push(" [!] data: URI in link \u2014 may execute code");
939
+ else if (m.ruleId === "ph_homograph") lines.push(" [!] Homograph domain \u2014 mimicking legitimate domain");
940
+ else if (m.ruleId === "ph_spoofed_sender") lines.push(" [!] Spoofed brand sender");
941
+ else if (m.ruleId === "de_webhook_exfil") lines.push(" [!] Suspicious webhook URL \u2014 data exfiltration risk");
942
+ else if (m.ruleId === "pi_invisible_unicode") lines.push(" [!] Invisible unicode \u2014 hidden instructions");
943
+ }
944
+ if (lines.length === 0) return "";
945
+ return `
946
+ --- Security ---
947
+ ${lines.join("\n")}`;
948
+ }
949
+ async function handleToolCall(name, args) {
950
+ const useMaster = MASTER_KEY_TOOLS.has(name);
951
+ switch (name) {
952
+ case "send_email": {
953
+ const sendBody = {
954
+ to: args.to,
955
+ subject: args.subject,
956
+ text: args.text ?? "",
957
+ html: args.html,
958
+ cc: args.cc,
959
+ inReplyTo: args.inReplyTo,
960
+ references: args.references
961
+ };
962
+ if (Array.isArray(args.attachments) && args.attachments.length > 0) {
963
+ sendBody.attachments = args.attachments.map((a) => ({
964
+ filename: a.filename,
965
+ content: a.content,
966
+ contentType: a.contentType,
967
+ ...a.encoding ? { encoding: a.encoding } : {}
968
+ }));
969
+ }
970
+ const result = await apiRequest("POST", "/mail/send", sendBody);
971
+ if (result?.blocked && result?.pendingId) {
972
+ scheduleFollowUp(result.pendingId, String(args.to), String(args.subject || "(no subject)"), makePendingCheck(result.pendingId));
973
+ return `Email NOT sent \u2014 blocked by outbound guard.
974
+ ${result.summary}
975
+
976
+ Pending ID: ${result.pendingId}
977
+ Your owner has been notified via email with the full content for review.
978
+
979
+ You MUST now:
980
+ 1. Inform your owner in this conversation that the email was blocked and needs their approval.
981
+ 2. Mention the recipient, subject, and why it was flagged.
982
+ 3. If this email is urgent or has a deadline, tell your owner about the time sensitivity.
983
+ 4. Periodically check with manage_pending_emails(action='list') and follow up with your owner if still pending.`;
984
+ }
985
+ let response = `Email sent successfully. Message ID: ${result?.messageId ?? "unknown"}`;
986
+ if (result?.outboundWarnings?.length) {
987
+ response += `
988
+
989
+ --- Outbound Guard ---
990
+ [WARNING] ${result.outboundWarnings.length} potential issue(s):
991
+ ${result.outboundWarnings.map((w) => ` [${w.severity?.toUpperCase()}] ${w.description}: ${w.match}`).join("\n")}`;
992
+ }
993
+ return response;
994
+ }
995
+ case "list_inbox": {
996
+ const limit = Math.min(Math.max(Number(args.limit) || 20, 1), 100);
997
+ const offset = Math.max(Number(args.offset) || 0, 0);
998
+ const result = await apiRequest("GET", `/mail/inbox?limit=${limit}&offset=${offset}`);
999
+ if (!result?.messages?.length) {
1000
+ return "Inbox is empty.";
1001
+ }
1002
+ const lines = result.messages.map(
1003
+ (m, i) => `${i + 1}. [UID:${m.uid}] From: ${m.from?.[0]?.address ?? "unknown"} | Subject: ${m.subject} | Date: ${m.date}`
1004
+ );
1005
+ return `Inbox (${result.messages.length} messages):
1006
+ ${lines.join("\n")}`;
1007
+ }
1008
+ case "read_email": {
1009
+ const uid = Number(args.uid);
1010
+ if (!uid || uid < 1 || !Number.isInteger(uid)) {
1011
+ throw new Error("uid must be a positive integer");
1012
+ }
1013
+ const result = await apiRequest("GET", `/mail/messages/${uid}`);
1014
+ if (!result) throw new Error("Email not found or empty response");
1015
+ const lines = [
1016
+ `From: ${result.from?.map((a) => a.address).join(", ") ?? "unknown"}`,
1017
+ `To: ${result.to?.map((a) => a.address).join(", ") ?? "unknown"}`,
1018
+ `Subject: ${result.subject}`,
1019
+ `Date: ${result.date}`,
1020
+ `Message-ID: ${result.messageId}`,
1021
+ result.inReplyTo ? `In-Reply-To: ${result.inReplyTo}` : null,
1022
+ "---",
1023
+ result.text ?? result.html ?? "(no body)"
1024
+ ];
1025
+ if (result.attachments?.length) {
1026
+ lines.push("---");
1027
+ lines.push(`Attachments (${result.attachments.length}):`);
1028
+ for (const att of result.attachments) {
1029
+ lines.push(` - ${att.filename} (${att.contentType}, ${Math.round(att.size / 1024)}KB)`);
1030
+ }
1031
+ }
1032
+ const secSection = mcpBuildSecuritySection(result.security, result.attachments);
1033
+ if (secSection) lines.push(secSection);
1034
+ return lines.filter((line) => line !== null).join("\n");
1035
+ }
1036
+ case "delete_email": {
1037
+ const uid = Number(args.uid);
1038
+ if (!uid || uid < 1 || !Number.isInteger(uid)) {
1039
+ throw new Error("uid must be a positive integer");
1040
+ }
1041
+ await apiRequest("DELETE", `/mail/messages/${uid}`);
1042
+ return `Email UID ${uid} deleted successfully.`;
1043
+ }
1044
+ case "search_emails": {
1045
+ const { from, to, subject, text, since, before, seen, searchRelay } = args;
1046
+ const result = await apiRequest("POST", "/mail/search", { from, to, subject, text, since, before, seen, searchRelay });
1047
+ const lines = [];
1048
+ if (result?.uids?.length) {
1049
+ lines.push(`Local inbox: ${result.uids.length} match${result.uids.length !== 1 ? "es" : ""}. UIDs: ${result.uids.join(", ")}`);
1050
+ }
1051
+ if (result?.relayResults?.length) {
1052
+ lines.push(`
1053
+ Connected account (${result.relayResults[0].account}): ${result.relayResults.length} match${result.relayResults.length !== 1 ? "es" : ""}`);
1054
+ for (const r of result.relayResults.slice(0, 20)) {
1055
+ const fromAddr = r.from?.[0]?.address ?? "unknown";
1056
+ const date = r.date ? new Date(r.date).toLocaleDateString() : "";
1057
+ lines.push(` [relay UID:${r.uid}] From: ${fromAddr} | Subject: ${r.subject} | Date: ${date}`);
1058
+ }
1059
+ lines.push("\nTo continue a thread from the connected account, use import_relay_email with the relay UID, then reply_email as normal.");
1060
+ }
1061
+ if (lines.length === 0) {
1062
+ return searchRelay ? "No matching emails found in local inbox or connected account." : "No matching emails found. Tip: set searchRelay=true to also search your connected Gmail/Outlook account.";
1063
+ }
1064
+ return lines.join("\n");
1065
+ }
1066
+ case "import_relay_email": {
1067
+ const uid = Number(args.uid);
1068
+ if (!uid || uid < 1 || !Number.isInteger(uid)) {
1069
+ throw new Error("uid must be a positive integer (relay UID from search results)");
1070
+ }
1071
+ const result = await apiRequest("POST", "/mail/import-relay", { uid });
1072
+ return result?.ok ? "Email imported to local inbox. You can now use list_inbox to find it and reply_email to continue the thread." : `Import failed: ${result?.error || "unknown error"}`;
1073
+ }
1074
+ case "reply_email": {
1075
+ const uid = Number(args.uid);
1076
+ if (!uid || uid < 1) throw new Error("uid must be a positive integer");
1077
+ const original = await apiRequest("GET", `/mail/messages/${uid}`);
1078
+ if (!original) throw new Error("Original email not found");
1079
+ const replyTo = original.replyTo?.[0]?.address || original.from?.[0]?.address;
1080
+ if (!replyTo) throw new Error("Original email has no sender address \u2014 cannot reply");
1081
+ const origSubject = original.subject ?? "";
1082
+ const subject = origSubject.startsWith("Re:") ? origSubject : `Re: ${origSubject}`;
1083
+ const refs = Array.isArray(original.references) ? [...original.references] : [];
1084
+ if (original.messageId) refs.push(original.messageId);
1085
+ const quotedBody = (original.text || "").split("\n").map((l) => `> ${l}`).join("\n");
1086
+ const fullText = `${args.text}
1087
+
1088
+ On ${original.date}, ${replyTo} wrote:
1089
+ ${quotedBody}`;
1090
+ let to = replyTo;
1091
+ if (args.replyAll) {
1092
+ const allTo = [...original.to || [], ...original.cc || []].map((a) => a.address).filter(Boolean);
1093
+ to = [replyTo, ...allTo].filter((v, i, a) => v && a.indexOf(v) === i).join(", ");
1094
+ }
1095
+ const replySendBody = {
1096
+ to,
1097
+ subject,
1098
+ text: fullText,
1099
+ html: args.html,
1100
+ inReplyTo: original.messageId,
1101
+ references: refs
1102
+ };
1103
+ const sendResult = await apiRequest("POST", "/mail/send", replySendBody);
1104
+ if (sendResult?.blocked && sendResult?.pendingId) {
1105
+ scheduleFollowUp(sendResult.pendingId, to, String(replySendBody.subject || "(no subject)"), makePendingCheck(sendResult.pendingId));
1106
+ return `Reply NOT sent \u2014 blocked by outbound guard.
1107
+ ${sendResult.summary}
1108
+
1109
+ Pending ID: ${sendResult.pendingId}
1110
+ Your owner has been notified via email with the full content for review.
1111
+
1112
+ You MUST now:
1113
+ 1. Inform your owner in this conversation that the reply was blocked and needs their approval.
1114
+ 2. Mention the recipient, subject, and why it was flagged.
1115
+ 3. If this reply is urgent or has a deadline, tell your owner about the time sensitivity.
1116
+ 4. Periodically check with manage_pending_emails(action='list') and follow up with your owner if still pending.`;
1117
+ }
1118
+ let response = `Reply sent to ${to}. Message ID: ${sendResult?.messageId ?? "unknown"}`;
1119
+ if (sendResult?.outboundWarnings?.length) {
1120
+ response += `
1121
+
1122
+ --- Outbound Guard ---
1123
+ ${sendResult.outboundWarnings.map((w) => ` [${w.severity?.toUpperCase()}] ${w.description}`).join("\n")}`;
1124
+ }
1125
+ return response;
1126
+ }
1127
+ case "forward_email": {
1128
+ const uid = Number(args.uid);
1129
+ if (!uid || uid < 1) throw new Error("uid must be a positive integer");
1130
+ const orig = await apiRequest("GET", `/mail/messages/${uid}`);
1131
+ if (!orig) throw new Error("Email not found");
1132
+ const fwdSubject = (orig.subject ?? "").startsWith("Fwd:") ? orig.subject : `Fwd: ${orig.subject}`;
1133
+ const origFrom = orig.from?.[0]?.address ?? "unknown";
1134
+ const origTo = (orig.to || []).map((a) => a.address).join(", ");
1135
+ const fwdBody = `${args.text ? args.text + "\n\n" : ""}---------- Forwarded message ----------
1136
+ From: ${origFrom}
1137
+ To: ${origTo}
1138
+ Date: ${orig.date}
1139
+ Subject: ${orig.subject}
1140
+
1141
+ ${orig.text || ""}`;
1142
+ const fwdSendBody = { to: args.to, subject: fwdSubject, text: fwdBody };
1143
+ if (Array.isArray(orig.attachments) && orig.attachments.length > 0) {
1144
+ fwdSendBody.attachments = orig.attachments.map((a) => ({
1145
+ filename: a.filename,
1146
+ content: a.content?.data ? Buffer.from(a.content.data).toString("base64") : a.content,
1147
+ contentType: a.contentType,
1148
+ encoding: "base64"
1149
+ }));
1150
+ }
1151
+ const fwdResult = await apiRequest("POST", "/mail/send", fwdSendBody);
1152
+ if (fwdResult?.blocked && fwdResult?.pendingId) {
1153
+ scheduleFollowUp(fwdResult.pendingId, String(args.to), fwdSubject, makePendingCheck(fwdResult.pendingId));
1154
+ return `Forward NOT sent \u2014 blocked by outbound guard.
1155
+ ${fwdResult.summary}
1156
+
1157
+ Pending ID: ${fwdResult.pendingId}
1158
+ Your owner has been notified via email with the full content for review.
1159
+
1160
+ You MUST now:
1161
+ 1. Inform your owner in this conversation that the forward was blocked and needs their approval.
1162
+ 2. Mention the recipient, subject, and why it was flagged.
1163
+ 3. If this forward is urgent or has a deadline, tell your owner about the time sensitivity.
1164
+ 4. Periodically check with manage_pending_emails(action='list') and follow up with your owner if still pending.`;
1165
+ }
1166
+ let response = `Forwarded to ${args.to}. Message ID: ${fwdResult?.messageId ?? "unknown"}`;
1167
+ if (fwdResult?.outboundWarnings?.length) {
1168
+ response += `
1169
+
1170
+ --- Outbound Guard ---
1171
+ ${fwdResult.outboundWarnings.map((w) => ` [${w.severity?.toUpperCase()}] ${w.description}`).join("\n")}`;
1172
+ }
1173
+ return response;
1174
+ }
1175
+ case "move_email": {
1176
+ const uid = Number(args.uid);
1177
+ if (!uid || uid < 1) throw new Error("uid must be a positive integer");
1178
+ await apiRequest("POST", `/mail/messages/${uid}/move`, { from: args.from || "INBOX", to: args.to });
1179
+ return `Moved message UID ${uid} to ${args.to}`;
1180
+ }
1181
+ case "mark_unread": {
1182
+ const uid = Number(args.uid);
1183
+ if (!uid || uid < 1) throw new Error("uid must be a positive integer");
1184
+ await apiRequest("POST", `/mail/messages/${uid}/unseen`);
1185
+ return `Marked message UID ${uid} as unread`;
1186
+ }
1187
+ case "mark_read": {
1188
+ const uid = Number(args.uid);
1189
+ if (!uid || uid < 1) throw new Error("uid must be a positive integer");
1190
+ await apiRequest("POST", `/mail/messages/${uid}/seen`);
1191
+ return `Marked message UID ${uid} as read`;
1192
+ }
1193
+ case "list_folders": {
1194
+ const result = await apiRequest("GET", "/mail/folders");
1195
+ if (!result?.folders?.length) return "No folders found.";
1196
+ return result.folders.map((f) => `${f.path}${f.specialUse ? ` (${f.specialUse})` : ""}`).join("\n");
1197
+ }
1198
+ case "list_folder": {
1199
+ const folder = encodeURIComponent(String(args.folder));
1200
+ const limit = Math.min(Math.max(Number(args.limit) || 20, 1), 100);
1201
+ const offset = Math.max(Number(args.offset) || 0, 0);
1202
+ const result = await apiRequest("GET", `/mail/folders/${folder}?limit=${limit}&offset=${offset}`);
1203
+ if (!result?.messages?.length) return `Folder "${args.folder}" is empty.`;
1204
+ const lines = result.messages.map((m, i) => `${i + 1}. [UID:${m.uid}] From: ${m.from?.[0]?.address ?? "unknown"} | Subject: ${m.subject} | Date: ${m.date}`);
1205
+ return `${args.folder} (${result.total} total):
1206
+ ${lines.join("\n")}`;
1207
+ }
1208
+ case "batch_delete": {
1209
+ const uids = args.uids;
1210
+ if (!Array.isArray(uids) || uids.length === 0) throw new Error("uids array required");
1211
+ await apiRequest("POST", "/mail/batch/delete", { uids, folder: args.folder });
1212
+ return `Deleted ${uids.length} messages.`;
1213
+ }
1214
+ case "batch_mark_read": {
1215
+ const uids = args.uids;
1216
+ if (!Array.isArray(uids) || uids.length === 0) throw new Error("uids array required");
1217
+ await apiRequest("POST", "/mail/batch/seen", { uids, folder: args.folder });
1218
+ return `Marked ${uids.length} messages as read.`;
1219
+ }
1220
+ case "manage_contacts": {
1221
+ if (args.action === "list") {
1222
+ const r = await apiRequest("GET", "/contacts");
1223
+ if (!r?.contacts?.length) return "No contacts.";
1224
+ return r.contacts.map((c) => `${c.name || "(no name)"} <${c.email}>`).join("\n");
1225
+ }
1226
+ if (args.action === "add") {
1227
+ if (!args.email) throw new Error("email is required");
1228
+ await apiRequest("POST", "/contacts", { email: args.email, name: args.name });
1229
+ return `Contact added: ${args.name || ""} <${args.email}>`;
1230
+ }
1231
+ if (args.action === "delete") {
1232
+ if (!args.id) throw new Error("id is required");
1233
+ await apiRequest("DELETE", `/contacts/${args.id}`);
1234
+ return "Contact deleted.";
1235
+ }
1236
+ throw new Error("Invalid action");
1237
+ }
1238
+ case "manage_drafts": {
1239
+ if (args.action === "list") {
1240
+ const r = await apiRequest("GET", "/drafts");
1241
+ if (!r?.drafts?.length) return "No drafts.";
1242
+ return r.drafts.map((d) => `[${d.id}] To: ${d.to_addr || "?"} | Subject: ${d.subject || "?"}`).join("\n");
1243
+ }
1244
+ if (args.action === "create") {
1245
+ const r = await apiRequest("POST", "/drafts", { to: args.to, subject: args.subject, text: args.text });
1246
+ return `Draft created: ${r?.id}`;
1247
+ }
1248
+ if (args.action === "update") {
1249
+ if (!args.id) throw new Error("id is required");
1250
+ await apiRequest("PUT", `/drafts/${args.id}`, { to: args.to, subject: args.subject, text: args.text });
1251
+ return `Draft ${args.id} updated.`;
1252
+ }
1253
+ if (args.action === "send") {
1254
+ if (!args.id) throw new Error("id is required");
1255
+ const r = await apiRequest("POST", `/drafts/${args.id}/send`);
1256
+ return `Draft sent. Message ID: ${r?.messageId ?? "unknown"}`;
1257
+ }
1258
+ if (args.action === "delete") {
1259
+ if (!args.id) throw new Error("id is required");
1260
+ await apiRequest("DELETE", `/drafts/${args.id}`);
1261
+ return "Draft deleted.";
1262
+ }
1263
+ throw new Error("Invalid action");
1264
+ }
1265
+ case "manage_scheduled": {
1266
+ const action = args.action || "create";
1267
+ if (action === "list") {
1268
+ const r2 = await apiRequest("GET", "/scheduled");
1269
+ if (!r2?.scheduled?.length) return "No scheduled emails.";
1270
+ return r2.scheduled.map(
1271
+ (s) => `[${s.id}] To: ${s.to_addr} | Subject: ${s.subject} | Send at: ${s.send_at} | Status: ${s.status}`
1272
+ ).join("\n");
1273
+ }
1274
+ if (action === "cancel") {
1275
+ if (!args.id) throw new Error("id is required");
1276
+ await apiRequest("DELETE", `/scheduled/${args.id}`);
1277
+ return "Scheduled email cancelled.";
1278
+ }
1279
+ const r = await apiRequest("POST", "/scheduled", {
1280
+ to: args.to,
1281
+ subject: args.subject,
1282
+ text: args.text,
1283
+ sendAt: args.sendAt
1284
+ });
1285
+ return `Email scheduled for ${r?.sendAt}. ID: ${r?.id}`;
1286
+ }
1287
+ case "create_folder": {
1288
+ if (!args.name) throw new Error("name is required");
1289
+ await apiRequest("POST", "/mail/folders", { name: args.name });
1290
+ return `Folder "${args.name}" created successfully.`;
1291
+ }
1292
+ case "manage_tags": {
1293
+ const action = args.action;
1294
+ if (action === "list") {
1295
+ const r = await apiRequest("GET", "/tags");
1296
+ if (!r?.tags?.length) return "No tags.";
1297
+ return r.tags.map((t) => `[${t.id.slice(0, 8)}] ${t.name} (${t.color})`).join("\n");
1298
+ }
1299
+ if (action === "create") {
1300
+ if (!args.name) throw new Error("name is required");
1301
+ const r = await apiRequest("POST", "/tags", { name: args.name, color: args.color });
1302
+ return `Tag "${args.name}" created (${r?.color}). ID: ${r?.id}`;
1303
+ }
1304
+ if (action === "delete") {
1305
+ if (!args.id) throw new Error("id is required");
1306
+ await apiRequest("DELETE", `/tags/${args.id}`);
1307
+ return "Tag deleted.";
1308
+ }
1309
+ if (action === "tag_message") {
1310
+ if (!args.id || !args.uid) throw new Error("id and uid are required");
1311
+ await apiRequest("POST", `/tags/${args.id}/messages`, { uid: args.uid, folder: args.folder });
1312
+ return `Tagged message UID ${args.uid} with tag ${args.id}`;
1313
+ }
1314
+ if (action === "untag_message") {
1315
+ if (!args.id || !args.uid) throw new Error("id and uid are required");
1316
+ const folder = args.folder || "INBOX";
1317
+ await apiRequest("DELETE", `/tags/${args.id}/messages/${args.uid}?folder=${encodeURIComponent(folder)}`);
1318
+ return `Removed tag from message UID ${args.uid} in ${folder}`;
1319
+ }
1320
+ if (action === "get_messages") {
1321
+ if (!args.id) throw new Error("id is required");
1322
+ const r = await apiRequest("GET", `/tags/${args.id}/messages`);
1323
+ if (!r?.messages?.length) return `No messages with this tag.`;
1324
+ return `Tag "${r.tag.name}" \u2014 ${r.messages.length} messages:
1325
+ ${r.messages.map((m) => ` UID ${m.uid} (${m.folder})`).join("\n")}`;
1326
+ }
1327
+ if (action === "get_message_tags") {
1328
+ if (!args.uid) throw new Error("uid is required");
1329
+ const r = await apiRequest("GET", `/messages/${args.uid}/tags`);
1330
+ if (!r?.tags?.length) return "No tags on this message.";
1331
+ return r.tags.map((t) => `[${t.id.slice(0, 8)}] ${t.name} (${t.color})`).join("\n");
1332
+ }
1333
+ throw new Error("Invalid action");
1334
+ }
1335
+ case "create_account": {
1336
+ const result = await apiRequest("POST", "/accounts", {
1337
+ name: args.name,
1338
+ domain: args.domain,
1339
+ role: args.role
1340
+ }, useMaster);
1341
+ if (!result) throw new Error("No response from account creation");
1342
+ return [
1343
+ `Account created successfully!`,
1344
+ ` Name: ${result.name}`,
1345
+ ` Email: ${result.email}`,
1346
+ ` Role: ${result.role}`,
1347
+ ` API Key: ${result.apiKey}`,
1348
+ ` ID: ${result.id}`
1349
+ ].join("\n");
1350
+ }
1351
+ case "setup_email_relay": {
1352
+ const result = await apiRequest("POST", "/gateway/relay", {
1353
+ provider: args.provider,
1354
+ email: args.email,
1355
+ password: args.password,
1356
+ smtpHost: args.smtpHost,
1357
+ smtpPort: args.smtpPort,
1358
+ imapHost: args.imapHost,
1359
+ imapPort: args.imapPort,
1360
+ agentName: args.agentName,
1361
+ agentRole: args.agentRole,
1362
+ skipDefaultAgent: args.skipDefaultAgent
1363
+ }, useMaster);
1364
+ if (!result) throw new Error("No response from relay setup");
1365
+ const lines = [
1366
+ `Email relay configured!`,
1367
+ ` Mode: ${result.mode}`,
1368
+ ` Provider: ${result.provider}`,
1369
+ ` Email: ${result.email}`
1370
+ ];
1371
+ if (result.agent) {
1372
+ lines.push(
1373
+ ` Default agent: ${result.agent.name} (${result.agent.role})`,
1374
+ ` Agent email: ${result.agent.subAddress}`,
1375
+ ` Agent API Key: ${result.agent.apiKey}`
1376
+ );
1377
+ }
1378
+ return lines.join("\n");
1379
+ }
1380
+ case "setup_email_domain": {
1381
+ const result = await apiRequest("POST", "/gateway/domain", {
1382
+ cloudflareToken: args.cloudflareToken,
1383
+ cloudflareAccountId: args.cloudflareAccountId,
1384
+ domain: args.domain,
1385
+ purchase: args.purchase,
1386
+ gmailRelay: args.gmailRelay
1387
+ }, useMaster);
1388
+ if (!result) throw new Error("No response from domain setup");
1389
+ const lines = [
1390
+ `Domain email configured!`,
1391
+ ` Domain: ${result.domain}`,
1392
+ ` DNS: ${result.dnsConfigured ? "configured" : "pending"}`,
1393
+ ` Tunnel: ${result.tunnelId}`
1394
+ ];
1395
+ if (result.outboundRelay) {
1396
+ lines.push(` Outbound relay: ${result.outboundRelay.configured ? "configured" : "failed"} (${result.outboundRelay.provider})`);
1397
+ }
1398
+ if (result.nextSteps?.length) {
1399
+ lines.push("", "Next steps:");
1400
+ for (const step of result.nextSteps) {
1401
+ lines.push(` ${step}`);
1402
+ }
1403
+ }
1404
+ return lines.join("\n");
1405
+ }
1406
+ case "setup_guide": {
1407
+ const result = await apiRequest("GET", "/gateway/setup-guide", void 0, useMaster);
1408
+ if (!result?.modes) throw new Error("No response from setup guide");
1409
+ const lines = ["Email Setup Options:", ""];
1410
+ for (const mode of result.modes) {
1411
+ lines.push(`=== ${mode.mode.toUpperCase()} MODE (${mode.difficulty}) ===`);
1412
+ lines.push(mode.description);
1413
+ lines.push(`From address: ${mode.fromAddress}`);
1414
+ lines.push("Requirements:");
1415
+ for (const req of mode.requirements) lines.push(` - ${req}`);
1416
+ lines.push("Pros:");
1417
+ for (const pro of mode.pros) lines.push(` + ${pro}`);
1418
+ lines.push("Cons:");
1419
+ for (const con of mode.cons) lines.push(` - ${con}`);
1420
+ lines.push("");
1421
+ }
1422
+ return lines.join("\n");
1423
+ }
1424
+ case "setup_gmail_alias": {
1425
+ const result = await apiRequest("POST", "/gateway/domain/alias-setup", {
1426
+ agentEmail: args.agentEmail,
1427
+ agentDisplayName: args.agentDisplayName
1428
+ }, useMaster);
1429
+ if (!result?.instructions) throw new Error("No response from alias setup");
1430
+ const lines = [
1431
+ result.instructions.summary,
1432
+ "",
1433
+ "Steps:"
1434
+ ];
1435
+ for (const step of result.instructions.steps) {
1436
+ lines.push(`${step.step}. ${step.action}`);
1437
+ if (step.fields) {
1438
+ for (const [k, v] of Object.entries(step.fields)) {
1439
+ lines.push(` ${k}: ${v}`);
1440
+ }
1441
+ }
1442
+ if (step.url) lines.push(` URL: ${step.url}`);
1443
+ }
1444
+ return lines.join("\n");
1445
+ }
1446
+ case "setup_payment": {
1447
+ const result = await apiRequest("GET", "/gateway/domain/payment-setup", void 0, useMaster);
1448
+ if (!result?.instructions) throw new Error("No response from payment setup");
1449
+ const lines = [result.instructions.summary, ""];
1450
+ for (const opt of result.instructions.options) {
1451
+ lines.push(`=== Option ${opt.option}: ${opt.label} ===`);
1452
+ if (opt.securityNote) lines.push(`Security: ${opt.securityNote}`);
1453
+ for (const step of opt.steps) {
1454
+ lines.push(` ${step.step}. ${step.action}`);
1455
+ if (step.url) lines.push(` URL: ${step.url}`);
1456
+ if (step.note) lines.push(` Note: ${step.note}`);
1457
+ }
1458
+ lines.push("");
1459
+ }
1460
+ return lines.join("\n");
1461
+ }
1462
+ case "purchase_domain": {
1463
+ const result = await apiRequest("POST", "/gateway/domain/purchase", {
1464
+ keywords: args.keywords,
1465
+ tld: args.tld
1466
+ }, useMaster);
1467
+ if (!result?.domains?.length) return "No domains found.";
1468
+ const lines = result.domains.map(
1469
+ (d) => ` ${d.domain}: ${d.available ? "available" : "taken"}${d.price ? ` ($${d.price})` : ""}${d.premium ? " (premium)" : ""}`
1470
+ );
1471
+ return `Domain search results:
1472
+ ${lines.join("\n")}`;
1473
+ }
1474
+ case "check_gateway_status": {
1475
+ const result = await apiRequest("GET", "/gateway/status", void 0, useMaster);
1476
+ if (!result) throw new Error("No response from gateway status");
1477
+ const lines = [`Gateway mode: ${result.mode}`, `Healthy: ${result.healthy}`];
1478
+ if (result.relay) {
1479
+ lines.push(`Relay provider: ${result.relay.provider}`, `Relay email: ${result.relay.email}`, `Polling: ${result.relay.polling}`);
1480
+ }
1481
+ if (result.domain) {
1482
+ lines.push(`Domain: ${result.domain.domain}`, `DNS: ${result.domain.dnsConfigured}`, `Tunnel: ${result.domain.tunnelActive}`);
1483
+ }
1484
+ return lines.join("\n");
1485
+ }
1486
+ case "send_test_email": {
1487
+ const result = await apiRequest("POST", "/gateway/test", { to: args.to }, useMaster);
1488
+ return `Test email sent! Message ID: ${result?.messageId ?? "unknown"}`;
1489
+ }
1490
+ case "list_agents": {
1491
+ const result = await apiRequest("GET", "/accounts/directory");
1492
+ const agents = result?.agents ?? [];
1493
+ if (agents.length === 0) return "No agents found.";
1494
+ const lines = agents.map((a) => ` ${a.name} (${a.email}) \u2014 ${a.role}`);
1495
+ return `Agents in the system:
1496
+ ${lines.join("\n")}`;
1497
+ }
1498
+ case "message_agent": {
1499
+ const agentName = String(args.agent ?? "").toLowerCase().trim();
1500
+ if (!agentName) throw new Error("agent name is required");
1501
+ try {
1502
+ await apiRequest("GET", `/accounts/directory/${encodeURIComponent(agentName)}`);
1503
+ } catch {
1504
+ throw new Error(`Agent "${agentName}" not found. Use list_agents to see available agents.`);
1505
+ }
1506
+ const to = `${agentName}@localhost`;
1507
+ const priority = String(args.priority ?? "normal");
1508
+ const subject = priority === "urgent" ? `[URGENT] ${args.subject}` : priority === "high" ? `[HIGH] ${args.subject}` : String(args.subject);
1509
+ const result = await apiRequest("POST", "/mail/send", { to, subject, text: args.text });
1510
+ return `Message sent to ${to}. Message ID: ${result?.messageId ?? "unknown"}`;
1511
+ }
1512
+ case "check_messages": {
1513
+ const searchResult = await apiRequest("POST", "/mail/search", { seen: false });
1514
+ const uids = searchResult?.uids ?? [];
1515
+ if (uids.length === 0) return "No unread messages.";
1516
+ const details = [];
1517
+ for (const uid of uids.slice(0, 10)) {
1518
+ try {
1519
+ const email = await apiRequest("GET", `/mail/messages/${uid}`);
1520
+ if (!email) continue;
1521
+ const from = email.from?.[0]?.address ?? "unknown";
1522
+ const subject = email.subject ?? "(no subject)";
1523
+ const tag = from.endsWith("@localhost") ? "[agent]" : "[external]";
1524
+ details.push(` ${tag} UID ${uid}: from ${from} \u2014 "${subject}"`);
1525
+ } catch {
1526
+ }
1527
+ }
1528
+ const more = uids.length > 10 ? `
1529
+ (${uids.length - 10} more not shown)` : "";
1530
+ return `${uids.length} unread message(s):
1531
+ ${details.join("\n")}${more}`;
1532
+ }
1533
+ case "delete_agent": {
1534
+ const agentName = String(args.name ?? "").trim();
1535
+ if (!agentName) throw new Error("name is required");
1536
+ const agents = await apiRequest("GET", "/accounts", void 0, true);
1537
+ const fullAgent = agents?.agents?.find((a) => a.name === agentName);
1538
+ if (!fullAgent) throw new Error(`Agent "${agentName}" not found`);
1539
+ const qs = new URLSearchParams({ archive: "true", deletedBy: "mcp-tool" });
1540
+ if (args.reason) qs.set("reason", String(args.reason));
1541
+ const report = await apiRequest("DELETE", `/accounts/${fullAgent.id}?${qs.toString()}`, void 0, true);
1542
+ const lines = [
1543
+ `Agent "${agentName}" deleted successfully.`,
1544
+ ` Deletion ID: ${report?.id}`,
1545
+ ` Emails archived: ${report?.summary?.totalEmails ?? 0}`,
1546
+ ` Deleted at: ${report?.deletedAt}`
1547
+ ];
1548
+ if (report?.summary?.topCorrespondents?.length) {
1549
+ lines.push(` Top correspondents: ${report.summary.topCorrespondents.map((c) => c.address).join(", ")}`);
1550
+ }
1551
+ return lines.join("\n");
1552
+ }
1553
+ case "deletion_reports": {
1554
+ if (args.id) {
1555
+ const report = await apiRequest("GET", `/accounts/deletions/${encodeURIComponent(String(args.id))}`, void 0, true);
1556
+ if (!report) throw new Error("Deletion report not found");
1557
+ const lines2 = [
1558
+ `Deletion Report: ${report.id}`,
1559
+ ` Agent: ${report.agent.name} (${report.agent.email})`,
1560
+ ` Role: ${report.agent.role}`,
1561
+ ` Deleted: ${report.deletedAt}`,
1562
+ ` By: ${report.deletedBy}`,
1563
+ report.reason ? ` Reason: ${report.reason}` : null,
1564
+ ` Total emails: ${report.summary.totalEmails}`,
1565
+ ` Inbox: ${report.summary.inboxCount}, Sent: ${report.summary.sentCount}, Other: ${report.summary.otherCount}`
1566
+ ];
1567
+ if (report.summary.firstEmailDate) {
1568
+ lines2.push(` Date range: ${report.summary.firstEmailDate} \u2192 ${report.summary.lastEmailDate}`);
1569
+ }
1570
+ if (report.summary.topCorrespondents?.length) {
1571
+ lines2.push(` Top correspondents: ${report.summary.topCorrespondents.map((c) => `${c.address} (${c.count})`).join(", ")}`);
1572
+ }
1573
+ return lines2.filter(Boolean).join("\n");
1574
+ }
1575
+ const result = await apiRequest("GET", "/accounts/deletions", void 0, true);
1576
+ const deletions = result?.deletions ?? [];
1577
+ if (deletions.length === 0) return "No deletion reports found.";
1578
+ const lines = deletions.map((d) => ` [${d.id}] ${d.agentName} (${d.agentEmail}) \u2014 deleted ${d.deletedAt}, ${d.emailCount} emails archived`);
1579
+ return `Deletion reports:
1580
+ ${lines.join("\n")}`;
1581
+ }
1582
+ case "manage_signatures": {
1583
+ if (args.action === "list") {
1584
+ const r = await apiRequest("GET", "/signatures");
1585
+ if (!r?.signatures?.length) return "No signatures.";
1586
+ return r.signatures.map((s) => `[${s.id}] ${s.name}${s.isDefault ? " (default)" : ""}: ${s.text}`).join("\n");
1587
+ }
1588
+ if (args.action === "create") {
1589
+ if (!args.name || !args.text) throw new Error("name and text are required");
1590
+ const r = await apiRequest("POST", "/signatures", { name: args.name, text: args.text, isDefault: args.isDefault });
1591
+ return `Signature "${args.name}" created. ID: ${r?.id}`;
1592
+ }
1593
+ if (args.action === "delete") {
1594
+ if (!args.id) throw new Error("id is required");
1595
+ await apiRequest("DELETE", `/signatures/${args.id}`);
1596
+ return "Signature deleted.";
1597
+ }
1598
+ throw new Error("Invalid action. Use: list, create, or delete");
1599
+ }
1600
+ case "manage_templates": {
1601
+ if (args.action === "list") {
1602
+ const r = await apiRequest("GET", "/templates");
1603
+ if (!r?.templates?.length) return "No templates.";
1604
+ return r.templates.map((t) => `[${t.id}] ${t.name}: ${t.subject}`).join("\n");
1605
+ }
1606
+ if (args.action === "create") {
1607
+ if (!args.name || !args.subject || !args.text) throw new Error("name, subject, and text are required");
1608
+ const r = await apiRequest("POST", "/templates", { name: args.name, subject: args.subject, text: args.text });
1609
+ return `Template "${args.name}" created. ID: ${r?.id}`;
1610
+ }
1611
+ if (args.action === "delete") {
1612
+ if (!args.id) throw new Error("id is required");
1613
+ await apiRequest("DELETE", `/templates/${args.id}`);
1614
+ return "Template deleted.";
1615
+ }
1616
+ throw new Error("Invalid action. Use: list, create, or delete");
1617
+ }
1618
+ case "batch_mark_unread": {
1619
+ const uids = args.uids;
1620
+ if (!Array.isArray(uids) || uids.length === 0) throw new Error("uids array required");
1621
+ await apiRequest("POST", "/mail/batch/unseen", { uids, folder: args.folder });
1622
+ return `Marked ${uids.length} messages as unread.`;
1623
+ }
1624
+ case "batch_move": {
1625
+ const uids = args.uids;
1626
+ if (!Array.isArray(uids) || uids.length === 0) throw new Error("uids array required");
1627
+ await apiRequest("POST", "/mail/batch/move", { uids, from: args.from || "INBOX", to: args.to });
1628
+ return `Moved ${uids.length} messages to ${args.to}.`;
1629
+ }
1630
+ case "whoami": {
1631
+ const result = await apiRequest("GET", "/accounts/me");
1632
+ if (!result) throw new Error("Could not retrieve agent info");
1633
+ return [
1634
+ `Name: ${result.name}`,
1635
+ `Email: ${result.email}`,
1636
+ `Role: ${result.role}`,
1637
+ `ID: ${result.id}`,
1638
+ `Created: ${result.createdAt}`,
1639
+ result.metadata && Object.keys(result.metadata).length > 0 ? `Metadata: ${JSON.stringify(result.metadata)}` : null
1640
+ ].filter(Boolean).join("\n");
1641
+ }
1642
+ case "update_metadata": {
1643
+ if (!args.metadata || typeof args.metadata !== "object") throw new Error("metadata object is required");
1644
+ const result = await apiRequest("PATCH", "/accounts/me", { metadata: args.metadata });
1645
+ return `Metadata updated successfully. Agent: ${result?.name ?? "unknown"}`;
1646
+ }
1647
+ case "check_health": {
1648
+ const result = await apiRequest("GET", "/health");
1649
+ if (!result) throw new Error("No response from health check");
1650
+ return `\u{1F380} AgenticMail server: ${result.status ?? "ok"}${result.stalwart ? `, Stalwart: ${result.stalwart}` : ""}`;
1651
+ }
1652
+ case "wait_for_email": {
1653
+ const timeoutSec = Math.min(Math.max(Number(args.timeout) || 120, 5), 300);
1654
+ const controller = new AbortController();
1655
+ const timer = setTimeout(() => controller.abort(), timeoutSec * 1e3);
1656
+ try {
1657
+ const res = await fetch(`${API_URL}/api/agenticmail/events`, {
1658
+ headers: { "Authorization": `Bearer ${API_KEY}`, "Accept": "text/event-stream" },
1659
+ signal: controller.signal
1660
+ });
1661
+ if (!res.ok) {
1662
+ clearTimeout(timer);
1663
+ const search = await apiRequest("POST", "/mail/search", { seen: false });
1664
+ const uids = search?.uids ?? [];
1665
+ if (uids.length > 0) {
1666
+ const email = await apiRequest("GET", `/mail/messages/${uids[0]}`);
1667
+ return JSON.stringify({
1668
+ arrived: true,
1669
+ mode: "poll-fallback",
1670
+ email: email ? {
1671
+ uid: uids[0],
1672
+ from: email.from?.[0]?.address ?? "",
1673
+ subject: email.subject ?? "(no subject)",
1674
+ date: email.date,
1675
+ preview: (email.text ?? "").slice(0, 300)
1676
+ } : null,
1677
+ totalUnread: uids.length
1678
+ });
1679
+ }
1680
+ return JSON.stringify({ arrived: false, reason: "SSE unavailable and no unread emails", timedOut: true });
1681
+ }
1682
+ if (!res.body) {
1683
+ clearTimeout(timer);
1684
+ return JSON.stringify({ arrived: false, reason: "SSE response has no body" });
1685
+ }
1686
+ const reader = res.body.getReader();
1687
+ const decoder = new TextDecoder();
1688
+ let buffer = "";
1689
+ try {
1690
+ while (true) {
1691
+ const { done, value } = await reader.read();
1692
+ if (done) break;
1693
+ buffer += decoder.decode(value, { stream: true });
1694
+ let boundary;
1695
+ while ((boundary = buffer.indexOf("\n\n")) !== -1) {
1696
+ const frame = buffer.slice(0, boundary);
1697
+ buffer = buffer.slice(boundary + 2);
1698
+ for (const line of frame.split("\n")) {
1699
+ if (line.startsWith("data: ")) {
1700
+ try {
1701
+ const event = JSON.parse(line.slice(6));
1702
+ if (event.type === "task" && event.taskId) {
1703
+ clearTimeout(timer);
1704
+ try {
1705
+ reader.cancel();
1706
+ } catch {
1707
+ }
1708
+ return JSON.stringify({
1709
+ arrived: true,
1710
+ mode: "push",
1711
+ eventType: "task",
1712
+ task: {
1713
+ taskId: event.taskId,
1714
+ taskType: event.taskType,
1715
+ description: event.task,
1716
+ from: event.from
1717
+ },
1718
+ hint: 'You have a new task. Use check_tasks(action="pending") to see and claim it.'
1719
+ });
1720
+ }
1721
+ if (event.type === "new" && event.uid) {
1722
+ clearTimeout(timer);
1723
+ try {
1724
+ reader.cancel();
1725
+ } catch {
1726
+ }
1727
+ const email = await apiRequest("GET", `/mail/messages/${event.uid}`);
1728
+ const fromAddr = email?.from?.[0]?.address ?? "";
1729
+ return JSON.stringify({
1730
+ arrived: true,
1731
+ mode: "push",
1732
+ eventType: "email",
1733
+ email: email ? {
1734
+ uid: event.uid,
1735
+ from: fromAddr,
1736
+ fromName: email.from?.[0]?.name ?? fromAddr,
1737
+ subject: email.subject ?? "(no subject)",
1738
+ date: email.date,
1739
+ preview: (email.text ?? "").slice(0, 300),
1740
+ messageId: email.messageId,
1741
+ isInterAgent: fromAddr.endsWith("@localhost")
1742
+ } : null
1743
+ });
1744
+ }
1745
+ } catch {
1746
+ }
1747
+ }
1748
+ }
1749
+ }
1750
+ }
1751
+ } finally {
1752
+ try {
1753
+ reader.cancel();
1754
+ } catch {
1755
+ }
1756
+ }
1757
+ clearTimeout(timer);
1758
+ return JSON.stringify({ arrived: false, reason: "SSE connection closed", timedOut: false });
1759
+ } catch (err) {
1760
+ clearTimeout(timer);
1761
+ if (err.name === "AbortError") {
1762
+ return JSON.stringify({ arrived: false, reason: `No email received within ${timeoutSec}s`, timedOut: true });
1763
+ }
1764
+ return JSON.stringify({ arrived: false, reason: err.message });
1765
+ }
1766
+ }
1767
+ case "batch_read": {
1768
+ const uids = args.uids;
1769
+ if (!Array.isArray(uids) || uids.length === 0) throw new Error("uids array required");
1770
+ const result = await apiRequest("POST", "/mail/batch/read", { uids, folder: args.folder });
1771
+ if (!result?.messages?.length) return "No messages found for the given UIDs.";
1772
+ const lines = result.messages.map((m) => {
1773
+ const from = m.from?.map((a) => a.address).join(", ") ?? "unknown";
1774
+ return `[UID:${m.uid}] From: ${from} | Subject: ${m.subject}
1775
+ ${(m.text ?? "").slice(0, 500)}`;
1776
+ });
1777
+ return `${result.count} messages:
1778
+
1779
+ ${lines.join("\n\n---\n\n")}`;
1780
+ }
1781
+ case "inbox_digest": {
1782
+ const qs = new URLSearchParams();
1783
+ if (args.limit) qs.set("limit", String(args.limit));
1784
+ if (args.offset) qs.set("offset", String(args.offset));
1785
+ if (args.folder) qs.set("folder", String(args.folder));
1786
+ if (args.previewLength) qs.set("previewLength", String(args.previewLength));
1787
+ const query = qs.toString();
1788
+ const result = await apiRequest("GET", `/mail/digest${query ? "?" + query : ""}`);
1789
+ if (!result?.messages?.length) return "Inbox is empty.";
1790
+ const lines = result.messages.map((m, i) => {
1791
+ const from = m.from?.[0]?.address ?? "unknown";
1792
+ const flags = m.flags?.length ? ` [${m.flags.join(", ")}]` : "";
1793
+ return `${i + 1}. [UID:${m.uid}] From: ${from} | Subject: ${m.subject}${flags}
1794
+ ${m.preview || "(no preview)"}`;
1795
+ });
1796
+ return `Inbox digest (${result.count}/${result.total}):
1797
+ ${lines.join("\n")}`;
1798
+ }
1799
+ case "template_send": {
1800
+ const result = await apiRequest("POST", `/templates/${args.id}/send`, {
1801
+ to: args.to,
1802
+ variables: args.variables,
1803
+ cc: args.cc,
1804
+ bcc: args.bcc
1805
+ });
1806
+ return `Template email sent. Message ID: ${result?.messageId ?? "unknown"}`;
1807
+ }
1808
+ case "manage_rules": {
1809
+ if (args.action === "list") {
1810
+ const r = await apiRequest("GET", "/rules");
1811
+ if (!r?.rules?.length) return "No email rules configured.";
1812
+ return r.rules.map(
1813
+ (rule) => `[${rule.id.slice(0, 8)}] ${rule.name} (priority: ${rule.priority}, enabled: ${rule.enabled})
1814
+ Conditions: ${JSON.stringify(rule.conditions)}
1815
+ Actions: ${JSON.stringify(rule.actions)}`
1816
+ ).join("\n");
1817
+ }
1818
+ if (args.action === "create") {
1819
+ const r = await apiRequest("POST", "/rules", {
1820
+ name: args.name,
1821
+ priority: args.priority,
1822
+ conditions: args.conditions,
1823
+ actions: args.actions
1824
+ });
1825
+ return `Rule "${r?.name}" created. ID: ${r?.id}`;
1826
+ }
1827
+ if (args.action === "delete") {
1828
+ if (!args.id) throw new Error("id is required");
1829
+ await apiRequest("DELETE", `/rules/${args.id}`);
1830
+ return "Rule deleted.";
1831
+ }
1832
+ throw new Error("Invalid action. Use: list, create, or delete");
1833
+ }
1834
+ case "cleanup_agents": {
1835
+ if (args.action === "list_inactive") {
1836
+ const qs = args.hours ? `?hours=${args.hours}` : "";
1837
+ const r = await apiRequest("GET", `/accounts/inactive${qs}`, void 0, true);
1838
+ if (!r?.agents?.length) return "No inactive agents found. All agents are either active or persistent.";
1839
+ return `${r.count} inactive agent(s):
1840
+ ${r.agents.map(
1841
+ (a) => ` ${a.name} (${a.email}) \u2014 last active: ${a.last_activity_at || "never"}, persistent: ${a.persistent}`
1842
+ ).join("\n")}`;
1843
+ }
1844
+ if (args.action === "cleanup") {
1845
+ const r = await apiRequest("POST", "/accounts/cleanup", { hours: args.hours, dryRun: args.dryRun }, true);
1846
+ if (r?.dryRun) {
1847
+ if (!r.count) return "No inactive agents to clean up. All agents are either active or persistent.";
1848
+ return `Would delete ${r.count} agent(s): ${r.wouldDelete.map((a) => a.name).join(", ")}`;
1849
+ }
1850
+ if (!r?.count) return "No inactive agents to clean up. All agents are either active or persistent.";
1851
+ return `Deleted ${r.count} agent(s): ${r.deleted.join(", ")}`;
1852
+ }
1853
+ if (args.action === "set_persistent") {
1854
+ if (!args.agentId) throw new Error("agentId is required");
1855
+ await apiRequest("PATCH", `/accounts/${args.agentId}/persistent`, { persistent: args.persistent !== false }, true);
1856
+ return `Agent ${args.agentId} persistent flag set to ${args.persistent !== false}`;
1857
+ }
1858
+ throw new Error("Invalid action. Use: list_inactive, cleanup, or set_persistent");
1859
+ }
1860
+ case "assign_task": {
1861
+ const result = await apiRequest("POST", "/tasks/assign", {
1862
+ assignee: args.assignee,
1863
+ taskType: args.taskType,
1864
+ payload: args.payload,
1865
+ expiresInSeconds: args.expiresInSeconds
1866
+ });
1867
+ return `Task assigned to ${result?.assignee}. Task ID: ${result?.id}`;
1868
+ }
1869
+ case "check_tasks": {
1870
+ let endpoint = args.direction === "outgoing" ? "/tasks/assigned" : "/tasks/pending";
1871
+ if (args.direction !== "outgoing" && args.assignee) {
1872
+ endpoint += `?assignee=${encodeURIComponent(args.assignee)}`;
1873
+ }
1874
+ const r = await apiRequest("GET", endpoint);
1875
+ if (!r?.tasks?.length) return args.direction === "outgoing" ? "No tasks assigned by you." : "No pending tasks.";
1876
+ return `${r.count} tasks:
1877
+ ${r.tasks.map(
1878
+ (t) => ` [${t.id.slice(0, 8)}] ${t.taskType} \u2014 status: ${t.status}, payload: ${JSON.stringify(t.payload).slice(0, 100)}`
1879
+ ).join("\n")}`;
1880
+ }
1881
+ case "claim_task": {
1882
+ const r = await apiRequest("POST", `/tasks/${args.id}/claim`);
1883
+ return `Task ${r?.id} claimed. Payload: ${JSON.stringify(r?.payload)}`;
1884
+ }
1885
+ case "submit_result": {
1886
+ await apiRequest("POST", `/tasks/${args.id}/result`, { result: args.result });
1887
+ return `Result submitted for task ${args.id}.`;
1888
+ }
1889
+ case "call_agent": {
1890
+ const timeoutSec = Math.min(Math.max(Number(args.timeout) || 180, 5), 300);
1891
+ const created = await apiRequest("POST", "/tasks/assign", {
1892
+ assignee: args.target,
1893
+ taskType: "rpc",
1894
+ payload: { task: args.task, ...args.payload || {} }
1895
+ });
1896
+ if (!created?.id) throw new Error("Failed to create task");
1897
+ const taskId = created.id;
1898
+ const deadline = Date.now() + timeoutSec * 1e3;
1899
+ while (Date.now() < deadline) {
1900
+ await new Promise((r) => setTimeout(r, 2e3));
1901
+ try {
1902
+ const task = await apiRequest("GET", `/tasks/${taskId}`);
1903
+ if (task?.status === "completed") return `RPC completed. Result: ${JSON.stringify(task.result)}`;
1904
+ if (task?.status === "failed") return `RPC failed: ${task.error}`;
1905
+ } catch {
1906
+ }
1907
+ }
1908
+ return `RPC timed out. Task ID: ${taskId} \u2014 check later with check_tasks.`;
1909
+ }
1910
+ case "manage_spam": {
1911
+ const action = args.action;
1912
+ if (action === "list") {
1913
+ const qs = new URLSearchParams();
1914
+ if (args.limit) qs.set("limit", String(args.limit));
1915
+ if (args.offset) qs.set("offset", String(args.offset));
1916
+ const query = qs.toString();
1917
+ const result = await apiRequest("GET", `/mail/spam${query ? "?" + query : ""}`);
1918
+ if (!result?.messages?.length) return "Spam folder is empty.";
1919
+ const lines = result.messages.map((m, i) => `${i + 1}. [UID:${m.uid}] From: ${m.from?.[0]?.address ?? "unknown"} | Subject: ${m.subject} | Date: ${m.date}`);
1920
+ return `Spam folder (${result.count}/${result.total}):
1921
+ ${lines.join("\n")}`;
1922
+ }
1923
+ if (action === "report") {
1924
+ const uid = Number(args.uid);
1925
+ if (!uid || uid < 1) throw new Error("uid is required");
1926
+ await apiRequest("POST", `/mail/messages/${uid}/spam`, { folder: args.folder || "INBOX" });
1927
+ return `Message UID ${uid} moved to Spam.`;
1928
+ }
1929
+ if (action === "not_spam") {
1930
+ const uid = Number(args.uid);
1931
+ if (!uid || uid < 1) throw new Error("uid is required");
1932
+ await apiRequest("POST", `/mail/messages/${uid}/not-spam`);
1933
+ return `Message UID ${uid} moved from Spam to INBOX.`;
1934
+ }
1935
+ if (action === "score") {
1936
+ const uid = Number(args.uid);
1937
+ if (!uid || uid < 1) throw new Error("uid is required");
1938
+ const folder = args.folder || "INBOX";
1939
+ const result = await apiRequest("GET", `/mail/messages/${uid}/spam-score?folder=${encodeURIComponent(folder)}`);
1940
+ const lines = [
1941
+ `Spam Score: ${result.score}/100 (${result.isSpam ? "SPAM" : result.isWarning ? "WARNING" : "CLEAN"})`,
1942
+ result.topCategory ? `Top Category: ${result.topCategory}` : null
1943
+ ];
1944
+ if (result.matches?.length) {
1945
+ lines.push("Matches:");
1946
+ for (const m of result.matches) {
1947
+ lines.push(` [${m.ruleId}] +${m.score} \u2014 ${m.description}`);
1948
+ }
1949
+ }
1950
+ return lines.filter(Boolean).join("\n");
1951
+ }
1952
+ throw new Error("Invalid action. Use: list, report, not_spam, or score");
1953
+ }
1954
+ case "manage_pending_emails": {
1955
+ const action = String(args.action);
1956
+ if (action === "list") {
1957
+ const result = await apiRequest("GET", "/mail/pending");
1958
+ if (result?.pending) {
1959
+ for (const p of result.pending) {
1960
+ if (p.status !== "pending") cancelFollowUp(p.id);
1961
+ }
1962
+ }
1963
+ if (!result?.pending?.length) return withReminders("No pending outbound emails.");
1964
+ const lines = result.pending.map((p, i) => `${i + 1}. [${p.id}] To: ${p.to} | Subject: ${p.subject} | Status: ${p.status} | Created: ${p.createdAt}`);
1965
+ return withReminders(`Pending emails (${result.count}):
1966
+ ${lines.join("\n")}`);
1967
+ }
1968
+ if (action === "get") {
1969
+ if (!args.id) throw new Error("id is required");
1970
+ const result = await apiRequest("GET", `/mail/pending/${encodeURIComponent(String(args.id))}`);
1971
+ if (!result) throw new Error("Pending email not found");
1972
+ if (result.status !== "pending") cancelFollowUp(String(args.id));
1973
+ return withReminders(`Pending Email: ${result.id}
1974
+ To: ${result.mailOptions?.to}
1975
+ Subject: ${result.mailOptions?.subject}
1976
+ Status: ${result.status}
1977
+ Created: ${result.createdAt}
1978
+ Warnings:
1979
+ ${result.summary}`);
1980
+ }
1981
+ if (action === "approve" || action === "reject") {
1982
+ return withReminders(`You cannot ${action} pending emails. Only the owner (human) can approve or reject blocked emails. Please inform the owner and wait for their decision.`);
1983
+ }
1984
+ throw new Error("Invalid action. Use: list or get");
1985
+ }
1986
+ default:
1987
+ throw new Error(`Unknown tool: ${name}`);
1988
+ }
1989
+ }
1990
+
1991
+ // src/resources.ts
1992
+ var API_URL2 = process.env.AGENTICMAIL_API_URL ?? "http://127.0.0.1:3100";
1993
+ var API_KEY2 = process.env.AGENTICMAIL_API_KEY ?? "";
1994
+ async function apiRequest2(path) {
1995
+ if (!API_KEY2) {
1996
+ throw new Error("API key is not configured. Set AGENTICMAIL_API_KEY.");
1997
+ }
1998
+ const response = await fetch(`${API_URL2}/api/agenticmail${path}`, {
1999
+ headers: {
2000
+ "Authorization": `Bearer ${API_KEY2}`
2001
+ },
2002
+ signal: AbortSignal.timeout(3e4)
2003
+ });
2004
+ if (!response.ok) {
2005
+ let text;
2006
+ try {
2007
+ text = await response.text();
2008
+ } catch {
2009
+ text = "(could not read response body)";
2010
+ }
2011
+ throw new Error(`API error ${response.status}: ${text}`);
2012
+ }
2013
+ const contentType = response.headers.get("content-type");
2014
+ if (contentType?.includes("application/json")) {
2015
+ try {
2016
+ return await response.json();
2017
+ } catch {
2018
+ throw new Error(`API returned invalid JSON from ${path}`);
2019
+ }
2020
+ }
2021
+ return null;
2022
+ }
2023
+ var resourceDefinitions = [
2024
+ {
2025
+ uri: "agenticmail://inbox",
2026
+ name: "Agent Inbox",
2027
+ description: "Browse the current agent's email inbox",
2028
+ mimeType: "text/plain"
2029
+ }
2030
+ ];
2031
+ async function handleResourceRead(uri) {
2032
+ if (uri === "agenticmail://inbox") {
2033
+ const result = await apiRequest2("/mail/inbox?limit=20");
2034
+ if (!result?.messages?.length) {
2035
+ return "Inbox is empty.";
2036
+ }
2037
+ const lines = result.messages.map(
2038
+ (m, i) => `${i + 1}. [UID:${m.uid}] From: ${m.from?.[0]?.address ?? "unknown"} | Subject: ${m.subject} | ${m.date}`
2039
+ );
2040
+ return `Inbox:
2041
+ ${lines.join("\n")}`;
2042
+ }
2043
+ throw new Error(`Unknown resource: ${uri}`);
2044
+ }
2045
+
2046
+ // src/index.ts
2047
+ var server = new McpServer({
2048
+ name: "\u{1F380} AgenticMail",
2049
+ version: "0.2.26",
2050
+ description: "\u{1F380} AgenticMail \u2014 Email infrastructure for AI agents. By Ope Olatunji (https://github.com/agenticmail/agenticmail)"
2051
+ });
2052
+ for (const tool of toolDefinitions) {
2053
+ server.tool(
2054
+ tool.name,
2055
+ tool.description,
2056
+ tool.inputSchema,
2057
+ async ({ arguments: args }) => {
2058
+ try {
2059
+ const result = await handleToolCall(tool.name, args);
2060
+ return { content: [{ type: "text", text: result }] };
2061
+ } catch (err) {
2062
+ const message = err instanceof Error ? err.message : String(err);
2063
+ return {
2064
+ content: [{ type: "text", text: `Error: ${message}` }],
2065
+ isError: true
2066
+ };
2067
+ }
2068
+ }
2069
+ );
2070
+ }
2071
+ for (const resource of resourceDefinitions) {
2072
+ server.resource(
2073
+ resource.name,
2074
+ resource.uri,
2075
+ { description: resource.description, mimeType: resource.mimeType },
2076
+ async () => {
2077
+ try {
2078
+ const content = await handleResourceRead(resource.uri);
2079
+ return {
2080
+ contents: [{ uri: resource.uri, text: content, mimeType: resource.mimeType }]
2081
+ };
2082
+ } catch (err) {
2083
+ const message = err instanceof Error ? err.message : String(err);
2084
+ return {
2085
+ contents: [{ uri: resource.uri, text: `Error: ${message}`, mimeType: "text/plain" }]
2086
+ };
2087
+ }
2088
+ }
2089
+ );
2090
+ }
2091
+ var transport = new StdioServerTransport();
2092
+ try {
2093
+ await server.connect(transport);
2094
+ } catch (err) {
2095
+ console.error("[agenticmail-mcp] Failed to start:", err);
2096
+ process.exit(1);
2097
+ }
2098
+ async function shutdown() {
2099
+ try {
2100
+ await server.close();
2101
+ } catch {
2102
+ }
2103
+ process.exit(0);
2104
+ }
2105
+ process.on("SIGTERM", () => shutdown());
2106
+ process.on("SIGINT", () => shutdown());