From e2fd8c3345065bbb48517265fad1c608e499baf3 Mon Sep 17 00:00:00 2001 From: Narek Hovhannisyan Date: Tue, 2 Sep 2025 15:08:29 +0400 Subject: [PATCH 01/12] config: add MCP_USER_AGENT to configuration for Mailtrap MCP --- src/config/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/config/index.ts b/src/config/index.ts index f1b5ad2..0859ce3 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -18,6 +18,7 @@ export default { GENERAL_ENDPOINT: "https://mailtrap.io", USER_AGENT: "mailtrap-nodejs (https://github.com/railsware/mailtrap-nodejs)", + MCP_USER_AGENT: "mailtrap-mcp (https://github.com/railsware/mailtrap-mcp)", MAX_REDIRECTS: 0, TIMEOUT: 10000, }, From 3bd652ada1f5ac642e04cd72477937b3bf2daf08 Mon Sep 17 00:00:00 2001 From: Narek Hovhannisyan Date: Tue, 2 Sep 2025 15:08:43 +0400 Subject: [PATCH 02/12] lib: replace static USER_AGENT with dynamic user agent function in MailtrapClient --- src/lib/MailtrapClient.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lib/MailtrapClient.ts b/src/lib/MailtrapClient.ts index a41060e..9fa46d5 100644 --- a/src/lib/MailtrapClient.ts +++ b/src/lib/MailtrapClient.ts @@ -6,12 +6,14 @@ import axios, { AxiosInstance } from "axios"; import encodeMailBuffers from "./mail-buffer-encoder"; import handleSendingError from "./axios-logger"; import MailtrapError from "./MailtrapError"; +import getDynamicUserAgent from "./get-agent"; import GeneralAPI from "./api/General"; import TestingAPI from "./api/Testing"; import ContactsBaseAPI from "./api/Contacts"; import ContactListsBaseAPI from "./api/ContactLists"; import TemplatesBaseAPI from "./api/Templates"; +import SuppressionsBaseAPI from "./api/Suppressions"; import CONFIG from "../config"; @@ -22,13 +24,11 @@ import { BatchSendResponse, BatchSendRequest, } from "../types/mailtrap"; -import SuppressionsBaseAPI from "./api/Suppressions"; const { CLIENT_SETTINGS, ERRORS } = CONFIG; const { SENDING_ENDPOINT, MAX_REDIRECTS, - USER_AGENT, TIMEOUT, TESTING_ENDPOINT, BULK_ENDPOINT, @@ -66,7 +66,7 @@ export default class MailtrapClient { headers: { Authorization: `Bearer ${token}`, Connection: "keep-alive", - "User-Agent": USER_AGENT, + "User-Agent": getDynamicUserAgent(), }, maxRedirects: MAX_REDIRECTS, timeout: TIMEOUT, From 9da5776fad1f706e61b9f1be3b928ddf9b7eaedf Mon Sep 17 00:00:00 2001 From: Narek Hovhannisyan Date: Tue, 2 Sep 2025 15:09:06 +0400 Subject: [PATCH 03/12] lib: implement dynamic User-Agent detection for Mailtrap MCP context --- src/lib/get-agent.ts | 63 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 src/lib/get-agent.ts diff --git a/src/lib/get-agent.ts b/src/lib/get-agent.ts new file mode 100644 index 0000000..6a6943c --- /dev/null +++ b/src/lib/get-agent.ts @@ -0,0 +1,63 @@ +import CONFIG from "../config"; + +const { USER_AGENT, MCP_USER_AGENT } = CONFIG.CLIENT_SETTINGS; + +/** + * Checks if the main module filename indicates MCP context. + * @returns true if main module contains "mailtrap-mcp" and is not in node_modules + */ +function isMainModuleMCP(): boolean { + const mainFile = require?.main?.filename; + + return !!( + mainFile && + mainFile.includes("mailtrap-mcp") && + !mainFile.includes("node_modules") + ); +} + +/** + * Checks if the current working directory indicates MCP context. + * @returns true if cwd contains "mailtrap-mcp" and is not in node_modules + */ +function isWorkingDirectoryMCP(): boolean { + try { + const cwd = process.cwd(); + return cwd.includes("mailtrap-mcp") && !cwd.includes("node_modules"); + } catch { + return false; + } +} + +/** + * Checks if the call stack indicates MCP context. + * @returns true if stack contains "mailtrap-mcp" and is not from node_modules/mailtrap + */ +function isCallStackMCP(): boolean { + const { stack } = new Error(); + + return !!( + stack && + stack.includes("mailtrap-mcp") && + !stack.includes("node_modules/mailtrap") + ); +} + +/** + * Determines if the code is running in a Mailtrap MCP context. + * Uses multiple detection methods to ensure accurate context identification. + * @returns true if running in MCP context, false otherwise + */ +function isMailtrapMCPContext(): boolean { + return isMainModuleMCP() || isWorkingDirectoryMCP() || isCallStackMCP(); +} + +/** + * Gets the appropriate User-Agent string based on the current context. + * @returns The User-Agent string for the current context + */ +function getDynamicUserAgent(): string { + return isMailtrapMCPContext() ? MCP_USER_AGENT : USER_AGENT; +} + +export default getDynamicUserAgent; From 5cb7895de146d6b164f48c2a0003340a641ed7ec Mon Sep 17 00:00:00 2001 From: Narek Hovhannisyan Date: Fri, 26 Sep 2025 16:35:10 +0400 Subject: [PATCH 04/12] refactor: clean up spacing in MailtrapClient send method for improved readability --- src/lib/MailtrapClient.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/MailtrapClient.ts b/src/lib/MailtrapClient.ts index 9fa46d5..7adf1f2 100644 --- a/src/lib/MailtrapClient.ts +++ b/src/lib/MailtrapClient.ts @@ -182,12 +182,12 @@ export default class MailtrapClient { */ public async send(mail: Mail): Promise { const host = this.determineHost(); - this.validateTestInboxIdPresence(); const url = `${host}/api/send${ this.sandbox && this.testInboxId ? `/${this.testInboxId}` : "" }`; + const preparedMail = encodeMailBuffers(mail); return this.axios.post(url, preparedMail); From 665cee5882e8d403b0fcf18a89688fa458d53f4f Mon Sep 17 00:00:00 2001 From: Narek Hovhannisyan Date: Fri, 26 Sep 2025 16:35:15 +0400 Subject: [PATCH 05/12] feat: add MCP runtime context detection and update User-Agent logic --- src/lib/get-agent.ts | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/src/lib/get-agent.ts b/src/lib/get-agent.ts index 6a6943c..98b043f 100644 --- a/src/lib/get-agent.ts +++ b/src/lib/get-agent.ts @@ -16,6 +16,21 @@ function isMainModuleMCP(): boolean { ); } +/** + * Checks if running in MCP runtime context (Claude Desktop). + * @returns true if main module is from MCP runtime + */ +function isMCPRuntimeContext(): boolean { + const mainFile = require?.main?.filename; + + return !!( + mainFile && + (mainFile.includes("mcp-runtime") || + mainFile.includes("nodeHost.js") || + mainFile.includes("Claude.app")) + ); +} + /** * Checks if the current working directory indicates MCP context. * @returns true if cwd contains "mailtrap-mcp" and is not in node_modules @@ -49,7 +64,12 @@ function isCallStackMCP(): boolean { * @returns true if running in MCP context, false otherwise */ function isMailtrapMCPContext(): boolean { - return isMainModuleMCP() || isWorkingDirectoryMCP() || isCallStackMCP(); + return ( + isMainModuleMCP() || + isWorkingDirectoryMCP() || + isCallStackMCP() || + isMCPRuntimeContext() + ); } /** @@ -57,7 +77,10 @@ function isMailtrapMCPContext(): boolean { * @returns The User-Agent string for the current context */ function getDynamicUserAgent(): string { - return isMailtrapMCPContext() ? MCP_USER_AGENT : USER_AGENT; + const isMCP = isMailtrapMCPContext(); + const selectedUA = isMCP ? MCP_USER_AGENT : USER_AGENT; + + return selectedUA; } export default getDynamicUserAgent; From 2d6992a2b768b08aca3718e3ea6bf57446165666 Mon Sep 17 00:00:00 2001 From: Narek Hovhannisyan Date: Fri, 26 Sep 2025 16:38:27 +0400 Subject: [PATCH 06/12] refactor: simplify Mailtrap MCP context detection in User-Agent logic --- src/lib/get-agent.ts | 23 ++++++----------------- 1 file changed, 6 insertions(+), 17 deletions(-) diff --git a/src/lib/get-agent.ts b/src/lib/get-agent.ts index 98b043f..7242b74 100644 --- a/src/lib/get-agent.ts +++ b/src/lib/get-agent.ts @@ -58,29 +58,18 @@ function isCallStackMCP(): boolean { ); } -/** - * Determines if the code is running in a Mailtrap MCP context. - * Uses multiple detection methods to ensure accurate context identification. - * @returns true if running in MCP context, false otherwise - */ -function isMailtrapMCPContext(): boolean { - return ( - isMainModuleMCP() || - isWorkingDirectoryMCP() || - isCallStackMCP() || - isMCPRuntimeContext() - ); -} - /** * Gets the appropriate User-Agent string based on the current context. * @returns The User-Agent string for the current context */ function getDynamicUserAgent(): string { - const isMCP = isMailtrapMCPContext(); - const selectedUA = isMCP ? MCP_USER_AGENT : USER_AGENT; + const isMailtrapMCPContext = + isMainModuleMCP() || + isWorkingDirectoryMCP() || + isCallStackMCP() || + isMCPRuntimeContext(); - return selectedUA; + return isMailtrapMCPContext ? MCP_USER_AGENT : USER_AGENT; } export default getDynamicUserAgent; From c4033880519f91be6cca0bf6fe038f8aee5c5a63 Mon Sep 17 00:00:00 2001 From: Narek Hovhannisyan Date: Fri, 26 Sep 2025 17:29:03 +0400 Subject: [PATCH 07/12] test: add unit tests for dynamic User-Agent detection in get-agent module --- src/__tests__/lib/get-agent.test.ts | 256 ++++++++++++++++++++++++++++ 1 file changed, 256 insertions(+) create mode 100644 src/__tests__/lib/get-agent.test.ts diff --git a/src/__tests__/lib/get-agent.test.ts b/src/__tests__/lib/get-agent.test.ts new file mode 100644 index 0000000..be216c2 --- /dev/null +++ b/src/__tests__/lib/get-agent.test.ts @@ -0,0 +1,256 @@ +import CONFIG from "../../config"; + +// Mock the config +jest.mock("../../config", () => ({ + CLIENT_SETTINGS: { + USER_AGENT: + "mailtrap-nodejs (https://github.com/railsware/mailtrap-nodejs)", + MCP_USER_AGENT: "mailtrap-mcp (https://github.com/railsware/mailtrap-mcp)", + }, +})); + +describe("get-agent", () => { + let originalCwd: string; + let originalError: typeof Error; + + beforeEach(() => { + // Store original values + originalCwd = process.cwd(); + originalError = global.Error; + }); + + afterEach(() => { + // Restore original values + process.cwd = jest.fn().mockReturnValue(originalCwd); + global.Error = originalError; + jest.clearAllMocks(); + }); + + describe("getDynamicUserAgent", () => { + it("should return USER_AGENT by default", () => { + // Import after mocking to get fresh module + const { default: getDynamicUserAgent } = jest.requireActual( + "../../lib/get-agent" + ); + + const result = getDynamicUserAgent(); + expect(result).toBe(CONFIG.CLIENT_SETTINGS.USER_AGENT); + }); + + it("should return MCP_USER_AGENT when cwd contains 'mailtrap-mcp' and not in node_modules", () => { + process.cwd = jest.fn().mockReturnValue("/path/to/mailtrap-mcp"); + + // Clear module cache and re-import to get fresh module + jest.resetModules(); + const { default: getDynamicUserAgent } = jest.requireActual( + "../../lib/get-agent" + ); + + const result = getDynamicUserAgent(); + expect(result).toBe(CONFIG.CLIENT_SETTINGS.MCP_USER_AGENT); + }); + + it("should return USER_AGENT when cwd contains 'mailtrap-mcp' but is in node_modules", () => { + process.cwd = jest + .fn() + .mockReturnValue("/path/to/node_modules/mailtrap-mcp"); + + jest.resetModules(); + const { default: getDynamicUserAgent } = jest.requireActual( + "../../lib/get-agent" + ); + + const result = getDynamicUserAgent(); + expect(result).toBe(CONFIG.CLIENT_SETTINGS.USER_AGENT); + }); + + it("should return USER_AGENT when process.cwd() throws an error", () => { + process.cwd = jest.fn().mockImplementation(() => { + throw new Error("Permission denied"); + }); + + jest.resetModules(); + const { default: getDynamicUserAgent } = jest.requireActual( + "../../lib/get-agent" + ); + + const result = getDynamicUserAgent(); + expect(result).toBe(CONFIG.CLIENT_SETTINGS.USER_AGENT); + }); + + it("should return MCP_USER_AGENT when call stack contains 'mailtrap-mcp' and not from node_modules/mailtrap", () => { + global.Error = jest.fn().mockImplementation(() => ({ + stack: ` +Error: Test error + at Object. (/path/to/mailtrap-mcp/index.js:10:1) + at Module._compile (internal/modules/cjs/loader.js:1063:30) + `, + })) as any; + + process.cwd = jest.fn().mockReturnValue("/path/to/regular-app"); + + jest.resetModules(); + const { default: getDynamicUserAgent } = jest.requireActual( + "../../lib/get-agent" + ); + + const result = getDynamicUserAgent(); + expect(result).toBe(CONFIG.CLIENT_SETTINGS.MCP_USER_AGENT); + }); + + it("should return USER_AGENT when call stack contains 'mailtrap-mcp' but is from node_modules/mailtrap", () => { + global.Error = jest.fn().mockImplementation(() => ({ + stack: ` +Error: Test error + at Object. (/path/to/node_modules/mailtrap/index.js:10:1) + at Module._compile (internal/modules/cjs/loader.js:1063:30) + `, + })) as any; + + process.cwd = jest.fn().mockReturnValue("/path/to/regular-app"); + + jest.resetModules(); + const { default: getDynamicUserAgent } = jest.requireActual( + "../../lib/get-agent" + ); + + const result = getDynamicUserAgent(); + expect(result).toBe(CONFIG.CLIENT_SETTINGS.USER_AGENT); + }); + + it("should return USER_AGENT when call stack does not contain 'mailtrap-mcp'", () => { + global.Error = jest.fn().mockImplementation(() => ({ + stack: ` +Error: Test error + at Object. (/path/to/regular-app/index.js:10:1) + at Module._compile (internal/modules/cjs/loader.js:1063:30) + `, + })) as any; + + process.cwd = jest.fn().mockReturnValue("/path/to/regular-app"); + + jest.resetModules(); + const { default: getDynamicUserAgent } = jest.requireActual( + "../../lib/get-agent" + ); + + const result = getDynamicUserAgent(); + expect(result).toBe(CONFIG.CLIENT_SETTINGS.USER_AGENT); + }); + + it("should return USER_AGENT when call stack is undefined", () => { + global.Error = jest.fn().mockImplementation(() => ({ + stack: undefined, + })) as any; + + process.cwd = jest.fn().mockReturnValue("/path/to/regular-app"); + + jest.resetModules(); + const { default: getDynamicUserAgent } = jest.requireActual( + "../../lib/get-agent" + ); + + const result = getDynamicUserAgent(); + expect(result).toBe(CONFIG.CLIENT_SETTINGS.USER_AGENT); + }); + + it("should handle edge cases gracefully", () => { + // All undefined/null cases + process.cwd = jest.fn().mockImplementation(() => { + throw new Error("Permission denied"); + }); + global.Error = jest.fn().mockImplementation(() => ({ + stack: undefined, + })) as any; + + jest.resetModules(); + const { default: getDynamicUserAgent } = jest.requireActual( + "../../lib/get-agent" + ); + + const result = getDynamicUserAgent(); + expect(result).toBe(CONFIG.CLIENT_SETTINGS.USER_AGENT); + }); + }); + + describe("real-world scenarios", () => { + it("should detect MCP context when running from MCP working directory", () => { + process.cwd = jest + .fn() + .mockReturnValue("/Users/user/projects/mailtrap-mcp"); + + jest.resetModules(); + const { default: getDynamicUserAgent } = jest.requireActual( + "../../lib/get-agent" + ); + + const result = getDynamicUserAgent(); + expect(result).toBe(CONFIG.CLIENT_SETTINGS.MCP_USER_AGENT); + }); + + it("should not detect MCP context when used as a regular npm package", () => { + process.cwd = jest.fn().mockReturnValue("/Users/user/projects/my-app"); + + jest.resetModules(); + const { default: getDynamicUserAgent } = jest.requireActual( + "../../lib/get-agent" + ); + + const result = getDynamicUserAgent(); + expect(result).toBe(CONFIG.CLIENT_SETTINGS.USER_AGENT); + }); + }); + + describe("integration with MailtrapClient", () => { + it("should be used by MailtrapClient for User-Agent header", () => { + // Mock the full config for MailtrapClient + jest.doMock("../../config", () => ({ + CLIENT_SETTINGS: { + SENDING_ENDPOINT: "https://send.api.mailtrap.io", + BULK_ENDPOINT: "https://bulk.api.mailtrap.io", + TESTING_ENDPOINT: "https://sandbox.api.mailtrap.io", + GENERAL_ENDPOINT: "https://mailtrap.io", + USER_AGENT: + "mailtrap-nodejs (https://github.com/railsware/mailtrap-nodejs)", + MCP_USER_AGENT: + "mailtrap-mcp (https://github.com/railsware/mailtrap-mcp)", + MAX_REDIRECTS: 0, + TIMEOUT: 10000, + }, + ERRORS: { + FILENAME_REQUIRED: "Filename is required.", + CONTENT_REQUIRED: "Content is required.", + SUBJECT_REQUIRED: "Subject is required.", + FROM_REQUIRED: "From is required.", + SENDING_FAILED: "Sending failed.", + NO_DATA_ERROR: "No Data.", + TEST_INBOX_ID_MISSING: + "testInboxId is missing, testing API will not work.", + ACCOUNT_ID_MISSING: + "accountId is missing, some features of testing API may not work properly.", + BULK_SANDBOX_INCOMPATIBLE: + "Bulk mode is not applicable for sandbox API.", + }, + TRANSPORT_SETTINGS: { + NAME: "MailtrapTransport", + }, + })); + + // Clear module cache and re-import + jest.resetModules(); + const { default: MailtrapClient } = jest.requireActual( + "../../lib/MailtrapClient" + ); + + // Create a client instance + const client = new MailtrapClient({ + token: "test-token", + }); + + // The User-Agent should be set in the axios instance + // We can't easily test the internal axios instance, but we can verify + // that the function is called during client creation + expect(client).toBeDefined(); + }); + }); +}); From cf6290019a21a1a3c758e5ce84dc4065945734dc Mon Sep 17 00:00:00 2001 From: Narek Hovhannisyan Date: Wed, 1 Oct 2025 20:27:42 +0400 Subject: [PATCH 08/12] test: remove obsolete unit tests for dynamic User-Agent detection in get-agent module --- src/__tests__/lib/get-agent.test.ts | 256 ---------------------------- 1 file changed, 256 deletions(-) delete mode 100644 src/__tests__/lib/get-agent.test.ts diff --git a/src/__tests__/lib/get-agent.test.ts b/src/__tests__/lib/get-agent.test.ts deleted file mode 100644 index be216c2..0000000 --- a/src/__tests__/lib/get-agent.test.ts +++ /dev/null @@ -1,256 +0,0 @@ -import CONFIG from "../../config"; - -// Mock the config -jest.mock("../../config", () => ({ - CLIENT_SETTINGS: { - USER_AGENT: - "mailtrap-nodejs (https://github.com/railsware/mailtrap-nodejs)", - MCP_USER_AGENT: "mailtrap-mcp (https://github.com/railsware/mailtrap-mcp)", - }, -})); - -describe("get-agent", () => { - let originalCwd: string; - let originalError: typeof Error; - - beforeEach(() => { - // Store original values - originalCwd = process.cwd(); - originalError = global.Error; - }); - - afterEach(() => { - // Restore original values - process.cwd = jest.fn().mockReturnValue(originalCwd); - global.Error = originalError; - jest.clearAllMocks(); - }); - - describe("getDynamicUserAgent", () => { - it("should return USER_AGENT by default", () => { - // Import after mocking to get fresh module - const { default: getDynamicUserAgent } = jest.requireActual( - "../../lib/get-agent" - ); - - const result = getDynamicUserAgent(); - expect(result).toBe(CONFIG.CLIENT_SETTINGS.USER_AGENT); - }); - - it("should return MCP_USER_AGENT when cwd contains 'mailtrap-mcp' and not in node_modules", () => { - process.cwd = jest.fn().mockReturnValue("/path/to/mailtrap-mcp"); - - // Clear module cache and re-import to get fresh module - jest.resetModules(); - const { default: getDynamicUserAgent } = jest.requireActual( - "../../lib/get-agent" - ); - - const result = getDynamicUserAgent(); - expect(result).toBe(CONFIG.CLIENT_SETTINGS.MCP_USER_AGENT); - }); - - it("should return USER_AGENT when cwd contains 'mailtrap-mcp' but is in node_modules", () => { - process.cwd = jest - .fn() - .mockReturnValue("/path/to/node_modules/mailtrap-mcp"); - - jest.resetModules(); - const { default: getDynamicUserAgent } = jest.requireActual( - "../../lib/get-agent" - ); - - const result = getDynamicUserAgent(); - expect(result).toBe(CONFIG.CLIENT_SETTINGS.USER_AGENT); - }); - - it("should return USER_AGENT when process.cwd() throws an error", () => { - process.cwd = jest.fn().mockImplementation(() => { - throw new Error("Permission denied"); - }); - - jest.resetModules(); - const { default: getDynamicUserAgent } = jest.requireActual( - "../../lib/get-agent" - ); - - const result = getDynamicUserAgent(); - expect(result).toBe(CONFIG.CLIENT_SETTINGS.USER_AGENT); - }); - - it("should return MCP_USER_AGENT when call stack contains 'mailtrap-mcp' and not from node_modules/mailtrap", () => { - global.Error = jest.fn().mockImplementation(() => ({ - stack: ` -Error: Test error - at Object. (/path/to/mailtrap-mcp/index.js:10:1) - at Module._compile (internal/modules/cjs/loader.js:1063:30) - `, - })) as any; - - process.cwd = jest.fn().mockReturnValue("/path/to/regular-app"); - - jest.resetModules(); - const { default: getDynamicUserAgent } = jest.requireActual( - "../../lib/get-agent" - ); - - const result = getDynamicUserAgent(); - expect(result).toBe(CONFIG.CLIENT_SETTINGS.MCP_USER_AGENT); - }); - - it("should return USER_AGENT when call stack contains 'mailtrap-mcp' but is from node_modules/mailtrap", () => { - global.Error = jest.fn().mockImplementation(() => ({ - stack: ` -Error: Test error - at Object. (/path/to/node_modules/mailtrap/index.js:10:1) - at Module._compile (internal/modules/cjs/loader.js:1063:30) - `, - })) as any; - - process.cwd = jest.fn().mockReturnValue("/path/to/regular-app"); - - jest.resetModules(); - const { default: getDynamicUserAgent } = jest.requireActual( - "../../lib/get-agent" - ); - - const result = getDynamicUserAgent(); - expect(result).toBe(CONFIG.CLIENT_SETTINGS.USER_AGENT); - }); - - it("should return USER_AGENT when call stack does not contain 'mailtrap-mcp'", () => { - global.Error = jest.fn().mockImplementation(() => ({ - stack: ` -Error: Test error - at Object. (/path/to/regular-app/index.js:10:1) - at Module._compile (internal/modules/cjs/loader.js:1063:30) - `, - })) as any; - - process.cwd = jest.fn().mockReturnValue("/path/to/regular-app"); - - jest.resetModules(); - const { default: getDynamicUserAgent } = jest.requireActual( - "../../lib/get-agent" - ); - - const result = getDynamicUserAgent(); - expect(result).toBe(CONFIG.CLIENT_SETTINGS.USER_AGENT); - }); - - it("should return USER_AGENT when call stack is undefined", () => { - global.Error = jest.fn().mockImplementation(() => ({ - stack: undefined, - })) as any; - - process.cwd = jest.fn().mockReturnValue("/path/to/regular-app"); - - jest.resetModules(); - const { default: getDynamicUserAgent } = jest.requireActual( - "../../lib/get-agent" - ); - - const result = getDynamicUserAgent(); - expect(result).toBe(CONFIG.CLIENT_SETTINGS.USER_AGENT); - }); - - it("should handle edge cases gracefully", () => { - // All undefined/null cases - process.cwd = jest.fn().mockImplementation(() => { - throw new Error("Permission denied"); - }); - global.Error = jest.fn().mockImplementation(() => ({ - stack: undefined, - })) as any; - - jest.resetModules(); - const { default: getDynamicUserAgent } = jest.requireActual( - "../../lib/get-agent" - ); - - const result = getDynamicUserAgent(); - expect(result).toBe(CONFIG.CLIENT_SETTINGS.USER_AGENT); - }); - }); - - describe("real-world scenarios", () => { - it("should detect MCP context when running from MCP working directory", () => { - process.cwd = jest - .fn() - .mockReturnValue("/Users/user/projects/mailtrap-mcp"); - - jest.resetModules(); - const { default: getDynamicUserAgent } = jest.requireActual( - "../../lib/get-agent" - ); - - const result = getDynamicUserAgent(); - expect(result).toBe(CONFIG.CLIENT_SETTINGS.MCP_USER_AGENT); - }); - - it("should not detect MCP context when used as a regular npm package", () => { - process.cwd = jest.fn().mockReturnValue("/Users/user/projects/my-app"); - - jest.resetModules(); - const { default: getDynamicUserAgent } = jest.requireActual( - "../../lib/get-agent" - ); - - const result = getDynamicUserAgent(); - expect(result).toBe(CONFIG.CLIENT_SETTINGS.USER_AGENT); - }); - }); - - describe("integration with MailtrapClient", () => { - it("should be used by MailtrapClient for User-Agent header", () => { - // Mock the full config for MailtrapClient - jest.doMock("../../config", () => ({ - CLIENT_SETTINGS: { - SENDING_ENDPOINT: "https://send.api.mailtrap.io", - BULK_ENDPOINT: "https://bulk.api.mailtrap.io", - TESTING_ENDPOINT: "https://sandbox.api.mailtrap.io", - GENERAL_ENDPOINT: "https://mailtrap.io", - USER_AGENT: - "mailtrap-nodejs (https://github.com/railsware/mailtrap-nodejs)", - MCP_USER_AGENT: - "mailtrap-mcp (https://github.com/railsware/mailtrap-mcp)", - MAX_REDIRECTS: 0, - TIMEOUT: 10000, - }, - ERRORS: { - FILENAME_REQUIRED: "Filename is required.", - CONTENT_REQUIRED: "Content is required.", - SUBJECT_REQUIRED: "Subject is required.", - FROM_REQUIRED: "From is required.", - SENDING_FAILED: "Sending failed.", - NO_DATA_ERROR: "No Data.", - TEST_INBOX_ID_MISSING: - "testInboxId is missing, testing API will not work.", - ACCOUNT_ID_MISSING: - "accountId is missing, some features of testing API may not work properly.", - BULK_SANDBOX_INCOMPATIBLE: - "Bulk mode is not applicable for sandbox API.", - }, - TRANSPORT_SETTINGS: { - NAME: "MailtrapTransport", - }, - })); - - // Clear module cache and re-import - jest.resetModules(); - const { default: MailtrapClient } = jest.requireActual( - "../../lib/MailtrapClient" - ); - - // Create a client instance - const client = new MailtrapClient({ - token: "test-token", - }); - - // The User-Agent should be set in the axios instance - // We can't easily test the internal axios instance, but we can verify - // that the function is called during client creation - expect(client).toBeDefined(); - }); - }); -}); From 581a433286ec4d53fb9e4abf1978ce90fe25e16b Mon Sep 17 00:00:00 2001 From: Narek Hovhannisyan Date: Wed, 1 Oct 2025 20:27:53 +0400 Subject: [PATCH 09/12] config: remove obsolete MCP_USER_AGENT from configuration --- src/config/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/config/index.ts b/src/config/index.ts index 0859ce3..f1b5ad2 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -18,7 +18,6 @@ export default { GENERAL_ENDPOINT: "https://mailtrap.io", USER_AGENT: "mailtrap-nodejs (https://github.com/railsware/mailtrap-nodejs)", - MCP_USER_AGENT: "mailtrap-mcp (https://github.com/railsware/mailtrap-mcp)", MAX_REDIRECTS: 0, TIMEOUT: 10000, }, From bc38c8ce367c1c2c8484bed64c11f1f5ce426d0c Mon Sep 17 00:00:00 2001 From: Narek Hovhannisyan Date: Wed, 1 Oct 2025 20:28:03 +0400 Subject: [PATCH 10/12] lib: remove getDynamicUserAgent function and related MCP context detection logic --- src/lib/get-agent.ts | 75 -------------------------------------------- 1 file changed, 75 deletions(-) delete mode 100644 src/lib/get-agent.ts diff --git a/src/lib/get-agent.ts b/src/lib/get-agent.ts deleted file mode 100644 index 7242b74..0000000 --- a/src/lib/get-agent.ts +++ /dev/null @@ -1,75 +0,0 @@ -import CONFIG from "../config"; - -const { USER_AGENT, MCP_USER_AGENT } = CONFIG.CLIENT_SETTINGS; - -/** - * Checks if the main module filename indicates MCP context. - * @returns true if main module contains "mailtrap-mcp" and is not in node_modules - */ -function isMainModuleMCP(): boolean { - const mainFile = require?.main?.filename; - - return !!( - mainFile && - mainFile.includes("mailtrap-mcp") && - !mainFile.includes("node_modules") - ); -} - -/** - * Checks if running in MCP runtime context (Claude Desktop). - * @returns true if main module is from MCP runtime - */ -function isMCPRuntimeContext(): boolean { - const mainFile = require?.main?.filename; - - return !!( - mainFile && - (mainFile.includes("mcp-runtime") || - mainFile.includes("nodeHost.js") || - mainFile.includes("Claude.app")) - ); -} - -/** - * Checks if the current working directory indicates MCP context. - * @returns true if cwd contains "mailtrap-mcp" and is not in node_modules - */ -function isWorkingDirectoryMCP(): boolean { - try { - const cwd = process.cwd(); - return cwd.includes("mailtrap-mcp") && !cwd.includes("node_modules"); - } catch { - return false; - } -} - -/** - * Checks if the call stack indicates MCP context. - * @returns true if stack contains "mailtrap-mcp" and is not from node_modules/mailtrap - */ -function isCallStackMCP(): boolean { - const { stack } = new Error(); - - return !!( - stack && - stack.includes("mailtrap-mcp") && - !stack.includes("node_modules/mailtrap") - ); -} - -/** - * Gets the appropriate User-Agent string based on the current context. - * @returns The User-Agent string for the current context - */ -function getDynamicUserAgent(): string { - const isMailtrapMCPContext = - isMainModuleMCP() || - isWorkingDirectoryMCP() || - isCallStackMCP() || - isMCPRuntimeContext(); - - return isMailtrapMCPContext ? MCP_USER_AGENT : USER_AGENT; -} - -export default getDynamicUserAgent; From 0d0ff63a5e5bf7b993f79bfb6010d373d735e15f Mon Sep 17 00:00:00 2001 From: Narek Hovhannisyan Date: Wed, 1 Oct 2025 20:28:21 +0400 Subject: [PATCH 11/12] lib: update MailtrapClient to use userAgent parameter or fallback to USER_AGENT constant --- src/lib/MailtrapClient.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/lib/MailtrapClient.ts b/src/lib/MailtrapClient.ts index 7adf1f2..f13eba6 100644 --- a/src/lib/MailtrapClient.ts +++ b/src/lib/MailtrapClient.ts @@ -6,7 +6,6 @@ import axios, { AxiosInstance } from "axios"; import encodeMailBuffers from "./mail-buffer-encoder"; import handleSendingError from "./axios-logger"; import MailtrapError from "./MailtrapError"; -import getDynamicUserAgent from "./get-agent"; import GeneralAPI from "./api/General"; import TestingAPI from "./api/Testing"; @@ -32,6 +31,7 @@ const { TIMEOUT, TESTING_ENDPOINT, BULK_ENDPOINT, + USER_AGENT, } = CLIENT_SETTINGS; const { ACCOUNT_ID_MISSING, BULK_SANDBOX_INCOMPATIBLE, TEST_INBOX_ID_MISSING } = ERRORS; @@ -59,6 +59,7 @@ export default class MailtrapClient { accountId, bulk = false, sandbox = false, + userAgent, }: MailtrapClientConfig) { this.axios = axios.create({ httpAgent: new http.Agent({ keepAlive: true }), @@ -66,7 +67,7 @@ export default class MailtrapClient { headers: { Authorization: `Bearer ${token}`, Connection: "keep-alive", - "User-Agent": getDynamicUserAgent(), + "User-Agent": userAgent || USER_AGENT, }, maxRedirects: MAX_REDIRECTS, timeout: TIMEOUT, From 84b609ace0578cd5beeeaa977db93e3f5d5a1b96 Mon Sep 17 00:00:00 2001 From: Narek Hovhannisyan Date: Wed, 1 Oct 2025 20:28:31 +0400 Subject: [PATCH 12/12] types: add optional userAgent property to MailtrapClientConfig --- src/types/mailtrap.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/types/mailtrap.ts b/src/types/mailtrap.ts index 6a7fe33..baa5982 100644 --- a/src/types/mailtrap.ts +++ b/src/types/mailtrap.ts @@ -67,6 +67,7 @@ export type MailtrapClientConfig = { accountId?: number; bulk?: boolean; sandbox?: boolean; + userAgent?: string; }; export type BatchMail = Mail[];