hypermail-mcp 0.7.16 → 0.7.18

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 CHANGED
@@ -3,6 +3,19 @@
3
3
  A **Model Context Protocol** server that lets an agent operate any of the user's
4
4
  inboxes through a single, unified tool surface.
5
5
 
6
+ > **v0.7.18** — `search_emails` can now omit `account` to search all
7
+ > registered accounts in parallel, returning account-annotated results plus
8
+ > per-account errors without discarding successful matches. All-account
9
+ > `get_new_emails` polling also collects and hydrates per-account batches in
10
+ > parallel while preserving the global oldest-first limit.
11
+ >
12
+ > **v0.7.17** — Hardened Outlook message IDs returned by `list_emails` and
13
+ > `search_emails`: Outlook reads now request Microsoft Graph immutable IDs,
14
+ > `search_emails` probes results and marks stale/not-found messages with
15
+ > `stale: true`, and `read_email` / `read_attachment` keep a legacy mutable-ID
16
+ > fallback for older IDs. Graph errors now include structured diagnostic details
17
+ > when available.
18
+ >
6
19
  > **v0.7.16** — Hardened `draft_email` after successful draft creation:
7
20
  > if post-save readback fails, the tool now returns the created draft ID with
8
21
  > `warning` and `draftReadbackError` instead of losing the draft behind an
@@ -284,7 +297,7 @@ account store.
284
297
  | `remove_account` | `email` | Deletes tokens for the account. |
285
298
  | `list_emails` | `account`, `folder?`, `limit?`, `unreadOnly?`, `skip?` | Defaults: folder=`inbox`, limit=25. Supports pagination via `skip` — response includes `hasMore`. |
286
299
  | `get_new_emails` | `account?`, `limit?` | Pull new inbox emails not previously returned by this tool. `limit` defaults to 10 and is global when `account` is omitted. Returns full markdown bodies with attachment metadata; bodies may be truncated. |
287
- | `search_emails` | `account`, `query`, `limit?` | KQL on Outlook. |
300
+ | `search_emails` | `account?`, `query`, `limit?` | Search one account when `account` is provided, or omit it to search all registered accounts in parallel. Returns account-annotated summaries and partial per-account errors. Uses KQL on Outlook. |
288
301
  | `read_email` | `account`, `id`, `format?` | Returns full body + recipients + attachment metadata. `format`: `markdown` (default), `html`, or `text`. |
289
302
  | `read_attachment` | `account`, `messageId`, `attachmentId` | Download an attachment to a temporary file and return its path. |
290
303
  | `archive_email` | `account`, `id` | Move a message to the Archive folder. |
@@ -312,6 +325,8 @@ tool on their own schedule, for example every 30–60 seconds.
312
325
  - `account` is optional. When omitted, the tool checks all registered accounts.
313
326
  - `limit` defaults to `10`. In all-account mode, the limit is a global total
314
327
  across accounts, selected by oldest `receivedAt` first.
328
+ - All-account polling performs per-account candidate collection and hydration in
329
+ parallel, then returns the combined batch in oldest-first order.
315
330
  - First use for an account initializes its checkpoint to the newest inbox email
316
331
  and returns no emails for that account.
317
332
  - Later calls return emails not previously returned by this tool, oldest first.
package/dist/cli.js CHANGED
@@ -14846,21 +14846,122 @@ import { ResponseType } from "@microsoft/microsoft-graph-client";
14846
14846
  import { writeFileSync } from "fs";
14847
14847
  import { tmpdir } from "os";
14848
14848
  import { join as pathJoin } from "path";
