From 11d216b71382e4c99d742b5ab90a7987a7721d80 Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Wed, 7 Jan 2026 12:00:28 -0500 Subject: [PATCH 1/8] feat: add send_email MCP tool MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add RECOUP_FROM_EMAIL constant to lib/const.ts - Create sendEmailSchema for input validation - Implement registerSendEmailTool for MCP server - Add unit tests for the new tool Migrates sendEmailTool from Recoup-Chat to API MCP server 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- lib/const.ts | 3 + lib/emails/sendEmailSchema.ts | 29 +++++ .../__tests__/registerSendEmailTool.test.ts | 101 ++++++++++++++++++ lib/mcp/tools/index.ts | 2 + lib/mcp/tools/registerSendEmailTool.ts | 49 +++++++++ 5 files changed, 184 insertions(+) create mode 100644 lib/emails/sendEmailSchema.ts create mode 100644 lib/mcp/tools/__tests__/registerSendEmailTool.test.ts create mode 100644 lib/mcp/tools/registerSendEmailTool.ts diff --git a/lib/const.ts b/lib/const.ts index 271afcf3..f7b70446 100644 --- a/lib/const.ts +++ b/lib/const.ts @@ -19,3 +19,6 @@ export const INBOUND_EMAIL_DOMAIN = "@mail.recoupable.com"; export const OUTBOUND_EMAIL_DOMAIN = "@recoupable.com"; export const SUPABASE_STORAGE_BUCKET = "user-files"; + +/** Default from address for outbound emails sent by the agent */ +export const RECOUP_FROM_EMAIL = "Agent by Recoup "; diff --git a/lib/emails/sendEmailSchema.ts b/lib/emails/sendEmailSchema.ts new file mode 100644 index 00000000..97198294 --- /dev/null +++ b/lib/emails/sendEmailSchema.ts @@ -0,0 +1,29 @@ +import { z } from "zod"; + +export const sendEmailSchema = z.object({ + to: z.array(z.string()).describe("Recipient email address or array of addresses"), + cc: z + .array(z.string()) + .describe( + "Optional array of CC email addresses. active_account_email should always be included unless already in 'to'.", + ) + .default([]) + .optional(), + subject: z.string().describe("Email subject line"), + text: z + .string() + .describe("Plain text body of the email. Use context to make this creative and engaging.") + .optional(), + html: z + .string() + .describe("HTML body of the email. Use context to make this creative and engaging.") + .default("") + .optional(), + headers: z + .record(z.string(), z.string()) + .describe("Optional custom headers for the email") + .default({}) + .optional(), +}); + +export type SendEmailInput = z.infer; diff --git a/lib/mcp/tools/__tests__/registerSendEmailTool.test.ts b/lib/mcp/tools/__tests__/registerSendEmailTool.test.ts new file mode 100644 index 00000000..848c7406 --- /dev/null +++ b/lib/mcp/tools/__tests__/registerSendEmailTool.test.ts @@ -0,0 +1,101 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { registerSendEmailTool } from "../registerSendEmailTool"; +import { NextResponse } from "next/server"; + +const mockSendEmailWithResend = vi.fn(); + +vi.mock("@/lib/emails/sendEmail", () => ({ + sendEmailWithResend: (...args: unknown[]) => mockSendEmailWithResend(...args), +})); + +describe("registerSendEmailTool", () => { + let mockServer: McpServer; + let registeredHandler: (args: unknown) => Promise; + + beforeEach(() => { + vi.clearAllMocks(); + + mockServer = { + registerTool: vi.fn((name, config, handler) => { + registeredHandler = handler; + }), + } as unknown as McpServer; + + registerSendEmailTool(mockServer); + }); + + it("registers the send_email tool", () => { + expect(mockServer.registerTool).toHaveBeenCalledWith( + "send_email", + expect.objectContaining({ + description: expect.stringContaining("Send an email using the Resend API"), + }), + expect.any(Function), + ); + }); + + it("returns success when email is sent successfully", async () => { + mockSendEmailWithResend.mockResolvedValue({ id: "email-123" }); + + const result = await registeredHandler({ + to: ["test@example.com"], + subject: "Test Subject", + text: "Test body", + }); + + expect(mockSendEmailWithResend).toHaveBeenCalledWith({ + from: "Agent by Recoup ", + to: ["test@example.com"], + cc: undefined, + subject: "Test Subject", + text: "Test body", + html: undefined, + headers: {}, + }); + + expect(result).toEqual({ + content: [ + { + type: "text", + text: expect.stringContaining("Email sent successfully"), + }, + ], + }); + }); + + it("includes CC addresses when provided", async () => { + mockSendEmailWithResend.mockResolvedValue({ id: "email-123" }); + + await registeredHandler({ + to: ["test@example.com"], + cc: ["cc@example.com"], + subject: "Test Subject", + }); + + expect(mockSendEmailWithResend).toHaveBeenCalledWith( + expect.objectContaining({ + cc: ["cc@example.com"], + }), + ); + }); + + it("returns error when sendEmailWithResend returns NextResponse", async () => { + const errorResponse = NextResponse.json({ error: { message: "Rate limited" } }, { status: 429 }); + mockSendEmailWithResend.mockResolvedValue(errorResponse); + + const result = await registeredHandler({ + to: ["test@example.com"], + subject: "Test Subject", + }); + + expect(result).toEqual({ + content: [ + { + type: "text", + text: expect.stringContaining("Rate limited"), + }, + ], + }); + }); +}); diff --git a/lib/mcp/tools/index.ts b/lib/mcp/tools/index.ts index 033b80a6..69199f36 100644 --- a/lib/mcp/tools/index.ts +++ b/lib/mcp/tools/index.ts @@ -13,6 +13,7 @@ import { registerAllFileTools } from "./files"; import { registerCreateSegmentsTool } from "./registerCreateSegmentsTool"; import { registerAllYouTubeTools } from "./youtube"; import { registerTranscribeTools } from "./transcribe"; +import { registerSendEmailTool } from "./registerSendEmailTool"; /** * Registers all MCP tools on the server. @@ -35,4 +36,5 @@ export const registerAllTools = (server: McpServer): void => { registerUpdateAccountInfoTool(server); registerCreateSegmentsTool(server); registerAllYouTubeTools(server); + registerSendEmailTool(server); }; diff --git a/lib/mcp/tools/registerSendEmailTool.ts b/lib/mcp/tools/registerSendEmailTool.ts new file mode 100644 index 00000000..8f0c02ef --- /dev/null +++ b/lib/mcp/tools/registerSendEmailTool.ts @@ -0,0 +1,49 @@ +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { sendEmailSchema, type SendEmailInput } from "@/lib/emails/sendEmailSchema"; +import { sendEmailWithResend } from "@/lib/emails/sendEmail"; +import { getToolResultSuccess } from "@/lib/mcp/getToolResultSuccess"; +import { getToolResultError } from "@/lib/mcp/getToolResultError"; +import { RECOUP_FROM_EMAIL } from "@/lib/const"; +import { NextResponse } from "next/server"; + +/** + * Registers the "send_email" tool on the MCP server. + * Send an email using the Resend API. + * + * @param server - The MCP server instance to register the tool on. + */ +export function registerSendEmailTool(server: McpServer): void { + server.registerTool( + "send_email", + { + description: `Send an email using the Resend API. Requires 'to' and 'subject'. Optionally include 'text', 'html', and custom headers.\n\nNotes:\n- Emails are sent from ${RECOUP_FROM_EMAIL}.\n- Use context to make the email creative and engaging.\n- Use this tool to send transactional or notification emails to users or admins.`, + inputSchema: sendEmailSchema, + }, + async (args: SendEmailInput) => { + const { to, cc = [], subject, text, html = "", headers = {} } = args; + + const result = await sendEmailWithResend({ + from: RECOUP_FROM_EMAIL, + to, + cc: cc.length > 0 ? cc : undefined, + subject, + text, + html: html || undefined, + headers, + }); + + if (result instanceof NextResponse) { + const data = await result.json(); + return getToolResultError( + data?.error?.message || `Failed to send email from ${RECOUP_FROM_EMAIL} to ${to}.`, + ); + } + + return getToolResultSuccess({ + success: true, + message: `Email sent successfully from ${RECOUP_FROM_EMAIL} to ${to}. CC: ${cc.length > 0 ? JSON.stringify(cc) : "none"}.`, + data: result, + }); + }, + ); +} From 77d163f9e8d9f59b499e7c3d735e086731c6d1f2 Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Wed, 7 Jan 2026 12:10:07 -0500 Subject: [PATCH 2/8] refactor: use OUTBOUND_EMAIL_DOMAIN in RECOUP_FROM_EMAIL constant MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DRY principle - reference existing domain constant 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- lib/const.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/const.ts b/lib/const.ts index f7b70446..72902d2a 100644 --- a/lib/const.ts +++ b/lib/const.ts @@ -21,4 +21,4 @@ export const OUTBOUND_EMAIL_DOMAIN = "@recoupable.com"; export const SUPABASE_STORAGE_BUCKET = "user-files"; /** Default from address for outbound emails sent by the agent */ -export const RECOUP_FROM_EMAIL = "Agent by Recoup "; +export const RECOUP_FROM_EMAIL = `Agent by Recoup `; From ec9eb1064df56b3ae19c0463bc0edda1941a60cb Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Wed, 7 Jan 2026 12:17:16 -0500 Subject: [PATCH 3/8] docs: sync test branch with main when starting new tasks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update CLAUDE.md to sync test with main at the start of each new task 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- CLAUDE.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index 4300a2ce..74e9f4fc 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -11,6 +11,18 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co 3. **NEVER push directly to `main` or `test` branches** - always use feature branches and PRs 4. Before pushing, verify the current branch is not `main` or `test` +### Starting a New Task + +When starting a new task, **first sync the `test` branch with `main`**: + +```bash +git checkout test && git pull origin test && git fetch origin main && git merge origin/main && git push origin test +``` + +Then checkout main, pull latest, and create your feature branch from there. + +This is the **only** time you should push directly to `test`. + ## Build Commands ```bash From 5076d4029893ee8f127fd054b8db6ad636e6f3b8 Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Wed, 7 Jan 2026 12:45:43 -0500 Subject: [PATCH 4/8] feat: add shared email footer and send_email MCP tool MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Extract email footer to shared getEmailFooter function - Update generateEmailResponse to use shared footer - Add send_email MCP tool with automatic footer appending - Add RECOUP_FROM_EMAIL constant for default sender - Add unit tests for getEmailFooter and sendEmailSchema 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- lib/const.ts | 3 + lib/emails/__tests__/getEmailFooter.test.ts | 41 +++++++++ lib/emails/getEmailFooter.ts | 27 ++++++ lib/emails/inbound/generateEmailResponse.ts | 16 +--- .../emails/__tests__/sendEmailSchema.test.ts | 83 +++++++++++++++++++ lib/mcp/tools/emails/index.ts | 11 +++ lib/mcp/tools/emails/registerSendEmailTool.ts | 49 +++++++++++ lib/mcp/tools/emails/sendEmailSchema.ts | 13 +++ lib/mcp/tools/index.ts | 2 + 9 files changed, 231 insertions(+), 14 deletions(-) create mode 100644 lib/emails/__tests__/getEmailFooter.test.ts create mode 100644 lib/emails/getEmailFooter.ts create mode 100644 lib/mcp/tools/emails/__tests__/sendEmailSchema.test.ts create mode 100644 lib/mcp/tools/emails/index.ts create mode 100644 lib/mcp/tools/emails/registerSendEmailTool.ts create mode 100644 lib/mcp/tools/emails/sendEmailSchema.ts diff --git a/lib/const.ts b/lib/const.ts index 271afcf3..8717e4a5 100644 --- a/lib/const.ts +++ b/lib/const.ts @@ -18,4 +18,7 @@ export const INBOUND_EMAIL_DOMAIN = "@mail.recoupable.com"; /** Domain for sending outbound emails (e.g., support@recoupable.com) */ export const OUTBOUND_EMAIL_DOMAIN = "@recoupable.com"; +/** Default from address for outbound emails */ +export const RECOUP_FROM_EMAIL = `Agent by Recoup `; + export const SUPABASE_STORAGE_BUCKET = "user-files"; diff --git a/lib/emails/__tests__/getEmailFooter.test.ts b/lib/emails/__tests__/getEmailFooter.test.ts new file mode 100644 index 00000000..0e5890a9 --- /dev/null +++ b/lib/emails/__tests__/getEmailFooter.test.ts @@ -0,0 +1,41 @@ +import { describe, expect, it } from "vitest"; +import { getEmailFooter } from "../getEmailFooter"; + +describe("getEmailFooter", () => { + it("includes reply note in all cases", () => { + const footer = getEmailFooter(); + expect(footer).toContain("you can reply directly to this email"); + }); + + it("includes horizontal rule", () => { + const footer = getEmailFooter(); + expect(footer).toContain(" { + const footer = getEmailFooter(); + expect(footer).not.toContain("chat.recoupable.com"); + expect(footer).not.toContain("Or continue the conversation"); + }); + + it("includes chat link when roomId is provided", () => { + const roomId = "test-room-123"; + const footer = getEmailFooter(roomId); + expect(footer).toContain(`https://chat.recoupable.com/chat/${roomId}`); + expect(footer).toContain("Or continue the conversation on Recoup"); + }); + + it("generates proper HTML with roomId", () => { + const roomId = "my-room-id"; + const footer = getEmailFooter(roomId); + expect(footer).toContain(`href="https://chat.recoupable.com/chat/${roomId}"`); + expect(footer).toContain('target="_blank"'); + expect(footer).toContain('rel="noopener noreferrer"'); + }); + + it("applies proper styling", () => { + const footer = getEmailFooter("room-id"); + expect(footer).toContain("font-size:12px"); + expect(footer).toContain("color:#6b7280"); + }); +}); diff --git a/lib/emails/getEmailFooter.ts b/lib/emails/getEmailFooter.ts new file mode 100644 index 00000000..2557ef35 --- /dev/null +++ b/lib/emails/getEmailFooter.ts @@ -0,0 +1,27 @@ +/** + * Generates a standardized email footer HTML. + * + * @param roomId - Optional room ID for the chat link. If not provided, only the reply note is shown. + * @returns HTML string for the email footer. + */ +export function getEmailFooter(roomId?: string): string { + const replyNote = ` +

