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
2 changes: 2 additions & 0 deletions src/server-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import addSelfHealTools from "./tools/selfheal.js";
import addAppLiveTools from "./tools/applive.js";
import { setupOnInitialized } from "./oninitialized.js";
import { BrowserStackConfig } from "./lib/types.js";
import addRCATools from "./tools/rca-agent.js";

/**
* Wrapper class for BrowserStack MCP Server
Expand Down Expand Up @@ -55,6 +56,7 @@ export class BrowserStackMcpServer {
addFailureLogsTools,
addAutomateTools,
addSelfHealTools,
addRCATools,
];

toolAdders.forEach((adder) => {
Expand Down
37 changes: 37 additions & 0 deletions src/tools/rca-agent-utils/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { z } from "zod";
import { TestStatus } from "./types.js";

export const FETCH_RCA_PARAMS = {
testId: z
.array(z.string())
.max(3)
.describe(
"Array of test IDs to fetch RCA data for (maximum 3 IDs). If not provided, use the listTestIds tool get all failed testcases. If more than 3 IDs are provided, only the first 3 will be processed."
),
};

export const GET_BUILD_ID_PARAMS = {
projectName: z
.string()
.describe(
"The Browserstack project name used while creation of test run. Check browserstack.yml or similar project configuration files. If found extract it and provide to user, IF not found or unsure, prompt the user for this value. Do not make assumptions"
),
buildName: z
.string()
.describe(
"The Browserstack build name used while creation of test run. Check browserstack.yml or similar project configuration files. If found extract it and provide to user, IF not found or unsure, prompt the user for this value. Do not make assumptions"
),
};

export const LIST_TEST_IDS_PARAMS = {
buildId: z
.string()
.describe(
"The Browserstack Build ID of the test run. If not known, use the getBuildId tool to fetch it using project and build name"
),
status: z
.nativeEnum(TestStatus)
.describe(
"Filter tests by status. If not provided, all tests are returned. Example for RCA usecase always use failed status"
),
};
44 changes: 44 additions & 0 deletions src/tools/rca-agent-utils/format-rca.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Utility function to format RCA data for better readability
export function formatRCAData(rcaData: any): string {
if (!rcaData || !rcaData.testCases || rcaData.testCases.length === 0) {
return "No RCA data available.";
}

let output = "## Root Cause Analysis Report\n\n";

rcaData.testCases.forEach((testCase: any, index: number) => {
// Show test case ID with smaller heading
output += `### Test Case ${index + 1}\n`;
output += `**Test ID:** ${testCase.id}\n`;
output += `**Status:** ${testCase.state}\n\n`;

// Access RCA data from the correct path
const rca = testCase.rcaData?.rcaData;

if (rca) {
if (rca.root_cause) {
output += `**Root Cause:** ${rca.root_cause}\n\n`;
}

if (rca.failure_type) {
output += `**Failure Type:** ${rca.failure_type}\n\n`;
}

if (rca.description) {
output += `**Detailed Analysis:**\n${rca.description}\n\n`;
}

if (rca.possible_fix) {
output += `**Recommended Fix:**\n${rca.possible_fix}\n\n`;
}
} else if (testCase.rcaData?.error) {
output += `**Error:** ${testCase.rcaData.error}\n\n`;
} else if (testCase.state === "failed") {
output += `**Note:** RCA analysis failed or is not available for this test case.\n\n`;
}

output += "---\n\n";
});

return output;
}
32 changes: 32 additions & 0 deletions src/tools/rca-agent-utils/get-build-id.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
export async function getBuildId(
projectName: string,
buildName: string,
username: string,
accessKey: string,
): Promise<string> {
const url = new URL(
"https://api-automation.browserstack.com/ext/v1/builds/latest",
);
url.searchParams.append("project_name", projectName);
url.searchParams.append("build_name", buildName);
url.searchParams.append("user_name", username);

const authHeader =
"Basic " + Buffer.from(`${username}:${accessKey}`).toString("base64");

const response = await fetch(url.toString(), {
headers: {
Authorization: authHeader,
"Content-Type": "application/json",
},
});

if (!response.ok) {
throw new Error(
`Failed to fetch build ID: ${response.status} ${response.statusText}`,
);
}

const data = await response.json();
return data.build_id;
}
88 changes: 88 additions & 0 deletions src/tools/rca-agent-utils/get-failed-test-id.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import logger from "../../logger.js";
import { TestStatus, FailedTestInfo, TestRun, TestDetails } from "./types.js";

export async function getTestIds(
buildId: string,
authString: string,
status?: TestStatus,
): Promise<FailedTestInfo[]> {
const baseUrl = `https://api-automation.browserstack.com/ext/v1/builds/${buildId}/testRuns`;
let url = status ? `${baseUrl}?test_statuses=${status}` : baseUrl;
let allFailedTests: FailedTestInfo[] = [];
let requestNumber = 0;

// Construct Basic auth header
const encodedCredentials = Buffer.from(authString).toString("base64");
const authHeader = `Basic ${encodedCredentials}`;

try {
while (true) {
requestNumber++;

const response = await fetch(url, {
headers: {
Authorization: authHeader,
"Content-Type": "application/json",
},
});

if (!response.ok) {
throw new Error(
`Failed to fetch test runs: ${response.status} ${response.statusText}`,
);
}

const data = (await response.json()) as TestRun;

// Extract failed IDs from current page
if (data.hierarchy && data.hierarchy.length > 0) {
const currentFailedTests = extractFailedTestIds(data.hierarchy);
allFailedTests = allFailedTests.concat(currentFailedTests);
}

// Check for pagination termination conditions
if (!data.pagination?.has_next || !data.pagination.next_page) {
break;
}

// Safety limit to prevent runaway requests
if (requestNumber >= 5) {
break;
}

// Prepare next request
url = `${baseUrl}?next_page=${encodeURIComponent(data.pagination.next_page)}`;
}

// Return unique failed test IDs
return allFailedTests;
} catch (error) {
logger.error("Error fetching failed tests:", error);
throw error;
}
}

// Recursive function to extract failed test IDs from hierarchy
function extractFailedTestIds(hierarchy: TestDetails[]): FailedTestInfo[] {
let failedTests: FailedTestInfo[] = [];

for (const node of hierarchy) {
if (node.details?.status === "failed" && node.details?.run_count) {
if (node.details?.observability_url) {
const idMatch = node.details.observability_url.match(/details=(\d+)/);
if (idMatch) {
failedTests.push({
test_id: idMatch[1],
test_name: node.display_name || `Test ${idMatch[1]}`,
});
}
}
}

if (node.children && node.children.length > 0) {
failedTests = failedTests.concat(extractFailedTestIds(node.children));
}
}

return failedTests;
}
Loading