hypermail-mcp 0.6.3 → 0.7.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 +92 -0
- package/dist/cli.js +271 -39
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -3,6 +3,16 @@
|
|
|
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.1** — Every config field is now settable via a dedicated
|
|
7
|
+
> `HYPERMAIL_*` env var. Legacy env vars (`MS_CLIENT_ID`, `MS_TENANT_ID`,
|
|
8
|
+
> `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`) still work as fallbacks. See
|
|
9
|
+
> [Environment Variables](#environment-variables) for the full reference.
|
|
10
|
+
>
|
|
11
|
+
> **v0.7.0** — Email watch mode: background poll loop detects new inbox
|
|
12
|
+
> messages and POSTs them to a configurable webhook URL (e.g. Mastra). Opt-in —
|
|
13
|
+
> disabled by default, enabled via `HYPERMAIL_WATCH_ENABLED=true` or config.
|
|
14
|
+
> Works in both stdio and HTTP transport modes.
|
|
15
|
+
>
|
|
6
16
|
> **v0.6.3** — Unify stdio and HTTP modes into a single feature set. Removed
|
|
7
17
|
> email watch (inbox polling, SSE push, notification buffer), agent
|
|
8
18
|
> multi-tenancy (`agents.yaml`, `x-api-key` auth, per-agent allowlists), and
|
|
@@ -149,6 +159,14 @@ looks for it in the same directory as `cli.js`.
|
|
|
149
159
|
},
|
|
150
160
|
"providers": {
|
|
151
161
|
"outlook": { "clientId": "...", "tenantId": "..." }
|
|
162
|
+
},
|
|
163
|
+
"watch": {
|
|
164
|
+
"enabled": true,
|
|
165
|
+
"pollIntervalSeconds": 10,
|
|
166
|
+
"webhook": {
|
|
167
|
+
"url": "http://your-agent:3000/api/email-webhook",
|
|
168
|
+
"retry": { "maxAttempts": 5, "baseDelayMs": 1000 }
|
|
169
|
+
}
|
|
152
170
|
}
|
|
153
171
|
}
|
|
154
172
|
```
|
|
@@ -157,6 +175,33 @@ Per-tool filtering (`tools.enabled` / `tools.disabled`) lets operators ship
|
|
|
157
175
|
minimal agent-facing surfaces — e.g. a read-only assistant that can only list
|
|
158
176
|
and read emails.
|
|
159
177
|
|
|
178
|
+
## Environment Variables
|
|
179
|
+
|
|
180
|
+
Every config field can be set via a dedicated `HYPERMAIL_*` env var, following
|
|
181
|
+
a dotted-path naming convention (`HYPERMAIL_HTTP_PORT`,
|
|
182
|
+
`HYPERMAIL_PROVIDERS_OUTLOOK_CLIENT_ID`, etc.). Legacy env vars
|
|
183
|
+
(`MS_CLIENT_ID`, `MS_TENANT_ID`, `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`)
|
|
184
|
+
still work as fallbacks for backward compatibility.
|
|
185
|
+
|
|
186
|
+
| Env var | Config path | Type |
|
|
187
|
+
| --- | --- | --- |
|
|
188
|
+
| `HYPERMAIL_HTTP_ENABLED` | `http.enabled` | `bool` |
|
|
189
|
+
| `HYPERMAIL_HTTP_PORT` | `http.port` | `int` |
|
|
190
|
+
| `HYPERMAIL_HTTP_HOST` | `http.host` | `string` |
|
|
191
|
+
| `HYPERMAIL_TOOLS_ENABLED` | `tools.enabled` | comma-sep strings |
|
|
192
|
+
| `HYPERMAIL_TOOLS_DISABLED` | `tools.disabled` | comma-sep strings |
|
|
193
|
+
| `HYPERMAIL_PROVIDERS_OUTLOOK_CLIENT_ID` | `providers.outlook.clientId` | `string` |
|
|
194
|
+
| `HYPERMAIL_PROVIDERS_OUTLOOK_TENANT_ID` | `providers.outlook.tenantId` | `string` |
|
|
195
|
+
| `HYPERMAIL_PROVIDERS_GMAIL_CLIENT_ID` | `providers.gmail.clientId` | `string` |
|
|
196
|
+
| `HYPERMAIL_PROVIDERS_GMAIL_CLIENT_SECRET` | `providers.gmail.clientSecret` | `string` |
|
|
197
|
+
| `HYPERMAIL_WATCH_ENABLED` | `watch.enabled` | `bool` |
|
|
198
|
+
| `HYPERMAIL_WATCH_POLL_INTERVAL` | `watch.pollIntervalSeconds` | `int` |
|
|
199
|
+
| `HYPERMAIL_WATCH_WEBHOOK_URL` | `watch.webhook.url` | `string` |
|
|
200
|
+
| `HYPERMAIL_WATCH_WEBHOOK_RETRY_MAX_ATTEMPTS` | `watch.webhook.retry.maxAttempts` | `int` |
|
|
201
|
+
| `HYPERMAIL_WATCH_WEBHOOK_RETRY_BASE_DELAY_MS` | `watch.webhook.retry.baseDelayMs` | `int` |
|
|
202
|
+
|
|
203
|
+
**Priority order:** CLI flags > config file > `HYPERMAIL_*` env var > hardcoded default.
|
|
204
|
+
|
|
160
205
|
## Tools
|
|
161
206
|
|
|
162
207
|
All "email" tools take an `account` argument — the email address of the inbox
|
|
@@ -190,6 +235,49 @@ account store.
|
|
|
190
235
|
| `mark_read` | `account`, `id` | Mark a message as read. Disabled under `--read-only`. |
|
|
191
236
|
| `mark_unread` | `account`, `id` | Mark a message as unread. Disabled under `--read-only`. |
|
|
192
237
|
|
|
238
|
+
## Email Watch
|
|
239
|
+
|
|
240
|
+
When enabled, hypermail-mcp runs a background poll loop that scans inboxes for
|
|
241
|
+
new messages and POSTs each one to a configurable webhook URL. Intended for
|
|
242
|
+
push-based email triage — downstream agents (e.g. Mastra) receive full email
|
|
243
|
+
content without polling.
|
|
244
|
+
|
|
245
|
+
```jsonc
|
|
246
|
+
{
|
|
247
|
+
"watch": {
|
|
248
|
+
"enabled": true,
|
|
249
|
+
"pollIntervalSeconds": 10,
|
|
250
|
+
"webhook": {
|
|
251
|
+
"url": "http://localhost:3000/api/email-webhook",
|
|
252
|
+
"retry": { "maxAttempts": 5, "baseDelayMs": 1000 }
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
| Setting | Default | Notes |
|
|
259
|
+
| --- | --- | --- |
|
|
260
|
+
| `watch.enabled` | `false` | Toggle via config or `HYPERMAIL_WATCH_ENABLED=true` env var |
|
|
261
|
+
| `watch.pollIntervalSeconds` | `10` | Min 10s, max 3600s |
|
|
262
|
+
| `watch.webhook.url` | — | Endpoint that receives `POST` with `EmailFull` JSON |
|
|
263
|
+
| `watch.webhook.retry.maxAttempts` | `5` | Max delivery attempts (1–10) |
|
|
264
|
+
| `watch.webhook.retry.baseDelayMs` | `1000` | Base backoff delay (× 2^attempt) |
|
|
265
|
+
|
|
266
|
+
**Behavior:**
|
|
267
|
+
- Polls **all accounts** in the store, **inbox only**.
|
|
268
|
+
- Detects new emails via `lastSeenIds` (capped at 200) stored in the encrypted
|
|
269
|
+
account file — no duplicate emits across restarts.
|
|
270
|
+
- One `POST` per email (full body: subject, sender, text, HTML, attachments
|
|
271
|
+
metadata, thread ID via `EmailFull`).
|
|
272
|
+
- Delivery uses exponential backoff (`baseDelay × 2^attempt`). Retries on
|
|
273
|
+
non-2xx responses and connection errors. Logs and moves on after
|
|
274
|
+
`maxAttempts` exhausted — never blocks the poll loop.
|
|
275
|
+
- Works in both **stdio** and **HTTP** transport modes — the poll interval
|
|
276
|
+
fires normally alongside MCP message handling.
|
|
277
|
+
|
|
278
|
+
**Rate limits:** Polling every 10s on a single inbox = 6 req/min = 0.6% of
|
|
279
|
+
Microsoft Graph's 10,000 req/10min per-user limit. Safe for personal inboxes.
|
|
280
|
+
|
|
193
281
|
## Add-account flow (Outlook)
|
|
194
282
|
|
|
195
283
|
1. Agent calls `add_account({ provider: "outlook" })`.
|
|
@@ -240,6 +328,10 @@ src/
|
|
|
240
328
|
client.ts # Gmail API (googleapis)
|
|
241
329
|
index.ts # GmailProvider implementation
|
|
242
330
|
shared/ # shared utilities across providers
|
|
331
|
+
watcher/
|
|
332
|
+
manager.ts # WatcherManager — inbox poll loop + dedup
|
|
333
|
+
webhook.ts # HTTP POST with exponential backoff retry
|
|
334
|
+
index.ts # barrel export
|
|
243
335
|
tools/
|
|
244
336
|
index.ts # MCP tool registrations
|
|
245
337
|
accounts.ts # list/add/remove/complete-add account tools
|
package/dist/cli.js
CHANGED
|
@@ -52,8 +52,6 @@ function decrypt(buf, key) {
|
|
|
52
52
|
}
|
|
53
53
|
function resolveDataDir(explicit) {
|
|
54
54
|
if (explicit && explicit.length > 0) return path.resolve(explicit);
|
|
55
|
-
const env = process.env.HYPERMAIL_MCP_DATA_DIR;
|
|
56
|
-
if (env && env.length > 0) return path.resolve(env);
|
|
57
55
|
return path.join(homedir(), ".hypermail-mcp");
|
|
58
56
|
}
|
|
59
57
|
function parseEnvKey(raw) {
|
|
@@ -219,8 +217,8 @@ function isSerializedTokens(obj) {
|
|
|
219
217
|
return typeof o.msalCache === "string" && typeof o.homeAccountId === "string" && typeof o.tenantId === "string" && typeof o.username === "string" && Array.isArray(o.scopes);
|
|
220
218
|
}
|
|
221
219
|
function makeConfig(prevCacheJson, clientIdOverride, tenantOverride) {
|
|
222
|
-
const clientId = clientIdOverride ||
|
|
223
|
-
const tenant = tenantOverride ||
|
|
220
|
+
const clientId = clientIdOverride || DEFAULT_CLIENT_ID;
|
|
221
|
+
const tenant = tenantOverride || "common";
|
|
224
222
|
return {
|
|
225
223
|
auth: {
|
|
226
224
|
clientId,
|
|
@@ -1602,13 +1600,13 @@ async function getEmailFromToken(accessToken) {
|
|
|
1602
1600
|
return data.emailAddress;
|
|
1603
1601
|
}
|
|
1604
1602
|
function beginDeviceCode2(scopes = DEFAULT_SCOPES2, clientIdOverride, clientSecretOverride) {
|
|
1605
|
-
const clientId = clientIdOverride
|
|
1603
|
+
const clientId = clientIdOverride;
|
|
1606
1604
|
if (!clientId) {
|
|
1607
1605
|
throw new Error(
|
|
1608
|
-
"GOOGLE_CLIENT_ID is required for Gmail OAuth \u2014 set it
|
|
1606
|
+
"GOOGLE_CLIENT_ID is required for Gmail OAuth \u2014 set it via HYPERMAIL_PROVIDERS_GMAIL_CLIENT_ID, GOOGLE_CLIENT_ID, or provider config"
|
|
1609
1607
|
);
|
|
1610
1608
|
}
|
|
1611
|
-
const clientSecret = clientSecretOverride ||
|
|
1609
|
+
const clientSecret = clientSecretOverride || void 0;
|
|
1612
1610
|
let resolve;
|
|
1613
1611
|
let reject;
|
|
1614
1612
|
const result = new Promise(
|
|
@@ -3568,7 +3566,7 @@ function registerTools(server, opts) {
|
|
|
3568
3566
|
// package.json
|
|
3569
3567
|
var package_default = {
|
|
3570
3568
|
name: "hypermail-mcp",
|
|
3571
|
-
version: "0.
|
|
3569
|
+
version: "0.7.1",
|
|
3572
3570
|
description: "Unified email MCP server \u2014 operate any inbox (Outlook now, IMAP/Gmail later) by passing an email address.",
|
|
3573
3571
|
type: "module",
|
|
3574
3572
|
bin: {
|
|
@@ -3638,6 +3636,116 @@ var package_default = {
|
|
|
3638
3636
|
// src/version.ts
|
|
3639
3637
|
var VERSION = package_default.version;
|
|
3640
3638
|
|
|
3639
|
+
// src/watcher/webhook.ts
|
|
3640
|
+
async function postWebhook(email, config) {
|
|
3641
|
+
if (!config.webhook) return false;
|
|
3642
|
+
const { url, retry } = config.webhook;
|
|
3643
|
+
const maxAttempts = retry.maxAttempts;
|
|
3644
|
+
const baseDelayMs = retry.baseDelayMs;
|
|
3645
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
3646
|
+
if (attempt > 0) {
|
|
3647
|
+
const delay = baseDelayMs * 2 ** (attempt - 1);
|
|
3648
|
+
await sleep(delay);
|
|
3649
|
+
}
|
|
3650
|
+
try {
|
|
3651
|
+
const res = await fetch(url, {
|
|
3652
|
+
method: "POST",
|
|
3653
|
+
headers: { "content-type": "application/json" },
|
|
3654
|
+
body: JSON.stringify(email)
|
|
3655
|
+
});
|
|
3656
|
+
if (res.ok) return true;
|
|
3657
|
+
console.error(
|
|
3658
|
+
`[hypermail-watch] webhook POST ${email.id} attempt ${attempt + 1}/${maxAttempts}: HTTP ${res.status}`
|
|
3659
|
+
);
|
|
3660
|
+
} catch (err) {
|
|
3661
|
+
const code = err.code ?? "";
|
|
3662
|
+
console.error(
|
|
3663
|
+
`[hypermail-watch] webhook POST ${email.id} attempt ${attempt + 1}/${maxAttempts}: ${code || String(err)}`
|
|
3664
|
+
);
|
|
3665
|
+
}
|
|
3666
|
+
}
|
|
3667
|
+
console.error(
|
|
3668
|
+
`[hypermail-watch] webhook delivery failed after ${maxAttempts} retries for ${email.id}`
|
|
3669
|
+
);
|
|
3670
|
+
return false;
|
|
3671
|
+
}
|
|
3672
|
+
function sleep(ms) {
|
|
3673
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
3674
|
+
}
|
|
3675
|
+
|
|
3676
|
+
// src/watcher/manager.ts
|
|
3677
|
+
var WatcherManager = class {
|
|
3678
|
+
constructor(store, registry, config) {
|
|
3679
|
+
this.store = store;
|
|
3680
|
+
this.registry = registry;
|
|
3681
|
+
this.config = config;
|
|
3682
|
+
}
|
|
3683
|
+
store;
|
|
3684
|
+
registry;
|
|
3685
|
+
config;
|
|
3686
|
+
intervalId = null;
|
|
3687
|
+
/** Start the poll loop. Fires immediately on the first tick, then every
|
|
3688
|
+
* `pollIntervalSeconds`. Safe to call multiple times — subsequent calls
|
|
3689
|
+
* are no-ops. */
|
|
3690
|
+
start() {
|
|
3691
|
+
if (this.intervalId !== null) return;
|
|
3692
|
+
this.poll();
|
|
3693
|
+
this.intervalId = setInterval(
|
|
3694
|
+
() => this.poll(),
|
|
3695
|
+
this.config.pollIntervalSeconds * 1e3
|
|
3696
|
+
);
|
|
3697
|
+
}
|
|
3698
|
+
/** Stop the poll loop and release the interval. Safe to call when already
|
|
3699
|
+
* stopped. */
|
|
3700
|
+
stop() {
|
|
3701
|
+
if (this.intervalId !== null) {
|
|
3702
|
+
clearInterval(this.intervalId);
|
|
3703
|
+
this.intervalId = null;
|
|
3704
|
+
}
|
|
3705
|
+
}
|
|
3706
|
+
// ── private ──
|
|
3707
|
+
async poll() {
|
|
3708
|
+
const accounts = this.store.listAccounts();
|
|
3709
|
+
for (const acct of accounts) {
|
|
3710
|
+
try {
|
|
3711
|
+
await this.pollAccount(acct.email);
|
|
3712
|
+
} catch (err) {
|
|
3713
|
+
console.error(
|
|
3714
|
+
`[hypermail-watch] poll failed for ${acct.email}:`,
|
|
3715
|
+
err
|
|
3716
|
+
);
|
|
3717
|
+
}
|
|
3718
|
+
}
|
|
3719
|
+
}
|
|
3720
|
+
async pollAccount(email) {
|
|
3721
|
+
const { provider, account } = this.registry.resolveByEmail(email);
|
|
3722
|
+
const result = await provider.listEmails(account, {
|
|
3723
|
+
folder: "inbox",
|
|
3724
|
+
limit: 50
|
|
3725
|
+
});
|
|
3726
|
+
const knownIds = [...account.lastSeenIds ?? []];
|
|
3727
|
+
const newEmails = result.items.filter((e) => !knownIds.includes(e.id));
|
|
3728
|
+
if (newEmails.length === 0) return;
|
|
3729
|
+
for (const summary of newEmails) {
|
|
3730
|
+
try {
|
|
3731
|
+
const full = await provider.readEmail(account, summary.id);
|
|
3732
|
+
await this.emit(full);
|
|
3733
|
+
knownIds.unshift(summary.id);
|
|
3734
|
+
} catch (err) {
|
|
3735
|
+
console.error(
|
|
3736
|
+
`[hypermail-watch] emission failed for ${email}/${summary.id}:`,
|
|
3737
|
+
err
|
|
3738
|
+
);
|
|
3739
|
+
}
|
|
3740
|
+
}
|
|
3741
|
+
const capped = knownIds.slice(0, 200);
|
|
3742
|
+
await this.store.upsertAccount({ ...account, lastSeenIds: capped });
|
|
3743
|
+
}
|
|
3744
|
+
async emit(full) {
|
|
3745
|
+
await postWebhook(full, this.config);
|
|
3746
|
+
}
|
|
3747
|
+
};
|
|
3748
|
+
|
|
3641
3749
|
// src/config.ts
|
|
3642
3750
|
import { readFileSync } from "fs";
|
|
3643
3751
|
import { z as z7 } from "zod";
|
|
@@ -3663,8 +3771,15 @@ var providersConfigSchema = z7.object({
|
|
|
3663
3771
|
gmail: gmailProviderSchema.optional()
|
|
3664
3772
|
});
|
|
3665
3773
|
var watchConfigSchema = z7.object({
|
|
3666
|
-
enabled: z7.boolean().default(
|
|
3667
|
-
pollIntervalSeconds: z7.number().int().min(10).max(3600).default(
|
|
3774
|
+
enabled: z7.boolean().default(false),
|
|
3775
|
+
pollIntervalSeconds: z7.number().int().min(10).max(3600).default(10),
|
|
3776
|
+
webhook: z7.object({
|
|
3777
|
+
url: z7.string(),
|
|
3778
|
+
retry: z7.object({
|
|
3779
|
+
maxAttempts: z7.number().int().min(1).max(10).default(5),
|
|
3780
|
+
baseDelayMs: z7.number().int().min(100).default(1e3)
|
|
3781
|
+
}).optional()
|
|
3782
|
+
}).optional()
|
|
3668
3783
|
});
|
|
3669
3784
|
var rawConfigSchema = z7.object({
|
|
3670
3785
|
dataDir: z7.string().optional(),
|
|
@@ -3700,6 +3815,24 @@ var KNOWN_TOOLS = [
|
|
|
3700
3815
|
"add_attachment_to_draft",
|
|
3701
3816
|
"check_notifications"
|
|
3702
3817
|
];
|
|
3818
|
+
var ENV_HTTP_ENABLED = "HYPERMAIL_HTTP_ENABLED";
|
|
3819
|
+
var ENV_HTTP_PORT = "HYPERMAIL_HTTP_PORT";
|
|
3820
|
+
var ENV_HTTP_HOST = "HYPERMAIL_HTTP_HOST";
|
|
3821
|
+
var ENV_TOOLS_DISABLED = "HYPERMAIL_TOOLS_DISABLED";
|
|
3822
|
+
var ENV_TOOLS_ENABLED = "HYPERMAIL_TOOLS_ENABLED";
|
|
3823
|
+
var ENV_OUTLOOK_CLIENT_ID = "HYPERMAIL_PROVIDERS_OUTLOOK_CLIENT_ID";
|
|
3824
|
+
var ENV_OUTLOOK_TENANT_ID = "HYPERMAIL_PROVIDERS_OUTLOOK_TENANT_ID";
|
|
3825
|
+
var ENV_GMAIL_CLIENT_ID = "HYPERMAIL_PROVIDERS_GMAIL_CLIENT_ID";
|
|
3826
|
+
var ENV_GMAIL_CLIENT_SECRET = "HYPERMAIL_PROVIDERS_GMAIL_CLIENT_SECRET";
|
|
3827
|
+
var ENV_WATCH_ENABLED = "HYPERMAIL_WATCH_ENABLED";
|
|
3828
|
+
var ENV_WATCH_POLL_INTERVAL = "HYPERMAIL_WATCH_POLL_INTERVAL";
|
|
3829
|
+
var ENV_WATCH_WEBHOOK_URL = "HYPERMAIL_WATCH_WEBHOOK_URL";
|
|
3830
|
+
var ENV_WATCH_WEBHOOK_RETRY_MAX_ATTEMPTS = "HYPERMAIL_WATCH_WEBHOOK_RETRY_MAX_ATTEMPTS";
|
|
3831
|
+
var ENV_WATCH_WEBHOOK_RETRY_BASE_DELAY = "HYPERMAIL_WATCH_WEBHOOK_RETRY_BASE_DELAY_MS";
|
|
3832
|
+
var LEGACY_MS_CLIENT_ID = "MS_CLIENT_ID";
|
|
3833
|
+
var LEGACY_MS_TENANT_ID = "MS_TENANT_ID";
|
|
3834
|
+
var LEGACY_GOOGLE_CLIENT_ID = "GOOGLE_CLIENT_ID";
|
|
3835
|
+
var LEGACY_GOOGLE_CLIENT_SECRET = "GOOGLE_CLIENT_SECRET";
|
|
3703
3836
|
var ENV_VAR_RE = /\$\{([A-Z_][A-Z0-9_]*)\}/g;
|
|
3704
3837
|
function resolveEnvVars(value) {
|
|
3705
3838
|
return value.replace(ENV_VAR_RE, (_match, name) => {
|
|
@@ -3718,6 +3851,26 @@ function deepResolve(obj) {
|
|
|
3718
3851
|
}
|
|
3719
3852
|
return obj;
|
|
3720
3853
|
}
|
|
3854
|
+
function parseBool(value) {
|
|
3855
|
+
if (value === void 0) return void 0;
|
|
3856
|
+
const lower = value.trim().toLowerCase();
|
|
3857
|
+
if (lower === "true" || lower === "1" || lower === "yes") return true;
|
|
3858
|
+
if (lower === "false" || lower === "0" || lower === "no" || lower === "") return false;
|
|
3859
|
+
return void 0;
|
|
3860
|
+
}
|
|
3861
|
+
function parseIntSafe(value) {
|
|
3862
|
+
if (value === void 0) return void 0;
|
|
3863
|
+
const trimmed = value.trim();
|
|
3864
|
+
if (trimmed === "") return void 0;
|
|
3865
|
+
const n = Number.parseInt(trimmed, 10);
|
|
3866
|
+
return Number.isNaN(n) ? void 0 : n;
|
|
3867
|
+
}
|
|
3868
|
+
function parseStringArray(value) {
|
|
3869
|
+
if (value === void 0) return void 0;
|
|
3870
|
+
const trimmed = value.trim();
|
|
3871
|
+
if (trimmed === "") return [];
|
|
3872
|
+
return trimmed.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
3873
|
+
}
|
|
3721
3874
|
function validateToolNames(toolNames, listName) {
|
|
3722
3875
|
if (!toolNames || toolNames.length === 0) return;
|
|
3723
3876
|
const known = new Set(KNOWN_TOOLS);
|
|
@@ -3741,30 +3894,69 @@ function loadConfig(configPath, cliOverrides = {}) {
|
|
|
3741
3894
|
}
|
|
3742
3895
|
raw = deepResolve(raw);
|
|
3743
3896
|
const parsed = rawConfigSchema.parse(raw);
|
|
3744
|
-
|
|
3745
|
-
|
|
3746
|
-
|
|
3747
|
-
|
|
3748
|
-
|
|
3897
|
+
const http = {
|
|
3898
|
+
enabled: cliOverrides.http ?? parsed.http?.enabled ?? parseBool(process.env[ENV_HTTP_ENABLED]) ?? false,
|
|
3899
|
+
port: cliOverrides.port ?? parsed.http?.port ?? parseIntSafe(process.env[ENV_HTTP_PORT]) ?? 3e3,
|
|
3900
|
+
host: cliOverrides.host ?? parsed.http?.host ?? process.env[ENV_HTTP_HOST] ?? "127.0.0.1"
|
|
3901
|
+
};
|
|
3902
|
+
const toolsDisabled = parsed.tools?.disabled ?? parseStringArray(process.env[ENV_TOOLS_DISABLED]);
|
|
3903
|
+
const toolsEnabled = parsed.tools?.enabled ?? parseStringArray(process.env[ENV_TOOLS_ENABLED]);
|
|
3904
|
+
if (toolsDisabled && toolsEnabled) {
|
|
3905
|
+
throw new Error(
|
|
3906
|
+
"tools.disabled and tools.enabled are mutually exclusive \u2014 use one or the other"
|
|
3907
|
+
);
|
|
3908
|
+
}
|
|
3909
|
+
if (toolsEnabled !== void 0 && toolsEnabled.length === 0) {
|
|
3910
|
+
throw new Error(
|
|
3911
|
+
"tools.enabled is empty \u2014 at least one tool must be listed. To enable all tools, omit the tools section entirely."
|
|
3912
|
+
);
|
|
3913
|
+
}
|
|
3914
|
+
validateToolNames(toolsDisabled, "tools.disabled");
|
|
3915
|
+
validateToolNames(toolsEnabled, "tools.enabled");
|
|
3916
|
+
const tools = toolsDisabled || toolsEnabled ? { disabled: toolsDisabled, enabled: toolsEnabled } : void 0;
|
|
3917
|
+
const outlookClientId = parsed.providers?.outlook?.clientId ?? process.env[ENV_OUTLOOK_CLIENT_ID] ?? process.env[LEGACY_MS_CLIENT_ID];
|
|
3918
|
+
const outlookTenantId = parsed.providers?.outlook?.tenantId ?? process.env[ENV_OUTLOOK_TENANT_ID] ?? process.env[LEGACY_MS_TENANT_ID];
|
|
3919
|
+
const gmailClientId = parsed.providers?.gmail?.clientId ?? process.env[ENV_GMAIL_CLIENT_ID] ?? process.env[LEGACY_GOOGLE_CLIENT_ID];
|
|
3920
|
+
const gmailClientSecret = parsed.providers?.gmail?.clientSecret ?? process.env[ENV_GMAIL_CLIENT_SECRET] ?? process.env[LEGACY_GOOGLE_CLIENT_SECRET];
|
|
3921
|
+
let providers;
|
|
3922
|
+
if (outlookClientId || outlookTenantId || gmailClientId || gmailClientSecret) {
|
|
3923
|
+
providers = {};
|
|
3924
|
+
if (outlookClientId || outlookTenantId) {
|
|
3925
|
+
providers.outlook = {};
|
|
3926
|
+
if (outlookClientId) providers.outlook.clientId = outlookClientId;
|
|
3927
|
+
if (outlookTenantId) providers.outlook.tenantId = outlookTenantId;
|
|
3749
3928
|
}
|
|
3750
|
-
if (
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
);
|
|
3929
|
+
if (gmailClientId || gmailClientSecret) {
|
|
3930
|
+
providers.gmail = {};
|
|
3931
|
+
if (gmailClientId) providers.gmail.clientId = gmailClientId;
|
|
3932
|
+
if (gmailClientSecret) providers.gmail.clientSecret = gmailClientSecret;
|
|
3754
3933
|
}
|
|
3755
|
-
validateToolNames(parsed.tools.disabled, "tools.disabled");
|
|
3756
|
-
validateToolNames(parsed.tools.enabled, "tools.enabled");
|
|
3757
3934
|
}
|
|
3758
|
-
const
|
|
3759
|
-
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
|
|
3935
|
+
const watchEnabledEnv = parseBool(process.env[ENV_WATCH_ENABLED]);
|
|
3936
|
+
let watch;
|
|
3937
|
+
if (parsed.watch || watchEnabledEnv !== void 0) {
|
|
3938
|
+
const webhookUrl = parsed.watch?.webhook?.url ?? process.env[ENV_WATCH_WEBHOOK_URL];
|
|
3939
|
+
let webhook;
|
|
3940
|
+
if (webhookUrl) {
|
|
3941
|
+
const retryMaxAttempts = parsed.watch?.webhook?.retry?.maxAttempts ?? parseIntSafe(process.env[ENV_WATCH_WEBHOOK_RETRY_MAX_ATTEMPTS]) ?? 5;
|
|
3942
|
+
const retryBaseDelayMs = parsed.watch?.webhook?.retry?.baseDelayMs ?? parseIntSafe(process.env[ENV_WATCH_WEBHOOK_RETRY_BASE_DELAY]) ?? 1e3;
|
|
3943
|
+
webhook = {
|
|
3944
|
+
url: webhookUrl,
|
|
3945
|
+
retry: { maxAttempts: retryMaxAttempts, baseDelayMs: retryBaseDelayMs }
|
|
3946
|
+
};
|
|
3947
|
+
}
|
|
3948
|
+
watch = {
|
|
3949
|
+
enabled: watchEnabledEnv ?? parsed.watch?.enabled ?? false,
|
|
3950
|
+
pollIntervalSeconds: parsed.watch?.pollIntervalSeconds ?? parseIntSafe(process.env[ENV_WATCH_POLL_INTERVAL]) ?? 10,
|
|
3951
|
+
webhook
|
|
3952
|
+
};
|
|
3953
|
+
}
|
|
3763
3954
|
return {
|
|
3764
3955
|
dataDir: cliOverrides.dataDir ?? parsed.dataDir ?? process.env.HYPERMAIL_MCP_DATA_DIR,
|
|
3765
3956
|
http,
|
|
3766
|
-
tools
|
|
3767
|
-
providers
|
|
3957
|
+
tools,
|
|
3958
|
+
providers,
|
|
3959
|
+
watch
|
|
3768
3960
|
};
|
|
3769
3961
|
}
|
|
3770
3962
|
function resolveTools(config) {
|
|
@@ -3783,6 +3975,14 @@ async function startServer(opts) {
|
|
|
3783
3975
|
const store = await AccountStore.open({ dataDir: config.dataDir });
|
|
3784
3976
|
const registry = buildRegistry({ store, providers: config.providers });
|
|
3785
3977
|
const tools = resolveTools(config);
|
|
3978
|
+
let watcher;
|
|
3979
|
+
if (config.watch?.enabled) {
|
|
3980
|
+
watcher = new WatcherManager(store, registry, config.watch);
|
|
3981
|
+
watcher.start();
|
|
3982
|
+
const stop = () => watcher?.stop();
|
|
3983
|
+
process.on("SIGTERM", stop);
|
|
3984
|
+
process.on("SIGINT", stop);
|
|
3985
|
+
}
|
|
3786
3986
|
const createServer = () => {
|
|
3787
3987
|
const s = new McpServer(
|
|
3788
3988
|
{ name: "hypermail-mcp", version: VERSION },
|
|
@@ -3897,19 +4097,51 @@ Options:
|
|
|
3897
4097
|
-h, --help Show this help
|
|
3898
4098
|
|
|
3899
4099
|
Configuration:
|
|
3900
|
-
All
|
|
3901
|
-
|
|
4100
|
+
All settings can be provided via environment variables \u2014 no config file
|
|
4101
|
+
required. Use hypermail-config.json for advanced scenarios.
|
|
3902
4102
|
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
|
|
3907
|
-
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
|
|
4103
|
+
Environment variables:
|
|
4104
|
+
|
|
4105
|
+
HYPERMAIL_MCP_DATA_DIR Data directory (string)
|
|
4106
|
+
HYPERMAIL_HTTP_ENABLED Enable HTTP mode (bool: true/false/1/0)
|
|
4107
|
+
HYPERMAIL_HTTP_PORT HTTP port (number)
|
|
4108
|
+
HYPERMAIL_HTTP_HOST HTTP bind address (string)
|
|
4109
|
+
HYPERMAIL_TOOLS_DISABLED Comma-separated tool names to disable
|
|
4110
|
+
HYPERMAIL_TOOLS_ENABLED Comma-separated tool names to enable
|
|
4111
|
+
HYPERMAIL_PROVIDERS_OUTLOOK_CLIENT_ID Outlook OAuth client ID (string)
|
|
4112
|
+
HYPERMAIL_PROVIDERS_OUTLOOK_TENANT_ID Outlook tenant ID (string)
|
|
4113
|
+
HYPERMAIL_PROVIDERS_GMAIL_CLIENT_ID Gmail OAuth client ID (string)
|
|
4114
|
+
HYPERMAIL_PROVIDERS_GMAIL_CLIENT_SECRET Gmail OAuth client secret (string)
|
|
4115
|
+
HYPERMAIL_WATCH_ENABLED Enable inbox polling (bool)
|
|
4116
|
+
HYPERMAIL_WATCH_POLL_INTERVAL Poll interval in seconds (number)
|
|
4117
|
+
HYPERMAIL_WATCH_WEBHOOK_URL Webhook URL for new-email events (string)
|
|
4118
|
+
HYPERMAIL_WATCH_WEBHOOK_RETRY_MAX_ATTEMPTS Retry max attempts (number)
|
|
4119
|
+
HYPERMAIL_WATCH_WEBHOOK_RETRY_BASE_DELAY_MS Retry base delay ms (number)
|
|
4120
|
+
HYPERMAIL_MCP_KEY Encryption master key (hex or base64)
|
|
4121
|
+
|
|
4122
|
+
Legacy env vars (still supported):
|
|
4123
|
+
MS_CLIENT_ID, MS_TENANT_ID, GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET
|
|
4124
|
+
|
|
4125
|
+
Priority: CLI flags > config file > env vars > defaults.
|
|
4126
|
+
|
|
4127
|
+
Example (env-only, no config file):
|
|
4128
|
+
HYPERMAIL_HTTP_ENABLED=true \\
|
|
4129
|
+
HYPERMAIL_HTTP_PORT=8080 \\
|
|
4130
|
+
HYPERMAIL_PROVIDERS_OUTLOOK_CLIENT_ID=abc123 \\
|
|
4131
|
+
HYPERMAIL_PROVIDERS_OUTLOOK_TENANT_ID=common \\
|
|
4132
|
+
HYPERMAIL_MCP_DATA_DIR=/data/hypermail \\
|
|
4133
|
+
hypermail-mcp --http
|
|
4134
|
+
|
|
4135
|
+
Example hypermail-config.json:
|
|
4136
|
+
{
|
|
4137
|
+
"dataDir": "/path/to/data",
|
|
4138
|
+
"http": { "enabled": false },
|
|
4139
|
+
"tools": { "disabled": ["send_email"] },
|
|
4140
|
+
"providers": {
|
|
4141
|
+
"outlook": { "clientId": "\${MS_CLIENT_ID}", "tenantId": "\${MS_TENANT_ID}" },
|
|
4142
|
+
"gmail": { "clientId": "\${GOOGLE_CLIENT_ID}", "clientSecret": "\${GOOGLE_CLIENT_SECRET}" }
|
|
4143
|
+
}
|
|
3911
4144
|
}
|
|
3912
|
-
}
|
|
3913
4145
|
`;
|
|
3914
4146
|
process.stdout.write(msg);
|
|
3915
4147
|
}
|