import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import dotenv from "dotenv";
import { createLogger } from "./logger.js";
import {
DEFAULT_CONNECT_TIMEOUT_MS,
DEFAULT_MAX_ATTACHMENT_BYTES,
DEFAULT_MAX_ATTACHMENTS,
DEFAULT_MAX_HTML_CHARS,
DEFAULT_MAX_MESSAGE_BYTES,
DEFAULT_MAX_RECIPIENTS,
DEFAULT_MAX_TEXT_CHARS,
DEFAULT_SOCKET_TIMEOUT_MS,
listAccountIds,
} from "./config.js";
import { createServer } from "./server.js";
import { checkStartupEnv } from "./startup.js";
function readEnvString(env: NodeJS.ProcessEnv, key: string): string | undefined {
const value = env[key];
return value?.trim() ? value.trim() : undefined;
}
function readEnvBoolean(env: NodeJS.ProcessEnv, key: string, fallback: boolean): boolean {
const value = readEnvString(env, key);
if (!value) {
return fallback;
}
return value.toLowerCase() === "true";
}
function formatValue(value: string | undefined): string {
if (value?.trim()) {
return value;
}
return "<unset>";
}
function formatDefaultValue(value: string | undefined, fallback: string): string {
if (value?.trim()) {
return value;
}
return `${fallback} (default)`;
}
function formatSecret(value: string | undefined): string {
return value?.trim() ? "set (redacted)" : "<unset>";
}
function renderAccountEnv(env: NodeJS.ProcessEnv, accountId: string): string[] {
const upperId = accountId.toUpperCase();
const prefix = `MAIL_SMTP_${upperId}_`;
const secure = readEnvBoolean(env, `${prefix}SECURE`, false);
const defaultPort = secure ? 465 : 587;
return [
`${prefix}HOST=${formatValue(readEnvString(env, `${prefix}HOST`))}`,
`${prefix}PORT=${formatDefaultValue(
readEnvString(env, `${prefix}PORT`),
defaultPort.toString(),
)}`,
`${prefix}SECURE=${formatDefaultValue(
readEnvString(env, `${prefix}SECURE`),
secure.toString(),
)}`,
`${prefix}USER=${formatValue(readEnvString(env, `${prefix}USER`))}`,
`${prefix}PASS=${formatSecret(readEnvString(env, `${prefix}PASS`))}`,
`${prefix}FROM=${formatValue(readEnvString(env, `${prefix}FROM`))}`,
];
}
function renderHelp(env: NodeJS.ProcessEnv): string {
const lines: string[] = [];
const accountIds = listAccountIds(env);
lines.push("mail-smtp-mcp");
lines.push("Usage: mail-smtp-mcp [--help|-h]");
lines.push("");
lines.push("Environment:");
lines.push(" Secrets are redacted in this output.");
lines.push(" Accounts are discovered via MAIL_SMTP_<ID>_HOST.");
lines.push(" If none are set, no accounts are configured (server will not start).");
lines.push("");
const resolvedAccountIds = accountIds.length === 0 ? ["default"] : accountIds;
resolvedAccountIds.forEach((accountId) => {
const upperId = accountId.toUpperCase();
lines.push(` Account: ${accountId} (MAIL_SMTP_${upperId}_*)`);
renderAccountEnv(env, accountId).forEach((line) => {
lines.push(` ${line}`);
});
lines.push("");
});
lines.push(" Server settings:");
lines.push(
` MAIL_SMTP_SEND_ENABLED=${formatDefaultValue(
readEnvString(env, "MAIL_SMTP_SEND_ENABLED"),
"false",
)}`,
);
lines.push(
` MAIL_SMTP_ALLOWLIST_DOMAINS=${formatDefaultValue(
readEnvString(env, "MAIL_SMTP_ALLOWLIST_DOMAINS"),
"[]",
)}`,
);
lines.push(
` MAIL_SMTP_ALLOWLIST_ADDRESSES=${formatDefaultValue(
readEnvString(env, "MAIL_SMTP_ALLOWLIST_ADDRESSES"),
"[]",
)}`,
);
lines.push(
` MAIL_SMTP_MAX_RECIPIENTS=${formatDefaultValue(
readEnvString(env, "MAIL_SMTP_MAX_RECIPIENTS"),
DEFAULT_MAX_RECIPIENTS.toString(),
)}`,
);
lines.push(
` MAIL_SMTP_MAX_MESSAGE_BYTES=${formatDefaultValue(
readEnvString(env, "MAIL_SMTP_MAX_MESSAGE_BYTES"),
DEFAULT_MAX_MESSAGE_BYTES.toString(),
)}`,
);
lines.push(
` MAIL_SMTP_MAX_ATTACHMENTS=${formatDefaultValue(
readEnvString(env, "MAIL_SMTP_MAX_ATTACHMENTS"),
DEFAULT_MAX_ATTACHMENTS.toString(),
)}`,
);
lines.push(
` MAIL_SMTP_MAX_ATTACHMENT_BYTES=${formatDefaultValue(
readEnvString(env, "MAIL_SMTP_MAX_ATTACHMENT_BYTES"),
DEFAULT_MAX_ATTACHMENT_BYTES.toString(),
)}`,
);
lines.push(
` MAIL_SMTP_MAX_TEXT_CHARS=${formatDefaultValue(
readEnvString(env, "MAIL_SMTP_MAX_TEXT_CHARS"),
DEFAULT_MAX_TEXT_CHARS.toString(),
)}`,
);
lines.push(
` MAIL_SMTP_MAX_HTML_CHARS=${formatDefaultValue(
readEnvString(env, "MAIL_SMTP_MAX_HTML_CHARS"),
DEFAULT_MAX_HTML_CHARS.toString(),
)}`,
);
lines.push(
` MAIL_SMTP_CONNECT_TIMEOUT_MS=${formatDefaultValue(
readEnvString(env, "MAIL_SMTP_CONNECT_TIMEOUT_MS"),
DEFAULT_CONNECT_TIMEOUT_MS.toString(),
)}`,
);
lines.push(
` MAIL_SMTP_SOCKET_TIMEOUT_MS=${formatDefaultValue(
readEnvString(env, "MAIL_SMTP_SOCKET_TIMEOUT_MS"),
DEFAULT_SOCKET_TIMEOUT_MS.toString(),
)}`,
);
return lines.join("\n");
}
/**
* Start the MCP server over stdio transport.
*/
async function start(): Promise<void> {
dotenv.config({ quiet: true });
if (process.argv.slice(2).some((arg) => arg === "--help" || arg === "-h")) {
process.stdout.write(`${renderHelp(process.env)}\n`);
return;
}
const logger = createLogger();
const check = checkStartupEnv(process.env);
if (!check.ok) {
if (check.accountIds.length === 0) {
process.stderr.write(
"No SMTP accounts configured. Set MAIL_SMTP_<ID>_HOST/USER/PASS (e.g., MAIL_SMTP_DEFAULT_HOST).\n",
);
} else {
process.stderr.write("Missing required SMTP environment variables:\n");
check.missing.forEach((missingKey) => {
process.stderr.write(`- ${missingKey}\n`);
});
}
process.exit(1);
}
const server = createServer({ logger, env: process.env });
const transport = new StdioServerTransport();
await server.connect(transport);
}
start().catch((error: unknown) => {
const message = error instanceof Error ? error.message : "Server failed to start.";
process.stderr.write(`${message}\n`);
process.exit(1);
});