Skip to content

Commit 54bb239

Browse files
authored
Merge pull request #95 from Recoupable-com/sweetmantech/myc-3863-api-email-tool-email-footer-shared-with-email-client
feat: add shared email footer and send_email MCP tool
2 parents ec9eb10 + a9ed734 commit 54bb239

File tree

9 files changed

+87
-23
lines changed

9 files changed

+87
-23
lines changed

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
1010
2. Push commits to the current feature branch
1111
3. **NEVER push directly to `main` or `test` branches** - always use feature branches and PRs
1212
4. Before pushing, verify the current branch is not `main` or `test`
13+
5. **Open PRs against the `test` branch**, not `main`
1314

1415
### Starting a New Task
1516

lib/const.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export const INBOUND_EMAIL_DOMAIN = "@mail.recoupable.com";
1818
/** Domain for sending outbound emails (e.g., support@recoupable.com) */
1919
export const OUTBOUND_EMAIL_DOMAIN = "@recoupable.com";
2020

21-
export const SUPABASE_STORAGE_BUCKET = "user-files";
22-
23-
/** Default from address for outbound emails sent by the agent */
21+
/** Default from address for outbound emails */
2422
export const RECOUP_FROM_EMAIL = `Agent by Recoup <agent${OUTBOUND_EMAIL_DOMAIN}>`;
23+
24+
export const SUPABASE_STORAGE_BUCKET = "user-files";
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { describe, expect, it } from "vitest";
2+
import { getEmailFooter } from "../getEmailFooter";
3+
4+
describe("getEmailFooter", () => {
5+
it("includes reply note in all cases", () => {
6+
const footer = getEmailFooter();
7+
expect(footer).toContain("you can reply directly to this email");
8+
});
9+
10+
it("includes horizontal rule", () => {
11+
const footer = getEmailFooter();
12+
expect(footer).toContain("<hr");
13+
});
14+
15+
it("excludes chat link when roomId is not provided", () => {
16+
const footer = getEmailFooter();
17+
expect(footer).not.toContain("chat.recoupable.com");
18+
expect(footer).not.toContain("Or continue the conversation");
19+
});
20+
21+
it("includes chat link when roomId is provided", () => {
22+
const roomId = "test-room-123";
23+
const footer = getEmailFooter(roomId);
24+
expect(footer).toContain(`https://chat.recoupable.com/chat/${roomId}`);
25+
expect(footer).toContain("Or continue the conversation on Recoup");
26+
});
27+
28+
it("generates proper HTML with roomId", () => {
29+
const roomId = "my-room-id";
30+
const footer = getEmailFooter(roomId);
31+
expect(footer).toContain(`href="https://chat.recoupable.com/chat/${roomId}"`);
32+
expect(footer).toContain('target="_blank"');
33+
expect(footer).toContain('rel="noopener noreferrer"');
34+
});
35+
36+
it("applies proper styling", () => {
37+
const footer = getEmailFooter("room-id");
38+
expect(footer).toContain("font-size:12px");
39+
expect(footer).toContain("color:#6b7280");
40+
});
41+
});

lib/emails/getEmailFooter.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/**
2+
* Generates a standardized email footer HTML.
3+
*
4+
* @param roomId - Optional room ID for the chat link. If not provided, only the reply note is shown.
5+
* @returns HTML string for the email footer.
6+
*/
7+
export function getEmailFooter(roomId?: string): string {
8+
const replyNote = `
9+
<p style="font-size:12px;color:#6b7280;margin:0 0 4px;">
10+
Note: you can reply directly to this email to continue the conversation.
11+
</p>`.trim();
12+
13+
const chatLink = roomId
14+
? `
15+
<p style="font-size:12px;color:#6b7280;margin:0;">
16+
Or continue the conversation on Recoup:
17+
<a href="https://chat.recoupable.com/chat/${roomId}" target="_blank" rel="noopener noreferrer">
18+
https://chat.recoupable.com/chat/${roomId}
19+
</a>
20+
</p>`.trim()
21+
: "";
22+
23+
return `
24+
<hr style="margin-top:24px;margin-bottom:16px;border:none;border-top:1px solid #e5e7eb;" />
25+
${replyNote}
26+
${chatLink}`.trim();
27+
}

