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 +16 -1
- package/dist/cli.js +285 -94
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
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
|
|
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
|
-
|
|
14876
|
-
|
|
14877
|
-
|
|
14878
|
-
|
|
14879
|
-
|
|
14880
|
-
|
|
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
|
|
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.
|
|
14907
|
-
attachments = attRes.
|
|
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
|
-
|
|
14929
|
-
|
|
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
|
-
|
|
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
|
-
|
|
17717
|
-
|
|
17718
|
-
|
|
17719
|
-
|
|
17720
|
-
|
|
17721
|
-
|
|
17722
|
-
|
|
17723
|
-
|
|
17724
|
-
|
|
17725
|
-
|
|
17726
|
-
|
|
17727
|
-
})
|
|
17728
|
-
|
|
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
|
-
|
|
17749
|
-
|
|
17750
|
-
|
|
17751
|
-
|
|
17752
|
-
|
|
17753
|
-
|
|
17754
|
-
|
|
17755
|
-
|
|
17756
|
-
provider,
|
|
17757
|
-
|
|
17758
|
-
|
|
17759
|
-
|
|
17760
|
-
)
|
|
17761
|
-
|
|
17762
|
-
|
|
17763
|
-
|
|
17764
|
-
|
|
17765
|
-
|
|
17766
|
-
}
|
|
17767
|
-
|
|
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(
|
|
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
|
-
|
|
18016
|
-
|
|
18017
|
-
|
|
18018
|
-
|
|
18019
|
-
|
|
18020
|
-
|
|
18021
|
-
|
|
18022
|
-
|
|
18023
|
-
|
|
18024
|
-
|
|
18025
|
-
|
|
18026
|
-
|
|
18027
|
-
|
|
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.
|
|
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: {
|