hypermail-mcp 0.7.16 → 0.7.17
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 +7 -0
- package/dist/cli.js +171 -45
- 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.17** — Hardened Outlook message IDs returned by `list_emails` and
|
|
7
|
+
> `search_emails`: Outlook reads now request Microsoft Graph immutable IDs,
|
|
8
|
+
> `search_emails` probes results and marks stale/not-found messages with
|
|
9
|
+
> `stale: true`, and `read_email` / `read_attachment` keep a legacy mutable-ID
|
|
10
|
+
> fallback for older IDs. Graph errors now include structured diagnostic details
|
|
11
|
+
> when available.
|
|
12
|
+
>
|
|
6
13
|
> **v0.7.16** — Hardened `draft_email` after successful draft creation:
|
|
7
14
|
> if post-save readback fails, the tool now returns the created draft ID with
|
|
8
15
|
> `warning` and `draftReadbackError` instead of losing the draft behind an
|
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(),
|
|
@@ -18873,7 +18999,7 @@ function registerTools(server, opts) {
|
|
|
18873
18999
|
// package.json
|
|
18874
19000
|
var package_default = {
|
|
18875
19001
|
name: "hypermail-mcp",
|
|
18876
|
-
version: "0.7.
|
|
19002
|
+
version: "0.7.17",
|
|
18877
19003
|
description: "Unified email MCP server \u2014 operate any inbox (Outlook now, IMAP/Gmail later) by passing an email address.",
|
|
18878
19004
|
type: "module",
|
|
18879
19005
|
bin: {
|