Skip to content

Commit 2be4820

Browse files
committed
Add get-sending-stats tool
1 parent 2589e11 commit 2be4820

8 files changed

Lines changed: 467 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1+
## [Unreleased]
2+
3+
* 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. Requires `MAILTRAP_ACCOUNT_ID`.
4+
15
## [0.1.0] - 2025-12-09
6+
27
* Adjust some info by @yanchuk in https://github.com/mailtrap/mailtrap-mcp/pull/41
38
* chore(deps-dev): bump js-yaml from 3.14.1 to 3.14.2 by @dependabot[bot] in https://github.com/mailtrap/mailtrap-mcp/pull/43
49
* chore(deps): bump body-parser from 2.2.0 to 2.2.1 by @dependabot[bot] in https://github.com/mailtrap/mailtrap-mcp/pull/45
@@ -9,10 +14,12 @@
914
* Update mailtrap version, refresh package-lock by @narekhovhannisyan in https://github.com/mailtrap/mailtrap-mcp/pull/49
1015

1116
## [0.0.5] - 2025-11-10
17+
1218
* Improve mcpb by @yanchuk in https://github.com/mailtrap/mailtrap-mcp/pull/39
1319
* Add tool annotation by @yanchuk in https://github.com/mailtrap/mailtrap-mcp/pull/40
1420

1521
## [0.0.4] - 2025-24-10
22+
1623
* Bump axios from 1.8.4 to 1.12.1 by @dependabot[bot] in https://github.com/mailtrap/mailtrap-mcp/pull/35
1724
* Bump @modelcontextprotocol/inspector from 0.14.1 to 0.16.6 by @dependabot[bot] in https://github.com/mailtrap/mailtrap-mcp/pull/33
1825
* mcpb (former dxt) implementation by @narekhovhannisyan in https://github.com/mailtrap/mailtrap-mcp/pull/34

CLAUDE.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ src/tools/{toolName}/
4646
### Environment Variables Required
4747
- `MAILTRAP_API_TOKEN`: Required API token from Mailtrap
4848
- `DEFAULT_FROM_EMAIL`: Default sender email address
49-
- `MAILTRAP_ACCOUNT_ID`: Optional account ID for multi-account setups
49+
- `MAILTRAP_ACCOUNT_ID`: Required for template management and sending stats (optional for send-email only)
5050
- `MAILTRAP_TEST_INBOX_ID`: Required for sandbox tools - test inbox ID for sandbox mode operations
5151

5252
### Testing Setup
@@ -76,4 +76,7 @@ src/tools/{toolName}/
7676
7. **get-sandbox-messages**: Get list of messages from the sandbox test inbox
7777
8. **show-sandbox-email-message**: Show sandbox email message details and content from the sandbox test inbox
7878

79+
#### Statistics
80+
9. **get-sending-stats**: Get email sending statistics (delivery, bounce, open, click, spam rates) for a date range; optionally break down by domain, category, email service provider, or date. Requires `MAILTRAP_ACCOUNT_ID`.
81+
7982
Each tool uses Zod schemas for input validation and follows the MCP protocol for response formatting.

README.md

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
# MCP Mailtrap Server
66

7-
An MCP server that provides tools for sending transactional emails and managing email templates via Mailtrap
7+
An MCP server that provides tools for sending transactional emails, managing email templates, checking sending statistics, and testing in sandbox via Mailtrap
88

99
## Prerequisites
1010

@@ -18,7 +18,7 @@ Before using this MCP server, you need to:
1818
**Required Environment Variables:**
1919
- `MAILTRAP_API_TOKEN` - Required for all functionality
2020
- `DEFAULT_FROM_EMAIL` - Required for all email sending operations
21-
- `MAILTRAP_ACCOUNT_ID` - Required for template management operations
21+
- `MAILTRAP_ACCOUNT_ID` - Required for template management and sending statistics
2222
- `MAILTRAP_TEST_INBOX_ID` - **Only** required for sandbox email functionality
2323

2424
## Quick Install
@@ -173,6 +173,12 @@ Once configured, you can ask agent to send emails and manage templates, for exam
173173
- "Update the template with ID 12345 to change the subject to 'Updated Welcome Message'"
174174
- "Delete the template with ID 67890"
175175

176+
**Statistics:**
177+
178+
- "Get sending stats for January 2025"
179+
- "Show delivery rates broken down by domain for last month"
180+
- "What are my email stats by category from 2025-01-01 to 2025-01-31?"
181+
176182
## Available Tools
177183

178184
### send-email
@@ -276,6 +282,23 @@ Deletes an existing email template.
276282

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