+ Note: you can reply directly to this email to continue the conversation. +

`.trim(); + + const chatLink = roomId + ? ` +

+ Or continue the conversation on Recoup: + + https://chat.recoupable.com/chat/${roomId} + +

`.trim() + : ""; + + return ` +
+${replyNote} +${chatLink}`.trim(); +} diff --git a/lib/emails/inbound/generateEmailResponse.ts b/lib/emails/inbound/generateEmailResponse.ts index 9cfd0113..95f19c43 100644 --- a/lib/emails/inbound/generateEmailResponse.ts +++ b/lib/emails/inbound/generateEmailResponse.ts @@ -2,6 +2,7 @@ import { marked } from "marked"; import { ChatRequestBody } from "@/lib/chat/validateChatRequest"; import getGeneralAgent from "@/lib/agents/generalAgent/getGeneralAgent"; import { getEmailRoomMessages } from "@/lib/emails/inbound/getEmailRoomMessages"; +import { getEmailFooter } from "@/lib/emails/getEmailFooter"; /** * Generates the assistant response HTML for an email, including: @@ -29,20 +30,7 @@ export async function generateEmailResponse( const text = chatResponse.text; const bodyHtml = marked(text); - - const footerHtml = ` -
-

- Note: you can reply directly to this email to continue the conversation. -

