hypermail-mcp 0.6.2 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +64 -145
- package/dist/cli.js +178 -643
- package/dist/cli.js.map +1 -1
- package/package.json +1 -3
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
|
|
1569
|
-
const result = await imap.mailboxCreate(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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,69 +3555,20 @@ 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
|
|
3808
|
-
registerAccountTools(server, { store, registry, tools
|
|
3809
|
-
registerBrowseTools(server, { registry, tools
|
|
3810
|
-
registerFolderTools(server, { registry, tools
|
|
3811
|
-
registerOrganizeTools(server, { registry, tools
|
|
3812
|
-
registerComposeTools(server, { store, registry, tools
|
|
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
|
|
|
3822
3568
|
// package.json
|
|
3823
3569
|
var package_default = {
|
|
3824
3570
|
name: "hypermail-mcp",
|
|
3825
|
-
version: "0.
|
|
3571
|
+
version: "0.7.0",
|
|
3826
3572
|
description: "Unified email MCP server \u2014 operate any inbox (Outlook now, IMAP/Gmail later) by passing an email address.",
|
|
3827
3573
|
type: "module",
|
|
3828
3574
|
bin: {
|
|
@@ -3871,7 +3617,6 @@ var package_default = {
|
|
|
3871
3617
|
googleapis: "^144.0.0",
|
|
3872
3618
|
imapflow: "^1.3.3",
|
|
3873
3619
|
"isomorphic-fetch": "^3.0.0",
|
|
3874
|
-
"js-yaml": "^4.2.0",
|
|
3875
3620
|
marked: "^18.0.4",
|
|
3876
3621
|
nodemailer: "^8.0.8",
|
|
3877
3622
|
turndown: "^7.2.4",
|
|
@@ -3882,7 +3627,6 @@ var package_default = {
|
|
|
3882
3627
|
},
|
|
3883
3628
|
devDependencies: {
|
|
3884
3629
|
"@types/isomorphic-fetch": "^0.0.39",
|
|
3885
|
-
"@types/js-yaml": "^4.0.9",
|
|
3886
3630
|
"@types/node": "^22.10.2",
|
|
3887
3631
|
"@types/nodemailer": "^8.0.0",
|
|
3888
3632
|
tsup: "^8.3.5",
|
|
@@ -3894,36 +3638,153 @@ var package_default = {
|
|
|
3894
3638
|
// src/version.ts
|
|
3895
3639
|
var VERSION = package_default.version;
|
|
3896
3640
|
|
|
3641
|
+
// src/watcher/webhook.ts
|
|
3642
|
+
async function postWebhook(email, config) {
|
|
3643
|
+
if (!config.webhook) return false;
|
|
3644
|
+
const { url, retry } = config.webhook;
|
|
3645
|
+
const maxAttempts = retry.maxAttempts;
|
|
3646
|
+
const baseDelayMs = retry.baseDelayMs;
|
|
3647
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
3648
|
+
if (attempt > 0) {
|
|
3649
|
+
const delay = baseDelayMs * 2 ** (attempt - 1);
|
|
3650
|
+
await sleep(delay);
|
|
3651
|
+
}
|
|
3652
|
+
try {
|
|
3653
|
+
const res = await fetch(url, {
|
|
3654
|
+
method: "POST",
|
|
3655
|
+
headers: { "content-type": "application/json" },
|
|
3656
|
+
body: JSON.stringify(email)
|
|
3657
|
+
});
|
|
3658
|
+
if (res.ok) return true;
|
|
3659
|
+
console.error(
|
|
3660
|
+
`[hypermail-watch] webhook POST ${email.id} attempt ${attempt + 1}/${maxAttempts}: HTTP ${res.status}`
|
|
3661
|
+
);
|
|
3662
|
+
} catch (err) {
|
|
3663
|
+
const code = err.code ?? "";
|
|
3664
|
+
console.error(
|
|
3665
|
+
`[hypermail-watch] webhook POST ${email.id} attempt ${attempt + 1}/${maxAttempts}: ${code || String(err)}`
|
|
3666
|
+
);
|
|
3667
|
+
}
|
|
3668
|
+
}
|
|
3669
|
+
console.error(
|
|
3670
|
+
`[hypermail-watch] webhook delivery failed after ${maxAttempts} retries for ${email.id}`
|
|
3671
|
+
);
|
|
3672
|
+
return false;
|
|
3673
|
+
}
|
|
3674
|
+
function sleep(ms) {
|
|
3675
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
3676
|
+
}
|
|
3677
|
+
|
|
3678
|
+
// src/watcher/manager.ts
|
|
3679
|
+
var WatcherManager = class {
|
|
3680
|
+
constructor(store, registry, config) {
|
|
3681
|
+
this.store = store;
|
|
3682
|
+
this.registry = registry;
|
|
3683
|
+
this.config = config;
|
|
3684
|
+
}
|
|
3685
|
+
store;
|
|
3686
|
+
registry;
|
|
3687
|
+
config;
|
|
3688
|
+
intervalId = null;
|
|
3689
|
+
/** Start the poll loop. Fires immediately on the first tick, then every
|
|
3690
|
+
* `pollIntervalSeconds`. Safe to call multiple times — subsequent calls
|
|
3691
|
+
* are no-ops. */
|
|
3692
|
+
start() {
|
|
3693
|
+
if (this.intervalId !== null) return;
|
|
3694
|
+
this.poll();
|
|
3695
|
+
this.intervalId = setInterval(
|
|
3696
|
+
() => this.poll(),
|
|
3697
|
+
this.config.pollIntervalSeconds * 1e3
|
|
3698
|
+
);
|
|
3699
|
+
}
|
|
3700
|
+
/** Stop the poll loop and release the interval. Safe to call when already
|
|
3701
|
+
* stopped. */
|
|
3702
|
+
stop() {
|
|
3703
|
+
if (this.intervalId !== null) {
|
|
3704
|
+
clearInterval(this.intervalId);
|
|
3705
|
+
this.intervalId = null;
|
|
3706
|
+
}
|
|
3707
|
+
}
|
|
3708
|
+
// ── private ──
|
|
3709
|
+
async poll() {
|
|
3710
|
+
const accounts = this.store.listAccounts();
|
|
3711
|
+
for (const acct of accounts) {
|
|
3712
|
+
try {
|
|
3713
|
+
await this.pollAccount(acct.email);
|
|
3714
|
+
} catch (err) {
|
|
3715
|
+
console.error(
|
|
3716
|
+
`[hypermail-watch] poll failed for ${acct.email}:`,
|
|
3717
|
+
err
|
|
3718
|
+
);
|
|
3719
|
+
}
|
|
3720
|
+
}
|
|
3721
|
+
}
|
|
3722
|
+
async pollAccount(email) {
|
|
3723
|
+
const { provider, account } = this.registry.resolveByEmail(email);
|
|
3724
|
+
const result = await provider.listEmails(account, {
|
|
3725
|
+
folder: "inbox",
|
|
3726
|
+
limit: 50
|
|
3727
|
+
});
|
|
3728
|
+
const knownIds = [...account.lastSeenIds ?? []];
|
|
3729
|
+
const newEmails = result.items.filter((e) => !knownIds.includes(e.id));
|
|
3730
|
+
if (newEmails.length === 0) return;
|
|
3731
|
+
for (const summary of newEmails) {
|
|
3732
|
+
try {
|
|
3733
|
+
const full = await provider.readEmail(account, summary.id);
|
|
3734
|
+
await this.emit(full);
|
|
3735
|
+
knownIds.unshift(summary.id);
|
|
3736
|
+
} catch (err) {
|
|
3737
|
+
console.error(
|
|
3738
|
+
`[hypermail-watch] emission failed for ${email}/${summary.id}:`,
|
|
3739
|
+
err
|
|
3740
|
+
);
|
|
3741
|
+
}
|
|
3742
|
+
}
|
|
3743
|
+
const capped = knownIds.slice(0, 200);
|
|
3744
|
+
await this.store.upsertAccount({ ...account, lastSeenIds: capped });
|
|
3745
|
+
}
|
|
3746
|
+
async emit(full) {
|
|
3747
|
+
await postWebhook(full, this.config);
|
|
3748
|
+
}
|
|
3749
|
+
};
|
|
3750
|
+
|
|
3897
3751
|
// src/config.ts
|
|
3898
3752
|
import { readFileSync } from "fs";
|
|
3899
|
-
import { z as
|
|
3900
|
-
var httpConfigSchema =
|
|
3901
|
-
enabled:
|
|
3902
|
-
port:
|
|
3903
|
-
host:
|
|
3753
|
+
import { z as z7 } from "zod";
|
|
3754
|
+
var httpConfigSchema = z7.object({
|
|
3755
|
+
enabled: z7.boolean().default(false),
|
|
3756
|
+
port: z7.number().int().min(1).max(65535).default(3e3),
|
|
3757
|
+
host: z7.string().default("127.0.0.1")
|
|
3904
3758
|
});
|
|
3905
|
-
var toolsConfigSchema =
|
|
3906
|
-
disabled:
|
|
3907
|
-
enabled:
|
|
3759
|
+
var toolsConfigSchema = z7.object({
|
|
3760
|
+
disabled: z7.array(z7.string()).optional(),
|
|
3761
|
+
enabled: z7.array(z7.string()).optional()
|
|
3908
3762
|
});
|
|
3909
|
-
var outlookProviderSchema =
|
|
3910
|
-
clientId:
|
|
3911
|
-
tenantId:
|
|
3763
|
+
var outlookProviderSchema = z7.object({
|
|
3764
|
+
clientId: z7.string().optional(),
|
|
3765
|
+
tenantId: z7.string().optional()
|
|
3912
3766
|
});
|
|
3913
|
-
var gmailProviderSchema =
|
|
3914
|
-
clientId:
|
|
3915
|
-
clientSecret:
|
|
3767
|
+
var gmailProviderSchema = z7.object({
|
|
3768
|
+
clientId: z7.string().optional(),
|
|
3769
|
+
clientSecret: z7.string().optional()
|
|
3916
3770
|
});
|
|
3917
|
-
var providersConfigSchema =
|
|
3771
|
+
var providersConfigSchema = z7.object({
|
|
3918
3772
|
outlook: outlookProviderSchema.optional(),
|
|
3919
3773
|
gmail: gmailProviderSchema.optional()
|
|
3920
3774
|
});
|
|
3921
|
-
var watchConfigSchema =
|
|
3922
|
-
enabled:
|
|
3923
|
-
pollIntervalSeconds:
|
|
3775
|
+
var watchConfigSchema = z7.object({
|
|
3776
|
+
enabled: z7.boolean().default(false),
|
|
3777
|
+
pollIntervalSeconds: z7.number().int().min(10).max(3600).default(10),
|
|
3778
|
+
webhook: z7.object({
|
|
3779
|
+
url: z7.string(),
|
|
3780
|
+
retry: z7.object({
|
|
3781
|
+
maxAttempts: z7.number().int().min(1).max(10).default(5),
|
|
3782
|
+
baseDelayMs: z7.number().int().min(100).default(1e3)
|
|
3783
|
+
}).optional()
|
|
3784
|
+
}).optional()
|
|
3924
3785
|
});
|
|
3925
|
-
var rawConfigSchema =
|
|
3926
|
-
dataDir:
|
|
3786
|
+
var rawConfigSchema = z7.object({
|
|
3787
|
+
dataDir: z7.string().optional(),
|
|
3927
3788
|
http: httpConfigSchema.optional(),
|
|
3928
3789
|
tools: toolsConfigSchema.optional(),
|
|
3929
3790
|
providers: providersConfigSchema.optional(),
|
|
@@ -4016,13 +3877,20 @@ function loadConfig(configPath, cliOverrides = {}) {
|
|
|
4016
3877
|
port: cliOverrides.port ?? parsed.http?.port ?? 3e3,
|
|
4017
3878
|
host: cliOverrides.host ?? parsed.http?.host ?? "127.0.0.1"
|
|
4018
3879
|
};
|
|
3880
|
+
let watch;
|
|
3881
|
+
if (parsed.watch || process.env.HYPERMAIL_WATCH_ENABLED === "true") {
|
|
3882
|
+
watch = {
|
|
3883
|
+
enabled: process.env.HYPERMAIL_WATCH_ENABLED === "true" || Boolean(parsed.watch?.enabled),
|
|
3884
|
+
pollIntervalSeconds: parsed.watch?.pollIntervalSeconds ?? 10,
|
|
3885
|
+
webhook: parsed.watch?.webhook
|
|
3886
|
+
};
|
|
3887
|
+
}
|
|
4019
3888
|
return {
|
|
4020
3889
|
dataDir: cliOverrides.dataDir ?? parsed.dataDir ?? process.env.HYPERMAIL_MCP_DATA_DIR,
|
|
4021
3890
|
http,
|
|
4022
3891
|
tools: parsed.tools ? { disabled: parsed.tools.disabled, enabled: parsed.tools.enabled } : void 0,
|
|
4023
3892
|
providers: parsed.providers,
|
|
4024
|
-
watch
|
|
4025
|
-
agentsConfigPath: cliOverrides.agentsConfig ?? process.env.HYPERMAIL_AGENTS_CONFIG
|
|
3893
|
+
watch
|
|
4026
3894
|
};
|
|
4027
3895
|
}
|
|
4028
3896
|
function resolveTools(config) {
|
|
@@ -4035,327 +3903,37 @@ function resolveTools(config) {
|
|
|
4035
3903
|
};
|
|
4036
3904
|
}
|
|
4037
3905
|
|
|
4038
|
-
// src/watcher/manager.ts
|
|
4039
|
-
var WatcherManager = class {
|
|
4040
|
-
opts;
|
|
4041
|
-
timers = [];
|
|
4042
|
-
running = false;
|
|
4043
|
-
/** Per-account inflight guards to prevent overlapping polls. */
|
|
4044
|
-
inflight = /* @__PURE__ */ new Map();
|
|
4045
|
-
/** Accounts with active polling timers (lowercased email). */
|
|
4046
|
-
tracked = /* @__PURE__ */ new Set();
|
|
4047
|
-
constructor(opts) {
|
|
4048
|
-
this.opts = opts;
|
|
4049
|
-
}
|
|
4050
|
-
start() {
|
|
4051
|
-
if (this.running) return;
|
|
4052
|
-
this.running = true;
|
|
4053
|
-
this.scanAccounts();
|
|
4054
|
-
const rescanTimer = setInterval(() => {
|
|
4055
|
-
if (!this.running) return;
|
|
4056
|
-
this.scanAccounts();
|
|
4057
|
-
}, this.opts.pollIntervalSeconds * 1e3);
|
|
4058
|
-
this.timers.push(rescanTimer);
|
|
4059
|
-
}
|
|
4060
|
-
stop() {
|
|
4061
|
-
this.running = false;
|
|
4062
|
-
for (const t of this.timers) clearInterval(t);
|
|
4063
|
-
this.timers = [];
|
|
4064
|
-
this.tracked.clear();
|
|
4065
|
-
}
|
|
4066
|
-
// ── internals ──
|
|
4067
|
-
scanAccounts() {
|
|
4068
|
-
let accounts = this.opts.store.listAccounts();
|
|
4069
|
-
if (this.opts.accountFilter) {
|
|
4070
|
-
const filter = new Set(this.opts.accountFilter.map((e) => e.toLowerCase()));
|
|
4071
|
-
accounts = accounts.filter((a) => filter.has(a.email.toLowerCase()));
|
|
4072
|
-
}
|
|
4073
|
-
for (const account of accounts) {
|
|
4074
|
-
this.schedulePoll(account);
|
|
4075
|
-
}
|
|
4076
|
-
}
|
|
4077
|
-
schedulePoll(account) {
|
|
4078
|
-
const key = account.email.toLowerCase();
|
|
4079
|
-
if (this.tracked.has(key)) return;
|
|
4080
|
-
this.tracked.add(key);
|
|
4081
|
-
this.pollAccount(account).catch(() => {
|
|
4082
|
-
});
|
|
4083
|
-
const timer = setInterval(() => {
|
|
4084
|
-
if (!this.running) return;
|
|
4085
|
-
this.pollAccount(account).catch(() => {
|
|
4086
|
-
});
|
|
4087
|
-
}, this.opts.pollIntervalSeconds * 1e3);
|
|
4088
|
-
this.timers.push(timer);
|
|
4089
|
-
}
|
|
4090
|
-
async pollAccount(account) {
|
|
4091
|
-
const key = account.email.toLowerCase();
|
|
4092
|
-
if (this.inflight.get(key)) return;
|
|
4093
|
-
this.inflight.set(key, true);
|
|
4094
|
-
try {
|
|
4095
|
-
const { provider } = this.opts.registry.resolveByEmail(account.email);
|
|
4096
|
-
const seenIds = new Set(account.lastSeenIds ?? []);
|
|
4097
|
-
const isFirstPoll = !account.lastSeenAt && !account.lastSeenIds?.length;
|
|
4098
|
-
const limit = 25;
|
|
4099
|
-
const MAX_PAGES = 5;
|
|
4100
|
-
let skip = 0;
|
|
4101
|
-
let pageCount = 0;
|
|
4102
|
-
const newEmails = [];
|
|
4103
|
-
let newestTimestamp = account.lastSeenAt ?? "";
|
|
4104
|
-
let hitBoundary = false;
|
|
4105
|
-
while (pageCount < MAX_PAGES) {
|
|
4106
|
-
const { items, hasMore } = await provider.listEmails(account, {
|
|
4107
|
-
folder: "inbox",
|
|
4108
|
-
limit,
|
|
4109
|
-
skip
|
|
4110
|
-
});
|
|
4111
|
-
pageCount++;
|
|
4112
|
-
for (const item of items) {
|
|
4113
|
-
if (!item.receivedAt) continue;
|
|
4114
|
-
if (seenIds.has(item.id)) {
|
|
4115
|
-
hitBoundary = true;
|
|
4116
|
-
break;
|
|
4117
|
-
}
|
|
4118
|
-
newEmails.push(item);
|
|
4119
|
-
if (item.receivedAt > newestTimestamp) {
|
|
4120
|
-
newestTimestamp = item.receivedAt;
|
|
4121
|
-
}
|
|
4122
|
-
}
|
|
4123
|
-
if (hitBoundary || !hasMore) break;
|
|
4124
|
-
skip += limit;
|
|
4125
|
-
}
|
|
4126
|
-
if (!isFirstPoll && newEmails.length > 0) {
|
|
4127
|
-
this.enqueue({
|
|
4128
|
-
type: "new_emails",
|
|
4129
|
-
account: account.email,
|
|
4130
|
-
emails: newEmails,
|
|
4131
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
4132
|
-
});
|
|
4133
|
-
}
|
|
4134
|
-
if (isFirstPoll && newEmails.length === 0 && !newestTimestamp) {
|
|
4135
|
-
newestTimestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
4136
|
-
}
|
|
4137
|
-
const newIds = newEmails.map((e) => e.id);
|
|
4138
|
-
const updatedLastSeenIds = [
|
|
4139
|
-
...newIds,
|
|
4140
|
-
...account.lastSeenIds ?? []
|
|
4141
|
-
].slice(0, 200);
|
|
4142
|
-
try {
|
|
4143
|
-
await this.opts.store.upsertAccount({
|
|
4144
|
-
...account,
|
|
4145
|
-
lastSeenAt: newestTimestamp || void 0,
|
|
4146
|
-
lastSeenIds: updatedLastSeenIds
|
|
4147
|
-
});
|
|
4148
|
-
} catch (storeErr) {
|
|
4149
|
-
console.error(
|
|
4150
|
-
"[hypermail-mcp] failed to persist poll state for",
|
|
4151
|
-
account.email,
|
|
4152
|
-
":",
|
|
4153
|
-
storeErr instanceof Error ? storeErr.message : String(storeErr)
|
|
4154
|
-
);
|
|
4155
|
-
}
|
|
4156
|
-
} catch (err) {
|
|
4157
|
-
this.enqueue({
|
|
4158
|
-
type: "auth_failure",
|
|
4159
|
-
account: account.email,
|
|
4160
|
-
error: err instanceof Error ? err.message : String(err),
|
|
4161
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
4162
|
-
});
|
|
4163
|
-
} finally {
|
|
4164
|
-
this.inflight.delete(key);
|
|
4165
|
-
}
|
|
4166
|
-
}
|
|
4167
|
-
enqueue(notification) {
|
|
4168
|
-
this.opts.buffer.push(notification);
|
|
4169
|
-
try {
|
|
4170
|
-
this.opts.onNotification(notification);
|
|
4171
|
-
} catch {
|
|
4172
|
-
}
|
|
4173
|
-
}
|
|
4174
|
-
};
|
|
4175
|
-
|
|
4176
|
-
// src/config/agents-config.ts
|
|
4177
|
-
import { readFileSync as readFileSync2, watch } from "fs";
|
|
4178
|
-
import { load as loadYaml } from "js-yaml";
|
|
4179
|
-
import { z as z9 } from "zod";
|
|
4180
|
-
var agentDefSchema = z9.object({
|
|
4181
|
-
id: z9.string().min(1).regex(
|
|
4182
|
-
/^[a-z0-9_-]+$/,
|
|
4183
|
-
"agent id must contain only lowercase letters, digits, hyphens, and underscores"
|
|
4184
|
-
),
|
|
4185
|
-
api_key: z9.string().min(1).regex(
|
|
4186
|
-
/^hm_sk_[a-f0-9]{64}$/,
|
|
4187
|
-
"api_key must match hm_sk_ prefix + 64 hex chars (use `hypermail-mcp generate-key`)"
|
|
4188
|
-
),
|
|
4189
|
-
name: z9.string().min(1),
|
|
4190
|
-
accounts: z9.array(z9.string().email()).optional().default([]),
|
|
4191
|
-
provisioning: z9.boolean().optional().default(false)
|
|
4192
|
-
});
|
|
4193
|
-
var emailAccountDefSchema = z9.object({
|
|
4194
|
-
provider: z9.enum(["outlook", "imap", "gmail"]),
|
|
4195
|
-
display_name: z9.string().optional()
|
|
4196
|
-
});
|
|
4197
|
-
var agentsConfigSchema = z9.object({
|
|
4198
|
-
agents: z9.array(agentDefSchema).optional().default([]),
|
|
4199
|
-
email_accounts: z9.record(z9.string().email(), emailAccountDefSchema).optional().default({})
|
|
4200
|
-
});
|
|
4201
|
-
function loadAgentsConfig(configPath) {
|
|
4202
|
-
let raw;
|
|
4203
|
-
try {
|
|
4204
|
-
raw = loadYaml(readFileSync2(configPath, "utf-8"));
|
|
4205
|
-
} catch (err) {
|
|
4206
|
-
if (err instanceof Error && "code" in err && err.code === "ENOENT") {
|
|
4207
|
-
throw new Error(
|
|
4208
|
-
`Agents config file not found: ${configPath}. Create an agents.yaml with at least one agent to enable HTTP multi-tenant mode.`
|
|
4209
|
-
);
|
|
4210
|
-
}
|
|
4211
|
-
const detail = err instanceof Error ? err.message : String(err);
|
|
4212
|
-
throw new Error(`Failed to parse agents config "${configPath}": ${detail}`);
|
|
4213
|
-
}
|
|
4214
|
-
const parsed = agentsConfigSchema.parse(raw ?? {});
|
|
4215
|
-
const ids = /* @__PURE__ */ new Set();
|
|
4216
|
-
for (const a of parsed.agents) {
|
|
4217
|
-
if (ids.has(a.id)) {
|
|
4218
|
-
throw new Error(`Duplicate agent id "${a.id}" in agents config`);
|
|
4219
|
-
}
|
|
4220
|
-
ids.add(a.id);
|
|
4221
|
-
}
|
|
4222
|
-
return {
|
|
4223
|
-
agents: parsed.agents.map((a) => ({
|
|
4224
|
-
id: a.id,
|
|
4225
|
-
api_key: a.api_key,
|
|
4226
|
-
name: a.name,
|
|
4227
|
-
accounts: a.accounts,
|
|
4228
|
-
provisioning: a.provisioning
|
|
4229
|
-
})),
|
|
4230
|
-
email_accounts: parsed.email_accounts
|
|
4231
|
-
};
|
|
4232
|
-
}
|
|
4233
|
-
async function syncAgentsToStore(config, store) {
|
|
4234
|
-
const configAgentIds = new Set(config.agents.map((a) => a.id));
|
|
4235
|
-
const storedAgents = store.listAgents();
|
|
4236
|
-
for (const def of config.agents) {
|
|
4237
|
-
await store.upsertAgent({
|
|
4238
|
-
id: def.id,
|
|
4239
|
-
plaintextApiKey: def.api_key,
|
|
4240
|
-
name: def.name,
|
|
4241
|
-
accounts: def.accounts,
|
|
4242
|
-
provisioning: def.provisioning
|
|
4243
|
-
});
|
|
4244
|
-
}
|
|
4245
|
-
const removed = [];
|
|
4246
|
-
for (const stored of storedAgents) {
|
|
4247
|
-
if (!configAgentIds.has(stored.id)) {
|
|
4248
|
-
await store.removeAgent(stored.id);
|
|
4249
|
-
removed.push(stored.id);
|
|
4250
|
-
}
|
|
4251
|
-
}
|
|
4252
|
-
return removed;
|
|
4253
|
-
}
|
|
4254
|
-
function watchAgentsConfig(configPath, store, onChange, onError) {
|
|
4255
|
-
let timer = null;
|
|
4256
|
-
let watcher = null;
|
|
4257
|
-
const reload = async () => {
|
|
4258
|
-
try {
|
|
4259
|
-
const config = loadAgentsConfig(configPath);
|
|
4260
|
-
const removed = await syncAgentsToStore(config, store);
|
|
4261
|
-
onChange(removed);
|
|
4262
|
-
} catch (err) {
|
|
4263
|
-
onError(err instanceof Error ? err : new Error(String(err)));
|
|
4264
|
-
}
|
|
4265
|
-
};
|
|
4266
|
-
reload().catch((err) => onError(err));
|
|
4267
|
-
try {
|
|
4268
|
-
watcher = watch(configPath, (_eventType) => {
|
|
4269
|
-
if (timer) clearTimeout(timer);
|
|
4270
|
-
timer = setTimeout(() => {
|
|
4271
|
-
timer = null;
|
|
4272
|
-
reload().catch((err) => onError(err));
|
|
4273
|
-
}, 200);
|
|
4274
|
-
});
|
|
4275
|
-
} catch (err) {
|
|
4276
|
-
}
|
|
4277
|
-
return {
|
|
4278
|
-
close() {
|
|
4279
|
-
if (timer) clearTimeout(timer);
|
|
4280
|
-
if (watcher) watcher.close();
|
|
4281
|
-
}
|
|
4282
|
-
};
|
|
4283
|
-
}
|
|
4284
|
-
|
|
4285
3906
|
// src/server.ts
|
|
4286
3907
|
async function startServer(opts) {
|
|
4287
3908
|
const { config } = opts;
|
|
4288
3909
|
const store = await AccountStore.open({ dataDir: config.dataDir });
|
|
4289
3910
|
const registry = buildRegistry({ store, providers: config.providers });
|
|
4290
3911
|
const tools = resolveTools(config);
|
|
4291
|
-
|
|
4292
|
-
|
|
4293
|
-
|
|
4294
|
-
|
|
3912
|
+
let watcher;
|
|
3913
|
+
if (config.watch?.enabled) {
|
|
3914
|
+
watcher = new WatcherManager(store, registry, config.watch);
|
|
3915
|
+
watcher.start();
|
|
3916
|
+
const stop = () => watcher?.stop();
|
|
3917
|
+
process.on("SIGTERM", stop);
|
|
3918
|
+
process.on("SIGINT", stop);
|
|
3919
|
+
}
|
|
3920
|
+
const createServer = () => {
|
|
4295
3921
|
const s = new McpServer(
|
|
4296
3922
|
{ name: "hypermail-mcp", version: VERSION },
|
|
4297
3923
|
{ capabilities: { tools: {}, logging: {} } }
|
|
4298
3924
|
);
|
|
4299
|
-
registerTools(s, { store, registry, tools
|
|
3925
|
+
registerTools(s, { store, registry, tools });
|
|
4300
3926
|
return s;
|
|
4301
3927
|
};
|
|
4302
3928
|
if (config.http.enabled) {
|
|
4303
|
-
|
|
4304
|
-
if (config.agentsConfigPath) {
|
|
4305
|
-
agentStoreForFactory = await AgentStore.open({ dataDir: config.dataDir });
|
|
4306
|
-
liveReloadHandle = watchAgentsConfig(
|
|
4307
|
-
path4.resolve(config.agentsConfigPath),
|
|
4308
|
-
agentStoreForFactory,
|
|
4309
|
-
(_removedIds) => {
|
|
4310
|
-
},
|
|
4311
|
-
(err) => {
|
|
4312
|
-
console.error("[hypermail-mcp] agents.yaml reload error:", err.message);
|
|
4313
|
-
}
|
|
4314
|
-
);
|
|
4315
|
-
}
|
|
4316
|
-
const notifyTargets = /* @__PURE__ */ new Set();
|
|
4317
|
-
const accountFilter = agentStoreForFactory ? (() => {
|
|
4318
|
-
const all = /* @__PURE__ */ new Set();
|
|
4319
|
-
for (const agent of agentStoreForFactory.listAgents()) {
|
|
4320
|
-
for (const email of agent.accounts) {
|
|
4321
|
-
all.add(email.toLowerCase());
|
|
4322
|
-
}
|
|
4323
|
-
}
|
|
4324
|
-
return all.size > 0 ? [...all] : void 0;
|
|
4325
|
-
})() : void 0;
|
|
4326
|
-
if (watchEnabled) {
|
|
4327
|
-
const watcher = new WatcherManager({
|
|
4328
|
-
registry,
|
|
4329
|
-
store,
|
|
4330
|
-
pollIntervalSeconds: config.watch?.pollIntervalSeconds ?? 60,
|
|
4331
|
-
accountFilter,
|
|
4332
|
-
onNotification: (notification) => {
|
|
4333
|
-
for (const fn of notifyTargets) {
|
|
4334
|
-
fn(notification);
|
|
4335
|
-
}
|
|
4336
|
-
},
|
|
4337
|
-
buffer: notificationBuffer
|
|
4338
|
-
});
|
|
4339
|
-
watcher.start();
|
|
4340
|
-
}
|
|
4341
|
-
await startHttp(
|
|
4342
|
-
createServer,
|
|
4343
|
-
config.http.host,
|
|
4344
|
-
config.http.port,
|
|
4345
|
-
notifyTargets,
|
|
4346
|
-
agentStoreForFactory
|
|
4347
|
-
);
|
|
4348
|
-
if (liveReloadHandle) {
|
|
4349
|
-
process.on("SIGINT", () => liveReloadHandle.close());
|
|
4350
|
-
process.on("SIGTERM", () => liveReloadHandle.close());
|
|
4351
|
-
}
|
|
3929
|
+
await startHttp(createServer, config.http.host, config.http.port);
|
|
4352
3930
|
} else {
|
|
4353
3931
|
const server = createServer();
|
|
4354
3932
|
const transport = new StdioServerTransport();
|
|
4355
3933
|
await server.connect(transport);
|
|
4356
3934
|
}
|
|
4357
3935
|
}
|
|
4358
|
-
async function startHttp(createServer, host, port
|
|
3936
|
+
async function startHttp(createServer, host, port) {
|
|
4359
3937
|
const sessions = /* @__PURE__ */ new Map();
|
|
4360
3938
|
const http = createHttpServer(async (req, res) => {
|
|
4361
3939
|
try {
|
|
@@ -4367,57 +3945,18 @@ async function startHttp(createServer, host, port, notifyTargets, agentStore) {
|
|
|
4367
3945
|
const sessionId = req.headers["mcp-session-id"] ?? void 0;
|
|
4368
3946
|
let session = sessionId ? sessions.get(sessionId) : void 0;
|
|
4369
3947
|
if (!session) {
|
|
4370
|
-
|
|
4371
|
-
if (agentStore) {
|
|
4372
|
-
const apiKey = req.headers["x-api-key"]?.trim();
|
|
4373
|
-
if (!apiKey) {
|
|
4374
|
-
res.statusCode = 401;
|
|
4375
|
-
res.setHeader("Content-Type", "application/json");
|
|
4376
|
-
res.end(JSON.stringify({ error: "Missing x-api-key header" }));
|
|
4377
|
-
return;
|
|
4378
|
-
}
|
|
4379
|
-
const agent = agentStore.findAgentByApiKey(apiKey);
|
|
4380
|
-
if (!agent) {
|
|
4381
|
-
res.statusCode = 401;
|
|
4382
|
-
res.setHeader("Content-Type", "application/json");
|
|
4383
|
-
res.end(JSON.stringify({ error: "Invalid API key" }));
|
|
4384
|
-
return;
|
|
4385
|
-
}
|
|
4386
|
-
agentContext = {
|
|
4387
|
-
agentId: agent.id,
|
|
4388
|
-
accounts: agent.accounts,
|
|
4389
|
-
provisioning: agent.provisioning
|
|
4390
|
-
};
|
|
4391
|
-
}
|
|
4392
|
-
const server = createServer(agentContext);
|
|
3948
|
+
const server = createServer();
|
|
4393
3949
|
const transport = new StreamableHTTPServerTransport({
|
|
4394
3950
|
sessionIdGenerator: () => randomUUID6(),
|
|
4395
3951
|
onsessioninitialized: (sid) => {
|
|
4396
|
-
sessions.set(sid, { transport, server
|
|
4397
|
-
const agentAccounts = agentContext ? new Set(agentContext.accounts.map((a) => a.toLowerCase())) : null;
|
|
4398
|
-
const notifyFn = (n) => {
|
|
4399
|
-
if (agentAccounts && !agentAccounts.has(n.account.toLowerCase())) {
|
|
4400
|
-
return;
|
|
4401
|
-
}
|
|
4402
|
-
server.server.notification({
|
|
4403
|
-
method: "notifications/message",
|
|
4404
|
-
params: {
|
|
4405
|
-
level: n.type === "new_emails" ? "notice" : "warning",
|
|
4406
|
-
logger: "hypermail-watch",
|
|
4407
|
-
data: n
|
|
4408
|
-
}
|
|
4409
|
-
}).catch(() => {
|
|
4410
|
-
});
|
|
4411
|
-
};
|
|
4412
|
-
notifyTargets.add(notifyFn);
|
|
3952
|
+
sessions.set(sid, { transport, server });
|
|
4413
3953
|
transport.onclose = () => {
|
|
4414
3954
|
if (transport.sessionId) sessions.delete(transport.sessionId);
|
|
4415
|
-
notifyTargets.delete(notifyFn);
|
|
4416
3955
|
};
|
|
4417
3956
|
}
|
|
4418
3957
|
});
|
|
4419
3958
|
await server.connect(transport);
|
|
4420
|
-
session = { transport, server
|
|
3959
|
+
session = { transport, server };
|
|
4421
3960
|
}
|
|
4422
3961
|
let body = void 0;
|
|
4423
3962
|
if (req.method === "POST" || req.method === "DELETE") {
|
|
@@ -4465,9 +4004,6 @@ function parseArgs(argv) {
|
|
|
4465
4004
|
case "--config":
|
|
4466
4005
|
out.config = String(argv[++i] ?? "");
|
|
4467
4006
|
break;
|
|
4468
|
-
case "--agents-config":
|
|
4469
|
-
out.agentsConfig = String(argv[++i] ?? "");
|
|
4470
|
-
break;
|
|
4471
4007
|
case "-h":
|
|
4472
4008
|
case "--help":
|
|
4473
4009
|
out.help = true;
|
|
@@ -4527,8 +4063,7 @@ async function main() {
|
|
|
4527
4063
|
http: opts.http,
|
|
4528
4064
|
port: opts.port,
|
|
4529
4065
|
host: opts.host,
|
|
4530
|
-
dataDir: opts.dataDir
|
|
4531
|
-
agentsConfig: opts.agentsConfig
|
|
4066
|
+
dataDir: opts.dataDir
|
|
4532
4067
|
});
|
|
4533
4068
|
await startServer({ config });
|
|
4534
4069
|
}
|