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 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
- "id",
14876
- "subject",
14877
- "from",
14878
- "toRecipients",
14879
- "receivedDateTime",
14880
- "bodyPreview",
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.api(`/me/messages/${encodeURIComponent(id)}`).select(
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.api(`/me/messages/${encodeURIComponent(id)}/attachments`).select("id,name,contentType,size").get();
14907
- attachments = attRes.value.map((a) => ({
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
- const att = await client.api(`/me/messages/${encodeURIComponent(messageId)}/attachments/${encodeURIComponent(attachmentId)}`).select("name,contentType").get();
14929
- const data = await client.api(`/me/messages/${encodeURIComponent(messageId)}/attachments/${encodeURIComponent(attachmentId)}/$value`).responseType(ResponseType.ARRAYBUFFER).get();
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
- return message;
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.16",
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: {