Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
cf49bdf
Enhance Mailtrap client configuration to conditionally set accountId …
narekhovhannisyan Aug 8, 2025
7ef5169
Update README.md to include new Mailtrap template management operatio…
narekhovhannisyan Aug 8, 2025
d48c223
Add email template management operations to the server
narekhovhannisyan Aug 8, 2025
4685d70
Add email template management functions: create, delete, list, and up…
narekhovhannisyan Aug 8, 2025
8925fa8
Add index file for email template management functions
narekhovhannisyan Aug 8, 2025
b4b09f0
Add unit tests for email template management functions
narekhovhannisyan Aug 8, 2025
19dd35f
Add schemas for email template management operations
narekhovhannisyan Aug 8, 2025
0a93350
Add interfaces for email template management requests
narekhovhannisyan Aug 8, 2025
ac0f7f2
Fix typo in README.md by correcting "MAILTRA_ACCOUNT_ID" to "MAILTRAP…
narekhovhannisyan Aug 11, 2025
58ddacf
Enhance Mailtrap client configuration to validate MAILTRAP_ACCOUNT_ID…
narekhovhannisyan Aug 11, 2025
dfd0606
Implement validation for updateTemplate function to ensure at least o…
narekhovhannisyan Aug 11, 2025
7f60905
Add test for updateTemplate function to reject updates with no fields…
narekhovhannisyan Aug 11, 2025
3c20f57
Update README.md to clarify requirements for update-template function
narekhovhannisyan Aug 12, 2025
e9a5127
improve readme
yanchuk Aug 12, 2025
5585290
Update Mailtrap dependency to version 4.2.0 and add yarn.lock file fo…
narekhovhannisyan Aug 15, 2025
de31afe
Update Mailtrap dependency to version 4.2.0 and remove yarn.lock file…
narekhovhannisyan Aug 15, 2025
9e6bdae
bump version
yanchuk Aug 18, 2025
e3eefec
fix cursor and vs code links
yanchuk Aug 18, 2025
7b495ed
add claude.md
yanchuk Aug 18, 2025
d250419
add agent.md
yanchuk Aug 18, 2025
1fe652e
Update SendMailToolRequest interface to require category field
narekhovhannisyan Aug 20, 2025
ed84ae3
Refactor category field in sendEmail schema to be required for tracking
narekhovhannisyan Aug 20, 2025
c344b54
Clarify requirements for email template fields in README.md
narekhovhannisyan Aug 20, 2025
0f8516c
Update README.md to change category field from optional to required f…
narekhovhannisyan Aug 21, 2025
62f8727
Update sendEmail tests to include required category field in email data
narekhovhannisyan Aug 21, 2025
6ec93c5
Implement validation for email template creation to ensure at least o…
narekhovhannisyan Aug 25, 2025
f2264aa
Enhance validation in updateTemplate function to ensure that when bot…
narekhovhannisyan Aug 25, 2025
5533355
Make 'html' field optional in createTemplate schema to allow for more…
narekhovhannisyan Aug 25, 2025
5c33992
Make 'html' field optional in CreateTemplateRequest interface to enha…
narekhovhannisyan Aug 25, 2025
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
69 changes: 63 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ If you are using `asdf` for managing Node.js you must use absolute path to execu
"ASDF_DATA_DIR": "/Users/<username>/.asdf",
"ASDF_NODEJS_VERSION": "20.6.1",
"MAILTRAP_API_TOKEN": "your_mailtrap_api_token",
"DEFAULT_FROM_EMAIL": "your_sender@example.com"
"DEFAULT_FROM_EMAIL": "your_sender@example.com",
"MAILTRAP_ACCOUNT_ID": "your_account_id"
}
}
}
Expand Down Expand Up @@ -102,11 +103,20 @@ Then, in the settings file, add the following configuration:

## Usage

Once configured, you can ask agent to send emails, for example:
Once configured, you can ask agent to send emails and manage templates, for example:

**Email Operations:**

- "Send an email to john.doe@example.com with the subject 'Meeting Tomorrow' and a friendly reminder about our upcoming meeting."
- "Email sarah@example.com about the project update, and CC the team at team@example.com"

**Template Operations:**

- "List all email templates in my Mailtrap account"
- "Create a new email template called 'Welcome Email' with subject 'Welcome to our platform!'"
- "Update the template with ID 12345 to change the subject to 'Updated Welcome Message'"
- "Delete the template with ID 67890"

## Available Tools

