hypermail-mcp 0.6.1 → 0.6.3

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
@@ -9,7 +9,6 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
9
9
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
10
10
  import { randomUUID as randomUUID6 } from "crypto";
11
11
  import { createServer as createHttpServer } from "http";
12
- import path4 from "path";
13
12
 
14
13
  // src/store/account-store.ts
15
14
  import { promises as fs2 } from "fs";
@@ -86,23 +85,6 @@ async function resolveKey(dataDir) {
86
85
  await tryKeytarSet(gen);
87
86
  return gen;
88
87
  }
89
- function hashApiKey(apiKey) {
90
- const salt = randomBytes(16).toString("hex");
91
- const hash = scryptSync(apiKey, salt, 32).toString("hex");
92
- return `${salt}:${hash}`;
93
- }
94
- function verifyApiKey(apiKey, stored) {
95
- const [salt, hash] = stored.split(":");
96
- if (!salt || !hash) return false;
97
- try {
98
- const computed = scryptSync(apiKey, salt, 32);
99
- const expected = Buffer.from(hash, "hex");
100
- if (computed.length !== expected.length) return false;
101
- return timingSafeEqual(computed, expected);
102
- } catch {
103
- return false;
104
- }
105
- }
106
88
  async function writeAtomic(filePath, data) {
107
89
  const tmp = `${filePath}.${process.pid}.${Date.now()}.tmp`;
108
90
  await fs.writeFile(tmp, data, { mode: 384 });
@@ -188,128 +170,6 @@ var AccountStore = class _AccountStore {
188
170
  }
189
171
  };
190
172
 
191
- // src/store/agent-store.ts
192
- import path3 from "path";
193
- import { promises as fs3 } from "fs";
194
- var FILE_NAME2 = "agents.json.enc";
195
- var AgentStore = class _AgentStore {
196
- constructor(filePath, key, data) {
197
- this.filePath = filePath;
198
- this.key = key;
199
- this.data = data;
200
- }
201
- filePath;
202
- key;
203
- data;
204
- static async open(opts = {}) {
205
- const dataDir = resolveDataDir(opts.dataDir);
206
- await fs3.mkdir(dataDir, { recursive: true, mode: 448 });
207
- const filePath = path3.join(dataDir, FILE_NAME2);
208
- const key = opts.key ?? await resolveKey(dataDir);
209
- let data;
210
- try {
211
- const buf = await fs3.readFile(filePath);
212
- data = decrypt(buf, key);
213
- } catch (err) {
214
- if (err.code === "ENOENT") {
215
- data = { version: 1, agents: [] };
216
- } else {
217
- throw err;
218
- }
219
- }
220
- return new _AgentStore(filePath, key, data);
221
- }
222
- // ── queries ──
223
- listAgents() {
224
- return this.data.agents.map((a) => ({ ...a }));
225
- }
226
- getAgent(id) {
227
- const rec = this.data.agents.find((a) => a.id === id);
228
- return rec ? { ...rec } : void 0;
229
- }
230
- /**
231
- * Look up an agent by plaintext API key. Hashes the incoming key and
232
- * compares against stored hashes with constant-time comparison.
233
- * Returns undefined if no agent matches.
234
- */
235
- findAgentByApiKey(apiKey) {
236
- for (const agent of this.data.agents) {
237
- if (verifyApiKey(apiKey, agent.apiKeyHash)) {
238
- return { ...agent };
239
- }
240
- }
241
- return void 0;
242
- }
243
- // ── mutations ──
244
- /**
245
- * Add or update an agent. If `plaintextApiKey` is provided, it is hashed
246
- * and stored; if omitted, the existing hash is preserved (useful for
247
- * updates that don't change the key).
248
- */
249
- async upsertAgent(rec) {
250
- const idx = this.data.agents.findIndex((a) => a.id === rec.id);
251
- const existing = idx >= 0 ? this.data.agents[idx] : void 0;
252
- const apiKeyHash = rec.plaintextApiKey ? hashApiKey(rec.plaintextApiKey) : existing?.apiKeyHash;
253
- if (!apiKeyHash) {
254
- throw new Error(
255
- `agent ${rec.id}: must provide plaintextApiKey for new agents`
256
- );
257
- }
258
- const next = {
259
- id: rec.id,
260
- apiKeyHash,
261
- name: rec.name,
262
- accounts: [...rec.accounts ?? []],
263
- provisioning: rec.provisioning ?? false,
264
- createdAt: existing?.createdAt ?? (/* @__PURE__ */ new Date()).toISOString()
265
- };
266
- if (idx >= 0) {
267
- this.data.agents[idx] = next;
268
- } else {
269
- this.data.agents.push(next);
270
- }
271
- await this.flush();
272
- return { ...next };
273
- }
274
- async removeAgent(id) {
275
- const before = this.data.agents.length;
276
- this.data.agents = this.data.agents.filter((a) => a.id !== id);
277
- if (this.data.agents.length === before) return false;
278
- await this.flush();
279
- return true;
280
- }
281
- /**
282
- * Assign an email account to an agent. Idempotent — no error if already
283
- * assigned. Auto-assignment from `add_account` in HTTP mode calls this.
284
- */
285
- async assignAccount(agentId, email) {
286
- const norm = email.trim().toLowerCase();
287
- const agent = this.data.agents.find((a) => a.id === agentId);
288
- if (!agent) throw new Error(`agent ${agentId} not found`);
289
- if (!agent.accounts.includes(norm)) {
290
- agent.accounts.push(norm);
291
- await this.flush();
292
- }
293
- return { ...agent };
294
- }
295
- /**
296
- * Remove an email account from an agent's assignments. Idempotent.
297
- */
298
- async unassignAccount(agentId, email) {
299
- const norm = email.trim().toLowerCase();
300
- const agent = this.data.agents.find((a) => a.id === agentId);
301
- if (!agent) throw new Error(`agent ${agentId} not found`);
302
- agent.accounts = agent.accounts.filter((a) => a !== norm);
303
- await this.flush();
304
- return { ...agent };
305
- }
306
- // ── persistence ──
307
- async flush() {
308
- const buf = encrypt(this.data, this.key);
309
- await writeAtomic(this.filePath, buf);
310
- }
311
- };
312
-
313
173
  // src/providers/outlook/index.ts
