hypermail-mcp 0.7.6 → 0.7.7

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/dist/cli.js CHANGED
@@ -6881,9 +6881,6 @@ var require_dist = __commonJS({
6881
6881
  }
6882
6882
  });
6883
6883
 
6884
- // src/cli.ts
6885
- import { randomBytes as randomBytes2 } from "crypto";
6886
-
6887
6884
  // node_modules/.pnpm/@modelcontextprotocol+sdk@1.29.0_zod@4.4.3/node_modules/@modelcontextprotocol/sdk/dist/esm/server/zod-compat.js
6888
6885
  import * as z3rt from "zod/v3";
6889
6886
  import * as z4mini from "zod/v4-mini";
@@ -13854,7 +13851,7 @@ var StreamableHTTPServerTransport = class {
13854
13851
 
13855
13852
  // src/server.ts
13856
13853
  import { randomUUID as randomUUID6 } from "crypto";
13857
- import { createServer as createHttpServer } from "http";
13854
+ import { createServer as createHttpServer2 } from "http";
13858
13855
 
13859
13856
  // src/store/account-store.ts
13860
13857
  import { promises as fs2 } from "fs";
@@ -13897,8 +13894,11 @@ function decrypt(buf, key) {
13897
13894
  return JSON.parse(pt.toString("utf8"));
13898
13895
  }
13899
13896
  function resolveDataDir(explicit) {
13900
- if (explicit && explicit.length > 0) return path.resolve(explicit);
13901
- return path.join(homedir(), ".hypermail-mcp");
13897
+ if (explicit && explicit.length > 0) return explicit;
13898
+ const envDataDir = process.env.HYPERMAIL_DATA_DIR;
13899
+ if (envDataDir && envDataDir.length > 0) return envDataDir;
13900
+ const dataHome = process.env.XDG_DATA_HOME && process.env.XDG_DATA_HOME.length > 0 ? process.env.XDG_DATA_HOME : path.join(homedir(), ".local", "share");
13901
+ return path.join(dataHome, "hypermail-mcp");
13902
13902
  }
13903
13903
  function parseEnvKey(raw) {
13904
13904
  const s = raw.trim();
@@ -13911,7 +13911,7 @@ function parseEnvKey(raw) {
13911
13911
  return createHash("sha256").update(s, "utf8").digest();
13912
13912
  }
13913
13913
  async function resolveKey(dataDir) {
13914
- const env = process.env.HYPERMAIL_MCP_KEY;
13914
+ const env = process.env.HYPERMAIL_KEY;
13915
13915
  if (env && env.length > 0) {
13916
13916
  const k = parseEnvKey(env);
13917
13917
  if (k) return k;
@@ -14082,7 +14082,7 @@ function beginDeviceCode(scopes = DEFAULT_SCOPES, clientIdOverride, tenantOverri
14082
14082
  if (!info.userCode || !info.verificationUri) {
14083
14083
  reject(
14084
14084
  new Error(
14085
- "Microsoft device-code endpoint returned no code. Check HYPERMAIL_PROVIDERS_OUTLOOK_CLIENT_ID is a valid Azure Entra public-client application."
14085
+ "Microsoft device-code endpoint returned no code. Check HYPERMAIL_OUTLOOK_CLIENT_ID is a valid Azure Entra public-client application."
14086
14086
  )
14087
14087
  );
14088
14088
  return;
@@ -15545,12 +15545,16 @@ var ImapProvider = class {
15545
15545
  import { randomUUID as randomUUID5 } from "crypto";
15546
15546
 
15547
15547
  // src/providers/gmail/auth.ts
15548
+ import { createHash as createHash2, randomBytes as randomBytes2 } from "crypto";
15549
+ import { createServer as createHttpServer } from "http";
15548
15550
  import { OAuth2Client } from "google-auth-library";
15549
- var GOOGLE_DEVICE_CODE_URL = "https://oauth2.googleapis.com/device/code";
15551
+ var GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
15550
15552
  var GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token";
15551
15553
  var DEFAULT_SCOPES2 = [
15552
15554
  "https://www.googleapis.com/auth/gmail.modify"
15553
15555
  ];
15556
+ var DEFAULT_GMAIL_OAUTH_CALLBACK_PATH = "/oauth/gmail/callback";
15557
+ var DEFAULT_FLOW_TTL_MS = 20 * 6e4;
15554
15558
  function isSerializedGmailTokens(obj) {
15555
15559
  if (typeof obj !== "object" || obj === null) return false;
15556
15560
  const o = obj;
@@ -15571,6 +15575,82 @@ function buildOAuth2Client(tokens) {
15571
15575
  }
15572
15576
  return client;
15573
15577
  }
15578
+ function base64Url(buf) {
15579
+ return buf.toString("base64").replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
15580
+ }
15581
+ function randomBase64Url(bytes = 32) {
15582
+ return base64Url(randomBytes2(bytes));
15583
+ }
15584
+ function codeChallenge(verifier) {
15585
+ return base64Url(createHash2("sha256").update(verifier).digest());
15586
+ }
15587
+ function htmlResponse(title, body) {
15588
+ return `<!doctype html>
15589
+ <html lang="en">
15590
+ <head><meta charset="utf-8"><title>${title}</title></head>
15591
+ <body><h1>${title}</h1><p>${body}</p></body>
15592
+ </html>`;
15593
+ }
15594
+ function sendHtml(res, status, title, body) {
15595
+ res.statusCode = status;
15596
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
15597
+ res.end(htmlResponse(title, body));
15598
+ }
15599
+ async function startLocalCallbackServer() {
15600
+ let redirectUri = "";
15601
+ let captured;
15602
+ let closed = false;
15603
+ const server = createHttpServer((req, res) => {
15604
+ if (!req.url) {
15605
+ sendHtml(res, 400, "Gmail authorization failed", "The callback URL was empty.");
15606
+ return;
15607
+ }
15608
+ const callbackUrl = new URL(req.url, redirectUri);
15609
+ if (callbackUrl.pathname !== "/oauth2callback") {
15610
+ sendHtml(res, 404, "Not found", "This local callback server only handles Gmail OAuth redirects.");
15611
+ return;
15612
+ }
15613
+ captured = callbackUrl.toString();
15614
+ const error = callbackUrl.searchParams.get("error");
15615
+ if (error) {
15616
+ sendHtml(res, 400, "Gmail authorization failed", "You can close this tab and return to your MCP client.");
15617
+ } else {
15618
+ sendHtml(res, 200, "Gmail authorization complete", "You can close this tab and return to your MCP client.");
15619
+ }
15620
+ setImmediate(closeServer);
15621
+ });
15622
+ await new Promise((resolve, reject) => {
15623
+ server.once("error", reject);
15624
+ server.listen(0, "127.0.0.1", () => {
15625
+ server.off("error", reject);
15626
+ resolve();
15627
+ });
15628
+ });
15629
+ const address = server.address();
15630
+ if (!address) {
15631
+ closeServer();
15632
+ throw new Error("Failed to start local Gmail OAuth callback server");
15633
+ }
15634
+ redirectUri = `http://127.0.0.1:${address.port}/oauth2callback`;
15635
+ function closeServer() {
15636
+ if (closed) return;
15637
+ closed = true;
15638
+ try {
15639
+ server.close();
15640
+ } catch {
15641
+ }
15642
+ }
15643
+ return {
15644
+ redirectUri,
15645
+ cancel: closeServer,
15646
+ consumeAuthorizationResponse() {
15647
+ if (!captured) return void 0;
15648
+ const authorizationResponse = captured;
15649
+ captured = void 0;
15650
+ return { authorizationResponse };
15651
+ }
15652
+ };
15653
+ }
15574
15654
  async function getEmailFromToken(accessToken) {
15575
15655
  const res = await fetch(
15576
15656
  "https://gmail.googleapis.com/gmail/v1/users/me/profile",
@@ -15589,134 +15669,115 @@ async function getEmailFromToken(accessToken) {
15589
15669
  const data = await res.json();
15590
15670
  return data.emailAddress;
15591
15671
  }
15592
- function beginDeviceCode2(scopes = DEFAULT_SCOPES2, clientIdOverride, clientSecretOverride) {
15593
- const clientId = clientIdOverride;
15672
+ async function beginAuthorizationCode(options = {}) {
15673
+ const scopes = options.scopes ?? DEFAULT_SCOPES2;
15674
+ const clientId = options.clientId;
15594
15675
  if (!clientId) {
15595
15676
  throw new Error(
15596
- "HYPERMAIL_PROVIDERS_GMAIL_CLIENT_ID is required for Gmail OAuth \u2014 set it via HYPERMAIL_PROVIDERS_GMAIL_CLIENT_ID or provider config"
15677
+ "HYPERMAIL_GMAIL_CLIENT_ID is required for Gmail OAuth \u2014 set HYPERMAIL_GMAIL_CLIENT_ID before adding a Gmail account"
15597
15678
  );
15598
15679
  }
15599
- const clientSecret = clientSecretOverride || void 0;
15600
- let resolve;
15601
- let reject;
15602
- const result = new Promise(
15603
- (res, rej) => {
15604
- resolve = res;
15605
- reject = rej;
15606
- }
15607
- );
15608
- let userCode = "";
15609
- let verificationUri = "";
15610
- let message = "";
15611
- let expiresAt = new Date(Date.now() + 15 * 6e4).toISOString();
15612
- let aborted = false;
15613
- const ready = (async () => {
15614
- try {
15615
- const dcParams = new URLSearchParams();
15616
- dcParams.set("client_id", clientId);
15617
- if (clientSecret) dcParams.set("client_secret", clientSecret);
15618
- dcParams.set("scope", scopes.join(" "));
15619
- const dcRes = await fetch(GOOGLE_DEVICE_CODE_URL, {
15620
- method: "POST",
15621
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
15622
- body: dcParams.toString()
15623
- });
15624
- if (!dcRes.ok) {
15625
- const errBody = await dcRes.json().catch(() => ({}));
15626
- throw new Error(
15627
- `Google device-code request failed: ${errBody.error_description ?? errBody.error ?? dcRes.statusText}`
15628
- );
15629
- }
15630
- const dcData = await dcRes.json();
15631
- userCode = dcData.user_code;
15632
- verificationUri = dcData.verification_url;
15633
- const deviceCode = dcData.device_code;
15634
- let interval = dcData.interval ?? 5;
15635
- if (dcData.expires_in) {
15636
- expiresAt = new Date(
15637
- Date.now() + dcData.expires_in * 1e3
15638
- ).toISOString();
15639
- }
15640
- message = `Go to ${verificationUri} and enter code: ${userCode}`;
15641
- const tokenParams = new URLSearchParams();
15642
- tokenParams.set("client_id", clientId);
15643
- if (clientSecret) tokenParams.set("client_secret", clientSecret);
15644
- tokenParams.set("device_code", deviceCode);
15645
- tokenParams.set(
15646
- "grant_type",
15647
- "urn:ietf:params:oauth:grant-type:device_code"
15648
- );
15649
- const deadline = Date.now() + dcData.expires_in * 1e3;
15650
- while (Date.now() < deadline && !aborted) {
15651
- await new Promise((r) => setTimeout(r, interval * 1e3));
15652
- if (aborted) return;
15653
- const tokenRes = await fetch(GOOGLE_TOKEN_URL, {
15654
- method: "POST",
15655
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
15656
- body: tokenParams.toString()
15657
- });
15658
- const tokenData = await tokenRes.json();
15659
- if (tokenData.access_token) {
15660
- const email = await getEmailFromToken(tokenData.access_token);
15661
- const tokens = {
15662
- clientId,
15663
- clientSecret,
15664
- accessToken: tokenData.access_token,
15665
- refreshToken: tokenData.refresh_token ?? "",
15666
- expiryDate: tokenData.expires_in ? Date.now() + tokenData.expires_in * 1e3 : Date.now() + 36e5,
15667
- scopes: tokenData.scope ? tokenData.scope.split(" ") : scopes,
15668
- email
15669
- };
15670
- resolve({ tokens, email });
15671
- return;
15672
- }
15673
- switch (tokenData.error) {
15674
- case "authorization_pending":
15675
- break;
15676
- // keep polling
15677
- case "slow_down":
15678
- interval += 1;
15679
- break;
15680
- case "expired_token":
15681
- throw new Error("Device code expired \u2014 please try again");
15682
- case "access_denied":
15683
- throw new Error("User denied access");
15684
- default:
15685
- throw new Error(
15686
- `Token request failed: ${tokenData.error ?? "unknown error"}`
15687
- );
15688
- }
15689
- }
15690
- if (!aborted) {
15691
- throw new Error("Device code expired \u2014 please try again");
15692
- }
15693
- } catch (err) {
15694
- if (!aborted) reject(err);
15695
- }
15696
- })();
15680
+ const localCallback = options.redirectUri ? void 0 : await startLocalCallbackServer();
15681
+ const redirectUri = options.redirectUri ?? localCallback?.redirectUri;
15682
+ if (!redirectUri) {
15683
+ throw new Error("Failed to resolve Gmail OAuth redirect URI");
15684
+ }
15685
+ const state = randomBase64Url(24);
15686
+ const codeVerifier = randomBase64Url(64);
15687
+ const expiresAt = new Date(Date.now() + DEFAULT_FLOW_TTL_MS).toISOString();
15688
+ const params = new URLSearchParams({
15689
+ client_id: clientId,
15690
+ redirect_uri: redirectUri,
15691
+ response_type: "code",
15692
+ scope: scopes.join(" "),
15693
+ access_type: "offline",
15694
+ prompt: "consent",
15695
+ state,
15696
+ code_challenge: codeChallenge(codeVerifier),
15697
+ code_challenge_method: "S256"
15698
+ });
15699
+ const verificationUri = `${GOOGLE_AUTH_URL}?${params.toString()}`;
15697
15700
  return {
15698
- get userCode() {
15699
- return userCode;
15700
- },
15701
- get verificationUri() {
15702
- return verificationUri;
15703
- },
15704
- get message() {
15705
- return message;
15706
- },
15707
- get expiresAt() {
15708
- return expiresAt;
15709
- },
15710
- result,
15701
+ userCode: "",
15702
+ verificationUri,
15703
+ message: "Open this URL in a browser to authorize Gmail access. After approval, the account setup should complete automatically when the agent polls. If the browser cannot reach the callback, paste the final redirected URL back to the agent.",
15704
+ expiresAt,
15705
+ state,
15706
+ codeVerifier,
15707
+ redirectUri,
15708
+ scopes,
15709
+ clientId,
15710
+ clientSecret: options.clientSecret || void 0,
15711
15711
  cancel() {
15712
- aborted = true;
15712
+ localCallback?.cancel();
15713
15713
  },
15714
- ...{ _ready: ready }
15714
+ consumeAuthorizationResponse() {
15715
+ return localCallback?.consumeAuthorizationResponse();
15716
+ }
15715
15717
  };
15716
15718
  }
15717
- async function awaitDeviceCodeReady2(b) {
15718
- const r = b._ready;
15719
- await r;
15719
+ function parseAuthorizationCompletion(flow, input) {
15720
+ if (input.authorizationResponse) {
15721
+ let url;
15722
+ try {
15723
+ url = new URL(input.authorizationResponse);
15724
+ } catch {
15725
+ throw new Error("authorizationResponse must be a full redirected URL");
15726
+ }
15727
+ const error = url.searchParams.get("error");
15728
+ if (error) {
15729
+ const description = url.searchParams.get("error_description");
15730
+ throw new Error(
15731
+ `Google OAuth failed: ${description || error}`
15732
+ );
15733
+ }
15734
+ const code = url.searchParams.get("code");
15735
+ if (!code) {
15736
+ throw new Error("authorizationResponse is missing the OAuth code");
15737
+ }
15738
+ return { code, state: url.searchParams.get("state") ?? void 0 };
15739
+ }
15740
+ if (input.code) {
15741
+ return { code: input.code, state: input.state };
15742
+ }
15743
+ throw new Error(
15744
+ "Paste the final redirected URL from Google as authorizationResponse, or provide code and state"
15745
+ );
15746
+ }
15747
+ async function completeAuthorizationCode(flow, input) {
15748
+ const { code, state } = parseAuthorizationCompletion(flow, input);
15749
+ if (state !== void 0 && state !== flow.state) {
15750
+ throw new Error("OAuth state mismatch \u2014 restart Gmail account setup");
15751
+ }
15752
+ const tokenParams = new URLSearchParams();
15753
+ tokenParams.set("client_id", flow.clientId);
15754
+ if (flow.clientSecret) tokenParams.set("client_secret", flow.clientSecret);
15755
+ tokenParams.set("code", code);
15756
+ tokenParams.set("redirect_uri", flow.redirectUri);
15757
+ tokenParams.set("grant_type", "authorization_code");
15758
+ tokenParams.set("code_verifier", flow.codeVerifier);
15759
+ const tokenRes = await fetch(GOOGLE_TOKEN_URL, {
15760
+ method: "POST",
15761
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
15762
+ body: tokenParams
15763
+ });
15764
+ const tokenData = await tokenRes.json().catch(() => ({}));
15765
+ if (!tokenRes.ok || !tokenData.access_token) {
15766
+ throw new Error(
15767
+ "Token request failed: " + (tokenData.error_description ?? tokenData.error ?? tokenRes.statusText)
15768
+ );
15769
+ }
15770
+ const email = await getEmailFromToken(tokenData.access_token);
15771
+ const tokens = {
15772
+ clientId: flow.clientId,
15773
+ clientSecret: flow.clientSecret,
15774
+ accessToken: tokenData.access_token,
15775
+ refreshToken: tokenData.refresh_token ?? "",
15776
+ expiryDate: tokenData.expires_in ? Date.now() + tokenData.expires_in * 1e3 : Date.now() + 36e5,
15777
+ scopes: tokenData.scope ? tokenData.scope.split(" ") : flow.scopes,
15778
+ email
15779
+ };
15780
+ return { tokens, email };
15720
15781
  }
15721
15782
 
15722
15783
  // src/providers/gmail/client.ts
@@ -16400,6 +16461,7 @@ var GmailProvider = class {
16400
16461
  this.opts = opts;
16401
16462
  this.clientId = opts.clientId;
16402
16463
  this.clientSecret = opts.clientSecret;
16464
+ this.redirectUri = opts.redirectUri;
16403
16465
  this.clients = new GmailClientFactory(
16404
16466
  opts.store,
16405
16467
  opts.clientId,
@@ -16412,14 +16474,14 @@ var GmailProvider = class {
16412
16474
  pending = /* @__PURE__ */ new Map();
16413
16475
  clientId;
16414
16476
  clientSecret;
16477
+ redirectUri;
16415
16478
  // ── account lifecycle ──
16416
16479
  async addAccount(input) {
16417
- const begin = beginDeviceCode2(
16418
- void 0,
16419
- this.clientId,
16420
- this.clientSecret
16421
- );
16422
- await awaitDeviceCodeReady2(begin);
16480
+ const begin = await beginAuthorizationCode({
16481
+ clientId: this.clientId,
16482
+ clientSecret: this.clientSecret,
16483
+ redirectUri: this.redirectUri
16484
+ });
16423
16485
  const handle = randomUUID5();
16424
16486
  const flow = {
16425
16487
  begin,
@@ -16428,31 +16490,11 @@ var GmailProvider = class {
16428
16490
  settled: "pending"
16429
16491
  };
16430
16492
  this.pending.set(handle, flow);
16431
- begin.result.then(async ({ tokens, email }) => {
16432
- const resolvedEmail = (email || input.email || "").toLowerCase();
16433
- if (!resolvedEmail) {
16434
- flow.settled = "error";
16435
- flow.error = "no email returned from Google account";
16436
- return;
16437
- }
16438
- const rec = {
16439
- email: resolvedEmail,
16440
- provider: "gmail",
16441
- displayName: resolvedEmail,
16442
- tokens,
16443
- addedAt: (/* @__PURE__ */ new Date()).toISOString()
16444
- };
16445
- const saved = await this.opts.store.upsertAccount(rec);
16446
- flow.account = saved;
16447
- flow.settled = "ready";
16448
- }).catch((err) => {
16449
- flow.settled = "error";
16450
- flow.error = err instanceof Error ? err.message : String(err);
16451
- });
16452
16493
  return {
16453
16494
  status: "pending",
16454
16495
  handle,
16455
16496
  verification: {
16497
+ type: "oauth_url",
16456
16498
  userCode: begin.userCode,
16457
16499
  verificationUri: begin.verificationUri,
16458
16500
  expiresAt: begin.expiresAt,
@@ -16460,7 +16502,7 @@ var GmailProvider = class {
16460
16502
  }
16461
16503
  };
16462
16504
  }
16463
- async completeAddAccount(handle) {
16505
+ async completeAddAccount(handle, input = {}) {
16464
16506
  const flow = this.pending.get(handle);
16465
16507
  if (!flow) return { status: "error", error: "unknown handle" };
16466
16508
  if (Date.now() - flow.startedAt > 20 * 6e4 && flow.settled === "pending") {
@@ -16469,17 +16511,67 @@ var GmailProvider = class {
16469
16511
  }
16470
16512
  if (flow.settled === "ready" && flow.account) {
16471
16513
  this.pending.delete(handle);
16514
+ flow.begin.cancel();
16472
16515
  return { status: "ready", account: flow.account };
16473
16516
  }
16474
16517
  if (flow.settled === "error") {
16475
16518
  this.pending.delete(handle);
16519
+ flow.begin.cancel();
16476
16520
  return { status: "error", error: flow.error ?? "unknown error" };
16477
16521
  }
16478
16522
  if (flow.settled === "expired") {
16479
16523
  this.pending.delete(handle);
16524
+ flow.begin.cancel();
16480
16525
  return { status: "expired" };
16481
16526
  }
16482
- return { status: "pending" };
16527
+ let completionInput = input;
16528
+ if (!completionInput.authorizationResponse && !completionInput.code) {
16529
+ const captured = flow.begin.consumeAuthorizationResponse();
16530
+ if (!captured) return { status: "pending" };
16531
+ completionInput = captured;
16532
+ }
16533
+ try {
16534
+ const { tokens, email } = await completeAuthorizationCode(flow.begin, completionInput);
16535
+ const resolvedEmail = (email || flow.emailHint || "").toLowerCase();
16536
+ if (!resolvedEmail) {
16537
+ throw new Error("no email returned from Google account");
16538
+ }
16539
+ const rec = {
16540
+ email: resolvedEmail,
16541
+ provider: "gmail",
16542
+ displayName: resolvedEmail,
16543
+ tokens,
16544
+ addedAt: (/* @__PURE__ */ new Date()).toISOString()
16545
+ };
16546
+ const saved = await this.opts.store.upsertAccount(rec);
16547
+ this.pending.delete(handle);
16548
+ flow.begin.cancel();
16549
+ return { status: "ready", account: saved };
16550
+ } catch (err) {
16551
+ this.pending.delete(handle);
16552
+ flow.begin.cancel();
16553
+ return {
16554
+ status: "error",
16555
+ error: err instanceof Error ? err.message : String(err)
16556
+ };
16557
+ }
16558
+ }
16559
+ async completeAddAccountFromRedirect(authorizationResponse) {
16560
+ let state;
16561
+ try {
16562
+ state = new URL(authorizationResponse).searchParams.get("state");
16563
+ } catch {
16564
+ return { status: "error", error: "authorizationResponse must be a full redirected URL" };
16565
+ }
16566
+ if (!state) {
16567
+ return { status: "error", error: "authorizationResponse is missing OAuth state" };
16568
+ }
16569
+ for (const [handle, flow] of this.pending) {
16570
+ if (flow.begin.state === state) {
16571
+ return this.completeAddAccount(handle, { authorizationResponse });
16572
+ }
16573
+ }
16574
+ return { status: "error", error: "unknown OAuth state \u2014 restart Gmail account setup" };
16483
16575
  }
16484
16576
  // ── browse ──
16485
16577
  async listEmails(account, opts) {
@@ -16556,7 +16648,8 @@ function buildRegistry(opts) {
16556
16648
  providers.set("gmail", new GmailProvider({
16557
16649
  store: opts.store,
16558
16650
  clientId: gmailCfg?.clientId,
16559
- clientSecret: gmailCfg?.clientSecret
16651
+ clientSecret: gmailCfg?.clientSecret,
16652
+ redirectUri: gmailCfg?.redirectUri
16560
16653
  }));
16561
16654
  function get(id) {
16562
16655
  const p = providers.get(id);
@@ -16605,8 +16698,11 @@ function fail(message) {
16605
16698
  };
16606
16699
  }
16607
16700
  function errMsg(err) {
16608
- if (err instanceof Error) return err.message;
16609
- return String(err);
16701
+ const message = err instanceof Error ? err.message : String(err);
16702
+ if (message.includes("HYPERMAIL_GMAIL_CLIENT_ID")) {
16703
+ return "Missing Gmail OAuth configuration: set HYPERMAIL_GMAIL_CLIENT_ID before adding a Gmail account.";
16704
+ }
16705
+ return message;
16610
16706
  }
16611
16707
  var providerIdEnum = z2.enum(["outlook", "imap", "gmail"]);
16612
16708
  var emailAddrSchema = z2.object({
@@ -16754,6 +16850,7 @@ function registerAccountTools(server, ctx) {
16754
16850
  status: z3.enum(["pending", "ready"]),
16755
16851
  handle: z3.string().optional(),
16756
16852
  verification: z3.object({
16853
+ type: z3.enum(["device_code", "oauth_url"]).optional(),
16757
16854
  userCode: z3.string(),
16758
16855
  verificationUri: z3.string(),
16759
16856
  expiresAt: z3.string(),
@@ -16803,7 +16900,16 @@ function registerAccountTools(server, ctx) {
16803
16900
  description: "Poll/finalize a pending add_account flow. Returns `pending` until the user completes the device-code step, then `ready` with the persisted account.",
16804
16901
  inputSchema: z3.object({
16805
16902
  provider: providerIdEnum,
16806
- handle: z3.string().min(1)
16903
+ handle: z3.string().min(1),
16904
+ authorizationResponse: z3.string().optional().describe(
16905
+ "For OAuth providers such as Gmail: paste the full final redirected URL from the browser after consent."
16906
+ ),
16907
+ code: z3.string().optional().describe(
16908
+ "For OAuth providers such as Gmail: raw authorization code if the client extracted it from the redirect URL."
16909
+ ),
16910
+ state: z3.string().optional().describe(
16911
+ "For OAuth providers such as Gmail: OAuth state returned with a raw authorization code."
16912
+ )
16807
16913
  }),
16808
16914
  outputSchema: completeAddAccountOutputSchema
16809
16915
  },
@@ -16815,7 +16921,11 @@ function registerAccountTools(server, ctx) {
16815
16921
  );
16816
16922
  }
16817
16923
  try {
16818
- const res = await provider.completeAddAccount(args.handle);
16924
+ const res = await provider.completeAddAccount(args.handle, {
16925
+ authorizationResponse: args.authorizationResponse,
16926
+ code: args.code,
16927
+ state: args.state
16928
+ });
16819
16929
  return ok(res, res);
16820
16930
  } catch (err) {
16821
16931
  return fail(errMsg(err));
@@ -17731,7 +17841,7 @@ function registerTools(server, opts) {
17731
17841
  // package.json
17732
17842
  var package_default = {
17733
17843
  name: "hypermail-mcp",
17734
- version: "0.7.6",
17844
+ version: "0.7.7",
17735
17845
  description: "Unified email MCP server \u2014 operate any inbox (Outlook now, IMAP/Gmail later) by passing an email address.",
17736
17846
  type: "module",
17737
17847
  bin: {
@@ -17746,7 +17856,7 @@ var package_default = {
17746
17856
  scripts: {
17747
17857
  build: "tsup",
17748
17858
  dev: "tsup --watch",
17749
- "dev:http": "tsup && node dist/cli.js --http --config hypermail-config.http.json",
17859
+ "dev:http": "tsup && node dist/cli.js --http",
17750
17860
  start: "node dist/cli.js",
17751
17861
  typecheck: "tsc --noEmit",
17752
17862
  check: "pnpm test && pnpm typecheck && pnpm build",
@@ -17842,11 +17952,11 @@ function sleep(ms) {
17842
17952
 
17843
17953
  // src/watcher/script.ts
17844
17954
  import { spawn } from "child_process";
17845
- async function runScript(email, config) {
17846
- if (!config.script) return false;
17847
- const { path: scriptPath, timeoutMs, retry } = config.script;
17848
- const maxAttempts = retry?.maxAttempts ?? 5;
17849
- const baseDelayMs = retry?.baseDelayMs ?? 1e3;
17955
+ async function runNotifyCommand(email, config) {
17956
+ if (!config.notifyCommand) return false;
17957
+ const { command, timeoutMs, retry } = config.notifyCommand;
17958
+ const maxAttempts = retry.maxAttempts;
17959
+ const baseDelayMs = retry.baseDelayMs;
17850
17960
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
17851
17961
  if (attempt > 0) {
17852
17962
  const delay = baseDelayMs * 2 ** (attempt - 1);
@@ -17854,28 +17964,29 @@ async function runScript(email, config) {
17854
17964
  }
17855
17965
  try {
17856
17966
  const ok2 = await spawnWithTimeout(
17857
- scriptPath,
17967
+ command,
17858
17968
  JSON.stringify(email),
17859
17969
  timeoutMs
17860
17970
  );
17861
17971
  if (ok2) return true;
17862
17972
  console.error(
17863
- `[hypermail-watch] script ${email.id} attempt ${attempt + 1}/${maxAttempts}: non-zero exit code`
17973
+ `[hypermail-watch] notify command ${email.id} attempt ${attempt + 1}/${maxAttempts}: non-zero exit code`
17864
17974
  );
17865
17975
  } catch (err) {
17866
17976
  console.error(
17867
- `[hypermail-watch] script ${email.id} attempt ${attempt + 1}/${maxAttempts}: ${String(err)}`
17977
+ `[hypermail-watch] notify command ${email.id} attempt ${attempt + 1}/${maxAttempts}: ${String(err)}`
17868
17978
  );
17869
17979
  }
17870
17980
  }
17871
17981
  console.error(
17872
- `[hypermail-watch] script delivery failed after ${maxAttempts} retries for ${email.id}`
17982
+ `[hypermail-watch] notify command delivery failed after ${maxAttempts} retries for ${email.id}`
17873
17983
  );
17874
17984
  return false;
17875
17985
  }
17876
- function spawnWithTimeout(scriptPath, stdinData, timeoutMs) {
17986
+ function spawnWithTimeout(command, stdinData, timeoutMs) {
17877
17987
  return new Promise((resolve) => {
17878
- const child = spawn("node", [scriptPath], {
17988
+ const child = spawn(command, {
17989
+ shell: true,
17879
17990
  stdio: ["pipe", "pipe", "pipe"]
17880
17991
  });
17881
17992
  let stderr = "";
@@ -17885,7 +17996,7 @@ function spawnWithTimeout(scriptPath, stdinData, timeoutMs) {
17885
17996
  const timer = setTimeout(() => {
17886
17997
  child.kill("SIGTERM");
17887
17998
  if (stderr) {
17888
- console.error(`[hypermail-watch] script timed out after ${timeoutMs}ms. stderr:
17999
+ console.error(`[hypermail-watch] notify command timed out after ${timeoutMs}ms. stderr:
17889
18000
  ${stderr}`);
17890
18001
  }
17891
18002
  resolve(false);
@@ -17893,14 +18004,14 @@ ${stderr}`);
17893
18004
  child.on("close", (code) => {
17894
18005
  clearTimeout(timer);
17895
18006
  if (stderr && code !== 0) {
17896
- console.error(`[hypermail-watch] script stderr:
18007
+ console.error(`[hypermail-watch] notify command stderr:
17897
18008
  ${stderr}`);
17898
18009
  }
17899
18010
  resolve(code === 0);
17900
18011
  });
17901
18012
  child.on("error", (err) => {
17902
18013
  clearTimeout(timer);
17903
- console.error(`[hypermail-watch] script spawn error: ${err.message}`);
18014
+ console.error(`[hypermail-watch] notify command spawn error: ${err.message}`);
17904
18015
  resolve(false);
17905
18016
  });
17906
18017
  child.stdin?.end(stdinData);
@@ -17980,69 +18091,86 @@ var WatcherManager = class {
17980
18091
  }
17981
18092
  async emit(full) {
17982
18093
  await postWebhook(full, this.config);
17983
- runScript(full, this.config).catch((err) => {
18094
+ runNotifyCommand(full, this.config).catch((err) => {
17984
18095
  console.error(
17985
- `[hypermail-watch] script unhandled error for ${full.id}:`,
18096
+ `[hypermail-watch] notify command unhandled error for ${full.id}:`,
17986
18097
  err
17987
18098
  );
17988
18099
  });
17989
18100
  }
17990
18101
  };
17991
18102
 
17992
- // src/config.ts
17993
- import { z as z8 } from "zod";
17994
-
17995
18103
  // src/config/load.ts
17996
- import { readFileSync as readFileSync2 } from "fs";
17997
- var ENV_HTTP_ENABLED = "HYPERMAIL_HTTP_ENABLED";
18104
+ var ENV_DATA_DIR = "HYPERMAIL_DATA_DIR";
18105
+ var ENV_KEY = "HYPERMAIL_KEY";
18106
+ var ENV_TRANSPORT = "HYPERMAIL_TRANSPORT";
17998
18107
  var ENV_HTTP_PORT = "HYPERMAIL_HTTP_PORT";
17999
18108
  var ENV_HTTP_HOST = "HYPERMAIL_HTTP_HOST";
18000
18109
  var ENV_TOOLS_DISABLED = "HYPERMAIL_TOOLS_DISABLED";
18001
18110
  var ENV_TOOLS_ENABLED = "HYPERMAIL_TOOLS_ENABLED";
18002
- var ENV_OUTLOOK_CLIENT_ID = "HYPERMAIL_PROVIDERS_OUTLOOK_CLIENT_ID";
18003
- var ENV_OUTLOOK_TENANT_ID = "HYPERMAIL_PROVIDERS_OUTLOOK_TENANT_ID";
18004
- var ENV_GMAIL_CLIENT_ID = "HYPERMAIL_PROVIDERS_GMAIL_CLIENT_ID";
18005
- var ENV_GMAIL_CLIENT_SECRET = "HYPERMAIL_PROVIDERS_GMAIL_CLIENT_SECRET";
18111
+ var ENV_OUTLOOK_CLIENT_ID = "HYPERMAIL_OUTLOOK_CLIENT_ID";
18112
+ var ENV_OUTLOOK_TENANT_ID = "HYPERMAIL_OUTLOOK_TENANT_ID";
18113
+ var ENV_GMAIL_CLIENT_ID = "HYPERMAIL_GMAIL_CLIENT_ID";
18114
+ var ENV_GMAIL_CLIENT_SECRET = "HYPERMAIL_GMAIL_CLIENT_SECRET";
18115
+ var ENV_GMAIL_REDIRECT_URI = "HYPERMAIL_GMAIL_REDIRECT_URI";
18006
18116
  var ENV_WATCH_ENABLED = "HYPERMAIL_WATCH_ENABLED";
18007
- var ENV_WATCH_POLL_INTERVAL = "HYPERMAIL_WATCH_POLL_INTERVAL";
18117
+ var ENV_WATCH_POLL_SECONDS = "HYPERMAIL_WATCH_POLL_SECONDS";
18008
18118
  var ENV_WATCH_WEBHOOK_URL = "HYPERMAIL_WATCH_WEBHOOK_URL";
18009
- var ENV_WATCH_WEBHOOK_RETRY_MAX_ATTEMPTS = "HYPERMAIL_WATCH_WEBHOOK_RETRY_MAX_ATTEMPTS";
18010
- var ENV_WATCH_WEBHOOK_RETRY_BASE_DELAY = "HYPERMAIL_WATCH_WEBHOOK_RETRY_BASE_DELAY_MS";
18011
- var ENV_WATCH_SCRIPT_PATH = "HYPERMAIL_WATCH_SCRIPT_PATH";
18012
- var ENV_WATCH_SCRIPT_TIMEOUT_MS = "HYPERMAIL_WATCH_SCRIPT_TIMEOUT_MS";
18013
- var ENV_WATCH_SCRIPT_RETRY_MAX_ATTEMPTS = "HYPERMAIL_WATCH_SCRIPT_RETRY_MAX_ATTEMPTS";
18014
- var ENV_WATCH_SCRIPT_RETRY_BASE_DELAY = "HYPERMAIL_WATCH_SCRIPT_RETRY_BASE_DELAY_MS";
18015
- var ENV_VAR_RE = /\$\{([A-Z_][A-Z0-9_]*)\}/g;
18016
- function resolveEnvVars(value) {
18017
- return value.replace(ENV_VAR_RE, (_match, name) => {
18018
- return process.env[name] ?? "";
18019
- });
18119
+ var ENV_WATCH_WEBHOOK_RETRY_ATTEMPTS = "HYPERMAIL_WATCH_WEBHOOK_RETRY_ATTEMPTS";
18120
+ var ENV_WATCH_WEBHOOK_RETRY_DELAY_MS = "HYPERMAIL_WATCH_WEBHOOK_RETRY_DELAY_MS";
18121
+ var ENV_WATCH_NOTIFY_COMMAND = "HYPERMAIL_WATCH_NOTIFY_COMMAND";
18122
+ var ENV_WATCH_NOTIFY_TIMEOUT_MS = "HYPERMAIL_WATCH_NOTIFY_TIMEOUT_MS";
18123
+ var ENV_WATCH_NOTIFY_RETRY_ATTEMPTS = "HYPERMAIL_WATCH_NOTIFY_RETRY_ATTEMPTS";
18124
+ var ENV_WATCH_NOTIFY_RETRY_DELAY_MS = "HYPERMAIL_WATCH_NOTIFY_RETRY_DELAY_MS";
18125
+ var DEFAULT_TRANSPORT = "stdio";
18126
+ var DEFAULT_HTTP_PORT = 3e3;
18127
+ var DEFAULT_HTTP_HOST = "127.0.0.1";
18128
+ var DEFAULT_WATCH_POLL_SECONDS = 10;
18129
+ var DEFAULT_RETRY_ATTEMPTS = 5;
18130
+ var DEFAULT_RETRY_DELAY_MS = 1e3;
18131
+ var DEFAULT_NOTIFY_TIMEOUT_MS = 3e4;
18132
+ function envRaw(name) {
18133
+ return process.env[name];
18020
18134
  }
18021
- function deepResolve(obj) {
18022
- if (typeof obj === "string") return resolveEnvVars(obj);
18023
- if (Array.isArray(obj)) return obj.map(deepResolve);
18024
- if (obj && typeof obj === "object") {
18025
- const out = {};
18026
- for (const [key, val] of Object.entries(obj)) {
18027
- out[key] = deepResolve(val);
18028
- }
18029
- return out;
18030
- }
18031
- return obj;
18135
+ function optionalEnvString(name) {
18136
+ const value = envRaw(name);
18137
+ if (value === void 0) return void 0;
18138
+ const trimmed = value.trim();
18139
+ return trimmed.length > 0 ? trimmed : void 0;
18032
18140
  }
18033
- function parseBool(value) {
18141
+ function parseBoolEnv(name) {
18142
+ const value = envRaw(name);
18034
18143
  if (value === void 0) return void 0;
18035
18144
  const lower = value.trim().toLowerCase();
18036
- if (lower === "true" || lower === "1" || lower === "yes") return true;
18037
- if (lower === "false" || lower === "0" || lower === "no" || lower === "") return false;
18038
- return void 0;
18145
+ if (lower === "true") return true;
18146
+ if (lower === "false") return false;
18147
+ throw new Error(`${name} must be either "true" or "false"`);
18039
18148
  }
18040
- function parseIntSafe(value) {
18149
+ function parseTransportEnv() {
18150
+ const value = envRaw(ENV_TRANSPORT);
18041
18151
  if (value === void 0) return void 0;
18152
+ const lower = value.trim().toLowerCase();
18153
+ if (lower === "stdio" || lower === "http") return lower;
18154
+ throw new Error(`${ENV_TRANSPORT} must be either "stdio" or "http"`);
18155
+ }
18156
+ function parsePositiveInteger(value) {
18157
+ if (value === void 0) return void 0;
18158
+ if (typeof value === "number") {
18159
+ return Number.isInteger(value) && value > 0 ? value : void 0;
18160
+ }
18042
18161
  const trimmed = value.trim();
18043
- if (trimmed === "") return void 0;
18044
- const n = Number.parseInt(trimmed, 10);
18045
- return Number.isNaN(n) ? void 0 : n;
18162
+ if (!/^[0-9]+$/.test(trimmed)) return void 0;
18163
+ const parsed = Number(trimmed);
18164
+ return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : void 0;
18165
+ }
18166
+ function parsePositiveIntegerEnv(name, defaultValue) {
18167
+ const value = envRaw(name);
18168
+ if (value === void 0) return defaultValue;
18169
+ const parsed = parsePositiveInteger(value);
18170
+ if (parsed === void 0) {
18171
+ throw new Error(`${name} must be a positive integer`);
18172
+ }
18173
+ return parsed;
18046
18174
  }
18047
18175
  function parseStringArray(value) {
18048
18176
  if (value === void 0) return void 0;
@@ -18050,159 +18178,173 @@ function parseStringArray(value) {
18050
18178
  if (trimmed === "") return [];
18051
18179
  return trimmed.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
18052
18180
  }
18053
- function validateToolNames(toolNames, listName) {
18181
+ function validateToolNames(toolNames, envName) {
18054
18182
  if (!toolNames || toolNames.length === 0) return;
18055
18183
  const known = new Set(KNOWN_TOOLS);
18056
18184
  for (const name of toolNames) {
18057
18185
  if (!known.has(name)) {
18058
18186
  throw new Error(
18059
- `Unknown tool "${name}" in ${listName}. Known tools: ${KNOWN_TOOLS.join(", ")}`
18187
+ `Unknown tool "${name}" in ${envName}. Known tools: ${KNOWN_TOOLS.join(", ")}`
18060
18188
  );
18061
18189
  }
18062
18190
  }
18063
18191
  }
18064
- function loadConfig(configPath, cliOverrides = {}) {
18065
- let raw = {};
18066
- if (configPath) {
18067
- try {
18068
- raw = JSON.parse(readFileSync2(configPath, "utf-8"));
18069
- } catch (err) {
18070
- const detail = err instanceof SyntaxError ? "Invalid JSON" : err instanceof Error ? err.message : String(err);
18071
- throw new Error(`Failed to read config file "${configPath}": ${detail}`);
18192
+ function validateWebhookUrl(raw) {
18193
+ try {
18194
+ const url = new URL(raw);
18195
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
18196
+ throw new Error("unsupported protocol");
18072
18197
  }
18198
+ return raw;
18199
+ } catch {
18200
+ throw new Error(`${ENV_WATCH_WEBHOOK_URL} must be a valid http(s) URL`);
18073
18201
  }
18074
- raw = deepResolve(raw);
18075
- const parsed = rawConfigSchema.parse(raw);
18076
- const http = {
18077
- enabled: cliOverrides.http ?? parsed.http?.enabled ?? parseBool(process.env[ENV_HTTP_ENABLED]) ?? false,
18078
- port: cliOverrides.port ?? parsed.http?.port ?? parseIntSafe(process.env[ENV_HTTP_PORT]) ?? 3e3,
18079
- host: cliOverrides.host ?? parsed.http?.host ?? process.env[ENV_HTTP_HOST] ?? "127.0.0.1"
18080
- };
18081
- const toolsDisabled = parsed.tools?.disabled ?? parseStringArray(process.env[ENV_TOOLS_DISABLED]);
18082
- const toolsEnabled = parsed.tools?.enabled ?? parseStringArray(process.env[ENV_TOOLS_ENABLED]);
18083
- if (toolsDisabled && toolsEnabled) {
18084
- throw new Error(
18085
- "tools.disabled and tools.enabled are mutually exclusive \u2014 use one or the other"
18202
+ }
18203
+ function resolveHttpConfig(transport, cliOverrides, warnings) {
18204
+ const portSource = cliOverrides.port !== void 0 ? "--port" : ENV_HTTP_PORT;
18205
+ const rawPort = cliOverrides.port ?? envRaw(ENV_HTTP_PORT);
18206
+ const parsedPort = parsePositiveInteger(rawPort);
18207
+ let port = DEFAULT_HTTP_PORT;
18208
+ if (parsedPort !== void 0 && parsedPort <= 65535) {
18209
+ port = parsedPort;
18210
+ } else if (rawPort !== void 0 && transport === "http") {
18211
+ warnings.push(
18212
+ `Invalid ${portSource}; using default HTTP port ${DEFAULT_HTTP_PORT}.`
18086
18213
  );
18087
18214
  }
18088
- if (toolsEnabled !== void 0 && toolsEnabled.length === 0) {
18215
+ const hostSource = cliOverrides.host !== void 0 ? "--host" : ENV_HTTP_HOST;
18216
+ const rawHost = cliOverrides.host ?? envRaw(ENV_HTTP_HOST);
18217
+ let host = DEFAULT_HTTP_HOST;
18218
+ if (rawHost !== void 0 && rawHost.trim().length > 0) {
18219
+ host = rawHost.trim();
18220
+ } else if (rawHost !== void 0 && transport === "http") {
18221
+ warnings.push(
18222
+ `Invalid ${hostSource}; using default HTTP host ${DEFAULT_HTTP_HOST}.`
18223
+ );
18224
+ }
18225
+ return { port, host };
18226
+ }
18227
+ function resolveToolsConfig() {
18228
+ const disabled = parseStringArray(envRaw(ENV_TOOLS_DISABLED));
18229
+ const enabled = parseStringArray(envRaw(ENV_TOOLS_ENABLED));
18230
+ const disabledIsNonEmpty = disabled !== void 0 && disabled.length > 0;
18231
+ const enabledIsNonEmpty = enabled !== void 0 && enabled.length > 0;
18232
+ if (disabledIsNonEmpty && enabledIsNonEmpty) {
18089
18233
  throw new Error(
18090
- "tools.enabled is empty \u2014 at least one tool must be listed. To enable all tools, omit the tools section entirely."
18234
+ `${ENV_TOOLS_DISABLED} and ${ENV_TOOLS_ENABLED} are mutually exclusive \u2014 use one or the other`
18091
18235
  );
18092
18236
  }
18093
- validateToolNames(toolsDisabled, "tools.disabled");
18094
- validateToolNames(toolsEnabled, "tools.enabled");
18095
- const tools = toolsDisabled || toolsEnabled ? { disabled: toolsDisabled, enabled: toolsEnabled } : void 0;
18096
- const outlookClientId = parsed.providers?.outlook?.clientId ?? process.env[ENV_OUTLOOK_CLIENT_ID];
18097
- const outlookTenantId = parsed.providers?.outlook?.tenantId ?? process.env[ENV_OUTLOOK_TENANT_ID];
18098
- const gmailClientId = parsed.providers?.gmail?.clientId ?? process.env[ENV_GMAIL_CLIENT_ID];
18099
- const gmailClientSecret = parsed.providers?.gmail?.clientSecret ?? process.env[ENV_GMAIL_CLIENT_SECRET];
18237
+ validateToolNames(disabled, ENV_TOOLS_DISABLED);
18238
+ validateToolNames(enabled, ENV_TOOLS_ENABLED);
18239
+ if (enabledIsNonEmpty) return { enabled };
18240
+ if (disabledIsNonEmpty) return { disabled };
18241
+ return void 0;
18242
+ }
18243
+ function resolveProvidersConfig() {
18244
+ const outlookClientId = optionalEnvString(ENV_OUTLOOK_CLIENT_ID);
18245
+ const outlookTenantId = optionalEnvString(ENV_OUTLOOK_TENANT_ID);
18246
+ const gmailClientId = optionalEnvString(ENV_GMAIL_CLIENT_ID);
18247
+ const gmailClientSecret = optionalEnvString(ENV_GMAIL_CLIENT_SECRET);
18248
+ const gmailRedirectUri = optionalEnvString(ENV_GMAIL_REDIRECT_URI);
18100
18249
  let providers;
18101
- if (outlookClientId || outlookTenantId || gmailClientId || gmailClientSecret) {
18250
+ if (outlookClientId || outlookTenantId || gmailClientId || gmailClientSecret || gmailRedirectUri) {
18102
18251
  providers = {};
18103
18252
  if (outlookClientId || outlookTenantId) {
18104
18253
  providers.outlook = {};
18105
18254
  if (outlookClientId) providers.outlook.clientId = outlookClientId;
18106
18255
  if (outlookTenantId) providers.outlook.tenantId = outlookTenantId;
18107
18256
  }
18108
- if (gmailClientId || gmailClientSecret) {
18257
+ if (gmailClientId || gmailClientSecret || gmailRedirectUri) {
18109
18258
  providers.gmail = {};
18110
18259
  if (gmailClientId) providers.gmail.clientId = gmailClientId;
18111
18260
  if (gmailClientSecret) providers.gmail.clientSecret = gmailClientSecret;
18261
+ if (gmailRedirectUri) providers.gmail.redirectUri = gmailRedirectUri;
18112
18262
  }
18113
18263
  }
18114
- const watchEnabledEnv = parseBool(process.env[ENV_WATCH_ENABLED]);
18115
- let watch;
18116
- if (parsed.watch || watchEnabledEnv !== void 0) {
18117
- const webhookUrl = parsed.watch?.webhook?.url ?? process.env[ENV_WATCH_WEBHOOK_URL];
18118
- let webhook;
18119
- if (webhookUrl) {
18120
- const retryMaxAttempts = parsed.watch?.webhook?.retry?.maxAttempts ?? parseIntSafe(process.env[ENV_WATCH_WEBHOOK_RETRY_MAX_ATTEMPTS]) ?? 5;
18121
- const retryBaseDelayMs = parsed.watch?.webhook?.retry?.baseDelayMs ?? parseIntSafe(process.env[ENV_WATCH_WEBHOOK_RETRY_BASE_DELAY]) ?? 1e3;
18122
- webhook = {
18123
- url: webhookUrl,
18124
- retry: { maxAttempts: retryMaxAttempts, baseDelayMs: retryBaseDelayMs }
18125
- };
18126
- }
18127
- const scriptPath = parsed.watch?.script?.path ?? process.env[ENV_WATCH_SCRIPT_PATH];
18128
- let script;
18129
- if (scriptPath) {
18130
- const scriptTimeoutMs = parsed.watch?.script?.timeoutMs ?? parseIntSafe(process.env[ENV_WATCH_SCRIPT_TIMEOUT_MS]) ?? 3e4;
18131
- const scriptRetryMaxAttempts = parsed.watch?.script?.retry?.maxAttempts ?? parseIntSafe(process.env[ENV_WATCH_SCRIPT_RETRY_MAX_ATTEMPTS]) ?? 5;
18132
- const scriptRetryBaseDelayMs = parsed.watch?.script?.retry?.baseDelayMs ?? parseIntSafe(process.env[ENV_WATCH_SCRIPT_RETRY_BASE_DELAY]) ?? 1e3;
18133
- script = {
18134
- path: scriptPath,
18135
- timeoutMs: scriptTimeoutMs,
18136
- retry: {
18137
- maxAttempts: scriptRetryMaxAttempts,
18138
- baseDelayMs: scriptRetryBaseDelayMs
18139
- }
18140
- };
18141
- }
18142
- watch = {
18143
- enabled: watchEnabledEnv ?? parsed.watch?.enabled ?? false,
18144
- pollIntervalSeconds: parsed.watch?.pollIntervalSeconds ?? parseIntSafe(process.env[ENV_WATCH_POLL_INTERVAL]) ?? 10,
18145
- webhook,
18146
- script
18264
+ return providers;
18265
+ }
18266
+ function resolveRetryConfig(attemptsEnv, delayEnv) {
18267
+ return {
18268
+ maxAttempts: parsePositiveIntegerEnv(attemptsEnv, DEFAULT_RETRY_ATTEMPTS),
18269
+ baseDelayMs: parsePositiveIntegerEnv(delayEnv, DEFAULT_RETRY_DELAY_MS)
18270
+ };
18271
+ }
18272
+ function resolveWatchConfig() {
18273
+ const enabled = parseBoolEnv(ENV_WATCH_ENABLED) ?? false;
18274
+ if (!enabled) return void 0;
18275
+ const pollIntervalSeconds = parsePositiveIntegerEnv(
18276
+ ENV_WATCH_POLL_SECONDS,
18277
+ DEFAULT_WATCH_POLL_SECONDS
18278
+ );
18279
+ const webhookUrl = optionalEnvString(ENV_WATCH_WEBHOOK_URL);
18280
+ let webhook;
18281
+ if (webhookUrl) {
18282
+ webhook = {
18283
+ url: validateWebhookUrl(webhookUrl),
18284
+ retry: resolveRetryConfig(
18285
+ ENV_WATCH_WEBHOOK_RETRY_ATTEMPTS,
18286
+ ENV_WATCH_WEBHOOK_RETRY_DELAY_MS
18287
+ )
18147
18288
  };
18148
18289
  }
18290
+ const rawNotifyCommand = envRaw(ENV_WATCH_NOTIFY_COMMAND);
18291
+ let notifyCommand;
18292
+ if (rawNotifyCommand !== void 0) {
18293
+ const command = rawNotifyCommand.trim();
18294
+ if (!command) {
18295
+ throw new Error(`${ENV_WATCH_NOTIFY_COMMAND} must not be empty when watch is enabled`);
18296
+ }
18297
+ notifyCommand = {
18298
+ command,
18299
+ timeoutMs: parsePositiveIntegerEnv(
18300
+ ENV_WATCH_NOTIFY_TIMEOUT_MS,
18301
+ DEFAULT_NOTIFY_TIMEOUT_MS
18302
+ ),
18303
+ retry: resolveRetryConfig(
18304
+ ENV_WATCH_NOTIFY_RETRY_ATTEMPTS,
18305
+ ENV_WATCH_NOTIFY_RETRY_DELAY_MS
18306
+ )
18307
+ };
18308
+ }
18309
+ if (!webhook && !notifyCommand) {
18310
+ throw new Error(
18311
+ `${ENV_WATCH_ENABLED}=true requires ${ENV_WATCH_WEBHOOK_URL} or ${ENV_WATCH_NOTIFY_COMMAND}`
18312
+ );
18313
+ }
18314
+ return {
18315
+ enabled: true,
18316
+ pollIntervalSeconds,
18317
+ webhook,
18318
+ notifyCommand
18319
+ };
18320
+ }
18321
+ function loadConfig(cliOverrides = {}) {
18322
+ const warnings = [];
18323
+ const transport = cliOverrides.transport ?? parseTransportEnv() ?? DEFAULT_TRANSPORT;
18324
+ const http = resolveHttpConfig(transport, cliOverrides, warnings);
18325
+ const tools = resolveToolsConfig();
18326
+ const providers = resolveProvidersConfig();
18327
+ const watch = resolveWatchConfig();
18328
+ const dataDir = cliOverrides.dataDir ?? optionalEnvString(ENV_DATA_DIR);
18329
+ if (!optionalEnvString(ENV_KEY)) {
18330
+ warnings.push(
18331
+ `${ENV_KEY} is not set; a local generated key will be used. Set ${ENV_KEY} explicitly for portable hosted deployments.`
18332
+ );
18333
+ }
18149
18334
  return {
18150
- dataDir: cliOverrides.dataDir ?? parsed.dataDir ?? process.env.HYPERMAIL_MCP_DATA_DIR,
18151
- http,
18152
- tools,
18153
- providers,
18154
- watch
18335
+ config: {
18336
+ dataDir,
18337
+ transport,
18338
+ http,
18339
+ tools,
18340
+ providers,
18341
+ watch
18342
+ },
18343
+ warnings
18155
18344
  };
18156
18345
  }
18157
18346
 
18158
18347
  // src/config.ts
18159
- var httpConfigSchema = z8.object({
18160
- enabled: z8.boolean().default(false),
18161
- port: z8.number().int().min(1).max(65535).default(3e3),
18162
- host: z8.string().default("127.0.0.1")
18163
- });
18164
- var toolsConfigSchema = z8.object({
18165
- disabled: z8.array(z8.string()).optional(),
18166
- enabled: z8.array(z8.string()).optional()
18167
- });
18168
- var outlookProviderSchema = z8.object({
18169
- clientId: z8.string().optional(),
18170
- tenantId: z8.string().optional()
18171
- });
18172
- var gmailProviderSchema = z8.object({
18173
- clientId: z8.string().optional(),
18174
- clientSecret: z8.string().optional()
18175
- });
18176
- var providersConfigSchema = z8.object({
18177
- outlook: outlookProviderSchema.optional(),
18178
- gmail: gmailProviderSchema.optional()
18179
- });
18180
- var watchRetrySchema = z8.object({
18181
- maxAttempts: z8.number().int().min(1).max(10).default(5),
18182
- baseDelayMs: z8.number().int().min(100).default(1e3)
18183
- });
18184
- var watchWebhookSchema = z8.object({
18185
- url: z8.string(),
18186
- retry: watchRetrySchema.optional()
18187
- });
18188
- var watchScriptSchema = z8.object({
18189
- path: z8.string(),
18190
- timeoutMs: z8.number().int().min(1e3).default(3e4),
18191
- retry: watchRetrySchema.optional()
18192
- });
18193
- var watchConfigSchema = z8.object({
18194
- enabled: z8.boolean().default(false),
18195
- pollIntervalSeconds: z8.number().int().min(10).max(3600).default(10),
18196
- webhook: watchWebhookSchema.optional(),
18197
- script: watchScriptSchema.optional()
18198
- });
18199
- var rawConfigSchema = z8.object({
18200
- dataDir: z8.string().optional(),
18201
- http: httpConfigSchema.optional(),
18202
- tools: toolsConfigSchema.optional(),
18203
- providers: providersConfigSchema.optional(),
18204
- watch: watchConfigSchema.optional()
18205
- });
18206
18348
  var KNOWN_TOOLS = [
18207
18349
  "list_accounts",
18208
18350
  "add_account",
@@ -18226,8 +18368,7 @@ var KNOWN_TOOLS = [
18226
18368
  "send_email",
18227
18369
  "draft_email",
18228
18370
  "edit_draft",
18229
- "send_draft",
18230
- "check_notifications"
18371
+ "send_draft"
18231
18372
  ];
18232
18373
  function resolveTools(config) {
18233
18374
  if (!config.tools) {
@@ -18261,19 +18402,68 @@ async function startServer(opts) {
18261
18402
  registerTools(s, { store, registry, tools });
18262
18403
  return s;
18263
18404
  };
18264
- if (config.http.enabled) {
18265
- await startHttp(createServer, config.http.host, config.http.port);
18405
+ if (config.transport === "http") {
18406
+ await startHttp(createServer, registry, config.http.host, config.http.port);
18266
18407
  } else {
18267
18408
  const server = createServer();
18268
18409
  const transport = new StdioServerTransport();
18269
18410
  await server.connect(transport);
18270
18411
  }
18271
18412
  }
18272
- async function startHttp(createServer, host, port) {
18413
+ function firstHeader(value) {
18414
+ return Array.isArray(value) ? value[0] : value;
18415
+ }
18416
+ function requestBaseUrl(req) {
18417
+ const proto = firstHeader(req.headers["x-forwarded-proto"]) ?? "http";
18418
+ const host = firstHeader(req.headers["x-forwarded-host"]) ?? firstHeader(req.headers.host) ?? "127.0.0.1";
18419
+ return `${proto}://${host}`;
18420
+ }
18421
+ function escapeHtml2(value) {
18422
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
18423
+ }
18424
+ function sendOAuthHtml(res, status, title, body) {
18425
+ res.statusCode = status;
18426
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
18427
+ res.end(`<!doctype html>
18428
+ <html lang="en">
18429
+ <head><meta charset="utf-8"><title>${escapeHtml2(title)}</title></head>
18430
+ <body><h1>${escapeHtml2(title)}</h1><p>${escapeHtml2(body)}</p></body>
18431
+ </html>`);
18432
+ }
18433
+ async function handleGmailOAuthCallback(req, res, registry) {
18434
+ const provider = registry.get("gmail");
18435
+ if (!provider.completeAddAccountFromRedirect) {
18436
+ sendOAuthHtml(res, 500, "Gmail authorization failed", "This server cannot complete Gmail OAuth callbacks.");
18437
+ return;
18438
+ }
18439
+ const authorizationResponse = new URL(req.url ?? "", requestBaseUrl(req)).toString();
18440
+ const result = await provider.completeAddAccountFromRedirect(authorizationResponse);
18441
+ if (result.status === "ready") {
18442
+ sendOAuthHtml(res, 200, "Gmail authorization complete", "You can close this tab and return to your MCP client.");
18443
+ return;
18444
+ }
18445
+ sendOAuthHtml(
18446
+ res,
18447
+ 400,
18448
+ "Gmail authorization failed",
18449
+ result.status === "error" ? result.error ?? "Unknown Gmail OAuth error." : "The Gmail OAuth flow was not ready. Restart account setup and try again."
18450
+ );
18451
+ }
18452
+ async function startHttp(createServer, registry, host, port) {
18273
18453
  const sessions = /* @__PURE__ */ new Map();
18274
- const http = createHttpServer(async (req, res) => {
18454
+ const http = createHttpServer2(async (req, res) => {
18275
18455
  try {
18276
- if (!req.url || !req.url.startsWith("/mcp")) {
18456
+ if (!req.url) {
18457
+ res.statusCode = 404;
18458
+ res.end("not found");
18459
+ return;
18460
+ }
18461
+ const pathname = new URL(req.url, requestBaseUrl(req)).pathname;
18462
+ if (req.method === "GET" && pathname === DEFAULT_GMAIL_OAUTH_CALLBACK_PATH) {
18463
+ await handleGmailOAuthCallback(req, res, registry);
18464
+ return;
18465
+ }
18466
+ if (!req.url.startsWith("/mcp")) {
18277
18467
  res.statusCode = 404;
18278
18468
  res.end("not found");
18279
18469
  return;
@@ -18314,128 +18504,136 @@ async function startHttp(createServer, host, port) {
18314
18504
  console.error(`[hypermail-mcp] listening on http://${host}:${port}/mcp`);
18315
18505
  }
18316
18506
 
18317
- // src/cli.ts
18507
+ // src/cli-args.ts
18508
+ import { randomBytes as randomBytes3 } from "crypto";
18509
+ function readValue(argv, index, flag) {
18510
+ const value = argv[index + 1];
18511
+ if (value === void 0 || value.startsWith("--")) {
18512
+ throw new Error(`${flag} requires a value`);
18513
+ }
18514
+ return value;
18515
+ }
18318
18516
  function parseArgs(argv) {
18517
+ if (argv[0] === "generate-key") {
18518
+ if (argv.length > 1) {
18519
+ throw new Error(`Unknown argument for generate-key: ${argv[1]}`);
18520
+ }
18521
+ return { command: "generate-key", help: false, overrides: {} };
18522
+ }
18319
18523
  const out = {
18320
- http: false,
18321
- port: 3e3,
18322
- host: "127.0.0.1",
18323
- help: false
18524
+ help: false,
18525
+ overrides: {}
18324
18526
  };
18325
18527
  for (let i = 0; i < argv.length; i++) {
18326
- const a = argv[i];
18327
- switch (a) {
18528
+ const arg = argv[i] ?? "";
18529
+ switch (arg) {
18328
18530
  case "--http":
18329
- out.http = true;
18531
+ out.overrides.transport = "http";
18330
18532
  break;
18331
- case "--port":
18332
- out.port = Number(argv[++i] ?? "3000");
18333
- break;
18334
- case "--host":
18335
- out.host = String(argv[++i] ?? "127.0.0.1");
18533
+ case "--port": {
18534
+ const value = readValue(argv, i, "--port");
18535
+ out.overrides.port = Number(value);
18536
+ i++;
18336
18537
  break;
18337
- case "--data-dir":
18338
- out.dataDir = String(argv[++i] ?? "");
18538
+ }
18539
+ case "--host": {
18540
+ const value = readValue(argv, i, "--host");
18541
+ out.overrides.host = value;
18542
+ i++;
18339
18543
  break;
18340
- case "--config":
18341
- out.config = String(argv[++i] ?? "");
18544
+ }
18545
+ case "--data-dir": {
18546
+ const value = readValue(argv, i, "--data-dir");
18547
+ out.overrides.dataDir = value;
18548
+ i++;
18342
18549
  break;
18550
+ }
18343
18551
  case "-h":
18344
18552
  case "--help":
18345
18553
  out.help = true;
18346
18554
  break;
18347
18555
  default:
18348
- if (a && a.startsWith("--")) {
18556
+ if (arg.startsWith("-")) {
18557
+ throw new Error(`Unknown option: ${arg}`);
18349
18558
  }
18559
+ throw new Error(`Unknown argument: ${arg}`);
18350
18560
  }
18351
18561
  }
18352
18562
  return out;
18353
18563
  }
18354
- function printHelp() {
18355
- const msg = `hypermail-mcp \u2014 unified email MCP server
18564
+ function generateKey() {
18565
+ return randomBytes3(32).toString("base64");
18566
+ }
18567
+ function helpText() {
18568
+ return `hypermail-mcp \u2014 unified email MCP server
18356
18569
 
18357
18570
  Usage:
18358
18571
  hypermail-mcp [options]
18572
+ hypermail-mcp generate-key
18359
18573
 
18360
18574
  Options:
18361
18575
  --http Run as Streamable HTTP server (default: stdio)
18362
18576
  --port <n> HTTP port (default: 3000)
18363
18577
  --host <addr> HTTP bind address (default: 127.0.0.1)
18364
18578
  --data-dir <path> Where to store the encrypted accounts file
18365
- (default: $HYPERMAIL_MCP_DATA_DIR or ~/.hypermail-mcp)
18366
- --config <path> Path to hypermail-config.json
18367
18579
  -h, --help Show this help
18368
18580
 
18369
18581
  Configuration:
18370
- All settings can be provided via environment variables \u2014 no config file
18371
- required. Use hypermail-config.json for advanced scenarios.
18582
+ Configure the server with flat HYPERMAIL_* environment variables.
18583
+ CLI flags override environment values for this invocation only.
18372
18584
 
18373
- Environment variables:
18585
+ Core environment variables:
18586
+ HYPERMAIL_DATA_DIR
18587
+ HYPERMAIL_KEY
18588
+ HYPERMAIL_TRANSPORT=stdio|http
18589
+ HYPERMAIL_HTTP_PORT
18590
+ HYPERMAIL_HTTP_HOST
18591
+ HYPERMAIL_TOOLS_ENABLED
18592
+ HYPERMAIL_TOOLS_DISABLED
18374
18593
 
18375
- HYPERMAIL_MCP_DATA_DIR Data directory (string)
18376
- HYPERMAIL_HTTP_ENABLED Enable HTTP mode (bool: true/false/1/0)
18377
- HYPERMAIL_HTTP_PORT HTTP port (number)
18378
- HYPERMAIL_HTTP_HOST HTTP bind address (string)
18379
- HYPERMAIL_TOOLS_DISABLED Comma-separated tool names to disable
18380
- HYPERMAIL_TOOLS_ENABLED Comma-separated tool names to enable
18381
- HYPERMAIL_PROVIDERS_OUTLOOK_CLIENT_ID Outlook OAuth client ID (string)
18382
- HYPERMAIL_PROVIDERS_OUTLOOK_TENANT_ID Outlook tenant ID (string)
18383
- HYPERMAIL_PROVIDERS_GMAIL_CLIENT_ID Gmail OAuth client ID (string)
18384
- HYPERMAIL_PROVIDERS_GMAIL_CLIENT_SECRET Gmail OAuth client secret (string)
18385
- HYPERMAIL_WATCH_ENABLED Enable inbox polling (bool)
18386
- HYPERMAIL_WATCH_POLL_INTERVAL Poll interval in seconds (number)
18387
- HYPERMAIL_WATCH_WEBHOOK_URL Webhook URL for new-email events (string)
18388
- HYPERMAIL_WATCH_WEBHOOK_RETRY_MAX_ATTEMPTS Retry max attempts (number)
18389
- HYPERMAIL_WATCH_WEBHOOK_RETRY_BASE_DELAY_MS Retry base delay ms (number)
18390
- HYPERMAIL_MCP_KEY Encryption master key (hex or base64)
18594
+ Provider environment variables:
18595
+ HYPERMAIL_OUTLOOK_CLIENT_ID
18596
+ HYPERMAIL_OUTLOOK_TENANT_ID
18597
+ HYPERMAIL_GMAIL_CLIENT_ID
18598
+ HYPERMAIL_GMAIL_CLIENT_SECRET
18599
+ HYPERMAIL_GMAIL_REDIRECT_URI
18391
18600
 
18392
- Priority: CLI flags > config file > env vars > defaults.
18601
+ Watcher environment variables:
18602
+ HYPERMAIL_WATCH_ENABLED=true|false
18603
+ HYPERMAIL_WATCH_POLL_SECONDS
18604
+ HYPERMAIL_WATCH_WEBHOOK_URL
18605
+ HYPERMAIL_WATCH_WEBHOOK_RETRY_ATTEMPTS
18606
+ HYPERMAIL_WATCH_WEBHOOK_RETRY_DELAY_MS
18607
+ HYPERMAIL_WATCH_NOTIFY_COMMAND
18608
+ HYPERMAIL_WATCH_NOTIFY_TIMEOUT_MS
18609
+ HYPERMAIL_WATCH_NOTIFY_RETRY_ATTEMPTS
18610
+ HYPERMAIL_WATCH_NOTIFY_RETRY_DELAY_MS
18393
18611
 
18394
- Example (env-only, no config file):
18395
- HYPERMAIL_HTTP_ENABLED=true \\
18396
- HYPERMAIL_HTTP_PORT=8080 \\
18397
- HYPERMAIL_PROVIDERS_OUTLOOK_CLIENT_ID=abc123 \\
18398
- HYPERMAIL_PROVIDERS_OUTLOOK_TENANT_ID=common \\
18399
- HYPERMAIL_MCP_DATA_DIR=/data/hypermail \\
18400
- hypermail-mcp --http
18401
-
18402
- Example hypermail-config.json:
18403
- {
18404
- "dataDir": "/path/to/data",
18405
- "http": { "enabled": false },
18406
- "tools": { "disabled": ["send_email"] },
18407
- "providers": {
18408
- "outlook": {
18409
- "clientId": "\${HYPERMAIL_PROVIDERS_OUTLOOK_CLIENT_ID}",
18410
- "tenantId": "\${HYPERMAIL_PROVIDERS_OUTLOOK_TENANT_ID}"
18411
- },
18412
- "gmail": {
18413
- "clientId": "\${HYPERMAIL_PROVIDERS_GMAIL_CLIENT_ID}",
18414
- "clientSecret": "\${HYPERMAIL_PROVIDERS_GMAIL_CLIENT_SECRET}"
18415
- }
18416
- }
18417
- }
18612
+ Example:
18613
+ HYPERMAIL_TRANSPORT=http \\
18614
+ HYPERMAIL_HTTP_PORT=8080 \\
18615
+ HYPERMAIL_OUTLOOK_CLIENT_ID=... \\
18616
+ HYPERMAIL_DATA_DIR=/data/hypermail \\
18617
+ hypermail-mcp
18418
18618
  `;
18419
- process.stdout.write(msg);
18420
18619
  }
18620
+
18621
+ // src/cli.ts
18421
18622
  async function main() {
18422
- const rawArgs = process.argv.slice(2);
18423
- if (rawArgs[0] === "generate-key") {
18424
- const key = `hm_sk_${randomBytes2(32).toString("hex")}`;
18425
- process.stdout.write(key + "\n");
18623
+ const opts = parseArgs(process.argv.slice(2));
18624
+ if (opts.command === "generate-key") {
18625
+ process.stdout.write(generateKey() + "\n");
18426
18626
  return;
18427
18627
  }
18428
- const opts = parseArgs(rawArgs);
18429
18628
  if (opts.help) {
18430
- printHelp();
18629
+ process.stdout.write(helpText());
18431
18630
  return;
18432
18631
  }
18433
- const config = loadConfig(opts.config, {
18434
- http: opts.http,
18435
- port: opts.port,
18436
- host: opts.host,
18437
- dataDir: opts.dataDir
18438
- });
18632
+ const { config, warnings } = loadConfig(opts.overrides);
18633
+ for (const warning of warnings) {
18634
+ process.stderr.write(`[hypermail-mcp] warning: ${warning}
18635
+ `);
18636
+ }
18439
18637
  await startServer({ config });
18440
18638
  }
18441
18639
  main().catch((err) => {