@agenticmail/mcp 0.7.2 → 0.7.4

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.
Files changed (3) hide show
  1. package/README.md +5 -1
  2. package/dist/index.js +307 -116
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,4 +1,8 @@
1
- # @agenticmail/mcp
1
+ <p align="center">
2
+ <img src="https://raw.githubusercontent.com/agenticmail/agenticmail/main/docs/images/logo-200.png" alt="AgenticMail logo (pink bow)" width="180" />
3
+ </p>
4
+
5
+ <h1 align="center">@agenticmail/mcp</h1>
2
6
 
3
7
  The MCP (Model Context Protocol) server for [AgenticMail](https://github.com/agenticmail/agenticmail) — gives any MCP-compatible AI client full email and SMS capabilities.
4
8
 
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 headline coordination primitive: synchronous RPC
22387
- // to another AgenticMail agent that returns a structured result. It is
22388
- // the reason multi-agent setups exist on this platform and belongs in
22389
- // the pre-loaded toolset, not behind a request_tools indirection.
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. 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.",
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: "CC recipients (optional)" },
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: {
@@ -23133,11 +23139,20 @@ var toolDefinitions = [
23133
23139
  },
23134
23140
  {
23135
23141
  name: "wait_for_email",
23136
- 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.",
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
  },
@@ -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 search = await apiRequest("POST", "/mail/search", { seen: false });
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
- if (uids.length > 0) {
24252
- const email2 = await apiRequest("GET", `/mail/messages/${uids[0]}`);
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
- email: email2 ? {
24257
- uid: uids[0],
24258
- from: email2.from?.[0]?.address ?? "",
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
- } : null,
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({ arrived: false, reason: "SSE unavailable and no unread emails", timedOut: true });
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
- const event = JSON.parse(line.slice(6));
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();
24356
+ } catch {
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();
24331
24380
  } catch {
24332
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({ arrived: false, reason: `No email received within ${timeoutSec}s`, timedOut: true });
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
  }
@@ -24869,6 +24945,69 @@ ${lines.join("\n")}`;
24869
24945
  import { setTelemetryVersion } from "@agenticmail/core";
24870
24946
  import { createServer } from "http";
24871
24947
  import { randomUUID } from "crypto";
24948
+
24949
+ // src/coerce.ts
24950
+ function coerceToArray(value, itemKind) {
24951
+ if (Array.isArray(value)) return value;
24952
+ if (typeof value !== "string") return value;
24953
+ const trimmed = value.trim();
24954
+ if (trimmed.startsWith("[")) {
24955
+ try {
24956
+ const parsed = JSON.parse(trimmed);
24957
+ if (Array.isArray(parsed)) return parsed;
24958
+ } catch {
24959
+ return value;
24960
+ }
24961
+ }
24962
+ if (itemKind === "number" || itemKind === "integer" || itemKind === "string") {
24963
+ const parts = trimmed.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
24964
+ if (itemKind === "number" || itemKind === "integer") {
24965
+ return parts.map((s) => {
24966
+ const n = Number(s);
24967
+ return Number.isNaN(n) ? s : n;
24968
+ });
24969
+ }
24970
+ return parts;
24971
+ }
24972
+ return value;
24973
+ }
24974
+ function coerceToObject(value) {
24975
+ if (value && typeof value === "object" && !Array.isArray(value)) return value;
24976
+ if (typeof value !== "string") return value;
24977
+ const trimmed = value.trim();
24978
+ if (!trimmed.startsWith("{")) return value;
24979
+ try {
24980
+ const parsed = JSON.parse(trimmed);
24981
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return parsed;
24982
+ } catch {
24983
+ }
24984
+ return value;
24985
+ }
24986
+ function coerceToNumber(value) {
24987
+ if (typeof value === "number") return value;
24988
+ if (typeof value === "string") {
24989
+ const trimmed = value.trim();
24990
+ if (trimmed === "") return value;
24991
+ const n = Number(trimmed);
24992
+ if (!Number.isNaN(n)) return n;
24993
+ }
24994
+ return value;
24995
+ }
24996
+ function coerceToBoolean(value) {
24997
+ if (typeof value === "boolean") return value;
24998
+ if (typeof value === "string") {
24999
+ const v = value.trim().toLowerCase();
25000
+ if (v === "true" || v === "yes" || v === "1") return true;
25001
+ if (v === "false" || v === "no" || v === "0") return false;
25002
+ }
25003
+ if (typeof value === "number") {
25004
+ if (value === 1) return true;
25005
+ if (value === 0) return false;
25006
+ }
25007
+ return value;
25008
+ }
25009
+
25010
+ // src/index.ts
24872
25011
  setTelemetryVersion("0.5.55");
24873
25012
  function jsonSchemaToZod(schema, topLevel = false) {
24874
25013
  if (!schema || typeof schema !== "object") return topLevel ? {} : external_exports.any();
@@ -24876,16 +25015,18 @@ function jsonSchemaToZod(schema, topLevel = false) {
24876
25015
  if (schema.type === "string") {
24877
25016
  result = schema.enum?.length ? external_exports.enum(schema.enum) : external_exports.string();
24878
25017
  } else if (schema.type === "number" || schema.type === "integer") {
24879
- result = external_exports.number();
25018
+ result = external_exports.preprocess(coerceToNumber, external_exports.number());
24880
25019
  } else if (schema.type === "boolean") {
24881
- result = external_exports.boolean();
25020
+ result = external_exports.preprocess(coerceToBoolean, external_exports.boolean());
24882
25021
  } else if (schema.type === "array") {
24883
25022
  const hasItems = !!schema.items && typeof schema.items === "object" && Object.keys(schema.items).length > 0;
24884
- result = external_exports.array(hasItems ? jsonSchemaToZod(schema.items, false) : external_exports.any());
25023
+ const inner = external_exports.array(hasItems ? jsonSchemaToZod(schema.items, false) : external_exports.any());
25024
+ const itemKind = schema.items?.type;
25025
+ result = external_exports.preprocess((v) => coerceToArray(v, itemKind), inner);
24885
25026
  } else if (schema.type === "object") {
24886
25027
  if (!schema.properties || Object.keys(schema.properties).length === 0) {
24887
25028
  if (topLevel) return {};
24888
- result = external_exports.record(external_exports.string(), external_exports.any());
25029
+ result = external_exports.preprocess(coerceToObject, external_exports.record(external_exports.string(), external_exports.any()));
24889
25030
  } else {
24890
25031
  const shape = {};
24891
25032
  const required2 = new Set(schema.required ?? []);
@@ -24895,7 +25036,7 @@ function jsonSchemaToZod(schema, topLevel = false) {
24895
25036
  shape[key] = child;
24896
25037
  }
24897
25038
  if (topLevel) return shape;
24898
- result = external_exports.object(shape);
25039
+ result = external_exports.preprocess(coerceToObject, external_exports.object(shape));
24899
25040
  }
24900
25041
  } else {
24901
25042
  result = external_exports.any();
@@ -24905,57 +25046,107 @@ function jsonSchemaToZod(schema, topLevel = false) {
24905
25046
  var SERVER_INSTRUCTIONS = [
24906
25047
  "\u{1F380} AgenticMail \u2014 multi-agent email + SMS infrastructure.",
24907
25048
  "",
24908
- "AgenticMail agents are NOT prompts you wrap around your host's native",
24909
- "sub-agent tool. Each AgenticMail agent is a persistent identity with its",
24910
- "own inbox, API key, persona, and audit trail. Address other agents through",
24911
- "AgenticMail's own primitives below \u2014 never roleplay them inside your host.",
25049
+ "AgenticMail agents are persistent identities with their own inboxes, API",
25050
+ "keys, personas, and audit trails. They coordinate the way humans do: in",
25051
+ "email threads, with every participant CC'd, taking turns implicitly from",
25052
+ "context. Address other agents through AgenticMail's own primitives \u2014",
25053
+ "never roleplay them inside your host's native sub-agent / sub-task tool.",
25054
+ "",
25055
+ "\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550",
25056
+ "PREFERRED PATTERN: Single thread, CC everyone, agents take turns",
25057
+ "\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550",
25058
+ "",
25059
+ "This is how a human boss coordinates a small team \u2014 and it is the right",
25060
+ "pattern for multi-agent work. One email thread is the shared workspace.",
25061
+ "",
25062
+ "1. Decide who participates.",
25063
+ " \u2022 `list_agents()` \u2014 find existing identities",
25064
+ " \u2022 `create_account({ name, role, ... })` \u2014 spawn fresh ones",
25065
+ "",
25066
+ "2. Send ONE kickoff email with all participants on To / CC:",
25067
+ "",
25068
+ " send_email({",
25069
+ ' to: "vesper@localhost", // primary owner of step 1',
25070
+ ' cc: "orion@localhost, claudecode@localhost", // teammates + yourself',
25071
+ ' subject: "Build a small terminal game",',
25072
+ " text: [",
25073
+ ' "Team \u2014",',
25074
+ ' "",',
25075
+ ' "Vesper, please design a minimal terminal game (under ~80 LOC).",',
25076
+ ' "Reply-all with the design doc when ready.",',
25077
+ ' "",',
25078
+ ' "Orion, once Vesper signs off, implement it and reply-all with the code.",',
25079
+ ' "",',
25080
+ ' "I (the host) will watch the thread and step in if needed.",',
25081
+ ' ].join("\\n"),',
25082
+ " })",
25083
+ "",
25084
+ " The mail server pushes a wake-up to every local recipient simultaneously.",
25085
+ " Each agent reads the thread, decides if it is THEIR turn, and either",
25086
+ " reply-all's to contribute or stays silent. Vesper goes first because she",
25087
+ " was named first; Orion stays silent until Vesper hands off; you (the",
25088
+ " host) see every reply land in your bridge inbox.",
25089
+ "",
25090
+ "3. Watch progress from the HOST session. The bridge inbox is yours to",
25091
+ " monitor \u2014 the dispatcher does NOT spawn an autonomous worker for the",
25092
+ " bridge (that would compete with you). Pick your monitoring style:",
25093
+ " \u2022 `wait_for_email({ timeout? })` \u2014 blocks the current turn (push-",
25094
+ " based, SSE-driven) until the next event lands in your inbox.",
25095
+ ' Best when you want a single-shot "ping me when anything new',
25096
+ ' arrives". Pair with `read_email` afterwards.',
25097
+ " \u2022 `list_inbox()` + `read_email({ uid })` \u2014 explicit poll. Best when",
25098
+ " the user is actively driving the conversation turn-by-turn.",
25099
+ " \u2022 `search_emails({ subject })` \u2014 load the full thread at any point.",
25100
+ " To unblock a stuck agent or change direction, just reply-all into the",
25101
+ " same thread.",
25102
+ "",
25103
+ '4. Done when the last hand-off (or an explicit "complete" message) lands',
25104
+ " in your inbox. Show the result to the user.",
25105
+ "",
25106
+ "Why this is right:",
25107
+ " \u2022 Every agent has FULL context every time they wake (they read the thread).",
25108
+ " \u2022 Turn-taking is implicit; no scheduler, no RPC ceremony.",
25109
+ " \u2022 The thread is searchable history. The host (you) sees everything.",
25110
+ " \u2022 Bringing in another teammate later is just adding them to CC.",
25111
+ "",
25112
+ "\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550",
25113
+ "When to use one-shot RPC instead",
25114
+ "\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550",
24912
25115
  "",
24913
- "How to coordinate work between agents \u2014 the right way:",
25116
+ "`call_agent({ target, task, timeout? })` is still the right tool when:",
25117
+ " \u2022 You need ONE structured answer from ONE agent and no multi-step work.",
25118
+ " \u2022 You need the result inline in your current call (not async).",
25119
+ ' \u2022 The work is short and there is no useful "thread" to share.',
24914
25120
  "",
24915
- "1. List or create the agents you need (`list_agents`, `create_account`).",
24916
- "2. Send them work using ONE of:",
24917
- " \u2022 `call_agent({ target, task, payload?, timeout? })` \u2014 SYNCHRONOUS RPC.",
24918
- " Fires a structured task at the target agent and waits for their",
24919
- " result. The target processes the task as themselves (under their",
24920
- " own identity and mailbox) and the structured result returns into",
24921
- " your call. Use this when you need an answer back.",
24922
- ' \u2022 `send_email({ to: "<name>@localhost", ... })` or',
24923
- " `message_agent({ agent, subject, text })` \u2014 ASYNCHRONOUS.",
24924
- " Mail lands in their inbox; they process it on their own schedule",
24925
- " and may reply by email. Use this for fire-and-forget handoffs.",
24926
- "3. Read replies with `list_inbox` + `read_email`, or `search_emails`.",
25121
+ "For multi-step / multi-agent coordination \u2014 use the thread pattern above.",
25122
+ "For fire-and-forget handoffs to a single agent \u2014 `message_agent` is fine.",
24927
25123
  "",
24928
- "What NOT to do (regardless of host \u2014 Claude Code, ChatGPT, Cursor, Grok, \u2026):",
25124
+ "\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550",
25125
+ "What NOT to do (regardless of host \u2014 Claude Code, ChatGPT, Cursor, Grok)",
25126
+ "\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550",
24929
25127
  "",
24930
- "\u2717 Do NOT spawn a native sub-agent / sub-task / parallel-thinking tool of",
24931
- ' your host and instruct it to "act as Lyra" / "write as Orion". That',
24932
- " produces output under YOUR identity, never reaches the named agent's",
24933
- " inbox, and bypasses their persona, signatures, outbound guard, and",
24934
- " audit trail. The real agent has not actually thought anything.",
25128
+ "\u2717 Do NOT spawn a native sub-agent / sub-task tool of your host and tell",
25129
+ ' it to "act as Vesper" / "write as Orion". That produces output under',
25130
+ " YOUR identity, never reaches the named agent's inbox, and bypasses",
25131
+ " their persona, signatures, outbound guard, and audit trail.",
24935
25132
  "\u2717 Do NOT compose an agent's reply yourself in the host session and then",
24936
- " `send_email` it on their behalf. Let `call_agent` (or an email handoff)",
24937
- " produce the reply from the real agent.",
25133
+ " `send_email` it on their behalf. Let the real agent reply from their",
25134
+ " own mailbox (via the thread pattern, or via call_agent for RPC).",
24938
25135
  '\u2717 Do NOT pass `_account: "<other-agent>"` to act AS another agent. That',
24939
- " falsifies the From: header. Use `call_agent` to delegate instead.",
25136
+ " falsifies the From: header.",
25137
+ '\u2717 Do NOT serialise the work yourself ("first I call_agent Vesper, get her',
25138
+ ' result, then I call_agent Orion with her result"). That works but it',
25139
+ " is fragile, slow, and burns one full Claude turn per hop. The thread",
25140
+ " pattern lets the agents drive their own handoffs.",
24940
25141
  "",
24941
- "How the wake-up actually happens (host-dependent \u2014 best-effort):",
24942
- " - With the Claude Code integration installed, a dispatcher daemon",
24943
- " auto-wakes the target agent as a Claude Code subagent when mail",
24944
- " arrives or `call_agent` fires.",
24945
- " - With other MCP hosts (no dispatcher), `send_email` / `message_agent`",
24946
- " still deliver the mail; a human or another scheduled process picks",
24947
- " it up later. `call_agent` will still queue the task and return its",
24948
- " result once any worker (yourself, a cron, another agent) processes",
24949
- " it \u2014 see `check_tasks` / `claim_task` / `submit_result`.",
24950
- " Either way, the RIGHT primitives from your side are the same:",
24951
- " call_agent / send_email / message_agent. Never roleplay.",
25142
+ "\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550",
25143
+ "Identity (`_account`) & tool surface",
25144
+ "\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550",
24952
25145
  "",
24953
- "Identity & `_account`:",
24954
- ' Every tool call accepts an optional `_account: "<your-agent-name>"` to',
24955
- " scope the call to a specific agent. From the HOST session, omit it to",
24956
- " use bridge identity, or pass it to read/write a specific agent's",
24957
- " mailbox directly. From inside an agent's own context, ALWAYS pass",
24958
- ' `_account: "<self>"`.',
25146
+ 'Every tool call accepts optional `_account: "<agent-name>"` to scope the',
25147
+ "call to a specific identity. From the host, omit it to use the bridge",
25148
+ "identity, or pass it to read/write a specific agent's mailbox directly.",
25149
+ 'From inside an agent\'s own context, ALWAYS pass `_account: "<self>"`.',
24959
25150
  "",
24960
25151
  "Tool surface: ~62 tools across email, SMS, contacts, drafts, templates,",
24961
25152
  "rules, tags, search, scheduling, RPC. Only ~10 are pre-loaded; the rest",
@@ -24965,7 +25156,7 @@ function createMcpServer() {
24965
25156
  const server = new McpServer(
24966
25157
  {
24967
25158
  name: "\u{1F380} AgenticMail",
24968
- version: "0.2.29",
25159
+ version: "0.2.30",
24969
25160
  description: "\u{1F380} AgenticMail \u2014 Email infrastructure for AI agents. By Ope Olatunji (https://github.com/agenticmail/agenticmail)"
24970
25161
  },
24971
25162
  { instructions: SERVER_INSTRUCTIONS }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agenticmail/mcp",
3
- "version": "0.7.2",
3
+ "version": "0.7.4",
4
4
  "mcpName": "io.github.agenticmail/mcp",
5
5
  "description": "MCP server for AgenticMail — give any AI client real email and SMS capabilities",
6
6
  "type": "module",