Skip to content

Remove axios references and use fetch #18

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: codex/add-templates-support
Choose a base branch
from
Draft
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
81 changes: 53 additions & 28 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,48 +23,74 @@ const server = new McpServer({
version: CONFIG.MCP_SERVER_VERSION,
});

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

server.tool("get-email-templates", "Get all email templates", {}, async () => {
try {
const templates = await getTemplates();
return { content: [{ type: "text", text: JSON.stringify(templates, null, 2) }] };
return {
content: [{ type: "text", text: JSON.stringify(templates, null, 2) }],
};
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
return { content: [{ type: "text", text: message }], isError: true };
}
});

server.tool("create-email-template", "Create a new email template", createTemplateSchema, async (args) => {
try {
const template = await createTemplate(args as any);
return { content: [{ type: "text", text: JSON.stringify(template, null, 2) }] };
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
return { content: [{ type: "text", text: message }], isError: true };
server.tool(
"create-email-template",
"Create a new email template",
createTemplateSchema,
async (args) => {
try {
const template = await createTemplate(args as any);
return {
content: [{ type: "text", text: JSON.stringify(template, null, 2) }],
};
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
return { content: [{ type: "text", text: message }], isError: true };
}
}
});
);

server.tool("update-email-template", "Update an email template", updateTemplateSchema, async (args) => {
try {
const { id, ...rest } = args as any;
const template = await updateTemplate(id, rest as any);
return { content: [{ type: "text", text: JSON.stringify(template, null, 2) }] };
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
return { content: [{ type: "text", text: message }], isError: true };
server.tool(
"update-email-template",
"Update an email template",
updateTemplateSchema,
async (args) => {
try {
const { id, ...rest } = args as any;
const template = await updateTemplate(id, rest as any);
return {
content: [{ type: "text", text: JSON.stringify(template, null, 2) }],
};
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
return { content: [{ type: "text", text: message }], isError: true };
}
}
});
);

server.tool("delete-email-template", "Delete an email template", deleteTemplateSchema, async ({ id }: any) => {
try {
await deleteTemplate(id);
return { content: [{ type: "text", text: "Deleted" }] };
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
return { content: [{ type: "text", text: message }], isError: true };
server.tool(
"delete-email-template",
"Delete an email template",
deleteTemplateSchema,
async ({ id }: any) => {
try {
await deleteTemplate(id);
return { content: [{ type: "text", text: "Deleted" }] };
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
return { content: [{ type: "text", text: message }], isError: true };
}
}
});
);

async function main() {
const transport = new StdioServerTransport();
Expand All @@ -75,4 +101,3 @@ main().catch((error) => {
console.error("Fatal error:", error);
process.exit(1);
});

65 changes: 47 additions & 18 deletions src/tools/templates/__tests__/templates.test.ts
Original file line number Diff line number Diff line change
@@ -1,42 +1,71 @@
import axios from "axios";

jest.mock("axios");
const mockedAxios = axios as jest.Mocked<typeof axios>;
const apiInstance = { get: jest.fn(), post: jest.fn(), patch: jest.fn(), delete: jest.fn() } as const;
(mockedAxios.create as jest.Mock).mockReturnValue(apiInstance);

import { getTemplates, createTemplate, updateTemplate, deleteTemplate } from "../templates";
import {
getTemplates,
createTemplate,
updateTemplate,
deleteTemplate,
} from "../templates";

describe("templates tools", () => {
const fetchMock = jest.fn();

beforeEach(() => {
jest.clearAllMocks();
fetchMock.mockReset();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(global as any).fetch = fetchMock;
});

it("should get templates", async () => {
apiInstance.get.mockResolvedValue({ data: [{ id: 1 }] });
fetchMock.mockResolvedValue({
ok: true,
status: 200,
json: async () => [{ id: 1 }],
});
const res = await getTemplates();
expect(apiInstance.get).toHaveBeenCalledWith("/email_templates");
expect(fetchMock).toHaveBeenCalledWith(
"https://mailtrap.io/api/accounts/123/email_templates",
expect.objectContaining({ method: "GET" })
);
expect(res).toEqual([{ id: 1 }]);
});

it("should create template", async () => {
apiInstance.post.mockResolvedValue({ data: { id: 2 } });
fetchMock.mockResolvedValue({
ok: true,
status: 200,
json: async () => ({ id: 2 }),
});
const res = await createTemplate({ name: "Template", subject: "Subject" });
expect(apiInstance.post).toHaveBeenCalledWith("/email_templates", { name: "Template", subject: "Subject" });
expect(fetchMock).toHaveBeenCalledWith(
"https://mailtrap.io/api/accounts/123/email_templates",
expect.objectContaining({ method: "POST" })
);
expect(res).toEqual({ id: 2 });
});

it("should update template", async () => {
apiInstance.patch.mockResolvedValue({ data: { id: 3 } });
fetchMock.mockResolvedValue({
ok: true,
status: 200,
json: async () => ({ id: 3 }),
});
const res = await updateTemplate(3, { name: "Updated" });
expect(apiInstance.patch).toHaveBeenCalledWith("/email_templates/3", { name: "Updated" });
expect(fetchMock).toHaveBeenCalledWith(
"https://mailtrap.io/api/accounts/123/email_templates/3",
expect.objectContaining({ method: "PATCH" })
);
expect(res).toEqual({ id: 3 });
});

it("should delete template", async () => {
apiInstance.delete.mockResolvedValue({});
fetchMock.mockResolvedValue({
ok: true,
status: 204,
text: async () => "",
});
await deleteTemplate(4);
expect(apiInstance.delete).toHaveBeenCalledWith("/email_templates/4");
expect(fetchMock).toHaveBeenCalledWith(
"https://mailtrap.io/api/accounts/123/email_templates/4",
expect.objectContaining({ method: "DELETE" })
);
});
});

23 changes: 20 additions & 3 deletions src/tools/templates/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,21 @@
import { createTemplateSchema, updateTemplateSchema, deleteTemplateSchema } from "./schemas";
import { getTemplates, createTemplate, updateTemplate, deleteTemplate } from "./templates";
import {
createTemplateSchema,
updateTemplateSchema,
deleteTemplateSchema,
} from "./schemas";
import {
getTemplates,
createTemplate,
updateTemplate,
deleteTemplate,
} from "./templates";

export { createTemplateSchema, updateTemplateSchema, deleteTemplateSchema, getTemplates, createTemplate, updateTemplate, deleteTemplate };
export {
createTemplateSchema,
updateTemplateSchema,
deleteTemplateSchema,
getTemplates,
createTemplate,
updateTemplate,
deleteTemplate,
};
1 change: 0 additions & 1 deletion src/tools/templates/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,3 @@ export const updateTemplateSchema = {
export const deleteTemplateSchema = {
id: z.number().describe("Template ID"),
};

77 changes: 53 additions & 24 deletions src/tools/templates/templates.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import axios, { AxiosError, AxiosInstance } from "axios";

const { MAILTRAP_API_TOKEN, MAILTRAP_ACCOUNT_ID } = process.env;

if (!MAILTRAP_API_TOKEN) {
Expand All @@ -11,13 +9,32 @@ if (!MAILTRAP_ACCOUNT_ID) {
process.exit(1);
}

const api: AxiosInstance = axios.create({
baseURL: `https://mailtrap.io/api/accounts/${MAILTRAP_ACCOUNT_ID}`,
headers: {
"Api-Token": MAILTRAP_API_TOKEN,
"Content-Type": "application/json",
},
});
const API_TOKEN = MAILTRAP_API_TOKEN as string;
const ACCOUNT_ID = MAILTRAP_ACCOUNT_ID as string;

const BASE_URL = `https://mailtrap.io/api/accounts/${ACCOUNT_ID}`;

async function request<T>(path: string, options: RequestInit): Promise<T> {
const response = await fetch(`${BASE_URL}${path}`, {
...options,
headers: {
"Api-Token": API_TOKEN,
"Content-Type": "application/json",
...(options.headers ?? {}),
},
});

if (!response.ok) {
const text = await response.text();
throw new Error(text || response.statusText);
}

if (response.status === 204) {
return undefined as T;
}

return (await response.json()) as T;
}

export interface EmailTemplate {
id: number;
Expand Down Expand Up @@ -48,50 +65,62 @@ export interface UpdateTemplateRequest {

export async function getTemplates(): Promise<EmailTemplate[]> {
try {
const { data } = await api.get<EmailTemplate[]>("/email_templates");
return data;
return await request<EmailTemplate[]>("/email_templates", {
method: "GET",
});
} catch (err) {
const message = (err as AxiosError).message;
const message = err instanceof Error ? err.message : String(err);
throw new Error(`Failed to list templates: ${message}`);
}
}

export async function createTemplate(req: CreateTemplateRequest): Promise<EmailTemplate> {
export async function createTemplate(
req: CreateTemplateRequest
): Promise<EmailTemplate> {
try {
const body: Record<string, unknown> = { name: req.name, subject: req.subject };
const body: Record<string, unknown> = {
name: req.name,
subject: req.subject,
};
if (req.category) body.category = req.category;
if (req.text) body.body_text = req.text;
if (req.html) body.body_html = req.html;
const { data } = await api.post<EmailTemplate>("/email_templates", body);
return data;
return await request<EmailTemplate>("/email_templates", {
method: "POST",
body: JSON.stringify(body),
});
} catch (err) {
const message = (err as AxiosError).message;
const message = err instanceof Error ? err.message : String(err);
throw new Error(`Failed to create template: ${message}`);
}
}

export async function updateTemplate(id: number, req: UpdateTemplateRequest): Promise<EmailTemplate> {
export async function updateTemplate(
id: number,
req: UpdateTemplateRequest
): Promise<EmailTemplate> {
try {
const body: Record<string, unknown> = {};
if (req.name) body.name = req.name;
if (req.subject) body.subject = req.subject;
if (req.category) body.category = req.category;
if (req.text) body.body_text = req.text;
if (req.html) body.body_html = req.html;
const { data } = await api.patch<EmailTemplate>(`/email_templates/${id}`, body);
return data;
return await request<EmailTemplate>(`/email_templates/${id}`, {
method: "PATCH",
body: JSON.stringify(body),
});
} catch (err) {
const message = (err as AxiosError).message;
const message = err instanceof Error ? err.message : String(err);
throw new Error(`Failed to update template: ${message}`);
}
}

export async function deleteTemplate(id: number): Promise<void> {
try {
await api.delete(`/email_templates/${id}`);
await request(`/email_templates/${id}`, { method: "DELETE" });
} catch (err) {
const message = (err as AxiosError).message;
const message = err instanceof Error ? err.message : String(err);
throw new Error(`Failed to delete template: ${message}`);
}
}