Skip to content

Commit 2f57fd9

Browse files
authored
Merge pull request #18 from resend/feat/add-list-audiences-tool
feat: add list audiences tool
2 parents 1e1e235 + 7b24075 commit 2f57fd9

File tree

6 files changed

+331
-1011
lines changed

6 files changed

+331
-1011
lines changed

CHANGELOG.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,16 @@ All notable changes to the MCP Send Email project will be documented in this fil
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8-
## [Unreleased]
8+
## [1.1.0] - 2025-07-08
99

1010
### Added
11+
- List audiences tool for Resend
12+
- Removed React Email dependencies since it's not used in the project
13+
- Updated Resend to latest version
14+
- Add biome for formatting
15+
16+
## [Unreleased]
17+
1118
- Improved instructions in README
1219
- Removed test email address from example email.md
1320

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ Built with:
1919
- Add CC and BCC recipients
2020
- Configure reply-to addresses
2121
- Customizable sender email (requires verification)
22+
- List Resend audiences
2223

2324
## Demo
2425

biome.json

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
{
2+
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
3+
"organizeImports": {
4+
"enabled": true
5+
},
6+
"formatter": {
7+
"indentStyle": "space",
8+
"indentWidth": 2,
9+
"lineWidth": 80
10+
},
11+
"javascript": {
12+
"formatter": {
13+
"quoteStyle": "single"
14+
}
15+
},
16+
"css": {
17+
"parser": {
18+
"cssModules": true
19+
}
20+
},
21+
"linter": {
22+
"enabled": true,
23+
"rules": {
24+
"recommended": true,
25+
"a11y": {
26+
"noSvgWithoutTitle": "off",
27+
"noAutofocus": "off",
28+
"useKeyWithClickEvents": "off",
29+
"useIframeTitle": "off",
30+
"useButtonType": "off",
31+
"useValidAnchor": "off",
32+
"noPositiveTabindex": "off",
33+
"useAriaPropsForRole": "off",
34+
"noBlankTarget": "off",
35+
"useFocusableInteractive": "off",
36+
"useSemanticElements": "off",
37+
"noLabelWithoutControl": "off"
38+
},
39+
"correctness": {
40+
"noUnusedImports": "error",
41+
"noUnusedVariables": "error",
42+
"useExhaustiveDependencies": "off",
43+
"useJsxKeyInIterable": "off",
44+
"noConstantCondition": "off"
45+
},
46+
"complexity": {
47+
"noUselessSwitchCase": "off",
48+
"noBannedTypes": "off",
49+
"noForEach": "off",
50+
"noUselessFragments": "off"
51+
},
52+
"suspicious": {
53+
"noExplicitAny": "off",
54+
"noAssignInExpressions": "off",
55+
"noArrayIndexKey": "off",
56+
"noFallthroughSwitchClause": "off",
57+
"noPrototypeBuiltins": "off",
58+
"useDefaultSwitchClauseLast": "off",
59+
"noConsoleLog": "error",
60+
"noCommentText": "off"
61+
},
62+
"security": {
63+
"noDangerouslySetInnerHtml": "off"
64+
},
65+
"style": {
66+
"noNonNullAssertion": "off",
67+
"useSingleVarDeclarator": "off",
68+
"noParameterAssign": "off"
69+
},
70+
"performance": {
71+
"noAccumulatingSpread": "off"
72+
}
73+
}
74+
},
75+
"files": {
76+
"ignore": ["build", "npm-lock.yaml", "node_modules/**/*"]
77+
}
78+
}

index.ts

Lines changed: 69 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2-
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3-
import { z } from "zod";
4-
import { Resend } from "resend";
5-
import minimist from "minimist";
1+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3+
import minimist from 'minimist';
4+
import { Resend } from 'resend';
5+
import { z } from 'zod';
66

