hypermail-mcp 0.7.9 → 0.7.10

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,10 @@
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.10** — Fixed an account-store race where Gmail/Outlook token
7
+ > refreshes could overwrite `get_new_emails` checkpoints. Checkpoint updates now
8
+ > preserve token data and merge delivered IDs at the same timestamp.
9
+ >
6
10
  > **v0.7.9** — Replaced server-side email watch/webhook/script delivery with
7
11
  > the pull-based `get_new_emails` tool. Agents schedule their own repeated calls
8
12
  > and fetch bounded batches of new inbox email.
package/dist/cli.js CHANGED
@@ -13965,6 +13965,7 @@ var AccountStore = class _AccountStore {
13965
13965
  filePath;
13966
13966
  key;
13967
13967
  data;
13968
+ writeLocks = /* @__PURE__ */ new Map();
13968
13969
  static async open(opts = {}) {
13969
13970
  const dataDir = resolveDataDir(opts.dataDir);
13970
13971
  await fs2.mkdir(dataDir, { recursive: true, mode: 448 });
@@ -13992,21 +13993,79 @@ var AccountStore = class _AccountStore {
13992
13993
  return rec ? { ...rec } : void 0;
13993
13994
  }
13994
13995
  async upsertAccount(rec) {
13995
- const norm = rec.email.trim().toLowerCase();
13996
- const next = { ...rec, email: norm };
13997
- const idx = this.data.accounts.findIndex((a) => a.email.toLowerCase() === norm);
13998
- if (idx >= 0) this.data.accounts[idx] = next;
13999
- else this.data.accounts.push(next);
14000
- await this.flush();
14001
- return { ...next };
13996
+ return this.runSerial(rec.email, async () => {
13997
+ const norm = rec.email.trim().toLowerCase();
13998
+ const next = { ...rec, email: norm };
13999
+ const idx = this.data.accounts.findIndex((a) => a.email.toLowerCase() === norm);
14000
+ if (idx >= 0) this.data.accounts[idx] = next;
14001
+ else this.data.accounts.push(next);
14002
+ await this.flush();
14003
+ return { ...next };
14004
+ });
14005
+ }
14006
+ async updateTokens(email, tokens) {
14007
+ return this.runSerial(email, async () => {
14008
+ const norm = email.trim().toLowerCase();
14009
+ const idx = this.data.accounts.findIndex((a) => a.email.toLowerCase() === norm);
14010
+ if (idx < 0) return void 0;
14011
+ const current = this.data.accounts[idx];
14012
+ const next = { ...current, tokens };
14013
+ this.data.accounts[idx] = next;
14014
+ await this.flush();
14015
+ return { ...next };
14016
+ });
14017
+ }
14018
+ async updateNewEmailCheckpoint(email, checkpoint) {
14019
+ return this.runSerial(email, async () => {
14020
+ const norm = email.trim().toLowerCase();
14021
+ const idx = this.data.accounts.findIndex((a) => a.email.toLowerCase() === norm);
14022
+ if (idx < 0) return void 0;
14023
+ const current = this.data.accounts[idx];
14024
+ const currentCheckpoint = current.newEmailCheckpoint;
14025
+ const mergedCheckpoint = currentCheckpoint?.receivedAt === checkpoint.receivedAt ? {
14026
+ receivedAt: checkpoint.receivedAt,
14027
+ deliveredIdsAtReceivedAt: [
14028
+ .../* @__PURE__ */ new Set([
14029
+ ...currentCheckpoint.deliveredIdsAtReceivedAt ?? [],
14030
+ ...checkpoint.deliveredIdsAtReceivedAt ?? []
14031
+ ])
14032
+ ]
14033
+ } : checkpoint;
14034
+ const next = {
14035
+ ...current,
14036
+ newEmailCheckpoint: mergedCheckpoint
14037
+ };
14038
+ this.data.accounts[idx] = next;
14039
+ await this.flush();
14040
+ return { ...next };
14041
+ });
14002
14042
  }
14003
14043
  async removeAccount(email) {
14044
+ return this.runSerial(email, async () => {
14045
+ const norm = email.trim().toLowerCase();
14046
+ const before = this.data.accounts.length;
14047
+ this.data.accounts = this.data.accounts.filter((a) => a.email.toLowerCase() !== norm);
14048
+ if (this.data.accounts.length === before) return false;
14049
+ await this.flush();
14050
+ return true;
14051
+ });
14052
+ }
14053
+ async runSerial(email, task) {
14004
14054
  const norm = email.trim().toLowerCase();
14005
- const before = this.data.accounts.length;
14006
- this.data.accounts = this.data.accounts.filter((a) => a.email.toLowerCase() !== norm);
14007
- if (this.data.accounts.length === before) return false;
14008
- await this.flush();
14009
- return true;
14055
+ const previous = this.writeLocks.get(norm) ?? Promise.resolve();
14056
+ const run = previous.catch(() => void 0).then(task);
14057
+ const lock = run.then(
14058
+ () => void 0,
14059
+ () => void 0
14060
+ );
14061
+ this.writeLocks.set(norm, lock);
14062
+ try {
14063
+ return await run;
14064
+ } finally {
14065
+ if (this.writeLocks.get(norm) === lock) {
14066
+ this.writeLocks.delete(norm);
14067
+ }
14068
+ }
14010
14069
  }
14011
14070
  async flush() {
14012
14071
  const buf = encrypt(this.data, this.key);
@@ -14203,10 +14262,10 @@ var OutlookClientFactory = class {
14203
14262
  this.tenantId
14204
14263
  );
14205
14264
  if (nextTokens.msalCache !== tokens.msalCache) {
14206
- store.upsertAccount({
14207
- ...fresh,
14208
- tokens: nextTokens
14209
- }).catch(() => {
14265
+ store.updateTokens(
14266
+ account.email,
14267
+ nextTokens
14268
+ ).catch(() => {
14210
14269
  });
14211
14270
  }
14212
14271
  return accessToken;
@@ -15826,10 +15885,10 @@ var GmailClientFactory = class {
15826
15885
  expiryDate: updated.expiry_date ?? currentTokens.expiryDate,
15827
15886
  scopes: updated.scope ? updated.scope.split(" ") : currentTokens.scopes
15828
15887
  };
15829
- await store.upsertAccount({
15830
- ...fresh,
15831
- tokens: nextTokens
15832
- }).catch(() => {
15888
+ await store.updateTokens(
15889
+ account.email,
15890
+ nextTokens
15891
+ ).catch(() => {
15833
15892
  });
15834
15893
  });
15835
15894
  persistLocks.set(key, chain);
@@ -17222,9 +17281,9 @@ async function initializeCheckpoint(store, provider, account) {
17222
17281
  const first = items[0];
17223
17282
  const receivedAt = first ? effectiveReceivedAt(first.receivedAt) : (/* @__PURE__ */ new Date()).toISOString();
17224
17283
  const deliveredIdsAtReceivedAt = items.filter((item) => effectiveReceivedAt(item.receivedAt) === receivedAt).map((item) => item.id);
17225
- await store.upsertAccount({
17226
- ...account,
17227
- newEmailCheckpoint: { receivedAt, deliveredIdsAtReceivedAt }
17284
+ await store.updateNewEmailCheckpoint(account.email, {
17285
+ receivedAt,
17286
+ deliveredIdsAtReceivedAt
17228
17287
  });
17229
17288
  }
17230
17289
  async function hydrateAndAdvance(store, provider, account, selected) {
@@ -17264,24 +17323,11 @@ async function advanceCheckpoint(store, account, selected) {
17264
17323
  const ordered = oldestCandidatesFirst(selected);
17265
17324
  const newest = ordered[ordered.length - 1];
17266
17325
  if (!newest) return;
17267
- const previous = normalizeCheckpoint(account.newEmailCheckpoint);
17268
17326
  const newestTimestamp = newest.timestamp;
17269
17327
  const idsAtNewest = ordered.filter((candidate) => candidate.timestamp === newestTimestamp).map((candidate) => candidate.summary.id);
17270
- let deliveredIdsAtReceivedAt = idsAtNewest;
17271
- if (previous?.receivedAt === newestTimestamp) {
17272
- deliveredIdsAtReceivedAt = [
17273
- .../* @__PURE__ */ new Set([
17274
- ...previous.deliveredIdsAtReceivedAt ?? [],
17275
- ...idsAtNewest
17276
- ])
17277
- ];
17278
- }
17279
- await store.upsertAccount({
17280
- ...account,
17281
- newEmailCheckpoint: {
17282
- receivedAt: newestTimestamp,
17283
- deliveredIdsAtReceivedAt
17284
- }
17328
+ await store.updateNewEmailCheckpoint(account.email, {
17329
+ receivedAt: newestTimestamp,
17330
+ deliveredIdsAtReceivedAt: idsAtNewest
17285
17331
  });
17286
17332
  }
17287
17333
  function normalizeCheckpoint(checkpoint) {
@@ -18098,7 +18144,7 @@ function registerTools(server, opts) {
18098
18144
  // package.json
18099
18145
  var package_default = {
18100
18146
  name: "hypermail-mcp",
18101
- version: "0.7.9",
18147
+ version: "0.7.10",
18102
18148
  description: "Unified email MCP server \u2014 operate any inbox (Outlook now, IMAP/Gmail later) by passing an email address.",
18103
18149
  type: "module",
18104
18150
  bin: {