@agenticmail/mcp 0.7.1 → 0.7.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -6
- package/dist/index.js +264 -76
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ When connected, your AI agent can send emails and texts, check inboxes, reply to
|
|
|
10
10
|
npm install -g @agenticmail/mcp
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
**Requirements:** Node.js
|
|
13
|
+
**Requirements:** Node.js 22+, AgenticMail API server running
|
|
14
14
|
|
|
15
15
|
---
|
|
16
16
|
|
|
@@ -35,7 +35,7 @@ Add to your MCP client configuration (e.g., `.mcp.json` or project settings):
|
|
|
35
35
|
"command": "npx",
|
|
36
36
|
"args": ["agenticmail-mcp"],
|
|
37
37
|
"env": {
|
|
38
|
-
"AGENTICMAIL_API_URL": "http://127.0.0.1:
|
|
38
|
+
"AGENTICMAIL_API_URL": "http://127.0.0.1:3829",
|
|
39
39
|
"AGENTICMAIL_API_KEY": "ak_your_agent_key"
|
|
40
40
|
}
|
|
41
41
|
}
|
|
@@ -56,7 +56,7 @@ For desktop AI applications, add to your MCP configuration file. Example paths:
|
|
|
56
56
|
"command": "npx",
|
|
57
57
|
"args": ["agenticmail-mcp"],
|
|
58
58
|
"env": {
|
|
59
|
-
"AGENTICMAIL_API_URL": "http://127.0.0.1:
|
|
59
|
+
"AGENTICMAIL_API_URL": "http://127.0.0.1:3829",
|
|
60
60
|
"AGENTICMAIL_API_KEY": "ak_your_agent_key"
|
|
61
61
|
}
|
|
62
62
|
}
|
|
@@ -68,9 +68,27 @@ For desktop AI applications, add to your MCP configuration file. Example paths:
|
|
|
68
68
|
|
|
69
69
|
| Variable | Required | Description |
|
|
70
70
|
|----------|----------|-------------|
|
|
71
|
-
| `AGENTICMAIL_API_URL` | Yes | AgenticMail API server URL (
|
|
72
|
-
| `AGENTICMAIL_API_KEY` | Yes | Agent API key (`ak_...`). Determines
|
|
73
|
-
| `AGENTICMAIL_MASTER_KEY` | No | Master key (`mk_...`). Required for admin operations (create/delete agents, approve emails, gateway config). |
|
|
71
|
+
| `AGENTICMAIL_API_URL` | Yes | AgenticMail API server URL. Default port is **`3829`** (changed from `3100` in `@agenticmail/mcp@0.7.x` to avoid common dev-tool conflicts). Example: `http://127.0.0.1:3829`. |
|
|
72
|
+
| `AGENTICMAIL_API_KEY` | Yes¹ | Agent API key (`ak_...`). Determines the default identity this MCP server acts as when a tool call doesn't pass `_account`. |
|
|
73
|
+
| `AGENTICMAIL_MASTER_KEY` | No | Master key (`mk_...`). Required for admin operations (create/delete agents, approve emails, gateway config). **Also enables on-demand `_account` resolution** — any tool call passing `_account: "<name>"` will lazily fetch that agent's API key via the master key the first time it sees the name, so freshly-`create_account`'d agents become addressable without restarting the MCP server. |
|
|
74
|
+
| `AGENTICMAIL_ACCOUNT_KEYS_JSON` | No | JSON map of `{"<agentName>": "<apiKey>"}` for per-call identity switching. When the caller passes `_account: "Fola"` (etc.), the server authenticates AS that agent for the duration of the call. Populated automatically by `agenticmail claudecode install` for the Claude Code integration. |
|
|
75
|
+
|
|
76
|
+
¹ Either `AGENTICMAIL_API_KEY` OR `AGENTICMAIL_MASTER_KEY` (or `AGENTICMAIL_ACCOUNT_KEYS_JSON`) must be set, but you don't strictly need all three.
|
|
77
|
+
|
|
78
|
+
### Per-call identity switching (`_account`)
|
|
79
|
+
|
|
80
|
+
Every tool's input schema accepts an optional `_account: "<name>"` parameter. When passed, the server resolves that name to an apiKey (from `AGENTICMAIL_ACCOUNT_KEYS_JSON`, then falling back to a live master-keyed lookup of `/accounts`) and runs the call as that agent. Without `_account`, the call uses `AGENTICMAIL_API_KEY` as the default identity.
|
|
81
|
+
|
|
82
|
+
This is what powers the [`@agenticmail/claudecode`](https://www.npmjs.com/package/@agenticmail/claudecode) integration: one MCP server process, many AgenticMail identities. A Claude Code subagent that "is" Fola passes `_account: "Fola"` on every call and ends up reading Fola's real inbox, sending mail from `fola@localhost`, and so on.
|
|
83
|
+
|
|
84
|
+
### Meta-tools for cheap discovery (`request_tools` + `invoke`)
|
|
85
|
+
|
|
86
|
+
To keep host context windows small, only ~10 of the 62 tools are pre-declared in a typical subagent's `tools:` whitelist. The other ~50 stay reachable through two always-on meta-tools:
|
|
87
|
+
|
|
88
|
+
- **`request_tools({ query?, sets? })`** — Returns a text catalogue of the unloaded tools, grouped by set (`mail_extras`, `mail_compose`, `sms`, `account_admin`, …). Optional substring filter or set-name filter.
|
|
89
|
+
- **`invoke({ tool, args, _account? })`** — Dispatches to any of the 62 tools by name. The agent uses `request_tools` to discover, `invoke` to call.
|
|
90
|
+
|
|
91
|
+
Token impact: a typical subagent spawn loads ~3K tokens of tool schemas instead of ~15K. The cost is one extra round trip for uncommon operations (discover → invoke), which is almost always a worthwhile trade.
|
|
74
92
|
|
|
75
93
|
---
|
|
76
94
|
|
package/dist/index.js
CHANGED
|
@@ -22383,11 +22383,18 @@ var TOOL_SETS = {
|
|
|
22383
22383
|
"search_emails",
|
|
22384
22384
|
"list_agents",
|
|
22385
22385
|
"message_agent",
|
|
22386
|
-
// call_agent is the
|
|
22387
|
-
//
|
|
22388
|
-
//
|
|
22389
|
-
//
|
|
22386
|
+
// call_agent is the one-shot RPC primitive — sync request, sync answer.
|
|
22387
|
+
// Used when you need ONE structured result from ONE teammate; for
|
|
22388
|
+
// multi-step coordination use the thread pattern (send_email with CC
|
|
22389
|
+
// + reply_email with replyAll) instead.
|
|
22390
22390
|
"call_agent",
|
|
22391
|
+
// wait_for_email is the thread-coordination primitive: block until a
|
|
22392
|
+
// specific reply lands in your inbox (filter by from / subject /
|
|
22393
|
+
// inReplyTo / participants). The host uses it to wake on the next
|
|
22394
|
+
// teammate reply; agents use it when they delegate and need an answer
|
|
22395
|
+
// back. Essential enough that paying its tokens at every spawn beats
|
|
22396
|
+
// making the agent discover it via request_tools.
|
|
22397
|
+
"wait_for_email",
|
|
22391
22398
|
"check_tasks"
|
|
22392
22399
|
],
|
|
22393
22400
|
/** Less-common mail operations. */
|
|
@@ -22429,7 +22436,6 @@ var TOOL_SETS = {
|
|
|
22429
22436
|
/** Agent coordination beyond the basics in `essential`. */
|
|
22430
22437
|
agent_coord: [
|
|
22431
22438
|
"check_messages",
|
|
22432
|
-
"wait_for_email",
|
|
22433
22439
|
"claim_task",
|
|
22434
22440
|
"submit_result"
|
|
22435
22441
|
],
|
|
@@ -22603,15 +22609,15 @@ async function apiRequest(method, path, body, useMasterKey = false, timeoutMs =
|
|
|
22603
22609
|
var toolDefinitions = [
|
|
22604
22610
|
{
|
|
22605
22611
|
name: "send_email",
|
|
22606
|
-
description: "Send an email from the agent's mailbox.
|
|
22612
|
+
description: "Send an email from the agent's mailbox. Supports multiple recipients on To and CC (comma-separated). This is the PRIMARY primitive for multi-agent coordination: kick off a thread with all participants on CC, and every local recipient is woken automatically. Each woken agent reads the thread, decides whose turn it is, and either reply-all's to contribute or stays silent. External emails are scanned for sensitive content; HIGH severity detections are BLOCKED for owner approval. You CANNOT bypass the outbound guard.",
|
|
22607
22613
|
inputSchema: {
|
|
22608
22614
|
type: "object",
|
|
22609
22615
|
properties: {
|
|
22610
|
-
to: { type: "string", description: "Recipient email address" },
|
|
22616
|
+
to: { type: "string", description: "Recipient email address(es). For multi-agent threads, put the primary actor here and CC the rest. Comma-separated supported." },
|
|
22611
22617
|
subject: { type: "string", description: "Email subject line" },
|
|
22612
22618
|
text: { type: "string", description: "Plain text body" },
|
|
22613
22619
|
html: { type: "string", description: "HTML body (optional)" },
|
|
22614
|
-
cc: { type: "string", description:
|
|
22620
|
+
cc: { type: "string", description: 'CC recipients \u2014 the team. Comma-separated, e.g. "vesper@localhost, orion@localhost". Every local @localhost recipient is auto-woken when this email lands.' },
|
|
22615
22621
|
inReplyTo: { type: "string", description: "Message-ID to reply to (optional)" },
|
|
22616
22622
|
references: {
|
|
22617
22623
|
type: "array",
|
|
@@ -22699,7 +22705,7 @@ var toolDefinitions = [
|
|
|
22699
22705
|
},
|
|
22700
22706
|
{
|
|
22701
22707
|
name: "reply_email",
|
|
22702
|
-
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.",
|
|
22708
|
+
description: "Reply to an email. Fetches the original message, auto-fills To, Subject (Re:), In-Reply-To, and References, then sends with quoted body. **For multi-agent thread coordination, pass `replyAll: true`** so every CC'd participant sees your contribution and stays in context \u2014 that is how the thread-as-workspace pattern works. Outbound guard applies \u2014 HIGH severity content is held for review.",
|
|
22703
22709
|
inputSchema: {
|
|
22704
22710
|
type: "object",
|
|
22705
22711
|
properties: {
|
|
@@ -22878,7 +22884,7 @@ var toolDefinitions = [
|
|
|
22878
22884
|
},
|
|
22879
22885
|
{
|
|
22880
22886
|
name: "create_account",
|
|
22881
|
-
description:
|
|
22887
|
+
description: 'Create a new AgenticMail agent (email account + identity + API key + persona derived from role/metadata). Requires master API key. After creation: address them at `<name>@localhost`, delegate work via `call_agent({ target: "<name>", task: ... })`, or hand off via `send_email` / `message_agent`. The new agent acts as themselves \u2014 you never need to (and must not) roleplay them inside your host\'s native sub-agent tool.',
|
|
22882
22888
|
inputSchema: {
|
|
22883
22889
|
type: "object",
|
|
22884
22890
|
properties: {
|
|
@@ -22999,7 +23005,7 @@ var toolDefinitions = [
|
|
|
22999
23005
|
},
|
|
23000
23006
|
{
|
|
23001
23007
|
name: "list_agents",
|
|
23002
|
-
description: "List all AI agents in the system with their email addresses and roles. Use this to discover which agents you can
|
|
23008
|
+
description: "List all AI agents in the system with their email addresses and roles. Use this to discover which agents you can call via call_agent (sync RPC) or email via send_email / message_agent (async). DO NOT spawn one of your host's native sub-agents and roleplay AS these agents \u2014 each one is a real identity with its own mailbox; just address them through AgenticMail and let them work as themselves.",
|
|
23003
23009
|
inputSchema: {
|
|
23004
23010
|
type: "object",
|
|
23005
23011
|
properties: {}
|
|
@@ -23007,7 +23013,7 @@ var toolDefinitions = [
|
|
|
23007
23013
|
},
|
|
23008
23014
|
{
|
|
23009
23015
|
name: "message_agent",
|
|
23010
|
-
description: "
|
|
23016
|
+
description: "Async fire-and-forget: deliver a message to another AI agent's inbox. They will process it on their own schedule (immediately if a dispatcher is attached, later otherwise) and may reply by email. Use this for non-blocking handoffs. Prefer `call_agent` when you need a structured reply back. Both flows let the target agent do the work AS THEMSELVES \u2014 never roleplay them inside your own host.",
|
|
23011
23017
|
inputSchema: {
|
|
23012
23018
|
type: "object",
|
|
23013
23019
|
properties: {
|
|
@@ -23133,11 +23139,20 @@ var toolDefinitions = [
|
|
|
23133
23139
|
},
|
|
23134
23140
|
{
|
|
23135
23141
|
name: "wait_for_email",
|
|
23136
|
-
description:
|
|
23142
|
+
description: 'Block until a matching email (or task) lands in your inbox. Push-based (SSE) \u2014 far more efficient than polling. Supports filtering by sender, subject substring, thread (In-Reply-To), or a participants list. The single-most-useful tool for thread-based coordination: send a kickoff email CC\'ing your team, then `wait_for_email({ subject: "<core thread subject>" })` to wake on the first reply. Non-matching events that arrive during the wait are ignored \u2014 you only resume when something you asked for shows up (or timeout).',
|
|
23137
23143
|
inputSchema: {
|
|
23138
23144
|
type: "object",
|
|
23139
23145
|
properties: {
|
|
23140
|
-
timeout: { type: "number", description: "Max seconds to wait (default: 120, max: 300)" }
|
|
23146
|
+
timeout: { type: "number", description: "Max seconds to wait (default: 120, max: 300)" },
|
|
23147
|
+
from: { type: "string", description: 'Only resume on an email FROM this address (case-insensitive substring match on the bare address \u2014 "orion" matches "orion@localhost").' },
|
|
23148
|
+
subject: { type: "string", description: `Only resume on an email whose subject contains this string (case-insensitive). The thread's core subject works \u2014 "Build a small game" matches "Re: Build a small game".` },
|
|
23149
|
+
inReplyTo: { type: "string", description: "Only resume on an email whose In-Reply-To header equals this Message-ID. Most precise thread filter \u2014 use when you have the exact Message-ID of the message you expect a reply to." },
|
|
23150
|
+
participants: {
|
|
23151
|
+
type: "array",
|
|
23152
|
+
items: { type: "string" },
|
|
23153
|
+
description: `Only resume on an email from ANY of these addresses (case-insensitive). Use this to wait for any teammate's reply, e.g. ["vesper@localhost", "orion@localhost"].`
|
|
23154
|
+
},
|
|
23155
|
+
includeTasks: { type: "boolean", description: "Include task-assignment events as matches (default: true). Set false if you only care about email." }
|
|
23141
23156
|
}
|
|
23142
23157
|
}
|
|
23143
23158
|
},
|
|
@@ -23248,7 +23263,7 @@ var toolDefinitions = [
|
|
|
23248
23263
|
},
|
|
23249
23264
|
{
|
|
23250
23265
|
name: "call_agent",
|
|
23251
|
-
description:
|
|
23266
|
+
description: `Synchronous RPC to delegate work to another AgenticMail agent. Pipeline: the task is queued in AgenticMail, the target agent processes it AS THEMSELVES (under their real identity, mailbox, persona, and audit trail), and the structured result returns into your call. THIS IS HOW MULTI-AGENT COORDINATION IS SUPPOSED TO WORK from any MCP host. Do not, instead, spawn one of your host's native sub-agents and tell it to "act as <target>" \u2014 that produces output under your identity, never touches the target's inbox, and skips their persona. Times out after the specified duration (default 180s, max 300s).`,
|
|
23252
23267
|
inputSchema: {
|
|
23253
23268
|
type: "object",
|
|
23254
23269
|
properties: {
|
|
@@ -24237,6 +24252,32 @@ ${lines.join("\n")}`;
|
|
|
24237
24252
|
}
|
|
24238
24253
|
case "wait_for_email": {
|
|
24239
24254
|
const timeoutSec = Math.min(Math.max(Number(args2.timeout) || 120, 5), 300);
|
|
24255
|
+
const includeTasks = args2.includeTasks !== false;
|
|
24256
|
+
const fromFilter = typeof args2.from === "string" ? args2.from.trim().toLowerCase() : "";
|
|
24257
|
+
const subjectFilter = typeof args2.subject === "string" ? args2.subject.trim().toLowerCase() : "";
|
|
24258
|
+
const inReplyToFilter = typeof args2.inReplyTo === "string" ? args2.inReplyTo.trim() : "";
|
|
24259
|
+
const participantsRaw = Array.isArray(args2.participants) ? args2.participants : [];
|
|
24260
|
+
const participantsFilter = participantsRaw.filter((p) => typeof p === "string" && p.trim().length > 0).map((p) => p.trim().toLowerCase());
|
|
24261
|
+
const hasAnyFilter = !!(fromFilter || subjectFilter || inReplyToFilter || participantsFilter.length);
|
|
24262
|
+
const bareAddr = (s) => {
|
|
24263
|
+
if (!s) return "";
|
|
24264
|
+
const m = s.match(/<([^>]+)>/);
|
|
24265
|
+
return (m ? m[1] : s).trim().toLowerCase();
|
|
24266
|
+
};
|
|
24267
|
+
const emailMatches = (email2) => {
|
|
24268
|
+
if (!hasAnyFilter) return true;
|
|
24269
|
+
const fromAddr = bareAddr(email2?.from?.[0]?.address ?? "");
|
|
24270
|
+
const subj = String(email2?.subject ?? "").toLowerCase();
|
|
24271
|
+
const ire = String(email2?.inReplyTo ?? "").trim();
|
|
24272
|
+
if (fromFilter && !fromAddr.includes(fromFilter)) return false;
|
|
24273
|
+
if (subjectFilter && !subj.includes(subjectFilter)) return false;
|
|
24274
|
+
if (inReplyToFilter && ire !== inReplyToFilter) return false;
|
|
24275
|
+
if (participantsFilter.length > 0) {
|
|
24276
|
+
const ok = participantsFilter.some((p) => fromAddr.includes(p));
|
|
24277
|
+
if (!ok) return false;
|
|
24278
|
+
}
|
|
24279
|
+
return true;
|
|
24280
|
+
};
|
|
24240
24281
|
const controller = new AbortController();
|
|
24241
24282
|
const timer = setTimeout(() => controller.abort(), timeoutSec * 1e3);
|
|
24242
24283
|
try {
|
|
@@ -24246,24 +24287,38 @@ ${lines.join("\n")}`;
|
|
|
24246
24287
|
});
|
|
24247
24288
|
if (!res.ok) {
|
|
24248
24289
|
clearTimeout(timer);
|
|
24249
|
-
const
|
|
24290
|
+
const searchBody = { seen: false };
|
|
24291
|
+
if (fromFilter) searchBody.from = fromFilter;
|
|
24292
|
+
if (subjectFilter) searchBody.subject = subjectFilter;
|
|
24293
|
+
const search = await apiRequest("POST", "/mail/search", searchBody);
|
|
24250
24294
|
const uids = search?.uids ?? [];
|
|
24251
|
-
|
|
24252
|
-
const email2 = await apiRequest("GET", `/mail/messages/${
|
|
24295
|
+
for (const uid of [...uids].reverse()) {
|
|
24296
|
+
const email2 = await apiRequest("GET", `/mail/messages/${uid}`);
|
|
24297
|
+
if (!email2 || !emailMatches(email2)) continue;
|
|
24298
|
+
const fromAddr = bareAddr(email2.from?.[0]?.address);
|
|
24253
24299
|
return JSON.stringify({
|
|
24254
24300
|
arrived: true,
|
|
24255
24301
|
mode: "poll-fallback",
|
|
24256
|
-
|
|
24257
|
-
|
|
24258
|
-
|
|
24302
|
+
eventType: "email",
|
|
24303
|
+
email: {
|
|
24304
|
+
uid,
|
|
24305
|
+
from: fromAddr,
|
|
24306
|
+
fromName: email2.from?.[0]?.name ?? fromAddr,
|
|
24259
24307
|
subject: email2.subject ?? "(no subject)",
|
|
24260
24308
|
date: email2.date,
|
|
24261
|
-
preview: (email2.text ?? "").slice(0, 300)
|
|
24262
|
-
|
|
24309
|
+
preview: (email2.text ?? "").slice(0, 300),
|
|
24310
|
+
messageId: email2.messageId,
|
|
24311
|
+
inReplyTo: email2.inReplyTo,
|
|
24312
|
+
isInterAgent: fromAddr.endsWith("@localhost")
|
|
24313
|
+
},
|
|
24263
24314
|
totalUnread: uids.length
|
|
24264
24315
|
});
|
|
24265
24316
|
}
|
|
24266
|
-
return JSON.stringify({
|
|
24317
|
+
return JSON.stringify({
|
|
24318
|
+
arrived: false,
|
|
24319
|
+
reason: hasAnyFilter ? "SSE unavailable and no unread emails match the filters" : "SSE unavailable and no unread emails",
|
|
24320
|
+
timedOut: true
|
|
24321
|
+
});
|
|
24267
24322
|
}
|
|
24268
24323
|
if (!res.body) {
|
|
24269
24324
|
clearTimeout(timer);
|
|
@@ -24272,6 +24327,7 @@ ${lines.join("\n")}`;
|
|
|
24272
24327
|
const reader = res.body.getReader();
|
|
24273
24328
|
const decoder = new TextDecoder();
|
|
24274
24329
|
let buffer = "";
|
|
24330
|
+
let skipped = 0;
|
|
24275
24331
|
try {
|
|
24276
24332
|
while (true) {
|
|
24277
24333
|
const { done, value } = await reader.read();
|
|
@@ -24282,54 +24338,65 @@ ${lines.join("\n")}`;
|
|
|
24282
24338
|
const frame = buffer.slice(0, boundary);
|
|
24283
24339
|
buffer = buffer.slice(boundary + 2);
|
|
24284
24340
|
for (const line of frame.split("\n")) {
|
|
24285
|
-
if (line.startsWith("data: "))
|
|
24341
|
+
if (!line.startsWith("data: ")) continue;
|
|
24342
|
+
let event;
|
|
24343
|
+
try {
|
|
24344
|
+
event = JSON.parse(line.slice(6));
|
|
24345
|
+
} catch {
|
|
24346
|
+
continue;
|
|
24347
|
+
}
|
|
24348
|
+
if (event.type === "task" && event.taskId) {
|
|
24349
|
+
if (!includeTasks || hasAnyFilter) {
|
|
24350
|
+
skipped++;
|
|
24351
|
+
continue;
|
|
24352
|
+
}
|
|
24353
|
+
clearTimeout(timer);
|
|
24286
24354
|
try {
|
|
24287
|
-
|
|
24288
|
-
if (event.type === "task" && event.taskId) {
|
|
24289
|
-
clearTimeout(timer);
|
|
24290
|
-
try {
|
|
24291
|
-
reader.cancel();
|
|
24292
|
-
} catch {
|
|
24293
|
-
}
|
|
24294
|
-
return JSON.stringify({
|
|
24295
|
-
arrived: true,
|
|
24296
|
-
mode: "push",
|
|
24297
|
-
eventType: "task",
|
|
24298
|
-
task: {
|
|
24299
|
-
taskId: event.taskId,
|
|
24300
|
-
taskType: event.taskType,
|
|
24301
|
-
description: event.task,
|
|
24302
|
-
from: event.from
|
|
24303
|
-
},
|
|
24304
|
-
hint: 'You have a new task. Use check_tasks(action="pending") to see and claim it.'
|
|
24305
|
-
});
|
|
24306
|
-
}
|
|
24307
|
-
if (event.type === "new" && event.uid) {
|
|
24308
|
-
clearTimeout(timer);
|
|
24309
|
-
try {
|
|
24310
|
-
reader.cancel();
|
|
24311
|
-
} catch {
|
|
24312
|
-
}
|
|
24313
|
-
const email2 = await apiRequest("GET", `/mail/messages/${event.uid}`);
|
|
24314
|
-
const fromAddr = email2?.from?.[0]?.address ?? "";
|
|
24315
|
-
return JSON.stringify({
|
|
24316
|
-
arrived: true,
|
|
24317
|
-
mode: "push",
|
|
24318
|
-
eventType: "email",
|
|
24319
|
-
email: email2 ? {
|
|
24320
|
-
uid: event.uid,
|
|
24321
|
-
from: fromAddr,
|
|
24322
|
-
fromName: email2.from?.[0]?.name ?? fromAddr,
|
|
24323
|
-
subject: email2.subject ?? "(no subject)",
|
|
24324
|
-
date: email2.date,
|
|
24325
|
-
preview: (email2.text ?? "").slice(0, 300),
|
|
24326
|
-
messageId: email2.messageId,
|
|
24327
|
-
isInterAgent: fromAddr.endsWith("@localhost")
|
|
24328
|
-
} : null
|
|
24329
|
-
});
|
|
24330
|
-
}
|
|
24355
|
+
reader.cancel();
|
|
24331
24356
|
} catch {
|
|
24332
24357
|
}
|
|
24358
|
+
return JSON.stringify({
|
|
24359
|
+
arrived: true,
|
|
24360
|
+
mode: "push",
|
|
24361
|
+
eventType: "task",
|
|
24362
|
+
task: {
|
|
24363
|
+
taskId: event.taskId,
|
|
24364
|
+
taskType: event.taskType,
|
|
24365
|
+
description: event.task,
|
|
24366
|
+
from: event.from
|
|
24367
|
+
},
|
|
24368
|
+
hint: 'You have a new task. Use check_tasks(action="pending") to see and claim it.'
|
|
24369
|
+
});
|
|
24370
|
+
}
|
|
24371
|
+
if (event.type === "new" && event.uid) {
|
|
24372
|
+
const email2 = await apiRequest("GET", `/mail/messages/${event.uid}`);
|
|
24373
|
+
if (!email2 || !emailMatches(email2)) {
|
|
24374
|
+
skipped++;
|
|
24375
|
+
continue;
|
|
24376
|
+
}
|
|
24377
|
+
clearTimeout(timer);
|
|
24378
|
+
try {
|
|
24379
|
+
reader.cancel();
|
|
24380
|
+
} catch {
|
|
24381
|
+
}
|
|
24382
|
+
const fromAddr = bareAddr(email2.from?.[0]?.address);
|
|
24383
|
+
return JSON.stringify({
|
|
24384
|
+
arrived: true,
|
|
24385
|
+
mode: "push",
|
|
24386
|
+
eventType: "email",
|
|
24387
|
+
skippedEvents: skipped,
|
|
24388
|
+
email: {
|
|
24389
|
+
uid: event.uid,
|
|
24390
|
+
from: fromAddr,
|
|
24391
|
+
fromName: email2.from?.[0]?.name ?? fromAddr,
|
|
24392
|
+
subject: email2.subject ?? "(no subject)",
|
|
24393
|
+
date: email2.date,
|
|
24394
|
+
preview: (email2.text ?? "").slice(0, 300),
|
|
24395
|
+
messageId: email2.messageId,
|
|
24396
|
+
inReplyTo: email2.inReplyTo,
|
|
24397
|
+
isInterAgent: fromAddr.endsWith("@localhost")
|
|
24398
|
+
}
|
|
24399
|
+
});
|
|
24333
24400
|
}
|
|
24334
24401
|
}
|
|
24335
24402
|
}
|
|
@@ -24341,11 +24408,20 @@ ${lines.join("\n")}`;
|
|
|
24341
24408
|
}
|
|
24342
24409
|
}
|
|
24343
24410
|
clearTimeout(timer);
|
|
24344
|
-
return JSON.stringify({ arrived: false, reason: "SSE connection closed", timedOut: false });
|
|
24411
|
+
return JSON.stringify({ arrived: false, reason: "SSE connection closed", timedOut: false, skippedEvents: skipped });
|
|
24345
24412
|
} catch (err) {
|
|
24346
24413
|
clearTimeout(timer);
|
|
24347
24414
|
if (err.name === "AbortError") {
|
|
24348
|
-
return JSON.stringify({
|
|
24415
|
+
return JSON.stringify({
|
|
24416
|
+
arrived: false,
|
|
24417
|
+
reason: hasAnyFilter ? `Timed out after ${timeoutSec}s \u2014 no matching email arrived (filters: ${[
|
|
24418
|
+
fromFilter && `from~="${fromFilter}"`,
|
|
24419
|
+
subjectFilter && `subject~="${subjectFilter}"`,
|
|
24420
|
+
inReplyToFilter && `inReplyTo="${inReplyToFilter}"`,
|
|
24421
|
+
participantsFilter.length && `participants=${JSON.stringify(participantsFilter)}`
|
|
24422
|
+
].filter(Boolean).join(", ")})` : `No email received within ${timeoutSec}s`,
|
|
24423
|
+
timedOut: true
|
|
24424
|
+
});
|
|
24349
24425
|
}
|
|
24350
24426
|
return JSON.stringify({ arrived: false, reason: err.message });
|
|
24351
24427
|
}
|
|
@@ -24902,12 +24978,124 @@ function jsonSchemaToZod(schema, topLevel = false) {
|
|
|
24902
24978
|
}
|
|
24903
24979
|
return schema.description ? result.describe(schema.description) : result;
|
|
24904
24980
|
}
|
|
24981
|
+
var SERVER_INSTRUCTIONS = [
|
|
24982
|
+
"\u{1F380} AgenticMail \u2014 multi-agent email + SMS infrastructure.",
|
|
24983
|
+
"",
|
|
24984
|
+
"AgenticMail agents are persistent identities with their own inboxes, API",
|
|
24985
|
+
"keys, personas, and audit trails. They coordinate the way humans do: in",
|
|
24986
|
+
"email threads, with every participant CC'd, taking turns implicitly from",
|
|
24987
|
+
"context. Address other agents through AgenticMail's own primitives \u2014",
|
|
24988
|
+
"never roleplay them inside your host's native sub-agent / sub-task tool.",
|
|
24989
|
+
"",
|
|
24990
|
+
"\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550",
|
|
24991
|
+
"PREFERRED PATTERN: Single thread, CC everyone, agents take turns",
|
|
24992
|
+
"\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550",
|
|
24993
|
+
"",
|
|
24994
|
+
"This is how a human boss coordinates a small team \u2014 and it is the right",
|
|
24995
|
+
"pattern for multi-agent work. One email thread is the shared workspace.",
|
|
24996
|
+
"",
|
|
24997
|
+
"1. Decide who participates.",
|
|
24998
|
+
" \u2022 `list_agents()` \u2014 find existing identities",
|
|
24999
|
+
" \u2022 `create_account({ name, role, ... })` \u2014 spawn fresh ones",
|
|
25000
|
+
"",
|
|
25001
|
+
"2. Send ONE kickoff email with all participants on To / CC:",
|
|
25002
|
+
"",
|
|
25003
|
+
" send_email({",
|
|
25004
|
+
' to: "vesper@localhost", // primary owner of step 1',
|
|
25005
|
+
' cc: "orion@localhost, claudecode@localhost", // teammates + yourself',
|
|
25006
|
+
' subject: "Build a small terminal game",',
|
|
25007
|
+
" text: [",
|
|
25008
|
+
' "Team \u2014",',
|
|
25009
|
+
' "",',
|
|
25010
|
+
' "Vesper, please design a minimal terminal game (under ~80 LOC).",',
|
|
25011
|
+
' "Reply-all with the design doc when ready.",',
|
|
25012
|
+
' "",',
|
|
25013
|
+
' "Orion, once Vesper signs off, implement it and reply-all with the code.",',
|
|
25014
|
+
' "",',
|
|
25015
|
+
' "I (the host) will watch the thread and step in if needed.",',
|
|
25016
|
+
' ].join("\\n"),',
|
|
25017
|
+
" })",
|
|
25018
|
+
"",
|
|
25019
|
+
" The mail server pushes a wake-up to every local recipient simultaneously.",
|
|
25020
|
+
" Each agent reads the thread, decides if it is THEIR turn, and either",
|
|
25021
|
+
" reply-all's to contribute or stays silent. Vesper goes first because she",
|
|
25022
|
+
" was named first; Orion stays silent until Vesper hands off; you (the",
|
|
25023
|
+
" host) see every reply land in your bridge inbox.",
|
|
25024
|
+
"",
|
|
25025
|
+
"3. Watch progress from the HOST session. The bridge inbox is yours to",
|
|
25026
|
+
" monitor \u2014 the dispatcher does NOT spawn an autonomous worker for the",
|
|
25027
|
+
" bridge (that would compete with you). Pick your monitoring style:",
|
|
25028
|
+
" \u2022 `wait_for_email({ timeout? })` \u2014 blocks the current turn (push-",
|
|
25029
|
+
" based, SSE-driven) until the next event lands in your inbox.",
|
|
25030
|
+
' Best when you want a single-shot "ping me when anything new',
|
|
25031
|
+
' arrives". Pair with `read_email` afterwards.',
|
|
25032
|
+
" \u2022 `list_inbox()` + `read_email({ uid })` \u2014 explicit poll. Best when",
|
|
25033
|
+
" the user is actively driving the conversation turn-by-turn.",
|
|
25034
|
+
" \u2022 `search_emails({ subject })` \u2014 load the full thread at any point.",
|
|
25035
|
+
" To unblock a stuck agent or change direction, just reply-all into the",
|
|
25036
|
+
" same thread.",
|
|
25037
|
+
"",
|
|
25038
|
+
'4. Done when the last hand-off (or an explicit "complete" message) lands',
|
|
25039
|
+
" in your inbox. Show the result to the user.",
|
|
25040
|
+
"",
|
|
25041
|
+
"Why this is right:",
|
|
25042
|
+
" \u2022 Every agent has FULL context every time they wake (they read the thread).",
|
|
25043
|
+
" \u2022 Turn-taking is implicit; no scheduler, no RPC ceremony.",
|
|
25044
|
+
" \u2022 The thread is searchable history. The host (you) sees everything.",
|
|
25045
|
+
" \u2022 Bringing in another teammate later is just adding them to CC.",
|
|
25046
|
+
"",
|
|
25047
|
+
"\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550",
|
|
25048
|
+
"When to use one-shot RPC instead",
|
|
25049
|
+
"\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550",
|
|
25050
|
+
"",
|
|
25051
|
+
"`call_agent({ target, task, timeout? })` is still the right tool when:",
|
|
25052
|
+
" \u2022 You need ONE structured answer from ONE agent and no multi-step work.",
|
|
25053
|
+
" \u2022 You need the result inline in your current call (not async).",
|
|
25054
|
+
' \u2022 The work is short and there is no useful "thread" to share.',
|
|
25055
|
+
"",
|
|
25056
|
+
"For multi-step / multi-agent coordination \u2014 use the thread pattern above.",
|
|
25057
|
+
"For fire-and-forget handoffs to a single agent \u2014 `message_agent` is fine.",
|
|
25058
|
+
"",
|
|
25059
|
+
"\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550",
|
|
25060
|
+
"What NOT to do (regardless of host \u2014 Claude Code, ChatGPT, Cursor, Grok)",
|
|
25061
|
+
"\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550",
|
|
25062
|
+
"",
|
|
25063
|
+
"\u2717 Do NOT spawn a native sub-agent / sub-task tool of your host and tell",
|
|
25064
|
+
' it to "act as Vesper" / "write as Orion". That produces output under',
|
|
25065
|
+
" YOUR identity, never reaches the named agent's inbox, and bypasses",
|
|
25066
|
+
" their persona, signatures, outbound guard, and audit trail.",
|
|
25067
|
+
"\u2717 Do NOT compose an agent's reply yourself in the host session and then",
|
|
25068
|
+
" `send_email` it on their behalf. Let the real agent reply from their",
|
|
25069
|
+
" own mailbox (via the thread pattern, or via call_agent for RPC).",
|
|
25070
|
+
'\u2717 Do NOT pass `_account: "<other-agent>"` to act AS another agent. That',
|
|
25071
|
+
" falsifies the From: header.",
|
|
25072
|
+
'\u2717 Do NOT serialise the work yourself ("first I call_agent Vesper, get her',
|
|
25073
|
+
' result, then I call_agent Orion with her result"). That works but it',
|
|
25074
|
+
" is fragile, slow, and burns one full Claude turn per hop. The thread",
|
|
25075
|
+
" pattern lets the agents drive their own handoffs.",
|
|
25076
|
+
"",
|
|
25077
|
+
"\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550",
|
|
25078
|
+
"Identity (`_account`) & tool surface",
|
|
25079
|
+
"\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550",
|
|
25080
|
+
"",
|
|
25081
|
+
'Every tool call accepts optional `_account: "<agent-name>"` to scope the',
|
|
25082
|
+
"call to a specific identity. From the host, omit it to use the bridge",
|
|
25083
|
+
"identity, or pass it to read/write a specific agent's mailbox directly.",
|
|
25084
|
+
'From inside an agent\'s own context, ALWAYS pass `_account: "<self>"`.',
|
|
25085
|
+
"",
|
|
25086
|
+
"Tool surface: ~62 tools across email, SMS, contacts, drafts, templates,",
|
|
25087
|
+
"rules, tags, search, scheduling, RPC. Only ~10 are pre-loaded; the rest",
|
|
25088
|
+
"are reachable via `request_tools` (discover) + `invoke` (call)."
|
|
25089
|
+
].join("\n");
|
|
24905
25090
|
function createMcpServer() {
|
|
24906
|
-
const server = new McpServer(
|
|
24907
|
-
|
|
24908
|
-
|
|
24909
|
-
|
|
24910
|
-
|
|
25091
|
+
const server = new McpServer(
|
|
25092
|
+
{
|
|
25093
|
+
name: "\u{1F380} AgenticMail",
|
|
25094
|
+
version: "0.2.30",
|
|
25095
|
+
description: "\u{1F380} AgenticMail \u2014 Email infrastructure for AI agents. By Ope Olatunji (https://github.com/agenticmail/agenticmail)"
|
|
25096
|
+
},
|
|
25097
|
+
{ instructions: SERVER_INSTRUCTIONS }
|
|
25098
|
+
);
|
|
24911
25099
|
const ACCOUNT_PROP = {
|
|
24912
25100
|
type: "string",
|
|
24913
25101
|
description: 'Optional. Override identity for THIS call: pass the AgenticMail agent name (e.g. "Fola") to authenticate as that agent. Requires AGENTICMAIL_ACCOUNT_KEYS_JSON to contain a matching key. Omit to use the default identity (AGENTICMAIL_API_KEY).'
|