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
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ Schema files define a JSON Schema–shaped object for MCP; optional Zod schemas
#### Sandbox Testing

- **send-sandbox-email**: Send email in sandbox mode to a test inbox.
- **batch-send-sandbox-email**: Send a batch of emails in sandbox mode to a test inbox in one call. Same `base` + `requests[]` shape as the transactional/bulk batch tools, plus `sandbox_id` (falls back to `MAILTRAP_SANDBOX_ID`).
- **get-sandbox-messages**: Get list of messages from the sandbox test inbox.
- **show-sandbox-email-message**: Show sandbox email message details and content from the sandbox test inbox.
- **list-sandbox-projects** / **create-sandbox-project** / **get-sandbox-project** / **update-sandbox-project** / **delete-sandbox-project**: Manage sandbox projects (group of inboxes).
Expand Down
17 changes: 13 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,13 @@ Before using this MCP server, you need to:
**Required Environment Variables:**

- `MAILTRAP_API_TOKEN` - Required for all functionality
- `MAILTRAP_ACCOUNT_ID` - Required for templates, stats, email logs, sandbox list/show, and sending domains. Optional only for send-email and send-sandbox-email.
- `MAILTRAP_ACCOUNT_ID` - Required for templates, stats, email logs, sandbox list/show, and sending domains. Optional only for the send tools (send-email, send-sandbox-email, and the batch-send-\* tools).

**Optional (can be passed as tool parameters instead):**

- `DEFAULT_FROM_EMAIL` - Default sender email when `from` is not provided to send-email or send-sandbox-email. Enables switching sender per call via the `from` parameter.
- `MAILTRAP_TEST_INBOX_ID` - Default test inbox ID for sandbox tools when `test_inbox_id` is not provided. Enables switching between inboxes per call via the `test_inbox_id` parameter.
- `DEFAULT_FROM_EMAIL` - Default sender email when `from` is not provided to send-email, send-sandbox-email, or the batch-send-\* tools (where it fills `base.from`). Enables switching sender per call via the `from` parameter.
- `MAILTRAP_SANDBOX_ID` - Default sandbox ID for sandbox tools when `sandbox_id` is not provided. Enables switching between sandboxes per call via the `sandbox_id` parameter.
- `MAILTRAP_TEST_INBOX_ID` - Default test inbox ID for sandbox tools when `test_inbox_id` is not provided. Enables switching between inboxes per call via the `test_inbox_id` parameter. Legacy alias for `MAILTRAP_SANDBOX_ID`, still honored as a fallback.
- `MAILTRAP_ORGANIZATION_ID` - Required for organization tools (`list-sub-accounts`, `create-sub-account`).
- `MAILTRAP_ORGANIZATION_API_TOKEN` - Organization-scoped API token. Required for organization tools (separate from `MAILTRAP_API_TOKEN`).

Expand Down Expand Up @@ -358,8 +358,17 @@ Sends an email to your Mailtrap test inbox for development and testing purposes.
- `template_uuid` (optional): Use a Mailtrap email template instead of inline content. When set, `subject` / `text` / `html` / `category` must be omitted.
- `template_variables` (optional): Object of variables substituted into the template referenced by `template_uuid`. Only allowed together with `template_uuid`.

### batch-send-sandbox-email

Sends a batch of emails to your Mailtrap test inbox in one API call, without delivering to real recipients. Same `base` + `requests[]` shape, validation, and inline-vs-template rules as `batch-send-transactional-email` — the difference is that this tool routes the call through the sandbox endpoint for a single test inbox.

**Parameters:**

- `sandbox_id` (optional): Mailtrap sandbox (test inbox) ID. Required unless `MAILTRAP_SANDBOX_ID` is set; pass per call to target a specific sandbox.
- `base` (optional), `requests` (required): See `batch-send-transactional-email` above.

> [!NOTE]
> For sandbox tools, provide `test_inbox_id` in the tool call or set the `MAILTRAP_TEST_INBOX_ID` environment variable. You can switch between inboxes per call by passing `test_inbox_id`.
> For sandbox tools, provide `test_inbox_id` in the tool call or set the `MAILTRAP_TEST_INBOX_ID` environment variable. You can switch between inboxes per call by passing `test_inbox_id`. Tools taking `sandbox_id` use `MAILTRAP_SANDBOX_ID` first.

### get-sandbox-messages

