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/README.md +174 -138
- package/dist/cli.js +622 -424
- package/dist/cli.js.map +1 -1
- package/package.json +2 -2
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
|
|
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
|
|
13901
|
-
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
15593
|
-
const
|
|
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
|
-
"
|
|
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
|
|
15600
|
-
|
|
15601
|
-
|
|
15602
|
-
|
|
15603
|
-
|
|
15604
|
-
|
|
15605
|
-
|
|
15606
|
-
|
|
15607
|
-
|
|
15608
|
-
|
|
15609
|
-
|
|
15610
|
-
|
|
15611
|
-
|
|
15612
|
-
|
|
15613
|
-
|
|
15614
|
-
|
|
15615
|
-
|
|
15616
|
-
|
|
15617
|
-
|
|
15618
|
-
|
|
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
|
-
|
|
15699
|
-
|
|
15700
|
-
|
|
15701
|
-
|
|
15702
|
-
|
|
15703
|
-
|
|
15704
|
-
|
|
15705
|
-
|
|
15706
|
-
|
|
15707
|
-
|
|
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
|
-
|
|
15712
|
+
localCallback?.cancel();
|
|
15713
15713
|
},
|
|
15714
|
-
|
|
15714
|
+
consumeAuthorizationResponse() {
|
|
15715
|
+
return localCallback?.consumeAuthorizationResponse();
|
|
15716
|
+
}
|
|
15715
15717
|
};
|
|
15716
15718
|
}
|
|
15717
|
-
|
|
15718
|
-
|
|
15719
|
-
|
|
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 =
|
|
16418
|
-
|
|
16419
|
-
this.
|
|
16420
|
-
this.
|
|
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
|
-
|
|
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
|
-
|
|
16609
|
-
|
|
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.
|
|
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
|
|
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
|
|
17846
|
-
if (!config.
|
|
17847
|
-
const {
|
|
17848
|
-
const maxAttempts = retry
|
|
17849
|
-
const baseDelayMs = retry
|
|
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
|
-
|
|
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]
|
|
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]
|
|
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]
|
|
17982
|
+
`[hypermail-watch] notify command delivery failed after ${maxAttempts} retries for ${email.id}`
|
|
17873
17983
|
);
|
|
17874
17984
|
return false;
|
|
17875
17985
|
}
|
|
17876
|
-
function spawnWithTimeout(
|
|
17986
|
+
function spawnWithTimeout(command, stdinData, timeoutMs) {
|
|
17877
17987
|
return new Promise((resolve) => {
|
|
17878
|
-
const child = spawn(
|
|
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]
|
|
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]
|
|
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]
|
|
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
|
-
|
|
18094
|
+
runNotifyCommand(full, this.config).catch((err) => {
|
|
17984
18095
|
console.error(
|
|
17985
|
-
`[hypermail-watch]
|
|
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
|
-
|
|
17997
|
-
var
|
|
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 = "
|
|
18003
|
-
var ENV_OUTLOOK_TENANT_ID = "
|
|
18004
|
-
var ENV_GMAIL_CLIENT_ID = "
|
|
18005
|
-
var ENV_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
|
|
18117
|
+
var ENV_WATCH_POLL_SECONDS = "HYPERMAIL_WATCH_POLL_SECONDS";
|
|
18008
18118
|
var ENV_WATCH_WEBHOOK_URL = "HYPERMAIL_WATCH_WEBHOOK_URL";
|
|
18009
|
-
var
|
|
18010
|
-
var
|
|
18011
|
-
var
|
|
18012
|
-
var
|
|
18013
|
-
var
|
|
18014
|
-
var
|
|
18015
|
-
var
|
|
18016
|
-
|
|
18017
|
-
|
|
18018
|
-
|
|
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
|
|
18022
|
-
|
|
18023
|
-
if (
|
|
18024
|
-
|
|
18025
|
-
|
|
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
|
|
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"
|
|
18037
|
-
if (lower === "false"
|
|
18038
|
-
|
|
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
|
|
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
|
|
18044
|
-
const
|
|
18045
|
-
return Number.
|
|
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,
|
|
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 ${
|
|
18187
|
+
`Unknown tool "${name}" in ${envName}. Known tools: ${KNOWN_TOOLS.join(", ")}`
|
|
18060
18188
|
);
|
|
18061
18189
|
}
|
|
18062
18190
|
}
|
|
18063
18191
|
}
|
|
18064
|
-
function
|
|
18065
|
-
|
|
18066
|
-
|
|
18067
|
-
|
|
18068
|
-
|
|
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
|
-
|
|
18075
|
-
|
|
18076
|
-
const
|
|
18077
|
-
|
|
18078
|
-
|
|
18079
|
-
|
|
18080
|
-
|
|
18081
|
-
|
|
18082
|
-
|
|
18083
|
-
|
|
18084
|
-
|
|
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
|
-
|
|
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
|
-
|
|
18234
|
+
`${ENV_TOOLS_DISABLED} and ${ENV_TOOLS_ENABLED} are mutually exclusive \u2014 use one or the other`
|
|
18091
18235
|
);
|
|
18092
18236
|
}
|
|
18093
|
-
validateToolNames(
|
|
18094
|
-
validateToolNames(
|
|
18095
|
-
|
|
18096
|
-
|
|
18097
|
-
|
|
18098
|
-
|
|
18099
|
-
|
|
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
|
-
|
|
18115
|
-
|
|
18116
|
-
|
|
18117
|
-
|
|
18118
|
-
|
|
18119
|
-
|
|
18120
|
-
|
|
18121
|
-
|
|
18122
|
-
|
|
18123
|
-
|
|
18124
|
-
|
|
18125
|
-
|
|
18126
|
-
|
|
18127
|
-
|
|
18128
|
-
|
|
18129
|
-
|
|
18130
|
-
|
|
18131
|
-
|
|
18132
|
-
|
|
18133
|
-
|
|
18134
|
-
|
|
18135
|
-
|
|
18136
|
-
|
|
18137
|
-
|
|
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
|
-
|
|
18151
|
-
|
|
18152
|
-
|
|
18153
|
-
|
|
18154
|
-
|
|
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
|
|
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
|
-
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
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 =
|
|
18454
|
+
const http = createHttpServer2(async (req, res) => {
|
|
18275
18455
|
try {
|
|
18276
|
-
if (!req.url
|
|
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
|
-
|
|
18321
|
-
|
|
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
|
|
18327
|
-
switch (
|
|
18528
|
+
const arg = argv[i] ?? "";
|
|
18529
|
+
switch (arg) {
|
|
18328
18530
|
case "--http":
|
|
18329
|
-
out.
|
|
18531
|
+
out.overrides.transport = "http";
|
|
18330
18532
|
break;
|
|
18331
|
-
case "--port":
|
|
18332
|
-
|
|
18333
|
-
|
|
18334
|
-
|
|
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
|
-
|
|
18338
|
-
|
|
18538
|
+
}
|
|
18539
|
+
case "--host": {
|
|
18540
|
+
const value = readValue(argv, i, "--host");
|
|
18541
|
+
out.overrides.host = value;
|
|
18542
|
+
i++;
|
|
18339
18543
|
break;
|
|
18340
|
-
|
|
18341
|
-
|
|
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 (
|
|
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
|
|
18355
|
-
|
|
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
|
-
|
|
18371
|
-
|
|
18582
|
+
Configure the server with flat HYPERMAIL_* environment variables.
|
|
18583
|
+
CLI flags override environment values for this invocation only.
|
|
18372
18584
|
|
|
18373
|
-
|
|
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
|
-
|
|
18376
|
-
|
|
18377
|
-
|
|
18378
|
-
|
|
18379
|
-
|
|
18380
|
-
|
|
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
|
-
|
|
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
|
-
|
|
18395
|
-
|
|
18396
|
-
|
|
18397
|
-
|
|
18398
|
-
|
|
18399
|
-
|
|
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
|
|
18423
|
-
if (
|
|
18424
|
-
|
|
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
|
-
|
|
18629
|
+
process.stdout.write(helpText());
|
|
18431
18630
|
return;
|
|
18432
18631
|
}
|
|
18433
|
-
const config = loadConfig(opts.
|
|
18434
|
-
|
|
18435
|
-
|
|
18436
|
-
|
|
18437
|
-
|
|
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) => {
|