import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import minimist from 'minimist';
import { z } from 'zod';
import type { EmailOptions, GetEventsOptions, GetEventsResponse } from 'sendlayer';
import { SendLayer as SendLayerClient } from 'sendlayer';
// Helper function to handle SendLayer errors and provide user-friendly messages
function handleSendLayerError(error: any): never {
// Check error name or message to identify error type
const errorName = error?.name || '';
const errorMessage = error?.message || 'An unknown error occurred';
if (errorName === 'SendLayerAuthenticationError' ||
errorMessage.includes('Invalid API key') ||
errorMessage.includes('401') ||
error?.response?.status === 401) {
throw new Error(
'Invalid SendLayer API key. Please check your API key and ensure it is correct. ' +
'You can get your API key from https://app.sendlayer.com'
);
}
if (errorName === 'SendLayerValidationError' || errorMessage.includes('Validation')) {
throw new Error(`Validation error: ${errorMessage}`);
}
if (errorName === 'SendLayerAPIError' || error?.response?.status) {
const status = error?.response?.status || error?.statusCode || 'unknown';
throw new Error(`SendLayer API error (${status}): ${errorMessage}`);
}
// Generic error fallback
throw new Error(`SendLayer error: ${errorMessage}`);
}
// Parse CLI args
const argv = minimist(process.argv.slice(2));
// API key: required
const apiKey = (argv.key as string) || process.env.SENDLAYER_API_KEY;
if (!apiKey) {
console.error('No API key provided. Set SENDLAYER_API_KEY or pass --key');
process.exit(1);
}
// Get sender email address from command line argument or fall back to environment variable
// Optional.
const senderEmailAddress: string = argv.sender || process.env.SENDER_EMAIL_ADDRESS;
// Optional attachment URL timeout in ms
const attachmentURLTimeout = argv.attachmentTimeout
? Number(argv.attachmentTimeout)
: process.env.ATTACHMENT_URL_TIMEOUT
? Number(process.env.ATTACHMENT_URL_TIMEOUT)
: undefined;
// Init SDK
const sendlayer = new SendLayerClient(apiKey);
if (attachmentURLTimeout && (sendlayer as any).client) {
(sendlayer as any).client.attachmentURLTimeout = attachmentURLTimeout;
}
// Create MCP server
const server = new McpServer({ name: 'sendlayer-mcp', version: '1.0.0' });
// send-email tool
server.tool(
'send-email',
'Send an email using SendLayer',
{
from: z.union([
z.string().email(),
z.object({ email: z.string().email(), name: z.string().optional() })
]).describe('Sender email or { email, name }. If omitted, uses configured default sender if available.'),
to: z.union([
z.string().email(),
z.object({ email: z.string().email(), name: z.string().optional() }),
z.array(z.union([
z.string().email(),
z.object({ email: z.string().email(), name: z.string().optional() })
]))
]).describe('Recipient(s) email or objects'),
subject: z.string(),
text: z.string().optional(),
html: z.string().optional(),
cc: z.array(z.union([
z.string().email(),
z.object({ email: z.string().email(), name: z.string().optional() })
])).optional(),
bcc: z.array(z.union([
z.string().email(),
z.object({ email: z.string().email(), name: z.string().optional() })
])).optional(),
replyTo: z.array(z.union([
z.string().email(),
z.object({ email: z.string().email(), name: z.string().optional() })
])).optional(),
tags: z.array(z.string()).optional(),
headers: z.record(z.string(), z.string()).optional(),
attachments: z.array(z.object({
path: z.string().describe('Local file path or https:// URL'),
type: z.string().describe('MIME type, e.g., application/pdf'),
filename: z.string().optional(),
disposition: z.string().optional(),
contentId: z.number().optional()
})).optional()
},
async (args) => {
try {
if (!args.text && !args.html) {
throw new Error('Either text or html must be provided');
}
// Type check on from, since "from" is optionally included in the arguments schema
const fromEmailAddress = args.from ?? senderEmailAddress;
if (typeof fromEmailAddress !== 'string') {
throw new Error('from argument must be provided.');
}
args.from = fromEmailAddress;
const response = await sendlayer.Emails.send(args as unknown as EmailOptions);
return {
content: [{ type: 'text', text: JSON.stringify(response) }]
};
} catch (error: any) {
handleSendLayerError(error);
}
}
);
// get-events tool
server.tool(
'get-events',
'Retrieve events with optional filters',
{
startDate: z.string().datetime().optional().describe('ISO date, inclusive'),
endDate: z.string().datetime().optional().describe('ISO date, inclusive'),
event: z.string().optional().describe('Event type filter'),
messageId: z.string().optional(),
retrieveCount: z.number().int().min(1).max(100).optional()
},
async ({ startDate, endDate, event, messageId, retrieveCount }) => {
try {
const options: GetEventsOptions = {} as any;
if (startDate) options.startDate = new Date(startDate);
if (endDate) options.endDate = new Date(endDate);
if (event) (options as any).event = event as any;
if (messageId) options.messageId = messageId;
if (retrieveCount) options.retrieveCount = retrieveCount;
const res: GetEventsResponse = await sendlayer.Events.get(options as any);
return { content: [{ type: 'text', text: JSON.stringify(res) }] };
} catch (error: any) {
handleSendLayerError(error);
}
}
);
// list-webhooks tool
server.tool(
'list-webhooks',
'List registered webhooks',
{},
async () => {
try {
const res = await sendlayer.Webhooks.get();
return { content: [{ type: 'text', text: JSON.stringify(res) }] };
} catch (error: any) {
handleSendLayerError(error);
}
}
);
// create-webhook tool
server.tool(
'create-webhook',
'Create a webhook',
{
url: z.string().url(),
event: z.string().describe('bounce|click|open|unsubscribe|complaint|delivery')
},
async ({ url, event }) => {
try {
const res = await sendlayer.Webhooks.create({ url, event });
return { content: [{ type: 'text', text: JSON.stringify(res) }] };
} catch (error: any) {
handleSendLayerError(error);
}
}
);
// delete-webhook tool
server.tool(
'delete-webhook',
'Delete a webhook by ID',
{ webhookId: z.number().int().positive() },
async ({ webhookId }) => {
try {
const res = await sendlayer.Webhooks.delete(webhookId);
return { content: [{ type: 'text', text: JSON.stringify(res) }] };
} catch (error: any) {
handleSendLayerError(error);
}
}
);
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error('SendLayer MCP Server running on stdio');
}
main().catch((err) => {
console.error('Fatal error in main():', err);
process.exit(1);
});