hypermail-mcp 0.7.0 → 0.7.2
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 +36 -0
- package/dist/cli.js +200 -52
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -3,6 +3,15 @@
|
|
|
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.2** — `add_attachment_to_draft` now accepts `filePath` (absolute path to
|
|
7
|
+
a local file) as an alternative to `contentBytes`. The file is read from disk and
|
|
8
|
+
base64-encoded automatically, the MIME type is inferred from the extension, and
|
|
9
|
+
`name` defaults to the file's basename.
|
|
10
|
+
>> **v0.7.1** — Every config field is now settable via a dedicated
|
|
11
|
+
> `HYPERMAIL_*` env var. Legacy env vars (`MS_CLIENT_ID`, `MS_TENANT_ID`,
|
|
12
|
+
> `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`) still work as fallbacks. See
|
|
13
|
+
> [Environment Variables](#environment-variables) for the full reference.
|
|
14
|
+
>
|
|
6
15
|
> **v0.7.0** — Email watch mode: background poll loop detects new inbox
|
|
7
16
|
> messages and POSTs them to a configurable webhook URL (e.g. Mastra). Opt-in —
|
|
8
17
|
> disabled by default, enabled via `HYPERMAIL_WATCH_ENABLED=true` or config.
|
|
@@ -170,6 +179,33 @@ Per-tool filtering (`tools.enabled` / `tools.disabled`) lets operators ship
|
|
|
170
179
|
minimal agent-facing surfaces — e.g. a read-only assistant that can only list
|
|
171
180
|
and read emails.
|
|
172
181
|
|
|
182
|
+
## Environment Variables
|
|
183
|
+
|
|
184
|
+
Every config field can be set via a dedicated `HYPERMAIL_*` env var, following
|
|
185
|
+
a dotted-path naming convention (`HYPERMAIL_HTTP_PORT`,
|
|
186
|
+
`HYPERMAIL_PROVIDERS_OUTLOOK_CLIENT_ID`, etc.). Legacy env vars
|
|
187
|
+
(`MS_CLIENT_ID`, `MS_TENANT_ID`, `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`)
|
|
188
|
+
still work as fallbacks for backward compatibility.
|
|
189
|
+
|
|
190
|
+
| Env var | Config path | Type |
|
|
191
|
+
| --- | --- | --- |
|
|
192
|
+
| `HYPERMAIL_HTTP_ENABLED` | `http.enabled` | `bool` |
|
|
193
|
+
| `HYPERMAIL_HTTP_PORT` | `http.port` | `int` |
|
|
194
|
+
| `HYPERMAIL_HTTP_HOST` | `http.host` | `string` |
|
|
195
|
+
| `HYPERMAIL_TOOLS_ENABLED` | `tools.enabled` | comma-sep strings |
|
|
196
|
+
| `HYPERMAIL_TOOLS_DISABLED` | `tools.disabled` | comma-sep strings |
|
|
197
|
+
| `HYPERMAIL_PROVIDERS_OUTLOOK_CLIENT_ID` | `providers.outlook.clientId` | `string` |
|
|
198
|
+
| `HYPERMAIL_PROVIDERS_OUTLOOK_TENANT_ID` | `providers.outlook.tenantId` | `string` |
|
|
199
|
+
| `HYPERMAIL_PROVIDERS_GMAIL_CLIENT_ID` | `providers.gmail.clientId` | `string` |
|
|
200
|
+
| `HYPERMAIL_PROVIDERS_GMAIL_CLIENT_SECRET` | `providers.gmail.clientSecret` | `string` |
|
|
201
|
+
| `HYPERMAIL_WATCH_ENABLED` | `watch.enabled` | `bool` |
|
|
202
|
+
| `HYPERMAIL_WATCH_POLL_INTERVAL` | `watch.pollIntervalSeconds` | `int` |
|
|
203
|
+
| `HYPERMAIL_WATCH_WEBHOOK_URL` | `watch.webhook.url` | `string` |
|
|
204
|
+
| `HYPERMAIL_WATCH_WEBHOOK_RETRY_MAX_ATTEMPTS` | `watch.webhook.retry.maxAttempts` | `int` |
|
|
205
|
+
| `HYPERMAIL_WATCH_WEBHOOK_RETRY_BASE_DELAY_MS` | `watch.webhook.retry.baseDelayMs` | `int` |
|
|
206
|
+
|
|
207
|
+
**Priority order:** CLI flags > config file > `HYPERMAIL_*` env var > hardcoded default.
|
|
208
|
+
|
|
173
209
|
## Tools
|
|
174
210
|
|
|
175
211
|
All "email" tools take an `account` argument — the email address of the inbox
|
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(
|
|
@@ -3303,6 +3301,8 @@ function registerOrganizeTools(server, ctx) {
|
|
|
3303
3301
|
|
|
3304
3302
|
// src/tools/compose.ts
|
|
3305
3303
|
import { z as z6 } from "zod";
|
|
3304
|
+
import { readFileSync } from "fs";
|
|
3305
|
+
import { basename, extname } from "path";
|
|
3306
3306
|
function registerComposeTools(server, ctx) {
|
|
3307
3307
|
const { store, registry, tools } = ctx;
|
|
3308
3308
|
const sendEmailSchema = z6.object({
|
|
@@ -3508,6 +3508,31 @@ function registerComposeTools(server, ctx) {
|
|
|
3508
3508
|
}
|
|
3509
3509
|
);
|
|
3510
3510
|
}
|
|
3511
|
+
const MIME_TYPES = {
|
|
3512
|
+
".pdf": "application/pdf",
|
|
3513
|
+
".png": "image/png",
|
|
3514
|
+
".jpg": "image/jpeg",
|
|
3515
|
+
".jpeg": "image/jpeg",
|
|
3516
|
+
".gif": "image/gif",
|
|
3517
|
+
".svg": "image/svg+xml",
|
|
3518
|
+
".webp": "image/webp",
|
|
3519
|
+
".txt": "text/plain",
|
|
3520
|
+
".html": "text/html",
|
|
3521
|
+
".css": "text/css",
|
|
3522
|
+
".csv": "text/csv",
|
|
3523
|
+
".json": "application/json",
|
|
3524
|
+
".xml": "application/xml",
|
|
3525
|
+
".zip": "application/zip",
|
|
3526
|
+
".gz": "application/gzip",
|
|
3527
|
+
".tar": "application/x-tar",
|
|
3528
|
+
".doc": "application/msword",
|
|
3529
|
+
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
3530
|
+
".xls": "application/vnd.ms-excel",
|
|
3531
|
+
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
3532
|
+
".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
3533
|
+
".mp3": "audio/mpeg",
|
|
3534
|
+
".mp4": "video/mp4"
|
|
3535
|
+
};
|
|
3511
3536
|
const addAttachmentOutputSchema = {
|
|
3512
3537
|
attached: z6.literal(true),
|
|
3513
3538
|
id: z6.string(),
|
|
@@ -3521,25 +3546,48 @@ function registerComposeTools(server, ctx) {
|
|
|
3521
3546
|
server.registerTool(
|
|
3522
3547
|
"add_attachment_to_draft",
|
|
3523
3548
|
{
|
|
3524
|
-
description: "Add a file attachment to an existing draft email by ID. `contentBytes`
|
|
3525
|
-
inputSchema: {
|
|
3549
|
+
description: "Add a file attachment to an existing draft email by ID. Provide exactly one of `contentBytes` (base64-encoded content) or `filePath` (absolute path to a local file). When using `filePath`, the file is read from disk and base64-encoded automatically, and `name` defaults to the file's basename if not provided. `contentType` is the MIME type (e.g. 'application/pdf'); when using `filePath` it is inferred from the extension if omitted. Disabled in --read-only mode.",
|
|
3550
|
+
inputSchema: z6.object({
|
|
3526
3551
|
account: z6.string().email(),
|
|
3527
3552
|
id: z6.string().min(1).describe("Draft message ID"),
|
|
3528
|
-
name: z6.string().min(1).describe(
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
3553
|
+
name: z6.string().min(1).optional().describe(
|
|
3554
|
+
"Attachment filename (e.g. 'report.pdf'). Defaults to the file's basename when `filePath` is used."
|
|
3555
|
+
),
|
|
3556
|
+
contentBytes: z6.string().min(1).optional().describe("Base64-encoded file content. Mutually exclusive with `filePath`."),
|
|
3557
|
+
filePath: z6.string().min(1).optional().describe(
|
|
3558
|
+
"Absolute path to a local file. The file is read and base64-encoded automatically. Mutually exclusive with `contentBytes`."
|
|
3559
|
+
),
|
|
3560
|
+
contentType: z6.string().optional().describe("MIME type (e.g. 'application/pdf'). Inferred from `filePath` extension if omitted.")
|
|
3561
|
+
}).refine(
|
|
3562
|
+
(v) => Boolean(v.contentBytes) !== Boolean(v.filePath),
|
|
3563
|
+
{ message: "Provide exactly one of `contentBytes` or `filePath`." }
|
|
3564
|
+
),
|
|
3532
3565
|
outputSchema: addAttachmentOutputSchema
|
|
3533
3566
|
},
|
|
3534
3567
|
async (args) => {
|
|
3535
3568
|
try {
|
|
3536
3569
|
const { provider, account } = registry.resolveByEmail(args.account);
|
|
3570
|
+
let contentBytes;
|
|
3571
|
+
let name;
|
|
3572
|
+
let contentType = args.contentType;
|
|
3573
|
+
if (args.filePath) {
|
|
3574
|
+
const fileData = readFileSync(args.filePath);
|
|
3575
|
+
contentBytes = fileData.toString("base64");
|
|
3576
|
+
name = args.name ?? basename(args.filePath);
|
|
3577
|
+
if (!contentType) {
|
|
3578
|
+
const ext = extname(args.filePath).toLowerCase();
|
|
3579
|
+
contentType = MIME_TYPES[ext] ?? "application/octet-stream";
|
|
3580
|
+
}
|
|
3581
|
+
} else {
|
|
3582
|
+
contentBytes = args.contentBytes;
|
|
3583
|
+
name = args.name;
|
|
3584
|
+
}
|
|
3537
3585
|
const res = await provider.addAttachmentToDraft(
|
|
3538
3586
|
account,
|
|
3539
3587
|
args.id,
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
3588
|
+
name,
|
|
3589
|
+
contentBytes,
|
|
3590
|
+
contentType
|
|
3543
3591
|
);
|
|
3544
3592
|
const data = {
|
|
3545
3593
|
attached: true,
|
|
@@ -3568,7 +3616,7 @@ function registerTools(server, opts) {
|
|
|
3568
3616
|
// package.json
|
|
3569
3617
|
var package_default = {
|
|
3570
3618
|
name: "hypermail-mcp",
|
|
3571
|
-
version: "0.7.
|
|
3619
|
+
version: "0.7.2",
|
|
3572
3620
|
description: "Unified email MCP server \u2014 operate any inbox (Outlook now, IMAP/Gmail later) by passing an email address.",
|
|
3573
3621
|
type: "module",
|
|
3574
3622
|
bin: {
|
|
@@ -3749,7 +3797,7 @@ var WatcherManager = class {
|
|
|
3749
3797
|
};
|
|
3750
3798
|
|
|
3751
3799
|
// src/config.ts
|
|
3752
|
-
import { readFileSync } from "fs";
|
|
3800
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
3753
3801
|
import { z as z7 } from "zod";
|
|
3754
3802
|
var httpConfigSchema = z7.object({
|
|
3755
3803
|
enabled: z7.boolean().default(false),
|
|
@@ -3817,6 +3865,24 @@ var KNOWN_TOOLS = [
|
|
|
3817
3865
|
"add_attachment_to_draft",
|
|
3818
3866
|
"check_notifications"
|
|
3819
3867
|
];
|
|
3868
|
+
var ENV_HTTP_ENABLED = "HYPERMAIL_HTTP_ENABLED";
|
|
3869
|
+
var ENV_HTTP_PORT = "HYPERMAIL_HTTP_PORT";
|
|
3870
|
+
var ENV_HTTP_HOST = "HYPERMAIL_HTTP_HOST";
|
|
3871
|
+
var ENV_TOOLS_DISABLED = "HYPERMAIL_TOOLS_DISABLED";
|
|
3872
|
+
var ENV_TOOLS_ENABLED = "HYPERMAIL_TOOLS_ENABLED";
|
|
3873
|
+
var ENV_OUTLOOK_CLIENT_ID = "HYPERMAIL_PROVIDERS_OUTLOOK_CLIENT_ID";
|
|
3874
|
+
var ENV_OUTLOOK_TENANT_ID = "HYPERMAIL_PROVIDERS_OUTLOOK_TENANT_ID";
|
|
3875
|
+
var ENV_GMAIL_CLIENT_ID = "HYPERMAIL_PROVIDERS_GMAIL_CLIENT_ID";
|
|
3876
|
+
var ENV_GMAIL_CLIENT_SECRET = "HYPERMAIL_PROVIDERS_GMAIL_CLIENT_SECRET";
|
|
3877
|
+
var ENV_WATCH_ENABLED = "HYPERMAIL_WATCH_ENABLED";
|
|
3878
|
+
var ENV_WATCH_POLL_INTERVAL = "HYPERMAIL_WATCH_POLL_INTERVAL";
|
|
3879
|
+
var ENV_WATCH_WEBHOOK_URL = "HYPERMAIL_WATCH_WEBHOOK_URL";
|
|
3880
|
+
var ENV_WATCH_WEBHOOK_RETRY_MAX_ATTEMPTS = "HYPERMAIL_WATCH_WEBHOOK_RETRY_MAX_ATTEMPTS";
|
|
3881
|
+
var ENV_WATCH_WEBHOOK_RETRY_BASE_DELAY = "HYPERMAIL_WATCH_WEBHOOK_RETRY_BASE_DELAY_MS";
|
|
3882
|
+
var LEGACY_MS_CLIENT_ID = "MS_CLIENT_ID";
|
|
3883
|
+
var LEGACY_MS_TENANT_ID = "MS_TENANT_ID";
|
|
3884
|
+
var LEGACY_GOOGLE_CLIENT_ID = "GOOGLE_CLIENT_ID";
|
|
3885
|
+
var LEGACY_GOOGLE_CLIENT_SECRET = "GOOGLE_CLIENT_SECRET";
|
|
3820
3886
|
var ENV_VAR_RE = /\$\{([A-Z_][A-Z0-9_]*)\}/g;
|
|
3821
3887
|
function resolveEnvVars(value) {
|
|
3822
3888
|
return value.replace(ENV_VAR_RE, (_match, name) => {
|
|
@@ -3835,6 +3901,26 @@ function deepResolve(obj) {
|
|
|
3835
3901
|
}
|
|
3836
3902
|
return obj;
|
|
3837
3903
|
}
|
|
3904
|
+
function parseBool(value) {
|
|
3905
|
+
if (value === void 0) return void 0;
|
|
3906
|
+
const lower = value.trim().toLowerCase();
|
|
3907
|
+
if (lower === "true" || lower === "1" || lower === "yes") return true;
|
|
3908
|
+
if (lower === "false" || lower === "0" || lower === "no" || lower === "") return false;
|
|
3909
|
+
return void 0;
|
|
3910
|
+
}
|
|
3911
|
+
function parseIntSafe(value) {
|
|
3912
|
+
if (value === void 0) return void 0;
|
|
3913
|
+
const trimmed = value.trim();
|
|
3914
|
+
if (trimmed === "") return void 0;
|
|
3915
|
+
const n = Number.parseInt(trimmed, 10);
|
|
3916
|
+
return Number.isNaN(n) ? void 0 : n;
|
|
3917
|
+
}
|
|
3918
|
+
function parseStringArray(value) {
|
|
3919
|
+
if (value === void 0) return void 0;
|
|
3920
|
+
const trimmed = value.trim();
|
|
3921
|
+
if (trimmed === "") return [];
|
|
3922
|
+
return trimmed.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
3923
|
+
}
|
|
3838
3924
|
function validateToolNames(toolNames, listName) {
|
|
3839
3925
|
if (!toolNames || toolNames.length === 0) return;
|
|
3840
3926
|
const known = new Set(KNOWN_TOOLS);
|
|
@@ -3850,7 +3936,7 @@ function loadConfig(configPath, cliOverrides = {}) {
|
|
|
3850
3936
|
let raw = {};
|
|
3851
3937
|
if (configPath) {
|
|
3852
3938
|
try {
|
|
3853
|
-
raw = JSON.parse(
|
|
3939
|
+
raw = JSON.parse(readFileSync2(configPath, "utf-8"));
|
|
3854
3940
|
} catch (err) {
|
|
3855
3941
|
const detail = err instanceof SyntaxError ? "Invalid JSON" : err instanceof Error ? err.message : String(err);
|
|
3856
3942
|
throw new Error(`Failed to read config file "${configPath}": ${detail}`);
|
|
@@ -3858,38 +3944,68 @@ function loadConfig(configPath, cliOverrides = {}) {
|
|
|
3858
3944
|
}
|
|
3859
3945
|
raw = deepResolve(raw);
|
|
3860
3946
|
const parsed = rawConfigSchema.parse(raw);
|
|
3861
|
-
|
|
3862
|
-
|
|
3863
|
-
|
|
3864
|
-
|
|
3865
|
-
|
|
3947
|
+
const http = {
|
|
3948
|
+
enabled: cliOverrides.http ?? parsed.http?.enabled ?? parseBool(process.env[ENV_HTTP_ENABLED]) ?? false,
|
|
3949
|
+
port: cliOverrides.port ?? parsed.http?.port ?? parseIntSafe(process.env[ENV_HTTP_PORT]) ?? 3e3,
|
|
3950
|
+
host: cliOverrides.host ?? parsed.http?.host ?? process.env[ENV_HTTP_HOST] ?? "127.0.0.1"
|
|
3951
|
+
};
|
|
3952
|
+
const toolsDisabled = parsed.tools?.disabled ?? parseStringArray(process.env[ENV_TOOLS_DISABLED]);
|
|
3953
|
+
const toolsEnabled = parsed.tools?.enabled ?? parseStringArray(process.env[ENV_TOOLS_ENABLED]);
|
|
3954
|
+
if (toolsDisabled && toolsEnabled) {
|
|
3955
|
+
throw new Error(
|
|
3956
|
+
"tools.disabled and tools.enabled are mutually exclusive \u2014 use one or the other"
|
|
3957
|
+
);
|
|
3958
|
+
}
|
|
3959
|
+
if (toolsEnabled !== void 0 && toolsEnabled.length === 0) {
|
|
3960
|
+
throw new Error(
|
|
3961
|
+
"tools.enabled is empty \u2014 at least one tool must be listed. To enable all tools, omit the tools section entirely."
|
|
3962
|
+
);
|
|
3963
|
+
}
|
|
3964
|
+
validateToolNames(toolsDisabled, "tools.disabled");
|
|
3965
|
+
validateToolNames(toolsEnabled, "tools.enabled");
|
|
3966
|
+
const tools = toolsDisabled || toolsEnabled ? { disabled: toolsDisabled, enabled: toolsEnabled } : void 0;
|
|
3967
|
+
const outlookClientId = parsed.providers?.outlook?.clientId ?? process.env[ENV_OUTLOOK_CLIENT_ID] ?? process.env[LEGACY_MS_CLIENT_ID];
|
|
3968
|
+
const outlookTenantId = parsed.providers?.outlook?.tenantId ?? process.env[ENV_OUTLOOK_TENANT_ID] ?? process.env[LEGACY_MS_TENANT_ID];
|
|
3969
|
+
const gmailClientId = parsed.providers?.gmail?.clientId ?? process.env[ENV_GMAIL_CLIENT_ID] ?? process.env[LEGACY_GOOGLE_CLIENT_ID];
|
|
3970
|
+
const gmailClientSecret = parsed.providers?.gmail?.clientSecret ?? process.env[ENV_GMAIL_CLIENT_SECRET] ?? process.env[LEGACY_GOOGLE_CLIENT_SECRET];
|
|
3971
|
+
let providers;
|
|
3972
|
+
if (outlookClientId || outlookTenantId || gmailClientId || gmailClientSecret) {
|
|
3973
|
+
providers = {};
|
|
3974
|
+
if (outlookClientId || outlookTenantId) {
|
|
3975
|
+
providers.outlook = {};
|
|
3976
|
+
if (outlookClientId) providers.outlook.clientId = outlookClientId;
|
|
3977
|
+
if (outlookTenantId) providers.outlook.tenantId = outlookTenantId;
|
|
3866
3978
|
}
|
|
3867
|
-
if (
|
|
3868
|
-
|
|
3869
|
-
|
|
3870
|
-
);
|
|
3979
|
+
if (gmailClientId || gmailClientSecret) {
|
|
3980
|
+
providers.gmail = {};
|
|
3981
|
+
if (gmailClientId) providers.gmail.clientId = gmailClientId;
|
|
3982
|
+
if (gmailClientSecret) providers.gmail.clientSecret = gmailClientSecret;
|
|
3871
3983
|
}
|
|
3872
|
-
validateToolNames(parsed.tools.disabled, "tools.disabled");
|
|
3873
|
-
validateToolNames(parsed.tools.enabled, "tools.enabled");
|
|
3874
3984
|
}
|
|
3875
|
-
const
|
|
3876
|
-
enabled: cliOverrides.http ?? parsed.http?.enabled ?? false,
|
|
3877
|
-
port: cliOverrides.port ?? parsed.http?.port ?? 3e3,
|
|
3878
|
-
host: cliOverrides.host ?? parsed.http?.host ?? "127.0.0.1"
|
|
3879
|
-
};
|
|
3985
|
+
const watchEnabledEnv = parseBool(process.env[ENV_WATCH_ENABLED]);
|
|
3880
3986
|
let watch;
|
|
3881
|
-
if (parsed.watch ||
|
|
3987
|
+
if (parsed.watch || watchEnabledEnv !== void 0) {
|
|
3988
|
+
const webhookUrl = parsed.watch?.webhook?.url ?? process.env[ENV_WATCH_WEBHOOK_URL];
|
|
3989
|
+
let webhook;
|
|
3990
|
+
if (webhookUrl) {
|
|
3991
|
+
const retryMaxAttempts = parsed.watch?.webhook?.retry?.maxAttempts ?? parseIntSafe(process.env[ENV_WATCH_WEBHOOK_RETRY_MAX_ATTEMPTS]) ?? 5;
|
|
3992
|
+
const retryBaseDelayMs = parsed.watch?.webhook?.retry?.baseDelayMs ?? parseIntSafe(process.env[ENV_WATCH_WEBHOOK_RETRY_BASE_DELAY]) ?? 1e3;
|
|
3993
|
+
webhook = {
|
|
3994
|
+
url: webhookUrl,
|
|
3995
|
+
retry: { maxAttempts: retryMaxAttempts, baseDelayMs: retryBaseDelayMs }
|
|
3996
|
+
};
|
|
3997
|
+
}
|
|
3882
3998
|
watch = {
|
|
3883
|
-
enabled:
|
|
3884
|
-
pollIntervalSeconds: parsed.watch?.pollIntervalSeconds ?? 10,
|
|
3885
|
-
webhook
|
|
3999
|
+
enabled: watchEnabledEnv ?? parsed.watch?.enabled ?? false,
|
|
4000
|
+
pollIntervalSeconds: parsed.watch?.pollIntervalSeconds ?? parseIntSafe(process.env[ENV_WATCH_POLL_INTERVAL]) ?? 10,
|
|
4001
|
+
webhook
|
|
3886
4002
|
};
|
|
3887
4003
|
}
|
|
3888
4004
|
return {
|
|
3889
4005
|
dataDir: cliOverrides.dataDir ?? parsed.dataDir ?? process.env.HYPERMAIL_MCP_DATA_DIR,
|
|
3890
4006
|
http,
|
|
3891
|
-
tools
|
|
3892
|
-
providers
|
|
4007
|
+
tools,
|
|
4008
|
+
providers,
|
|
3893
4009
|
watch
|
|
3894
4010
|
};
|
|
3895
4011
|
}
|
|
@@ -4031,19 +4147,51 @@ Options:
|
|
|
4031
4147
|
-h, --help Show this help
|
|
4032
4148
|
|
|
4033
4149
|
Configuration:
|
|
4034
|
-
All
|
|
4035
|
-
|
|
4150
|
+
All settings can be provided via environment variables \u2014 no config file
|
|
4151
|
+
required. Use hypermail-config.json for advanced scenarios.
|
|
4152
|
+
|
|
4153
|
+
Environment variables:
|
|
4154
|
+
|
|
4155
|
+
HYPERMAIL_MCP_DATA_DIR Data directory (string)
|
|
4156
|
+
HYPERMAIL_HTTP_ENABLED Enable HTTP mode (bool: true/false/1/0)
|
|
4157
|
+
HYPERMAIL_HTTP_PORT HTTP port (number)
|
|
4158
|
+
HYPERMAIL_HTTP_HOST HTTP bind address (string)
|
|
4159
|
+
HYPERMAIL_TOOLS_DISABLED Comma-separated tool names to disable
|
|
4160
|
+
HYPERMAIL_TOOLS_ENABLED Comma-separated tool names to enable
|
|
4161
|
+
HYPERMAIL_PROVIDERS_OUTLOOK_CLIENT_ID Outlook OAuth client ID (string)
|
|
4162
|
+
HYPERMAIL_PROVIDERS_OUTLOOK_TENANT_ID Outlook tenant ID (string)
|
|
4163
|
+
HYPERMAIL_PROVIDERS_GMAIL_CLIENT_ID Gmail OAuth client ID (string)
|
|
4164
|
+
HYPERMAIL_PROVIDERS_GMAIL_CLIENT_SECRET Gmail OAuth client secret (string)
|
|
4165
|
+
HYPERMAIL_WATCH_ENABLED Enable inbox polling (bool)
|
|
4166
|
+
HYPERMAIL_WATCH_POLL_INTERVAL Poll interval in seconds (number)
|
|
4167
|
+
HYPERMAIL_WATCH_WEBHOOK_URL Webhook URL for new-email events (string)
|
|
4168
|
+
HYPERMAIL_WATCH_WEBHOOK_RETRY_MAX_ATTEMPTS Retry max attempts (number)
|
|
4169
|
+
HYPERMAIL_WATCH_WEBHOOK_RETRY_BASE_DELAY_MS Retry base delay ms (number)
|
|
4170
|
+
HYPERMAIL_MCP_KEY Encryption master key (hex or base64)
|
|
4171
|
+
|
|
4172
|
+
Legacy env vars (still supported):
|
|
4173
|
+
MS_CLIENT_ID, MS_TENANT_ID, GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET
|
|
4174
|
+
|
|
4175
|
+
Priority: CLI flags > config file > env vars > defaults.
|
|
4036
4176
|
|
|
4037
|
-
Example
|
|
4038
|
-
|
|
4039
|
-
|
|
4040
|
-
|
|
4041
|
-
|
|
4042
|
-
|
|
4043
|
-
|
|
4044
|
-
|
|
4177
|
+
Example (env-only, no config file):
|
|
4178
|
+
HYPERMAIL_HTTP_ENABLED=true \\
|
|
4179
|
+
HYPERMAIL_HTTP_PORT=8080 \\
|
|
4180
|
+
HYPERMAIL_PROVIDERS_OUTLOOK_CLIENT_ID=abc123 \\
|
|
4181
|
+
HYPERMAIL_PROVIDERS_OUTLOOK_TENANT_ID=common \\
|
|
4182
|
+
HYPERMAIL_MCP_DATA_DIR=/data/hypermail \\
|
|
4183
|
+
hypermail-mcp --http
|
|
4184
|
+
|
|
4185
|
+
Example hypermail-config.json:
|
|
4186
|
+
{
|
|
4187
|
+
"dataDir": "/path/to/data",
|
|
4188
|
+
"http": { "enabled": false },
|
|
4189
|
+
"tools": { "disabled": ["send_email"] },
|
|
4190
|
+
"providers": {
|
|
4191
|
+
"outlook": { "clientId": "\${MS_CLIENT_ID}", "tenantId": "\${MS_TENANT_ID}" },
|
|
4192
|
+
"gmail": { "clientId": "\${GOOGLE_CLIENT_ID}", "clientSecret": "\${GOOGLE_CLIENT_SECRET}" }
|
|
4193
|
+
}
|
|
4045
4194
|
}
|
|
4046
|
-
}
|
|
4047
4195
|
`;
|
|
4048
4196
|
process.stdout.write(msg);
|
|
4049
4197
|
}
|