hypermail-mcp 0.7.0 → 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 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.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
+ >
6
11
  > **v0.7.0** — Email watch mode: background poll loop detects new inbox
7
12
  > messages and POSTs them to a configurable webhook URL (e.g. Mastra). Opt-in —
8
13
  > disabled by default, enabled via `HYPERMAIL_WATCH_ENABLED=true` or config.
@@ -170,6 +175,33 @@ Per-tool filtering (`tools.enabled` / `tools.disabled`) lets operators ship
170
175
  minimal agent-facing surfaces — e.g. a read-only assistant that can only list
171
176
  and read emails.
172
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
+
173
205
  ## Tools
174
206
 
175
207
  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 || process.env.MS_CLIENT_ID || DEFAULT_CLIENT_ID;
223
- const tenant = tenantOverride || process.env.MS_TENANT_ID || "common";
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 || process.env.GOOGLE_CLIENT_ID;
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 in env or provider config"
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 || process.env.GOOGLE_CLIENT_SECRET || void 0;
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.7.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: {
@@ -3817,6 +3815,24 @@ var KNOWN_TOOLS = [
3817
3815
  "add_attachment_to_draft",
3818
3816
  "check_notifications"
3819
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";
3820
3836
  var ENV_VAR_RE = /\$\{([A-Z_][A-Z0-9_]*)\}/g;
3821
3837
  function resolveEnvVars(value) {
3822
3838
  return value.replace(ENV_VAR_RE, (_match, name) => {
@@ -3835,6 +3851,26 @@ function deepResolve(obj) {
3835
3851
  }
3836
3852
  return obj;
3837
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
+ }
3838
3874
  function validateToolNames(toolNames, listName) {
3839
3875
  if (!toolNames || toolNames.length === 0) return;
3840
3876
  const known = new Set(KNOWN_TOOLS);
@@ -3858,38 +3894,68 @@ function loadConfig(configPath, cliOverrides = {}) {
3858
3894
  }
3859
3895
  raw = deepResolve(raw);
3860
3896
  const parsed = rawConfigSchema.parse(raw);
3861
- if (parsed.tools) {
3862
- if (parsed.tools.disabled && parsed.tools.enabled) {
3863
- throw new Error(
3864
- "tools.disabled and tools.enabled are mutually exclusive \u2014 use one or the other"
3865
- );
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;
3866
3928
  }
3867
- if (parsed.tools.enabled !== void 0 && parsed.tools.enabled.length === 0) {
3868
- throw new Error(
3869
- "tools.enabled is empty \u2014 at least one tool must be listed. To enable all tools, omit the tools section entirely."
3870
- );
3929
+ if (gmailClientId || gmailClientSecret) {
3930
+ providers.gmail = {};
3931
+ if (gmailClientId) providers.gmail.clientId = gmailClientId;
3932
+ if (gmailClientSecret) providers.gmail.clientSecret = gmailClientSecret;
3871
3933
  }
3872
- validateToolNames(parsed.tools.disabled, "tools.disabled");
3873
- validateToolNames(parsed.tools.enabled, "tools.enabled");
3874
3934
  }
3875
- const http = {
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
- };
3935
+ const watchEnabledEnv = parseBool(process.env[ENV_WATCH_ENABLED]);
3880
3936
  let watch;
3881
- if (parsed.watch || process.env.HYPERMAIL_WATCH_ENABLED === "true") {
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
+ }
3882
3948
  watch = {
3883
- enabled: process.env.HYPERMAIL_WATCH_ENABLED === "true" || Boolean(parsed.watch?.enabled),
3884
- pollIntervalSeconds: parsed.watch?.pollIntervalSeconds ?? 10,
3885
- webhook: parsed.watch?.webhook
3949
+ enabled: watchEnabledEnv ?? parsed.watch?.enabled ?? false,
3950
+ pollIntervalSeconds: parsed.watch?.pollIntervalSeconds ?? parseIntSafe(process.env[ENV_WATCH_POLL_INTERVAL]) ?? 10,
3951
+ webhook
3886
3952
  };
3887
3953
  }
3888
3954
  return {
3889
3955
  dataDir: cliOverrides.dataDir ?? parsed.dataDir ?? process.env.HYPERMAIL_MCP_DATA_DIR,
3890
3956
  http,
3891
- tools: parsed.tools ? { disabled: parsed.tools.disabled, enabled: parsed.tools.enabled } : void 0,
3892
- providers: parsed.providers,
3957
+ tools,
3958
+ providers,
3893
3959
  watch
3894
3960
  };
3895
3961
  }
@@ -4031,19 +4097,51 @@ Options:
4031
4097
  -h, --help Show this help
4032
4098
 
4033
4099
  Configuration:
4034
- All server settings (data dir, HTTP, tool filtering, provider credentials)
4035
- live in hypermail-config.json. Pass it via --config.
4100
+ All settings can be provided via environment variables \u2014 no config file
4101
+ required. Use hypermail-config.json for advanced scenarios.
4102
+
4103
+ Environment variables:
4036
4104
 
4037
- Example hypermail-config.json:
4038
- {
4039
- "dataDir": "/path/to/data",
4040
- "http": { "enabled": false },
4041
- "tools": { "disabled": ["send_email"] },
4042
- "providers": {
4043
- "outlook": { "clientId": "\${MS_CLIENT_ID}", "tenantId": "\${MS_TENANT_ID}" },
4044
- "gmail": { "clientId": "\${GOOGLE_CLIENT_ID}", "clientSecret": "\${GOOGLE_CLIENT_SECRET}" }
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
+ }
4045
4144
  }
4046
- }
4047
4145
  `;
4048
4146
  process.stdout.write(msg);
4049
4147
  }