Skip to content

Commit 8da95c0

Browse files
authored
feat: fetch and analyze screenshots of automate and app-automate (#40)
* feat: fetch and analyze screenshots of automate and app-automate * fix: correct import name for automate tools in index.ts
1 parent 9ae27d2 commit 8da95c0

File tree

6 files changed

+195
-30
lines changed

6 files changed

+195
-30
lines changed

src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import addAccessibilityTools from "./tools/accessibility.js";
1313
import addTestManagementTools from "./tools/testmanagement.js";
1414
import addAppAutomationTools from "./tools/appautomate.js";
1515
import addFailureLogsTools from "./tools/getFailureLogs.js";
16+
import addAutomateTools from "./tools/automate.js";
1617
import { trackMCP } from "./lib/instrumentation.js";
1718

1819
function registerTools(server: McpServer) {
@@ -24,6 +25,7 @@ function registerTools(server: McpServer) {
2425
addTestManagementTools(server);
2526
addAppAutomationTools(server);
2627
addFailureLogsTools(server);
28+
addAutomateTools(server);
2729
}
2830

2931
// Create an MCP server

src/lib/constants.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
export const SessionType = {
2+
Automate: "automate",
3+
AppAutomate: "app-automate",
4+
} as const;
5+
6+
export const AutomateLogType = {
7+
NetworkLogs: "networkLogs",
8+
SessionLogs: "sessionLogs",
9+
ConsoleLogs: "consoleLogs",
10+
} as const;
11+
12+
export const AppAutomateLogType = {
13+
DeviceLogs: "deviceLogs",
14+
AppiumLogs: "appiumLogs",
15+
CrashLogs: "crashLogs",
16+
} as const;
17+
18+
export type SessionType = typeof SessionType[keyof typeof SessionType];
19+
export type AutomateLogType = typeof AutomateLogType[keyof typeof AutomateLogType];
20+
export type AppAutomateLogType = typeof AppAutomateLogType[keyof typeof AppAutomateLogType];

src/lib/utils.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,13 @@ export interface HarEntry {
2727
time?: number;
2828
}
2929

30-
/**
31-
* Compresses a base64 image intelligently to keep it under 1 MB if needed.
32-
*/
30+
const ONE_MB = 1048576;
31+
32+
//Compresses a base64 image intelligently to keep it under 1 MB if needed.
3333
export async function maybeCompressBase64(base64: string): Promise<string> {
3434
const buffer = Buffer.from(base64, "base64");
3535

36-
if (buffer.length <= 1048576) {
36+
if (buffer.length <= ONE_MB) {
3737
return base64;
3838
}
3939

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import config from "../../config.js";
2+
import { assertOkResponse, maybeCompressBase64 } from "../../lib/utils.js";
3+
import { SessionType } from "../../lib/constants.js";
4+
5+
//Extracts screenshot URLs from BrowserStack session logs
6+
async function extractScreenshotUrls(sessionId: string, sessionType: SessionType): Promise<string[]> {
7+
8+
const credentials = `${config.browserstackUsername}:${config.browserstackAccessKey}`;
9+
const auth = Buffer.from(credentials).toString("base64");
10+
11+
const baseUrl = `https://api.browserstack.com/${sessionType === SessionType.Automate ? 'automate' : 'app-automate'}`;
12+
13+
const url = `${baseUrl}/sessions/${sessionId}/logs`;
14+
const response = await fetch(url, {
15+
headers: {
16+
"Content-Type": "application/json",
17+
Authorization: `Basic ${auth}`,
18+
},
19+
});
20+
21+
await assertOkResponse(response, "Session");
22+
23+
const text = await response.text();
24+
25+
const urls: string[] = [];
26+
const SCREENSHOT_PATTERN = /REQUEST.*GET.*\/screenshot/;
27+
const RESPONSE_VALUE_PATTERN = /"value"\s*:\s*"([^"]+)"/;
28+
29+
// Split logs into lines and process them
30+
const lines = text.split("\n");
31+
32+
for (let i = 0; i < lines.length - 1; i++) {
33+
const currentLine = lines[i];
34+
const nextLine = lines[i + 1];
35+
36+
if (SCREENSHOT_PATTERN.test(currentLine)) {
37+
const match = nextLine.match(RESPONSE_VALUE_PATTERN);
38+
if (match && match[1]) {
39+
urls.push(match[1]);
40+
}
41+
}
42+
}
43+
44+
return urls;
45+
}
46+
47+
//Converts screenshot URLs to base64 encoded images
48+
async function convertUrlsToBase64(
49+
urls: string[],
50+
): Promise<Array<{ url: string; base64: string }>> {
51+
const screenshots = await Promise.all(
52+
urls.map(async (url) => {
53+
const response = await fetch(url);
54+
const arrayBuffer = await response.arrayBuffer();
55+
const base64 = Buffer.from(arrayBuffer).toString("base64");
56+
57+
// Compress the base64 image if needed
58+
const compressedBase64 = await maybeCompressBase64(base64);
59+
60+
return {
61+
url,
62+
base64: compressedBase64,
63+
};
64+
}),
65+
);
66+
67+
return screenshots;
68+
}
69+
70+
//Fetches and converts screenshot URLs to base64 encoded images
71+
export async function fetchAutomationScreenshots(sessionId: string, sessionType: SessionType = SessionType.Automate) {
72+
const urls = await extractScreenshotUrls(sessionId, sessionType);
73+
if (urls.length === 0) {
74+
return [];
75+
}
76+
77+
// Take only the last 5 URLs
78+
const lastFiveUrls = urls.slice(-5);
79+
return await convertUrlsToBase64(lastFiveUrls);
80+
}

src/tools/automate.ts

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2+
import { z } from "zod";
3+
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
4+
import { fetchAutomationScreenshots } from "./automate-utils/fetch-screenshots.js";
5+
import { SessionType } from "../lib/constants.js";
6+
import { trackMCP } from "../lib/instrumentation.js";
7+
import logger from "../logger.js";
8+
9+
// Tool function that fetches and processes screenshots from BrowserStack Automate session
10+
export async function fetchAutomationScreenshotsTool(args: {
11+
sessionId: string;
12+
sessionType: SessionType;
13+
}): Promise<CallToolResult> {
14+
try {
15+
const screenshots = await fetchAutomationScreenshots(args.sessionId, args.sessionType);
16+
17+
if (screenshots.length === 0) {
18+
return {
19+
content: [
20+
{
21+
type: "text",
22+
text: "No screenshots found in the session or some unexpected error occurred",
23+
},
24+
],
25+
isError: true,
26+
};
27+
}
28+
29+
const results = screenshots.map((screenshot, index) => ({
30+
type: "image" as const,
31+
text: `Screenshot ${index + 1}`,
32+
data: screenshot.base64,
33+
mimeType: "image/png",
34+
metadata: { url: screenshot.url },
35+
}));
36+
37+
return {
38+
content: [
39+
{
40+
type: "text",
41+
text: `Retrieved ${screenshots.length} screenshot(s) from the end of the session.`,
42+
},
43+
...results,
44+
],
45+
};
46+
} catch (error) {
47+
logger.error("Error during fetching screenshots", error);
48+
throw error;
49+
}
50+
}
51+
52+
//Registers the fetchAutomationScreenshots tool with the MCP server
53+
export default function addAutomationTools(server: McpServer) {
54+
server.tool(
55+
"fetchAutomationScreenshots",
56+
"Fetch and process screenshots from a BrowserStack Automate session",
57+
{
58+
sessionId: z
59+
.string()
60+
.describe("The BrowserStack session ID to fetch screenshots from"),
61+
sessionType: z
62+
.enum([SessionType.Automate, SessionType.AppAutomate])
63+
.describe("Type of BrowserStack session")
64+
},
65+
async (args) => {
66+
try {
67+
trackMCP("fetchAutomationScreenshots", server.server.getClientVersion()!);
68+
return await fetchAutomationScreenshotsTool(args);
69+
} catch (error) {
70+
trackMCP("fetchAutomationScreenshots", server.server.getClientVersion()!,error);
71+
const errorMessage =
72+
error instanceof Error ? error.message : "Unknown error";
73+
return {
74+
content: [
75+
{
76+
type: "text",
77+
text: `Error during fetching automate screenshots: ${errorMessage}`,
78+
},
79+
],
80+
};
81+
}
82+
},
83+
);
84+
}

src/tools/getFailureLogs.ts

Lines changed: 5 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -15,31 +15,10 @@ import {
1515
retrieveCrashLogs,
1616
} from "./failurelogs-utils/app-automate.js";
1717
import { trackMCP } from "../lib/instrumentation.js";
18+
import { AppAutomateLogType, AutomateLogType, SessionType } from "../lib/constants.js";
1819

19-
const AutomateLogType = {
20-
NetworkLogs: "networkLogs",
21-
SessionLogs: "sessionLogs",
22-
ConsoleLogs: "consoleLogs",
23-
} as const;
24-
25-
const AppAutomateLogType = {
26-
DeviceLogs: "deviceLogs",
27-
AppiumLogs: "appiumLogs",
28-
CrashLogs: "crashLogs",
29-
} as const;
30-
31-
const SessionType = {
32-
Automate: "automate",
33-
AppAutomate: "app-automate",
34-
} as const;
35-
36-
// Type aliases
37-
type AutomateLogTypeValues =
38-
(typeof AutomateLogType)[keyof typeof AutomateLogType];
39-
type AppAutomateLogTypeValues =
40-
(typeof AppAutomateLogType)[keyof typeof AppAutomateLogType];
41-
type LogType = AutomateLogTypeValues | AppAutomateLogTypeValues;
42-
type SessionTypeValues = (typeof SessionType)[keyof typeof SessionType];
20+
type LogType = AutomateLogType | AppAutomateLogType;
21+
type SessionTypeValues = SessionType;
4322

4423
// Main log fetcher function
4524
export async function getFailureLogs(args: {
@@ -63,10 +42,10 @@ export async function getFailureLogs(args: {
6342
// Validate log types and collect errors
6443
validLogTypes = args.logTypes.filter((logType) => {
6544
const isAutomate = Object.values(AutomateLogType).includes(
66-
logType as AutomateLogTypeValues,
45+
logType as AutomateLogType,
6746
);
6847
const isAppAutomate = Object.values(AppAutomateLogType).includes(
69-
logType as AppAutomateLogTypeValues,
48+
logType as AppAutomateLogType,
7049
);
7150

7251
if (!isAutomate && !isAppAutomate) {

0 commit comments

Comments
 (0)