285+
### get-sending-stats
286+
287+
Get email sending statistics (delivery, bounce, open, click, spam rates) for a date range. Optionally break down by domain, category, email service provider, or date. Check delivery rates without leaving the editor.
288+
289+
**Parameters:**
290+
291+
- `start_date` (required): Start date for the stats range (YYYY-MM-DD)
292+
- `end_date` (required): End date for the stats range (YYYY-MM-DD)
293+
- `breakdown` (optional): How to break down the stats: `aggregated` (default), `by_domain`, `by_category`, `by_email_service_provider`, or `by_date`
294+
- `sending_domain_ids` (optional): Limit results to these sending domain IDs (array of integers)
295+
- `sending_streams` (optional): Limit to `transactional` and/or `bulk` (array of strings)
296+
- `categories` (optional): Limit to these email categories (array of strings)
297+
- `email_service_providers` (optional): Limit to these providers, e.g. Google, Yahoo, Outlook (array of strings)
298+
299+
> [!NOTE]
300+
> `MAILTRAP_ACCOUNT_ID` must be set for this tool to work.
301+
279302
## Development
280303

281304
1. Clone the repository:

src/server.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import {
3030
showEmailMessage,
3131
showEmailMessageSchema,
3232
} from "./tools/sandbox";
33+
import { getSendingStats, getSendingStatsSchema } from "./tools/stats";
3334

3435
// Define the tools registry
3536
const tools = [
@@ -102,6 +103,16 @@ const tools = [
102103
inputSchema: showEmailMessageSchema,
103104
handler: showEmailMessage,
104105
},
106+
{
107+
name: "get-sending-stats",
108+
description:
109+
"Get email sending statistics (delivery, bounce, open, click, spam rates) for a date range. Optionally break down by domain, category, email service provider, or date.",
110+
inputSchema: getSendingStatsSchema,
111+
handler: getSendingStats,
112+
annotations: {
113+
readOnlyHint: true,
114+
},
115+
},
105116
];
106117

