From 5cf0ea1808c6c27acaa6c678a16ecebc87b9cbd0 Mon Sep 17 00:00:00 2001 From: Ruturaj-Browserstack Date: Wed, 16 Jul 2025 17:29:41 +0530 Subject: [PATCH 1/3] feat: implement HTTPS agent support in ApiClient for proxy and CA certificate handling --- src/lib/apiClient.ts | 61 +++++++++++++++++++++++++++++++++++++++----- 1 file changed, 54 insertions(+), 7 deletions(-) diff --git a/src/lib/apiClient.ts b/src/lib/apiClient.ts index 555f51b..23cdeea 100644 --- a/src/lib/apiClient.ts +++ b/src/lib/apiClient.ts @@ -1,4 +1,9 @@ import axios, { AxiosRequestConfig, AxiosResponse } from "axios"; +import httpsProxyAgentPkg from "https-proxy-agent"; +const { HttpsProxyAgent } = httpsProxyAgentPkg; +import * as https from "https"; +import * as fs from "fs"; +import config from "../config.js"; type RequestOptions = { url: string; @@ -54,15 +59,52 @@ class ApiResponse { } } +// Utility to create HTTPS agent if needed (proxy/CA) +function getAxiosAgent(): AxiosRequestConfig["httpsAgent"] | undefined { + const proxyHost = config.browserstackLocalOptions.proxyHost; + const proxyPort = config.browserstackLocalOptions.proxyPort; + const caCertPath = config.browserstackLocalOptions.useCaCertificate; + + // If both proxy host and port are defined + if (proxyHost && proxyPort) { + const proxyUrl = `http://${proxyHost}:${proxyPort}`; + if (caCertPath && fs.existsSync(caCertPath)) { + // Proxy + CA cert + const ca = fs.readFileSync(caCertPath); + return new HttpsProxyAgent({ + host: proxyHost, + port: Number(proxyPort), + ca, + rejectUnauthorized: false, // Set to true if you want strict SSL + }); + } else { + // Proxy only + return new HttpsProxyAgent(proxyUrl); + } + } else if (caCertPath && fs.existsSync(caCertPath)) { + // CA only + return new https.Agent({ + ca: fs.readFileSync(caCertPath), + rejectUnauthorized: false, // Set to true for strict SSL + }); + } + // Default agent (no proxy, no CA) + return undefined; +} + class ApiClient { private instance = axios.create(); + private get axiosAgent() { + return getAxiosAgent(); + } + private async requestWrapper( - fn: () => Promise>, + fn: (agent: AxiosRequestConfig["httpsAgent"]) => Promise>, raise_error: boolean = true, ): Promise> { try { - const res = await fn(); + const res = await fn(this.axiosAgent); return new ApiResponse(res); } catch (error: any) { if (error.response && !raise_error) { @@ -79,7 +121,8 @@ class ApiClient { raise_error = true, }: RequestOptions): Promise> { return this.requestWrapper( - () => this.instance.get(url, { headers, params }), + (agent) => + this.instance.get(url, { headers, params, httpsAgent: agent }), raise_error, ); } @@ -91,7 +134,8 @@ class ApiClient { raise_error = true, }: RequestOptions): Promise> { return this.requestWrapper( - () => this.instance.post(url, body, { headers }), + (agent) => + this.instance.post(url, body, { headers, httpsAgent: agent }), raise_error, ); } @@ -103,7 +147,8 @@ class ApiClient { raise_error = true, }: RequestOptions): Promise> { return this.requestWrapper( - () => this.instance.put(url, body, { headers }), + (agent) => + this.instance.put(url, body, { headers, httpsAgent: agent }), raise_error, ); } @@ -115,7 +160,8 @@ class ApiClient { raise_error = true, }: RequestOptions): Promise> { return this.requestWrapper( - () => this.instance.patch(url, body, { headers }), + (agent) => + this.instance.patch(url, body, { headers, httpsAgent: agent }), raise_error, ); } @@ -127,7 +173,8 @@ class ApiClient { raise_error = true, }: RequestOptions): Promise> { return this.requestWrapper( - () => this.instance.delete(url, { headers, params }), + (agent) => + this.instance.delete(url, { headers, params, httpsAgent: agent }), raise_error, ); } From fe8e5e9dfd210e35ac5575119c6db4ffba16cdb7 Mon Sep 17 00:00:00 2001 From: Ruturaj-Browserstack Date: Fri, 18 Jul 2025 12:06:44 +0530 Subject: [PATCH 2/3] chore: update version to 1.2.0 in package.json and package-lock.json --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9a07d19..df1fa34 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@browserstack/mcp-server", - "version": "1.1.9", + "version": "1.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@browserstack/mcp-server", - "version": "1.1.9", + "version": "1.2.0", "license": "ISC", "dependencies": { "@modelcontextprotocol/sdk": "^1.11.4", diff --git a/package.json b/package.json index e1bb823..f171664 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@browserstack/mcp-server", - "version": "1.1.9", + "version": "1.2.0", "description": "BrowserStack's Official MCP Server", "main": "dist/index.js", "repository": { From 8d50fad474a43ad9110c9df78a611ffb98a19308 Mon Sep 17 00:00:00 2001 From: Ruturaj-Browserstack Date: Fri, 18 Jul 2025 12:29:25 +0530 Subject: [PATCH 3/3] refactor: rename logger variable to currentLogger and update proxy implementation --- src/logger.ts | 31 ++++++++++++++++++++++--------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/src/logger.ts b/src/logger.ts index 3dc2eb2..6fb686c 100644 --- a/src/logger.ts +++ b/src/logger.ts @@ -1,9 +1,10 @@ import { pino } from "pino"; -let logger: any; +// 1. The actual logger instance, swapped out as needed +let currentLogger: any; if (process.env.NODE_ENV === "development") { - logger = pino({ + currentLogger = pino({ level: "debug", transport: { targets: [ @@ -23,8 +24,8 @@ if (process.env.NODE_ENV === "development") { }, }); } else { - // NULL logger - logger = pino({ + // Null logger (logs go to /dev/null or NUL) + currentLogger = pino({ level: "info", transport: { target: "pino/file", @@ -35,12 +36,24 @@ if (process.env.NODE_ENV === "development") { }); } -/** - * Set a custom logger instance - * @param customLogger - The logger instance to use - */ +// 2. Proxy logger: always delegates to the currentLogger +const logger: any = new Proxy( + {}, + { + get(_target, prop) { + // Forward function calls to currentLogger + if (typeof currentLogger[prop] === "function") { + return (...args: any[]) => currentLogger[prop](...args); + } + // Forward property gets + return currentLogger[prop]; + }, + }, +); + +// 3. Setter to update the logger instance everywhere export function setLogger(customLogger: any): void { - logger = customLogger; + currentLogger = customLogger; } export default logger;