77
// Parse command line arguments
88
const argv = minimist(process.argv.slice(2));
@@ -17,17 +17,17 @@ const senderEmailAddress = argv.sender || process.env.SENDER_EMAIL_ADDRESS;
1717
// Get reply to email addresses from command line argument or fall back to environment variable
1818
let replierEmailAddresses: string[] = [];
1919

20-
if (Array.isArray(argv["reply-to"])) {
21-
replierEmailAddresses = argv["reply-to"];
22-
} else if (typeof argv["reply-to"] === "string") {
23-
replierEmailAddresses = [argv["reply-to"]];
20+
if (Array.isArray(argv['reply-to'])) {
21+
replierEmailAddresses = argv['reply-to'];
22+
} else if (typeof argv['reply-to'] === 'string') {
23+
replierEmailAddresses = [argv['reply-to']];
2424
} else if (process.env.REPLY_TO_EMAIL_ADDRESSES) {
25-
replierEmailAddresses = process.env.REPLY_TO_EMAIL_ADDRESSES.split(",");
25+
replierEmailAddresses = process.env.REPLY_TO_EMAIL_ADDRESSES.split(',');
2626
}
2727

2828
if (!apiKey) {
2929
console.error(
30-
"No API key provided. Please set RESEND_API_KEY environment variable or use --key argument"
30+
'No API key provided. Please set RESEND_API_KEY environment variable or use --key argument',
3131
);
3232
process.exit(1);
3333
}
@@ -36,40 +36,44 @@ const resend = new Resend(apiKey);
3636

3737
// Create server instance
3838
const server = new McpServer({
39-
name: "email-sending-service",
40-
version: "1.0.0",
39+
name: 'email-sending-service',
40+
version: '1.0.0',
4141
});
4242

4343
server.tool(
44-
"send-email",
45-
"Send an email using Resend",
44+
'send-email',
45+
'Send an email using Resend',
4646
{
47-
to: z.string().email().describe("Recipient email address"),
48-
subject: z.string().describe("Email subject line"),
49-
text: z.string().describe("Plain text email content"),
47+
to: z.string().email().describe('Recipient email address'),
48+
subject: z.string().describe('Email subject line'),
49+
text: z.string().describe('Plain text email content'),
5050
html: z
5151
.string()
5252
.optional()
5353
.describe(
54-
"HTML email content. When provided, the plain text argument MUST be provided as well."
54+
'HTML email content. When provided, the plain text argument MUST be provided as well.',
5555
),
5656
cc: z
5757
.string()
5858
.email()
5959
.array()
6060
.optional()
61-
.describe("Optional array of CC email addresses. You MUST ask the user for this parameter. Under no circumstance provide it yourself"),
61+
.describe(
62+
'Optional array of CC email addresses. You MUST ask the user for this parameter. Under no circumstance provide it yourself',
63+
),
6264
bcc: z
6365
.string()
6466
.email()
6567
.array()
6668
.optional()
67-
.describe("Optional array of BCC email addresses. You MUST ask the user for this parameter. Under no circumstance provide it yourself"),
69+
.describe(
70+
'Optional array of BCC email addresses. You MUST ask the user for this parameter. Under no circumstance provide it yourself',
71+
),
6872
scheduledAt: z
6973
.string()
7074
.optional()
7175
.describe(
72-
"Optional parameter to schedule the email. This uses natural language. Examples would be 'tomorrow at 10am' or 'in 2 hours' or 'next day at 9am PST' or 'Friday at 3pm ET'."
76+
"Optional parameter to schedule the email. This uses natural language. Examples would be 'tomorrow at 10am' or 'in 2 hours' or 'next day at 9am PST' or 'Friday at 3pm ET'.",
7377
),
7478
// If sender email address is not provided, the tool requires it as an argument
7579
...(!senderEmailAddress
@@ -79,7 +83,7 @@ server.tool(
7983
.email()
8084
.nonempty()
8185
.describe(
82-
"Sender email address. You MUST ask the user for this parameter. Under no circumstance provide it yourself"
86+
'Sender email address. You MUST ask the user for this parameter. Under no circumstance provide it yourself',
8387
),
8488
}
8589
: {}),
@@ -91,7 +95,7 @@ server.tool(
9195
.array()
9296
.optional()
9397
.describe(
94-
"Optional email addresses for the email readers to reply to. You MUST ask the user for this parameter. Under no circumstance provide it yourself"
98+
'Optional email addresses for the email readers to reply to. You MUST ask the user for this parameter. Under no circumstance provide it yourself',
9599
),
96100
}
97101
: {}),
@@ -102,20 +106,20 @@ server.tool(
102106

103107
// Type check on from, since "from" is optionally included in the arguments schema
104108
// This should never happen.
105-
if (typeof fromEmailAddress !== "string") {
106-
throw new Error("from argument must be provided.");
109+
if (typeof fromEmailAddress !== 'string') {
110+
throw new Error('from argument must be provided.');
107111
}
108112

109113
// Similar type check for "reply-to" email addresses.
110114
if (
111-
typeof replyToEmailAddresses !== "string" &&
115+
typeof replyToEmailAddresses !== 'string' &&
112116
!Array.isArray(replyToEmailAddresses)
113117
) {
114-
throw new Error("replyTo argument must be provided.");
118+
throw new Error('replyTo argument must be provided.');
115119
}
116120

117121
console.error(`Debug - Sending email with from: ${fromEmailAddress}`);
118-
122+
119123
// Explicitly structure the request with all parameters to ensure they're passed correctly
120124
const emailRequest: {
121125
to: string;
@@ -134,52 +138,78 @@ server.tool(
134138
from: fromEmailAddress,
135139
replyTo: replyToEmailAddresses,
136140
};
137-
141+
138142
// Add optional parameters conditionally
139143
if (html) {
140144
emailRequest.html = html;
141145
}
142-
146+
143147
if (scheduledAt) {
144148
emailRequest.scheduledAt = scheduledAt;
145149
}
146-
150+
147151
if (cc) {
148152
emailRequest.cc = cc;
149153
}
150-
154+
151155
if (bcc) {
152156
emailRequest.bcc = bcc;
153157
}
154-
158+
155159
console.error(`Email request: ${JSON.stringify(emailRequest)}`);
156160

157161
const response = await resend.emails.send(emailRequest);
158162

159163
if (response.error) {
160164
throw new Error(
161-
`Email failed to send: ${JSON.stringify(response.error)}`
165+
`Email failed to send: ${JSON.stringify(response.error)}`,
162166
);
163167
}
164168

165169
return {
166170
content: [
167171
{
168-
type: "text",
172+
type: 'text',
169173
text: `Email sent successfully! ${JSON.stringify(response.data)}`,
170174
},
171175
],
172176
};
173-
}
177+
},
178+
);
179+
180+
server.tool(
181+
'list-audiences',
182+
'List all audiences from Resend. This tool is useful for getting the audience ID to help the user find the audience they want to use for other tools. If you need an audience ID, you MUST use this tool to get all available audiences and then ask the user to select the audience they want to use.',
183+
{},
184+
async () => {
185+
console.error('Debug - Listing audiences');
186+
187+
const response = await resend.audiences.list();
188+
189+
if (response.error) {
190+
throw new Error(
191+
`Failed to list audiences: ${JSON.stringify(response.error)}`,
192+
);
193+
}
194+
195+
return {
196+
content: [
197+
{
198+
type: 'text',
199+
text: `Audiences found: ${JSON.stringify(response.data)}`,
200+
},
201+
],
202+
};
203+
},
174204
);
175205

176206
async function main() {
177207
const transport = new StdioServerTransport();
178208
await server.connect(transport);
179-
console.error("Email sending service MCP Server running on stdio");
209+
console.error('Email sending service MCP Server running on stdio');
180210
}
181211

182212
main().catch((error) => {
183-
console.error("Fatal error in main():", error);
213+
console.error('Fatal error in main():', error);
184214
process.exit(1);
185215
});

0 commit comments

Comments
 (0)