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..34d09262 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 !== undefined ? { 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..4a8b22ba 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,65 @@ 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('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, + 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); + }); +});