-

- Or continue the conversation on Recoup: - - https://chat.recoupable.com/chat/${roomId} - -

-`.trim(); - + const footerHtml = getEmailFooter(roomId); const html = `${bodyHtml}\n\n${footerHtml}`; return { text, html }; diff --git a/lib/mcp/tools/emails/__tests__/sendEmailSchema.test.ts b/lib/mcp/tools/emails/__tests__/sendEmailSchema.test.ts new file mode 100644 index 00000000..dca8fdfd --- /dev/null +++ b/lib/mcp/tools/emails/__tests__/sendEmailSchema.test.ts @@ -0,0 +1,83 @@ +import { describe, expect, it } from "vitest"; +import { sendEmailSchema } from "../sendEmailSchema"; + +describe("sendEmailSchema", () => { + it("accepts valid input with required fields", () => { + const input = { + to: ["test@example.com"], + subject: "Test Subject", + body: "Test body content", + }; + const result = sendEmailSchema.safeParse(input); + expect(result.success).toBe(true); + }); + + it("accepts valid input with optional room_id", () => { + const input = { + to: ["test@example.com"], + subject: "Test Subject", + body: "Test body content", + room_id: "room-123", + }; + const result = sendEmailSchema.safeParse(input); + expect(result.success).toBe(true); + }); + + it("accepts multiple recipients", () => { + const input = { + to: ["test1@example.com", "test2@example.com"], + subject: "Test Subject", + body: "Test body content", + }; + const result = sendEmailSchema.safeParse(input); + expect(result.success).toBe(true); + }); + + it("rejects empty to array", () => { + const input = { + to: [], + subject: "Test Subject", + body: "Test body content", + }; + const result = sendEmailSchema.safeParse(input); + expect(result.success).toBe(false); + }); + + it("rejects invalid email addresses", () => { + const input = { + to: ["not-an-email"], + subject: "Test Subject", + body: "Test body content", + }; + const result = sendEmailSchema.safeParse(input); + expect(result.success).toBe(false); + }); + + it("rejects empty subject", () => { + const input = { + to: ["test@example.com"], + subject: "", + body: "Test body content", + }; + const result = sendEmailSchema.safeParse(input); + expect(result.success).toBe(false); + }); + + it("rejects empty body", () => { + const input = { + to: ["test@example.com"], + subject: "Test Subject", + body: "", + }; + const result = sendEmailSchema.safeParse(input); + expect(result.success).toBe(false); + }); + + it("rejects missing required fields", () => { + const input = { + to: ["test@example.com"], + }; + const result = sendEmailSchema.safeParse(input); + expect(result.success).toBe(false); + }); +}); diff --git a/lib/mcp/tools/emails/index.ts b/lib/mcp/tools/emails/index.ts new file mode 100644 index 00000000..050c1a9f --- /dev/null +++ b/lib/mcp/tools/emails/index.ts @@ -0,0 +1,11 @@ +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { registerSendEmailTool } from "./registerSendEmailTool"; + +/** + * Registers all email-related MCP tools on the server. + * + * @param server - The MCP server instance to register tools on. + */ +export const registerAllEmailTools = (server: McpServer): void => { + registerSendEmailTool(server); +}; diff --git a/lib/mcp/tools/emails/registerSendEmailTool.ts b/lib/mcp/tools/emails/registerSendEmailTool.ts new file mode 100644 index 00000000..aeaac232 --- /dev/null +++ b/lib/mcp/tools/emails/registerSendEmailTool.ts @@ -0,0 +1,49 @@ +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { sendEmailSchema, type SendEmailInput } from "./sendEmailSchema"; +import { getToolResultSuccess } from "@/lib/mcp/getToolResultSuccess"; +import { getToolResultError } from "@/lib/mcp/getToolResultError"; +import { getResendClient } from "@/lib/emails/client"; +import { getEmailFooter } from "@/lib/emails/getEmailFooter"; +import { RECOUP_FROM_EMAIL } from "@/lib/const"; + +/** + * Registers the "send_email" tool on the MCP server. + * Sends an email with the specified recipients, subject, and body. + * Automatically appends the standard email footer. + * + * @param server - The MCP server instance to register the tool on. + */ +export function registerSendEmailTool(server: McpServer): void { + server.registerTool( + "send_email", + { + description: + "Send an email to one or more recipients. Use this to send emails on behalf of the user.", + inputSchema: sendEmailSchema, + }, + async (args: SendEmailInput) => { + const { to, subject, body, room_id } = args; + + const footer = getEmailFooter(room_id); + const htmlBody = `${body}\n\n${footer}`; + + const resend = getResendClient(); + const { data, error } = await resend.emails.send({ + from: RECOUP_FROM_EMAIL, + to, + subject, + html: htmlBody, + }); + + if (error) { + return getToolResultError(`Failed to send email: ${error.message}`); + } + + return getToolResultSuccess({ + success: true, + email_id: data?.id, + message: `Email sent successfully to ${to.join(", ")}`, + }); + }, + ); +} diff --git a/lib/mcp/tools/emails/sendEmailSchema.ts b/lib/mcp/tools/emails/sendEmailSchema.ts new file mode 100644 index 00000000..590ffec5 --- /dev/null +++ b/lib/mcp/tools/emails/sendEmailSchema.ts @@ -0,0 +1,13 @@ +import { z } from "zod"; + +export const sendEmailSchema = z.object({ + to: z + .array(z.string().email()) + .min(1, "At least one recipient is required") + .describe("Array of recipient email addresses"), + subject: z.string().min(1, "Subject cannot be empty").describe("Email subject line"), + body: z.string().min(1, "Body cannot be empty").describe("Email body content (can include HTML)"), + room_id: z.string().optional().describe("Optional room ID to include in the email footer link"), +}); + +export type SendEmailInput = z.infer; diff --git a/lib/mcp/tools/index.ts b/lib/mcp/tools/index.ts index 033b80a6..a6b08f36 100644 --- a/lib/mcp/tools/index.ts +++ b/lib/mcp/tools/index.ts @@ -13,6 +13,7 @@ import { registerAllFileTools } from "./files"; import { registerCreateSegmentsTool } from "./registerCreateSegmentsTool"; import { registerAllYouTubeTools } from "./youtube"; import { registerTranscribeTools } from "./transcribe"; +import { registerAllEmailTools } from "./emails"; /** * Registers all MCP tools on the server. @@ -23,6 +24,7 @@ import { registerTranscribeTools } from "./transcribe"; export const registerAllTools = (server: McpServer): void => { registerAllArtistSocialsTools(server); registerAllCatalogTools(server); + registerAllEmailTools(server); registerAllFileTools(server); registerAllImageTools(server); registerAllSora2Tools(server); From a3519ef7427d905fc5e02f75d50022209cfc7f73 Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Wed, 7 Jan 2026 12:49:45 -0500 Subject: [PATCH 5/8] docs: note PRs should target test branch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- CLAUDE.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CLAUDE.md b/CLAUDE.md index 4300a2ce..63e578ca 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -10,6 +10,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co 2. Push commits to the current feature branch 3. **NEVER push directly to `main` or `test` branches** - always use feature branches and PRs 4. Before pushing, verify the current branch is not `main` or `test` +5. **Open PRs against the `test` branch**, not `main` ## Build Commands From a9ed73414184a4ca28bbd36f75b099947ccbb1f7 Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Wed, 7 Jan 2026 13:13:13 -0500 Subject: [PATCH 6/8] fix: always use HTML for emails, convert text to HTML if needed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Footer is now always included regardless of text vs html input - Text input is converted to HTML with

tags - Removed text field from sendEmailWithResend call (DRY/KISS) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- lib/mcp/tools/__tests__/registerSendEmailTool.test.ts | 3 +-- lib/mcp/tools/registerSendEmailTool.ts | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/mcp/tools/__tests__/registerSendEmailTool.test.ts b/lib/mcp/tools/__tests__/registerSendEmailTool.test.ts index f93c5bab..509e1aba 100644 --- a/lib/mcp/tools/__tests__/registerSendEmailTool.test.ts +++ b/lib/mcp/tools/__tests__/registerSendEmailTool.test.ts @@ -49,8 +49,7 @@ describe("registerSendEmailTool", () => { to: ["test@example.com"], cc: undefined, subject: "Test Subject", - text: "Test body", - html: expect.stringContaining("you can reply directly to this email"), + html: expect.stringMatching(/Test body.*you can reply directly to this email/s), headers: {}, }); diff --git a/lib/mcp/tools/registerSendEmailTool.ts b/lib/mcp/tools/registerSendEmailTool.ts index 5e259e19..dd4fa3ef 100644 --- a/lib/mcp/tools/registerSendEmailTool.ts +++ b/lib/mcp/tools/registerSendEmailTool.ts @@ -24,14 +24,14 @@ export function registerSendEmailTool(server: McpServer): void { const { to, cc = [], subject, text, html = "", headers = {}, room_id } = args; const footer = getEmailFooter(room_id); - const htmlWithFooter = html ? `${html}\n\n${footer}` : footer; + const bodyHtml = html || (text ? `