### send-email
Expand All @@ -118,12 +128,56 @@ Sends a transactional email through Mailtrap.
- `to` (required): Email address of the recipient
- `subject` (required): Email subject line
- `from` (optional): Email address of the sender, if not provided "DEFAULT_FROM_EMAIL" will be used
- `text` (optional): Email body text, require if "html" is empty
- `text` (optional): Email body text, required if "html" is empty
- `html` (optional): HTML version of the email body, required if "text" is empty
- `cc` (optional): Array of CC recipient email addresses
- `bcc` (optional): Array of BCC recipient email addresses
- `category` (optional): Email category for tracking

### create-template

Creates a new email template in your Mailtrap account.

**Parameters:**

- `name` (required): Name of the template
- `subject` (required): Email subject line
- `html` (required): HTML content of the template
- `text` (optional): Plain text version of the template
- `category` (optional): Template category (defaults to "General")

### list-templates

Lists all email templates in your Mailtrap account.

**Parameters:**

- No parameters required

### update-template

Updates an existing email template.

**Parameters:**

- `template_id` (required): ID of the template to update
- `name` (optional): New name for the template
- `subject` (optional): New email subject line
- `html` (optional): New HTML content of the template
- `text` (optional): New plain text version of the template
- `category` (optional): New category for the template

> [!NOTE]
> At least one updatable field (name, subject, html, text, or category) must be provided when calling update-template to perform an update.

### delete-template

Deletes an existing email template.

**Parameters:**

- `template_id` (required): ID of the template to delete

## Development