14849
+ var OUTLOOK_IMMUTABLE_ID_PREFER = 'IdType="ImmutableId"';
14850
+ var MESSAGE_SELECT = [
14851
+ "id",
14852
+ "subject",
14853
+ "from",
14854
+ "toRecipients",
14855
+ "receivedDateTime",
14856
+ "bodyPreview",
14857
+ "isRead",
14858
+ "hasAttachments"
14859
+ ].join(",");
14860
+ var FULL_MESSAGE_SELECT = [
14861
+ "id",
14862
+ "subject",
14863
+ "from",
14864
+ "toRecipients",
14865
+ "ccRecipients",
14866
+ "bccRecipients",
14867
+ "receivedDateTime",
14868
+ "bodyPreview",
14869
+ "isRead",
14870
+ "hasAttachments",
14871
+ "body"
14872
+ ].join(",");
14873
+ function graphErrorFields(err) {
14874
+ if (typeof err !== "object" || err === null) {
14875
+ return [String(err)];
14876
+ }
14877
+ const record2 = err;
14878
+ const fields = [];
14879
+ for (const key of ["code", "statusCode", "message"]) {
14880
+ const value = record2[key];
14881
+ if (typeof value === "string" || typeof value === "number") fields.push(value);
14882
+ }
14883
+ if (typeof record2.body === "string") {
14884
+ try {
14885
+ const parsed = JSON.parse(record2.body);
14886
+ if (typeof parsed === "object" && parsed !== null) {
14887
+ const graphError = parsed.error;
14888
+ if (typeof graphError === "object" && graphError !== null) {
14889
+ const graphRecord = graphError;
14890
+ for (const key of ["code", "message"]) {
14891
+ const value = graphRecord[key];
14892
+ if (typeof value === "string" || typeof value === "number") {
14893
+ fields.push(value);
14894
+ }
14895
+ }
14896
+ }
14897
+ }
14898
+ } catch {
14899
+ fields.push(record2.body);
14900
+ }
14901
+ }
14902
+ return fields;
14903
+ }
14904
+ function isStaleMessageIdError(err) {
14905
+ const fields = graphErrorFields(err);
14906
+ if (fields.some((value) => value === 404)) return true;
14907
+ return fields.some((value) => {
14908
+ if (typeof value !== "string") return false;
14909
+ const normalized = value.toLowerCase();
14910
+ return normalized.includes("erroritemnotfound") || normalized.includes("invalidid") || normalized.includes("object was not found in the store") || normalized.includes("not found in the store");
14911
+ });
14912
+ }
14913
+ async function probeMessage(client, id, useImmutableIds) {
14914
+ let req = client.api(`/me/messages/${encodeURIComponent(id)}`).select("id");
14915
+ if (useImmutableIds) req = req.header("Prefer", OUTLOOK_IMMUTABLE_ID_PREFER);
14916
+ await req.get();
14917
+ }
14918
+ async function probeSearchResult(client, id) {
14919
+ try {
14920
+ await probeMessage(client, id, true);
14921
+ return true;
14922
+ } catch (err) {
14923
+ if (!isStaleMessageIdError(err)) throw err;
14924
+ }
14925
+ try {
14926
+ await probeMessage(client, id, false);
14927
+ return true;
14928
+ } catch (err) {
14929
+ if (isStaleMessageIdError(err)) return false;
14930
+ throw err;
14931
+ }
14932
+ }
14933
+ async function getMessage(client, id) {
14934
+ try {
14935
+ const message2 = await client.api(`/me/messages/${encodeURIComponent(id)}`).header("Prefer", OUTLOOK_IMMUTABLE_ID_PREFER).select(FULL_MESSAGE_SELECT).get();
14936
+ return { message: message2, useImmutableIds: true };
14937
+ } catch (err) {
14938
+ if (!isStaleMessageIdError(err)) throw err;
14939
+ }
14940
+ const message = await client.api(`/me/messages/${encodeURIComponent(id)}`).select(FULL_MESSAGE_SELECT).get();
14941
+ return { message, useImmutableIds: false };
14942
+ }
14943
+ async function listAttachments(client, messageId, useImmutableIds) {
14944
+ let req = client.api(`/me/messages/${encodeURIComponent(messageId)}/attachments`).select("id,name,contentType,size");
14945
+ if (useImmutableIds) req = req.header("Prefer", OUTLOOK_IMMUTABLE_ID_PREFER);
14946
+ const attRes = await req.get();
14947
+ return attRes.value;
14948
+ }
14949
+ async function getAttachmentMetadata(client, messageId, attachmentId, useImmutableIds) {
14950
+ let req = client.api(`/me/messages/${encodeURIComponent(messageId)}/attachments/${encodeURIComponent(attachmentId)}`).select("name,contentType");
14951
+ if (useImmutableIds) req = req.header("Prefer", OUTLOOK_IMMUTABLE_ID_PREFER);
14952
+ return await req.get();
14953
+ }
14954
+ async function getAttachmentValue(client, messageId, attachmentId, useImmutableIds) {
14955
+ let req = client.api(`/me/messages/${encodeURIComponent(messageId)}/attachments/${encodeURIComponent(attachmentId)}/$value`).responseType(ResponseType.ARRAYBUFFER);
14956
+ if (useImmutableIds) req = req.header("Prefer", OUTLOOK_IMMUTABLE_ID_PREFER);
14957
+ return await req.get();
14958
+ }
14849
14959
  async function listEmails(client, account, opts) {
14850
14960
  const limit = clampLimit(opts.limit, 25, 100);
14851
14961
  const folder = opts.folder ?? "inbox";
14852
14962
  const filterParts = [];
14853
14963
  if (opts.unreadOnly) filterParts.push("isRead eq false");
14854
- let req = client.api(`/me/mailFolders/${encodeURIComponent(folder)}/messages`).top(limit).skip(opts.skip ?? 0).select([
14855
- "id",
14856
- "subject",
14857
- "from",
14858
- "toRecipients",
14859
- "receivedDateTime",
14860
- "bodyPreview",
14861
- "isRead",
14862
- "hasAttachments"
14863
- ].join(",")).orderby("receivedDateTime DESC");
14964
+ let req = client.api(`/me/mailFolders/${encodeURIComponent(folder)}/messages`).header("Prefer", OUTLOOK_IMMUTABLE_ID_PREFER).top(limit).skip(opts.skip ?? 0).select(MESSAGE_SELECT).orderby("receivedDateTime DESC");
14864
14965
  if (filterParts.length > 0) req = req.filter(filterParts.join(" and "));
14865
14966
  const res = await req.get();
14866
14967
  return {
@@ -14870,41 +14971,22 @@ async function listEmails(client, account, opts) {
14870
14971
  }
14871
14972
  async function searchEmails(client, account, query, opts) {
14872
14973
  const limit = clampLimit(opts.limit, 25, 100);
14873
- const res = await client.api("/me/messages").header("ConsistencyLevel", "eventual").top(limit).search(`"${query.replace(/"/g, '\\"')}"`).select(
14874
- [
14875
- "id",
14876
- "subject",
14877
- "from",
14878
- "toRecipients",
14879
- "receivedDateTime",
14880
- "bodyPreview",
14881
- "isRead",
14882
- "hasAttachments"
14883
- ].join(",")
14884
- ).get();
14885
- return res.value.map((m) => mapSummary(m));
14974
+ const res = await client.api("/me/messages").header("ConsistencyLevel", "eventual").header("Prefer", OUTLOOK_IMMUTABLE_ID_PREFER).top(limit).search(`"${query.replace(/"/g, '\\"')}"`).select(MESSAGE_SELECT).get();
14975
+ const summaries = res.value.map((m) => mapSummary(m));
14976
+ return Promise.all(
14977
+ summaries.map(async (summary) => {
14978
+ const readable = await probeSearchResult(client, summary.id);
14979
+ return readable ? summary : { ...summary, stale: true };
14980
+ })
14981
+ );
14886
14982
  }
14887
14983
  async function readEmail(client, account, id) {
14888
- const m = await client.api(`/me/messages/${encodeURIComponent(id)}`).select(
14889
- [
14890
- "id",
14891
- "subject",
14892
- "from",
14893
- "toRecipients",
14894
- "ccRecipients",
14895
- "bccRecipients",
14896
- "receivedDateTime",
14897
- "bodyPreview",
14898
- "isRead",
14899
- "hasAttachments",
14900
- "body"
14901
- ].join(",")
14902
- ).get();
14984
+ const { message: m, useImmutableIds } = await getMessage(client, id);
14903
14985
  let attachments = void 0;
14904
14986
  if (m.hasAttachments) {
14905
14987
  try {
14906
- const attRes = await client.api(`/me/messages/${encodeURIComponent(id)}/attachments`).select("id,name,contentType,size").get();
14907
- attachments = attRes.value.map((a) => ({
14988
+ const attRes = await listAttachments(client, m.id, useImmutableIds);
14989
+ attachments = attRes.map((a) => ({
14908
14990
  id: a.id,
14909
14991
  name: a.name,
14910
14992
  contentType: a.contentType,
@@ -14925,8 +15007,22 @@ async function readEmail(client, account, id) {
14925
15007
  };
14926
15008
  }
14927
15009
  async function readAttachment(client, account, messageId, attachmentId) {
14928
- const att = await client.api(`/me/messages/${encodeURIComponent(messageId)}/attachments/${encodeURIComponent(attachmentId)}`).select("name,contentType").get();
14929
- const data = await client.api(`/me/messages/${encodeURIComponent(messageId)}/attachments/${encodeURIComponent(attachmentId)}/$value`).responseType(ResponseType.ARRAYBUFFER).get();
15010
+ let useImmutableIds = true;
15011
+ let att;
15012
+ try {
15013
+ att = await getAttachmentMetadata(client, messageId, attachmentId, true);
15014
+ } catch (err) {
15015
+ if (!isStaleMessageIdError(err)) throw err;
15016
+ useImmutableIds = false;
15017
+ att = await getAttachmentMetadata(client, messageId, attachmentId, false);
15018
+ }
15019
+ let data;
15020
+ try {
15021
+ data = await getAttachmentValue(client, messageId, attachmentId, useImmutableIds);
15022
+ } catch (err) {
15023
+ if (!useImmutableIds || !isStaleMessageIdError(err)) throw err;
15024
+ data = await getAttachmentValue(client, messageId, attachmentId, false);
15025
+ }
14930
15026
  const outPath = pathJoin(tmpdir(), att.name);
14931
15027
  writeFileSync(outPath, Buffer.from(data));
14932
15028
  return {
@@ -17229,12 +17325,41 @@ function fail(message) {
17229
17325
  content: [{ type: "text", text: message }]
17230
17326
  };
17231
17327
  }
17328
+ function graphErrorDetails(err) {
17329
+ if (typeof err !== "object" || err === null) return void 0;
17330
+ const record2 = err;
17331
+ const details = {};
17332
+ for (const key of ["code", "statusCode", "requestId", "date", "innerError"]) {
17333
+ if (record2[key] !== void 0) details[key] = record2[key];
17334
+ }
17335
+ if (typeof record2.body === "string") {
17336
+ try {
17337
+ const parsed = JSON.parse(record2.body);
17338
+ if (typeof parsed === "object" && parsed !== null) {
17339
+ const graphError = parsed.error;
17340
+ if (typeof graphError === "object" && graphError !== null) {
17341
+ const graphRecord = graphError;
17342
+ for (const key of ["code", "message", "innerError"]) {
17343
+ if (graphRecord[key] !== void 0 && details[key] === void 0) {
17344
+ details[key] = graphRecord[key];
17345
+ }
17346
+ }
17347
+ }
17348
+ }
17349
+ } catch {
17350
+ }
17351
+ }
17352
+ return Object.keys(details).length > 0 ? details : void 0;
17353
+ }
17232
17354
  function errMsg(err) {
17233
17355
  const message = err instanceof Error ? err.message : String(err);
17234
17356
  if (message.includes("HYPERMAIL_GMAIL_CLIENT_ID")) {
17235
17357
  return "Missing Gmail OAuth configuration: set HYPERMAIL_GMAIL_CLIENT_ID before adding a Gmail account.";
17236
17358
  }
17237
- return message;
17359
+ const details = graphErrorDetails(err);
17360
+ if (!details) return message;
17361
+ return `${message}
17362
+ ${JSON.stringify({ error: details }, null, 2)}`;
17238
17363
  }
17239
17364
  var providerIdEnum = z2.enum(["outlook", "imap", "gmail"]);
17240
17365
  var emailAddrSchema = z2.object({
@@ -17294,7 +17419,8 @@ var emailSummaryOutputSchema = z2.object({
17294
17419
  preview: z2.string().optional(),
17295
17420
  isRead: z2.boolean().optional(),
17296
17421
  hasAttachments: z2.boolean().optional(),
17297
- folder: z2.string().optional()
17422
+ folder: z2.string().optional(),
17423
+ stale: z2.boolean().optional()
17298
17424
  });
17299
17425
  var attachmentMetaOutputSchema = z2.object({
17300
17426
  id: z2.string(),
@@ -17713,20 +17839,36 @@ function registerNewEmailTool(server, ctx) {
17713
17839
  const collected = [];
17714
17840
  const providersByEmail = /* @__PURE__ */ new Map();
17715
17841
  const accountsByEmail = /* @__PURE__ */ new Map();
17716
- for (const stored of accounts) {
17717
- try {
17718
- const { provider, account } = registry.resolveByEmail(stored.email);
17719
- const result = await collectCandidatesForAccount(store, provider, account, logger);
17720
- providersByEmail.set(result.account.email, provider);
17721
- accountsByEmail.set(result.account.email, result.account);
17722
- collected.push(...result.candidates);
17723
- } catch (err) {
17724
- logger.debug("get-new-emails", "accountError", {
17725
- account: stored.email,
17726
- message: errMsg(err)
17727
- });
17728
- errors.push({ account: stored.email, message: errMsg(err) });
17842
+ const collectedByAccount = await Promise.all(
17843
+ accounts.map(async (stored) => {
17844
+ try {
17845
+ const { provider, account } = registry.resolveByEmail(stored.email);
17846
+ const result = await collectCandidatesForAccount(store, provider, account, logger);
17847
+ return {
17848
+ status: "ok",
17849
+ account: result.account,
17850
+ provider,
17851
+ candidates: result.candidates
17852
+ };
17853
+ } catch (err) {
17854
+ const message = errMsg(err);
17855
+ logger.debug("get-new-emails", "accountError", { account: stored.email, message });
17856
+ return {
17857
+ status: "error",
17858
+ account: stored.email,
17859
+ message
17860
+ };
17861
+ }
17862
+ })
17863
+ );
17864
+ for (const result of collectedByAccount) {
17865
+ if (result.status === "error") {
17866
+ errors.push({ account: result.account, message: result.message });
17867
+ continue;
17729
17868
  }
17869
+ providersByEmail.set(result.account.email, result.provider);
17870
+ accountsByEmail.set(result.account.email, result.account);
17871
+ collected.push(...result.candidates);
17730
17872
  }
17731
17873
  const selected = oldestCandidatesFirst(collected).slice(0, limit);
17732
17874
  logger.debug("get-new-emails", "selected", {
@@ -17745,27 +17887,33 @@ function registerNewEmailTool(server, ctx) {
17745
17887
  items.push(candidate);
17746
17888
  byAccount.set(candidate.account.email, items);
17747
17889
  }
17748
- for (const [email, accountCandidates] of byAccount) {
17749
- const provider = providersByEmail.get(email);
17750
- const account = accountsByEmail.get(email);
17751
- if (!provider || !account) continue;
17752
- try {
17753
- emails.push(
17754
- ...await hydrateAndAdvance(
17755
- store,
17756
- provider,
17757
- account,
17758
- accountCandidates,
17759
- logger
17760
- )
17761
- );
17762
- } catch (err) {
17763
- logger.debug("get-new-emails", "accountError", {
17764
- account: email,
17765
- message: errMsg(err)
17766
- });
17767
- errors.push({ account: email, message: errMsg(err) });
17890
+ const hydratedByAccount = await Promise.all(
17891
+ [...byAccount].map(async ([email, accountCandidates]) => {
17892
+ const provider = providersByEmail.get(email);
17893
+ const account = accountsByEmail.get(email);
17894
+ if (!provider || !account) return { status: "ok", emails: [] };
17895
+ try {
17896
+ return {
17897
+ status: "ok",
17898
+ emails: await hydrateAndAdvance(store, provider, account, accountCandidates, logger)
17899
+ };
17900
+ } catch (err) {
17901
+ const message = errMsg(err);
17902
+ logger.debug("get-new-emails", "accountError", { account: email, message });
17903
+ return {
17904
+ status: "error",
17905
+ account: email,
17906
+ message
17907
+ };
17908
+ }
17909
+ })
17910
+ );
17911
+ for (const result of hydratedByAccount) {
17912
+ if (result.status === "error") {
17913
+ errors.push({ account: result.account, message: result.message });
17914
+ continue;
17768
17915
  }
17916
+ emails.push(...result.emails);
17769
17917
  }
17770
17918
  }
17771
17919
  const orderedEmails = emails.sort(compareNewEmailOutputOldestFirst);
@@ -17956,10 +18104,15 @@ function registerBrowseTools(server, ctx) {
17956
18104
  skip: z5.number(),
17957
18105
  hasMore: z5.boolean()
17958
18106
  });
18107
+ const searchEmailSummaryOutputSchema = emailSummaryOutputSchema.extend({
18108
+ account: z5.string()
18109
+ });
17959
18110
  const searchEmailsOutputSchema = z5.object({
17960
18111
  account: z5.string(),
17961
18112
  count: z5.number(),
17962
- items: z5.array(emailSummaryOutputSchema)
18113
+ items: z5.array(searchEmailSummaryOutputSchema),
18114
+ accounts: z5.array(z5.string()).optional(),
18115
+ errors: z5.array(z5.object({ account: z5.string(), message: z5.string() })).optional()
17963
18116
  });
17964
18117
  if (shouldRegister("list_emails", tools)) {
17965
18118
  server.registerTool(
@@ -18003,29 +18156,67 @@ function registerBrowseTools(server, ctx) {
18003
18156
  server.registerTool(
18004
18157
  "search_emails",
18005
18158
  {
18006
- description: "Search emails by free-text query (KQL on Outlook). Returns lightweight summaries.",
18159
+ description: "Search emails by free-text query (KQL on Outlook). Returns lightweight summaries. Pass `account` to search one account, or omit it to search all registered accounts in parallel.",
18007
18160
  inputSchema: z5.object({
18008
- account: z5.string().email(),
18161
+ account: z5.string().email().optional(),
18009
18162
  query: z5.string().min(1),
18010
18163
  limit: z5.number().int().positive().max(100).optional()
18011
18164
  }),
18012
18165
  outputSchema: searchEmailsOutputSchema
18013
18166
  },
18014
18167
  async (args) => {
18015
- try {
18016
- const { provider, account } = registry.resolveByEmail(args.account);
18017
- const items = await provider.searchEmails(account, args.query, {
18018
- limit: args.limit
18019
- });
18020
- const data = {
18021
- account: account.email,
18022
- count: items.length,
18023
- items
18024
- };
18025
- return ok(data, data);
18026
- } catch (err) {
18027
- return fail(errMsg(err));
18168
+ if (args.account) {
18169
+ try {
18170
+ const { provider, account } = registry.resolveByEmail(args.account);
18171
+ const items2 = await provider.searchEmails(account, args.query, {
18172
+ limit: args.limit
18173
+ });
18174
+ const data2 = {
18175
+ account: account.email,
18176
+ count: items2.length,
18177
+ items: items2.map((item) => ({ ...item, account: account.email })),
18178
+ errors: []
18179
+ };
18180
+ return ok(data2, data2);
18181
+ } catch (err) {
18182
+ return fail(errMsg(err));
18183
+ }
18184
+ }
18185
+ const accounts = store.listAccounts();
18186
+ if (accounts.length === 0) {
18187
+ return fail("no accounts registered. Call add_account first.");
18028
18188
  }
18189
+ const results = await Promise.all(
18190
+ accounts.map(async (stored) => {
18191
+ try {
18192
+ const { provider, account } = registry.resolveByEmail(stored.email);
18193
+ const items2 = await provider.searchEmails(account, args.query, {
18194
+ limit: args.limit
18195
+ });
18196
+ return {
18197
+ account: account.email,
18198
+ items: items2.map((item) => ({ ...item, account: account.email }))
18199
+ };
18200
+ } catch (err) {
18201
+ return {
18202
+ account: stored.email,
18203
+ error: errMsg(err)
18204
+ };
18205
+ }
18206
+ })
18207
+ );
18208
+ const items = results.flatMap((result) => result.items ?? []);
18209
+ const errors = results.filter(
18210
+ (result) => "error" in result
18211
+ ).map((result) => ({ account: result.account, message: result.error }));
18212
+ const data = {
18213
+ account: "all",
18214
+ accounts: accounts.map((account) => account.email),
18215
+ count: items.length,
18216
+ items,
18217
+ errors
18218
+ };
18219
+ return ok(data, data);
18029
18220
  }
18030
18221
  );
18031
18222
  }
@@ -18873,7 +19064,7 @@ function registerTools(server, opts) {
18873
19064
  // package.json
18874
19065
  var package_default = {
18875
19066
  name: "hypermail-mcp",
18876
- version: "0.7.16",
19067
+ version: "0.7.18",
18877
19068
  description: "Unified email MCP server \u2014 operate any inbox (Outlook now, IMAP/Gmail later) by passing an email address.",
18878
19069
  type: "module",
18879
19070
  bin: {