${text}

` : ""); + const htmlWithFooter = `${bodyHtml}\n\n${footer}`; const result = await sendEmailWithResend({ from: RECOUP_FROM_EMAIL, to, cc: cc.length > 0 ? cc : undefined, subject, - text, html: htmlWithFooter, headers, }); From 1f05769669b32256941e69093d78b7476e6e2878 Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Wed, 7 Jan 2026 13:36:45 -0500 Subject: [PATCH 7/8] refactor: use marked for text-to-HTML conversion in send_email tool MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace hard-coded

tags with marked library for consistent markdown parsing 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- lib/mcp/tools/registerSendEmailTool.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/mcp/tools/registerSendEmailTool.ts b/lib/mcp/tools/registerSendEmailTool.ts index dd4fa3ef..3f29eb5d 100644 --- a/lib/mcp/tools/registerSendEmailTool.ts +++ b/lib/mcp/tools/registerSendEmailTool.ts @@ -6,6 +6,7 @@ import { getToolResultError } from "@/lib/mcp/getToolResultError"; import { RECOUP_FROM_EMAIL } from "@/lib/const"; import { getEmailFooter } from "@/lib/emails/getEmailFooter"; import { NextResponse } from "next/server"; +import { marked } from "marked"; /** * Registers the "send_email" tool on the MCP server. @@ -24,7 +25,7 @@ export function registerSendEmailTool(server: McpServer): void { const { to, cc = [], subject, text, html = "", headers = {}, room_id } = args; const footer = getEmailFooter(room_id); - const bodyHtml = html || (text ? `

${text}

` : ""); + const bodyHtml = html || (text ? marked(text) : ""); const htmlWithFooter = `${bodyHtml}\n\n${footer}`; const result = await sendEmailWithResend({ From ed459441e496fb9ba3329b04af5afd2dba29669a Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Wed, 7 Jan 2026 13:39:40 -0500 Subject: [PATCH 8/8] refactor: use zod email validation for to and cc fields MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- lib/emails/sendEmailSchema.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/emails/sendEmailSchema.ts b/lib/emails/sendEmailSchema.ts index a8512630..820a59de 100644 --- a/lib/emails/sendEmailSchema.ts +++ b/lib/emails/sendEmailSchema.ts @@ -1,9 +1,9 @@ import { z } from "zod"; export const sendEmailSchema = z.object({ - to: z.array(z.string()).describe("Recipient email address or array of addresses"), + to: z.array(z.string().email()).describe("Recipient email address or array of addresses"), cc: z - .array(z.string()) + .array(z.string().email()) .describe( "Optional array of CC email addresses. active_account_email should always be included unless already in 'to'.", )