Skip to content

Commit aad76f1

Browse files
authored
Merge pull request #63 from mailtrap/sending-domains
Add sending-domains tools
2 parents 0dcfc72 + e4ebb13 commit aad76f1

15 files changed

Lines changed: 777 additions & 2 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
* Add **list-email-logs** and **get-email-log-message** tools: query sent-mail delivery history with filters and pagination; inspect a single log by UUID (summary, event timeline, optional body via `include_content`).
44
* Add **get-sending-stats** tool: check delivery, bounce, open, click, and spam rates for a date range; optional breakdown by domain, category, email service provider, or date.
5+
* Add **sending domains** tools: **list-sending-domains**, **get-sending-domain**, **create-sending-domain**, **delete-sending-domain**. **get-sending-domain** accepts optional `include_setup_instructions: true` to append DNS setup instructions to the response.
56

67
## [0.1.0] - 2025-12-09
78

CLAUDE.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ Schema files define a JSON Schema–shaped object for MCP; optional Zod schemas
5252

5353
- `MAILTRAP_API_TOKEN`: Required API token from Mailtrap
5454
- `DEFAULT_FROM_EMAIL`: Default sender email address
55-
- `MAILTRAP_ACCOUNT_ID`: Required for almost all tools (templates, stats, email logs, sandbox list/show). Optional only for send-email and send-sandbox-email.
55+
- `MAILTRAP_ACCOUNT_ID`: Required for templates, stats, email logs, sandbox list/show, and sending domains. Optional only for send-email and send-sandbox-email.
5656
- `MAILTRAP_TEST_INBOX_ID`: Required for sandbox tools - test inbox ID for sandbox mode operations
5757

5858
### Testing Setup
@@ -96,4 +96,12 @@ Schema files define a JSON Schema–shaped object for MCP; optional Zod schemas
9696
- **get-sandbox-messages**: Get list of messages from the sandbox test inbox.
9797
- **show-sandbox-email-message**: Show sandbox email message details and content from the sandbox test inbox.
9898

99+
100+
#### Sending Domains
101+
102+
- **list-sending-domains**: List sending domains and their DNS verification status.
103+
- **get-sending-domain**: Get a sending domain by ID and its verification status. With `include_setup_instructions: true`, append DNS setup instructions to the response.
104+
- **create-sending-domain**: Create a new sending domain.
105+
- **delete-sending-domain**: Delete a sending domain.
106+
99107
Tools use input schemas (JSON Schema format) for MCP; handlers may validate input with Zod. Response format follows the MCP protocol.

README.md

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ Before using this MCP server, you need to:
1919

2020
- `MAILTRAP_API_TOKEN` - Required for all functionality
2121
- `DEFAULT_FROM_EMAIL` - Required for all email sending operations
22-
- `MAILTRAP_ACCOUNT_ID` - Required for almost all tools (templates, stats, email logs, sandbox list/show). Optional only for send-email and send-sandbox-email.
22+
- `MAILTRAP_ACCOUNT_ID` - Required for templates, stats, email logs, sandbox list/show, and sending domains. Optional only for send-email and send-sandbox-email.
2323
- `MAILTRAP_TEST_INBOX_ID` - Required for sandbox tools (send, list messages, show message)
2424

2525
## Quick Install
@@ -188,6 +188,14 @@ Once configured, you can ask agent to send emails and manage templates, for exam
188188
- "Update the template with ID 12345 to change the subject to 'Updated Welcome Message'"
189189
- "Delete the template with ID 67890"
190190

191+
**Sending Domains:**
192+
193+
- "List my sending domains"
194+
- "Get sending domain with ID 3938"
195+
- "Create a sending domain for example.com"
196+
- "Delete sending domain 3938"
197+
- "Get sending domain 3938 with DNS setup instructions"
198+
191199
## Available Tools
192200