Expand Down
12 changes: 12 additions & 0 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ import {
import {
sendSandboxEmail,
sendSandboxEmailSchema,
batchSendSandboxEmail,
batchSendSandboxEmailSchema,
getMessages,
getMessagesSchema,
showEmailMessage,
Expand Down Expand Up @@ -331,6 +333,16 @@ const tools = [
destructiveHint: false,
},
},
{
name: "batch-send-sandbox-email",
description:
"Send a batch of emails in sandbox mode to a test inbox in one Mailtrap API call. Shared fields go on `base`; per-recipient overrides go in `requests[]`. Requires `sandbox_id` or the MAILTRAP_SANDBOX_ID env var.",
inputSchema: batchSendSandboxEmailSchema,
handler: batchSendSandboxEmail,
annotations: {
destructiveHint: false,
},
},
{
name: "get-sandbox-messages",
description: "Get list of messages from the sandbox test inbox",
Expand Down
144 changes: 144 additions & 0 deletions src/tools/sandbox/__tests__/batchSendSandboxEmail.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import batchSendSandboxEmail from "../batchSendSandboxEmail";
import { getSandboxClient } from "../../../client";

const mockClient = {
batchSend: jest.fn(),
};

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

describe("batchSendSandboxEmail", () => {
const originalEnv = process.env;

beforeEach(() => {
jest.clearAllMocks();
(getSandboxClient as jest.Mock).mockReturnValue(mockClient);
process.env = { ...originalEnv };
delete process.env.MAILTRAP_SANDBOX_ID;
delete process.env.MAILTRAP_TEST_INBOX_ID;
});

afterAll(() => {
process.env = originalEnv;
});

it("uses the sandbox client for the given inbox and forwards the SDK payload", async () => {
mockClient.batchSend.mockResolvedValue({
success: true,
responses: [{ success: true, message_ids: ["m-1"] }],
});

const result = await batchSendSandboxEmail({
sandbox_id: 4242,
base: {
from: { email: "sender@example.com", name: "Sender" },
subject: "Sandbox hello",
text: "Hello sandbox",
},
requests: [{ to: "alice@example.com" }],
});

expect(getSandboxClient).toHaveBeenCalledWith(4242);
expect(mockClient.batchSend).toHaveBeenCalledWith({
base: {
from: { email: "sender@example.com", name: "Sender" },
subject: "Sandbox hello",
text: "Hello sandbox",
},
requests: [{ to: [{ email: "alice@example.com" }] }],
});
expect(result.isError).toBeUndefined();
});

it("does not forward sandbox_id into the SDK payload", async () => {
mockClient.batchSend.mockResolvedValue({ success: true, responses: [] });

await batchSendSandboxEmail({
sandbox_id: 4242,
base: { from: "sender@example.com", subject: "Hi", text: "x" },
requests: [{ to: "alice@example.com" }],
});

const payload = mockClient.batchSend.mock.calls[0][0];
expect(payload.base).not.toHaveProperty("sandbox_id");
expect(payload).not.toHaveProperty("sandbox_id");
});

it("falls back to MAILTRAP_SANDBOX_ID when sandbox_id is omitted", async () => {
process.env.MAILTRAP_SANDBOX_ID = "777";
mockClient.batchSend.mockResolvedValue({ success: true, responses: [] });

const result = await batchSendSandboxEmail({
base: { from: "sender@example.com", subject: "Hi", text: "x" },
requests: [{ to: "alice@example.com" }],
});

expect(getSandboxClient).toHaveBeenCalledWith(777);
expect(result.isError).toBeUndefined();
});

it("falls back to the legacy MAILTRAP_TEST_INBOX_ID, but MAILTRAP_SANDBOX_ID wins", async () => {
process.env.MAILTRAP_TEST_INBOX_ID = "777";
mockClient.batchSend.mockResolvedValue({ success: true, responses: [] });

await batchSendSandboxEmail({
base: { from: "sender@example.com", subject: "Hi", text: "x" },
requests: [{ to: "alice@example.com" }],
});

expect(getSandboxClient).toHaveBeenCalledWith(777);

process.env.MAILTRAP_SANDBOX_ID = "888";

await batchSendSandboxEmail({
base: { from: "sender@example.com", subject: "Hi", text: "x" },
requests: [{ to: "alice@example.com" }],
});

expect(getSandboxClient).toHaveBeenLastCalledWith(888);
});

it("errors when no sandbox is configured", async () => {
const result = await batchSendSandboxEmail({
base: { from: "sender@example.com", subject: "Hi", text: "x" },
requests: [{ to: "alice@example.com" }],
});

expect(result.isError).toBe(true);
expect(result.content[0].text).toBe(
"Failed to batch send sandbox email: Provide sandbox_id or set MAILTRAP_SANDBOX_ID environment variable for sandbox mode"
);
expect(mockClient.batchSend).not.toHaveBeenCalled();
});

it("propagates payload validation errors", async () => {
const result = await batchSendSandboxEmail({
sandbox_id: 4242,
base: { from: "sender@example.com", subject: "Hi", text: "x" },
requests: [{}],
});

expect(result.isError).toBe(true);
expect(result.content[0].text).toContain(
"Failed to batch send sandbox email: requests[0]: provide at least one recipient"
);
expect(mockClient.batchSend).not.toHaveBeenCalled();
});

it("surfaces API errors with a sandbox-specific prefix", async () => {
mockClient.batchSend.mockRejectedValue(new Error("inbox is full"));

const result = await batchSendSandboxEmail({
sandbox_id: 4242,
base: { from: "sender@example.com", subject: "Hi", text: "x" },
requests: [{ to: "alice@example.com" }],
});

expect(result.isError).toBe(true);
expect(result.content[0].text).toBe(
"Failed to batch send sandbox email: inbox is full"
);
});
});
32 changes: 32 additions & 0 deletions src/tools/sandbox/batchSendSandboxEmail.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { getSandboxClient } from "../../client";
import { BatchSendSandboxEmailToolRequest } from "../../types/mailtrap";
import buildBatchPayload from "../sendEmail/buildBatchPayload";
import {
buildErrorResponse,
buildSuccessResponse,
ToolResponse,
} from "../utils/responses";
import resolveSandboxId from "./utils/resolveSandboxId";

async function batchSendSandboxEmail({
sandbox_id,
...body
}: BatchSendSandboxEmailToolRequest): Promise<ToolResponse> {
try {
const inboxId = resolveSandboxId(sandbox_id);

const payload = buildBatchPayload(body);

const mailtrap = getSandboxClient(inboxId);

const response = await mailtrap.batchSend(
payload as unknown as Parameters<typeof mailtrap.batchSend>[0]
);

return buildSuccessResponse(JSON.stringify(response, null, 2));
} catch (error) {
return buildErrorResponse("batch send sandbox email", error);
}
}

export default batchSendSandboxEmail;
4 changes: 4 additions & 0 deletions src/tools/sandbox/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import sendSandboxEmailSchema from "./schemas/sendSandboxEmail";
import sendSandboxEmail from "./sendSandboxEmail";
import batchSendSandboxEmailSchema from "./schemas/batchSendSandboxEmail";
import batchSendSandboxEmail from "./batchSendSandboxEmail";
import getMessagesSchema from "./schemas/getMessages";
import getMessages from "./getSandboxMessages";
import showEmailMessageSchema from "./schemas/showEmailMessage";
Expand Down Expand Up @@ -58,6 +60,8 @@ import getSandboxAttachment from "./getSandboxAttachment";
export {
sendSandboxEmailSchema,
sendSandboxEmail,
batchSendSandboxEmailSchema,
batchSendSandboxEmail,
getMessagesSchema,
getMessages,
showEmailMessageSchema,
Expand Down
15 changes: 15 additions & 0 deletions src/tools/sandbox/schemas/batchSendSandboxEmail.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import batchSendStreamEmailSchema from "../../sendEmail/schemas/batchSendStreamEmail";

const batchSendSandboxEmailSchema = {
...batchSendStreamEmailSchema,
properties: {
sandbox_id: {
type: "number",
description:
"Mailtrap sandbox (test inbox) ID. Optional if MAILTRAP_SANDBOX_ID env var is set. Use to target a specific sandbox.",
},
...batchSendStreamEmailSchema.properties,
},
};

export default batchSendSandboxEmailSchema;
5 changes: 5 additions & 0 deletions src/types/mailtrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,11 @@ export interface BatchSendEmailToolRequest {
requests: BatchSendEmailRequest[];
}

export interface BatchSendSandboxEmailToolRequest
extends BatchSendEmailToolRequest {
sandbox_id?: number;
Comment thread
izikaj marked this conversation as resolved.
}

export interface CreateTemplateRequest {
name: string;
subject: string;
Expand Down
Loading