Send Emails from MCP with React Email & Brevo — Guide
Enable your MCP server to render Markdown with React Email and deliver transactional emails via Brevo; includes…

⚛️ Advanced React Development Guides
Comprehensive React guides covering hooks, performance optimization, and React 19 features. Includes code examples and prompts to boost your workflow.
Related Posts:
This is Part 4 of the MCP Server Series. This guide assumes you have a working MCP server. If you're starting fresh, begin with Part 1: Build a Production MCP Server.
Moving from debugging code in your IDE to logging into an email marketing platform just to send a single, personalized follow-up feels like a massive context switch. I recently faced this when I wanted to notify specific users about fixes relevant to their previous feedback. Accessing that capability directly from my AI chat interface seemed like the perfect solution.
Here is the step-by-step process I developed to enable my MCP server to send beautifully rendered emails on command.
Prerequisites
To handle email rendering, markdown processing, and delivery, we need to install a few essential dependencies.
npm install @react-email/components @react-email/render @getbrevo/brevo marked zod mcp-handler
# or
pnpm add @react-email/components @react-email/render @getbrevo/brevo marked zod mcp-handler
You will also need a Brevo API Key and a verified sender email address from your Brevo account. Add these to your environment variables:
BREVO_API_KEY=xkeysib-... BREVO_SENDER_EMAIL=matija@buildwithmatija.com
1. The Email Infrastructure
To send professional emails, we need a solid foundation. I chose React Email for templating because it allows us to build layouts using React components while handling the complex job of inlining CSS for email clients.
First, create a reusable email component with rich styling for content generated via markdown.
// File: src/lib/emails/NewsletterEmail.tsx
import { Body, Button, Container, Head, Heading, Html, Preview, Section, Tailwind, Text } from '@react-email/components';
import * as React from 'react';
interface NewsletterEmailProps {
previewText: string;
title: string;
content: string; // HTML content from processed Markdown
ctaText?: string;
ctaUrl?: string;
}
export const NewsletterEmail = ({ previewText, title, content, ctaText, ctaUrl }: NewsletterEmailProps) => {
return (
<Html>
<Head>
<style>{`
.content-area p { margin: 16px 0; line-height: 1.6; }
.content-area a { color: #F97316; text-decoration: underline; }
.content-area blockquote {
border-left: 4px solid #F97316;
padding-left: 16px;
margin: 16px 0;
color: #4B5563;
font-style: italic;
}
`}</style>
</Head>
<Preview>{previewText}</Preview>
<Tailwind>
<Body className="bg-[#F9FAFB] m-auto font-sans text-[#111827]">
<Container className="mb-10 mx-auto p-0 max-w-[600px] bg-white rounded-xl my-[40px] overflow-hidden">
<Section style={{ backgroundColor: '#D4866D', padding: '40px 24px' }}>
<Heading className="text-2xl font-bold py-0 px-0 inline-block m-0" style={{ color: 'white' }}>
Build with Matija
</Heading>
</Section>
<Heading className="text-3xl font-bold text-start p-8 m-0 leading-tight">
{title}
</Heading>
<Section className="px-8 pb-8">
<div className="content-area" dangerouslySetInnerHTML={{ __html: content }} />
</Section>
{ctaText && ctaUrl && (
<Section className="text-center mt-8 mb-8 px-8">
<Button
className="py-3.5 px-7 rounded-full text-base font-bold"
href={ctaUrl}
style={{ backgroundColor: '#F97316', color: 'white' }}
>
{ctaText}
</Button>
</Section>
)}
</Container>
</Body>
</Tailwind>
</Html>
);
};
This component uses a custom <style> block in the <Head> to ensure that elements inside our .content-area (like paragraphs and links) look great when rendered from Markdown.
2. The Delivery Backend
Next, we need a helper function to handle the actual transmission via the Brevo SDK. I separated this logic to keep the MCP tool handler focused on execution logic.
// File: src/lib/brevo/brevo.ts
import { TransactionalEmailsApi, SendSmtpEmail, TransactionalEmailsApiApiKeys } from '@getbrevo/brevo'
const brevoApiKey = process.env.BREVO_API_KEY
const transactionalEmailsApi = brevoApiKey ? new TransactionalEmailsApi() : null
if (brevoApiKey && transactionalEmailsApi) {
transactionalEmailsApi.setApiKey(TransactionalEmailsApiApiKeys.apiKey, brevoApiKey)
}
export async function sendTransactionalEmail(params: {
to: { email: string; name?: string }[]
subject: string
htmlContent: string
}) {
if (!transactionalEmailsApi) {
return { success: false, error: 'Brevo integration disabled' }
}
const senderEmail = process.env.BREVO_SENDER_EMAIL
if (!senderEmail) {
return { success: false, error: 'BREVO_SENDER_EMAIL not configured' }
}
try {
const sendSmtpEmail = new SendSmtpEmail()
sendSmtpEmail.subject = params.subject
sendSmtpEmail.htmlContent = params.htmlContent
sendSmtpEmail.sender = { email: senderEmail, name: 'Matija from BuildwithMatija' }
sendSmtpEmail.to = params.to
const result = await transactionalEmailsApi.sendTransacEmail(sendSmtpEmail)
return { success: true, data: result.body }
} catch (error: any) {
return { success: false, error: error.message }
}
}
By using process.env.BREVO_SENDER_EMAIL, we ensure that the code only attempts to send if we have a verified identity ready.
3. The MCP Tool Definition
The final piece is connecting this capability to the MCP server. We define a tool called send_newsletter that allows the AI to send messages using Markdown, which we then convert to HTML.
// File: src/app/api/mcp/[transport]/route.ts
import * as React from 'react'
import { z } from 'zod'
import { render } from '@react-email/render'
import { marked } from 'marked'
import { createMcpHandler } from 'mcp-handler'
import { NewsletterEmail } from '@/lib/emails/NewsletterEmail'
import { sendTransactionalEmail } from '@/lib/brevo/brevo'
const handler = createMcpHandler((server) => {
server.tool(
'send_newsletter',
'Send a personalized email or newsletter via MCP.',
{
subject: z.string().describe('Email subject line'),
title: z.string().describe('Title shown in the email header'),
content: z.string().describe('Message content (Markdown supported)'),
to: z.array(z.string().email()).optional().describe('Direct list of recipients'),
categoryInterest: z.string().optional().describe('Target category slug'),
ctaText: z.string().optional(),
ctaUrl: z.string().url().optional()
},
async ({ subject, title, content, to, categoryInterest, ctaText, ctaUrl }) => {
let recipients = []
if (to && to.length > 0) {
recipients = to.map(email => ({ email }))
} else if (categoryInterest) {
// Fetch from your database (e.g. Sanity)
// recipients = await fetchSubscribers(categoryInterest)
}
if (recipients.length === 0) {
return { content: [{ type: 'text', text: 'Error: No recipients found.' }], isError: true }
}
// Convert Markdown content provided by AI into HTML
const processedContent = await marked.parse(content)
// Render the React Email template to an HTML string
const htmlContent = await render(
React.createElement(NewsletterEmail, {
previewText: subject,
title,
content: processedContent,
ctaText,
ctaUrl
})
)
// Deliver via Brevo
const result = await sendTransactionalEmail({ to: recipients, subject, htmlContent })
return {
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }]
}
}
)
})
export { handler as GET, handler as POST }
Using marked.parse(content) allows the AI to use its natural storytelling abilities—using bold text, lists, and links—while ensuring the recipient sees a perfectly formatted email.
The Complete MCP Server Series
This guide completes the 4-part series on building production-ready MCP servers:
- Why Your Business Needs an MCP Server — The business case and ROI
- Part 1: Build a Production MCP Server — Foundation with Redis-backed SSE
- Part 2: Write Operations — Content editing and cache revalidation
- Part 3: OAuth Security — Protect your endpoints
By enabling the AI to act on its own suggestions, we move from passive assistance to active automation.
Thanks, Matija
Frequently Asked Questions
Comments
You might be interested in

30th November 2025

30th November 2025

6th December 2025

10th December 2025

7th December 2025