193201
### send-email
@@ -340,6 +348,39 @@ Shows detailed information and content of a specific email message from your Mai
340348
> Use `get-sandbox-messages` first to get the list of messages and their IDs, then use this tool to view the full content of a specific message.
341349
342350

351+
### list-sending-domains
352+
353+
List sending domains and their DNS verification status.
354+
355+
**Parameters:**
356+
357+
- No parameters required
358+
359+
### get-sending-domain
360+
361+
Get a sending domain by ID and its verification status (including DNS records). Optionally include DNS setup instructions by setting `include_setup_instructions` to `true`.
362+
363+
**Parameters:**
364+
365+
- `sending_domain_id` (required): Sending domain ID
366+
- `include_setup_instructions` (optional): If `true`, append DNS setup instructions to the response. Default: `false`
367+
368+
### create-sending-domain
369+
370+
Create a new sending domain. After creation, add DNS records to verify the domain (use get-sending-domain with `include_setup_instructions: true` to see the records).
371+
372+
**Parameters:**
373+
374+
- `domain_name` (required): Domain name (e.g. example.com)
375+
376+
### delete-sending-domain
377+
378+
Delete a sending domain.
379+
380+
**Parameters:**
381+
382+
- `sending_domain_id` (required): Sending domain ID to delete
383+
343384
## Development
344385

345386
1. Clone the repository:

src/server.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,16 @@ import {
3737
getEmailLogMessage,
3838
getEmailLogMessageSchema,
3939
} from "./tools/emailLogs";
40+
import {
41+
listSendingDomains,
42+
listSendingDomainsSchema,
43+
getSendingDomain,
44+
getSendingDomainSchema,
45+
createSendingDomain,
46+
createSendingDomainSchema,
47+
deleteSendingDomain,
48+
deleteSendingDomainSchema,
49+
} from "./tools/sendingDomains";
4050

4151
// Define the tools registry
4252
const tools = [
@@ -139,6 +149,43 @@ const tools = [
139149
readOnlyHint: true,
140150
},
141151
},
152+
{
153+
name: "list-sending-domains",
154+
description: "List sending domains and their DNS verification status",
155+
inputSchema: listSendingDomainsSchema,
156+
handler: listSendingDomains,
157+
annotations: {
158+
readOnlyHint: true,
159+
},
160+
},
161+
{
162+
name: "get-sending-domain",
163+
description:
164+
"Get a sending domain by ID and its verification status. Optionally include DNS setup instructions via include_setup_instructions.",
165+
inputSchema: getSendingDomainSchema,
166+
handler: getSendingDomain,
167+
annotations: {
168+
readOnlyHint: true,
169+
},
170+
},
171+
{
172+
name: "create-sending-domain",
173+
description: "Create a new sending domain",
174+
inputSchema: createSendingDomainSchema,
175+
handler: createSendingDomain,
176+
annotations: {
177+
destructiveHint: true,
178+
},
179+
},
180+
{
181+
name: "delete-sending-domain",
182+
description: "Delete a sending domain",
183+
inputSchema: deleteSendingDomainSchema,
184+
handler: deleteSendingDomain,
185+
annotations: {
186+
destructiveHint: true,
187+
},
188+
},
142189
];
143190

