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…

·Matija Žiberna·
Send Emails from MCP with React Email & Brevo — Guide

⚛️ Advanced React Development Guides

Comprehensive React guides covering hooks, performance optimization, and React 19 features. Includes code examples and prompts to boost your workflow.

No spam. Unsubscribe anytime.

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:

  1. Why Your Business Needs an MCP Server — The business case and ROI
  2. Part 1: Build a Production MCP Server — Foundation with Redis-backed SSE
  3. Part 2: Write Operations — Content editing and cache revalidation
  4. 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

5

Frequently Asked Questions

Comments

Leave a Comment

Your email will not be published

10-2000 characters

• Comments are automatically approved and will appear immediately

• Your name and email will be saved for future comments

• Be respectful and constructive in your feedback

• No spam, self-promotion, or off-topic content

Matija Žiberna
Matija Žiberna
Full-stack developer, co-founder

I'm Matija Žiberna, a self-taught full-stack developer and co-founder passionate about building products, writing clean code, and figuring out how to turn ideas into businesses. I write about web development with Next.js, lessons from entrepreneurship, and the journey of learning by doing. My goal is to provide value through code—whether it's through tools, content, or real-world software.

You might be interested in