hypermail-mcp 0.6.3 → 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 +60 -0
- package/dist/cli.js +138 -4
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -3,6 +3,11 @@
|
|
|
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.7.0** — Email watch mode: background poll loop detects new inbox
|
|
7
|
+
> messages and POSTs them to a configurable webhook URL (e.g. Mastra). Opt-in —
|
|
8
|
+
> disabled by default, enabled via `HYPERMAIL_WATCH_ENABLED=true` or config.
|
|
9
|
+
> Works in both stdio and HTTP transport modes.
|
|
10
|
+
>
|
|
6
11
|
> **v0.6.3** — Unify stdio and HTTP modes into a single feature set. Removed
|
|
7
12
|
> email watch (inbox polling, SSE push, notification buffer), agent
|
|
8
13
|
> multi-tenancy (`agents.yaml`, `x-api-key` auth, per-agent allowlists), and
|
|
@@ -149,6 +154,14 @@ looks for it in the same directory as `cli.js`.
|
|
|
149
154
|
},
|
|
150
155
|
"providers": {
|
|
151
156
|
"outlook": { "clientId": "...", "tenantId": "..." }
|
|
157
|
+
},
|
|
158
|
+
"watch": {
|
|
159
|
+
"enabled": true,
|
|
160
|
+
"pollIntervalSeconds": 10,
|
|
161
|
+
"webhook": {
|
|
162
|
+
"url": "http://your-agent:3000/api/email-webhook",
|
|
163
|
+
"retry": { "maxAttempts": 5, "baseDelayMs": 1000 }
|
|
164
|
+
}
|
|
152
165
|
}
|
|
153
166
|
}
|
|
154
167
|
```
|
|
@@ -190,6 +203,49 @@ account store.
|
|
|
190
203
|
| `mark_read` | `account`, `id` | Mark a message as read. Disabled under `--read-only`. |
|
|
191
204
|
| `mark_unread` | `account`, `id` | Mark a message as unread. Disabled under `--read-only`. |
|
|
192
205
|
|
|
206
|
+
## Email Watch
|
|
207
|
+
|
|
208
|
+
When enabled, hypermail-mcp runs a background poll loop that scans inboxes for
|
|
209
|
+
new messages and POSTs each one to a configurable webhook URL. Intended for
|
|
210
|
+
push-based email triage — downstream agents (e.g. Mastra) receive full email
|
|
211
|
+
content without polling.
|
|
212
|
+
|
|
213
|
+
```jsonc
|
|
214
|
+
{
|
|
215
|
+
"watch": {
|
|
216
|
+
"enabled": true,
|
|
217
|
+
"pollIntervalSeconds": 10,
|
|
218
|
+
"webhook": {
|
|
219
|
+
"url": "http://localhost:3000/api/email-webhook",
|
|
220
|
+
"retry": { "maxAttempts": 5, "baseDelayMs": 1000 }
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
| Setting | Default | Notes |
|
|
227
|
+
| --- | --- | --- |
|
|
228
|
+
| `watch.enabled` | `false` | Toggle via config or `HYPERMAIL_WATCH_ENABLED=true` env var |
|
|
229
|
+
| `watch.pollIntervalSeconds` | `10` | Min 10s, max 3600s |
|
|
230
|
+
| `watch.webhook.url` | — | Endpoint that receives `POST` with `EmailFull` JSON |
|
|
231
|
+
| `watch.webhook.retry.maxAttempts` | `5` | Max delivery attempts (1–10) |
|
|
232
|
+
| `watch.webhook.retry.baseDelayMs` | `1000` | Base backoff delay (× 2^attempt) |
|
|
233
|
+
|
|
234
|
+
**Behavior:**
|
|
235
|
+
- Polls **all accounts** in the store, **inbox only**.
|
|
236
|
+
- Detects new emails via `lastSeenIds` (capped at 200) stored in the encrypted
|
|
237
|
+
account file — no duplicate emits across restarts.
|
|
238
|
+
- One `POST` per email (full body: subject, sender, text, HTML, attachments
|
|
239
|
+
metadata, thread ID via `EmailFull`).
|
|
240
|
+
- Delivery uses exponential backoff (`baseDelay × 2^attempt`). Retries on
|
|
241
|
+
non-2xx responses and connection errors. Logs and moves on after
|
|
242
|
+
`maxAttempts` exhausted — never blocks the poll loop.
|
|
243
|
+
- Works in both **stdio** and **HTTP** transport modes — the poll interval
|
|
244
|
+
fires normally alongside MCP message handling.
|
|
245
|
+
|
|
246
|
+
**Rate limits:** Polling every 10s on a single inbox = 6 req/min = 0.6% of
|
|
247
|
+
Microsoft Graph's 10,000 req/10min per-user limit. Safe for personal inboxes.
|
|
248
|
+
|
|
193
249
|
## Add-account flow (Outlook)
|
|
194
250
|
|
|
195
251
|
1. Agent calls `add_account({ provider: "outlook" })`.
|
|
@@ -240,6 +296,10 @@ src/
|
|
|
240
296
|
client.ts # Gmail API (googleapis)
|
|
241
297
|
index.ts # GmailProvider implementation
|
|
242
298
|
shared/ # shared utilities across providers
|
|
299
|
+
watcher/
|
|
300
|
+
manager.ts # WatcherManager — inbox poll loop + dedup
|
|
301
|
+
webhook.ts # HTTP POST with exponential backoff retry
|
|
302
|
+
index.ts # barrel export
|
|
243
303
|
tools/
|
|
244
304
|
index.ts # MCP tool registrations
|
|
245
305
|
accounts.ts # list/add/remove/complete-add account tools
|
package/dist/cli.js
CHANGED
|
@@ -3568,7 +3568,7 @@ function registerTools(server, opts) {
|
|
|
3568
3568
|
// package.json
|
|
3569
3569
|
var package_default = {
|
|
3570
3570
|
name: "hypermail-mcp",
|
|
3571
|
-
version: "0.
|
|
3571
|
+
version: "0.7.0",
|
|
3572
3572
|
description: "Unified email MCP server \u2014 operate any inbox (Outlook now, IMAP/Gmail later) by passing an email address.",
|
|
3573
3573
|
type: "module",
|
|
3574
3574
|
bin: {
|
|
@@ -3638,6 +3638,116 @@ var package_default = {
|
|
|
3638
3638
|
// src/version.ts
|
|
3639
3639
|
var VERSION = package_default.version;
|
|
3640
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
|
+
|
|
3641
3751
|
// src/config.ts
|
|
3642
3752
|
import { readFileSync } from "fs";
|
|
3643
3753
|
import { z as z7 } from "zod";
|
|
@@ -3663,8 +3773,15 @@ var providersConfigSchema = z7.object({
|
|
|
3663
3773
|
gmail: gmailProviderSchema.optional()
|
|
3664
3774
|
});
|
|
3665
3775
|
var watchConfigSchema = z7.object({
|
|
3666
|
-
enabled: z7.boolean().default(
|
|
3667
|
-
pollIntervalSeconds: z7.number().int().min(10).max(3600).default(
|
|
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()
|
|
3668
3785
|
});
|
|
3669
3786
|
var rawConfigSchema = z7.object({
|
|
3670
3787
|
dataDir: z7.string().optional(),
|
|
@@ -3760,11 +3877,20 @@ function loadConfig(configPath, cliOverrides = {}) {
|
|
|
3760
3877
|
port: cliOverrides.port ?? parsed.http?.port ?? 3e3,
|
|
3761
3878
|
host: cliOverrides.host ?? parsed.http?.host ?? "127.0.0.1"
|
|
3762
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
|
+
}
|
|
3763
3888
|
return {
|
|
3764
3889
|
dataDir: cliOverrides.dataDir ?? parsed.dataDir ?? process.env.HYPERMAIL_MCP_DATA_DIR,
|
|
3765
3890
|
http,
|
|
3766
3891
|
tools: parsed.tools ? { disabled: parsed.tools.disabled, enabled: parsed.tools.enabled } : void 0,
|
|
3767
|
-
providers: parsed.providers
|
|
3892
|
+
providers: parsed.providers,
|
|
3893
|
+
watch
|
|
3768
3894
|
};
|
|
3769
3895
|
}
|
|
3770
3896
|
function resolveTools(config) {
|
|
@@ -3783,6 +3909,14 @@ async function startServer(opts) {
|
|
|
3783
3909
|
const store = await AccountStore.open({ dataDir: config.dataDir });
|
|
3784
3910
|
const registry = buildRegistry({ store, providers: config.providers });
|
|
3785
3911
|
const tools = resolveTools(config);
|
|
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
|
+
}
|
|
3786
3920
|
const createServer = () => {
|
|
3787
3921
|
const s = new McpServer(
|
|
3788
3922
|
{ name: "hypermail-mcp", version: VERSION },
|