#!/usr/bin/env node
/**
* Unosend MCP Server
*
* Allows LLMs to interact with Unosend API.
* Works with Claude Desktop, Cursor, and other MCP clients.
*/
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from '@modelcontextprotocol/sdk/types.js'
// Parse command line arguments
function parseArgs(): { apiKey: string; sender?: string; replyTo?: string } {
const args = process.argv.slice(2)
let apiKey = process.env.UNOSEND_API_KEY || ''
let sender = process.env.SENDER_EMAIL_ADDRESS
let replyTo = process.env.REPLY_TO_EMAIL_ADDRESS
for (const arg of args) {
if (arg.startsWith('--key=')) {
apiKey = arg.slice(6)
} else if (arg.startsWith('--sender=')) {
sender = arg.slice(9)
} else if (arg.startsWith('--reply-to=')) {
replyTo = arg.slice(11)
}
}
if (!apiKey) {
console.error('Error: API key is required.')
console.error('Usage: node index.js --key=YOUR_API_KEY [--sender=email] [--reply-to=email]')
console.error('Or set UNOSEND_API_KEY environment variable.')
process.exit(1)
}
return { apiKey, sender, replyTo }
}
const config = parseArgs()
const API_BASE_URL = 'https://www.unosend.co/api/v1'
// Generic API request helper
async function apiRequest(
endpoint: string,
method: string = 'GET',
body?: Record<string, unknown>
): Promise<{ success: boolean; data?: unknown; error?: string }> {
try {
const options: RequestInit = {
method,
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${config.apiKey}`,
},
}
if (body) options.body = JSON.stringify(body)
const response = await fetch(`${API_BASE_URL}${endpoint}`, options)
const data = await response.json()
if (!response.ok) {
return {
success: false,
error: (data as { error?: string; message?: string }).error ||
(data as { error?: string; message?: string }).message ||
`HTTP ${response.status}`
}
}
return { success: true, data }
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
}
}
}
// Create MCP server
const server = new Server(
{
name: 'unosend',
version: '1.0.0',
},
{
capabilities: {
tools: {},
},
}
)
// Define all tools
const tools = [
// ============= EMAIL TOOLS =============
{
name: 'send_email',
description: 'Send an email using Unosend API. Supports HTML/text content, CC/BCC, reply-to, and scheduling.',
inputSchema: {
type: 'object',
properties: {
to: { type: 'string', description: 'Recipient email address (or comma-separated list)' },
subject: { type: 'string', description: 'Email subject line' },
html: { type: 'string', description: 'HTML content of the email' },
text: { type: 'string', description: 'Plain text content (used if html not provided)' },
from: { type: 'string', description: 'Sender email (must be from verified domain)' },
cc: { type: 'string', description: 'CC recipient(s), comma-separated' },
bcc: { type: 'string', description: 'BCC recipient(s), comma-separated' },
reply_to: { type: 'string', description: 'Reply-to email address' },
scheduled_at: { type: 'string', description: 'ISO 8601 datetime to schedule (e.g., "2026-01-28T10:00:00Z")' },
},
required: ['to', 'subject'],
},
},
{
name: 'get_email',
description: 'Get details and status of a sent email by its ID.',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string', description: 'The email ID (returned from send_email)' },
},
required: ['id'],
},
},
{
name: 'list_emails',
description: 'List recent emails sent from your account.',
inputSchema: {
type: 'object',
properties: {
limit: { type: 'number', description: 'Number of emails to return (default: 10, max: 100)' },
},
required: [],
},
},
{
name: 'cancel_email',
description: 'Cancel a scheduled email before it is sent.',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string', description: 'The scheduled email ID to cancel' },
},
required: ['id'],
},
},
// ============= SMS TOOLS =============
{
name: 'send_sms',
description: 'Send an SMS message. Charges from wallet at $0.0075 per segment.',
inputSchema: {
type: 'object',
properties: {
to: { type: 'string', description: 'Recipient phone number in E.164 format (e.g., +1234567890)' },
body: { type: 'string', description: 'SMS message content (160 chars = 1 segment)' },
from: { type: 'string', description: 'Sender phone number (if you have one)' },
},
required: ['to', 'body'],
},
},
{
name: 'get_sms',
description: 'Get details of an SMS message by its ID.',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string', description: 'The SMS message ID' },
},
required: ['id'],
},
},
// ============= VALIDATION TOOLS =============
{
name: 'validate_email',
description: 'Validate an email address to check if it exists and is deliverable. Costs $0.01 per validation.',
inputSchema: {
type: 'object',
properties: {
email: { type: 'string', description: 'Email address to validate' },
},
required: ['email'],
},
},
// ============= DOMAIN TOOLS =============
{
name: 'list_domains',
description: 'List all verified domains in your account.',
inputSchema: {
type: 'object',
properties: {},
required: [],
},
},
{
name: 'get_domain',
description: 'Get details and DNS records for a specific domain.',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string', description: 'The domain ID' },
},
required: ['id'],
},
},
// ============= AUDIENCE/CONTACT TOOLS =============
{
name: 'list_audiences',
description: 'List all audiences (contact lists) in your account.',
inputSchema: {
type: 'object',
properties: {},
required: [],
},
},
{
name: 'create_contact',
description: 'Add a contact to an audience.',
inputSchema: {
type: 'object',
properties: {
audience_id: { type: 'string', description: 'The audience ID to add the contact to' },
email: { type: 'string', description: 'Contact email address' },
first_name: { type: 'string', description: 'Contact first name' },
last_name: { type: 'string', description: 'Contact last name' },
unsubscribed: { type: 'boolean', description: 'Whether the contact is unsubscribed' },
},
required: ['audience_id', 'email'],
},
},
{
name: 'list_contacts',
description: 'List contacts in an audience.',
inputSchema: {
type: 'object',
properties: {
audience_id: { type: 'string', description: 'The audience ID' },
},
required: ['audience_id'],
},
},
// ============= UTILITY TOOLS =============
{
name: 'check_api_status',
description: 'Check if the Unosend API is accessible and the API key is valid.',
inputSchema: {
type: 'object',
properties: {},
required: [],
},
},
]
// List available tools
server.setRequestHandler(ListToolsRequestSchema, async () => {
return { tools }
})
// Handle tool calls
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params
const params = args as Record<string, unknown>
// ============= EMAIL HANDLERS =============
if (name === 'send_email') {
const from = (params.from as string) || config.sender
if (!from) {
return {
content: [{ type: 'text', text: '❌ Sender email required. Provide --sender argument or specify "from" parameter.' }],
isError: true,
}
}
const toList = (params.to as string).split(',').map(e => e.trim())
const body: Record<string, unknown> = {
from,
to: toList,
subject: params.subject,
}
if (params.html) body.html = params.html
if (params.text) body.text = params.text
if (params.cc) body.cc = (params.cc as string).split(',').map(e => e.trim())
if (params.bcc) body.bcc = (params.bcc as string).split(',').map(e => e.trim())
if (params.reply_to || config.replyTo) body.reply_to = params.reply_to || config.replyTo
if (params.scheduled_at) body.scheduled_at = params.scheduled_at
const result = await apiRequest('/emails', 'POST', body)
if (result.success) {
const data = result.data as { id: string }
return {
content: [{
type: 'text',
text: `✅ Email sent!\n\nID: ${data.id}\nTo: ${params.to}\nSubject: ${params.subject}${params.scheduled_at ? `\nScheduled: ${params.scheduled_at}` : ''}`,
}],
}
}
return { content: [{ type: 'text', text: `❌ Failed: ${result.error}` }], isError: true }
}
if (name === 'get_email') {
const result = await apiRequest(`/emails/${params.id}`)
if (result.success) {
const data = result.data as { id: string; status: string; to: string[]; subject: string; created_at: string }
return {
content: [{
type: 'text',
text: `📧 Email Details\n\nID: ${data.id}\nStatus: ${data.status}\nTo: ${data.to?.join(', ')}\nSubject: ${data.subject}\nCreated: ${data.created_at}`,
}],
}
}
return { content: [{ type: 'text', text: `❌ Failed: ${result.error}` }], isError: true }
}
if (name === 'list_emails') {
const limit = params.limit || 10
const result = await apiRequest(`/emails?limit=${limit}`)
if (result.success) {
const data = result.data as { data: Array<{ id: string; to: string[]; subject: string; status: string; created_at: string }> }
const emails = data.data || []
if (emails.length === 0) {
return { content: [{ type: 'text', text: '📭 No emails found.' }] }
}
const list = emails.map((e, i) => `${i + 1}. [${e.status}] ${e.subject} → ${e.to?.join(', ')}`).join('\n')
return { content: [{ type: 'text', text: `📧 Recent Emails (${emails.length})\n\n${list}` }] }
}
return { content: [{ type: 'text', text: `❌ Failed: ${result.error}` }], isError: true }
}
if (name === 'cancel_email') {
const result = await apiRequest(`/emails/${params.id}/cancel`, 'POST')
if (result.success) {
return { content: [{ type: 'text', text: `✅ Email ${params.id} cancelled.` }] }
}
return { content: [{ type: 'text', text: `❌ Failed: ${result.error}` }], isError: true }
}
// ============= SMS HANDLERS =============
if (name === 'send_sms') {
const body: Record<string, unknown> = {
to: params.to,
body: params.body,
}
if (params.from) body.from = params.from
const result = await apiRequest('/sms', 'POST', body)
if (result.success) {
const data = result.data as { id: string; segments: number; cost: number }
return {
content: [{
type: 'text',
text: `✅ SMS sent!\n\nID: ${data.id}\nTo: ${params.to}\nSegments: ${data.segments}\nCost: $${data.cost?.toFixed(4) || '0.0075'}`,
}],
}
}
return { content: [{ type: 'text', text: `❌ Failed: ${result.error}` }], isError: true }
}
if (name === 'get_sms') {
const result = await apiRequest(`/sms/${params.id}`)
if (result.success) {
const data = result.data as { id: string; status: string; to: string; body: string; segments: number }
return {
content: [{
type: 'text',
text: `📱 SMS Details\n\nID: ${data.id}\nStatus: ${data.status}\nTo: ${data.to}\nMessage: ${data.body}\nSegments: ${data.segments}`,
}],
}
}
return { content: [{ type: 'text', text: `❌ Failed: ${result.error}` }], isError: true }
}
// ============= VALIDATION HANDLER =============
if (name === 'validate_email') {
const result = await apiRequest('/validate/email', 'POST', { email: params.email })
if (result.success) {
const data = result.data as { email: string; valid: boolean; reason?: string; risk?: string }
const status = data.valid ? '✅ Valid' : '❌ Invalid'
return {
content: [{
type: 'text',
text: `${status}\n\nEmail: ${data.email}${data.reason ? `\nReason: ${data.reason}` : ''}${data.risk ? `\nRisk: ${data.risk}` : ''}`,
}],
}
}
return { content: [{ type: 'text', text: `❌ Failed: ${result.error}` }], isError: true }
}
// ============= DOMAIN HANDLERS =============
if (name === 'list_domains') {
const result = await apiRequest('/domains')
if (result.success) {
const data = result.data as { data: Array<{ id: string; name: string; status: string }> }
const domains = data.data || []
if (domains.length === 0) {
return { content: [{ type: 'text', text: '🌐 No domains found.' }] }
}
const list = domains.map((d, i) => `${i + 1}. ${d.name} [${d.status}]`).join('\n')
return { content: [{ type: 'text', text: `🌐 Domains (${domains.length})\n\n${list}` }] }
}
return { content: [{ type: 'text', text: `❌ Failed: ${result.error}` }], isError: true }
}
if (name === 'get_domain') {
const result = await apiRequest(`/domains/${params.id}`)
if (result.success) {
const data = result.data as { id: string; name: string; status: string; dns_records: unknown[] }
return {
content: [{
type: 'text',
text: `🌐 Domain: ${data.name}\n\nStatus: ${data.status}\nDNS Records: ${data.dns_records?.length || 0} records`,
}],
}
}
return { content: [{ type: 'text', text: `❌ Failed: ${result.error}` }], isError: true }
}
// ============= AUDIENCE/CONTACT HANDLERS =============
if (name === 'list_audiences') {
const result = await apiRequest('/audiences')
if (result.success) {
const data = result.data as { data: Array<{ id: string; name: string }> }
const audiences = data.data || []
if (audiences.length === 0) {
return { content: [{ type: 'text', text: '👥 No audiences found.' }] }
}
const list = audiences.map((a, i) => `${i + 1}. ${a.name} (ID: ${a.id})`).join('\n')
return { content: [{ type: 'text', text: `👥 Audiences (${audiences.length})\n\n${list}` }] }
}
return { content: [{ type: 'text', text: `❌ Failed: ${result.error}` }], isError: true }
}
if (name === 'create_contact') {
const body: Record<string, unknown> = { email: params.email }
if (params.first_name) body.first_name = params.first_name
if (params.last_name) body.last_name = params.last_name
if (params.unsubscribed !== undefined) body.unsubscribed = params.unsubscribed
const result = await apiRequest(`/audiences/${params.audience_id}/contacts`, 'POST', body)
if (result.success) {
const data = result.data as { id: string }
return { content: [{ type: 'text', text: `✅ Contact added!\n\nID: ${data.id}\nEmail: ${params.email}` }] }
}
return { content: [{ type: 'text', text: `❌ Failed: ${result.error}` }], isError: true }
}
if (name === 'list_contacts') {
const result = await apiRequest(`/audiences/${params.audience_id}/contacts`)
if (result.success) {
const data = result.data as { data: Array<{ id: string; email: string; first_name?: string; last_name?: string }> }
const contacts = data.data || []
if (contacts.length === 0) {
return { content: [{ type: 'text', text: '👤 No contacts in this audience.' }] }
}
const list = contacts.map((c, i) => {
const name = [c.first_name, c.last_name].filter(Boolean).join(' ')
return `${i + 1}. ${c.email}${name ? ` (${name})` : ''}`
}).join('\n')
return { content: [{ type: 'text', text: `👤 Contacts (${contacts.length})\n\n${list}` }] }
}
return { content: [{ type: 'text', text: `❌ Failed: ${result.error}` }], isError: true }
}
// ============= UTILITY HANDLERS =============
if (name === 'check_api_status') {
const result = await apiRequest('/domains')
if (result.success) {
return { content: [{ type: 'text', text: '✅ Unosend API is accessible and API key is valid.' }] }
}
if (result.error?.includes('401') || result.error?.includes('Unauthorized')) {
return { content: [{ type: 'text', text: '❌ Invalid API key.' }], isError: true }
}
return { content: [{ type: 'text', text: `⚠️ API issue: ${result.error}` }], isError: true }
}
return { content: [{ type: 'text', text: `Unknown tool: ${name}` }], isError: true }
})
// Start server
async function main() {
const transport = new StdioServerTransport()
await server.connect(transport)
console.error('Unosend MCP Server running...')
}
main().catch(console.error)