1. Clone the repository:
Expand Down Expand Up @@ -154,7 +208,8 @@ Add the following configuration:
"args": ["/path/to/mailtrap-mcp/dist/index.js"],
"env": {
"MAILTRAP_API_TOKEN": "your_mailtrap_api_token",
"DEFAULT_FROM_EMAIL": "your_sender@example.com"
"DEFAULT_FROM_EMAIL": "your_sender@example.com",
"MAILTRAP_ACCOUNT_ID": "your_account_id"
}
}
}
Expand All @@ -177,7 +232,8 @@ If you are using `asdf` for managing Node.js you should use absolute path to exe
"ASDF_DATA_DIR": "/Users/<username>/.asdf",
"ASDF_NODEJS_VERSION": "20.6.1",
"MAILTRAP_API_TOKEN": "your_mailtrap_api_token",
"DEFAULT_FROM_EMAIL": "your_sender@example.com"
"DEFAULT_FROM_EMAIL": "your_sender@example.com",
"MAILTRAP_ACCOUNT_ID": "your_account_id"
}
}
}
Expand All @@ -198,7 +254,8 @@ If you are using `asdf` for managing Node.js you should use absolute path to exe
"args": ["/path/to/mailtrap-mcp/dist/index.js"],
"env": {
"MAILTRAP_API_TOKEN": "your_mailtrap_api_token",
"DEFAULT_FROM_EMAIL": "your_sender@example.com"
"DEFAULT_FROM_EMAIL": "your_sender@example.com",
"MAILTRAP_ACCOUNT_ID": "your_account_id"
}
}
}
Expand Down
4 changes: 4 additions & 0 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ if (!MAILTRAP_API_TOKEN) {

const client = new MailtrapClient({
token: MAILTRAP_API_TOKEN,
// conditionally set accountId if it's a valid number
...(process.env.MAILTRAP_ACCOUNT_ID && !isNaN(Number(process.env.MAILTRAP_ACCOUNT_ID))
? { accountId: Number(process.env.MAILTRAP_ACCOUNT_ID) }
: {}),
});

// eslint-disable-next-line import/prefer-default-export
Expand Down
41 changes: 41 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,16 @@ import dotenv from "dotenv";
import CONFIG from "./config";

import { sendEmailSchema, sendEmail } from "./tools/sendEmail";
import {
createTemplate,
createTemplateSchema,
deleteTemplate,
deleteTemplateSchema,
listTemplates,
listTemplatesSchema,
updateTemplate,
updateTemplateSchema,
} from "./tools/templates";

dotenv.config();

Expand All @@ -14,13 +24,44 @@ const server = new McpServer({
version: CONFIG.MCP_SERVER_VERSION,
});

/**
* Sending operations.
*/
server.tool(
"send-email",
"Send transactional email using Mailtrap",
sendEmailSchema,
sendEmail
);

/**
* Templates operations.
*/
server.tool(
"create-template",
"Create a new email template",
createTemplateSchema,
createTemplate
);
server.tool(
"list-templates",
"List all email templates",
listTemplatesSchema,
listTemplates
);
server.tool(
"update-template",
"Update an existing email template",
updateTemplateSchema,
updateTemplate
);
server.tool(
"delete-template",
"Delete an existing email template",
deleteTemplateSchema,
deleteTemplate
);

async function main() {
const transport = new StdioServerTransport();

Expand Down
173 changes: 173 additions & 0 deletions src/tools/templates/__tests__/createTemplate.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
import createTemplate from "../createTemplate";
import { client } from "../../../client";

jest.mock("../../../client", () => ({
client: {
templates: {
create: jest.fn(),
},
},
}));

describe("createTemplate", () => {
const mockTemplateData = {
name: "Test Template",
subject: "Test Email Subject",
html: "<h1>Test Template</h1><p>This is a test template.</p>",
text: "Test Template\n\nThis is a test template.",
category: "Test",
};

const mockResponse = {
id: 12345,
uuid: "abc-def-ghi",
name: mockTemplateData.name,
subject: mockTemplateData.subject,
category: mockTemplateData.category,
};

beforeEach(() => {
jest.clearAllMocks();
(client.templates.create as jest.Mock).mockResolvedValue(mockResponse);
});

it("should create template successfully with all required fields", async () => {
const result = await createTemplate(mockTemplateData);

expect(client.templates.create).toHaveBeenCalledWith({
name: mockTemplateData.name,
subject: mockTemplateData.subject,
category: mockTemplateData.category,
body_html: mockTemplateData.html,
body_text: mockTemplateData.text,
});

expect(result).toEqual({
content: [
{
type: "text",
text: `Template "${mockTemplateData.name}" created successfully!\nTemplate ID: ${mockResponse.id}\nTemplate UUID: ${mockResponse.uuid}`,
},
],
});
});

it("should create template successfully with default category", async () => {
const templateDataWithoutCategory = {
name: mockTemplateData.name,
subject: mockTemplateData.subject,
html: mockTemplateData.html,
text: mockTemplateData.text,
};

const result = await createTemplate(templateDataWithoutCategory);

expect(client.templates.create).toHaveBeenCalledWith({
name: mockTemplateData.name,
subject: mockTemplateData.subject,
category: "General",
body_html: mockTemplateData.html,
body_text: mockTemplateData.text,
});

expect(result).toEqual({
content: [
{
type: "text",
text: `Template "${mockTemplateData.name}" created successfully!\nTemplate ID: ${mockResponse.id}\nTemplate UUID: ${mockResponse.uuid}`,
},
],
});
});

it("should create template successfully without text content", async () => {
const templateDataWithoutText = {
name: mockTemplateData.name,
subject: mockTemplateData.subject,
html: mockTemplateData.html,
category: mockTemplateData.category,
};

const result = await createTemplate(templateDataWithoutText);

expect(client.templates.create).toHaveBeenCalledWith({
name: mockTemplateData.name,
subject: mockTemplateData.subject,
category: mockTemplateData.category,
body_html: mockTemplateData.html,
body_text: undefined,
});

expect(result).toEqual({
content: [
{
type: "text",
text: `Template "${mockTemplateData.name}" created successfully!\nTemplate ID: ${mockResponse.id}\nTemplate UUID: ${mockResponse.uuid}`,
},
],
});
});

it("should create template successfully with custom category", async () => {
const customCategory = "Marketing";
const templateDataWithCustomCategory = {
...mockTemplateData,
category: customCategory,
};

const result = await createTemplate(templateDataWithCustomCategory);

expect(client.templates.create).toHaveBeenCalledWith({
name: mockTemplateData.name,
subject: mockTemplateData.subject,
category: customCategory,
body_html: mockTemplateData.html,
body_text: mockTemplateData.text,
});

expect(result).toEqual({
content: [
{
type: "text",
text: `Template "${mockTemplateData.name}" created successfully!\nTemplate ID: ${mockResponse.id}\nTemplate UUID: ${mockResponse.uuid}`,
},
],
});
});

describe("error handling", () => {
it("should handle client.templates.create failure", async () => {
const mockError = new Error("Failed to create template");
(client.templates.create as jest.Mock).mockRejectedValue(mockError);

const result = await createTemplate(mockTemplateData);

expect(result).toEqual({
content: [
{
type: "text",
text: "Failed to create template: Failed to create template",
},
],
isError: true,
});
});

it("should handle non-Error exceptions", async () => {
const mockError = "String error";
(client.templates.create as jest.Mock).mockRejectedValue(mockError);

const result = await createTemplate(mockTemplateData);

expect(result).toEqual({
content: [
{
type: "text",
text: "Failed to create template: String error",
},
],
isError: true,
});
});
});
});
Loading