hypermail-mcp 0.7.18 → 0.7.19
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 +10 -3
- package/dist/cli.js +88 -14
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -3,6 +3,13 @@
|
|
|
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.19** — Hardened Outlook ID handling when Microsoft Graph `$search`
|
|
7
|
+
> returns IDs that `/me/messages/{id}` rejects as malformed: `search_emails`
|
|
8
|
+
> now returns a translated readable immutable ID when available, and
|
|
9
|
+
> `read_email` retries malformed/stale IDs through `translateExchangeIds`
|
|
10
|
+
> before failing. `list_emails` also falls back from localized folder display
|
|
11
|
+
> names such as `Éléments envoyés` to the matching Graph folder ID.
|
|
12
|
+
>
|
|
6
13
|
> **v0.7.18** — `search_emails` can now omit `account` to search all
|
|
7
14
|
> registered accounts in parallel, returning account-annotated results plus
|
|
8
15
|
> per-account errors without discarding successful matches. All-account
|
|
@@ -295,10 +302,10 @@ account store.
|
|
|
295
302
|
| `get_account_settings` | `account` | Get signature (HTML) and style preferences for an account. |
|
|
296
303
|
| `set_account_settings` | `account`, `signature?`, `signaturePath?`, `style?` | Set signature HTML (inline or via file path) and font preferences. |
|
|
297
304
|
| `remove_account` | `email` | Deletes tokens for the account. |
|
|
298
|
-
| `list_emails` | `account`, `folder?`, `limit?`, `unreadOnly?`, `skip?` | Defaults: folder=`inbox`, limit=25. Supports pagination via `skip` — response includes `hasMore`. |
|
|
305
|
+
| `list_emails` | `account`, `folder?`, `limit?`, `unreadOnly?`, `skip?` | Defaults: folder=`inbox`, limit=25. Supports pagination via `skip` — response includes `hasMore`. Outlook accepts well-known folder names, folder IDs, and falls back from localized display names to matching folder IDs. |
|
|
299
306
|
| `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. |
|
|
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. |
|
|
301
|
-
| `read_email` | `account`, `id`, `format?` | Returns full body + recipients + attachment metadata. `format`: `markdown` (default), `html`, or `text`. |
|
|
307
|
+
| `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, and normalizes malformed Outlook search-result IDs to readable immutable IDs when possible. |
|
|
308
|
+
| `read_email` | `account`, `id`, `format?` | Returns full body + recipients + attachment metadata. `format`: `markdown` (default), `html`, or `text`. Outlook retries malformed/stale IDs through Graph ID translation before failing. |
|
|
302
309
|
| `read_attachment` | `account`, `messageId`, `attachmentId` | Download an attachment to a temporary file and return its path. |
|
|
303
310
|
| `archive_email` | `account`, `id` | Move a message to the Archive folder. |
|
|
304
311
|
| `trash_email` | `account`, `id` | Move a message to Deleted Items (trash). |
|
package/dist/cli.js
CHANGED
|
@@ -14870,6 +14870,10 @@ var FULL_MESSAGE_SELECT = [
|
|
|
14870
14870
|
"hasAttachments",
|
|
14871
14871
|
"body"
|
|
14872
14872
|
].join(",");
|
|
14873
|
+
var EXCHANGE_ID_TRANSLATION_SOURCE_TYPES = [
|
|
14874
|
+
"ewsId",
|
|
14875
|
+
"restId"
|
|
14876
|
+
];
|
|
14873
14877
|
function graphErrorFields(err) {
|
|
14874
14878
|
if (typeof err !== "object" || err === null) {
|
|
14875
14879
|
return [String(err)];
|
|
@@ -14910,6 +14914,23 @@ function isStaleMessageIdError(err) {
|
|
|
14910
14914
|
return normalized.includes("erroritemnotfound") || normalized.includes("invalidid") || normalized.includes("object was not found in the store") || normalized.includes("not found in the store");
|
|
14911
14915
|
});
|
|
14912
14916
|
}
|
|
14917
|
+
async function translateMessageIdToImmutable(client, id) {
|
|
14918
|
+
for (const sourceIdType of EXCHANGE_ID_TRANSLATION_SOURCE_TYPES) {
|
|
14919
|
+
try {
|
|
14920
|
+
const res = await client.api("/me/translateExchangeIds").post({
|
|
14921
|
+
inputIds: [id],
|
|
14922
|
+
sourceIdType,
|
|
14923
|
+
targetIdType: "restImmutableEntryId"
|
|
14924
|
+
});
|
|
14925
|
+
const targetId = res.value?.find((item) => item.targetId)?.targetId;
|
|
14926
|
+
if (typeof targetId === "string" && targetId !== "" && targetId !== id) {
|
|
14927
|
+
return targetId;
|
|
14928
|
+
}
|
|
14929
|
+
} catch {
|
|
14930
|
+
}
|
|
14931
|
+
}
|
|
14932
|
+
return void 0;
|
|
14933
|
+
}
|
|
14913
14934
|
async function probeMessage(client, id, useImmutableIds) {
|
|
14914
14935
|
let req = client.api(`/me/messages/${encodeURIComponent(id)}`).select("id");
|
|
14915
14936
|
if (useImmutableIds) req = req.header("Prefer", OUTLOOK_IMMUTABLE_ID_PREFER);
|
|
@@ -14918,27 +14939,58 @@ async function probeMessage(client, id, useImmutableIds) {
|
|
|
14918
14939
|
async function probeSearchResult(client, id) {
|
|
14919
14940
|
try {
|
|
14920
14941
|
await probeMessage(client, id, true);
|
|
14921
|
-
return
|
|
14942
|
+
return id;
|
|
14922
14943
|
} catch (err) {
|
|
14923
14944
|
if (!isStaleMessageIdError(err)) throw err;
|
|
14924
14945
|
}
|
|
14925
14946
|
try {
|
|
14926
14947
|
await probeMessage(client, id, false);
|
|
14927
|
-
return
|
|
14948
|
+
return id;
|
|
14949
|
+
} catch (err) {
|
|
14950
|
+
if (!isStaleMessageIdError(err)) throw err;
|
|
14951
|
+
}
|
|
14952
|
+
const translatedId = await translateMessageIdToImmutable(client, id);
|
|
14953
|
+
if (!translatedId) return void 0;
|
|
14954
|
+
try {
|
|
14955
|
+
await probeMessage(client, translatedId, true);
|
|
14956
|
+
return translatedId;
|
|
14928
14957
|
} catch (err) {
|
|
14929
|
-
if (isStaleMessageIdError(err))
|
|
14930
|
-
|
|
14958
|
+
if (!isStaleMessageIdError(err)) throw err;
|
|
14959
|
+
return void 0;
|
|
14931
14960
|
}
|
|
14932
14961
|
}
|
|
14962
|
+
async function getMessageById(client, id, useImmutableIds) {
|
|
14963
|
+
let req = client.api(`/me/messages/${encodeURIComponent(id)}`).select(FULL_MESSAGE_SELECT);
|
|
14964
|
+
if (useImmutableIds) req = req.header("Prefer", OUTLOOK_IMMUTABLE_ID_PREFER);
|
|
14965
|
+
return await req.get();
|
|
14966
|
+
}
|
|
14933
14967
|
async function getMessage(client, id) {
|
|
14968
|
+
let lastStaleError;
|
|
14934
14969
|
try {
|
|
14935
|
-
|
|
14936
|
-
return { message: message2, useImmutableIds: true };
|
|
14970
|
+
return { message: await getMessageById(client, id, true), useImmutableIds: true };
|
|
14937
14971
|
} catch (err) {
|
|
14938
14972
|
if (!isStaleMessageIdError(err)) throw err;
|
|
14973
|
+
lastStaleError = err;
|
|
14939
14974
|
}
|
|
14940
|
-
|
|
14941
|
-
|
|
14975
|
+
try {
|
|
14976
|
+
return { message: await getMessageById(client, id, false), useImmutableIds: false };
|
|
14977
|
+
} catch (err) {
|
|
14978
|
+
if (!isStaleMessageIdError(err)) throw err;
|
|
14979
|
+
lastStaleError = err;
|
|
14980
|
+
}
|
|
14981
|
+
const translatedId = await translateMessageIdToImmutable(client, id);
|
|
14982
|
+
if (translatedId) {
|
|
14983
|
+
try {
|
|
14984
|
+
return {
|
|
14985
|
+
message: await getMessageById(client, translatedId, true),
|
|
14986
|
+
useImmutableIds: true
|
|
14987
|
+
};
|
|
14988
|
+
} catch (err) {
|
|
14989
|
+
if (!isStaleMessageIdError(err)) throw err;
|
|
14990
|
+
lastStaleError = err;
|
|
14991
|
+
}
|
|
14992
|
+
}
|
|
14993
|
+
throw lastStaleError ?? new Error("Message not found");
|
|
14942
14994
|
}
|
|
14943
14995
|
async function listAttachments(client, messageId, useImmutableIds) {
|
|
14944
14996
|
let req = client.api(`/me/messages/${encodeURIComponent(messageId)}/attachments`).select("id,name,contentType,size");
|
|
@@ -14956,14 +15008,36 @@ async function getAttachmentValue(client, messageId, attachmentId, useImmutableI
|
|
|
14956
15008
|
if (useImmutableIds) req = req.header("Prefer", OUTLOOK_IMMUTABLE_ID_PREFER);
|
|
14957
15009
|
return await req.get();
|
|
14958
15010
|
}
|
|
15011
|
+
async function listMessagePage(client, folder, limit, skip, filterParts) {
|
|
15012
|
+
let req = client.api(`/me/mailFolders/${encodeURIComponent(folder)}/messages`).header("Prefer", OUTLOOK_IMMUTABLE_ID_PREFER).top(limit).skip(skip).select(MESSAGE_SELECT).orderby("receivedDateTime DESC");
|
|
15013
|
+
if (filterParts.length > 0) req = req.filter(filterParts.join(" and "));
|
|
15014
|
+
return await req.get();
|
|
15015
|
+
}
|
|
15016
|
+
function normalizeFolderDisplayName(value) {
|
|
15017
|
+
return value.trim().normalize("NFD").replace(new RegExp("\\p{Diacritic}", "gu"), "").toLowerCase();
|
|
15018
|
+
}
|
|
15019
|
+
async function resolveFolderIdByDisplayName(client, displayName) {
|
|
15020
|
+
const wanted = normalizeFolderDisplayName(displayName);
|
|
15021
|
+
if (!wanted) return void 0;
|
|
15022
|
+
const res = await client.api("/me/mailFolders").top(100).select("id,displayName").get();
|
|
15023
|
+
return (res.value ?? []).find(
|
|
15024
|
+
(folder) => normalizeFolderDisplayName(folder.displayName) === wanted
|
|
15025
|
+
)?.id;
|
|
15026
|
+
}
|
|
14959
15027
|
async function listEmails(client, account, opts) {
|
|
14960
15028
|
const limit = clampLimit(opts.limit, 25, 100);
|
|
14961
15029
|
const folder = opts.folder ?? "inbox";
|
|
14962
15030
|
const filterParts = [];
|
|
14963
15031
|
if (opts.unreadOnly) filterParts.push("isRead eq false");
|
|
14964
|
-
let
|
|
14965
|
-
|
|
14966
|
-
|
|
15032
|
+
let res;
|
|
15033
|
+
try {
|
|
15034
|
+
res = await listMessagePage(client, folder, limit, opts.skip ?? 0, filterParts);
|
|
15035
|
+
} catch (err) {
|
|
15036
|
+
if (!isStaleMessageIdError(err)) throw err;
|
|
15037
|
+
const resolvedFolderId = await resolveFolderIdByDisplayName(client, folder);
|
|
15038
|
+
if (!resolvedFolderId || resolvedFolderId === folder) throw err;
|
|
15039
|
+
res = await listMessagePage(client, resolvedFolderId, limit, opts.skip ?? 0, filterParts);
|
|
15040
|
+
}
|
|
14967
15041
|
return {
|
|
14968
15042
|
items: res.value.map((m) => mapSummary(m, folder)),
|
|
14969
15043
|
hasMore: !!res["@odata.nextLink"]
|
|
@@ -14975,8 +15049,8 @@ async function searchEmails(client, account, query, opts) {
|
|
|
14975
15049
|
const summaries = res.value.map((m) => mapSummary(m));
|
|
14976
15050
|
return Promise.all(
|
|
14977
15051
|
summaries.map(async (summary) => {
|
|
14978
|
-
const
|
|
14979
|
-
return
|
|
15052
|
+
const readableId = await probeSearchResult(client, summary.id);
|
|
15053
|
+
return readableId ? { ...summary, id: readableId } : { ...summary, stale: true };
|
|
14980
15054
|
})
|
|
14981
15055
|
);
|
|
14982
15056
|
}
|
|
@@ -19064,7 +19138,7 @@ function registerTools(server, opts) {
|
|
|
19064
19138
|
// package.json
|
|
19065
19139
|
var package_default = {
|
|
19066
19140
|
name: "hypermail-mcp",
|
|
19067
|
-
version: "0.7.
|
|
19141
|
+
version: "0.7.19",
|
|
19068
19142
|
description: "Unified email MCP server \u2014 operate any inbox (Outlook now, IMAP/Gmail later) by passing an email address.",
|
|
19069
19143
|
type: "module",
|
|
19070
19144
|
bin: {
|