314
174
  import { randomUUID as randomUUID2 } from "crypto";
315
175
  import { writeFileSync } from "fs";
@@ -1565,8 +1425,8 @@ async function markRead(clients, account, id, isRead) {
1565
1425
  async function createFolder(clients, account, input) {
1566
1426
  const client = clients.get(account);
1567
1427
  const imap = await client.getImap();
1568
- const path5 = input.parentFolderId ? `${input.parentFolderId}/${input.displayName}` : input.displayName;
1569
- const result = await imap.mailboxCreate(path5);
1428
+ const path3 = input.parentFolderId ? `${input.parentFolderId}/${input.displayName}` : input.displayName;
1429
+ const result = await imap.mailboxCreate(path3);
1570
1430
  return {
1571
1431
  id: result.path,
1572
1432
  displayName: result.path,
@@ -2754,27 +2614,10 @@ function shouldRegister(name, tools) {
2754
2614
  }
2755
2615
 
2756
2616
  // src/tools/accounts.ts
2757
- import { promises as fs4 } from "fs";
2617
+ import { promises as fs3 } from "fs";
2758
2618
  import { z as z2 } from "zod";
2759
-
2760
- // src/tools/agent-context.ts
2761
- function checkAccountAccess(agentContext, accountEmail) {
2762
- if (!agentContext) return null;
2763
- const norm = accountEmail.trim().toLowerCase();
2764
- if (agentContext.accounts.some((a) => a.toLowerCase() === norm)) {
2765
- return null;
2766
- }
2767
- return `Agent "${agentContext.agentId}" is not authorized for account "${accountEmail}"`;
2768
- }
2769
- function checkProvisioning(agentContext) {
2770
- if (!agentContext) return null;
2771
- if (agentContext.provisioning) return null;
2772
- return `Agent "${agentContext.agentId}" does not have provisioning permission`;
2773
- }
2774
-
2775
- // src/tools/accounts.ts
2776
2619
  function registerAccountTools(server, ctx) {
2777
- const { store, registry, tools, agentContext, agentStore } = ctx;
2620
+ const { store, registry, tools } = ctx;
2778
2621
  const listAccountsOutputSchema = z2.object({
2779
2622
  accounts: z2.array(accountSummaryOutputSchema)
2780
2623
  });
@@ -2828,8 +2671,6 @@ function registerAccountTools(server, ctx) {
2828
2671
  outputSchema: addAccountOutputSchema
2829
2672
  },
2830
2673
  async (args) => {
2831
- const permErr = checkProvisioning(agentContext ?? null);
2832
- if (permErr) return fail(permErr);
2833
2674
  const provider = registry.get(args.provider);
2834
2675
  try {
2835
2676
  const res = await provider.addAccount({
@@ -2860,8 +2701,6 @@ function registerAccountTools(server, ctx) {
2860
2701
  outputSchema: completeAddAccountOutputSchema
2861
2702
  },
2862
2703
  async (args) => {
2863
- const permErr = checkProvisioning(agentContext ?? null);
2864
- if (permErr) return fail(permErr);
2865
2704
  const provider = registry.get(args.provider);
2866
2705
  if (!provider.completeAddAccount) {
2867
2706
  return fail(
@@ -2870,10 +2709,6 @@ function registerAccountTools(server, ctx) {
2870
2709
  }
2871
2710
  try {
2872
2711
  const res = await provider.completeAddAccount(args.handle);
2873
- if (res.status === "ready" && res.account && agentContext && agentStore) {
2874
- agentStore.assignAccount(agentContext.agentId, res.account.email).catch(() => {
2875
- });
2876
- }
2877
2712
  return ok(res, res);
2878
2713
  } catch (err) {
2879
2714
  return fail(errMsg(err));
@@ -2895,8 +2730,6 @@ function registerAccountTools(server, ctx) {
2895
2730
  },
2896
2731
  async (args) => {
2897
2732
  try {
2898
- const accessErr = checkAccountAccess(agentContext ?? null, args.account);
2899
- if (accessErr) return fail(accessErr);
2900
2733
  const acct = store.getAccount(args.account);
2901
2734
  if (!acct)
2902
2735
  return fail(`no account registered for "${args.account}"`);
@@ -2941,14 +2774,12 @@ function registerAccountTools(server, ctx) {
2941
2774
  },
2942
2775
  async (args) => {
2943
2776
  try {
2944
- const accessErr = checkAccountAccess(agentContext ?? null, args.account);
2945
- if (accessErr) return fail(accessErr);
2946
2777
  const acct = store.getAccount(args.account);
2947
2778
  if (!acct)
2948
2779
  return fail(`no account registered for "${args.account}"`);
2949
2780
  let resolvedSignature = acct.signature;
2950
2781
  if (args.signaturePath) {
2951
- resolvedSignature = await fs4.readFile(args.signaturePath, "utf-8");
2782
+ resolvedSignature = await fs3.readFile(args.signaturePath, "utf-8");
2952
2783
  } else if (args.signature !== void 0) {
2953
2784
  resolvedSignature = args.signature || void 0;
2954
2785
  }
@@ -2981,8 +2812,6 @@ function registerAccountTools(server, ctx) {
2981
2812
  outputSchema: removeAccountOutputSchema
2982
2813
  },
2983
2814
  async (args) => {
2984
- const permErr = checkProvisioning(agentContext ?? null);
2985
- if (permErr) return fail(permErr);
2986
2815
  const removed = await store.removeAccount(args.email);
2987
2816
  const data = { removed, email: args.email };
2988
2817
  return ok(data, data);
@@ -3022,7 +2851,7 @@ function selectBody(msg, format) {
3022
2851
 
3023
2852
  // src/tools/browse.ts
3024
2853
  function registerBrowseTools(server, ctx) {
3025
- const { registry, tools, agentContext } = ctx;
2854
+ const { registry, tools } = ctx;
3026
2855
  const emailListOutputSchema = z3.object({
3027
2856
  account: z3.string(),
3028
2857
  count: z3.number(),
@@ -3051,8 +2880,6 @@ function registerBrowseTools(server, ctx) {
3051
2880
  },
3052
2881
  async (args) => {
3053
2882
  try {
3054
- const accessErr = checkAccountAccess(agentContext ?? null, args.account);
3055
- if (accessErr) return fail(accessErr);
3056
2883
  const { provider, account } = registry.resolveByEmail(args.account);
3057
2884
  const { items, hasMore } = await provider.listEmails(account, {
3058
2885
  folder: args.folder,
@@ -3088,8 +2915,6 @@ function registerBrowseTools(server, ctx) {
3088
2915
  },
3089
2916
  async (args) => {
3090
2917
  try {
3091
- const accessErr = checkAccountAccess(agentContext ?? null, args.account);
3092
- if (accessErr) return fail(accessErr);
3093
2918
  const { provider, account } = registry.resolveByEmail(args.account);
3094
2919
  const items = await provider.searchEmails(account, args.query, {
3095
2920
  limit: args.limit
@@ -3138,8 +2963,6 @@ function registerBrowseTools(server, ctx) {
3138
2963
  },
3139
2964
  async (args) => {
3140
2965
  try {
3141
- const accessErr = checkAccountAccess(agentContext ?? null, args.account);
3142
- if (accessErr) return fail(accessErr);
3143
2966
  const { provider, account } = registry.resolveByEmail(args.account);
3144
2967
  const msg = await provider.readEmail(account, args.id);
3145
2968
  const format = args.format ?? "markdown";
@@ -3186,8 +3009,6 @@ function registerBrowseTools(server, ctx) {
3186
3009
  },
3187
3010
  async (args) => {
3188
3011
  try {
3189
- const accessErr = checkAccountAccess(agentContext ?? null, args.account);
3190
- if (accessErr) return fail(accessErr);
3191
3012
  const { provider, account } = registry.resolveByEmail(args.account);
3192
3013
  const res = await provider.readAttachment(
3193
3014
  account,
@@ -3206,7 +3027,7 @@ function registerBrowseTools(server, ctx) {
3206
3027
  // src/tools/folders.ts
3207
3028
  import { z as z4 } from "zod";
3208
3029
  function registerFolderTools(server, ctx) {
3209
- const { registry, tools, agentContext } = ctx;
3030
+ const { registry, tools } = ctx;
3210
3031
  const listFoldersOutputSchema = z4.object({
3211
3032
  account: z4.string(),
3212
3033
  count: z4.number(),
@@ -3227,8 +3048,6 @@ function registerFolderTools(server, ctx) {
3227
3048
  },
3228
3049
  async (args) => {
3229
3050
  try {
3230
- const accessErr = checkAccountAccess(agentContext ?? null, args.account);
3231
- if (accessErr) return fail(accessErr);
3232
3051
  const { provider, account } = registry.resolveByEmail(args.account);
3233
3052
  const items = await provider.listFolders(account, {
3234
3053
  parentFolderId: args.parentFolderId
@@ -3265,8 +3084,6 @@ function registerFolderTools(server, ctx) {
3265
3084
  },
3266
3085
  async (args) => {
3267
3086
  try {
3268
- const accessErr = checkAccountAccess(agentContext ?? null, args.account);
3269
- if (accessErr) return fail(accessErr);
3270
3087
  const { provider, account } = registry.resolveByEmail(args.account);
3271
3088
  const folder = await provider.createFolder(account, {
3272
3089
  displayName: args.displayName,
@@ -3297,8 +3114,6 @@ function registerFolderTools(server, ctx) {
3297
3114
  },
3298
3115
  async (args) => {
3299
3116
  try {
3300
- const accessErr = checkAccountAccess(agentContext ?? null, args.account);
3301
- if (accessErr) return fail(accessErr);
3302
3117
  const { provider, account } = registry.resolveByEmail(args.account);
3303
3118
  await provider.deleteFolder(account, args.folderId);
3304
3119
  const data = { deleted: true, id: args.folderId };
@@ -3327,8 +3142,6 @@ function registerFolderTools(server, ctx) {
3327
3142
  },
3328
3143
  async (args) => {
3329
3144
  try {
3330
- const accessErr = checkAccountAccess(agentContext ?? null, args.account);
3331
- if (accessErr) return fail(accessErr);
3332
3145
  const { provider, account } = registry.resolveByEmail(args.account);
3333
3146
  const folder = await provider.renameFolder(
3334
3147
  account,
@@ -3348,7 +3161,7 @@ function registerFolderTools(server, ctx) {
3348
3161
  // src/tools/organize.ts
3349
3162
  import { z as z5 } from "zod";
3350
3163
  function registerOrganizeTools(server, ctx) {
3351
- const { registry, tools, agentContext } = ctx;
3164
+ const { registry, tools } = ctx;
3352
3165
  async function moveToWellKnown(args, destination, resultKey) {
3353
3166
  const { provider, account } = registry.resolveByEmail(args.account);
3354
3167
  await provider.moveEmail(account, args.id, destination);
@@ -3380,8 +3193,6 @@ function registerOrganizeTools(server, ctx) {
3380
3193
  },
3381
3194
  async (args) => {
3382
3195
  try {
3383
- const accessErr = checkAccountAccess(agentContext ?? null, args.account);
3384
- if (accessErr) return fail(accessErr);
3385
3196
  return await moveToWellKnown(args, "archive", "archived");
3386
3197
  } catch (err) {
3387
3198
  return fail(errMsg(err));
@@ -3403,8 +3214,6 @@ function registerOrganizeTools(server, ctx) {
3403
3214
  },
3404
3215
  async (args) => {
3405
3216
  try {
3406
- const accessErr = checkAccountAccess(agentContext ?? null, args.account);
3407
- if (accessErr) return fail(accessErr);
3408
3217
  return await moveToWellKnown(args, "deleteditems", "trashed");
3409
3218
  } catch (err) {
3410
3219
  return fail(errMsg(err));
@@ -3433,8 +3242,6 @@ function registerOrganizeTools(server, ctx) {
3433
3242
  },
3434
3243
  async (args) => {
3435
3244
  try {
3436
- const accessErr = checkAccountAccess(agentContext ?? null, args.account);
3437
- if (accessErr) return fail(accessErr);
3438
3245
  const { provider, account } = registry.resolveByEmail(args.account);
3439
3246
  await provider.moveEmail(account, args.id, args.destination);
3440
3247
  const data = {
@@ -3468,8 +3275,6 @@ function registerOrganizeTools(server, ctx) {
3468
3275
  },
3469
3276
  async (args) => {
3470
3277
  try {
3471
- const accessErr = checkAccountAccess(agentContext ?? null, args.account);
3472
- if (accessErr) return fail(accessErr);
3473
3278
  return await markReadState(args, true);
3474
3279
  } catch (err) {
3475
3280
  return fail(errMsg(err));
@@ -3487,8 +3292,6 @@ function registerOrganizeTools(server, ctx) {
3487
3292
  },
3488
3293
  async (args) => {
3489
3294
  try {
3490
- const accessErr = checkAccountAccess(agentContext ?? null, args.account);
3491
- if (accessErr) return fail(accessErr);
3492
3295
  return await markReadState(args, false);
3493
3296
  } catch (err) {
3494
3297
  return fail(errMsg(err));
@@ -3501,7 +3304,7 @@ function registerOrganizeTools(server, ctx) {
3501
3304
  // src/tools/compose.ts
3502
3305
  import { z as z6 } from "zod";
3503
3306
  function registerComposeTools(server, ctx) {
3504
- const { store, registry, tools, agentContext } = ctx;
3307
+ const { store, registry, tools } = ctx;
3505
3308
  const sendEmailSchema = z6.object({
3506
3309
  account: z6.string().email(),
3507
3310
  to: z6.array(emailAddrSchema).min(1),
@@ -3527,8 +3330,6 @@ function registerComposeTools(server, ctx) {
3527
3330
  });
3528
3331
  async function handleSendOrDraft(args, action, resultKey, toolName) {
3529
3332
  try {
3530
- const accessErr = checkAccountAccess(agentContext ?? null, args.account);
3531
- if (accessErr) return fail(accessErr);
3532
3333
  const { provider, account } = registry.resolveByEmail(args.account);
3533
3334
  if (args.include_signature && !account.signature) {
3534
3335
  return fail(
@@ -3640,8 +3441,6 @@ function registerComposeTools(server, ctx) {
3640
3441
  async (args) => {
3641
3442
  const a = args;
3642
3443
  try {
3643
- const accessErr = checkAccountAccess(agentContext ?? null, a.account);
3644
- if (accessErr) return fail(accessErr);
3645
3444
  const { provider, account } = registry.resolveByEmail(a.account);
3646
3445
  if (a.include_signature && !account.signature) {
3647
3446
  return fail(
@@ -3699,8 +3498,6 @@ function registerComposeTools(server, ctx) {
3699
3498
  },
3700
3499
  async (args) => {
3701
3500
  try {
3702
- const accessErr = checkAccountAccess(agentContext ?? null, args.account);
3703
- if (accessErr) return fail(accessErr);
3704
3501
  const { provider, account } = registry.resolveByEmail(args.account);
3705
3502
  const res = await provider.sendDraft(account, args.id);
3706
3503
  const data = { sent: true, id: res.id };
@@ -3736,8 +3533,6 @@ function registerComposeTools(server, ctx) {
3736
3533
  },
3737
3534
  async (args) => {
3738
3535
  try {
3739
- const accessErr = checkAccountAccess(agentContext ?? null, args.account);
3740
- if (accessErr) return fail(accessErr);
3741
3536
  const { provider, account } = registry.resolveByEmail(args.account);
3742
3537
  const res = await provider.addAttachmentToDraft(
3743
3538
  account,
@@ -3760,98 +3555,119 @@ function registerComposeTools(server, ctx) {
3760
3555
  }
3761
3556
  }
3762
3557
 
3763
- // src/tools/notifications.ts
3764
- import { z as z7 } from "zod";
3765
- function registerNotificationTools(server, ctx) {
3766
- const { tools, notificationBuffer, agentContext } = ctx;
3767
- const notifyOutputSchema = z7.object({
3768
- count: z7.number(),
3769
- items: z7.array(
3770
- z7.object({
3771
- type: z7.enum(["new_emails", "auth_failure"]),
3772
- account: z7.string(),
3773
- emails: z7.array(z7.unknown()).optional(),
3774
- error: z7.string().optional(),
3775
- timestamp: z7.string()
3776
- })
3777
- )
3778
- });
3779
- if (shouldRegister("check_notifications", tools)) {
3780
- server.registerTool(
3781
- "check_notifications",
3782
- {
3783
- description: "Check for pending email watch notifications. Returns new-email alerts and auth-failure warnings that the inbox watcher has accumulated since the last call. Drains the notification buffer on read.",
3784
- inputSchema: z7.object({}),
3785
- outputSchema: notifyOutputSchema
3786
- },
3787
- async () => {
3788
- try {
3789
- const pending = notificationBuffer.splice(0);
3790
- const filtered = agentContext ? pending.filter(
3791
- (n) => agentContext.accounts.some(
3792
- (a) => a.toLowerCase() === n.account.toLowerCase()
3793
- )
3794
- ) : pending;
3795
- const data = { count: filtered.length, items: filtered };
3796
- return ok(data, data);
3797
- } catch (err) {
3798
- return fail(errMsg(err));
3799
- }
3800
- }
3801
- );
3802
- }
3803
- }
3804
-
3805
3558
  // src/tools/index.ts
3806
3559
  function registerTools(server, opts) {
3807
- const { store, registry, tools, agentContext, agentStore } = opts;
3808
- registerAccountTools(server, { store, registry, tools, agentContext, agentStore });
3809
- registerBrowseTools(server, { registry, tools, agentContext });
3810
- registerFolderTools(server, { registry, tools, agentContext });
3811
- registerOrganizeTools(server, { registry, tools, agentContext });
3812
- registerComposeTools(server, { store, registry, tools, agentContext });
3813
- if (opts.notificationBuffer) {
3814
- registerNotificationTools(server, {
3815
- tools,
3816
- notificationBuffer: opts.notificationBuffer,
3817
- agentContext
3818
- });
3819
- }
3560
+ const { store, registry, tools } = opts;
3561
+ registerAccountTools(server, { store, registry, tools });
3562
+ registerBrowseTools(server, { registry, tools });
3563
+ registerFolderTools(server, { registry, tools });
3564
+ registerOrganizeTools(server, { registry, tools });
3565
+ registerComposeTools(server, { store, registry, tools });
3820
3566
  }
3821
3567
 
3568
+ // package.json
3569
+ var package_default = {
3570
+ name: "hypermail-mcp",
3571
+ version: "0.6.3",
3572
+ description: "Unified email MCP server \u2014 operate any inbox (Outlook now, IMAP/Gmail later) by passing an email address.",
3573
+ type: "module",
3574
+ bin: {
3575
+ "hypermail-mcp": "dist/cli.js"
3576
+ },
3577
+ main: "dist/cli.js",
3578
+ files: [
3579
+ "dist",
3580
+ "README.md",
3581
+ "LICENSE"
3582
+ ],
3583
+ scripts: {
3584
+ build: "tsup",
3585
+ dev: "tsup --watch",
3586
+ "dev:http": "tsup && node dist/cli.js --http --config hypermail-config.http.json",
3587
+ start: "node dist/cli.js",
3588
+ typecheck: "tsc --noEmit",
3589
+ test: "vitest run",
3590
+ "test:watch": "vitest",
3591
+ prepublishOnly: "pnpm build && pnpm test"
3592
+ },
3593
+ engines: {
3594
+ node: ">=20"
3595
+ },
3596
+ keywords: [
3597
+ "mcp",
3598
+ "model-context-protocol",
3599
+ "email",
3600
+ "outlook",
3601
+ "microsoft-graph",
3602
+ "imap"
3603
+ ],
3604
+ license: "MIT",
3605
+ repository: {
3606
+ type: "git",
3607
+ url: "git+https://github.com/mateotiedra/hypermail-mcp.git"
3608
+ },
3609
+ bugs: {
3610
+ url: "https://github.com/mateotiedra/hypermail-mcp/issues"
3611
+ },
3612
+ dependencies: {
3613
+ "@azure/msal-node": "^2.16.2",
3614
+ "@microsoft/microsoft-graph-client": "^3.0.7",
3615
+ "@modelcontextprotocol/sdk": "^1.0.4",
3616
+ "google-auth-library": "^9.15.1",
3617
+ googleapis: "^144.0.0",
3618
+ imapflow: "^1.3.3",
3619
+ "isomorphic-fetch": "^3.0.0",
3620
+ marked: "^18.0.4",
3621
+ nodemailer: "^8.0.8",
3622
+ turndown: "^7.2.4",
3623
+ zod: "^4.4.3"
3624
+ },
3625
+ optionalDependencies: {
3626
+ keytar: "^7.9.0"
3627
+ },
3628
+ devDependencies: {
3629
+ "@types/isomorphic-fetch": "^0.0.39",
3630
+ "@types/node": "^22.10.2",
3631
+ "@types/nodemailer": "^8.0.0",
3632
+ tsup: "^8.3.5",
3633
+ typescript: "^5.7.2",
3634
+ vitest: "^2.1.8"
3635
+ }
3636
+ };
3637
+
3822
3638
  // src/version.ts
3823
- var VERSION = "0.4.1";
3639
+ var VERSION = package_default.version;
3824
3640
 
3825
3641
  // src/config.ts
3826
3642
  import { readFileSync } from "fs";
3827
- import { z as z8 } from "zod";
3828
- var httpConfigSchema = z8.object({
3829
- enabled: z8.boolean().default(false),
3830
- port: z8.number().int().min(1).max(65535).default(3e3),
3831
- host: z8.string().default("127.0.0.1")
3643
+ import { z as z7 } from "zod";
3644
+ var httpConfigSchema = z7.object({
3645
+ enabled: z7.boolean().default(false),
3646
+ port: z7.number().int().min(1).max(65535).default(3e3),
3647
+ host: z7.string().default("127.0.0.1")
3832
3648
  });
3833
- var toolsConfigSchema = z8.object({
3834
- disabled: z8.array(z8.string()).optional(),
3835
- enabled: z8.array(z8.string()).optional()
3649
+ var toolsConfigSchema = z7.object({
3650
+ disabled: z7.array(z7.string()).optional(),
3651
+ enabled: z7.array(z7.string()).optional()
3836
3652
  });
3837
- var outlookProviderSchema = z8.object({
3838
- clientId: z8.string().optional(),
3839
- tenantId: z8.string().optional()
3653
+ var outlookProviderSchema = z7.object({
3654
+ clientId: z7.string().optional(),
3655
+ tenantId: z7.string().optional()
3840
3656
  });
3841
- var gmailProviderSchema = z8.object({
3842
- clientId: z8.string().optional(),
3843
- clientSecret: z8.string().optional()
3657
+ var gmailProviderSchema = z7.object({
3658
+ clientId: z7.string().optional(),
3659
+ clientSecret: z7.string().optional()
3844
3660
  });
3845
- var providersConfigSchema = z8.object({
3661
+ var providersConfigSchema = z7.object({
3846
3662
  outlook: outlookProviderSchema.optional(),
3847
3663
  gmail: gmailProviderSchema.optional()
3848
3664
  });
3849
- var watchConfigSchema = z8.object({
3850
- enabled: z8.boolean().default(true),
3851
- pollIntervalSeconds: z8.number().int().min(10).max(3600).default(60)
3665
+ var watchConfigSchema = z7.object({
3666
+ enabled: z7.boolean().default(true),
3667
+ pollIntervalSeconds: z7.number().int().min(10).max(3600).default(60)
3852
3668
  });
3853
- var rawConfigSchema = z8.object({
3854
- dataDir: z8.string().optional(),
3669
+ var rawConfigSchema = z7.object({
3670
+ dataDir: z7.string().optional(),
3855
3671
  http: httpConfigSchema.optional(),
3856
3672
  tools: toolsConfigSchema.optional(),
3857
3673
  providers: providersConfigSchema.optional(),
@@ -3948,9 +3764,7 @@ function loadConfig(configPath, cliOverrides = {}) {
3948
3764
  dataDir: cliOverrides.dataDir ?? parsed.dataDir ?? process.env.HYPERMAIL_MCP_DATA_DIR,
3949
3765
  http,
3950
3766
  tools: parsed.tools ? { disabled: parsed.tools.disabled, enabled: parsed.tools.enabled } : void 0,
3951
- providers: parsed.providers,
3952
- watch: parsed.watch,
3953
- agentsConfigPath: cliOverrides.agentsConfig ?? process.env.HYPERMAIL_AGENTS_CONFIG
3767
+ providers: parsed.providers
3954
3768
  };
3955
3769
  }
3956
3770
  function resolveTools(config) {
@@ -3963,327 +3777,29 @@ function resolveTools(config) {
3963
3777
  };
3964
3778
  }
3965
3779
 
3966
- // src/watcher/manager.ts
3967
- var WatcherManager = class {
3968
- opts;
3969
- timers = [];
3970
- running = false;
3971
- /** Per-account inflight guards to prevent overlapping polls. */
3972
- inflight = /* @__PURE__ */ new Map();
3973
- /** Accounts with active polling timers (lowercased email). */
3974
- tracked = /* @__PURE__ */ new Set();
3975
- constructor(opts) {
3976
- this.opts = opts;
3977
- }
3978
- start() {
3979
- if (this.running) return;
3980
- this.running = true;
3981
- this.scanAccounts();
3982
- const rescanTimer = setInterval(() => {
3983
- if (!this.running) return;
3984
- this.scanAccounts();
3985
- }, this.opts.pollIntervalSeconds * 1e3);
3986
- this.timers.push(rescanTimer);
3987
- }
3988
- stop() {
3989
- this.running = false;
3990
- for (const t of this.timers) clearInterval(t);
3991
- this.timers = [];
3992
- this.tracked.clear();
3993
- }
3994
- // ── internals ──
3995
- scanAccounts() {
3996
- let accounts = this.opts.store.listAccounts();
3997
- if (this.opts.accountFilter) {
3998
- const filter = new Set(this.opts.accountFilter.map((e) => e.toLowerCase()));
3999
- accounts = accounts.filter((a) => filter.has(a.email.toLowerCase()));
4000
- }
4001
- for (const account of accounts) {
4002
- this.schedulePoll(account);
4003
- }
4004
- }
4005
- schedulePoll(account) {
4006
- const key = account.email.toLowerCase();
4007
- if (this.tracked.has(key)) return;
4008
- this.tracked.add(key);
4009
- this.pollAccount(account).catch(() => {
4010
- });
4011
- const timer = setInterval(() => {
4012
- if (!this.running) return;
4013
- this.pollAccount(account).catch(() => {
4014
- });
4015
- }, this.opts.pollIntervalSeconds * 1e3);
4016
- this.timers.push(timer);
4017
- }
4018
- async pollAccount(account) {
4019
- const key = account.email.toLowerCase();
4020
- if (this.inflight.get(key)) return;
4021
- this.inflight.set(key, true);
4022
- try {
4023
- const { provider } = this.opts.registry.resolveByEmail(account.email);
4024
- const seenIds = new Set(account.lastSeenIds ?? []);
4025
- const isFirstPoll = !account.lastSeenAt && !account.lastSeenIds?.length;
4026
- const limit = 25;
4027
- const MAX_PAGES = 5;
4028
- let skip = 0;
4029
- let pageCount = 0;
4030
- const newEmails = [];
4031
- let newestTimestamp = account.lastSeenAt ?? "";
4032
- let hitBoundary = false;
4033
- while (pageCount < MAX_PAGES) {
4034
- const { items, hasMore } = await provider.listEmails(account, {
4035
- folder: "inbox",
4036
- limit,
4037
- skip
4038
- });
4039
- pageCount++;
4040
- for (const item of items) {
4041
- if (!item.receivedAt) continue;
4042
- if (seenIds.has(item.id)) {
4043
- hitBoundary = true;
4044
- break;
4045
- }
4046
- newEmails.push(item);
4047
- if (item.receivedAt > newestTimestamp) {
4048
- newestTimestamp = item.receivedAt;
4049
- }
4050
- }
4051
- if (hitBoundary || !hasMore) break;
4052
- skip += limit;
4053
- }
4054
- if (!isFirstPoll && newEmails.length > 0) {
4055
- this.enqueue({
4056
- type: "new_emails",
4057
- account: account.email,
4058
- emails: newEmails,
4059
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
4060
- });
4061
- }
4062
- if (isFirstPoll && newEmails.length === 0 && !newestTimestamp) {
4063
- newestTimestamp = (/* @__PURE__ */ new Date()).toISOString();
4064
- }
4065
- const newIds = newEmails.map((e) => e.id);
4066
- const updatedLastSeenIds = [
4067
- ...newIds,
4068
- ...account.lastSeenIds ?? []
4069
- ].slice(0, 200);
4070
- try {
4071
- await this.opts.store.upsertAccount({
4072
- ...account,
4073
- lastSeenAt: newestTimestamp || void 0,
4074
- lastSeenIds: updatedLastSeenIds
4075
- });
4076
- } catch (storeErr) {
4077
- console.error(
4078
- "[hypermail-mcp] failed to persist poll state for",
4079
- account.email,
4080
- ":",
4081
- storeErr instanceof Error ? storeErr.message : String(storeErr)
4082
- );
4083
- }
4084
- } catch (err) {
4085
- this.enqueue({
4086
- type: "auth_failure",
4087
- account: account.email,
4088
- error: err instanceof Error ? err.message : String(err),
4089
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
4090
- });
4091
- } finally {
4092
- this.inflight.delete(key);
4093
- }
4094
- }
4095
- enqueue(notification) {
4096
- this.opts.buffer.push(notification);
4097
- try {
4098
- this.opts.onNotification(notification);
4099
- } catch {
4100
- }
4101
- }
4102
- };
4103
-
4104
- // src/config/agents-config.ts
4105
- import { readFileSync as readFileSync2, watch } from "fs";
4106
- import { load as loadYaml } from "js-yaml";
4107
- import { z as z9 } from "zod";
4108
- var agentDefSchema = z9.object({
4109
- id: z9.string().min(1).regex(
4110
- /^[a-z0-9_-]+$/,
4111
- "agent id must contain only lowercase letters, digits, hyphens, and underscores"
4112
- ),
4113
- api_key: z9.string().min(1).regex(
4114
- /^hm_sk_[a-f0-9]{64}$/,
4115
- "api_key must match hm_sk_ prefix + 64 hex chars (use `hypermail-mcp generate-key`)"
4116
- ),
4117
- name: z9.string().min(1),
4118
- accounts: z9.array(z9.string().email()).optional().default([]),
4119
- provisioning: z9.boolean().optional().default(false)
4120
- });
4121
- var emailAccountDefSchema = z9.object({
4122
- provider: z9.enum(["outlook", "imap", "gmail"]),
4123
- display_name: z9.string().optional()
4124
- });
4125
- var agentsConfigSchema = z9.object({
4126
- agents: z9.array(agentDefSchema).optional().default([]),
4127
- email_accounts: z9.record(z9.string().email(), emailAccountDefSchema).optional().default({})
4128
- });
4129
- function loadAgentsConfig(configPath) {
4130
- let raw;
4131
- try {
4132
- raw = loadYaml(readFileSync2(configPath, "utf-8"));
4133
- } catch (err) {
4134
- if (err instanceof Error && "code" in err && err.code === "ENOENT") {
4135
- throw new Error(
4136
- `Agents config file not found: ${configPath}. Create an agents.yaml with at least one agent to enable HTTP multi-tenant mode.`
4137
- );
4138
- }
4139
- const detail = err instanceof Error ? err.message : String(err);
4140
- throw new Error(`Failed to parse agents config "${configPath}": ${detail}`);
4141
- }
4142
- const parsed = agentsConfigSchema.parse(raw ?? {});
4143
- const ids = /* @__PURE__ */ new Set();
4144
- for (const a of parsed.agents) {
4145
- if (ids.has(a.id)) {
4146
- throw new Error(`Duplicate agent id "${a.id}" in agents config`);
4147
- }
4148
- ids.add(a.id);
4149
- }
4150
- return {
4151
- agents: parsed.agents.map((a) => ({
4152
- id: a.id,
4153
- api_key: a.api_key,
4154
- name: a.name,
4155
- accounts: a.accounts,
4156
- provisioning: a.provisioning
4157
- })),
4158
- email_accounts: parsed.email_accounts
4159
- };
4160
- }
4161
- async function syncAgentsToStore(config, store) {
4162
- const configAgentIds = new Set(config.agents.map((a) => a.id));
4163
- const storedAgents = store.listAgents();
4164
- for (const def of config.agents) {
4165
- await store.upsertAgent({
4166
- id: def.id,
4167
- plaintextApiKey: def.api_key,
4168
- name: def.name,
4169
- accounts: def.accounts,
4170
- provisioning: def.provisioning
4171
- });
4172
- }
4173
- const removed = [];
4174
- for (const stored of storedAgents) {
4175
- if (!configAgentIds.has(stored.id)) {
4176
- await store.removeAgent(stored.id);
4177
- removed.push(stored.id);
4178
- }
4179
- }
4180
- return removed;
4181
- }
4182
- function watchAgentsConfig(configPath, store, onChange, onError) {
4183
- let timer = null;
4184
- let watcher = null;
4185
- const reload = async () => {
4186
- try {
4187
- const config = loadAgentsConfig(configPath);
4188
- const removed = await syncAgentsToStore(config, store);
4189
- onChange(removed);
4190
- } catch (err) {
4191
- onError(err instanceof Error ? err : new Error(String(err)));
4192
- }
4193
- };
4194
- reload().catch((err) => onError(err));
4195
- try {
4196
- watcher = watch(configPath, (_eventType) => {
4197
- if (timer) clearTimeout(timer);
4198
- timer = setTimeout(() => {
4199
- timer = null;
4200
- reload().catch((err) => onError(err));
4201
- }, 200);
4202
- });
4203
- } catch (err) {
4204
- }
4205
- return {
4206
- close() {
4207
- if (timer) clearTimeout(timer);
4208
- if (watcher) watcher.close();
4209
- }
4210
- };
4211
- }
4212
-
4213
3780
  // src/server.ts
4214
3781
  async function startServer(opts) {
4215
3782
  const { config } = opts;
4216
3783
  const store = await AccountStore.open({ dataDir: config.dataDir });
4217
3784
  const registry = buildRegistry({ store, providers: config.providers });
4218
3785
  const tools = resolveTools(config);
4219
- const watchEnabled = config.http.enabled && config.watch?.enabled !== false;
4220
- const notificationBuffer = watchEnabled ? [] : void 0;
4221
- let agentStoreForFactory;
4222
- const createServer = (agentContext = null) => {
3786
+ const createServer = () => {
4223
3787
  const s = new McpServer(
4224
3788
  { name: "hypermail-mcp", version: VERSION },
4225
3789
  { capabilities: { tools: {}, logging: {} } }
4226
3790
  );
4227
- registerTools(s, { store, registry, tools, notificationBuffer, agentContext, agentStore: agentStoreForFactory });
3791
+ registerTools(s, { store, registry, tools });
4228
3792
  return s;
4229
3793
  };
4230
3794
  if (config.http.enabled) {
4231
- let liveReloadHandle;
4232
- if (config.agentsConfigPath) {
4233
- agentStoreForFactory = await AgentStore.open({ dataDir: config.dataDir });
4234
- liveReloadHandle = watchAgentsConfig(
4235
- path4.resolve(config.agentsConfigPath),
4236
- agentStoreForFactory,
4237
- (_removedIds) => {
4238
- },
4239
- (err) => {
4240
- console.error("[hypermail-mcp] agents.yaml reload error:", err.message);
4241
- }
4242
- );
4243
- }
4244
- const notifyTargets = /* @__PURE__ */ new Set();
4245
- const accountFilter = agentStoreForFactory ? (() => {
4246
- const all = /* @__PURE__ */ new Set();
4247
- for (const agent of agentStoreForFactory.listAgents()) {
4248
- for (const email of agent.accounts) {
4249
- all.add(email.toLowerCase());
4250
- }
4251
- }
4252
- return all.size > 0 ? [...all] : void 0;
4253
- })() : void 0;
4254
- if (watchEnabled) {
4255
- const watcher = new WatcherManager({
4256
- registry,
4257
- store,
4258
- pollIntervalSeconds: config.watch?.pollIntervalSeconds ?? 60,
4259
- accountFilter,
4260
- onNotification: (notification) => {
4261
- for (const fn of notifyTargets) {
4262
- fn(notification);
4263
- }
4264
- },
4265
- buffer: notificationBuffer
4266
- });
4267
- watcher.start();
4268
- }
4269
- await startHttp(
4270
- createServer,
4271
- config.http.host,
4272
- config.http.port,
4273
- notifyTargets,
4274
- agentStoreForFactory
4275
- );
4276
- if (liveReloadHandle) {
4277
- process.on("SIGINT", () => liveReloadHandle.close());
4278
- process.on("SIGTERM", () => liveReloadHandle.close());
4279
- }
3795
+ await startHttp(createServer, config.http.host, config.http.port);
4280
3796
  } else {
4281
3797
  const server = createServer();
4282
3798
  const transport = new StdioServerTransport();
4283
3799
  await server.connect(transport);
4284
3800
  }
4285
3801
  }
4286
- async function startHttp(createServer, host, port, notifyTargets, agentStore) {
3802
+ async function startHttp(createServer, host, port) {
4287
3803
  const sessions = /* @__PURE__ */ new Map();
4288
3804
  const http = createHttpServer(async (req, res) => {
4289
3805
  try {
@@ -4295,57 +3811,18 @@ async function startHttp(createServer, host, port, notifyTargets, agentStore) {
4295
3811
  const sessionId = req.headers["mcp-session-id"] ?? void 0;
4296
3812
  let session = sessionId ? sessions.get(sessionId) : void 0;
4297
3813
  if (!session) {
4298
- let agentContext = null;
4299
- if (agentStore) {
4300
- const apiKey = req.headers["x-api-key"]?.trim();
4301
- if (!apiKey) {
4302
- res.statusCode = 401;
4303
- res.setHeader("Content-Type", "application/json");
4304
- res.end(JSON.stringify({ error: "Missing x-api-key header" }));
4305
- return;
4306
- }
4307
- const agent = agentStore.findAgentByApiKey(apiKey);
4308
- if (!agent) {
4309
- res.statusCode = 401;
4310
- res.setHeader("Content-Type", "application/json");
4311
- res.end(JSON.stringify({ error: "Invalid API key" }));
4312
- return;
4313
- }
4314
- agentContext = {
4315
- agentId: agent.id,
4316
- accounts: agent.accounts,
4317
- provisioning: agent.provisioning
4318
- };
4319
- }
4320
- const server = createServer(agentContext);
3814
+ const server = createServer();
4321
3815
  const transport = new StreamableHTTPServerTransport({
4322
3816
  sessionIdGenerator: () => randomUUID6(),
4323
3817
  onsessioninitialized: (sid) => {
4324
- sessions.set(sid, { transport, server, agentContext });
4325
- const agentAccounts = agentContext ? new Set(agentContext.accounts.map((a) => a.toLowerCase())) : null;
4326
- const notifyFn = (n) => {
4327
- if (agentAccounts && !agentAccounts.has(n.account.toLowerCase())) {
4328
- return;
4329
- }
4330
- server.server.notification({
4331
- method: "notifications/message",
4332
- params: {
4333
- level: n.type === "new_emails" ? "notice" : "warning",
4334
- logger: "hypermail-watch",
4335
- data: n
4336
- }
4337
- }).catch(() => {
4338
- });
4339
- };
4340
- notifyTargets.add(notifyFn);
3818
+ sessions.set(sid, { transport, server });
4341
3819
  transport.onclose = () => {
4342
3820
  if (transport.sessionId) sessions.delete(transport.sessionId);
4343
- notifyTargets.delete(notifyFn);
4344
3821
  };
4345
3822
  }
4346
3823
  });
4347
3824
  await server.connect(transport);
4348
- session = { transport, server, agentContext };
3825
+ session = { transport, server };
4349
3826
  }
4350
3827
  let body = void 0;
4351
3828
  if (req.method === "POST" || req.method === "DELETE") {
@@ -4393,9 +3870,6 @@ function parseArgs(argv) {
4393
3870
  case "--config":
4394
3871
  out.config = String(argv[++i] ?? "");
4395
3872
  break;
4396
- case "--agents-config":
4397
- out.agentsConfig = String(argv[++i] ?? "");
4398
- break;
4399
3873
  case "-h":
4400
3874
  case "--help":
4401
3875
  out.help = true;
@@ -4455,8 +3929,7 @@ async function main() {
4455
3929
  http: opts.http,
4456
3930
  port: opts.port,
4457
3931
  host: opts.host,
4458
- dataDir: opts.dataDir,
4459
- agentsConfig: opts.agentsConfig
3932
+ dataDir: opts.dataDir
4460
3933
  });
4461
3934
  await startServer({ config });
4462
3935
  }