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 +9 -1
- package/dist/cli.js +115 -50
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
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
|
|
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
|
-
|
|
17843
|
-
|
|
17844
|
-
|
|
17845
|
-
|
|
17846
|
-
|
|
17847
|
-
|
|
17848
|
-
|
|
17849
|
-
|
|
17850
|
-
|
|
17851
|
-
|
|
17852
|
-
|
|
17853
|
-
})
|
|
17854
|
-
|
|
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
|
-
|
|
17875
|
-
|
|
17876
|
-
|
|
17877
|
-
|
|
17878
|
-
|
|
17879
|
-
|
|
17880
|
-
|
|
17881
|
-
|
|
17882
|
-
provider,
|
|
17883
|
-
|
|
17884
|
-
|
|
17885
|
-
|
|
17886
|
-
)
|
|
17887
|
-
|
|
17888
|
-
|
|
17889
|
-
|
|
17890
|
-
|
|
17891
|
-
|
|
17892
|
-
}
|
|
17893
|
-
|
|
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(
|
|
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
|
-
|
|
18142
|
-
|
|
18143
|
-
|
|
18144
|
-
|
|
18145
|
-
|
|
18146
|
-
|
|
18147
|
-
|
|
18148
|
-
|
|
18149
|
-
|
|
18150
|
-
|
|
18151
|
-
|
|
18152
|
-
|
|
18153
|
-
|
|
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.
|
|
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: {
|