From aa28e26bacfe4a3cc8cbc6f5123445441d5e72bd Mon Sep 17 00:00:00 2001 From: dielduarte Date: Thu, 20 Aug 2026 14:54:39 -0300 Subject: [PATCH 1/2] feat(emails): add share-email tool --- README.md | 2 +- src/tools/emails.ts | 41 ++++++++++++++++++++++++++++ tests/tools/emails.test.ts | 55 +++++++++++++++++++++++++++++++++++++- 3 files changed, 96 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 33253485..fde7f520 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, custom headers, topic-based sending, and idempotency keys. +- **Emails**: Send, list, get, cancel, update, share, 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 129a3685..f8cc07cf 100644 --- a/src/tools/emails.ts +++ b/src/tools/emails.ts @@ -847,6 +847,47 @@ export function addEmailTools( }, ); + server.registerTool( + 'share-email', + { + title: 'Share Email', + description: + 'Create a shareable link for a sent or received email, so anyone with the link can view it without Resend dashboard access. Works for any email ID, sent or received.', + inputSchema: { + id: z + .string() + .describe('The ID of the sent or received email to share'), + expiresIn: z + .string() + .optional() + .describe( + 'How long the share link stays valid, as a human-readable duration (e.g. "10m", "2 hours", "1 day", "1h 30m"). Defaults to 48 hours; capped at 48 hours.', + ), + }, + }, + async ({ id, expiresIn }) => { + const response = await resend.emails.share( + id, + expiresIn ? { expiresIn } : undefined, + ); + + if (response.error) { + throw new Error( + `Failed to share email: ${JSON.stringify(response.error)}`, + ); + } + + return { + content: [ + { + type: 'text', + text: `Share link for email ${response.data?.id}: ${response.data?.url}`, + }, + ], + }; + }, + ); + server.registerTool( 'list-sent-email-attachments', { diff --git a/tests/tools/emails.test.ts b/tests/tools/emails.test.ts index dd713ab6..be53d04e 100644 --- a/tests/tools/emails.test.ts +++ b/tests/tools/emails.test.ts @@ -6,9 +6,10 @@ import { addEmailTools } from '../../src/tools/emails.js'; const send = vi.fn(); const batchSend = vi.fn(); +const share = vi.fn(); const resend = { - emails: { send }, + emails: { send, share }, batch: { send: batchSend }, } as unknown as Resend; @@ -404,3 +405,55 @@ describe('send-batch-emails custom headers', () => { expect(secondEmail).not.toHaveProperty('headers'); }); }); + +describe('share-email', () => { + beforeEach(() => { + vi.clearAllMocks(); + share.mockResolvedValue({ + data: { + object: 'email', + id: 'email_1', + url: 'https://resend.com/share/abc123', + }, + error: null, + }); + }); + + it('shares an email and returns the share URL', async () => { + const client = await makeClient(); + const result = await client.callTool({ + name: 'share-email', + arguments: { id: 'email_1' }, + }); + + expect(result.isError).toBeFalsy(); + expect(share).toHaveBeenCalledWith('email_1', undefined); + expect(textOf(result)).toContain('https://resend.com/share/abc123'); + }); + + it('passes expiresIn through to the SDK when provided', async () => { + const client = await makeClient(); + const result = await client.callTool({ + name: 'share-email', + arguments: { id: 'email_1', expiresIn: '1h 30m' }, + }); + + expect(result.isError).toBeFalsy(); + expect(share).toHaveBeenCalledWith('email_1', { expiresIn: '1h 30m' }); + }); + + it('surfaces SDK errors', async () => { + share.mockResolvedValue({ + data: null, + error: { name: 'not_found', message: 'Email not found' }, + }); + + const client = await makeClient(); + const result = await client.callTool({ + name: 'share-email', + arguments: { id: 'missing_email' }, + }); + + expect(result.isError).toBe(true); + }); +}); From a0e710ae9292502efeb6d0f73f67bb4e228dddf4 Mon Sep 17 00:00:00 2001 From: dielduarte Date: Thu, 20 Aug 2026 17:15:21 -0300 Subject: [PATCH 2/2] fix(emails): don't drop an explicit empty expiresIn Truthiness collapsed an explicitly-empty value into "not provided", silently applying the API's default instead of surfacing its validation error. --- src/tools/emails.ts | 2 +- tests/tools/emails.test.ts | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/tools/emails.ts b/src/tools/emails.ts index f8cc07cf..34d09262 100644 --- a/src/tools/emails.ts +++ b/src/tools/emails.ts @@ -868,7 +868,7 @@ export function addEmailTools( async ({ id, expiresIn }) => { const response = await resend.emails.share( id, - expiresIn ? { expiresIn } : undefined, + expiresIn !== undefined ? { expiresIn } : undefined, ); if (response.error) { diff --git a/tests/tools/emails.test.ts b/tests/tools/emails.test.ts index be53d04e..4a8b22ba 100644 --- a/tests/tools/emails.test.ts +++ b/tests/tools/emails.test.ts @@ -442,6 +442,16 @@ describe('share-email', () => { expect(share).toHaveBeenCalledWith('email_1', { expiresIn: '1h 30m' }); }); + it('passes an empty expiresIn through to the SDK instead of dropping it', async () => { + const client = await makeClient(); + await client.callTool({ + name: 'share-email', + arguments: { id: 'email_1', expiresIn: '' }, + }); + + expect(share).toHaveBeenCalledWith('email_1', { expiresIn: '' }); + }); + it('surfaces SDK errors', async () => { share.mockResolvedValue({ data: null,