hypermail-mcp 0.7.17 → 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,12 @@
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
+ >
6
12
  > **v0.7.17** — Hardened Outlook message IDs returned by `list_emails` and
7
13
  > `search_emails`: Outlook reads now request Microsoft Graph immutable IDs,
8
14
  > `search_emails` probes results and marks stale/not-found messages with
@@ -291,7 +297,7 @@ account store.
291
297
  | `remove_account` | `email` | Deletes tokens for the account. |
292
298
  | `list_emails` | `account`, `folder?`, `limit?`, `unreadOnly?`, `skip?` | Defaults: folder=`inbox`, limit=25. Supports pagination via `skip` — response includes `hasMore`. |
293
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. |
294
- | `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. |
295
301
  | `read_email` | `account`, `id`, `format?` | Returns full body + recipients + attachment metadata. `format`: `markdown` (default), `html`, or `text`. |
296
302
  | `read_attachment` | `account`, `messageId`, `attachmentId` | Download an attachment to a temporary file and return its path. |
297
303
  | `archive_email` | `account`, `id` | Move a message to the Archive folder. |
@@ -319,6 +325,8 @@ tool on their own schedule, for example every 30–60 seconds.
319
325
  - `account` is optional. When omitted, the tool checks all registered accounts.
320
326
  - `limit` defaults to `10`. In all-account mode, the limit is a global total
321
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.
322
330
  - First use for an account initializes its checkpoint to the newest inbox email
323
331
  and returns no emails for that account.
324
332
  - Later calls return emails not previously returned by this tool, oldest first.
package/dist/cli.js CHANGED
@@ -17839,20 +17839,36 @@ function registerNewEmailTool(server, ctx) {
17839
17839
  const collected = [];
17840
17840
  const providersByEmail = /* @__PURE__ */ new Map();
17841
17841
  const accountsByEmail = /* @__PURE__ */ new Map();
17842
- for (const stored of accounts) {
17843
- try {
17844
- const { provider, account } = registry.resolveByEmail(stored.email);
17845
- const result = await collectCandidatesForAccount(store, provider, account, logger);
17846
- providersByEmail.set(result.account.email, provider);
17847
- accountsByEmail.set(result.account.email, result.account);
17848
- collected.push(...result.candidates);
17849
- } catch (err) {
17850
- logger.debug("get-new-emails", "accountError", {
17851
- account: stored.email,
17852
- message: errMsg(err)
17853
- });
17854
- 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;
17855
17868
  }
17869
+ providersByEmail.set(result.account.email, result.provider);
17870
+ accountsByEmail.set(result.account.email, result.account);
17871
+ collected.push(...result.candidates);
17856
17872
  }
17857
17873
  const selected = oldestCandidatesFirst(collected).slice(0, limit);
17858
17874
  logger.debug("get-new-emails", "selected", {
@@ -17871,27 +17887,33 @@ function registerNewEmailTool(server, ctx) {
17871
17887
  items.push(candidate);
17872
17888
  byAccount.set(candidate.account.email, items);
17873
17889
  }
17874
- for (const [email, accountCandidates] of byAccount) {
17875
- const provider = providersByEmail.get(email);
17876
- const account = accountsByEmail.get(email);
17877
- if (!provider || !account) continue;
17878
- try {
17879
- emails.push(
17880
- ...await hydrateAndAdvance(
17881
- store,
17882
- provider,
17883
- account,
17884
- accountCandidates,
17885
- logger
17886
- )
17887
- );
17888
- } catch (err) {
17889
- logger.debug("get-new-emails", "accountError", {
17890
- account: email,
17891
- message: errMsg(err)
17892
- });
17893
- 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;
17894
17915
  }
17916
+ emails.push(...result.emails);
17895
17917
  }
17896
17918
  }
17897
17919
  const orderedEmails = emails.sort(compareNewEmailOutputOldestFirst);
@@ -18082,10 +18104,15 @@ function registerBrowseTools(server, ctx) {
18082
18104
  skip: z5.number(),
18083
18105
  hasMore: z5.boolean()
18084
18106
  });
18107
+ const searchEmailSummaryOutputSchema = emailSummaryOutputSchema.extend({
18108
+ account: z5.string()
18109
+ });
18085
18110
  const searchEmailsOutputSchema = z5.object({
18086
18111
  account: z5.string(),
18087
18112
  count: z5.number(),
18088
- 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()
18089
18116
  });
18090
18117
  if (shouldRegister("list_emails", tools)) {
18091
18118
  server.registerTool(
@@ -18129,29 +18156,67 @@ function registerBrowseTools(server, ctx) {
18129
18156
  server.registerTool(
18130
18157
  "search_emails",
18131
18158
  {
18132
- 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.",
18133
18160
  inputSchema: z5.object({
18134
- account: z5.string().email(),
18161
+ account: z5.string().email().optional(),
18135
18162
  query: z5.string().min(1),
18136
18163
  limit: z5.number().int().positive().max(100).optional()
18137
18164
  }),
18138
18165
  outputSchema: searchEmailsOutputSchema
18139
18166
  },
18140
18167
  async (args) => {
18141
- try {
18142
- const { provider, account } = registry.resolveByEmail(args.account);
18143
- const items = await provider.searchEmails(account, args.query, {
18144
- limit: args.limit
18145
- });
18146
- const data = {
18147
- account: account.email,
18148
- count: items.length,
18149
- items
18150
- };
18151
- return ok(data, data);
18152
- } catch (err) {
18153
- 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.");
18154
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);
18155
18220
  }
18156
18221
  );
18157
18222
  }
@@ -18999,7 +19064,7 @@ function registerTools(server, opts) {
18999
19064
  // package.json
19000
19065
  var package_default = {
19001
19066
  name: "hypermail-mcp",
19002
- version: "0.7.17",
19067
+ version: "0.7.18",
19003
19068
  description: "Unified email MCP server \u2014 operate any inbox (Outlook now, IMAP/Gmail later) by passing an email address.",
19004
19069
  type: "module",
19005
19070
  bin: {