hypermail-mcp 0.5.0 → 0.6.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 +182 -9
- package/dist/cli.js +781 -154
- package/dist/cli.js.map +1 -1
- package/package.json +4 -1
package/dist/cli.js
CHANGED
|
@@ -1,85 +1,36 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { randomBytes as randomBytes2 } from "crypto";
|
|
5
|
+
|
|
3
6
|
// src/server.ts
|
|
4
7
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
8
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
9
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
7
10
|
import { randomUUID as randomUUID6 } from "crypto";
|
|
8
11
|
import { createServer as createHttpServer } from "http";
|
|
12
|
+
import path4 from "path";
|
|
9
13
|
|
|
10
14
|
// src/store/account-store.ts
|
|
15
|
+
import { promises as fs2 } from "fs";
|
|
16
|
+
import path2 from "path";
|
|
17
|
+
|
|
18
|
+
// src/store/crypto.ts
|
|
11
19
|
import {
|
|
12
20
|
createCipheriv,
|
|
13
21
|
createDecipheriv,
|
|
14
22
|
randomBytes,
|
|
15
|
-
createHash
|
|
23
|
+
createHash,
|
|
24
|
+
scryptSync,
|
|
25
|
+
timingSafeEqual
|
|
16
26
|
} from "crypto";
|
|
17
27
|
import { promises as fs } from "fs";
|
|
18
28
|
import { homedir } from "os";
|
|
19
29
|
import path from "path";
|
|
20
|
-
var FILE_NAME = "accounts.json.enc";
|
|
21
30
|
var ALGO = "aes-256-gcm";
|
|
22
31
|
var KEY_LEN = 32;
|
|
23
|
-
var AccountStore = class _AccountStore {
|
|
24
|
-
constructor(filePath, key, data) {
|
|
25
|
-
this.filePath = filePath;
|
|
26
|
-
this.key = key;
|
|
27
|
-
this.data = data;
|
|
28
|
-
}
|
|
29
|
-
filePath;
|
|
30
|
-
key;
|
|
31
|
-
data;
|
|
32
|
-
static async open(opts = {}) {
|
|
33
|
-
const dataDir = resolveDataDir(opts.dataDir);
|
|
34
|
-
await fs.mkdir(dataDir, { recursive: true, mode: 448 });
|
|
35
|
-
const filePath = path.join(dataDir, FILE_NAME);
|
|
36
|
-
const key = opts.key ?? await resolveKey(dataDir);
|
|
37
|
-
let data;
|
|
38
|
-
try {
|
|
39
|
-
const buf = await fs.readFile(filePath);
|
|
40
|
-
data = decrypt(buf, key);
|
|
41
|
-
} catch (err) {
|
|
42
|
-
if (err.code === "ENOENT") {
|
|
43
|
-
data = { version: 1, accounts: [] };
|
|
44
|
-
} else {
|
|
45
|
-
throw err;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
return new _AccountStore(filePath, key, data);
|
|
49
|
-
}
|
|
50
|
-
listAccounts() {
|
|
51
|
-
return this.data.accounts.map((a) => ({ ...a }));
|
|
52
|
-
}
|
|
53
|
-
getAccount(email) {
|
|
54
|
-
const norm = email.trim().toLowerCase();
|
|
55
|
-
const rec = this.data.accounts.find((a) => a.email.toLowerCase() === norm);
|
|
56
|
-
return rec ? { ...rec } : void 0;
|
|
57
|
-
}
|
|
58
|
-
async upsertAccount(rec) {
|
|
59
|
-
const norm = rec.email.trim().toLowerCase();
|
|
60
|
-
const next = { ...rec, email: norm };
|
|
61
|
-
const idx = this.data.accounts.findIndex((a) => a.email.toLowerCase() === norm);
|
|
62
|
-
if (idx >= 0) this.data.accounts[idx] = next;
|
|
63
|
-
else this.data.accounts.push(next);
|
|
64
|
-
await this.flush();
|
|
65
|
-
return { ...next };
|
|
66
|
-
}
|
|
67
|
-
async removeAccount(email) {
|
|
68
|
-
const norm = email.trim().toLowerCase();
|
|
69
|
-
const before = this.data.accounts.length;
|
|
70
|
-
this.data.accounts = this.data.accounts.filter((a) => a.email.toLowerCase() !== norm);
|
|
71
|
-
if (this.data.accounts.length === before) return false;
|
|
72
|
-
await this.flush();
|
|
73
|
-
return true;
|
|
74
|
-
}
|
|
75
|
-
async flush() {
|
|
76
|
-
const buf = encrypt(this.data, this.key);
|
|
77
|
-
const tmp = `${this.filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
78
|
-
await fs.writeFile(tmp, buf, { mode: 384 });
|
|
79
|
-
await fs.rename(tmp, this.filePath);
|
|
80
|
-
}
|
|
81
|
-
};
|
|
82
32
|
function encrypt(data, key) {
|
|
33
|
+
if (key.length !== KEY_LEN) throw new Error(`key must be ${KEY_LEN} bytes`);
|
|
83
34
|
const iv = randomBytes(12);
|
|
84
35
|
const cipher = createCipheriv(ALGO, key, iv);
|
|
85
36
|
const plaintext = Buffer.from(JSON.stringify(data), "utf8");
|
|
@@ -88,20 +39,17 @@ function encrypt(data, key) {
|
|
|
88
39
|
return Buffer.concat([Buffer.from([1]), iv, tag, ct]);
|
|
89
40
|
}
|
|
90
41
|
function decrypt(buf, key) {
|
|
91
|
-
if (
|
|
42
|
+
if (key.length !== KEY_LEN) throw new Error(`key must be ${KEY_LEN} bytes`);
|
|
43
|
+
if (buf.length < 1 + 12 + 16 + 1) throw new Error("encrypted data truncated");
|
|
92
44
|
const v = buf[0];
|
|
93
|
-
if (v !== 1) throw new Error(`unsupported
|
|
45
|
+
if (v !== 1) throw new Error(`unsupported data version: ${v}`);
|
|
94
46
|
const iv = buf.subarray(1, 13);
|
|
95
47
|
const tag = buf.subarray(13, 29);
|
|
96
48
|
const ct = buf.subarray(29);
|
|
97
49
|
const decipher = createDecipheriv(ALGO, key, iv);
|
|
98
50
|
decipher.setAuthTag(tag);
|
|
99
51
|
const pt = Buffer.concat([decipher.update(ct), decipher.final()]);
|
|
100
|
-
|
|
101
|
-
if (parsed.version !== 1 || !Array.isArray(parsed.accounts)) {
|
|
102
|
-
throw new Error("accounts file is malformed");
|
|
103
|
-
}
|
|
104
|
-
return parsed;
|
|
52
|
+
return JSON.parse(pt.toString("utf8"));
|
|
105
53
|
}
|
|
106
54
|
function resolveDataDir(explicit) {
|
|
107
55
|
if (explicit && explicit.length > 0) return path.resolve(explicit);
|
|
@@ -138,6 +86,28 @@ async function resolveKey(dataDir) {
|
|
|
138
86
|
await tryKeytarSet(gen);
|
|
139
87
|
return gen;
|
|
140
88
|
}
|
|
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
|
+
async function writeAtomic(filePath, data) {
|
|
107
|
+
const tmp = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
108
|
+
await fs.writeFile(tmp, data, { mode: 384 });
|
|
109
|
+
await fs.rename(tmp, filePath);
|
|
110
|
+
}
|
|
141
111
|
async function tryKeytarGet() {
|
|
142
112
|
try {
|
|
143
113
|
const mod = await import("keytar");
|
|
@@ -158,6 +128,188 @@ async function tryKeytarSet(key) {
|
|
|
158
128
|
}
|
|
159
129
|
}
|
|
160
130
|
|
|
131
|
+
// src/store/account-store.ts
|
|
132
|
+
var FILE_NAME = "accounts.json.enc";
|
|
133
|
+
var AccountStore = class _AccountStore {
|
|
134
|
+
constructor(filePath, key, data) {
|
|
135
|
+
this.filePath = filePath;
|
|
136
|
+
this.key = key;
|
|
137
|
+
this.data = data;
|
|
138
|
+
}
|
|
139
|
+
filePath;
|
|
140
|
+
key;
|
|
141
|
+
data;
|
|
142
|
+
static async open(opts = {}) {
|
|
143
|
+
const dataDir = resolveDataDir(opts.dataDir);
|
|
144
|
+
await fs2.mkdir(dataDir, { recursive: true, mode: 448 });
|
|
145
|
+
const filePath = path2.join(dataDir, FILE_NAME);
|
|
146
|
+
const key = opts.key ?? await resolveKey(dataDir);
|
|
147
|
+
let data;
|
|
148
|
+
try {
|
|
149
|
+
const buf = await fs2.readFile(filePath);
|
|
150
|
+
data = decrypt(buf, key);
|
|
151
|
+
} catch (err) {
|
|
152
|
+
if (err.code === "ENOENT") {
|
|
153
|
+
data = { version: 1, accounts: [] };
|
|
154
|
+
} else {
|
|
155
|
+
throw err;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return new _AccountStore(filePath, key, data);
|
|
159
|
+
}
|
|
160
|
+
listAccounts() {
|
|
161
|
+
return this.data.accounts.map((a) => ({ ...a }));
|
|
162
|
+
}
|
|
163
|
+
getAccount(email) {
|
|
164
|
+
const norm = email.trim().toLowerCase();
|
|
165
|
+
const rec = this.data.accounts.find((a) => a.email.toLowerCase() === norm);
|
|
166
|
+
return rec ? { ...rec } : void 0;
|
|
167
|
+
}
|
|
168
|
+
async upsertAccount(rec) {
|
|
169
|
+
const norm = rec.email.trim().toLowerCase();
|
|
170
|
+
const next = { ...rec, email: norm };
|
|
171
|
+
const idx = this.data.accounts.findIndex((a) => a.email.toLowerCase() === norm);
|
|
172
|
+
if (idx >= 0) this.data.accounts[idx] = next;
|
|
173
|
+
else this.data.accounts.push(next);
|
|
174
|
+
await this.flush();
|
|
175
|
+
return { ...next };
|
|
176
|
+
}
|
|
177
|
+
async removeAccount(email) {
|
|
178
|
+
const norm = email.trim().toLowerCase();
|
|
179
|
+
const before = this.data.accounts.length;
|
|
180
|
+
this.data.accounts = this.data.accounts.filter((a) => a.email.toLowerCase() !== norm);
|
|
181
|
+
if (this.data.accounts.length === before) return false;
|
|
182
|
+
await this.flush();
|
|
183
|
+
return true;
|
|
184
|
+
}
|
|
185
|
+
async flush() {
|
|
186
|
+
const buf = encrypt(this.data, this.key);
|
|
187
|
+
await writeAtomic(this.filePath, buf);
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
|
|
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
|
+
|
|
161
313
|
// src/providers/outlook/index.ts
|
|
162
314
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
163
315
|
import { writeFileSync } from "fs";
|
|
@@ -338,34 +490,52 @@ var OutlookClientFactory = class {
|
|
|
338
490
|
clientId;
|
|
339
491
|
tenantId;
|
|
340
492
|
cache = /* @__PURE__ */ new Map();
|
|
493
|
+
/** Serialize token refreshes per email to prevent concurrent-refresh races. */
|
|
494
|
+
refreshLocks = /* @__PURE__ */ new Map();
|
|
341
495
|
get(account) {
|
|
342
496
|
const key = account.email.toLowerCase();
|
|
343
497
|
const existing = this.cache.get(key);
|
|
344
498
|
if (existing) return existing;
|
|
345
499
|
const store = this.store;
|
|
500
|
+
const refreshLocks = this.refreshLocks;
|
|
346
501
|
const provider = {
|
|
347
502
|
getAccessToken: async () => {
|
|
348
|
-
const
|
|
349
|
-
if (
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
503
|
+
const existing2 = refreshLocks.get(key);
|
|
504
|
+
if (existing2) {
|
|
505
|
+
try {
|
|
506
|
+
return await existing2;
|
|
507
|
+
} catch {
|
|
508
|
+
}
|
|
353
509
|
}
|
|
354
|
-
const
|
|
355
|
-
|
|
356
|
-
tokens
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
510
|
+
const promise = (async () => {
|
|
511
|
+
const fresh = store.getAccount(account.email) ?? account;
|
|
512
|
+
if (!isSerializedTokens(fresh.tokens)) {
|
|
513
|
+
throw new Error(
|
|
514
|
+
"Outlook account tokens are missing or corrupted \u2014 re-run add_account"
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
const tokens = fresh.tokens;
|
|
518
|
+
const { accessToken, tokens: nextTokens } = await acquireAccessToken(
|
|
519
|
+
tokens,
|
|
520
|
+
void 0,
|
|
521
|
+
this.clientId,
|
|
522
|
+
this.tenantId
|
|
523
|
+
);
|
|
524
|
+
if (nextTokens.msalCache !== tokens.msalCache) {
|
|
525
|
+
store.upsertAccount({
|
|
526
|
+
...fresh,
|
|
527
|
+
tokens: nextTokens
|
|
528
|
+
}).catch(() => {
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
return accessToken;
|
|
532
|
+
})();
|
|
533
|
+
refreshLocks.set(key, promise);
|
|
534
|
+
try {
|
|
535
|
+
return await promise;
|
|
536
|
+
} finally {
|
|
537
|
+
refreshLocks.delete(key);
|
|
367
538
|
}
|
|
368
|
-
return accessToken;
|
|
369
539
|
}
|
|
370
540
|
};
|
|
371
541
|
const client = Client.initWithMiddleware({ authProvider: provider });
|
|
@@ -1395,8 +1565,8 @@ async function markRead(clients, account, id, isRead) {
|
|
|
1395
1565
|
async function createFolder(clients, account, input) {
|
|
1396
1566
|
const client = clients.get(account);
|
|
1397
1567
|
const imap = await client.getImap();
|
|
1398
|
-
const
|
|
1399
|
-
const result = await imap.mailboxCreate(
|
|
1568
|
+
const path5 = input.parentFolderId ? `${input.parentFolderId}/${input.displayName}` : input.displayName;
|
|
1569
|
+
const result = await imap.mailboxCreate(path5);
|
|
1400
1570
|
return {
|
|
1401
1571
|
id: result.path,
|
|
1402
1572
|
displayName: result.path,
|
|
@@ -1713,6 +1883,8 @@ var GmailClientFactory = class {
|
|
|
1713
1883
|
clientId;
|
|
1714
1884
|
clientSecret;
|
|
1715
1885
|
cache = /* @__PURE__ */ new Map();
|
|
1886
|
+
/** Serialize token-persist per email to prevent concurrent upsert races. */
|
|
1887
|
+
persistLocks = /* @__PURE__ */ new Map();
|
|
1716
1888
|
get(account) {
|
|
1717
1889
|
const key = account.email.toLowerCase();
|
|
1718
1890
|
const existing = this.cache.get(key);
|
|
@@ -1731,21 +1903,29 @@ var GmailClientFactory = class {
|
|
|
1731
1903
|
clientSecret: resolvedSecret
|
|
1732
1904
|
});
|
|
1733
1905
|
const store = this.store;
|
|
1906
|
+
const persistLocks = this.persistLocks;
|
|
1734
1907
|
auth.on("tokens", (updated) => {
|
|
1735
1908
|
if (!updated.refresh_token && !updated.access_token) return;
|
|
1736
|
-
const
|
|
1737
|
-
const
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1909
|
+
const existing2 = persistLocks.get(key);
|
|
1910
|
+
const chain = (existing2 ?? Promise.resolve()).then(async () => {
|
|
1911
|
+
const fresh = store.getAccount(account.email) ?? account;
|
|
1912
|
+
const currentTokens = isSerializedGmailTokens(fresh.tokens) ? fresh.tokens : tokens;
|
|
1913
|
+
const nextTokens = {
|
|
1914
|
+
...currentTokens,
|
|
1915
|
+
accessToken: updated.access_token ?? currentTokens.accessToken,
|
|
1916
|
+
refreshToken: updated.refresh_token ?? currentTokens.refreshToken,
|
|
1917
|
+
expiryDate: updated.expiry_date ?? currentTokens.expiryDate,
|
|
1918
|
+
scopes: updated.scope ? updated.scope.split(" ") : currentTokens.scopes
|
|
1919
|
+
};
|
|
1920
|
+
await store.upsertAccount({
|
|
1921
|
+
...fresh,
|
|
1922
|
+
tokens: nextTokens
|
|
1923
|
+
}).catch(() => {
|
|
1924
|
+
});
|
|
1925
|
+
});
|
|
1926
|
+
persistLocks.set(key, chain);
|
|
1927
|
+
chain.finally(() => {
|
|
1928
|
+
if (persistLocks.get(key) === chain) persistLocks.delete(key);
|
|
1749
1929
|
});
|
|
1750
1930
|
});
|
|
1751
1931
|
const gmail = google.gmail({ version: "v1", auth });
|
|
@@ -2574,9 +2754,27 @@ function shouldRegister(name, tools) {
|
|
|
2574
2754
|
}
|
|
2575
2755
|
|
|
2576
2756
|
// src/tools/accounts.ts
|
|
2757
|
+
import { promises as fs4 } from "fs";
|
|
2577
2758
|
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
|
|
2578
2776
|
function registerAccountTools(server, ctx) {
|
|
2579
|
-
const { store, registry, tools } = ctx;
|
|
2777
|
+
const { store, registry, tools, agentContext, agentStore } = ctx;
|
|
2580
2778
|
const listAccountsOutputSchema = z2.object({
|
|
2581
2779
|
accounts: z2.array(accountSummaryOutputSchema)
|
|
2582
2780
|
});
|
|
@@ -2630,6 +2828,8 @@ function registerAccountTools(server, ctx) {
|
|
|
2630
2828
|
outputSchema: addAccountOutputSchema
|
|
2631
2829
|
},
|
|
2632
2830
|
async (args) => {
|
|
2831
|
+
const permErr = checkProvisioning(agentContext ?? null);
|
|
2832
|
+
if (permErr) return fail(permErr);
|
|
2633
2833
|
const provider = registry.get(args.provider);
|
|
2634
2834
|
try {
|
|
2635
2835
|
const res = await provider.addAccount({
|
|
@@ -2660,6 +2860,8 @@ function registerAccountTools(server, ctx) {
|
|
|
2660
2860
|
outputSchema: completeAddAccountOutputSchema
|
|
2661
2861
|
},
|
|
2662
2862
|
async (args) => {
|
|
2863
|
+
const permErr = checkProvisioning(agentContext ?? null);
|
|
2864
|
+
if (permErr) return fail(permErr);
|
|
2663
2865
|
const provider = registry.get(args.provider);
|
|
2664
2866
|
if (!provider.completeAddAccount) {
|
|
2665
2867
|
return fail(
|
|
@@ -2668,6 +2870,10 @@ function registerAccountTools(server, ctx) {
|
|
|
2668
2870
|
}
|
|
2669
2871
|
try {
|
|
2670
2872
|
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
|
+
}
|
|
2671
2877
|
return ok(res, res);
|
|
2672
2878
|
} catch (err) {
|
|
2673
2879
|
return fail(errMsg(err));
|
|
@@ -2689,6 +2895,8 @@ function registerAccountTools(server, ctx) {
|
|
|
2689
2895
|
},
|
|
2690
2896
|
async (args) => {
|
|
2691
2897
|
try {
|
|
2898
|
+
const accessErr = checkAccountAccess(agentContext ?? null, args.account);
|
|
2899
|
+
if (accessErr) return fail(accessErr);
|
|
2692
2900
|
const acct = store.getAccount(args.account);
|
|
2693
2901
|
if (!acct)
|
|
2694
2902
|
return fail(`no account registered for "${args.account}"`);
|
|
@@ -2707,11 +2915,14 @@ function registerAccountTools(server, ctx) {
|
|
|
2707
2915
|
server.registerTool(
|
|
2708
2916
|
"set_account_settings",
|
|
2709
2917
|
{
|
|
2710
|
-
description: "Set signature (HTML snippet) and/or style preferences for an account. Disabled in --read-only mode.",
|
|
2918
|
+
description: "Set signature (HTML snippet) and/or style preferences for an account. Use `signaturePath` to load a signature from a file (useful for signatures with base64 images). `signature` and `signaturePath` are mutually exclusive. Disabled in --read-only mode.",
|
|
2711
2919
|
inputSchema: z2.object({
|
|
2712
2920
|
account: z2.string().email(),
|
|
2713
2921
|
signature: z2.string().optional().describe(
|
|
2714
|
-
"HTML snippet \u2014 may contain formatting, images, links. Pass
|
|
2922
|
+
"HTML snippet \u2014 may contain formatting, images, links. Pass an empty string to clear. Mutually exclusive with `signaturePath`."
|
|
2923
|
+
),
|
|
2924
|
+
signaturePath: z2.string().optional().describe(
|
|
2925
|
+
"Path to a file containing the signature HTML. The file content is read and stored as the signature. Useful when the signature contains large base64 images. Mutually exclusive with `signature`."
|
|
2715
2926
|
),
|
|
2716
2927
|
style: z2.object({
|
|
2717
2928
|
fontFamily: z2.string().optional(),
|
|
@@ -2720,17 +2931,30 @@ function registerAccountTools(server, ctx) {
|
|
|
2720
2931
|
}).optional().describe(
|
|
2721
2932
|
"Font preferences applied to outgoing HTML emails. Pass null to clear."
|
|
2722
2933
|
)
|
|
2723
|
-
})
|
|
2934
|
+
}).refine(
|
|
2935
|
+
(data) => !(data.signature !== void 0 && data.signaturePath),
|
|
2936
|
+
{
|
|
2937
|
+
message: "signature and signaturePath are mutually exclusive \u2014 use one or the other"
|
|
2938
|
+
}
|
|
2939
|
+
),
|
|
2724
2940
|
outputSchema: accountSettingsOutputSchema
|
|
2725
2941
|
},
|
|
2726
2942
|
async (args) => {
|
|
2727
2943
|
try {
|
|
2944
|
+
const accessErr = checkAccountAccess(agentContext ?? null, args.account);
|
|
2945
|
+
if (accessErr) return fail(accessErr);
|
|
2728
2946
|
const acct = store.getAccount(args.account);
|
|
2729
2947
|
if (!acct)
|
|
2730
2948
|
return fail(`no account registered for "${args.account}"`);
|
|
2949
|
+
let resolvedSignature = acct.signature;
|
|
2950
|
+
if (args.signaturePath) {
|
|
2951
|
+
resolvedSignature = await fs4.readFile(args.signaturePath, "utf-8");
|
|
2952
|
+
} else if (args.signature !== void 0) {
|
|
2953
|
+
resolvedSignature = args.signature || void 0;
|
|
2954
|
+
}
|
|
2731
2955
|
const updated = await store.upsertAccount({
|
|
2732
2956
|
...acct,
|
|
2733
|
-
signature:
|
|
2957
|
+
signature: resolvedSignature,
|
|
2734
2958
|
style: args.style ?? acct.style
|
|
2735
2959
|
});
|
|
2736
2960
|
const data = {
|
|
@@ -2757,6 +2981,8 @@ function registerAccountTools(server, ctx) {
|
|
|
2757
2981
|
outputSchema: removeAccountOutputSchema
|
|
2758
2982
|
},
|
|
2759
2983
|
async (args) => {
|
|
2984
|
+
const permErr = checkProvisioning(agentContext ?? null);
|
|
2985
|
+
if (permErr) return fail(permErr);
|
|
2760
2986
|
const removed = await store.removeAccount(args.email);
|
|
2761
2987
|
const data = { removed, email: args.email };
|
|
2762
2988
|
return ok(data, data);
|
|
@@ -2796,7 +3022,7 @@ function selectBody(msg, format) {
|
|
|
2796
3022
|
|
|
2797
3023
|
// src/tools/browse.ts
|
|
2798
3024
|
function registerBrowseTools(server, ctx) {
|
|
2799
|
-
const { registry, tools } = ctx;
|
|
3025
|
+
const { registry, tools, agentContext } = ctx;
|
|
2800
3026
|
const emailListOutputSchema = z3.object({
|
|
2801
3027
|
account: z3.string(),
|
|
2802
3028
|
count: z3.number(),
|
|
@@ -2825,6 +3051,8 @@ function registerBrowseTools(server, ctx) {
|
|
|
2825
3051
|
},
|
|
2826
3052
|
async (args) => {
|
|
2827
3053
|
try {
|
|
3054
|
+
const accessErr = checkAccountAccess(agentContext ?? null, args.account);
|
|
3055
|
+
if (accessErr) return fail(accessErr);
|
|
2828
3056
|
const { provider, account } = registry.resolveByEmail(args.account);
|
|
2829
3057
|
const { items, hasMore } = await provider.listEmails(account, {
|
|
2830
3058
|
folder: args.folder,
|
|
@@ -2860,6 +3088,8 @@ function registerBrowseTools(server, ctx) {
|
|
|
2860
3088
|
},
|
|
2861
3089
|
async (args) => {
|
|
2862
3090
|
try {
|
|
3091
|
+
const accessErr = checkAccountAccess(agentContext ?? null, args.account);
|
|
3092
|
+
if (accessErr) return fail(accessErr);
|
|
2863
3093
|
const { provider, account } = registry.resolveByEmail(args.account);
|
|
2864
3094
|
const items = await provider.searchEmails(account, args.query, {
|
|
2865
3095
|
limit: args.limit
|
|
@@ -2908,6 +3138,8 @@ function registerBrowseTools(server, ctx) {
|
|
|
2908
3138
|
},
|
|
2909
3139
|
async (args) => {
|
|
2910
3140
|
try {
|
|
3141
|
+
const accessErr = checkAccountAccess(agentContext ?? null, args.account);
|
|
3142
|
+
if (accessErr) return fail(accessErr);
|
|
2911
3143
|
const { provider, account } = registry.resolveByEmail(args.account);
|
|
2912
3144
|
const msg = await provider.readEmail(account, args.id);
|
|
2913
3145
|
const format = args.format ?? "markdown";
|
|
@@ -2954,6 +3186,8 @@ function registerBrowseTools(server, ctx) {
|
|
|
2954
3186
|
},
|
|
2955
3187
|
async (args) => {
|
|
2956
3188
|
try {
|
|
3189
|
+
const accessErr = checkAccountAccess(agentContext ?? null, args.account);
|
|
3190
|
+
if (accessErr) return fail(accessErr);
|
|
2957
3191
|
const { provider, account } = registry.resolveByEmail(args.account);
|
|
2958
3192
|
const res = await provider.readAttachment(
|
|
2959
3193
|
account,
|
|
@@ -2972,7 +3206,7 @@ function registerBrowseTools(server, ctx) {
|
|
|
2972
3206
|
// src/tools/folders.ts
|
|
2973
3207
|
import { z as z4 } from "zod";
|
|
2974
3208
|
function registerFolderTools(server, ctx) {
|
|
2975
|
-
const { registry, tools } = ctx;
|
|
3209
|
+
const { registry, tools, agentContext } = ctx;
|
|
2976
3210
|
const listFoldersOutputSchema = z4.object({
|
|
2977
3211
|
account: z4.string(),
|
|
2978
3212
|
count: z4.number(),
|
|
@@ -2993,6 +3227,8 @@ function registerFolderTools(server, ctx) {
|
|
|
2993
3227
|
},
|
|
2994
3228
|
async (args) => {
|
|
2995
3229
|
try {
|
|
3230
|
+
const accessErr = checkAccountAccess(agentContext ?? null, args.account);
|
|
3231
|
+
if (accessErr) return fail(accessErr);
|
|
2996
3232
|
const { provider, account } = registry.resolveByEmail(args.account);
|
|
2997
3233
|
const items = await provider.listFolders(account, {
|
|
2998
3234
|
parentFolderId: args.parentFolderId
|
|
@@ -3029,6 +3265,8 @@ function registerFolderTools(server, ctx) {
|
|
|
3029
3265
|
},
|
|
3030
3266
|
async (args) => {
|
|
3031
3267
|
try {
|
|
3268
|
+
const accessErr = checkAccountAccess(agentContext ?? null, args.account);
|
|
3269
|
+
if (accessErr) return fail(accessErr);
|
|
3032
3270
|
const { provider, account } = registry.resolveByEmail(args.account);
|
|
3033
3271
|
const folder = await provider.createFolder(account, {
|
|
3034
3272
|
displayName: args.displayName,
|
|
@@ -3059,6 +3297,8 @@ function registerFolderTools(server, ctx) {
|
|
|
3059
3297
|
},
|
|
3060
3298
|
async (args) => {
|
|
3061
3299
|
try {
|
|
3300
|
+
const accessErr = checkAccountAccess(agentContext ?? null, args.account);
|
|
3301
|
+
if (accessErr) return fail(accessErr);
|
|
3062
3302
|
const { provider, account } = registry.resolveByEmail(args.account);
|
|
3063
3303
|
await provider.deleteFolder(account, args.folderId);
|
|
3064
3304
|
const data = { deleted: true, id: args.folderId };
|
|
@@ -3087,6 +3327,8 @@ function registerFolderTools(server, ctx) {
|
|
|
3087
3327
|
},
|
|
3088
3328
|
async (args) => {
|
|
3089
3329
|
try {
|
|
3330
|
+
const accessErr = checkAccountAccess(agentContext ?? null, args.account);
|
|
3331
|
+
if (accessErr) return fail(accessErr);
|
|
3090
3332
|
const { provider, account } = registry.resolveByEmail(args.account);
|
|
3091
3333
|
const folder = await provider.renameFolder(
|
|
3092
3334
|
account,
|
|
@@ -3106,7 +3348,7 @@ function registerFolderTools(server, ctx) {
|
|
|
3106
3348
|
// src/tools/organize.ts
|
|
3107
3349
|
import { z as z5 } from "zod";
|
|
3108
3350
|
function registerOrganizeTools(server, ctx) {
|
|
3109
|
-
const { registry, tools } = ctx;
|
|
3351
|
+
const { registry, tools, agentContext } = ctx;
|
|
3110
3352
|
async function moveToWellKnown(args, destination, resultKey) {
|
|
3111
3353
|
const { provider, account } = registry.resolveByEmail(args.account);
|
|
3112
3354
|
await provider.moveEmail(account, args.id, destination);
|
|
@@ -3138,6 +3380,8 @@ function registerOrganizeTools(server, ctx) {
|
|
|
3138
3380
|
},
|
|
3139
3381
|
async (args) => {
|
|
3140
3382
|
try {
|
|
3383
|
+
const accessErr = checkAccountAccess(agentContext ?? null, args.account);
|
|
3384
|
+
if (accessErr) return fail(accessErr);
|
|
3141
3385
|
return await moveToWellKnown(args, "archive", "archived");
|
|
3142
3386
|
} catch (err) {
|
|
3143
3387
|
return fail(errMsg(err));
|
|
@@ -3159,6 +3403,8 @@ function registerOrganizeTools(server, ctx) {
|
|
|
3159
3403
|
},
|
|
3160
3404
|
async (args) => {
|
|
3161
3405
|
try {
|
|
3406
|
+
const accessErr = checkAccountAccess(agentContext ?? null, args.account);
|
|
3407
|
+
if (accessErr) return fail(accessErr);
|
|
3162
3408
|
return await moveToWellKnown(args, "deleteditems", "trashed");
|
|
3163
3409
|
} catch (err) {
|
|
3164
3410
|
return fail(errMsg(err));
|
|
@@ -3187,6 +3433,8 @@ function registerOrganizeTools(server, ctx) {
|
|
|
3187
3433
|
},
|
|
3188
3434
|
async (args) => {
|
|
3189
3435
|
try {
|
|
3436
|
+
const accessErr = checkAccountAccess(agentContext ?? null, args.account);
|
|
3437
|
+
if (accessErr) return fail(accessErr);
|
|
3190
3438
|
const { provider, account } = registry.resolveByEmail(args.account);
|
|
3191
3439
|
await provider.moveEmail(account, args.id, args.destination);
|
|
3192
3440
|
const data = {
|
|
@@ -3220,6 +3468,8 @@ function registerOrganizeTools(server, ctx) {
|
|
|
3220
3468
|
},
|
|
3221
3469
|
async (args) => {
|
|
3222
3470
|
try {
|
|
3471
|
+
const accessErr = checkAccountAccess(agentContext ?? null, args.account);
|
|
3472
|
+
if (accessErr) return fail(accessErr);
|
|
3223
3473
|
return await markReadState(args, true);
|
|
3224
3474
|
} catch (err) {
|
|
3225
3475
|
return fail(errMsg(err));
|
|
@@ -3237,6 +3487,8 @@ function registerOrganizeTools(server, ctx) {
|
|
|
3237
3487
|
},
|
|
3238
3488
|
async (args) => {
|
|
3239
3489
|
try {
|
|
3490
|
+
const accessErr = checkAccountAccess(agentContext ?? null, args.account);
|
|
3491
|
+
if (accessErr) return fail(accessErr);
|
|
3240
3492
|
return await markReadState(args, false);
|
|
3241
3493
|
} catch (err) {
|
|
3242
3494
|
return fail(errMsg(err));
|
|
@@ -3249,7 +3501,7 @@ function registerOrganizeTools(server, ctx) {
|
|
|
3249
3501
|
// src/tools/compose.ts
|
|
3250
3502
|
import { z as z6 } from "zod";
|
|
3251
3503
|
function registerComposeTools(server, ctx) {
|
|
3252
|
-
const { store, registry, tools } = ctx;
|
|
3504
|
+
const { store, registry, tools, agentContext } = ctx;
|
|
3253
3505
|
const sendEmailSchema = z6.object({
|
|
3254
3506
|
account: z6.string().email(),
|
|
3255
3507
|
to: z6.array(emailAddrSchema).min(1),
|
|
@@ -3275,6 +3527,8 @@ function registerComposeTools(server, ctx) {
|
|
|
3275
3527
|
});
|
|
3276
3528
|
async function handleSendOrDraft(args, action, resultKey, toolName) {
|
|
3277
3529
|
try {
|
|
3530
|
+
const accessErr = checkAccountAccess(agentContext ?? null, args.account);
|
|
3531
|
+
if (accessErr) return fail(accessErr);
|
|
3278
3532
|
const { provider, account } = registry.resolveByEmail(args.account);
|
|
3279
3533
|
if (args.include_signature && !account.signature) {
|
|
3280
3534
|
return fail(
|
|
@@ -3386,6 +3640,8 @@ function registerComposeTools(server, ctx) {
|
|
|
3386
3640
|
async (args) => {
|
|
3387
3641
|
const a = args;
|
|
3388
3642
|
try {
|
|
3643
|
+
const accessErr = checkAccountAccess(agentContext ?? null, a.account);
|
|
3644
|
+
if (accessErr) return fail(accessErr);
|
|
3389
3645
|
const { provider, account } = registry.resolveByEmail(a.account);
|
|
3390
3646
|
if (a.include_signature && !account.signature) {
|
|
3391
3647
|
return fail(
|
|
@@ -3443,6 +3699,8 @@ function registerComposeTools(server, ctx) {
|
|
|
3443
3699
|
},
|
|
3444
3700
|
async (args) => {
|
|
3445
3701
|
try {
|
|
3702
|
+
const accessErr = checkAccountAccess(agentContext ?? null, args.account);
|
|
3703
|
+
if (accessErr) return fail(accessErr);
|
|
3446
3704
|
const { provider, account } = registry.resolveByEmail(args.account);
|
|
3447
3705
|
const res = await provider.sendDraft(account, args.id);
|
|
3448
3706
|
const data = { sent: true, id: res.id };
|
|
@@ -3478,6 +3736,8 @@ function registerComposeTools(server, ctx) {
|
|
|
3478
3736
|
},
|
|
3479
3737
|
async (args) => {
|
|
3480
3738
|
try {
|
|
3739
|
+
const accessErr = checkAccountAccess(agentContext ?? null, args.account);
|
|
3740
|
+
if (accessErr) return fail(accessErr);
|
|
3481
3741
|
const { provider, account } = registry.resolveByEmail(args.account);
|
|
3482
3742
|
const res = await provider.addAttachmentToDraft(
|
|
3483
3743
|
account,
|
|
@@ -3500,14 +3760,63 @@ function registerComposeTools(server, ctx) {
|
|
|
3500
3760
|
}
|
|
3501
3761
|
}
|
|
3502
3762
|
|
|
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
|
+
|
|
3503
3805
|
// src/tools/index.ts
|
|
3504
3806
|
function registerTools(server, opts) {
|
|
3505
|
-
const { store, registry, tools } = opts;
|
|
3506
|
-
registerAccountTools(server, { store, registry, tools });
|
|
3507
|
-
registerBrowseTools(server, { registry, tools });
|
|
3508
|
-
registerFolderTools(server, { registry, tools });
|
|
3509
|
-
registerOrganizeTools(server, { registry, tools });
|
|
3510
|
-
registerComposeTools(server, { store, registry, tools });
|
|
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
|
+
}
|
|
3511
3820
|
}
|
|
3512
3821
|
|
|
3513
3822
|
// src/version.ts
|
|
@@ -3515,33 +3824,38 @@ var VERSION = "0.4.1";
|
|
|
3515
3824
|
|
|
3516
3825
|
// src/config.ts
|
|
3517
3826
|
import { readFileSync } from "fs";
|
|
3518
|
-
import { z as
|
|
3519
|
-
var httpConfigSchema =
|
|
3520
|
-
enabled:
|
|
3521
|
-
port:
|
|
3522
|
-
host:
|
|
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")
|
|
3523
3832
|
});
|
|
3524
|
-
var toolsConfigSchema =
|
|
3525
|
-
disabled:
|
|
3526
|
-
enabled:
|
|
3833
|
+
var toolsConfigSchema = z8.object({
|
|
3834
|
+
disabled: z8.array(z8.string()).optional(),
|
|
3835
|
+
enabled: z8.array(z8.string()).optional()
|
|
3527
3836
|
});
|
|
3528
|
-
var outlookProviderSchema =
|
|
3529
|
-
clientId:
|
|
3530
|
-
tenantId:
|
|
3837
|
+
var outlookProviderSchema = z8.object({
|
|
3838
|
+
clientId: z8.string().optional(),
|
|
3839
|
+
tenantId: z8.string().optional()
|
|
3531
3840
|
});
|
|
3532
|
-
var gmailProviderSchema =
|
|
3533
|
-
clientId:
|
|
3534
|
-
clientSecret:
|
|
3841
|
+
var gmailProviderSchema = z8.object({
|
|
3842
|
+
clientId: z8.string().optional(),
|
|
3843
|
+
clientSecret: z8.string().optional()
|
|
3535
3844
|
});
|
|
3536
|
-
var providersConfigSchema =
|
|
3845
|
+
var providersConfigSchema = z8.object({
|
|
3537
3846
|
outlook: outlookProviderSchema.optional(),
|
|
3538
3847
|
gmail: gmailProviderSchema.optional()
|
|
3539
3848
|
});
|
|
3540
|
-
var
|
|
3541
|
-
|
|
3849
|
+
var watchConfigSchema = z8.object({
|
|
3850
|
+
enabled: z8.boolean().default(true),
|
|
3851
|
+
pollIntervalSeconds: z8.number().int().min(10).max(3600).default(60)
|
|
3852
|
+
});
|
|
3853
|
+
var rawConfigSchema = z8.object({
|
|
3854
|
+
dataDir: z8.string().optional(),
|
|
3542
3855
|
http: httpConfigSchema.optional(),
|
|
3543
3856
|
tools: toolsConfigSchema.optional(),
|
|
3544
|
-
providers: providersConfigSchema.optional()
|
|
3857
|
+
providers: providersConfigSchema.optional(),
|
|
3858
|
+
watch: watchConfigSchema.optional()
|
|
3545
3859
|
});
|
|
3546
3860
|
var KNOWN_TOOLS = [
|
|
3547
3861
|
"list_accounts",
|
|
@@ -3567,7 +3881,8 @@ var KNOWN_TOOLS = [
|
|
|
3567
3881
|
"draft_email",
|
|
3568
3882
|
"edit_draft",
|
|
3569
3883
|
"send_draft",
|
|
3570
|
-
"add_attachment_to_draft"
|
|
3884
|
+
"add_attachment_to_draft",
|
|
3885
|
+
"check_notifications"
|
|
3571
3886
|
];
|
|
3572
3887
|
var ENV_VAR_RE = /\$\{([A-Z_][A-Z0-9_]*)\}/g;
|
|
3573
3888
|
function resolveEnvVars(value) {
|
|
@@ -3633,7 +3948,9 @@ function loadConfig(configPath, cliOverrides = {}) {
|
|
|
3633
3948
|
dataDir: cliOverrides.dataDir ?? parsed.dataDir ?? process.env.HYPERMAIL_MCP_DATA_DIR,
|
|
3634
3949
|
http,
|
|
3635
3950
|
tools: parsed.tools ? { disabled: parsed.tools.disabled, enabled: parsed.tools.enabled } : void 0,
|
|
3636
|
-
providers: parsed.providers
|
|
3951
|
+
providers: parsed.providers,
|
|
3952
|
+
watch: parsed.watch,
|
|
3953
|
+
agentsConfigPath: cliOverrides.agentsConfig ?? process.env.HYPERMAIL_AGENTS_CONFIG
|
|
3637
3954
|
};
|
|
3638
3955
|
}
|
|
3639
3956
|
function resolveTools(config) {
|
|
@@ -3646,25 +3963,284 @@ function resolveTools(config) {
|
|
|
3646
3963
|
};
|
|
3647
3964
|
}
|
|
3648
3965
|
|
|
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
|
+
constructor(opts) {
|
|
3974
|
+
this.opts = opts;
|
|
3975
|
+
}
|
|
3976
|
+
start() {
|
|
3977
|
+
if (this.running) return;
|
|
3978
|
+
this.running = true;
|
|
3979
|
+
let accounts = this.opts.store.listAccounts();
|
|
3980
|
+
if (this.opts.accountFilter) {
|
|
3981
|
+
const filter = new Set(this.opts.accountFilter.map((e) => e.toLowerCase()));
|
|
3982
|
+
accounts = accounts.filter((a) => filter.has(a.email.toLowerCase()));
|
|
3983
|
+
}
|
|
3984
|
+
for (const account of accounts) {
|
|
3985
|
+
this.schedulePoll(account);
|
|
3986
|
+
}
|
|
3987
|
+
}
|
|
3988
|
+
stop() {
|
|
3989
|
+
this.running = false;
|
|
3990
|
+
for (const t of this.timers) clearInterval(t);
|
|
3991
|
+
this.timers = [];
|
|
3992
|
+
}
|
|
3993
|
+
// ── internals ──
|
|
3994
|
+
schedulePoll(account) {
|
|
3995
|
+
this.pollAccount(account).catch(() => {
|
|
3996
|
+
});
|
|
3997
|
+
const timer = setInterval(() => {
|
|
3998
|
+
if (!this.running) return;
|
|
3999
|
+
this.pollAccount(account).catch(() => {
|
|
4000
|
+
});
|
|
4001
|
+
}, this.opts.pollIntervalSeconds * 1e3);
|
|
4002
|
+
this.timers.push(timer);
|
|
4003
|
+
}
|
|
4004
|
+
async pollAccount(account) {
|
|
4005
|
+
const key = account.email.toLowerCase();
|
|
4006
|
+
if (this.inflight.get(key)) return;
|
|
4007
|
+
this.inflight.set(key, true);
|
|
4008
|
+
try {
|
|
4009
|
+
const { provider } = this.opts.registry.resolveByEmail(account.email);
|
|
4010
|
+
const lastSeen = account.lastSeenAt;
|
|
4011
|
+
const limit = 25;
|
|
4012
|
+
let skip = 0;
|
|
4013
|
+
const newEmails = [];
|
|
4014
|
+
let newestTimestamp = lastSeen ?? "";
|
|
4015
|
+
let hitBoundary = false;
|
|
4016
|
+
while (true) {
|
|
4017
|
+
const { items, hasMore } = await provider.listEmails(account, {
|
|
4018
|
+
folder: "inbox",
|
|
4019
|
+
limit,
|
|
4020
|
+
skip
|
|
4021
|
+
});
|
|
4022
|
+
for (const item of items) {
|
|
4023
|
+
if (!item.receivedAt) continue;
|
|
4024
|
+
if (lastSeen && item.receivedAt <= lastSeen) {
|
|
4025
|
+
hitBoundary = true;
|
|
4026
|
+
break;
|
|
4027
|
+
}
|
|
4028
|
+
newEmails.push(item);
|
|
4029
|
+
if (item.receivedAt > newestTimestamp) {
|
|
4030
|
+
newestTimestamp = item.receivedAt;
|
|
4031
|
+
}
|
|
4032
|
+
}
|
|
4033
|
+
if (hitBoundary || !hasMore) break;
|
|
4034
|
+
skip += limit;
|
|
4035
|
+
}
|
|
4036
|
+
if (!lastSeen) {
|
|
4037
|
+
if (newEmails.length > 0) {
|
|
4038
|
+
newestTimestamp = newEmails[0].receivedAt;
|
|
4039
|
+
}
|
|
4040
|
+
} else if (newEmails.length > 0) {
|
|
4041
|
+
this.enqueue({
|
|
4042
|
+
type: "new_emails",
|
|
4043
|
+
account: account.email,
|
|
4044
|
+
emails: newEmails,
|
|
4045
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
4046
|
+
});
|
|
4047
|
+
}
|
|
4048
|
+
if (newestTimestamp !== (lastSeen ?? "")) {
|
|
4049
|
+
await this.opts.store.upsertAccount({
|
|
4050
|
+
...account,
|
|
4051
|
+
lastSeenAt: newestTimestamp || void 0
|
|
4052
|
+
});
|
|
4053
|
+
}
|
|
4054
|
+
} catch (err) {
|
|
4055
|
+
this.enqueue({
|
|
4056
|
+
type: "auth_failure",
|
|
4057
|
+
account: account.email,
|
|
4058
|
+
error: err instanceof Error ? err.message : String(err),
|
|
4059
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
4060
|
+
});
|
|
4061
|
+
} finally {
|
|
4062
|
+
this.inflight.delete(key);
|
|
4063
|
+
}
|
|
4064
|
+
}
|
|
4065
|
+
enqueue(notification) {
|
|
4066
|
+
this.opts.buffer.push(notification);
|
|
4067
|
+
try {
|
|
4068
|
+
this.opts.onNotification(notification);
|
|
4069
|
+
} catch {
|
|
4070
|
+
}
|
|
4071
|
+
}
|
|
4072
|
+
};
|
|
4073
|
+
|
|
4074
|
+
// src/config/agents-config.ts
|
|
4075
|
+
import { readFileSync as readFileSync2, watch } from "fs";
|
|
4076
|
+
import { load as loadYaml } from "js-yaml";
|
|
4077
|
+
import { z as z9 } from "zod";
|
|
4078
|
+
var agentDefSchema = z9.object({
|
|
4079
|
+
id: z9.string().min(1).regex(
|
|
4080
|
+
/^[a-z0-9_-]+$/,
|
|
4081
|
+
"agent id must contain only lowercase letters, digits, hyphens, and underscores"
|
|
4082
|
+
),
|
|
4083
|
+
api_key: z9.string().min(1).regex(
|
|
4084
|
+
/^hm_sk_[a-f0-9]{64}$/,
|
|
4085
|
+
"api_key must match hm_sk_ prefix + 64 hex chars (use `hypermail-mcp generate-key`)"
|
|
4086
|
+
),
|
|
4087
|
+
name: z9.string().min(1),
|
|
4088
|
+
accounts: z9.array(z9.string().email()).optional().default([]),
|
|
4089
|
+
provisioning: z9.boolean().optional().default(false)
|
|
4090
|
+
});
|
|
4091
|
+
var emailAccountDefSchema = z9.object({
|
|
4092
|
+
provider: z9.enum(["outlook", "imap", "gmail"]),
|
|
4093
|
+
display_name: z9.string().optional()
|
|
4094
|
+
});
|
|
4095
|
+
var agentsConfigSchema = z9.object({
|
|
4096
|
+
agents: z9.array(agentDefSchema).optional().default([]),
|
|
4097
|
+
email_accounts: z9.record(z9.string().email(), emailAccountDefSchema).optional().default({})
|
|
4098
|
+
});
|
|
4099
|
+
function loadAgentsConfig(configPath) {
|
|
4100
|
+
let raw;
|
|
4101
|
+
try {
|
|
4102
|
+
raw = loadYaml(readFileSync2(configPath, "utf-8"));
|
|
4103
|
+
} catch (err) {
|
|
4104
|
+
if (err instanceof Error && "code" in err && err.code === "ENOENT") {
|
|
4105
|
+
throw new Error(
|
|
4106
|
+
`Agents config file not found: ${configPath}. Create an agents.yaml with at least one agent to enable HTTP multi-tenant mode.`
|
|
4107
|
+
);
|
|
4108
|
+
}
|
|
4109
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
4110
|
+
throw new Error(`Failed to parse agents config "${configPath}": ${detail}`);
|
|
4111
|
+
}
|
|
4112
|
+
const parsed = agentsConfigSchema.parse(raw ?? {});
|
|
4113
|
+
const ids = /* @__PURE__ */ new Set();
|
|
4114
|
+
for (const a of parsed.agents) {
|
|
4115
|
+
if (ids.has(a.id)) {
|
|
4116
|
+
throw new Error(`Duplicate agent id "${a.id}" in agents config`);
|
|
4117
|
+
}
|
|
4118
|
+
ids.add(a.id);
|
|
4119
|
+
}
|
|
4120
|
+
return {
|
|
4121
|
+
agents: parsed.agents.map((a) => ({
|
|
4122
|
+
id: a.id,
|
|
4123
|
+
api_key: a.api_key,
|
|
4124
|
+
name: a.name,
|
|
4125
|
+
accounts: a.accounts,
|
|
4126
|
+
provisioning: a.provisioning
|
|
4127
|
+
})),
|
|
4128
|
+
email_accounts: parsed.email_accounts
|
|
4129
|
+
};
|
|
4130
|
+
}
|
|
4131
|
+
async function syncAgentsToStore(config, store) {
|
|
4132
|
+
const configAgentIds = new Set(config.agents.map((a) => a.id));
|
|
4133
|
+
const storedAgents = store.listAgents();
|
|
4134
|
+
for (const def of config.agents) {
|
|
4135
|
+
await store.upsertAgent({
|
|
4136
|
+
id: def.id,
|
|
4137
|
+
plaintextApiKey: def.api_key,
|
|
4138
|
+
name: def.name,
|
|
4139
|
+
accounts: def.accounts,
|
|
4140
|
+
provisioning: def.provisioning
|
|
4141
|
+
});
|
|
4142
|
+
}
|
|
4143
|
+
const removed = [];
|
|
4144
|
+
for (const stored of storedAgents) {
|
|
4145
|
+
if (!configAgentIds.has(stored.id)) {
|
|
4146
|
+
await store.removeAgent(stored.id);
|
|
4147
|
+
removed.push(stored.id);
|
|
4148
|
+
}
|
|
4149
|
+
}
|
|
4150
|
+
return removed;
|
|
4151
|
+
}
|
|
4152
|
+
function watchAgentsConfig(configPath, store, onChange, onError) {
|
|
4153
|
+
let timer = null;
|
|
4154
|
+
let watcher = null;
|
|
4155
|
+
const reload = async () => {
|
|
4156
|
+
try {
|
|
4157
|
+
const config = loadAgentsConfig(configPath);
|
|
4158
|
+
const removed = await syncAgentsToStore(config, store);
|
|
4159
|
+
onChange(removed);
|
|
4160
|
+
} catch (err) {
|
|
4161
|
+
onError(err instanceof Error ? err : new Error(String(err)));
|
|
4162
|
+
}
|
|
4163
|
+
};
|
|
4164
|
+
reload().catch((err) => onError(err));
|
|
4165
|
+
try {
|
|
4166
|
+
watcher = watch(configPath, (_eventType) => {
|
|
4167
|
+
if (timer) clearTimeout(timer);
|
|
4168
|
+
timer = setTimeout(() => {
|
|
4169
|
+
timer = null;
|
|
4170
|
+
reload().catch((err) => onError(err));
|
|
4171
|
+
}, 200);
|
|
4172
|
+
});
|
|
4173
|
+
} catch (err) {
|
|
4174
|
+
}
|
|
4175
|
+
return {
|
|
4176
|
+
close() {
|
|
4177
|
+
if (timer) clearTimeout(timer);
|
|
4178
|
+
if (watcher) watcher.close();
|
|
4179
|
+
}
|
|
4180
|
+
};
|
|
4181
|
+
}
|
|
4182
|
+
|
|
3649
4183
|
// src/server.ts
|
|
3650
4184
|
async function startServer(opts) {
|
|
3651
4185
|
const { config } = opts;
|
|
3652
4186
|
const store = await AccountStore.open({ dataDir: config.dataDir });
|
|
3653
4187
|
const registry = buildRegistry({ store, providers: config.providers });
|
|
3654
4188
|
const tools = resolveTools(config);
|
|
3655
|
-
const
|
|
3656
|
-
|
|
3657
|
-
|
|
3658
|
-
|
|
3659
|
-
|
|
4189
|
+
const notificationBuffer = config.http.enabled ? [] : void 0;
|
|
4190
|
+
let agentStoreForFactory;
|
|
4191
|
+
const createServer = (agentContext = null) => {
|
|
4192
|
+
const s = new McpServer(
|
|
4193
|
+
{ name: "hypermail-mcp", version: VERSION },
|
|
4194
|
+
{ capabilities: { tools: {}, logging: {} } }
|
|
4195
|
+
);
|
|
4196
|
+
registerTools(s, { store, registry, tools, notificationBuffer, agentContext, agentStore: agentStoreForFactory });
|
|
4197
|
+
return s;
|
|
4198
|
+
};
|
|
3660
4199
|
if (config.http.enabled) {
|
|
3661
|
-
|
|
4200
|
+
let liveReloadHandle;
|
|
4201
|
+
if (config.agentsConfigPath) {
|
|
4202
|
+
agentStoreForFactory = await AgentStore.open({ dataDir: config.dataDir });
|
|
4203
|
+
liveReloadHandle = watchAgentsConfig(
|
|
4204
|
+
path4.resolve(config.agentsConfigPath),
|
|
4205
|
+
agentStoreForFactory,
|
|
4206
|
+
(_removedIds) => {
|
|
4207
|
+
},
|
|
4208
|
+
(err) => {
|
|
4209
|
+
console.error("[hypermail-mcp] agents.yaml reload error:", err.message);
|
|
4210
|
+
}
|
|
4211
|
+
);
|
|
4212
|
+
}
|
|
4213
|
+
const notifyTargets = /* @__PURE__ */ new Set();
|
|
4214
|
+
const watcher = new WatcherManager({
|
|
4215
|
+
registry,
|
|
4216
|
+
store,
|
|
4217
|
+
pollIntervalSeconds: config.watch?.pollIntervalSeconds ?? 60,
|
|
4218
|
+
onNotification: (notification) => {
|
|
4219
|
+
for (const fn of notifyTargets) {
|
|
4220
|
+
fn(notification);
|
|
4221
|
+
}
|
|
4222
|
+
},
|
|
4223
|
+
buffer: notificationBuffer
|
|
4224
|
+
});
|
|
4225
|
+
watcher.start();
|
|
4226
|
+
await startHttp(
|
|
4227
|
+
createServer,
|
|
4228
|
+
config.http.host,
|
|
4229
|
+
config.http.port,
|
|
4230
|
+
notifyTargets,
|
|
4231
|
+
agentStoreForFactory
|
|
4232
|
+
);
|
|
4233
|
+
if (liveReloadHandle) {
|
|
4234
|
+
process.on("SIGINT", () => liveReloadHandle.close());
|
|
4235
|
+
process.on("SIGTERM", () => liveReloadHandle.close());
|
|
4236
|
+
}
|
|
3662
4237
|
} else {
|
|
4238
|
+
const server = createServer();
|
|
3663
4239
|
const transport = new StdioServerTransport();
|
|
3664
4240
|
await server.connect(transport);
|
|
3665
4241
|
}
|
|
3666
4242
|
}
|
|
3667
|
-
async function startHttp(
|
|
4243
|
+
async function startHttp(createServer, host, port, notifyTargets, agentStore) {
|
|
3668
4244
|
const sessions = /* @__PURE__ */ new Map();
|
|
3669
4245
|
const http = createHttpServer(async (req, res) => {
|
|
3670
4246
|
try {
|
|
@@ -3674,18 +4250,59 @@ async function startHttp(server, host, port) {
|
|
|
3674
4250
|
return;
|
|
3675
4251
|
}
|
|
3676
4252
|
const sessionId = req.headers["mcp-session-id"] ?? void 0;
|
|
3677
|
-
let
|
|
3678
|
-
if (!
|
|
3679
|
-
|
|
4253
|
+
let session = sessionId ? sessions.get(sessionId) : void 0;
|
|
4254
|
+
if (!session) {
|
|
4255
|
+
let agentContext = null;
|
|
4256
|
+
if (agentStore) {
|
|
4257
|
+
const apiKey = req.headers["x-api-key"]?.trim();
|
|
4258
|
+
if (!apiKey) {
|
|
4259
|
+
res.statusCode = 401;
|
|
4260
|
+
res.setHeader("Content-Type", "application/json");
|
|
4261
|
+
res.end(JSON.stringify({ error: "Missing x-api-key header" }));
|
|
4262
|
+
return;
|
|
4263
|
+
}
|
|
4264
|
+
const agent = agentStore.findAgentByApiKey(apiKey);
|
|
4265
|
+
if (!agent) {
|
|
4266
|
+
res.statusCode = 401;
|
|
4267
|
+
res.setHeader("Content-Type", "application/json");
|
|
4268
|
+
res.end(JSON.stringify({ error: "Invalid API key" }));
|
|
4269
|
+
return;
|
|
4270
|
+
}
|
|
4271
|
+
agentContext = {
|
|
4272
|
+
agentId: agent.id,
|
|
4273
|
+
accounts: agent.accounts,
|
|
4274
|
+
provisioning: agent.provisioning
|
|
4275
|
+
};
|
|
4276
|
+
}
|
|
4277
|
+
const server = createServer(agentContext);
|
|
4278
|
+
const transport = new StreamableHTTPServerTransport({
|
|
3680
4279
|
sessionIdGenerator: () => randomUUID6(),
|
|
3681
4280
|
onsessioninitialized: (sid) => {
|
|
3682
|
-
sessions.set(sid, transport);
|
|
4281
|
+
sessions.set(sid, { transport, server, agentContext });
|
|
4282
|
+
const agentAccounts = agentContext ? new Set(agentContext.accounts.map((a) => a.toLowerCase())) : null;
|
|
4283
|
+
const notifyFn = (n) => {
|
|
4284
|
+
if (agentAccounts && !agentAccounts.has(n.account.toLowerCase())) {
|
|
4285
|
+
return;
|
|
4286
|
+
}
|
|
4287
|
+
server.server.notification({
|
|
4288
|
+
method: "notifications/message",
|
|
4289
|
+
params: {
|
|
4290
|
+
level: n.type === "new_emails" ? "notice" : "warning",
|
|
4291
|
+
logger: "hypermail-watch",
|
|
4292
|
+
data: n
|
|
4293
|
+
}
|
|
4294
|
+
}).catch(() => {
|
|
4295
|
+
});
|
|
4296
|
+
};
|
|
4297
|
+
notifyTargets.add(notifyFn);
|
|
4298
|
+
transport.onclose = () => {
|
|
4299
|
+
if (transport.sessionId) sessions.delete(transport.sessionId);
|
|
4300
|
+
notifyTargets.delete(notifyFn);
|
|
4301
|
+
};
|
|
3683
4302
|
}
|
|
3684
4303
|
});
|
|
3685
|
-
transport.onclose = () => {
|
|
3686
|
-
if (transport.sessionId) sessions.delete(transport.sessionId);
|
|
3687
|
-
};
|
|
3688
4304
|
await server.connect(transport);
|
|
4305
|
+
session = { transport, server, agentContext };
|
|
3689
4306
|
}
|
|
3690
4307
|
let body = void 0;
|
|
3691
4308
|
if (req.method === "POST" || req.method === "DELETE") {
|
|
@@ -3694,7 +4311,7 @@ async function startHttp(server, host, port) {
|
|
|
3694
4311
|
const raw = Buffer.concat(chunks).toString("utf8");
|
|
3695
4312
|
body = raw ? JSON.parse(raw) : void 0;
|
|
3696
4313
|
}
|
|
3697
|
-
await transport.handleRequest(req, res, body);
|
|
4314
|
+
await session.transport.handleRequest(req, res, body);
|
|
3698
4315
|
} catch (err) {
|
|
3699
4316
|
console.error("[hypermail-mcp] http error:", err);
|
|
3700
4317
|
if (!res.headersSent) {
|
|
@@ -3733,6 +4350,9 @@ function parseArgs(argv) {
|
|
|
3733
4350
|
case "--config":
|
|
3734
4351
|
out.config = String(argv[++i] ?? "");
|
|
3735
4352
|
break;
|
|
4353
|
+
case "--agents-config":
|
|
4354
|
+
out.agentsConfig = String(argv[++i] ?? "");
|
|
4355
|
+
break;
|
|
3736
4356
|
case "-h":
|
|
3737
4357
|
case "--help":
|
|
3738
4358
|
out.help = true;
|
|
@@ -3777,7 +4397,13 @@ Example hypermail-config.json:
|
|
|
3777
4397
|
process.stdout.write(msg);
|
|
3778
4398
|
}
|
|
3779
4399
|
async function main() {
|
|
3780
|
-
const
|
|
4400
|
+
const rawArgs = process.argv.slice(2);
|
|
4401
|
+
if (rawArgs[0] === "generate-key") {
|
|
4402
|
+
const key = `hm_sk_${randomBytes2(32).toString("hex")}`;
|
|
4403
|
+
process.stdout.write(key + "\n");
|
|
4404
|
+
return;
|
|
4405
|
+
}
|
|
4406
|
+
const opts = parseArgs(rawArgs);
|
|
3781
4407
|
if (opts.help) {
|
|
3782
4408
|
printHelp();
|
|
3783
4409
|
return;
|
|
@@ -3786,7 +4412,8 @@ async function main() {
|
|
|
3786
4412
|
http: opts.http,
|
|
3787
4413
|
port: opts.port,
|
|
3788
4414
|
host: opts.host,
|
|
3789
|
-
dataDir: opts.dataDir
|
|
4415
|
+
dataDir: opts.dataDir,
|
|
4416
|
+
agentsConfig: opts.agentsConfig
|
|
3790
4417
|
});
|
|
3791
4418
|
await startServer({ config });
|
|
3792
4419
|
}
|