hypermail-mcp 0.7.8 → 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/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);
@@ -17038,7 +17097,7 @@ function registerAccountTools(server, ctx) {
17038
17097
  }
17039
17098
 
17040
17099
  // src/tools/browse.ts
17041
- import { z as z4 } from "zod";
17100
+ import { z as z5 } from "zod";
17042
17101
 
17043
17102
  // src/html-to-markdown.ts
17044
17103
  import TurndownService from "turndown";
@@ -17066,32 +17125,275 @@ function selectBody(msg, format) {
17066
17125
  }
17067
17126
  }
17068
17127
 
17069
- // src/tools/browse.ts
17070
- function registerBrowseTools(server, ctx) {
17071
- const { registry, tools } = ctx;
17072
- const emailListOutputSchema = z4.object({
17128
+ // src/tools/new-emails.ts
17129
+ import { z as z4 } from "zod";
17130
+ var DEFAULT_LIMIT = 10;
17131
+ var BODY_LIMIT = 2e4;
17132
+ var PAGE_SIZE = 100;
17133
+ var MISSING_RECEIVED_AT = "1970-01-01T00:00:00.000Z";
17134
+ function registerNewEmailTool(server, ctx) {
17135
+ const { store, registry, tools } = ctx;
17136
+ if (!shouldRegister("get_new_emails", tools)) return;
17137
+ const newEmailOutputSchema = z4.object({
17073
17138
  account: z4.string(),
17074
- count: z4.number(),
17075
- items: z4.array(emailSummaryOutputSchema),
17076
- skip: z4.number(),
17077
- hasMore: z4.boolean()
17139
+ id: z4.string(),
17140
+ subject: z4.string(),
17141
+ from: emailAddrOutputSchema.optional(),
17142
+ to: z4.array(emailAddrOutputSchema).optional(),
17143
+ cc: z4.array(emailAddrOutputSchema).optional(),
17144
+ bcc: z4.array(emailAddrOutputSchema).optional(),
17145
+ receivedAt: z4.string().optional(),
17146
+ preview: z4.string().optional(),
17147
+ isRead: z4.boolean().optional(),
17148
+ hasAttachments: z4.boolean().optional(),
17149
+ folder: z4.string().optional(),
17150
+ attachments: z4.array(attachmentMetaOutputSchema).optional(),
17151
+ body: z4.string(),
17152
+ bodyFormat: z4.literal("markdown"),
17153
+ bodyTruncated: z4.boolean(),
17154
+ bodyOriginalLength: z4.number()
17078
17155
  });
17079
- const searchEmailsOutputSchema = z4.object({
17080
- account: z4.string(),
17156
+ const outputSchema = z4.object({
17081
17157
  count: z4.number(),
17082
- items: z4.array(emailSummaryOutputSchema)
17158
+ emails: z4.array(newEmailOutputSchema),
17159
+ errors: z4.array(z4.object({ account: z4.string(), message: z4.string() }))
17160
+ });
17161
+ server.registerTool(
17162
+ "get_new_emails",
17163
+ {
17164
+ description: "Fetch a bounded batch of inbox emails not previously returned by this tool. Agents should call this on their own schedule. Bodies are returned as markdown and may be truncated.",
17165
+ inputSchema: z4.object({
17166
+ account: z4.string().email().optional().describe(
17167
+ "Email account to poll. If omitted, polls all accounts with one global limit."
17168
+ ),
17169
+ limit: z4.number().int().min(0).default(DEFAULT_LIMIT).optional().describe(
17170
+ "Maximum emails to return. Defaults to 10. Use 0 to initialize/check without fetching bodies."
17171
+ )
17172
+ }),
17173
+ outputSchema
17174
+ },
17175
+ async (args) => {
17176
+ const limit = args.limit ?? DEFAULT_LIMIT;
17177
+ if (args.account) {
17178
+ try {
17179
+ const { provider, account } = registry.resolveByEmail(args.account);
17180
+ const result = await collectCandidatesForAccount(store, provider, account);
17181
+ const selected2 = oldestCandidatesFirst(result.candidates).slice(0, limit);
17182
+ const emails2 = limit === 0 ? [] : await hydrateAndAdvance(store, provider, result.account, selected2);
17183
+ const data2 = { count: emails2.length, emails: emails2, errors: [] };
17184
+ return ok(data2, data2);
17185
+ } catch (err) {
17186
+ return fail(errMsg(err));
17187
+ }
17188
+ }
17189
+ const accounts = store.listAccounts();
17190
+ if (accounts.length === 0) {
17191
+ return fail("no accounts registered. Call add_account first.");
17192
+ }
17193
+ const errors = [];
17194
+ const collected = [];
17195
+ const providersByEmail = /* @__PURE__ */ new Map();
17196
+ const accountsByEmail = /* @__PURE__ */ new Map();
17197
+ for (const stored of accounts) {
17198
+ try {
17199
+ const { provider, account } = registry.resolveByEmail(stored.email);
17200
+ const result = await collectCandidatesForAccount(store, provider, account);
17201
+ providersByEmail.set(result.account.email, provider);
17202
+ accountsByEmail.set(result.account.email, result.account);
17203
+ collected.push(...result.candidates);
17204
+ } catch (err) {
17205
+ errors.push({ account: stored.email, message: errMsg(err) });
17206
+ }
17207
+ }
17208
+ const selected = oldestCandidatesFirst(collected).slice(0, limit);
17209
+ const emails = [];
17210
+ if (limit > 0) {
17211
+ const byAccount = /* @__PURE__ */ new Map();
17212
+ for (const candidate of selected) {
17213
+ const items = byAccount.get(candidate.account.email) ?? [];
17214
+ items.push(candidate);
17215
+ byAccount.set(candidate.account.email, items);
17216
+ }
17217
+ for (const [email, accountCandidates] of byAccount) {
17218
+ const provider = providersByEmail.get(email);
17219
+ const account = accountsByEmail.get(email);
17220
+ if (!provider || !account) continue;
17221
+ try {
17222
+ emails.push(
17223
+ ...await hydrateAndAdvance(
17224
+ store,
17225
+ provider,
17226
+ account,
17227
+ accountCandidates
17228
+ )
17229
+ );
17230
+ } catch (err) {
17231
+ errors.push({ account: email, message: errMsg(err) });
17232
+ }
17233
+ }
17234
+ }
17235
+ const orderedEmails = emails.sort(compareNewEmailOutputOldestFirst);
17236
+ const data = { count: orderedEmails.length, emails: orderedEmails, errors };
17237
+ return ok(data, data);
17238
+ }
17239
+ );
17240
+ }
17241
+ async function collectCandidatesForAccount(store, provider, account) {
17242
+ const checkpoint = normalizeCheckpoint(account.newEmailCheckpoint);
17243
+ if (!checkpoint) {
17244
+ await initializeCheckpoint(store, provider, account);
17245
+ return { account, candidates: [] };
17246
+ }
17247
+ const deliveredAtCheckpoint = new Set(checkpoint.deliveredIdsAtReceivedAt ?? []);
17248
+ const candidates = [];
17249
+ let skip = 0;
17250
+ while (true) {
17251
+ const { items, hasMore } = await provider.listEmails(account, {
17252
+ folder: "inbox",
17253
+ limit: PAGE_SIZE,
17254
+ skip
17255
+ });
17256
+ if (items.length === 0) break;
17257
+ let sawOlderThanCheckpoint = false;
17258
+ for (const item of items) {
17259
+ const timestamp = effectiveReceivedAt(item.receivedAt);
17260
+ const comparison = compareTimestamp(timestamp, checkpoint.receivedAt);
17261
+ if (comparison > 0) {
17262
+ candidates.push({ account, summary: item, timestamp });
17263
+ } else if (comparison === 0) {
17264
+ if (!deliveredAtCheckpoint.has(item.id)) {
17265
+ candidates.push({ account, summary: item, timestamp });
17266
+ }
17267
+ } else {
17268
+ sawOlderThanCheckpoint = true;
17269
+ }
17270
+ }
17271
+ if (sawOlderThanCheckpoint || !hasMore) break;
17272
+ skip += items.length;
17273
+ }
17274
+ return { account, candidates };
17275
+ }
17276
+ async function initializeCheckpoint(store, provider, account) {
17277
+ const { items } = await provider.listEmails(account, {
17278
+ folder: "inbox",
17279
+ limit: PAGE_SIZE
17280
+ });
17281
+ const first = items[0];
17282
+ const receivedAt = first ? effectiveReceivedAt(first.receivedAt) : (/* @__PURE__ */ new Date()).toISOString();
17283
+ const deliveredIdsAtReceivedAt = items.filter((item) => effectiveReceivedAt(item.receivedAt) === receivedAt).map((item) => item.id);
17284
+ await store.updateNewEmailCheckpoint(account.email, {
17285
+ receivedAt,
17286
+ deliveredIdsAtReceivedAt
17287
+ });
17288
+ }
17289
+ async function hydrateAndAdvance(store, provider, account, selected) {
17290
+ if (selected.length === 0) return [];
17291
+ const emails = [];
17292
+ for (const candidate of selected) {
17293
+ const full = await provider.readEmail(account, candidate.summary.id);
17294
+ emails.push(formatNewEmail(account.email, full, candidate.summary));
17295
+ }
17296
+ await advanceCheckpoint(store, account, selected);
17297
+ return emails;
17298
+ }
17299
+ function formatNewEmail(account, msg, summary) {
17300
+ const body = selectBody(msg, "markdown");
17301
+ const bodyTruncated = body.length > BODY_LIMIT;
17302
+ return {
17303
+ account,
17304
+ id: msg.id,
17305
+ subject: msg.subject,
17306
+ from: msg.from,
17307
+ to: msg.to,
17308
+ cc: msg.cc,
17309
+ bcc: msg.bcc,
17310
+ receivedAt: msg.receivedAt ?? summary.receivedAt,
17311
+ preview: msg.preview,
17312
+ isRead: msg.isRead,
17313
+ hasAttachments: msg.hasAttachments,
17314
+ folder: msg.folder,
17315
+ attachments: msg.attachments,
17316
+ body: bodyTruncated ? body.slice(0, BODY_LIMIT) : body,
17317
+ bodyFormat: "markdown",
17318
+ bodyTruncated,
17319
+ bodyOriginalLength: body.length
17320
+ };
17321
+ }
17322
+ async function advanceCheckpoint(store, account, selected) {
17323
+ const ordered = oldestCandidatesFirst(selected);
17324
+ const newest = ordered[ordered.length - 1];
17325
+ if (!newest) return;
17326
+ const newestTimestamp = newest.timestamp;
17327
+ const idsAtNewest = ordered.filter((candidate) => candidate.timestamp === newestTimestamp).map((candidate) => candidate.summary.id);
17328
+ await store.updateNewEmailCheckpoint(account.email, {
17329
+ receivedAt: newestTimestamp,
17330
+ deliveredIdsAtReceivedAt: idsAtNewest
17331
+ });
17332
+ }
17333
+ function normalizeCheckpoint(checkpoint) {
17334
+ const receivedAt = normalizeTimestamp(checkpoint?.receivedAt);
17335
+ if (!receivedAt) return null;
17336
+ return {
17337
+ receivedAt,
17338
+ deliveredIdsAtReceivedAt: checkpoint?.deliveredIdsAtReceivedAt ?? []
17339
+ };
17340
+ }
17341
+ function effectiveReceivedAt(receivedAt) {
17342
+ return normalizeTimestamp(receivedAt) ?? MISSING_RECEIVED_AT;
17343
+ }
17344
+ function normalizeTimestamp(value) {
17345
+ if (!value) return null;
17346
+ const ms = Date.parse(value);
17347
+ if (!Number.isFinite(ms)) return null;
17348
+ return new Date(ms).toISOString();
17349
+ }
17350
+ function oldestCandidatesFirst(items) {
17351
+ return [...items].sort((a, b) => {
17352
+ const byTimestamp = compareTimestamp(a.timestamp, b.timestamp);
17353
+ if (byTimestamp !== 0) return byTimestamp;
17354
+ return a.summary.id.localeCompare(b.summary.id);
17355
+ });
17356
+ }
17357
+ function compareNewEmailOutputOldestFirst(a, b) {
17358
+ const byTimestamp = compareTimestamp(
17359
+ effectiveReceivedAt(a.receivedAt),
17360
+ effectiveReceivedAt(b.receivedAt)
17361
+ );
17362
+ if (byTimestamp !== 0) return byTimestamp;
17363
+ return a.id.localeCompare(b.id);
17364
+ }
17365
+ function compareTimestamp(a, b) {
17366
+ if (a < b) return -1;
17367
+ if (a > b) return 1;
17368
+ return 0;
17369
+ }
17370
+
17371
+ // src/tools/browse.ts
17372
+ function registerBrowseTools(server, ctx) {
17373
+ const { store, registry, tools } = ctx;
17374
+ const emailListOutputSchema = z5.object({
17375
+ account: z5.string(),
17376
+ count: z5.number(),
17377
+ items: z5.array(emailSummaryOutputSchema),
17378
+ skip: z5.number(),
17379
+ hasMore: z5.boolean()
17380
+ });
17381
+ const searchEmailsOutputSchema = z5.object({
17382
+ account: z5.string(),
17383
+ count: z5.number(),
17384
+ items: z5.array(emailSummaryOutputSchema)
17083
17385
  });
17084
17386
  if (shouldRegister("list_emails", tools)) {
17085
17387
  server.registerTool(
17086
17388
  "list_emails",
17087
17389
  {
17088
17390
  description: "List recent emails in a folder of the given account. Pass the user's email address as `account`; the server routes to the correct backend automatically.",
17089
- inputSchema: z4.object({
17090
- account: z4.string().email(),
17091
- folder: z4.string().default("inbox").optional(),
17092
- limit: z4.number().int().positive().max(100).optional(),
17093
- unreadOnly: z4.boolean().optional(),
17094
- skip: z4.number().int().min(0).optional()
17391
+ inputSchema: z5.object({
17392
+ account: z5.string().email(),
17393
+ folder: z5.string().default("inbox").optional(),
17394
+ limit: z5.number().int().positive().max(100).optional(),
17395
+ unreadOnly: z5.boolean().optional(),
17396
+ skip: z5.number().int().min(0).optional()
17095
17397
  }),
17096
17398
  outputSchema: emailListOutputSchema
17097
17399
  },
@@ -17118,15 +17420,16 @@ function registerBrowseTools(server, ctx) {
17118
17420
  }
17119
17421
  );
17120
17422
  }
17423
+ registerNewEmailTool(server, { store, registry, tools });
17121
17424
  if (shouldRegister("search_emails", tools)) {
17122
17425
  server.registerTool(
17123
17426
  "search_emails",
17124
17427
  {
17125
17428
  description: "Search emails by free-text query (KQL on Outlook). Returns lightweight summaries.",
17126
- inputSchema: z4.object({
17127
- account: z4.string().email(),
17128
- query: z4.string().min(1),
17129
- limit: z4.number().int().positive().max(100).optional()
17429
+ inputSchema: z5.object({
17430
+ account: z5.string().email(),
17431
+ query: z5.string().min(1),
17432
+ limit: z5.number().int().positive().max(100).optional()
17130
17433
  }),
17131
17434
  outputSchema: searchEmailsOutputSchema
17132
17435
  },
@@ -17148,31 +17451,31 @@ function registerBrowseTools(server, ctx) {
17148
17451
  }
17149
17452
  );
17150
17453
  }
17151
- const readEmailOutputSchema = z4.object({
17152
- id: z4.string(),
17153
- subject: z4.string(),
17454
+ const readEmailOutputSchema = z5.object({
17455
+ id: z5.string(),
17456
+ subject: z5.string(),
17154
17457
  from: emailAddrOutputSchema.optional(),
17155
- to: z4.array(emailAddrOutputSchema).optional(),
17156
- cc: z4.array(emailAddrOutputSchema).optional(),
17157
- bcc: z4.array(emailAddrOutputSchema).optional(),
17158
- receivedAt: z4.string().optional(),
17159
- preview: z4.string().optional(),
17160
- isRead: z4.boolean().optional(),
17161
- hasAttachments: z4.boolean().optional(),
17162
- folder: z4.string().optional(),
17163
- attachments: z4.array(attachmentMetaOutputSchema).optional(),
17164
- body: z4.string(),
17165
- bodyFormat: z4.enum(["markdown", "html", "text"])
17458
+ to: z5.array(emailAddrOutputSchema).optional(),
17459
+ cc: z5.array(emailAddrOutputSchema).optional(),
17460
+ bcc: z5.array(emailAddrOutputSchema).optional(),
17461
+ receivedAt: z5.string().optional(),
17462
+ preview: z5.string().optional(),
17463
+ isRead: z5.boolean().optional(),
17464
+ hasAttachments: z5.boolean().optional(),
17465
+ folder: z5.string().optional(),
17466
+ attachments: z5.array(attachmentMetaOutputSchema).optional(),
17467
+ body: z5.string(),
17468
+ bodyFormat: z5.enum(["markdown", "html", "text"])
17166
17469
  });
17167
17470
  if (shouldRegister("read_email", tools)) {
17168
17471
  server.registerTool(
17169
17472
  "read_email",
17170
17473
  {
17171
17474
  description: "Fetch a single email with full body and recipients by id. Body is returned as `body` with `bodyFormat` indicating the format. Default format is 'markdown' \u2014 HTML is automatically converted to save context tokens.",
17172
- inputSchema: z4.object({
17173
- account: z4.string().email(),
17174
- id: z4.string().min(1),
17175
- format: z4.enum(["markdown", "html", "text"]).default("markdown").optional().describe(
17475
+ inputSchema: z5.object({
17476
+ account: z5.string().email(),
17477
+ id: z5.string().min(1),
17478
+ format: z5.enum(["markdown", "html", "text"]).default("markdown").optional().describe(
17176
17479
  "Output body format. 'markdown' converts HTML to Markdown (default), 'html' returns the raw HTML, 'text' returns plain text."
17177
17480
  )
17178
17481
  }),
@@ -17207,20 +17510,20 @@ function registerBrowseTools(server, ctx) {
17207
17510
  }
17208
17511
  );
17209
17512
  }
17210
- const readAttachmentOutputSchema = z4.object({
17211
- name: z4.string(),
17212
- contentType: z4.string().optional(),
17213
- path: z4.string()
17513
+ const readAttachmentOutputSchema = z5.object({
17514
+ name: z5.string(),
17515
+ contentType: z5.string().optional(),
17516
+ path: z5.string()
17214
17517
  });
17215
17518
  if (shouldRegister("read_attachment", tools)) {
17216
17519
  server.registerTool(
17217
17520
  "read_attachment",
17218
17521
  {
17219
17522
  description: "Download an email attachment to a temporary file and return its path. Use messageId and attachmentId from a prior read_email call.",
17220
- inputSchema: z4.object({
17221
- account: z4.string().email(),
17222
- messageId: z4.string().min(1),
17223
- attachmentId: z4.string().min(1)
17523
+ inputSchema: z5.object({
17524
+ account: z5.string().email(),
17525
+ messageId: z5.string().min(1),
17526
+ attachmentId: z5.string().min(1)
17224
17527
  }),
17225
17528
  outputSchema: readAttachmentOutputSchema
17226
17529
  },
@@ -17242,22 +17545,22 @@ function registerBrowseTools(server, ctx) {
17242
17545
  }
17243
17546
 
17244
17547
  // src/tools/folders.ts
17245
- import { z as z5 } from "zod";
17548
+ import { z as z6 } from "zod";
17246
17549
  function registerFolderTools(server, ctx) {
17247
17550
  const { registry, tools } = ctx;
17248
- const listFoldersOutputSchema = z5.object({
17249
- account: z5.string(),
17250
- count: z5.number(),
17251
- items: z5.array(folderInfoOutputSchema)
17551
+ const listFoldersOutputSchema = z6.object({
17552
+ account: z6.string(),
17553
+ count: z6.number(),
17554
+ items: z6.array(folderInfoOutputSchema)
17252
17555
  });
17253
17556
  if (shouldRegister("list_folders", tools)) {
17254
17557
  server.registerTool(
17255
17558
  "list_folders",
17256
17559
  {
17257
17560
  description: "List available mail folders. Returns top-level folders by default, or child folders of the given parent when `parentFolderId` is provided.",
17258
- inputSchema: z5.object({
17259
- account: z5.string().email(),
17260
- parentFolderId: z5.string().optional().describe(
17561
+ inputSchema: z6.object({
17562
+ account: z6.string().email(),
17563
+ parentFolderId: z6.string().optional().describe(
17261
17564
  "When provided, lists child folders of this folder. When omitted, lists top-level folders (children of the root)."
17262
17565
  )
17263
17566
  }),
@@ -17281,8 +17584,8 @@ function registerFolderTools(server, ctx) {
17281
17584
  }
17282
17585
  );
17283
17586
  }
17284
- const createFolderOutputSchema = z5.object({
17285
- created: z5.literal(true),
17587
+ const createFolderOutputSchema = z6.object({
17588
+ created: z6.literal(true),
17286
17589
  folder: folderInfoOutputSchema
17287
17590
  });
17288
17591
  if (shouldRegister("create_folder", tools)) {
@@ -17290,10 +17593,10 @@ function registerFolderTools(server, ctx) {
17290
17593
  "create_folder",
17291
17594
  {
17292
17595
  description: "Create a new mail folder. Creates under the root folder by default, or under the specified parent when `parentFolderId` is provided. Disabled in --read-only mode.",
17293
- inputSchema: z5.object({
17294
- account: z5.string().email(),
17295
- displayName: z5.string().min(1).describe("Name of the new folder"),
17296
- parentFolderId: z5.string().optional().describe(
17596
+ inputSchema: z6.object({
17597
+ account: z6.string().email(),
17598
+ displayName: z6.string().min(1).describe("Name of the new folder"),
17599
+ parentFolderId: z6.string().optional().describe(
17297
17600
  "When provided, creates the folder as a child of this folder. When omitted, creates under the root folder."
17298
17601
  )
17299
17602
  }),
@@ -17314,18 +17617,18 @@ function registerFolderTools(server, ctx) {
17314
17617
  }
17315
17618
  );
17316
17619
  }
17317
- const deleteFolderOutputSchema = z5.object({
17318
- deleted: z5.literal(true),
17319
- id: z5.string()
17620
+ const deleteFolderOutputSchema = z6.object({
17621
+ deleted: z6.literal(true),
17622
+ id: z6.string()
17320
17623
  });
17321
17624
  if (shouldRegister("delete_folder", tools)) {
17322
17625
  server.registerTool(
17323
17626
  "delete_folder",
17324
17627
  {
17325
17628
  description: "Delete a mail folder by ID. Disabled in --read-only mode.",
17326
- inputSchema: z5.object({
17327
- account: z5.string().email(),
17328
- folderId: z5.string().min(1).describe("ID of the folder to delete")
17629
+ inputSchema: z6.object({
17630
+ account: z6.string().email(),
17631
+ folderId: z6.string().min(1).describe("ID of the folder to delete")
17329
17632
  }),
17330
17633
  outputSchema: deleteFolderOutputSchema
17331
17634
  },
@@ -17341,8 +17644,8 @@ function registerFolderTools(server, ctx) {
17341
17644
  }
17342
17645
  );
17343
17646
  }
17344
- const renameFolderOutputSchema = z5.object({
17345
- renamed: z5.literal(true),
17647
+ const renameFolderOutputSchema = z6.object({
17648
+ renamed: z6.literal(true),
17346
17649
  folder: folderInfoOutputSchema
17347
17650
  });
17348
17651
  if (shouldRegister("rename_folder", tools)) {
@@ -17350,10 +17653,10 @@ function registerFolderTools(server, ctx) {
17350
17653
  "rename_folder",
17351
17654
  {
17352
17655
  description: "Rename an existing mail folder. Disabled in --read-only mode.",
17353
- inputSchema: z5.object({
17354
- account: z5.string().email(),
17355
- folderId: z5.string().min(1).describe("ID of the folder to rename"),
17356
- newName: z5.string().min(1).describe("New display name for the folder")
17656
+ inputSchema: z6.object({
17657
+ account: z6.string().email(),
17658
+ folderId: z6.string().min(1).describe("ID of the folder to rename"),
17659
+ newName: z6.string().min(1).describe("New display name for the folder")
17357
17660
  }),
17358
17661
  outputSchema: renameFolderOutputSchema
17359
17662
  },
@@ -17376,7 +17679,7 @@ function registerFolderTools(server, ctx) {
17376
17679
  }
17377
17680
 
17378
17681
  // src/tools/organize.ts
17379
- import { z as z6 } from "zod";
17682
+ import { z as z7 } from "zod";
17380
17683
  function registerOrganizeTools(server, ctx) {
17381
17684
  const { registry, tools } = ctx;
17382
17685
  async function moveToWellKnown(args, destination, resultKey) {
@@ -17392,13 +17695,13 @@ function registerOrganizeTools(server, ctx) {
17392
17695
  const data = { marked: true, id: args.id, isRead };
17393
17696
  return ok(data, data);
17394
17697
  }
17395
- const archiveMoveSchema = z6.object({
17396
- account: z6.string().email(),
17397
- id: z6.string().min(1).describe("Message ID to move")
17698
+ const archiveMoveSchema = z7.object({
17699
+ account: z7.string().email(),
17700
+ id: z7.string().min(1).describe("Message ID to move")
17398
17701
  });
17399
- const archiveOutputSchema = z6.object({
17400
- archived: z6.literal(true),
17401
- id: z6.string()
17702
+ const archiveOutputSchema = z7.object({
17703
+ archived: z7.literal(true),
17704
+ id: z7.string()
17402
17705
  });
17403
17706
  if (shouldRegister("archive_email", tools)) {
17404
17707
  server.registerTool(
@@ -17417,9 +17720,9 @@ function registerOrganizeTools(server, ctx) {
17417
17720
  }
17418
17721
  );
17419
17722
  }
17420
- const trashOutputSchema = z6.object({
17421
- trashed: z6.literal(true),
17422
- id: z6.string()
17723
+ const trashOutputSchema = z7.object({
17724
+ trashed: z7.literal(true),
17725
+ id: z7.string()
17423
17726
  });
17424
17727
  if (shouldRegister("trash_email", tools)) {
17425
17728
  server.registerTool(
@@ -17438,20 +17741,20 @@ function registerOrganizeTools(server, ctx) {
17438
17741
  }
17439
17742
  );
17440
17743
  }
17441
- const moveEmailOutputSchema = z6.object({
17442
- moved: z6.literal(true),
17443
- id: z6.string(),
17444
- destination: z6.string()
17744
+ const moveEmailOutputSchema = z7.object({
17745
+ moved: z7.literal(true),
17746
+ id: z7.string(),
17747
+ destination: z7.string()
17445
17748
  });
17446
17749
  if (shouldRegister("move_email", tools)) {
17447
17750
  server.registerTool(
17448
17751
  "move_email",
17449
17752
  {
17450
17753
  description: "Move a message to any folder by well-known name (e.g. 'inbox', 'drafts', 'junkemail', 'sentitems', 'outbox') or custom folder ID. Disabled in --read-only mode.",
17451
- inputSchema: z6.object({
17452
- account: z6.string().email(),
17453
- id: z6.string().min(1).describe("Message ID to move"),
17454
- destination: z6.string().min(1).describe(
17754
+ inputSchema: z7.object({
17755
+ account: z7.string().email(),
17756
+ id: z7.string().min(1).describe("Message ID to move"),
17757
+ destination: z7.string().min(1).describe(
17455
17758
  "Destination folder \u2014 a well-known folder name ('archive', 'deleteditems', 'inbox', 'drafts', 'junkemail', 'sentitems', 'outbox') or a raw folder ID."
17456
17759
  )
17457
17760
  }),
@@ -17473,14 +17776,14 @@ function registerOrganizeTools(server, ctx) {
17473
17776
  }
17474
17777
  );
17475
17778
  }
17476
- const markReadInputSchema = z6.object({
17477
- account: z6.string().email(),
17478
- id: z6.string().min(1).describe("Message ID to mark as read")
17779
+ const markReadInputSchema = z7.object({
17780
+ account: z7.string().email(),
17781
+ id: z7.string().min(1).describe("Message ID to mark as read")
17479
17782
  });
17480
- const markReadOutputSchema = z6.object({
17481
- marked: z6.literal(true),
17482
- id: z6.string(),
17483
- isRead: z6.boolean()
17783
+ const markReadOutputSchema = z7.object({
17784
+ marked: z7.literal(true),
17785
+ id: z7.string(),
17786
+ isRead: z7.boolean()
17484
17787
  });
17485
17788
  if (shouldRegister("mark_read", tools)) {
17486
17789
  server.registerTool(
@@ -17519,7 +17822,7 @@ function registerOrganizeTools(server, ctx) {
17519
17822
  }
17520
17823
 
17521
17824
  // src/tools/compose.ts
17522
- import { z as z7 } from "zod";
17825
+ import { z as z8 } from "zod";
17523
17826
  import { readFileSync } from "fs";
17524
17827
  import { basename, extname } from "path";
17525
17828
  function registerComposeTools(server, ctx) {
@@ -17549,32 +17852,32 @@ function registerComposeTools(server, ctx) {
17549
17852
  ".mp3": "audio/mpeg",
17550
17853
  ".mp4": "video/mp4"
17551
17854
  };
17552
- const sendEmailSchema = z7.object({
17553
- account: z7.string().email(),
17554
- to: z7.array(emailAddrSchema).min(1),
17555
- cc: z7.array(emailAddrSchema).optional(),
17556
- bcc: z7.array(emailAddrSchema).optional(),
17557
- subject: z7.string(),
17558
- body: z7.string(),
17559
- format: z7.enum(["html", "markdown"]).describe(
17855
+ const sendEmailSchema = z8.object({
17856
+ account: z8.string().email(),
17857
+ to: z8.array(emailAddrSchema).min(1),
17858
+ cc: z8.array(emailAddrSchema).optional(),
17859
+ bcc: z8.array(emailAddrSchema).optional(),
17860
+ subject: z8.string(),
17861
+ body: z8.string(),
17862
+ format: z8.enum(["html", "markdown"]).describe(
17560
17863
  "Body format. 'html' sends the body as-is (must be valid HTML). 'markdown' converts the body from Markdown to HTML for clean rendering on the recipient side."
17561
17864
  ),
17562
- include_signature: z7.boolean().describe(
17865
+ include_signature: z8.boolean().describe(
17563
17866
  "Whether to append the account's saved HTML signature to the email. If true, don't include a signature in the body param to avoid double signature. Returns an error if true but no signature is configured for this account."
17564
17867
  ),
17565
- inReplyTo: z7.union([z7.string(), z7.literal(false)]).describe(
17868
+ inReplyTo: z8.union([z8.string(), z8.literal(false)]).describe(
17566
17869
  "Message ID to reply to. When set, sends as a threaded reply which includes the quoted thread history automatically. Set to `false` for a new email (not a reply)."
17567
17870
  ),
17568
- replyAll: z7.boolean().default(false).optional().describe(
17871
+ replyAll: z8.boolean().default(false).optional().describe(
17569
17872
  "When true and `inReplyTo` is set, reply to all recipients instead of just the sender."
17570
17873
  ),
17571
- forwardMessageId: z7.string().optional().describe(
17874
+ forwardMessageId: z8.string().optional().describe(
17572
17875
  "Message ID to forward. When set, sends as a forward of the specified message, preserving the original content. Mutually exclusive with `inReplyTo`."
17573
17876
  ),
17574
- attachments: z7.array(
17575
- z7.object({
17576
- filePath: z7.string().min(1).describe("Absolute path to a local file"),
17577
- name: z7.string().optional().describe(
17877
+ attachments: z8.array(
17878
+ z8.object({
17879
+ filePath: z8.string().min(1).describe("Absolute path to a local file"),
17880
+ name: z8.string().optional().describe(
17578
17881
  "Attachment filename. Defaults to the file's basename."
17579
17882
  )
17580
17883
  })
@@ -17637,8 +17940,8 @@ function registerComposeTools(server, ctx) {
17637
17940
  }
17638
17941
  }
17639
17942
  const sendEmailOutputSchema = {
17640
- sent: z7.literal(true),
17641
- id: z7.string()
17943
+ sent: z8.literal(true),
17944
+ id: z8.string()
17642
17945
  };
17643
17946
  if (shouldRegister("send_email", tools)) {
17644
17947
  server.registerTool(
@@ -17657,9 +17960,9 @@ function registerComposeTools(server, ctx) {
17657
17960
  );
17658
17961
  }
17659
17962
  const draftEmailOutputSchema = {
17660
- draft: z7.literal(true),
17661
- id: z7.string(),
17662
- draftHtml: z7.string().optional()
17963
+ draft: z8.literal(true),
17964
+ id: z8.string(),
17965
+ draftHtml: z8.string().optional()
17663
17966
  };
17664
17967
  if (shouldRegister("draft_email", tools)) {
17665
17968
  server.registerTool(
@@ -17677,38 +17980,38 @@ function registerComposeTools(server, ctx) {
17677
17980
  )
17678
17981
  );
17679
17982
  }
17680
- const editDraftSchema = z7.object({
17681
- account: z7.string().email(),
17682
- id: z7.string().min(1).describe("Draft message ID to edit"),
17683
- to: z7.array(emailAddrSchema).optional(),
17684
- cc: z7.array(emailAddrSchema).optional(),
17685
- bcc: z7.array(emailAddrSchema).optional(),
17686
- subject: z7.string().optional(),
17687
- body: z7.string().optional(),
17688
- format: z7.enum(["html", "markdown"]).optional().describe(
17983
+ const editDraftSchema = z8.object({
17984
+ account: z8.string().email(),
17985
+ id: z8.string().min(1).describe("Draft message ID to edit"),
17986
+ to: z8.array(emailAddrSchema).optional(),
17987
+ cc: z8.array(emailAddrSchema).optional(),
17988
+ bcc: z8.array(emailAddrSchema).optional(),
17989
+ subject: z8.string().optional(),
17990
+ body: z8.string().optional(),
17991
+ format: z8.enum(["html", "markdown"]).optional().describe(
17689
17992
  "Body format. Only meaningful when `body` is also provided. 'html' sends the body as-is (must be valid HTML). 'markdown' converts the body from Markdown to HTML for clean rendering on the recipient side."
17690
17993
  ),
17691
- include_signature: z7.boolean().optional().describe(
17994
+ include_signature: z8.boolean().optional().describe(
17692
17995
  "Whether to re-apply the account's saved HTML signature to the body. If true, don't include a signature in the body param. Only meaningful when `body` is also provided. Returns an error if true but no signature is configured for this account."
17693
17996
  ),
17694
- new_attachments: z7.array(
17695
- z7.object({
17696
- filePath: z7.string().min(1).describe("Absolute path to a local file"),
17697
- name: z7.string().optional().describe(
17997
+ new_attachments: z8.array(
17998
+ z8.object({
17999
+ filePath: z8.string().min(1).describe("Absolute path to a local file"),
18000
+ name: z8.string().optional().describe(
17698
18001
  "Attachment filename. Defaults to the file's basename."
17699
18002
  )
17700
18003
  })
17701
18004
  ).optional().describe(
17702
18005
  "New file attachments to add to the draft. The server reads the files from disk and base64-encodes them automatically."
17703
18006
  ),
17704
- remove_attachments: z7.array(z7.string().min(1)).optional().describe(
18007
+ remove_attachments: z8.array(z8.string().min(1)).optional().describe(
17705
18008
  "Attachment IDs to remove from the draft. Get attachment IDs from read_email."
17706
18009
  )
17707
18010
  });
17708
18011
  const editDraftOutputSchema = {
17709
- edited: z7.literal(true),
17710
- id: z7.string(),
17711
- draftHtml: z7.string().optional()
18012
+ edited: z8.literal(true),
18013
+ id: z8.string(),
18014
+ draftHtml: z8.string().optional()
17712
18015
  };
17713
18016
  if (shouldRegister("edit_draft", tools)) {
17714
18017
  server.registerTool(
@@ -17800,8 +18103,8 @@ function registerComposeTools(server, ctx) {
17800
18103
  );
17801
18104
  }
17802
18105
  const sendDraftOutputSchema = {
17803
- sent: z7.literal(true),
17804
- id: z7.string()
18106
+ sent: z8.literal(true),
18107
+ id: z8.string()
17805
18108
  };
17806
18109
  if (shouldRegister("send_draft", tools)) {
17807
18110
  server.registerTool(
@@ -17809,8 +18112,8 @@ function registerComposeTools(server, ctx) {
17809
18112
  {
17810
18113
  description: "Send an existing draft email by ID. Use this with draft IDs returned by `draft_email` or `edit_draft`. Disabled in --read-only mode.",
17811
18114
  inputSchema: {
17812
- account: z7.string().email(),
17813
- id: z7.string().min(1).describe("Draft message ID to send")
18115
+ account: z8.string().email(),
18116
+ id: z8.string().min(1).describe("Draft message ID to send")
17814
18117
  },
17815
18118
  outputSchema: sendDraftOutputSchema
17816
18119
  },
@@ -17832,7 +18135,7 @@ function registerComposeTools(server, ctx) {
17832
18135
  function registerTools(server, opts) {
17833
18136
  const { store, registry, tools } = opts;
17834
18137
  registerAccountTools(server, { store, registry, tools });
17835
- registerBrowseTools(server, { registry, tools });
18138
+ registerBrowseTools(server, { store, registry, tools });
17836
18139
  registerFolderTools(server, { registry, tools });
17837
18140
  registerOrganizeTools(server, { registry, tools });
17838
18141
  registerComposeTools(server, { store, registry, tools });
@@ -17841,7 +18144,7 @@ function registerTools(server, opts) {
17841
18144
  // package.json
17842
18145
  var package_default = {
17843
18146
  name: "hypermail-mcp",
17844
- version: "0.7.8",
18147
+ version: "0.7.10",
17845
18148
  description: "Unified email MCP server \u2014 operate any inbox (Outlook now, IMAP/Gmail later) by passing an email address.",
17846
18149
  type: "module",
17847
18150
  bin: {
@@ -17913,193 +18216,6 @@ var package_default = {
17913
18216
  // src/version.ts
17914
18217
  var VERSION = package_default.version;
17915
18218
 
17916
- // src/watcher/webhook.ts
17917
- async function postWebhook(email, config) {
17918
- if (!config.webhook) return false;
17919
- const { url, retry } = config.webhook;
17920
- const maxAttempts = retry.maxAttempts;
17921
- const baseDelayMs = retry.baseDelayMs;
17922
- for (let attempt = 0; attempt < maxAttempts; attempt++) {
17923
- if (attempt > 0) {
17924
- const delay = baseDelayMs * 2 ** (attempt - 1);
17925
- await sleep(delay);
17926
- }
17927
- try {
17928
- const res = await fetch(url, {
17929
- method: "POST",
17930
- headers: { "content-type": "application/json" },
17931
- body: JSON.stringify(email)
17932
- });
17933
- if (res.ok) return true;
17934
- console.error(
17935
- `[hypermail-watch] webhook POST ${email.id} attempt ${attempt + 1}/${maxAttempts}: HTTP ${res.status}`
17936
- );
17937
- } catch (err) {
17938
- const code = err.code ?? "";
17939
- console.error(
17940
- `[hypermail-watch] webhook POST ${email.id} attempt ${attempt + 1}/${maxAttempts}: ${code || String(err)}`
17941
- );
17942
- }
17943
- }
17944
- console.error(
17945
- `[hypermail-watch] webhook delivery failed after ${maxAttempts} retries for ${email.id}`
17946
- );
17947
- return false;
17948
- }
17949
- function sleep(ms) {
17950
- return new Promise((resolve) => setTimeout(resolve, ms));
17951
- }
17952
-
17953
- // src/watcher/script.ts
17954
- import { spawn } from "child_process";
17955
- async function runNotifyCommand(email, config) {
17956
- if (!config.notifyCommand) return false;
17957
- const { command, timeoutMs, retry } = config.notifyCommand;
17958
- const maxAttempts = retry.maxAttempts;
17959
- const baseDelayMs = retry.baseDelayMs;
17960
- for (let attempt = 0; attempt < maxAttempts; attempt++) {
17961
- if (attempt > 0) {
17962
- const delay = baseDelayMs * 2 ** (attempt - 1);
17963
- await sleep2(delay);
17964
- }
17965
- try {
17966
- const ok2 = await spawnWithTimeout(
17967
- command,
17968
- JSON.stringify(email),
17969
- timeoutMs
17970
- );
17971
- if (ok2) return true;
17972
- console.error(
17973
- `[hypermail-watch] notify command ${email.id} attempt ${attempt + 1}/${maxAttempts}: non-zero exit code`
17974
- );
17975
- } catch (err) {
17976
- console.error(
17977
- `[hypermail-watch] notify command ${email.id} attempt ${attempt + 1}/${maxAttempts}: ${String(err)}`
17978
- );
17979
- }
17980
- }
17981
- console.error(
17982
- `[hypermail-watch] notify command delivery failed after ${maxAttempts} retries for ${email.id}`
17983
- );
17984
- return false;
17985
- }
17986
- function spawnWithTimeout(command, stdinData, timeoutMs) {
17987
- return new Promise((resolve) => {
17988
- const child = spawn(command, {
17989
- shell: true,
17990
- stdio: ["pipe", "pipe", "pipe"]
17991
- });
17992
- let stderr = "";
17993
- child.stderr?.on("data", (chunk) => {
17994
- stderr += chunk.toString("utf-8");
17995
- });
17996
- const timer = setTimeout(() => {
17997
- child.kill("SIGTERM");
17998
- if (stderr) {
17999
- console.error(`[hypermail-watch] notify command timed out after ${timeoutMs}ms. stderr:
18000
- ${stderr}`);
18001
- }
18002
- resolve(false);
18003
- }, timeoutMs);
18004
- child.on("close", (code) => {
18005
- clearTimeout(timer);
18006
- if (stderr && code !== 0) {
18007
- console.error(`[hypermail-watch] notify command stderr:
18008
- ${stderr}`);
18009
- }
18010
- resolve(code === 0);
18011
- });
18012
- child.on("error", (err) => {
18013
- clearTimeout(timer);
18014
- console.error(`[hypermail-watch] notify command spawn error: ${err.message}`);
18015
- resolve(false);
18016
- });
18017
- child.stdin?.end(stdinData);
18018
- });
18019
- }
18020
- function sleep2(ms) {
18021
- return new Promise((resolve) => setTimeout(resolve, ms));
18022
- }
18023
-
18024
- // src/watcher/manager.ts
18025
- var WatcherManager = class {
18026
- constructor(store, registry, config) {
18027
- this.store = store;
18028
- this.registry = registry;
18029
- this.config = config;
18030
- }
18031
- store;
18032
- registry;
18033
- config;
18034
- intervalId = null;
18035
- /** Start the poll loop. Fires immediately on the first tick, then every
18036
- * `pollIntervalSeconds`. Safe to call multiple times — subsequent calls
18037
- * are no-ops. */
18038
- start() {
18039
- if (this.intervalId !== null) return;
18040
- this.poll();
18041
- this.intervalId = setInterval(
18042
- () => this.poll(),
18043
- this.config.pollIntervalSeconds * 1e3
18044
- );
18045
- }
18046
- /** Stop the poll loop and release the interval. Safe to call when already
18047
- * stopped. */
18048
- stop() {
18049
- if (this.intervalId !== null) {
18050
- clearInterval(this.intervalId);
18051
- this.intervalId = null;
18052
- }
18053
- }
18054
- // ── private ──
18055
- async poll() {
18056
- const accounts = this.store.listAccounts();
18057
- for (const acct of accounts) {
18058
- try {
18059
- await this.pollAccount(acct.email);
18060
- } catch (err) {
18061
- console.error(
18062
- `[hypermail-watch] poll failed for ${acct.email}:`,
18063
- err
18064
- );
18065
- }
18066
- }
18067
- }
18068
- async pollAccount(email) {
18069
- const { provider, account } = this.registry.resolveByEmail(email);
18070
- const result = await provider.listEmails(account, {
18071
- folder: "inbox",
18072
- limit: 50
18073
- });
18074
- const knownIds = [...account.lastSeenIds ?? []];
18075
- const newEmails = result.items.filter((e) => !knownIds.includes(e.id));
18076
- if (newEmails.length === 0) return;
18077
- for (const summary of newEmails) {
18078
- try {
18079
- const full = await provider.readEmail(account, summary.id);
18080
- await this.emit(full);
18081
- knownIds.unshift(summary.id);
18082
- } catch (err) {
18083
- console.error(
18084
- `[hypermail-watch] emission failed for ${email}/${summary.id}:`,
18085
- err
18086
- );
18087
- }
18088
- }
18089
- const capped = knownIds.slice(0, 200);
18090
- await this.store.upsertAccount({ ...account, lastSeenIds: capped });
18091
- }
18092
- async emit(full) {
18093
- await postWebhook(full, this.config);
18094
- runNotifyCommand(full, this.config).catch((err) => {
18095
- console.error(
18096
- `[hypermail-watch] notify command unhandled error for ${full.id}:`,
18097
- err
18098
- );
18099
- });
18100
- }
18101
- };
18102
-
18103
18219
  // src/config/load.ts
18104
18220
  var ENV_DATA_DIR = "HYPERMAIL_DATA_DIR";
18105
18221
  var ENV_KEY = "HYPERMAIL_KEY";
@@ -18113,22 +18229,9 @@ var ENV_OUTLOOK_TENANT_ID = "HYPERMAIL_OUTLOOK_TENANT_ID";
18113
18229
  var ENV_GMAIL_CLIENT_ID = "HYPERMAIL_GMAIL_CLIENT_ID";
18114
18230
  var ENV_GMAIL_CLIENT_SECRET = "HYPERMAIL_GMAIL_CLIENT_SECRET";
18115
18231
  var ENV_GMAIL_REDIRECT_URI = "HYPERMAIL_GMAIL_REDIRECT_URI";
18116
- var ENV_WATCH_ENABLED = "HYPERMAIL_WATCH_ENABLED";
18117
- var ENV_WATCH_POLL_SECONDS = "HYPERMAIL_WATCH_POLL_SECONDS";
18118
- var ENV_WATCH_WEBHOOK_URL = "HYPERMAIL_WATCH_WEBHOOK_URL";
18119
- var ENV_WATCH_WEBHOOK_RETRY_ATTEMPTS = "HYPERMAIL_WATCH_WEBHOOK_RETRY_ATTEMPTS";
18120
- var ENV_WATCH_WEBHOOK_RETRY_DELAY_MS = "HYPERMAIL_WATCH_WEBHOOK_RETRY_DELAY_MS";
18121
- var ENV_WATCH_NOTIFY_COMMAND = "HYPERMAIL_WATCH_NOTIFY_COMMAND";
18122
- var ENV_WATCH_NOTIFY_TIMEOUT_MS = "HYPERMAIL_WATCH_NOTIFY_TIMEOUT_MS";
18123
- var ENV_WATCH_NOTIFY_RETRY_ATTEMPTS = "HYPERMAIL_WATCH_NOTIFY_RETRY_ATTEMPTS";
18124
- var ENV_WATCH_NOTIFY_RETRY_DELAY_MS = "HYPERMAIL_WATCH_NOTIFY_RETRY_DELAY_MS";
18125
18232
  var DEFAULT_TRANSPORT = "stdio";
18126
18233
  var DEFAULT_HTTP_PORT = 3e3;
18127
18234
  var DEFAULT_HTTP_HOST = "127.0.0.1";
18128
- var DEFAULT_WATCH_POLL_SECONDS = 10;
18129
- var DEFAULT_RETRY_ATTEMPTS = 5;
18130
- var DEFAULT_RETRY_DELAY_MS = 1e3;
18131
- var DEFAULT_NOTIFY_TIMEOUT_MS = 3e4;
18132
18235
  function envRaw(name) {
18133
18236
  return process.env[name];
18134
18237
  }
@@ -18138,14 +18241,6 @@ function optionalEnvString(name) {
18138
18241
  const trimmed = value.trim();
18139
18242
  return trimmed.length > 0 ? trimmed : void 0;
18140
18243
  }
18141
- function parseBoolEnv(name) {
18142
- const value = envRaw(name);
18143
- if (value === void 0) return void 0;
18144
- const lower = value.trim().toLowerCase();
18145
- if (lower === "true") return true;
18146
- if (lower === "false") return false;
18147
- throw new Error(`${name} must be either "true" or "false"`);
18148
- }
18149
18244
  function parseTransportEnv() {
18150
18245
  const value = envRaw(ENV_TRANSPORT);
18151
18246
  if (value === void 0) return void 0;
@@ -18163,15 +18258,6 @@ function parsePositiveInteger(value) {
18163
18258
  const parsed = Number(trimmed);
18164
18259
  return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : void 0;
18165
18260
  }
18166
- function parsePositiveIntegerEnv(name, defaultValue) {
18167
- const value = envRaw(name);
18168
- if (value === void 0) return defaultValue;
18169
- const parsed = parsePositiveInteger(value);
18170
- if (parsed === void 0) {
18171
- throw new Error(`${name} must be a positive integer`);
18172
- }
18173
- return parsed;
18174
- }
18175
18261
  function parseStringArray(value) {
18176
18262
  if (value === void 0) return void 0;
18177
18263
  const trimmed = value.trim();
@@ -18189,17 +18275,6 @@ function validateToolNames(toolNames, envName) {
18189
18275
  }
18190
18276
  }
18191
18277
  }
18192
- function validateWebhookUrl(raw) {
18193
- try {
18194
- const url = new URL(raw);
18195
- if (url.protocol !== "http:" && url.protocol !== "https:") {
18196
- throw new Error("unsupported protocol");
18197
- }
18198
- return raw;
18199
- } catch {
18200
- throw new Error(`${ENV_WATCH_WEBHOOK_URL} must be a valid http(s) URL`);
18201
- }
18202
- }
18203
18278
  function resolveHttpConfig(transport, cliOverrides, warnings) {
18204
18279
  const portSource = cliOverrides.port !== void 0 ? "--port" : ENV_HTTP_PORT;
18205
18280
  const rawPort = cliOverrides.port ?? envRaw(ENV_HTTP_PORT);
@@ -18263,68 +18338,12 @@ function resolveProvidersConfig() {
18263
18338
  }
18264
18339
  return providers;
18265
18340
  }
18266
- function resolveRetryConfig(attemptsEnv, delayEnv) {
18267
- return {
18268
- maxAttempts: parsePositiveIntegerEnv(attemptsEnv, DEFAULT_RETRY_ATTEMPTS),
18269
- baseDelayMs: parsePositiveIntegerEnv(delayEnv, DEFAULT_RETRY_DELAY_MS)
18270
- };
18271
- }
18272
- function resolveWatchConfig() {
18273
- const enabled = parseBoolEnv(ENV_WATCH_ENABLED) ?? false;
18274
- if (!enabled) return void 0;
18275
- const pollIntervalSeconds = parsePositiveIntegerEnv(
18276
- ENV_WATCH_POLL_SECONDS,
18277
- DEFAULT_WATCH_POLL_SECONDS
18278
- );
18279
- const webhookUrl = optionalEnvString(ENV_WATCH_WEBHOOK_URL);
18280
- let webhook;
18281
- if (webhookUrl) {
18282
- webhook = {
18283
- url: validateWebhookUrl(webhookUrl),
18284
- retry: resolveRetryConfig(
18285
- ENV_WATCH_WEBHOOK_RETRY_ATTEMPTS,
18286
- ENV_WATCH_WEBHOOK_RETRY_DELAY_MS
18287
- )
18288
- };
18289
- }
18290
- const rawNotifyCommand = envRaw(ENV_WATCH_NOTIFY_COMMAND);
18291
- let notifyCommand;
18292
- if (rawNotifyCommand !== void 0) {
18293
- const command = rawNotifyCommand.trim();
18294
- if (!command) {
18295
- throw new Error(`${ENV_WATCH_NOTIFY_COMMAND} must not be empty when watch is enabled`);
18296
- }
18297
- notifyCommand = {
18298
- command,
18299
- timeoutMs: parsePositiveIntegerEnv(
18300
- ENV_WATCH_NOTIFY_TIMEOUT_MS,
18301
- DEFAULT_NOTIFY_TIMEOUT_MS
18302
- ),
18303
- retry: resolveRetryConfig(
18304
- ENV_WATCH_NOTIFY_RETRY_ATTEMPTS,
18305
- ENV_WATCH_NOTIFY_RETRY_DELAY_MS
18306
- )
18307
- };
18308
- }
18309
- if (!webhook && !notifyCommand) {
18310
- throw new Error(
18311
- `${ENV_WATCH_ENABLED}=true requires ${ENV_WATCH_WEBHOOK_URL} or ${ENV_WATCH_NOTIFY_COMMAND}`
18312
- );
18313
- }
18314
- return {
18315
- enabled: true,
18316
- pollIntervalSeconds,
18317
- webhook,
18318
- notifyCommand
18319
- };
18320
- }
18321
18341
  function loadConfig(cliOverrides = {}) {
18322
18342
  const warnings = [];
18323
18343
  const transport = cliOverrides.transport ?? parseTransportEnv() ?? DEFAULT_TRANSPORT;
18324
18344
  const http = resolveHttpConfig(transport, cliOverrides, warnings);
18325
18345
  const tools = resolveToolsConfig();
18326
18346
  const providers = resolveProvidersConfig();
18327
- const watch = resolveWatchConfig();
18328
18347
  const dataDir = cliOverrides.dataDir ?? optionalEnvString(ENV_DATA_DIR);
18329
18348
  if (!optionalEnvString(ENV_KEY)) {
18330
18349
  warnings.push(
@@ -18337,8 +18356,7 @@ function loadConfig(cliOverrides = {}) {
18337
18356
  transport,
18338
18357
  http,
18339
18358
  tools,
18340
- providers,
18341
- watch
18359
+ providers
18342
18360
  },
18343
18361
  warnings
18344
18362
  };
@@ -18353,6 +18371,7 @@ var KNOWN_TOOLS = [
18353
18371
  "set_account_settings",
18354
18372
  "remove_account",
18355
18373
  "list_emails",
18374
+ "get_new_emails",
18356
18375
  "search_emails",
18357
18376
  "read_email",
18358
18377
  "read_attachment",
@@ -18386,14 +18405,6 @@ async function startServer(opts) {
18386
18405
  const store = await AccountStore.open({ dataDir: config.dataDir });
18387
18406
  const registry = buildRegistry({ store, providers: config.providers });
18388
18407
  const tools = resolveTools(config);
18389
- let watcher;
18390
- if (config.watch?.enabled) {
18391
- watcher = new WatcherManager(store, registry, config.watch);
18392
- watcher.start();
18393
- const stop = () => watcher?.stop();
18394
- process.on("SIGTERM", stop);
18395
- process.on("SIGINT", stop);
18396
- }
18397
18408
  const createServer = () => {
18398
18409
  const s = new McpServer(
18399
18410
  { name: "hypermail-mcp", version: VERSION },
@@ -18598,17 +18609,6 @@ Provider environment variables:
18598
18609
  HYPERMAIL_GMAIL_CLIENT_SECRET
18599
18610
  HYPERMAIL_GMAIL_REDIRECT_URI
18600
18611
 
18601
- Watcher environment variables:
18602
- HYPERMAIL_WATCH_ENABLED=true|false
18603
- HYPERMAIL_WATCH_POLL_SECONDS
18604
- HYPERMAIL_WATCH_WEBHOOK_URL
18605
- HYPERMAIL_WATCH_WEBHOOK_RETRY_ATTEMPTS
18606
- HYPERMAIL_WATCH_WEBHOOK_RETRY_DELAY_MS
18607
- HYPERMAIL_WATCH_NOTIFY_COMMAND
18608
- HYPERMAIL_WATCH_NOTIFY_TIMEOUT_MS
18609
- HYPERMAIL_WATCH_NOTIFY_RETRY_ATTEMPTS
18610
- HYPERMAIL_WATCH_NOTIFY_RETRY_DELAY_MS
18611
-
18612
18612
  Example:
18613
18613
  HYPERMAIL_TRANSPORT=http \\
18614
18614
  HYPERMAIL_HTTP_PORT=8080 \\