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
9 changes: 8 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,16 @@ All notable changes to the MCP Send Email project will be documented in this fil
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]
## [1.1.0] - 2025-07-08

### Added
- List audiences tool for Resend
- Removed React Email dependencies since it's not used in the project
- Updated Resend to latest version
- Add biome for formatting

## [Unreleased]

- Improved instructions in README
- Removed test email address from example email.md

Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ Built with:
- Add CC and BCC recipients
- Configure reply-to addresses
- Customizable sender email (requires verification)
- List Resend audiences

## Demo

Expand Down
78 changes: 78 additions & 0 deletions biome.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
{
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
"organizeImports": {
"enabled": true
},
"formatter": {
"indentStyle": "space",
"indentWidth": 2,
"lineWidth": 80
},
"javascript": {
"formatter": {
"quoteStyle": "single"
}
},
"css": {
"parser": {
"cssModules": true
}
},
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"a11y": {
"noSvgWithoutTitle": "off",
"noAutofocus": "off",
"useKeyWithClickEvents": "off",
"useIframeTitle": "off",
"useButtonType": "off",
"useValidAnchor": "off",
"noPositiveTabindex": "off",
"useAriaPropsForRole": "off",
"noBlankTarget": "off",
"useFocusableInteractive": "off",
"useSemanticElements": "off",
"noLabelWithoutControl": "off"
},
"correctness": {
"noUnusedImports": "error",
"noUnusedVariables": "error",
"useExhaustiveDependencies": "off",
"useJsxKeyInIterable": "off",
"noConstantCondition": "off"
},
"complexity": {
"noUselessSwitchCase": "off",
"noBannedTypes": "off",
"noForEach": "off",
"noUselessFragments": "off"
},
"suspicious": {
"noExplicitAny": "off",
"noAssignInExpressions": "off",
"noArrayIndexKey": "off",
"noFallthroughSwitchClause": "off",
"noPrototypeBuiltins": "off",
"useDefaultSwitchClauseLast": "off",
"noConsoleLog": "error",
"noCommentText": "off"
},
"security": {
"noDangerouslySetInnerHtml": "off"
},
"style": {
"noNonNullAssertion": "off",
"useSingleVarDeclarator": "off",
"noParameterAssign": "off"
},
"performance": {
"noAccumulatingSpread": "off"
}
}
},
"files": {
"ignore": ["build", "npm-lock.yaml", "node_modules/**/*"]
}
}
108 changes: 69 additions & 39 deletions index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import { Resend } from "resend";
import minimist from "minimist";
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import minimist from 'minimist';
import { Resend } from 'resend';
import { z } from 'zod';

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