lib/emails/inbound/generateEmailResponse.ts

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { marked } from "marked";
22
import { ChatRequestBody } from "@/lib/chat/validateChatRequest";
33
import getGeneralAgent from "@/lib/agents/generalAgent/getGeneralAgent";
44
import { getEmailRoomMessages } from "@/lib/emails/inbound/getEmailRoomMessages";
5+
import { getEmailFooter } from "@/lib/emails/getEmailFooter";
56

67
/**
78
* Generates the assistant response HTML for an email, including:
@@ -29,20 +30,7 @@ export async function generateEmailResponse(
2930
const text = chatResponse.text;
3031

3132
const bodyHtml = marked(text);
32-
33-
const footerHtml = `
34-
<hr style="margin-top:24px;margin-bottom:16px;border:none;border-top:1px solid #e5e7eb;" />
35-
<p style="font-size:12px;color:#6b7280;margin:0 0 4px;">
36-
Note: you can reply directly to this email to continue the conversation.
37-
</p>
38-
<p style="font-size:12px;color:#6b7280;margin:0;">
39-
Or continue the conversation on Recoup:
40-
<a href="https://chat.recoupable.com/chat/${roomId}" target="_blank" rel="noopener noreferrer">
41-
https://chat.recoupable.com/chat/${roomId}
42-
</a>
43-
</p>
44-
`.trim();
45-
33+
const footerHtml = getEmailFooter(roomId);
4634
const html = `${bodyHtml}\n\n${footerHtml}`;
4735

4836
return { text, html };

lib/emails/sendEmailSchema.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ export const sendEmailSchema = z.object({
2424
.describe("Optional custom headers for the email")
2525
.default({})
2626
.optional(),
27+
room_id: z
28+
.string()
29+
.describe("Optional room ID to include in the email footer link")
30+
.optional(),
2731
});
2832

2933
export type SendEmailInput = z.infer<typeof sendEmailSchema>;

lib/mcp/tools/__tests__/registerSendEmailTool.test.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,7 @@ describe("registerSendEmailTool", () => {
4949
to: ["test@example.com"],
5050
cc: undefined,
5151
subject: "Test Subject",
52-
text: "Test body",
53-
html: undefined,
52+
html: expect.stringMatching(/Test body.*you can reply directly to this email/s),
5453
headers: {},
5554
});
5655

lib/mcp/tools/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@ export const registerAllTools = (server: McpServer): void => {
3333
registerContactTeamTool(server);
3434
registerGetLocalTimeTool(server);
3535
registerSearchWebTool(server);
36+
registerSendEmailTool(server);
3637
registerUpdateAccountInfoTool(server);
3738
registerCreateSegmentsTool(server);
3839
registerAllYouTubeTools(server);
39-
registerSendEmailTool(server);
4040
};

lib/mcp/tools/registerSendEmailTool.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { sendEmailWithResend } from "@/lib/emails/sendEmail";
44
import { getToolResultSuccess } from "@/lib/mcp/getToolResultSuccess";
55
import { getToolResultError } from "@/lib/mcp/getToolResultError";
66
import { RECOUP_FROM_EMAIL } from "@/lib/const";
7+
import { getEmailFooter } from "@/lib/emails/getEmailFooter";
78
import { NextResponse } from "next/server";
89

910
/**
@@ -20,15 +21,18 @@ export function registerSendEmailTool(server: McpServer): void {
2021
inputSchema: sendEmailSchema,
2122
},
2223
async (args: SendEmailInput) => {
23-
const { to, cc = [], subject, text, html = "", headers = {} } = args;
24+
const { to, cc = [], subject, text, html = "", headers = {}, room_id } = args;
25+
26+
const footer = getEmailFooter(room_id);
27+
const bodyHtml = html || (text ? `<p>${text}</p>` : "");
28+
const htmlWithFooter = `${bodyHtml}\n\n${footer}`;
2429

2530
const result = await sendEmailWithResend({
2631
from: RECOUP_FROM_EMAIL,
2732
to,
2833
cc: cc.length > 0 ? cc : undefined,
2934
subject,
30-
text,
31-
html: html || undefined,
35+
html: htmlWithFooter,
3236
headers,
3337
});
3438

0 commit comments

Comments
 (0)