hypermail-mcp 0.6.0 → 0.6.1
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 +25 -0
- package/dist/cli.js +72 -29
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
A **Model Context Protocol** server that lets an agent operate any of the user's
|
|
4
4
|
inboxes through a single, unified tool surface.
|
|
5
5
|
|
|
6
|
+
> **v0.6.1** — Docker deployment (standalone Dockerfile with HEALTHCHECK),
|
|
7
|
+
> email notification bug fixes (ID-based dedup, pagination cap, dynamic
|
|
8
|
+
> re-scan), Node 22 base image, dropped docker-compose.
|
|
9
|
+
>
|
|
6
10
|
> **v0.6.0** — Email watch notifications (polling-based), `signaturePath`
|
|
7
11
|
> support in `set_account_settings` for loading signatures from files,
|
|
8
12
|
> and a `check_notifications` tool for draining pending alerts.
|
|
@@ -72,6 +76,27 @@ hypermail-mcp --http --port 3000 --host 0.0.0.0
|
|
|
72
76
|
When hosted you **must** set `HYPERMAIL_MCP_KEY` so the account file is
|
|
73
77
|
reproducibly decryptable.
|
|
74
78
|
|
|
79
|
+
### Docker
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
# Build
|
|
83
|
+
docker build -t hypermail-mcp .
|
|
84
|
+
|
|
85
|
+
# Run
|
|
86
|
+
docker run -d \
|
|
87
|
+
--name hypermail-mcp \
|
|
88
|
+
-p 3000:3000 \
|
|
89
|
+
-e HYPERMAIL_MCP_KEY=<32-byte-key> \
|
|
90
|
+
-e MS_CLIENT_ID=<your-client-id> \
|
|
91
|
+
-e MS_TENANT_ID=<your-tenant-id> \
|
|
92
|
+
-v hypermail-data:/data \
|
|
93
|
+
hypermail-mcp
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
The image runs the server in HTTP mode on port 3000 with a 30-second
|
|
97
|
+
HEALTHCHECK against `/mcp`. Data is persisted via a Docker volume at `/data`.
|
|
98
|
+
Pass `HYPERMAIL_AGENTS_CONFIG` and mount a config file for agent multi-tenancy.
|
|
99
|
+
|
|
75
100
|
### Development (HTTP mode + email watch)
|
|
76
101
|
|
|
77
102
|
To test the email watch feature locally:
|
package/dist/cli.js
CHANGED
|
@@ -3970,12 +3970,29 @@ var WatcherManager = class {
|
|
|
3970
3970
|
running = false;
|
|
3971
3971
|
/** Per-account inflight guards to prevent overlapping polls. */
|
|
3972
3972
|
inflight = /* @__PURE__ */ new Map();
|
|
3973
|
+
/** Accounts with active polling timers (lowercased email). */
|
|
3974
|
+
tracked = /* @__PURE__ */ new Set();
|
|
3973
3975
|
constructor(opts) {
|
|
3974
3976
|
this.opts = opts;
|
|
3975
3977
|
}
|
|
3976
3978
|
start() {
|
|
3977
3979
|
if (this.running) return;
|
|
3978
3980
|
this.running = true;
|
|
3981
|
+
this.scanAccounts();
|
|
3982
|
+
const rescanTimer = setInterval(() => {
|
|
3983
|
+
if (!this.running) return;
|
|
3984
|
+
this.scanAccounts();
|
|
3985
|
+
}, this.opts.pollIntervalSeconds * 1e3);
|
|
3986
|
+
this.timers.push(rescanTimer);
|
|
3987
|
+
}
|
|
3988
|
+
stop() {
|
|
3989
|
+
this.running = false;
|
|
3990
|
+
for (const t of this.timers) clearInterval(t);
|
|
3991
|
+
this.timers = [];
|
|
3992
|
+
this.tracked.clear();
|
|
3993
|
+
}
|
|
3994
|
+
// ── internals ──
|
|
3995
|
+
scanAccounts() {
|
|
3979
3996
|
let accounts = this.opts.store.listAccounts();
|
|
3980
3997
|
if (this.opts.accountFilter) {
|
|
3981
3998
|
const filter = new Set(this.opts.accountFilter.map((e) => e.toLowerCase()));
|
|
@@ -3985,13 +4002,10 @@ var WatcherManager = class {
|
|
|
3985
4002
|
this.schedulePoll(account);
|
|
3986
4003
|
}
|
|
3987
4004
|
}
|
|
3988
|
-
stop() {
|
|
3989
|
-
this.running = false;
|
|
3990
|
-
for (const t of this.timers) clearInterval(t);
|
|
3991
|
-
this.timers = [];
|
|
3992
|
-
}
|
|
3993
|
-
// ── internals ──
|
|
3994
4005
|
schedulePoll(account) {
|
|
4006
|
+
const key = account.email.toLowerCase();
|
|
4007
|
+
if (this.tracked.has(key)) return;
|
|
4008
|
+
this.tracked.add(key);
|
|
3995
4009
|
this.pollAccount(account).catch(() => {
|
|
3996
4010
|
});
|
|
3997
4011
|
const timer = setInterval(() => {
|
|
@@ -4007,21 +4021,25 @@ var WatcherManager = class {
|
|
|
4007
4021
|
this.inflight.set(key, true);
|
|
4008
4022
|
try {
|
|
4009
4023
|
const { provider } = this.opts.registry.resolveByEmail(account.email);
|
|
4010
|
-
const
|
|
4024
|
+
const seenIds = new Set(account.lastSeenIds ?? []);
|
|
4025
|
+
const isFirstPoll = !account.lastSeenAt && !account.lastSeenIds?.length;
|
|
4011
4026
|
const limit = 25;
|
|
4027
|
+
const MAX_PAGES = 5;
|
|
4012
4028
|
let skip = 0;
|
|
4029
|
+
let pageCount = 0;
|
|
4013
4030
|
const newEmails = [];
|
|
4014
|
-
let newestTimestamp =
|
|
4031
|
+
let newestTimestamp = account.lastSeenAt ?? "";
|
|
4015
4032
|
let hitBoundary = false;
|
|
4016
|
-
while (
|
|
4033
|
+
while (pageCount < MAX_PAGES) {
|
|
4017
4034
|
const { items, hasMore } = await provider.listEmails(account, {
|
|
4018
4035
|
folder: "inbox",
|
|
4019
4036
|
limit,
|
|
4020
4037
|
skip
|
|
4021
4038
|
});
|
|
4039
|
+
pageCount++;
|
|
4022
4040
|
for (const item of items) {
|
|
4023
4041
|
if (!item.receivedAt) continue;
|
|
4024
|
-
if (
|
|
4042
|
+
if (seenIds.has(item.id)) {
|
|
4025
4043
|
hitBoundary = true;
|
|
4026
4044
|
break;
|
|
4027
4045
|
}
|
|
@@ -4033,11 +4051,7 @@ var WatcherManager = class {
|
|
|
4033
4051
|
if (hitBoundary || !hasMore) break;
|
|
4034
4052
|
skip += limit;
|
|
4035
4053
|
}
|
|
4036
|
-
if (!
|
|
4037
|
-
if (newEmails.length > 0) {
|
|
4038
|
-
newestTimestamp = newEmails[0].receivedAt;
|
|
4039
|
-
}
|
|
4040
|
-
} else if (newEmails.length > 0) {
|
|
4054
|
+
if (!isFirstPoll && newEmails.length > 0) {
|
|
4041
4055
|
this.enqueue({
|
|
4042
4056
|
type: "new_emails",
|
|
4043
4057
|
account: account.email,
|
|
@@ -4045,11 +4059,27 @@ var WatcherManager = class {
|
|
|
4045
4059
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
4046
4060
|
});
|
|
4047
4061
|
}
|
|
4048
|
-
if (
|
|
4062
|
+
if (isFirstPoll && newEmails.length === 0 && !newestTimestamp) {
|
|
4063
|
+
newestTimestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
4064
|
+
}
|
|
4065
|
+
const newIds = newEmails.map((e) => e.id);
|
|
4066
|
+
const updatedLastSeenIds = [
|
|
4067
|
+
...newIds,
|
|
4068
|
+
...account.lastSeenIds ?? []
|
|
4069
|
+
].slice(0, 200);
|
|
4070
|
+
try {
|
|
4049
4071
|
await this.opts.store.upsertAccount({
|
|
4050
4072
|
...account,
|
|
4051
|
-
lastSeenAt: newestTimestamp || void 0
|
|
4073
|
+
lastSeenAt: newestTimestamp || void 0,
|
|
4074
|
+
lastSeenIds: updatedLastSeenIds
|
|
4052
4075
|
});
|
|
4076
|
+
} catch (storeErr) {
|
|
4077
|
+
console.error(
|
|
4078
|
+
"[hypermail-mcp] failed to persist poll state for",
|
|
4079
|
+
account.email,
|
|
4080
|
+
":",
|
|
4081
|
+
storeErr instanceof Error ? storeErr.message : String(storeErr)
|
|
4082
|
+
);
|
|
4053
4083
|
}
|
|
4054
4084
|
} catch (err) {
|
|
4055
4085
|
this.enqueue({
|
|
@@ -4186,7 +4216,8 @@ async function startServer(opts) {
|
|
|
4186
4216
|
const store = await AccountStore.open({ dataDir: config.dataDir });
|
|
4187
4217
|
const registry = buildRegistry({ store, providers: config.providers });
|
|
4188
4218
|
const tools = resolveTools(config);
|
|
4189
|
-
const
|
|
4219
|
+
const watchEnabled = config.http.enabled && config.watch?.enabled !== false;
|
|
4220
|
+
const notificationBuffer = watchEnabled ? [] : void 0;
|
|
4190
4221
|
let agentStoreForFactory;
|
|
4191
4222
|
const createServer = (agentContext = null) => {
|
|
4192
4223
|
const s = new McpServer(
|
|
@@ -4211,18 +4242,30 @@ async function startServer(opts) {
|
|
|
4211
4242
|
);
|
|
4212
4243
|
}
|
|
4213
4244
|
const notifyTargets = /* @__PURE__ */ new Set();
|
|
4214
|
-
const
|
|
4215
|
-
|
|
4216
|
-
|
|
4217
|
-
|
|
4218
|
-
|
|
4219
|
-
for (const fn of notifyTargets) {
|
|
4220
|
-
fn(notification);
|
|
4245
|
+
const accountFilter = agentStoreForFactory ? (() => {
|
|
4246
|
+
const all = /* @__PURE__ */ new Set();
|
|
4247
|
+
for (const agent of agentStoreForFactory.listAgents()) {
|
|
4248
|
+
for (const email of agent.accounts) {
|
|
4249
|
+
all.add(email.toLowerCase());
|
|
4221
4250
|
}
|
|
4222
|
-
}
|
|
4223
|
-
|
|
4224
|
-
});
|
|
4225
|
-
|
|
4251
|
+
}
|
|
4252
|
+
return all.size > 0 ? [...all] : void 0;
|
|
4253
|
+
})() : void 0;
|
|
4254
|
+
if (watchEnabled) {
|
|
4255
|
+
const watcher = new WatcherManager({
|
|
4256
|
+
registry,
|
|
4257
|
+
store,
|
|
4258
|
+
pollIntervalSeconds: config.watch?.pollIntervalSeconds ?? 60,
|
|
4259
|
+
accountFilter,
|
|
4260
|
+
onNotification: (notification) => {
|
|
4261
|
+
for (const fn of notifyTargets) {
|
|
4262
|
+
fn(notification);
|
|
4263
|
+
}
|
|
4264
|
+
},
|
|
4265
|
+
buffer: notificationBuffer
|
|
4266
|
+
});
|
|
4267
|
+
watcher.start();
|
|
4268
|
+
}
|
|
4226
4269
|
await startHttp(
|
|
4227
4270
|
createServer,
|
|
4228
4271
|
config.http.host,
|