import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import {
listAccountsResultSchema,
listAccountsSchema,
sendMessageResultSchema,
sendMessageSchema,
verifyAccountResultSchema,
verifyAccountSchema,
} from "./schemas.js";
import { handleListAccounts } from "./tools/list_accounts.js";
import { handleSendMessage } from "./tools/send_message.js";
import { handleVerifyAccount } from "./tools/verify_account.js";
import type { Logger } from "./logger.js";
import type { ToolCallResult } from "./tools/response.js";
/**
* Configuration options for creating an MCP server instance.
*
* @property logger - The structured logger instance for logging events and errors.
* @property env - The environment variables containing SMTP configuration.
*/
export interface ServerOptions {
readonly logger: Logger;
readonly env: NodeJS.ProcessEnv;
}
/**
* Generic handler type for MCP tools.
*
* @template Input - The input type accepted by the tool handler.
*/
interface ToolHandler<Input> {
(input: Input): ToolCallResult | Promise<ToolCallResult>;
}
/**
* Get the current time in nanoseconds for duration measurement.
*
* @returns The current high-resolution time in nanoseconds.
*/
function nowNs(): bigint {
return process.hrtime.bigint();
}
/**
* Calculate elapsed milliseconds from a nanosecond timestamp.
*
* @param startedAtNs - The start time in nanoseconds.
* @returns The elapsed time in milliseconds.
*/
function durationMs(startedAtNs: bigint): number {
return Number(process.hrtime.bigint() - startedAtNs) / 1_000_000;
}
/**
* Return a safe, metric-focused summary of tool arguments for audit logs.
*
* Avoids logging message bodies, recipients, or attachment content.
*/
function summarizeToolArguments(toolName: string, input: unknown): unknown {
if (!input || typeof input !== "object") {
return input;
}
const data = input as Record<string, unknown>;
if (toolName !== "smtp_send_message") {
return data;
}
const listCount = (value: unknown): number => {
if (Array.isArray(value)) {
return value.length;
}
if (typeof value === "string") {
return value.length > 0 ? 1 : 0;
}
return 0;
};
const stringLength = (value: unknown): number | undefined =>
typeof value === "string" ? value.length : undefined;
const attachmentBase64Chars = (value: unknown): number => {
if (!Array.isArray(value)) {
return 0;
}
return value.reduce<number>((total, entry) => {
if (!entry || typeof entry !== "object") {
return total;
}
const content = (entry as Record<string, unknown>)["content_base64"];
if (typeof content !== "string") {
return total;
}
return total + content.length;
}, 0);
};
return {
account_id: typeof data["account_id"] === "string" ? data["account_id"] : undefined,
dry_run: typeof data["dry_run"] === "boolean" ? data["dry_run"] : undefined,
to_count: listCount(data["to"]),
cc_count: listCount(data["cc"]),
bcc_count: listCount(data["bcc"]),
subject_chars: stringLength(data["subject"]),
text_body_chars: stringLength(data["text_body"]),
html_body_chars: stringLength(data["html_body"]),
attachments_count: Array.isArray(data["attachments"]) ? data["attachments"].length : 0,
attachments_base64_chars: attachmentBase64Chars(data["attachments"]),
};
}
/**
* Wrap a tool handler with logging and error tracking.
*
* This wrapper automatically records execution duration and audit events,
* extracting error messages from tool results for observability.
*
* @template Input - The input type accepted by the tool handler.
* @param toolName - The name of the tool for logging purposes.
* @param handler - The original tool handler function to wrap.
* @param logger - The logger instance to use for audit events.
* @returns A wrapped tool handler with audit logging.
*/
function wrapToolHandler<Input>(
toolName: string,
handler: ToolHandler<Input>,
logger: Logger,
): ToolHandler<Input> {
return async (input: Input) => {
const startedAt = nowNs();
let errorMessage: string | undefined;
try {
const result = await handler(input);
// Extract error message from tool result if it indicates failure
if (result.isError) {
const first = result.content[0];
if (first) {
try {
const parsed = JSON.parse(first.text) as { error?: { message?: string } };
errorMessage = parsed.error?.message;
} catch {
errorMessage = "Tool failed.";
}
}
}
return result;
} catch (error) {
// Capture error from thrown exceptions
errorMessage = error instanceof Error ? error.message : "Tool failed.";
throw error;
} finally {
// Always log audit event with duration and error status
logger.audit(toolName, {
duration_ms: Math.round(durationMs(startedAt)),
arguments: summarizeToolArguments(toolName, input),
error: errorMessage ? { message: errorMessage } : undefined,
});
}
};
}
/**
* Create an MCP server instance wired with mail-smtp tools.
*/
/**
* Create an MCP server instance wired with mail-smtp tools.
*
* Registers three tools for SMTP operations:
* - smtp_list_accounts: List configured accounts
* - smtp_verify_account: Test account connectivity
* - smtp_send_message: Send emails with optional attachments
*
* All tool handlers are wrapped with audit logging and duration tracking.
*
* @param options - Server configuration including logger and environment.
* @returns A configured MCP server instance ready for connection.
*/
export function createServer(options: ServerOptions): McpServer {
const server = new McpServer({
name: "mail-smtp-mcp",
version: "0.1.0",
});
// Register list_accounts tool with input/output schemas
server.registerTool(
"smtp_list_accounts",
{
description: "List configured SMTP accounts (metadata only).",
inputSchema: listAccountsSchema,
outputSchema: listAccountsResultSchema,
},
wrapToolHandler(
"smtp_list_accounts",
(input) => handleListAccounts(input, options.env, options.logger),
options.logger,
),
);
// Register verify_account tool with input/output schemas
server.registerTool(
"smtp_verify_account",
{
description: "Verify SMTP account connectivity without sending an email.",
inputSchema: verifyAccountSchema,
outputSchema: verifyAccountResultSchema,
},
wrapToolHandler(
"smtp_verify_account",
(input) => handleVerifyAccount(input, options.env, options.logger),
options.logger,
),
);
// Register send_message tool with input/output schemas
server.registerTool(
"smtp_send_message",
{
description: "Send an outbound email via SMTP with optional attachments.",
inputSchema: sendMessageSchema,
outputSchema: sendMessageResultSchema,
},
wrapToolHandler(
"smtp_send_message",
(input) => handleSendMessage(input, options.env, options.logger),
options.logger,
),
);
return server;
}