diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 31af99f8..e866f2e4 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -5,6 +5,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; import { trackMCP } from "../index.js"; import logger from "../logger.js"; +import globalConfig from "../config.js"; export function sanitizeUrlParam(param: string): string { // Remove any characters that could be used for command injection @@ -12,6 +13,7 @@ export function sanitizeUrlParam(param: string): string { } const ONE_MB = 1048576; +let sharpConfigured = false; //Compresses a base64 image intelligently to keep it under 1 MB if needed. export async function maybeCompressBase64(base64: string): Promise { @@ -27,6 +29,12 @@ export async function maybeCompressBase64(base64: string): Promise { try { const { default: sharp } = await import("sharp"); + if (!sharpConfigured && globalConfig.REMOTE_MCP) { + // Bound memory on constrained hosted pods: no libvips cache, single worker thread. + sharp.cache(false); + sharp.concurrency(1); + sharpConfigured = true; + } const compressedBuffer = await sharp(buffer).png({ quality }).toBuffer(); return compressedBuffer.toString("base64"); } catch (err) { diff --git a/src/tools/automate-utils/fetch-screenshots.ts b/src/tools/automate-utils/fetch-screenshots.ts index 46eff517..a570b6f2 100644 --- a/src/tools/automate-utils/fetch-screenshots.ts +++ b/src/tools/automate-utils/fetch-screenshots.ts @@ -3,6 +3,7 @@ import { SessionType } from "../../lib/constants.js"; import { getBrowserStackAuth } from "../../lib/get-auth.js"; import { BrowserStackConfig } from "../../lib/types.js"; import { apiClient } from "../../lib/apiClient.js"; +import globalConfig from "../../config.js"; async function extractScreenshotUrls( sessionId: string, @@ -53,30 +54,30 @@ async function extractScreenshotUrls( return urls; } +async function fetchAndCompress( + url: string, +): Promise<{ url: string; base64: string }> { + const response = await apiClient.get({ url, responseType: "arraybuffer" }); + // Axios returns response.data as a Buffer for binary data + const base64 = Buffer.from(response.data).toString("base64"); + return { url, base64: await maybeCompressBase64(base64) }; +} + //Converts screenshot URLs to base64 encoded images async function convertUrlsToBase64( urls: string[], ): Promise> { - const screenshots = await Promise.all( - urls.map(async (url) => { - const response = await apiClient.get({ - url, - responseType: "arraybuffer", - }); - // Axios returns response.data as a Buffer for binary data - const base64 = Buffer.from(response.data).toString("base64"); - - // Compress the base64 image if needed - const compressedBase64 = await maybeCompressBase64(base64); - - return { - url, - base64: compressedBase64, - }; - }), - ); + // Hosted pods are memory-constrained: process one image at a time. Local runs + // keep the concurrent path. + if (globalConfig.REMOTE_MCP) { + const screenshots: Array<{ url: string; base64: string }> = []; + for (const url of urls) { + screenshots.push(await fetchAndCompress(url)); + } + return screenshots; + } - return screenshots; + return Promise.all(urls.map(fetchAndCompress)); } //Fetches and converts screenshot URLs to base64 encoded images diff --git a/tests/setup.ts b/tests/setup.ts index b56b9f4a..e634ea58 100644 --- a/tests/setup.ts +++ b/tests/setup.ts @@ -1,10 +1,13 @@ import { vi } from "vitest"; // Mock sharp globally — it's a native binary module that may not be available in CI -vi.mock("sharp", () => ({ - default: vi.fn().mockReturnValue({ +vi.mock("sharp", () => { + const sharp: any = vi.fn().mockReturnValue({ png: vi.fn().mockReturnValue({ toBuffer: vi.fn().mockResolvedValue(Buffer.from("mock-image")), }), - }), -})); + }); + sharp.cache = vi.fn(); + sharp.concurrency = vi.fn(); + return { default: sharp }; +});