Skip to content
Back to Vibe coding

AI code editor

Updated

Add email to your Cursor app with Lumail

Cursor writes the code; Lumail sends the email from your own domain. Give the agent a prompt with the rules it tends to get wrong, keep the key in .env, and connect the Lumail MCP server so the agent can check domains and drafts without leaving the editor.

TL;DR

Paste the prompt below into Cursor's agent. It points the agent at lumail.io/integration/install, installs the lumail package, creates a server-only client that reads LUMAIL_API_KEY from .env, and sends with lumail.emails.send({ from, to, subject, html }) plus an idempotency key. Add https://lumail.io/mcp to .cursor/mcp.json for the MCP tools.

1. Prompt Cursor

Open the agent in Cursor and paste this. The last rule sends it to Lumail's machine-readable install guide, which detects your framework and wires the client the same way the guides on this site do.

Prompt for Cursor
Add transactional email to this app with Lumail (https://lumail.io). Stack: the framework already used in this repository. Detect it before writing code. Rules: - Send email only from server-side code. Never call Lumail from the browser and never expose the API key to client code. - Read the API key from the LUMAIL_API_KEY environment variable. Add it to .env (and .env.example with a placeholder) and make sure .env is git-ignored. Throw a clear error if it is missing. - In TypeScript/JavaScript use the official `lumail` npm package: `new Lumail({ apiKey })`, then `lumail.emails.send({ from, to, subject, html })`. - Elsewhere call the REST API: POST https://lumail.io/api/v2/emails with `Authorization: Bearer <LUMAIL_API_KEY>` and a JSON body. - Pass exactly one body format: `html`, `markdown` or `tiptap`. Add `text` as a plain-text fallback when sending html. - `to` is a single recipient. Loop or use `lumail.emails.batch` (max 100) for several people. - The SDK returns `{ data, error }` and never throws on HTTP errors. Check `error` and log `error.name` and `error.message`. - Pass an `idempotencyKey` (header `Idempotency-Key` over REST) built from the event that triggered the email, such as `welcome:<userId>`. - `from` must use a domain verified in my Lumail organization. Put it in a LUMAIL_FROM environment variable, for example `Acme <[email protected]>`. - Before writing code, read lumail.io/integration/install and follow it. First task: send a welcome email when a user signs up. Show me which files you changed and how to test it.

2. Put the API key in the right place

Create a token in Lumail under API tokens; it starts with lum_ and belongs to one organization. Put it in .env at the project root, check that .env is in .gitignore, and commit only a placeholder in .env.example.

Cursor indexes your project, so if you keep secrets out of the agent's context, list .env in .cursorignore as well. The code reads the variable at runtime; the agent never needs the real value.

.env
# Server-side only. Never prefix with VITE_ or NEXT_PUBLIC_. LUMAIL_API_KEY=lum_your_api_token LUMAIL_FROM="Acme <[email protected]>"

3. The code Cursor should generate

Whatever framework you use, the result should look like this: one server-only module that creates the client once, and a function that sends with an idempotency key and checks error. If the agent produced something different, the next sections tell you what to push back on.

For framework-specific code, compare against the Next.js, Node.js and TanStack Start guides.

server/send-welcome.ts
import { Lumail } from "lumail"; const apiKey = process.env.LUMAIL_API_KEY; if (!apiKey) throw new Error("LUMAIL_API_KEY is not set"); const lumail = new Lumail({ apiKey }); export async function sendWelcome(user: { id: string; email: string; name?: string }) { const { data, error } = await lumail.emails.send( { from: process.env.LUMAIL_FROM ?? "Acme <[email protected]>", to: user.email, subject: "Welcome to Acme", markdown: `Hi ${user.name ?? "there"}, thanks for signing up.`, }, { idempotencyKey: `welcome:${user.id}` }, ); if (error) { console.error("Lumail send failed", error.name, error.message); return { ok: false as const }; } return { ok: true as const, id: data.id }; }

4. Lumail MCP server and agent plugins

The Lumail MCP server lets Cursor's agent read your organization: domains and their DNS status, subscribers, campaigns and drafts. The OAuth endpoint https://lumail.io/mcp can read and write drafts but has no send or delete tools, which is the safe default inside an editor.

Add it per project in .cursor/mcp.json, approve the OAuth prompt, and keep one Lumail organization per project. Do not register the same URL twice in one scope. npx lumail setup can write this config for you.

.cursor/mcp.json
{ "mcpServers": { "lumail": { "url": "https://lumail.io/mcp" } } }

5. Verify your sending domain

Lumail only sends from a domain you have verified. Add the domain in your organization's Domains settings, then publish the SPF, DKIM and DMARC records it shows at your DNS provider. Until the domain verifies, every send fails with an error saying the domain is not authorized or verified.

Use a subdomain such as mail.yourdomain.com if your root domain already sends from another provider. Start DMARC at p=none, then tighten it once reports look clean.

Common pitfalls

  • Two configs for one server. Registering https://lumail.io/mcp in both the global and project mcp.json can connect the agent to the wrong organization. Keep one entry per scope.
  • Client-side sends. If the AI imports lumail in a React component or uses fetch to the Lumail API from the browser, the key ships to every visitor. Move it to a server route or function and rotate the token.
  • Unverified `from` domain. Sends from a domain that is not verified in the same organization are rejected with a 400 that names the domain. Verify it first, or use the exact address Lumail shows you.
  • Treating `{ error }` as an exception. The SDK never throws on HTTP errors. Code that only wraps the call in try/catch silently drops failures.
  • Duplicate emails on retry. Without an idempotency key, a retried request or double-clicked button can send twice.

Frequently asked questions

Does Cursor need the Lumail MCP server to send email from my app?

No. Your app sends email through the lumail SDK or REST API with an API token. The MCP server is for the agent itself: checking domain status, reading subscribers and preparing drafts while you build.

Can Cursor's agent send emails through MCP?

Not through the OAuth endpoint at https://lumail.io/mcp, which has no send or delete tools. The token endpoint at https://lumail.io/api/mcp/sse exposes the full tool set, so only use it when you want the agent to be able to send.

Where should the API key live in a Cursor project?

In .env, read with process.env.LUMAIL_API_KEY in server code only. Keep .env git-ignored, add it to .cursorignore if you do not want it in the agent's context, and set the same variable on your host.

What is lumail.io/integration/install?

A plain-text install guide written for coding agents. It tells the agent how to detect your framework, install the SDK, create the client and send a first email, so the prompt stays short.

How do I test without emailing real users?

Send to fixture addresses such as [email protected] or any .test domain. Lumail runs the full send path and returns an id, but never delivers the message.

Keep building

Ship email from your Cursor app today.

3,000 emails a month free. Transactional and marketing email on one verified domain, with unlimited subscribers on every plan.