MCP Server Resend
by Hawstein
Verified
#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
Tool,
} from "@modelcontextprotocol/sdk/types.js";
import { Resend } from "resend";
import { readFileSync, existsSync } from "fs";
// Get API key from environment variable
const RESEND_API_KEY = process.env.RESEND_API_KEY!;
if (!RESEND_API_KEY) {
console.error("Error: RESEND_API_KEY environment variable is required");
process.exit(1);
}
// Get sender email from environment variable
const SENDER_EMAIL_ADDRESS = process.env.SENDER_EMAIL_ADDRESS;
if (!SENDER_EMAIL_ADDRESS) {
console.error("Error: SENDER_EMAIL_ADDRESS environment variable is required");
process.exit(1);
}
// Get optional reply-to emails from environment variable
const REPLY_TO_EMAIL_ADDRESSES = process.env.REPLY_TO_EMAIL_ADDRESSES ?
process.env.REPLY_TO_EMAIL_ADDRESSES.split(",").map(e => e.trim()).filter(Boolean) : [];
// Initialize Resend client
const resend = new Resend(RESEND_API_KEY.trim());
// Define email sending tool
const SEND_EMAIL_TOOL: Tool = {
name: "send_email",
description:
"Sends an email using the Resend API. " +
"Supports plain text content, attachments and optional scheduling. " +
"Can specify custom sender and reply-to addresses.",
inputSchema: {
type: "object",
properties: {
to: {
type: "string",
format: "email",
description: "Recipient email address"
},
subject: {
type: "string",
description: "Email subject line"
},
content: {
type: "string",
description: "Plain text email content"
},
from: {
type: "string",
format: "email",
description: "Optional. If provided, uses this as the sender email address; otherwise uses SENDER_EMAIL_ADDRESS environment variable"
},
replyTo: {
type: "array",
items: {
type: "string",
format: "email"
},
description: "Optional. If provided, uses these as the reply-to email addresses; otherwise uses REPLY_TO_EMAIL_ADDRESSES environment variable"
},
scheduledAt: {
type: "string",
description: "Optional parameter to schedule the email. This uses natural language. Examples would be 'tomorrow at 10am' or 'in 2 hours' or 'next day at 9am PST' or 'Friday at 3pm ET'."
},
attachments: {
type: "array",
items: {
type: "object",
properties: {
filename: {
type: "string",
description: "Name of the attachment file"
},
localPath: {
type: "string",
description: "Absolute path to a local file on user's computer. Required if remoteUrl is not provided."
},
remoteUrl: {
type: "string",
description: "URL to a file on the internet. Required if localPath is not provided."
}
},
required: ["filename"],
oneOf: [
{ required: ["localPath"] },
{ required: ["remoteUrl"] }
]
},
description: "Optional. List of attachments. Each attachment must have a filename and either localPath (path to a local file) or remoteUrl (URL to a file on the internet)."
}
},
required: ["to", "subject", "content"]
}
};
// Type guard for email args
interface Attachment {
filename: string;
localPath?: string;
remoteUrl?: string;
}
function isAttachment(arg: unknown): arg is Attachment {
if (typeof arg !== "object" || arg === null) return false;
const attachment = arg as Attachment;
if (typeof attachment.filename !== "string") return false;
// Must have either localPath or remoteUrl, but not both
const hasLocalPath = "localPath" in attachment && typeof attachment.localPath === "string";
const hasRemoteUrl = "remoteUrl" in attachment && typeof attachment.remoteUrl === "string";
return hasLocalPath !== hasRemoteUrl; // XOR operation
}
function isEmailArgs(args: unknown): args is {
to: string;
subject: string;
content: string;
from?: string;
replyTo?: string[];
scheduledAt?: string;
attachments?: Attachment[];
} {
if (
typeof args !== "object" ||
args === null
) {
return false;
}
const emailArgs = args as {
to: unknown;
subject: unknown;
content: unknown;
attachments?: unknown[];
};
if (
!("to" in emailArgs) ||
typeof emailArgs.to !== "string" ||
!("subject" in emailArgs) ||
typeof emailArgs.subject !== "string" ||
!("content" in emailArgs) ||
typeof emailArgs.content !== "string"
) {
return false;
}
// Check optional attachments if present
if ("attachments" in emailArgs) {
if (!Array.isArray(emailArgs.attachments)) return false;
if (!emailArgs.attachments.every(isAttachment)) return false;
}
return true;
}
// Server implementation
const server = new Server(
{
name: "resend-mcp",
version: "0.1.0",
},
{
capabilities: {
tools: {},
},
},
);
// Tool handlers
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [SEND_EMAIL_TOOL],
}));
server.setRequestHandler(CallToolRequestSchema, async (request) => {
try {
const { name, arguments: args } = request.params;
if (!args) {
throw new Error("No arguments provided");
}
switch (name) {
case SEND_EMAIL_TOOL.name: {
if (!isEmailArgs(args)) {
throw new Error("Invalid arguments for send_email tool");
}
const fromEmail = args.from || SENDER_EMAIL_ADDRESS.trim();
if (!fromEmail) {
throw new Error("Sender email must be provided either via args or SENDER_EMAIL_ADDRESS environment variable");
}
const replyToEmails = args.replyTo || REPLY_TO_EMAIL_ADDRESSES;
// Convert attachments to Resend API format
const attachments = args.attachments?.map(attachment => {
if (attachment.localPath) {
// Check if file exists
if (!existsSync(attachment.localPath)) {
throw new Error(`Attachment file not found: ${attachment.localPath}`);
}
// Try to read the file
try {
// readFileSync can read any file format as it reads files in binary mode
const content = readFileSync(attachment.localPath).toString('base64');
return {
filename: attachment.filename,
content,
path: undefined
};
} catch (error) {
throw new Error(`Failed to read attachment file: ${attachment.localPath}. Error: ${error instanceof Error ? error.message : String(error)}`);
}
}
// If using remoteUrl
return {
filename: attachment.filename,
content: undefined,
path: attachment.remoteUrl
};
});
const response = await resend.emails.send({
to: args.to,
from: fromEmail,
subject: args.subject,
text: args.content,
replyTo: replyToEmails,
scheduledAt: args.scheduledAt,
attachments,
});
if (response.error) {
throw new Error(`Failed to send email: ${JSON.stringify(response.error)}`);
}
return {
content: [{
type: "text",
text: `Email sent successfully! ${JSON.stringify(response.data)}`
}]
};
}
default:
return {
content: [{ type: "text", text: `Unknown tool: ${name}` }],
isError: true,
};
}
} catch (error) {
return {
content: [{
type: "text",
text: `Error: ${error instanceof Error ? error.message : String(error)}`,
}],
isError: true,
};
}
});
// Start server
async function runServer() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Resend MCP Server running on stdio");
}
runServer().catch((error) => {
console.error("Fatal error running server:", error);
process.exit(1);
});