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
8 changes: 8 additions & 0 deletions src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@ 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
return param.replace(/[;&|`$(){}[\]<>]/g, "");
}

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<string> {
Expand All @@ -27,6 +29,12 @@ export async function maybeCompressBase64(base64: string): Promise<string> {

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) {
Expand Down
39 changes: 20 additions & 19 deletions src/tools/automate-utils/fetch-screenshots.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<Array<{ url: string; base64: string }>> {
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
Expand Down
11 changes: 7 additions & 4 deletions tests/setup.ts
Original file line number Diff line number Diff line change
@@ -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 };
});
Loading