hypermail-mcp 0.7.7 → 0.7.9

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
@@ -15606,7 +15606,7 @@ async function startLocalCallbackServer() {
15606
15606
  return;
15607
15607
  }
15608
15608
  const callbackUrl = new URL(req.url, redirectUri);
15609
- if (callbackUrl.pathname !== "/oauth2callback") {
15609
+ if (callbackUrl.pathname !== "/callback") {
15610
15610
  sendHtml(res, 404, "Not found", "This local callback server only handles Gmail OAuth redirects.");
15611
15611
  return;
15612
15612
  }
@@ -15621,7 +15621,7 @@ async function startLocalCallbackServer() {
15621
15621
  });
15622
15622
  await new Promise((resolve, reject) => {
15623
15623
  server.once("error", reject);
15624
- server.listen(0, "127.0.0.1", () => {
15624
+ server.listen(33333, "127.0.0.1", () => {
15625
15625
  server.off("error", reject);
15626
15626
  resolve();
15627
15627
  });
@@ -15631,7 +15631,7 @@ async function startLocalCallbackServer() {
15631
15631
  closeServer();
15632
15632
  throw new Error("Failed to start local Gmail OAuth callback server");
15633
15633
  }
15634
- redirectUri = `http://127.0.0.1:${address.port}/oauth2callback`;
15634
+ redirectUri = "http://127.0.0.1:33333/callback";
15635
15635
  function closeServer() {
15636
15636
  if (closed) return;
15637
15637
  closed = true;
@@ -17038,7 +17038,7 @@ function registerAccountTools(server, ctx) {
17038
17038
  }
17039
17039
 
17040
17040
  // src/tools/browse.ts
17041
- import { z as z4 } from "zod";
17041
+ import { z as z5 } from "zod";
17042
17042
 
17043
17043
  // src/html-to-markdown.ts
17044
17044
  import TurndownService from "turndown";
@@ -17066,32 +17066,288 @@ function selectBody(msg, format) {
17066
17066
  }
17067
17067
  }
17068
17068
 
17069
- // src/tools/browse.ts
17070
- function registerBrowseTools(server, ctx) {
17071
- const { registry, tools } = ctx;
17072
- const emailListOutputSchema = z4.object({
17069
+ // src/tools/new-emails.ts
17070
+ import { z as z4 } from "zod";
17071
+ var DEFAULT_LIMIT = 10;
17072
+ var BODY_LIMIT = 2e4;
17073
+ var PAGE_SIZE = 100;
17074
+ var MISSING_RECEIVED_AT = "1970-01-01T00:00:00.000Z";
17075
+ function registerNewEmailTool(server, ctx) {
17076
+ const { store, registry, tools } = ctx;
17077
+ if (!shouldRegister("get_new_emails", tools)) return;
17078
+ const newEmailOutputSchema = z4.object({
17073
17079
  account: z4.string(),
17074
- count: z4.number(),
17075
- items: z4.array(emailSummaryOutputSchema),
17076
- skip: z4.number(),
17077
- hasMore: z4.boolean()
17080
+ id: z4.string(),
17081
+ subject: z4.string(),
17082
+ from: emailAddrOutputSchema.optional(),
17083
+ to: z4.array(emailAddrOutputSchema).optional(),
17084
+ cc: z4.array(emailAddrOutputSchema).optional(),
17085
+ bcc: z4.array(emailAddrOutputSchema).optional(),
17086
+ receivedAt: z4.string().optional(),
17087
+ preview: z4.string().optional(),
17088
+ isRead: z4.boolean().optional(),
17089
+ hasAttachments: z4.boolean().optional(),
17090
+ folder: z4.string().optional(),
17091
+ attachments: z4.array(attachmentMetaOutputSchema).optional(),
17092
+ body: z4.string(),
17093
+ bodyFormat: z4.literal("markdown"),
17094
+ bodyTruncated: z4.boolean(),
17095
+ bodyOriginalLength: z4.number()
17078
17096
  });
17079
- const searchEmailsOutputSchema = z4.object({
17080
- account: z4.string(),
17097
+ const outputSchema = z4.object({
17081
17098
  count: z4.number(),
17082
- items: z4.array(emailSummaryOutputSchema)
17099
+ emails: z4.array(newEmailOutputSchema),
17100
+ errors: z4.array(z4.object({ account: z4.string(), message: z4.string() }))
17101
+ });
17102
+ server.registerTool(
17103
+ "get_new_emails",
17104
+ {
17105
+ 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.",
17106
+ inputSchema: z4.object({
17107
+ account: z4.string().email().optional().describe(
17108
+ "Email account to poll. If omitted, polls all accounts with one global limit."
17109
+ ),
17110
+ limit: z4.number().int().min(0).default(DEFAULT_LIMIT).optional().describe(
17111
+ "Maximum emails to return. Defaults to 10. Use 0 to initialize/check without fetching bodies."
17112
+ )
17113
+ }),
17114
+ outputSchema
17115
+ },
17116
+ async (args) => {
17117
+ const limit = args.limit ?? DEFAULT_LIMIT;
17118
+ if (args.account) {
17119
+ try {
17120
+ const { provider, account } = registry.resolveByEmail(args.account);
17121
+ const result = await collectCandidatesForAccount(store, provider, account);
17122
+ const selected2 = oldestCandidatesFirst(result.candidates).slice(0, limit);
17123
+ const emails2 = limit === 0 ? [] : await hydrateAndAdvance(store, provider, result.account, selected2);
17124
+ const data2 = { count: emails2.length, emails: emails2, errors: [] };
17125
+ return ok(data2, data2);
17126
+ } catch (err) {
17127
+ return fail(errMsg(err));
17128
+ }
17129
+ }
17130
+ const accounts = store.listAccounts();
17131
+ if (accounts.length === 0) {
17132
+ return fail("no accounts registered. Call add_account first.");
17133
+ }
17134
+ const errors = [];
17135
+ const collected = [];
17136
+ const providersByEmail = /* @__PURE__ */ new Map();
17137
+ const accountsByEmail = /* @__PURE__ */ new Map();
17138
+ for (const stored of accounts) {
17139
+ try {
17140
+ const { provider, account } = registry.resolveByEmail(stored.email);
17141
+ const result = await collectCandidatesForAccount(store, provider, account);
17142
+ providersByEmail.set(result.account.email, provider);
17143
+ accountsByEmail.set(result.account.email, result.account);
17144
+ collected.push(...result.candidates);
17145
+ } catch (err) {
17146
+ errors.push({ account: stored.email, message: errMsg(err) });
17147
+ }
17148
+ }
17149
+ const selected = oldestCandidatesFirst(collected).slice(0, limit);
17150
+ const emails = [];
17151
+ if (limit > 0) {
17152
+ const byAccount = /* @__PURE__ */ new Map();
17153
+ for (const candidate of selected) {
17154
+ const items = byAccount.get(candidate.account.email) ?? [];
17155
+ items.push(candidate);
17156
+ byAccount.set(candidate.account.email, items);
17157
+ }
17158
+ for (const [email, accountCandidates] of byAccount) {
17159
+ const provider = providersByEmail.get(email);
17160
+ const account = accountsByEmail.get(email);
17161
+ if (!provider || !account) continue;
17162
+ try {
17163
+ emails.push(
17164
+ ...await hydrateAndAdvance(
17165
+ store,
17166
+ provider,
17167
+ account,
17168
+ accountCandidates
17169
+ )
17170
+ );
17171
+ } catch (err) {
17172
+ errors.push({ account: email, message: errMsg(err) });
17173
+ }
17174
+ }
17175
+ }
17176
+ const orderedEmails = emails.sort(compareNewEmailOutputOldestFirst);
17177
+ const data = { count: orderedEmails.length, emails: orderedEmails, errors };
17178
+ return ok(data, data);
17179
+ }
17180
+ );
17181
+ }
17182
+ async function collectCandidatesForAccount(store, provider, account) {
17183
+ const checkpoint = normalizeCheckpoint(account.newEmailCheckpoint);
17184
+ if (!checkpoint) {
17185
+ await initializeCheckpoint(store, provider, account);
17186
+ return { account, candidates: [] };
17187
+ }
17188
+ const deliveredAtCheckpoint = new Set(checkpoint.deliveredIdsAtReceivedAt ?? []);
17189
+ const candidates = [];
17190
+ let skip = 0;
17191
+ while (true) {
17192
+ const { items, hasMore } = await provider.listEmails(account, {
17193
+ folder: "inbox",
17194
+ limit: PAGE_SIZE,
17195
+ skip
17196
+ });
17197
+ if (items.length === 0) break;
17198
+ let sawOlderThanCheckpoint = false;
17199
+ for (const item of items) {
17200
+ const timestamp = effectiveReceivedAt(item.receivedAt);
17201
+ const comparison = compareTimestamp(timestamp, checkpoint.receivedAt);
17202
+ if (comparison > 0) {
17203
+ candidates.push({ account, summary: item, timestamp });
17204
+ } else if (comparison === 0) {
17205
+ if (!deliveredAtCheckpoint.has(item.id)) {
17206
+ candidates.push({ account, summary: item, timestamp });
17207
+ }
17208
+ } else {
17209
+ sawOlderThanCheckpoint = true;
17210
+ }
17211
+ }
17212
+ if (sawOlderThanCheckpoint || !hasMore) break;
17213
+ skip += items.length;
17214
+ }
17215
+ return { account, candidates };
17216
+ }
17217
+ async function initializeCheckpoint(store, provider, account) {
17218
+ const { items } = await provider.listEmails(account, {
17219
+ folder: "inbox",
17220
+ limit: PAGE_SIZE
17221
+ });
17222
+ const first = items[0];
17223
+ const receivedAt = first ? effectiveReceivedAt(first.receivedAt) : (/* @__PURE__ */ new Date()).toISOString();
17224
+ const deliveredIdsAtReceivedAt = items.filter((item) => effectiveReceivedAt(item.receivedAt) === receivedAt).map((item) => item.id);
17225
+ await store.upsertAccount({
17226
+ ...account,
17227
+ newEmailCheckpoint: { receivedAt, deliveredIdsAtReceivedAt }
17228
+ });
17229
+ }
17230
+ async function hydrateAndAdvance(store, provider, account, selected) {
17231
+ if (selected.length === 0) return [];
17232
+ const emails = [];
17233
+ for (const candidate of selected) {
17234
+ const full = await provider.readEmail(account, candidate.summary.id);
17235
+ emails.push(formatNewEmail(account.email, full, candidate.summary));
17236
+ }
17237
+ await advanceCheckpoint(store, account, selected);
17238
+ return emails;
17239
+ }
17240
+ function formatNewEmail(account, msg, summary) {
17241
+ const body = selectBody(msg, "markdown");
17242
+ const bodyTruncated = body.length > BODY_LIMIT;
17243
+ return {
17244
+ account,
17245
+ id: msg.id,
17246
+ subject: msg.subject,
17247
+ from: msg.from,
17248
+ to: msg.to,
17249
+ cc: msg.cc,
17250
+ bcc: msg.bcc,
17251
+ receivedAt: msg.receivedAt ?? summary.receivedAt,
17252
+ preview: msg.preview,
17253
+ isRead: msg.isRead,
17254
+ hasAttachments: msg.hasAttachments,
17255
+ folder: msg.folder,
17256
+ attachments: msg.attachments,
17257
+ body: bodyTruncated ? body.slice(0, BODY_LIMIT) : body,
17258
+ bodyFormat: "markdown",
17259
+ bodyTruncated,
17260
+ bodyOriginalLength: body.length
17261
+ };
17262
+ }
17263
+ async function advanceCheckpoint(store, account, selected) {
17264
+ const ordered = oldestCandidatesFirst(selected);
17265
+ const newest = ordered[ordered.length - 1];
17266
+ if (!newest) return;
17267
+ const previous = normalizeCheckpoint(account.newEmailCheckpoint);
17268
+ const newestTimestamp = newest.timestamp;
17269
+ 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
+ }
17285
+ });
17286
+ }
17287
+ function normalizeCheckpoint(checkpoint) {
17288
+ const receivedAt = normalizeTimestamp(checkpoint?.receivedAt);
17289
+ if (!receivedAt) return null;
17290
+ return {
17291
+ receivedAt,
17292
+ deliveredIdsAtReceivedAt: checkpoint?.deliveredIdsAtReceivedAt ?? []
17293
+ };
17294
+ }
17295
+ function effectiveReceivedAt(receivedAt) {
17296
+ return normalizeTimestamp(receivedAt) ?? MISSING_RECEIVED_AT;
17297
+ }
17298
+ function normalizeTimestamp(value) {
17299
+ if (!value) return null;
17300
+ const ms = Date.parse(value);
17301
+ if (!Number.isFinite(ms)) return null;
17302
+ return new Date(ms).toISOString();
17303
+ }
17304
+ function oldestCandidatesFirst(items) {
17305
+ return [...items].sort((a, b) => {
17306
+ const byTimestamp = compareTimestamp(a.timestamp, b.timestamp);
17307
+ if (byTimestamp !== 0) return byTimestamp;
17308
+ return a.summary.id.localeCompare(b.summary.id);
17309
+ });
17310
+ }
17311
+ function compareNewEmailOutputOldestFirst(a, b) {
17312
+ const byTimestamp = compareTimestamp(
17313
+ effectiveReceivedAt(a.receivedAt),
17314
+ effectiveReceivedAt(b.receivedAt)
17315
+ );
17316
+ if (byTimestamp !== 0) return byTimestamp;
17317
+ return a.id.localeCompare(b.id);
17318
+ }
17319
+ function compareTimestamp(a, b) {
17320
+ if (a < b) return -1;
17321
+ if (a > b) return 1;
17322
+ return 0;
17323
+ }
17324
+
17325
+ // src/tools/browse.ts
17326
+ function registerBrowseTools(server, ctx) {
17327
+ const { store, registry, tools } = ctx;
17328
+ const emailListOutputSchema = z5.object({
17329
+ account: z5.string(),
17330
+ count: z5.number(),
17331
+ items: z5.array(emailSummaryOutputSchema),
17332
+ skip: z5.number(),
17333
+ hasMore: z5.boolean()
17334
+ });
17335
+ const searchEmailsOutputSchema = z5.object({
17336
+ account: z5.string(),
17337
+ count: z5.number(),
17338
+ items: z5.array(emailSummaryOutputSchema)
17083
17339
  });
17084
17340
  if (shouldRegister("list_emails", tools)) {
17085
17341
  server.registerTool(
17086
17342
  "list_emails",
17087
17343
  {
17088
17344
  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()
17345
+ inputSchema: z5.object({
17346
+ account: z5.string().email(),
17347
+ folder: z5.string().default("inbox").optional(),
17348
+ limit: z5.number().int().positive().max(100).optional(),
17349
+ unreadOnly: z5.boolean().optional(),
17350
+ skip: z5.number().int().min(0).optional()
17095
17351
  }),
17096
17352
  outputSchema: emailListOutputSchema
17097
17353
  },
@@ -17118,15 +17374,16 @@ function registerBrowseTools(server, ctx) {
17118
17374
  }
17119
17375
  );
17120
17376
  }
17377
+ registerNewEmailTool(server, { store, registry, tools });
17121
17378
  if (shouldRegister("search_emails", tools)) {
17122
17379
  server.registerTool(
17123
17380
  "search_emails",
17124
17381
  {
17125
17382
  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()
17383
+ inputSchema: z5.object({
17384
+ account: z5.string().email(),
17385
+ query: z5.string().min(1),
17386
+ limit: z5.number().int().positive().max(100).optional()
17130
17387
  }),
17131
17388
  outputSchema: searchEmailsOutputSchema
17132
17389
  },
@@ -17148,31 +17405,31 @@ function registerBrowseTools(server, ctx) {
17148
17405
  }
17149
17406
  );
17150
17407
  }
17151
- const readEmailOutputSchema = z4.object({
17152
- id: z4.string(),
17153
- subject: z4.string(),
17408
+ const readEmailOutputSchema = z5.object({
17409
+ id: z5.string(),
17410
+ subject: z5.string(),
17154
17411
  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"])
17412
+ to: z5.array(emailAddrOutputSchema).optional(),
17413
+ cc: z5.array(emailAddrOutputSchema).optional(),
17414
+ bcc: z5.array(emailAddrOutputSchema).optional(),
17415
+ receivedAt: z5.string().optional(),
17416
+ preview: z5.string().optional(),
17417
+ isRead: z5.boolean().optional(),
17418
+ hasAttachments: z5.boolean().optional(),
17419
+ folder: z5.string().optional(),
17420
+ attachments: z5.array(attachmentMetaOutputSchema).optional(),
17421
+ body: z5.string(),
17422
+ bodyFormat: z5.enum(["markdown", "html", "text"])
17166
17423
  });
17167
17424
  if (shouldRegister("read_email", tools)) {
17168
17425
  server.registerTool(
17169
17426
  "read_email",
17170
17427
  {
17171
17428
  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(
17429
+ inputSchema: z5.object({
17430
+ account: z5.string().email(),
17431
+ id: z5.string().min(1),
17432
+ format: z5.enum(["markdown", "html", "text"]).default("markdown").optional().describe(
17176
17433
  "Output body format. 'markdown' converts HTML to Markdown (default), 'html' returns the raw HTML, 'text' returns plain text."
17177
17434
  )
17178
17435
  }),
@@ -17207,20 +17464,20 @@ function registerBrowseTools(server, ctx) {
17207
17464
  }
17208
17465
  );
17209
17466
  }
17210
- const readAttachmentOutputSchema = z4.object({
17211
- name: z4.string(),
17212
- contentType: z4.string().optional(),
17213
- path: z4.string()
17467
+ const readAttachmentOutputSchema = z5.object({
17468
+ name: z5.string(),
17469
+ contentType: z5.string().optional(),
17470
+ path: z5.string()
17214
17471
  });
17215
17472
  if (shouldRegister("read_attachment", tools)) {
17216
17473
  server.registerTool(
17217
17474
  "read_attachment",
17218
17475
  {
17219
17476
  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)
17477
+ inputSchema: z5.object({
17478
+ account: z5.string().email(),
17479
+ messageId: z5.string().min(1),
17480
+ attachmentId: z5.string().min(1)
17224
17481
  }),
17225
17482
  outputSchema: readAttachmentOutputSchema
17226
17483
  },
@@ -17242,22 +17499,22 @@ function registerBrowseTools(server, ctx) {
17242
17499
  }
17243
17500
 
17244
17501
  // src/tools/folders.ts
17245
- import { z as z5 } from "zod";
17502
+ import { z as z6 } from "zod";
17246
17503
  function registerFolderTools(server, ctx) {
17247
17504
  const { registry, tools } = ctx;
17248
- const listFoldersOutputSchema = z5.object({
17249
- account: z5.string(),
17250
- count: z5.number(),
17251
- items: z5.array(folderInfoOutputSchema)
17505
+ const listFoldersOutputSchema = z6.object({
17506
+ account: z6.string(),
17507
+ count: z6.number(),
17508
+ items: z6.array(folderInfoOutputSchema)
17252
17509
  });
17253
17510
  if (shouldRegister("list_folders", tools)) {
17254
17511
  server.registerTool(
17255
17512
  "list_folders",
17256
17513
  {
17257
17514
  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(
17515
+ inputSchema: z6.object({
17516
+ account: z6.string().email(),
17517
+ parentFolderId: z6.string().optional().describe(
17261
17518
  "When provided, lists child folders of this folder. When omitted, lists top-level folders (children of the root)."
17262
17519
  )
17263
17520
  }),
@@ -17281,8 +17538,8 @@ function registerFolderTools(server, ctx) {
17281
17538
  }
17282
17539
  );
17283
17540
  }
17284
- const createFolderOutputSchema = z5.object({
17285
- created: z5.literal(true),
17541
+ const createFolderOutputSchema = z6.object({
17542
+ created: z6.literal(true),
17286
17543
  folder: folderInfoOutputSchema
17287
17544
  });
17288
17545
  if (shouldRegister("create_folder", tools)) {
@@ -17290,10 +17547,10 @@ function registerFolderTools(server, ctx) {
17290
17547
  "create_folder",
17291
17548
  {
17292
17549
  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(
17550
+ inputSchema: z6.object({
17551
+ account: z6.string().email(),
17552
+ displayName: z6.string().min(1).describe("Name of the new folder"),
17553
+ parentFolderId: z6.string().optional().describe(
17297
17554
  "When provided, creates the folder as a child of this folder. When omitted, creates under the root folder."
17298
17555
  )
17299
17556
  }),
@@ -17314,18 +17571,18 @@ function registerFolderTools(server, ctx) {
17314
17571
  }
17315
17572
  );
17316
17573
  }
17317
- const deleteFolderOutputSchema = z5.object({
17318
- deleted: z5.literal(true),
17319
- id: z5.string()
17574
+ const deleteFolderOutputSchema = z6.object({
17575
+ deleted: z6.literal(true),
17576
+ id: z6.string()
17320
17577
  });
17321
17578
  if (shouldRegister("delete_folder", tools)) {
17322
17579
  server.registerTool(
17323
17580
  "delete_folder",
17324
17581
  {
17325
17582
  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")
17583
+ inputSchema: z6.object({
17584
+ account: z6.string().email(),
17585
+ folderId: z6.string().min(1).describe("ID of the folder to delete")
17329
17586
  }),
17330
17587
  outputSchema: deleteFolderOutputSchema
17331
17588
  },
@@ -17341,8 +17598,8 @@ function registerFolderTools(server, ctx) {
17341
17598
  }
17342
17599
  );
17343
17600
  }
17344
- const renameFolderOutputSchema = z5.object({
17345
- renamed: z5.literal(true),
17601
+ const renameFolderOutputSchema = z6.object({
17602
+ renamed: z6.literal(true),
17346
17603
  folder: folderInfoOutputSchema
17347
17604
  });
17348
17605
  if (shouldRegister("rename_folder", tools)) {
@@ -17350,10 +17607,10 @@ function registerFolderTools(server, ctx) {
17350
17607
  "rename_folder",
17351
17608
  {
17352
17609
  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")
17610
+ inputSchema: z6.object({
17611
+ account: z6.string().email(),
17612
+ folderId: z6.string().min(1).describe("ID of the folder to rename"),
17613
+ newName: z6.string().min(1).describe("New display name for the folder")
17357
17614
  }),
17358
17615
  outputSchema: renameFolderOutputSchema
17359
17616
  },
@@ -17376,7 +17633,7 @@ function registerFolderTools(server, ctx) {
17376
17633
  }
17377
17634
 
17378
17635
  // src/tools/organize.ts
17379
- import { z as z6 } from "zod";
17636
+ import { z as z7 } from "zod";
17380
17637
  function registerOrganizeTools(server, ctx) {
17381
17638
  const { registry, tools } = ctx;
17382
17639
  async function moveToWellKnown(args, destination, resultKey) {
@@ -17392,13 +17649,13 @@ function registerOrganizeTools(server, ctx) {
17392
17649
  const data = { marked: true, id: args.id, isRead };
17393
17650
  return ok(data, data);
17394
17651
  }
17395
- const archiveMoveSchema = z6.object({
17396
- account: z6.string().email(),
17397
- id: z6.string().min(1).describe("Message ID to move")
17652
+ const archiveMoveSchema = z7.object({
17653
+ account: z7.string().email(),
17654
+ id: z7.string().min(1).describe("Message ID to move")
17398
17655
  });
17399
- const archiveOutputSchema = z6.object({
17400
- archived: z6.literal(true),
17401
- id: z6.string()
17656
+ const archiveOutputSchema = z7.object({
17657
+ archived: z7.literal(true),
17658
+ id: z7.string()
17402
17659
  });
17403
17660
  if (shouldRegister("archive_email", tools)) {
17404
17661
  server.registerTool(
@@ -17417,9 +17674,9 @@ function registerOrganizeTools(server, ctx) {
17417
17674
  }
17418
17675
  );
17419
17676
  }
17420
- const trashOutputSchema = z6.object({
17421
- trashed: z6.literal(true),
17422
- id: z6.string()
17677
+ const trashOutputSchema = z7.object({
17678
+ trashed: z7.literal(true),
17679
+ id: z7.string()
17423
17680
  });
17424
17681
  if (shouldRegister("trash_email", tools)) {
17425
17682
  server.registerTool(
@@ -17438,20 +17695,20 @@ function registerOrganizeTools(server, ctx) {
17438
17695
  }
17439
17696
  );
17440
17697
  }
17441
- const moveEmailOutputSchema = z6.object({
17442
- moved: z6.literal(true),
17443
- id: z6.string(),
17444
- destination: z6.string()
17698
+ const moveEmailOutputSchema = z7.object({
17699
+ moved: z7.literal(true),
17700
+ id: z7.string(),
17701
+ destination: z7.string()
17445
17702
  });
17446
17703
  if (shouldRegister("move_email", tools)) {
17447
17704
  server.registerTool(
17448
17705
  "move_email",
17449
17706
  {
17450
17707
  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(
17708
+ inputSchema: z7.object({
17709
+ account: z7.string().email(),
17710
+ id: z7.string().min(1).describe("Message ID to move"),
17711
+ destination: z7.string().min(1).describe(
17455
17712
  "Destination folder \u2014 a well-known folder name ('archive', 'deleteditems', 'inbox', 'drafts', 'junkemail', 'sentitems', 'outbox') or a raw folder ID."
17456
17713
  )
17457
17714
  }),
@@ -17473,14 +17730,14 @@ function registerOrganizeTools(server, ctx) {
17473
17730
  }
17474
17731
  );
17475
17732
  }
17476
- const markReadInputSchema = z6.object({
17477
- account: z6.string().email(),
17478
- id: z6.string().min(1).describe("Message ID to mark as read")
17733
+ const markReadInputSchema = z7.object({
17734
+ account: z7.string().email(),
17735
+ id: z7.string().min(1).describe("Message ID to mark as read")
17479
17736
  });
17480
- const markReadOutputSchema = z6.object({
17481
- marked: z6.literal(true),
17482
- id: z6.string(),
17483
- isRead: z6.boolean()
17737
+ const markReadOutputSchema = z7.object({
17738
+ marked: z7.literal(true),
17739
+ id: z7.string(),
17740
+ isRead: z7.boolean()
17484
17741
  });
17485
17742
  if (shouldRegister("mark_read", tools)) {
17486
17743
  server.registerTool(
@@ -17519,7 +17776,7 @@ function registerOrganizeTools(server, ctx) {
17519
17776
  }
17520
17777
 
17521
17778
  // src/tools/compose.ts
17522
- import { z as z7 } from "zod";
17779
+ import { z as z8 } from "zod";
17523
17780
  import { readFileSync } from "fs";
17524
17781
  import { basename, extname } from "path";
17525
17782
  function registerComposeTools(server, ctx) {
@@ -17549,32 +17806,32 @@ function registerComposeTools(server, ctx) {
17549
17806
  ".mp3": "audio/mpeg",
17550
17807
  ".mp4": "video/mp4"
17551
17808
  };
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(
17809
+ const sendEmailSchema = z8.object({
17810
+ account: z8.string().email(),
17811
+ to: z8.array(emailAddrSchema).min(1),
17812
+ cc: z8.array(emailAddrSchema).optional(),
17813
+ bcc: z8.array(emailAddrSchema).optional(),
17814
+ subject: z8.string(),
17815
+ body: z8.string(),
17816
+ format: z8.enum(["html", "markdown"]).describe(
17560
17817
  "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
17818
  ),
17562
- include_signature: z7.boolean().describe(
17819
+ include_signature: z8.boolean().describe(
17563
17820
  "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
17821
  ),
17565
- inReplyTo: z7.union([z7.string(), z7.literal(false)]).describe(
17822
+ inReplyTo: z8.union([z8.string(), z8.literal(false)]).describe(
17566
17823
  "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
17824
  ),
17568
- replyAll: z7.boolean().default(false).optional().describe(
17825
+ replyAll: z8.boolean().default(false).optional().describe(
17569
17826
  "When true and `inReplyTo` is set, reply to all recipients instead of just the sender."
17570
17827
  ),
17571
- forwardMessageId: z7.string().optional().describe(
17828
+ forwardMessageId: z8.string().optional().describe(
17572
17829
  "Message ID to forward. When set, sends as a forward of the specified message, preserving the original content. Mutually exclusive with `inReplyTo`."
17573
17830
  ),
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(
17831
+ attachments: z8.array(
17832
+ z8.object({
17833
+ filePath: z8.string().min(1).describe("Absolute path to a local file"),
17834
+ name: z8.string().optional().describe(
17578
17835
  "Attachment filename. Defaults to the file's basename."
17579
17836
  )
17580
17837
  })
@@ -17637,8 +17894,8 @@ function registerComposeTools(server, ctx) {
17637
17894
  }
17638
17895
  }
17639
17896
  const sendEmailOutputSchema = {
17640
- sent: z7.literal(true),
17641
- id: z7.string()
17897
+ sent: z8.literal(true),
17898
+ id: z8.string()
17642
17899
  };
17643
17900
  if (shouldRegister("send_email", tools)) {
17644
17901
  server.registerTool(
@@ -17657,9 +17914,9 @@ function registerComposeTools(server, ctx) {
17657
17914
  );
17658
17915
  }
17659
17916
  const draftEmailOutputSchema = {
17660
- draft: z7.literal(true),
17661
- id: z7.string(),
17662
- draftHtml: z7.string().optional()
17917
+ draft: z8.literal(true),
17918
+ id: z8.string(),
17919
+ draftHtml: z8.string().optional()
17663
17920
  };
17664
17921
  if (shouldRegister("draft_email", tools)) {
17665
17922
  server.registerTool(
@@ -17677,38 +17934,38 @@ function registerComposeTools(server, ctx) {
17677
17934
  )
17678
17935
  );
17679
17936
  }
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(
17937
+ const editDraftSchema = z8.object({
17938
+ account: z8.string().email(),
17939
+ id: z8.string().min(1).describe("Draft message ID to edit"),
17940
+ to: z8.array(emailAddrSchema).optional(),
17941
+ cc: z8.array(emailAddrSchema).optional(),
17942
+ bcc: z8.array(emailAddrSchema).optional(),
17943
+ subject: z8.string().optional(),
17944
+ body: z8.string().optional(),
17945
+ format: z8.enum(["html", "markdown"]).optional().describe(
17689
17946
  "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
17947
  ),
17691
- include_signature: z7.boolean().optional().describe(
17948
+ include_signature: z8.boolean().optional().describe(
17692
17949
  "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
17950
  ),
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(
17951
+ new_attachments: z8.array(
17952
+ z8.object({
17953
+ filePath: z8.string().min(1).describe("Absolute path to a local file"),
17954
+ name: z8.string().optional().describe(
17698
17955
  "Attachment filename. Defaults to the file's basename."
17699
17956
  )
17700
17957
  })
17701
17958
  ).optional().describe(
17702
17959
  "New file attachments to add to the draft. The server reads the files from disk and base64-encodes them automatically."
17703
17960
  ),
17704
- remove_attachments: z7.array(z7.string().min(1)).optional().describe(
17961
+ remove_attachments: z8.array(z8.string().min(1)).optional().describe(
17705
17962
  "Attachment IDs to remove from the draft. Get attachment IDs from read_email."
17706
17963
  )
17707
17964
  });
17708
17965
  const editDraftOutputSchema = {
17709
- edited: z7.literal(true),
17710
- id: z7.string(),
17711
- draftHtml: z7.string().optional()
17966
+ edited: z8.literal(true),
17967
+ id: z8.string(),
17968
+ draftHtml: z8.string().optional()
17712
17969
  };
17713
17970
  if (shouldRegister("edit_draft", tools)) {
17714
17971
  server.registerTool(
@@ -17800,8 +18057,8 @@ function registerComposeTools(server, ctx) {
17800
18057
  );
17801
18058
  }
17802
18059
  const sendDraftOutputSchema = {
17803
- sent: z7.literal(true),
17804
- id: z7.string()
18060
+ sent: z8.literal(true),
18061
+ id: z8.string()
17805
18062
  };
17806
18063
  if (shouldRegister("send_draft", tools)) {
17807
18064
  server.registerTool(
@@ -17809,8 +18066,8 @@ function registerComposeTools(server, ctx) {
17809
18066
  {
17810
18067
  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
18068
  inputSchema: {
17812
- account: z7.string().email(),
17813
- id: z7.string().min(1).describe("Draft message ID to send")
18069
+ account: z8.string().email(),
18070
+ id: z8.string().min(1).describe("Draft message ID to send")
17814
18071
  },
17815
18072
  outputSchema: sendDraftOutputSchema
17816
18073
  },
@@ -17832,7 +18089,7 @@ function registerComposeTools(server, ctx) {
17832
18089
  function registerTools(server, opts) {
17833
18090
  const { store, registry, tools } = opts;
17834
18091
  registerAccountTools(server, { store, registry, tools });
17835
- registerBrowseTools(server, { registry, tools });
18092
+ registerBrowseTools(server, { store, registry, tools });
17836
18093
  registerFolderTools(server, { registry, tools });
17837
18094
  registerOrganizeTools(server, { registry, tools });
17838
18095
  registerComposeTools(server, { store, registry, tools });
@@ -17841,7 +18098,7 @@ function registerTools(server, opts) {
17841
18098
  // package.json
17842
18099
  var package_default = {
17843
18100
  name: "hypermail-mcp",
17844
- version: "0.7.7",
18101
+ version: "0.7.9",
17845
18102
  description: "Unified email MCP server \u2014 operate any inbox (Outlook now, IMAP/Gmail later) by passing an email address.",
17846
18103
  type: "module",
17847
18104
  bin: {
@@ -17913,193 +18170,6 @@ var package_default = {
17913
18170
  // src/version.ts
17914
18171
  var VERSION = package_default.version;
17915
18172
 
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
18173
  // src/config/load.ts
18104
18174
  var ENV_DATA_DIR = "HYPERMAIL_DATA_DIR";
18105
18175
  var ENV_KEY = "HYPERMAIL_KEY";
@@ -18113,22 +18183,9 @@ var ENV_OUTLOOK_TENANT_ID = "HYPERMAIL_OUTLOOK_TENANT_ID";
18113
18183
  var ENV_GMAIL_CLIENT_ID = "HYPERMAIL_GMAIL_CLIENT_ID";
18114
18184
  var ENV_GMAIL_CLIENT_SECRET = "HYPERMAIL_GMAIL_CLIENT_SECRET";
18115
18185
  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
18186
  var DEFAULT_TRANSPORT = "stdio";
18126
18187
  var DEFAULT_HTTP_PORT = 3e3;
18127
18188
  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
18189
  function envRaw(name) {
18133
18190
  return process.env[name];
18134
18191
  }
@@ -18138,14 +18195,6 @@ function optionalEnvString(name) {
18138
18195
  const trimmed = value.trim();
18139
18196
  return trimmed.length > 0 ? trimmed : void 0;
18140
18197
  }
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
18198
  function parseTransportEnv() {
18150
18199
  const value = envRaw(ENV_TRANSPORT);
18151
18200
  if (value === void 0) return void 0;
@@ -18163,15 +18212,6 @@ function parsePositiveInteger(value) {
18163
18212
  const parsed = Number(trimmed);
18164
18213
  return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : void 0;
18165
18214
  }
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
18215
  function parseStringArray(value) {
18176
18216
  if (value === void 0) return void 0;
18177
18217
  const trimmed = value.trim();
@@ -18189,17 +18229,6 @@ function validateToolNames(toolNames, envName) {
18189
18229
  }
18190
18230
  }
18191
18231
  }
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
18232
  function resolveHttpConfig(transport, cliOverrides, warnings) {
18204
18233
  const portSource = cliOverrides.port !== void 0 ? "--port" : ENV_HTTP_PORT;
18205
18234
  const rawPort = cliOverrides.port ?? envRaw(ENV_HTTP_PORT);
@@ -18263,68 +18292,12 @@ function resolveProvidersConfig() {
18263
18292
  }
18264
18293
  return providers;
18265
18294
  }
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
18295
  function loadConfig(cliOverrides = {}) {
18322
18296
  const warnings = [];
18323
18297
  const transport = cliOverrides.transport ?? parseTransportEnv() ?? DEFAULT_TRANSPORT;
18324
18298
  const http = resolveHttpConfig(transport, cliOverrides, warnings);
18325
18299
  const tools = resolveToolsConfig();
18326
18300
  const providers = resolveProvidersConfig();
18327
- const watch = resolveWatchConfig();
18328
18301
  const dataDir = cliOverrides.dataDir ?? optionalEnvString(ENV_DATA_DIR);
18329
18302
  if (!optionalEnvString(ENV_KEY)) {
18330
18303
  warnings.push(
@@ -18337,8 +18310,7 @@ function loadConfig(cliOverrides = {}) {
18337
18310
  transport,
18338
18311
  http,
18339
18312
  tools,
18340
- providers,
18341
- watch
18313
+ providers
18342
18314
  },
18343
18315
  warnings
18344
18316
  };
@@ -18353,6 +18325,7 @@ var KNOWN_TOOLS = [
18353
18325
  "set_account_settings",
18354
18326
  "remove_account",
18355
18327
  "list_emails",
18328
+ "get_new_emails",
18356
18329
  "search_emails",
18357
18330
  "read_email",
18358
18331
  "read_attachment",
@@ -18386,14 +18359,6 @@ async function startServer(opts) {
18386
18359
  const store = await AccountStore.open({ dataDir: config.dataDir });
18387
18360
  const registry = buildRegistry({ store, providers: config.providers });
18388
18361
  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
18362
  const createServer = () => {
18398
18363
  const s = new McpServer(
18399
18364
  { name: "hypermail-mcp", version: VERSION },
@@ -18598,17 +18563,6 @@ Provider environment variables:
18598
18563
  HYPERMAIL_GMAIL_CLIENT_SECRET
18599
18564
  HYPERMAIL_GMAIL_REDIRECT_URI
18600
18565
 
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
18566
  Example:
18613
18567
  HYPERMAIL_TRANSPORT=http \\
18614
18568
  HYPERMAIL_HTTP_PORT=8080 \\