144191
export function createServer(): Server {
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import createSendingDomain from "../createSendingDomain";
2+
import { client } from "../../../client";
3+
4+
jest.mock("../../../client", () => ({
5+
client: {
6+
sendingDomains: {
7+
create: jest.fn(),
8+
},
9+
},
10+
}));
11+
12+
const originalEnv = { ...process.env };
13+
14+
describe("createSendingDomain", () => {
15+
beforeEach(() => {
16+
jest.clearAllMocks();
17+
Object.assign(process.env, { MAILTRAP_ACCOUNT_ID: "12345" });
18+
(client.sendingDomains.create as jest.Mock).mockResolvedValue({
19+
id: 3938,
20+
domain_name: "example.com",
21+
dns_records: [
22+
{
23+
key: "verification",
24+
type: "CNAME",
25+
name: "verify123",
26+
value: "smtp.mailtrap.live",
27+
status: "pending",
28+
},
29+
],
30+
});
31+
});
32+
33+
afterEach(() => {
34+
Object.assign(process.env, originalEnv);
35+
});
36+
37+
it("should create sending domain successfully and show DNS setup instructions", async () => {
38+
const result = await createSendingDomain({ domain_name: "example.com" });
39+
40+
expect(client.sendingDomains.create).toHaveBeenCalledWith({
41+
domain_name: "example.com",
42+
});
43+
expect(result.content[0].text).toContain("example.com");
44+
expect(result.content[0].text).toContain("created successfully");
45+
expect(result.content[0].text).toContain("3938");
46+
expect(result.content[0].text).toContain("Add DNS records for example.com");
47+
expect(result.content[0].text).toContain("DNS records to add:");
48+
expect(result.content[0].text).toContain("verification");
49+
expect(result.isError).toBeUndefined();
50+
});
51+
52+
it("should require MAILTRAP_ACCOUNT_ID", async () => {
53+
delete process.env.MAILTRAP_ACCOUNT_ID;
54+
55+
const result = await createSendingDomain({ domain_name: "example.com" });
56+
57+
expect(client.sendingDomains.create).not.toHaveBeenCalled();
58+
expect(result.isError).toBe(true);
59+
});
60+
61+
it("should handle API failure", async () => {
62+
(client.sendingDomains.create as jest.Mock).mockRejectedValue(
63+
new Error("Domain already exists")
64+
);
65+
66+
const result = await createSendingDomain({ domain_name: "example.com" });
67+
68+
expect(result.content[0].text).toEqual(
69+
"Failed to create sending domain: Domain already exists"
70+
);
71+
expect(result.isError).toBe(true);
72+
});
73+
});
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import deleteSendingDomain from "../deleteSendingDomain";
2+
import { client } from "../../../client";
3+
4+
jest.mock("../../../client", () => ({
5+
client: {
6+
sendingDomains: {
7+
delete: jest.fn(),
8+
},
9+
},
10+
}));
11+
12+
const originalEnv = { ...process.env };
13+
14+
describe("deleteSendingDomain", () => {
15+
beforeEach(() => {
16+
jest.clearAllMocks();
17+
Object.assign(process.env, { MAILTRAP_ACCOUNT_ID: "12345" });
18+
(client.sendingDomains.delete as jest.Mock).mockResolvedValue(undefined);
19+
});
20+
21+
afterEach(() => {
22+
Object.assign(process.env, originalEnv);
23+
});
24+
25+
it("should delete sending domain successfully", async () => {
26+
const result = await deleteSendingDomain({ sending_domain_id: 3938 });
27+
28+
expect(client.sendingDomains.delete).toHaveBeenCalledWith(3938);
29+
expect(result.content[0].text).toContain("deleted successfully");
30+
expect(result.content[0].text).toContain("3938");
31+
expect(result.isError).toBeUndefined();
32+
});
33+
34+
it("should require MAILTRAP_ACCOUNT_ID", async () => {
35+
delete process.env.MAILTRAP_ACCOUNT_ID;
36+
37+
const result = await deleteSendingDomain({ sending_domain_id: 3938 });
38+
39+
expect(client.sendingDomains.delete).not.toHaveBeenCalled();
40+
expect(result.isError).toBe(true);
41+
});
42+
43+
it("should handle API failure", async () => {
44+
(client.sendingDomains.delete as jest.Mock).mockRejectedValue(
45+
new Error("Not found")
46+
);
47+
48+
const result = await deleteSendingDomain({ sending_domain_id: 999 });
49+
50+
expect(result.content[0].text).toContain("Failed to delete sending domain");
51+
expect(result.isError).toBe(true);
52+
});
53+
});
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import getSendingDomain from "../getSendingDomain";
2+
import { client } from "../../../client";
3+
4+
jest.mock("../../../client", () => ({
5+
client: {
6+
sendingDomains: {
7+
get: jest.fn(),
8+
},
9+
},
10+
}));
11+
12+
const originalEnv = { ...process.env };
13+
14+
describe("getSendingDomain", () => {
15+
const mockDomain = {
16+
id: 3938,
17+
domain_name: "example.com",
18+
demo: false,
19+
compliance_status: "compliant",
20+
dns_verified: true,
21+
dns_verified_at: "2024-12-26T09:40:44.161Z",
22+
dns_records: [
23+
{
24+
key: "spf",
25+
type: "TXT",
26+
name: "",
27+
value: "v=spf1 include:_spf.smtp.mailtrap.live ~all",
28+
status: "pass",
29+
domain: "example.com",
30+
},
31+
],
32+
};
33+
34+
beforeEach(() => {
35+
jest.clearAllMocks();
36+
Object.assign(process.env, { MAILTRAP_ACCOUNT_ID: "12345" });
37+
(client.sendingDomains.get as jest.Mock).mockResolvedValue(mockDomain);
38+
});
39+
40+
afterEach(() => {
41+
Object.assign(process.env, originalEnv);
42+
});
43+
44+
it("should get sending domain successfully", async () => {
45+
const result = await getSendingDomain({ sending_domain_id: 3938 });
46+
47+
expect(client.sendingDomains.get).toHaveBeenCalledWith(3938);
48+
expect(result.content[0].text).toContain("example.com");
49+
expect(result.content[0].text).toContain("ID: 3938");
50+
expect(result.content[0].text).toContain("DNS verified: true");
51+
expect(result.content[0].text).toContain("spf");
52+
expect(result.content[0].text).toContain("pass");
53+
expect(result.isError).toBeUndefined();
54+
});
55+
56+
it("should include setup instructions when include_setup_instructions is true", async () => {
57+
const result = await getSendingDomain({
58+
sending_domain_id: 3938,
59+
include_setup_instructions: true,
60+
});
61+
62+
expect(client.sendingDomains.get).toHaveBeenCalledWith(3938);
63+
expect(result.content[0].text).toContain("Domain: example.com");
64+
expect(result.content[0].text).toContain("Add DNS records for example.com");
65+
expect(result.content[0].text).toContain("What?");
66+
expect(result.content[0].text).toContain("Why?");
67+
expect(result.content[0].text).toContain("DNS records to add");
68+
expect(result.content[0].text).toContain("docs.mailtrap.io");
69+
expect(result.isError).toBeUndefined();
70+
});
71+
72+
it("should not include setup instructions when include_setup_instructions is false or omitted", async () => {
73+
const result = await getSendingDomain({
74+
sending_domain_id: 3938,
75+
include_setup_instructions: false,
76+
});
77+
78+
expect(result.content[0].text).not.toContain("Add DNS records for");
79+
expect(result.content[0].text).not.toContain("What?");
80+
});
81+
82+
it("should require MAILTRAP_ACCOUNT_ID", async () => {
83+
delete process.env.MAILTRAP_ACCOUNT_ID;
84+
85+
const result = await getSendingDomain({ sending_domain_id: 3938 });
86+
87+
expect(client.sendingDomains.get).not.toHaveBeenCalled();
88+
expect(result.isError).toBe(true);
89+
});
90+
91+
it("should handle API failure", async () => {
92+
(client.sendingDomains.get as jest.Mock).mockRejectedValue(
93+
new Error("Not found")
94+
);
95+
96+
const result = await getSendingDomain({ sending_domain_id: 999 });
97+
98+
expect(result.content[0].text).toContain("Failed to get sending domain");
99+
expect(result.isError).toBe(true);
100+
});
101+
});

0 commit comments

Comments
 (0)