107118
export function createServer(): Server {
Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
import getSendingStats from "../getSendingStats";
2+
import { client } from "../../../client";
3+
4+
jest.mock("../../../client", () => ({
5+
client: {
6+
stats: {
7+
get: jest.fn(),
8+
byDomain: jest.fn(),
9+
byCategory: jest.fn(),
10+
byEmailServiceProvider: jest.fn(),
11+
byDate: jest.fn(),
12+
},
13+
},
14+
}));
15+
16+
const mockStats = {
17+
delivery_count: 100,
18+
delivery_rate: 0.95,
19+
bounce_count: 5,
20+
bounce_rate: 0.05,
21+
open_count: 80,
22+
open_rate: 0.8,
23+
click_count: 50,
24+
click_rate: 0.5,
25+
spam_count: 2,
26+
spam_rate: 0.02,
27+
};
28+
29+
const originalEnv = { ...process.env };
30+
31+
describe("getSendingStats", () => {
32+
beforeEach(() => {
33+
jest.clearAllMocks();
34+
Object.assign(process.env, {
35+
MAILTRAP_ACCOUNT_ID: "12345",
36+
});
37+
(client as any).stats.get.mockResolvedValue(mockStats);
38+
(client as any).stats.byDomain.mockResolvedValue([
39+
{ name: "sending_domain_id", value: 3938, stats: mockStats },
40+
]);
41+
(client as any).stats.byDate.mockResolvedValue([
42+
{ name: "date", value: "2025-01-01", stats: mockStats },
43+
]);
44+
});
45+
46+
afterEach(() => {
47+
Object.assign(process.env, originalEnv);
48+
});
49+
50+
it("should return aggregated stats successfully", async () => {
51+
const result = await getSendingStats({
52+
start_date: "2025-01-01",
53+
end_date: "2025-01-31",
54+
});
55+
56+
expect((client as any).stats.get).toHaveBeenCalledWith({
57+
start_date: "2025-01-01",
58+
end_date: "2025-01-31",
59+
});
60+
expect(result.content).toHaveLength(1);
61+
expect(result.content[0].type).toBe("text");
62+
expect(result.content[0].text).toContain("Sending stats (aggregated)");
63+
expect(result.content[0].text).toContain("2025-01-01 to 2025-01-31");
64+
expect(result.content[0].text).toContain("Delivery: 100 (95.00%)");
65+
expect(result.content[0].text).toContain("Bounces: 5 (5.00%)");
66+
expect(result.content[0].text).toContain("Opens: 80 (80.00%)");
67+
expect(result.content[0].text).toContain("Clicks: 50 (50.00%)");
68+
expect(result.content[0].text).toContain("Spam: 2 (2.00%)");
69+
expect(result.isError).toBeUndefined();
70+
});
71+
72+
it("should return stats by domain when breakdown is by_domain", async () => {
73+
const result = await getSendingStats({
74+
start_date: "2025-01-01",
75+
end_date: "2025-01-31",
76+
breakdown: "by_domain",
77+
});
78+
79+
expect((client as any).stats.byDomain).toHaveBeenCalledWith({
80+
start_date: "2025-01-01",
81+
end_date: "2025-01-31",
82+
});
83+
expect(result.content[0].text).toContain("by domain");
84+
expect(result.content[0].text).toContain("3938:");
85+
expect(result.content[0].text).toContain("Delivery: 100 (95.00%)");
86+
expect(result.isError).toBeUndefined();
87+
});
88+
89+
it("should return stats by date when breakdown is by_date", async () => {
90+
const result = await getSendingStats({
91+
start_date: "2025-01-01",
92+
end_date: "2025-01-31",
93+
breakdown: "by_date",
94+
});
95+
96+
expect((client as any).stats.byDate).toHaveBeenCalledWith({
97+
start_date: "2025-01-01",
98+
end_date: "2025-01-31",
99+
});
100+
expect(result.content[0].text).toContain("by date");
101+
expect(result.content[0].text).toContain("2025-01-01:");
102+
expect(result.isError).toBeUndefined();
103+
});
104+
105+
it("should pass optional filters to the API", async () => {
106+
await getSendingStats({
107+
start_date: "2025-01-01",
108+
end_date: "2025-01-31",
109+
sending_domain_ids: [1, 2],
110+
sending_streams: ["transactional"],
111+
categories: ["Welcome"],
112+
email_service_providers: ["Google"],
113+
});
114+
115+
expect((client as any).stats.get).toHaveBeenCalledWith({
116+
start_date: "2025-01-01",
117+
end_date: "2025-01-31",
118+
sending_domain_ids: [1, 2],
119+
sending_streams: ["transactional"],
120+
categories: ["Welcome"],
121+
email_service_providers: ["Google"],
122+
});
123+
});
124+
125+
it("should return error when MAILTRAP_ACCOUNT_ID is missing", async () => {
126+
delete process.env.MAILTRAP_ACCOUNT_ID;
127+
128+
const result = await getSendingStats({
129+
start_date: "2025-01-01",
130+
end_date: "2025-01-31",
131+
});
132+
133+
expect(result.isError).toBe(true);
134+
expect(result.content[0].text).toContain(
135+
"MAILTRAP_ACCOUNT_ID environment variable is required for sending stats"
136+
);
137+
expect((client as any).stats.get).not.toHaveBeenCalled();
138+
});
139+
140+
it("should return error when MAILTRAP_ACCOUNT_ID is invalid", async () => {
141+
process.env.MAILTRAP_ACCOUNT_ID = "not-a-number";
142+
143+
const result = await getSendingStats({
144+
start_date: "2025-01-01",
145+
end_date: "2025-01-31",
146+
});
147+
148+
expect(result.isError).toBe(true);
149+
expect(result.content[0].text).toContain(
150+
"MAILTRAP_ACCOUNT_ID environment variable is required for sending stats"
151+
);
152+
});
153+
154+
it("should return error when client is null", async () => {
155+
jest.doMock("../../../client", () => ({ client: null }));
156+
jest.resetModules();
157+
const { default: getSendingStatsWithNullClient } = await import(
158+
"../getSendingStats"
159+
);
160+
const result = await getSendingStatsWithNullClient({
161+
start_date: "2025-01-01",
162+
end_date: "2025-01-31",
163+
});
164+
expect(result.isError).toBe(true);
165+
expect(result.content[0].text).toContain("MAILTRAP_API_TOKEN");
166+
jest.resetModules();
167+
});
168+
169+
it("should handle API errors", async () => {
170+
(client as any).stats.get.mockRejectedValue(
171+
new Error("Rate limit exceeded")
172+
);
173+
174+
const result = await getSendingStats({
175+
start_date: "2025-01-01",
176+
end_date: "2025-01-31",
177+
});
178+
179+
expect(result.isError).toBe(true);
180+
expect(result.content[0].text).toContain("Failed to get sending stats");
181+
expect(result.content[0].text).toContain("Rate limit exceeded");
182+
});
183+
184+
it("should call byCategory when breakdown is by_category", async () => {
185+
(client as any).stats.byCategory.mockResolvedValue([
186+
{ name: "category", value: "Welcome", stats: mockStats },
187+
]);
188+
189+
const result = await getSendingStats({
190+
start_date: "2025-01-01",
191+
end_date: "2025-01-31",
192+
breakdown: "by_category",
193+
});
194+
195+
expect((client as any).stats.byCategory).toHaveBeenCalledWith({
196+
start_date: "2025-01-01",
197+
end_date: "2025-01-31",
198+
});
199+
expect(result.content[0].text).toContain("Welcome:");
200+
});
201+
202+
it("should call byEmailServiceProvider when breakdown is by_email_service_provider", async () => {
203+
(client as any).stats.byEmailServiceProvider.mockResolvedValue([
204+
{ name: "email_service_provider", value: "Google", stats: mockStats },
205+
]);
206+
207+
const result = await getSendingStats({
208+
start_date: "2025-01-01",
209+
end_date: "2025-01-31",
210+
breakdown: "by_email_service_provider",
211+
});
212+
213+
expect((client as any).stats.byEmailServiceProvider).toHaveBeenCalledWith({
214+
start_date: "2025-01-01",
215+
end_date: "2025-01-31",
216+
});
217+
expect(result.content[0].text).toContain("Google:");
218+
});
219+
});

0 commit comments

Comments
 (0)