if (Array.isArray(argv["reply-to"])) {
replierEmailAddresses = argv["reply-to"];
} else if (typeof argv["reply-to"] === "string") {
replierEmailAddresses = [argv["reply-to"]];
if (Array.isArray(argv['reply-to'])) {
replierEmailAddresses = argv['reply-to'];
} else if (typeof argv['reply-to'] === 'string') {
replierEmailAddresses = [argv['reply-to']];
} else if (process.env.REPLY_TO_EMAIL_ADDRESSES) {
replierEmailAddresses = process.env.REPLY_TO_EMAIL_ADDRESSES.split(",");
replierEmailAddresses = process.env.REPLY_TO_EMAIL_ADDRESSES.split(',');
}

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

// Create server instance
const server = new McpServer({
name: "email-sending-service",
version: "1.0.0",
name: 'email-sending-service',
version: '1.0.0',
});

server.tool(
"send-email",
"Send an email using Resend",
'send-email',
'Send an email using Resend',
{
to: z.string().email().describe("Recipient email address"),
subject: z.string().describe("Email subject line"),
text: z.string().describe("Plain text email content"),
to: z.string().email().describe('Recipient email address'),
subject: z.string().describe('Email subject line'),
text: z.string().describe('Plain text email content'),
html: z
.string()
.optional()
.describe(
"HTML email content. When provided, the plain text argument MUST be provided as well."
'HTML email content. When provided, the plain text argument MUST be provided as well.',
),
cc: z
.string()
.email()
.array()
.optional()
.describe("Optional array of CC email addresses. You MUST ask the user for this parameter. Under no circumstance provide it yourself"),
.describe(
'Optional array of CC email addresses. You MUST ask the user for this parameter. Under no circumstance provide it yourself',
),
bcc: z
.string()
.email()
.array()
.optional()
.describe("Optional array of BCC email addresses. You MUST ask the user for this parameter. Under no circumstance provide it yourself"),
.describe(
'Optional array of BCC email addresses. You MUST ask the user for this parameter. Under no circumstance provide it yourself',
),
scheduledAt: z
.string()
.optional()
.describe(
"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'."
"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'.",
),
// If sender email address is not provided, the tool requires it as an argument
...(!senderEmailAddress
Expand All @@ -79,7 +83,7 @@ server.tool(
.email()
.nonempty()
.describe(
"Sender email address. You MUST ask the user for this parameter. Under no circumstance provide it yourself"
'Sender email address. You MUST ask the user for this parameter. Under no circumstance provide it yourself',
),
}
: {}),
Expand All @@ -91,7 +95,7 @@ server.tool(
.array()
.optional()
.describe(
"Optional email addresses for the email readers to reply to. You MUST ask the user for this parameter. Under no circumstance provide it yourself"
'Optional email addresses for the email readers to reply to. You MUST ask the user for this parameter. Under no circumstance provide it yourself',
),
}
: {}),
Expand All @@ -102,20 +106,20 @@ server.tool(

// Type check on from, since "from" is optionally included in the arguments schema
// This should never happen.
if (typeof fromEmailAddress !== "string") {
throw new Error("from argument must be provided.");
if (typeof fromEmailAddress !== 'string') {
throw new Error('from argument must be provided.');
}

// Similar type check for "reply-to" email addresses.
if (
typeof replyToEmailAddresses !== "string" &&
typeof replyToEmailAddresses !== 'string' &&
!Array.isArray(replyToEmailAddresses)
) {
throw new Error("replyTo argument must be provided.");
throw new Error('replyTo argument must be provided.');
}

console.error(`Debug - Sending email with from: ${fromEmailAddress}`);

// Explicitly structure the request with all parameters to ensure they're passed correctly
const emailRequest: {
to: string;
Expand All @@ -134,52 +138,78 @@ server.tool(
from: fromEmailAddress,
replyTo: replyToEmailAddresses,
};

// Add optional parameters conditionally
if (html) {
emailRequest.html = html;
}

if (scheduledAt) {
emailRequest.scheduledAt = scheduledAt;
}

if (cc) {
emailRequest.cc = cc;
}

if (bcc) {
emailRequest.bcc = bcc;
}

console.error(`Email request: ${JSON.stringify(emailRequest)}`);

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

if (response.error) {
throw new Error(
`Email failed to send: ${JSON.stringify(response.error)}`
`Email failed to send: ${JSON.stringify(response.error)}`,
);
}

return {
content: [
{
type: "text",
type: 'text',
text: `Email sent successfully! ${JSON.stringify(response.data)}`,
},
],
};
}
},
);

server.tool(
'list-audiences',
'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.',
{},
async () => {
console.error('Debug - Listing audiences');

const response = await resend.audiences.list();

if (response.error) {
throw new Error(
`Failed to list audiences: ${JSON.stringify(response.error)}`,
);
}

return {
content: [
{
type: 'text',
text: `Audiences found: ${JSON.stringify(response.data)}`,
},
],
};
},
);

async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Email sending service MCP Server running on stdio");
console.error('Email sending service MCP Server running on stdio');
}

main().catch((error) => {
console.error("Fatal error in main():", error);
console.error('Fatal error in main():', error);
process.exit(1);
});
Loading