Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
41 changes: 41 additions & 0 deletions src/tools/emails.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
{
Expand Down
65 changes: 64 additions & 1 deletion tests/tools/emails.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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);
});
});