From 055734e629ba55b2e3b500166065557a5e9c453c Mon Sep 17 00:00:00 2001 From: mayankbohradev Date: Sat, 11 Jul 2026 09:44:03 +0530 Subject: [PATCH] feat: add custom headers to send-email and send-batch-emails --- README.md | 2 +- src/tools/emails.ts | 21 ++++++++ tests/tools/emails.test.ts | 108 +++++++++++++++++++++++++++++++++++++ 3 files changed, 130 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a7458aa..51a74c3 100644 --- a/README.md +++ b/README.md @@ -380,7 +380,7 @@ You can pass additional arguments to configure the local server: Resend's MCP server gives your AI agent native access to the full Resend platform through a single integration. You can manage all aspects of your email infrastructure using natural language. -- **Emails**: Send, list, get, cancel, update, and batch send emails. Supports HTML, plain text, attachments (local file, URL, or base64), CC/BCC, reply-to, scheduling, tags, and topic-based sending. +- **Emails**: Send, list, get, cancel, update, and batch send emails. Supports HTML, plain text, attachments (local file, URL, or base64), CC/BCC, reply-to, scheduling, tags, custom headers, topic-based sending, and idempotency keys. - **Received Emails**: List and read inbound emails. List and download received email attachments. - **Templates**: Create, list, get, update, publish, duplicate, and remove email templates. Supports composing template content and `{{{VARIABLE}}}` placeholders. - **Contacts**: Create, list, get, update, and remove contacts. Manage segment memberships, topic subscriptions, and CSV contact imports. Supports custom contact properties. diff --git a/src/tools/emails.ts b/src/tools/emails.ts index e79a441..3308ca7 100644 --- a/src/tools/emails.ts +++ b/src/tools/emails.ts @@ -122,6 +122,12 @@ export function addEmailTools( .describe( 'Topic ID for subscription-based sending. When set, the email respects contact subscription preferences for this topic.', ), + headers: z + .record(z.string(), z.string()) + .optional() + .describe( + 'Optional custom email headers as key/value pairs (e.g. {"List-Unsubscribe": "", "X-Entity-Ref-ID": "unique-id"}). Use for one-click unsubscribe, preventing Gmail threading, or other MIME headers Resend accepts.', + ), idempotencyKey: z .string() .max(256) @@ -165,6 +171,7 @@ export function addEmailTools( attachments, tags, topicId, + headers, idempotencyKey, }) => { const fromEmailAddress = from ?? senderEmailAddress; @@ -207,6 +214,7 @@ export function addEmailTools( value: string; }>; topicId?: string; + headers?: Record; } = { to, subject, @@ -273,6 +281,10 @@ export function addEmailTools( emailRequest.topicId = topicId; } + if (headers && Object.keys(headers).length > 0) { + emailRequest.headers = headers; + } + const response = await resend.emails.send( emailRequest, idempotencyKey ? { idempotencyKey } : undefined, @@ -1020,6 +1032,12 @@ export function addEmailTools( .string() .optional() .describe('Topic ID for subscription-based sending'), + headers: z + .record(z.string(), z.string()) + .optional() + .describe( + 'Optional custom email headers as key/value pairs (e.g. {"List-Unsubscribe": "", "X-Entity-Ref-ID": "unique-id"}).', + ), }), ) .min(1) @@ -1059,6 +1077,9 @@ export function addEmailTools( if (email.scheduledAt) request.scheduledAt = email.scheduledAt; if (email.tags && email.tags.length > 0) request.tags = email.tags; if (email.topicId) request.topicId = email.topicId; + if (email.headers && Object.keys(email.headers).length > 0) { + request.headers = email.headers; + } return request; }); diff --git a/tests/tools/emails.test.ts b/tests/tools/emails.test.ts index b0aa152..94888dc 100644 --- a/tests/tools/emails.test.ts +++ b/tests/tools/emails.test.ts @@ -195,3 +195,111 @@ describe('send-batch-emails idempotency key', () => { expect(batchSend).toHaveBeenCalledWith(expect.any(Array), undefined); }); }); + +describe('send-email custom headers', () => { + beforeEach(() => { + vi.clearAllMocks(); + send.mockResolvedValue({ + data: { id: 'email_1' }, + error: null, + }); + }); + + it('passes headers through to the SDK email payload', async () => { + const client = await makeClient(); + const result = await client.callTool({ + name: 'send-email', + arguments: { + from: 'Acme ', + to: ['delivered@resend.dev'], + subject: 'hello', + text: 'world', + headers: { + 'List-Unsubscribe': '', + 'X-Entity-Ref-ID': 'order-123', + }, + }, + }); + + expect(result.isError).toBeFalsy(); + expect(send).toHaveBeenCalledWith( + expect.objectContaining({ + headers: { + 'List-Unsubscribe': '', + 'X-Entity-Ref-ID': 'order-123', + }, + }), + undefined, + ); + }); + + it('omits headers from the SDK payload when not provided', async () => { + const client = await makeClient(); + await client.callTool({ + name: 'send-email', + arguments: { + from: 'onboarding@resend.dev', + to: ['delivered@resend.dev'], + subject: 'hello', + text: 'world', + }, + }); + + const [payload] = send.mock.calls[0]; + expect(payload).not.toHaveProperty('headers'); + }); +}); + +describe('send-batch-emails custom headers', () => { + beforeEach(() => { + vi.clearAllMocks(); + batchSend.mockResolvedValue({ + data: { data: [{ id: 'email_1' }, { id: 'email_2' }] }, + error: null, + }); + }); + + it('passes per-email headers through to the SDK batch payload', async () => { + const client = await makeClient(); + const result = await client.callTool({ + name: 'send-batch-emails', + arguments: { + emails: [ + { + from: 'Acme ', + to: ['foo@example.com'], + subject: 'hello', + text: 'one', + headers: { + 'X-Entity-Ref-ID': 'batch-1', + }, + }, + { + from: 'Acme ', + to: ['bar@example.com'], + subject: 'hello', + text: 'two', + }, + ], + }, + }); + + expect(result.isError).toBeFalsy(); + expect(batchSend).toHaveBeenCalledWith( + [ + expect.objectContaining({ + to: ['foo@example.com'], + headers: { + 'X-Entity-Ref-ID': 'batch-1', + }, + }), + expect.objectContaining({ + to: ['bar@example.com'], + }), + ], + undefined, + ); + const [, secondEmail] = batchSend.mock.calls[0][0]; + expect(secondEmail).not.toHaveProperty('headers'); + }); +});