@browserstack/mcp-server 1.2.3 → 1.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. package/README.md +88 -2
  2. package/dist/lib/device-cache.js +20 -17
  3. package/dist/lib/inmemory-store.d.ts +1 -0
  4. package/dist/lib/inmemory-store.js +1 -0
  5. package/dist/lib/utils.d.ts +5 -0
  6. package/dist/lib/utils.js +27 -0
  7. package/dist/server-factory.js +6 -0
  8. package/dist/tools/add-percy-snapshots.d.ts +5 -0
  9. package/dist/tools/add-percy-snapshots.js +17 -0
  10. package/dist/tools/appautomate-utils/appium-sdk/config-generator.d.ts +1 -0
  11. package/dist/tools/appautomate-utils/appium-sdk/config-generator.js +50 -0
  12. package/dist/tools/appautomate-utils/appium-sdk/constants.d.ts +23 -0
  13. package/dist/tools/appautomate-utils/appium-sdk/constants.js +43 -0
  14. package/dist/tools/appautomate-utils/appium-sdk/formatter.d.ts +8 -0
  15. package/dist/tools/appautomate-utils/appium-sdk/formatter.js +59 -0
  16. package/dist/tools/appautomate-utils/appium-sdk/handler.d.ts +3 -0
  17. package/dist/tools/appautomate-utils/appium-sdk/handler.js +52 -0
  18. package/dist/tools/appautomate-utils/appium-sdk/index.d.ts +7 -0
  19. package/dist/tools/appautomate-utils/appium-sdk/index.js +8 -0
  20. package/dist/tools/appautomate-utils/appium-sdk/instructions.d.ts +3 -0
  21. package/dist/tools/appautomate-utils/appium-sdk/instructions.js +47 -0
  22. package/dist/tools/appautomate-utils/appium-sdk/languages/csharp.d.ts +2 -0
  23. package/dist/tools/appautomate-utils/appium-sdk/languages/csharp.js +78 -0
  24. package/dist/tools/appautomate-utils/appium-sdk/languages/java.d.ts +8 -0
  25. package/dist/tools/appautomate-utils/appium-sdk/languages/java.js +87 -0
  26. package/dist/tools/appautomate-utils/appium-sdk/languages/nodejs.d.ts +3 -0
  27. package/dist/tools/appautomate-utils/appium-sdk/languages/nodejs.js +194 -0
  28. package/dist/tools/appautomate-utils/appium-sdk/languages/python.d.ts +3 -0
  29. package/dist/tools/appautomate-utils/appium-sdk/languages/python.js +76 -0
  30. package/dist/tools/appautomate-utils/appium-sdk/languages/ruby.d.ts +2 -0
  31. package/dist/tools/appautomate-utils/appium-sdk/languages/ruby.js +85 -0
  32. package/dist/tools/appautomate-utils/appium-sdk/types.d.ts +57 -0
  33. package/dist/tools/appautomate-utils/appium-sdk/types.js +54 -0
  34. package/dist/tools/appautomate-utils/appium-sdk/utils.d.ts +17 -0
  35. package/dist/tools/appautomate-utils/appium-sdk/utils.js +64 -0
  36. package/dist/tools/appautomate-utils/{appautomate.d.ts → native-execution/appautomate.d.ts} +1 -1
  37. package/dist/tools/appautomate-utils/{appautomate.js → native-execution/appautomate.js} +2 -2
  38. package/dist/tools/appautomate-utils/native-execution/constants.d.ts +10 -0
  39. package/dist/tools/appautomate-utils/native-execution/constants.js +36 -0
  40. package/dist/tools/appautomate-utils/native-execution/types.d.ts +19 -0
  41. package/dist/tools/appautomate-utils/{types.js → native-execution/types.js} +5 -1
  42. package/dist/tools/appautomate.js +25 -40
  43. package/dist/tools/bstack-sdk.d.ts +2 -15
  44. package/dist/tools/bstack-sdk.js +10 -119
  45. package/dist/tools/build-insights.d.ts +7 -0
  46. package/dist/tools/build-insights.js +67 -0
  47. package/dist/tools/list-test-files.d.ts +2 -0
  48. package/dist/tools/list-test-files.js +36 -0
  49. package/dist/tools/percy-sdk.d.ts +4 -0
  50. package/dist/tools/percy-sdk.js +71 -0
  51. package/dist/tools/percy-snapshot-utils/constants.d.ts +16 -0
  52. package/dist/tools/percy-snapshot-utils/constants.js +500 -0
  53. package/dist/tools/percy-snapshot-utils/detect-test-files.d.ts +10 -0
  54. package/dist/tools/percy-snapshot-utils/detect-test-files.js +175 -0
  55. package/dist/tools/percy-snapshot-utils/types.d.ts +15 -0
  56. package/dist/tools/percy-snapshot-utils/utils.d.ts +4 -0
  57. package/dist/tools/percy-snapshot-utils/utils.js +30 -0
  58. package/dist/tools/rca-agent-utils/constants.d.ts +13 -0
  59. package/dist/tools/rca-agent-utils/constants.js +24 -0
  60. package/dist/tools/rca-agent-utils/format-rca.d.ts +1 -0
  61. package/dist/tools/rca-agent-utils/format-rca.js +37 -0
  62. package/dist/tools/rca-agent-utils/get-build-id.d.ts +1 -0
  63. package/dist/tools/rca-agent-utils/get-build-id.js +18 -0
  64. package/dist/tools/rca-agent-utils/get-failed-test-id.d.ts +2 -0
  65. package/dist/tools/rca-agent-utils/get-failed-test-id.js +69 -0
  66. package/dist/tools/rca-agent-utils/rca-data.d.ts +9 -0
  67. package/dist/tools/rca-agent-utils/rca-data.js +196 -0
  68. package/dist/tools/rca-agent-utils/types.d.ts +48 -0
  69. package/dist/tools/rca-agent-utils/types.js +20 -0
  70. package/dist/tools/rca-agent.d.ts +14 -0
  71. package/dist/tools/rca-agent.js +119 -0
  72. package/dist/tools/review-agent-utils/build-counts.d.ts +7 -0
  73. package/dist/tools/review-agent-utils/build-counts.js +44 -0
  74. package/dist/tools/review-agent-utils/percy-approve-reject.d.ts +6 -0
  75. package/dist/tools/review-agent-utils/percy-approve-reject.js +39 -0
  76. package/dist/tools/review-agent-utils/percy-diffs.d.ts +9 -0
  77. package/dist/tools/review-agent-utils/percy-diffs.js +35 -0
  78. package/dist/tools/review-agent-utils/percy-snapshots.d.ts +11 -0
  79. package/dist/tools/review-agent-utils/percy-snapshots.js +58 -0
  80. package/dist/tools/review-agent.d.ts +5 -0
  81. package/dist/tools/review-agent.js +56 -0
  82. package/dist/tools/run-percy-scan.d.ts +8 -0
  83. package/dist/tools/run-percy-scan.js +37 -0
  84. package/dist/tools/sdk-utils/{commands.d.ts → bstack/commands.d.ts} +1 -1
  85. package/dist/tools/sdk-utils/bstack/commands.js +88 -0
  86. package/dist/tools/sdk-utils/bstack/configUtils.d.ts +4 -0
  87. package/dist/tools/sdk-utils/bstack/configUtils.js +66 -0
  88. package/dist/tools/sdk-utils/bstack/constants.d.ts +58 -0
  89. package/dist/tools/sdk-utils/{constants.js → bstack/constants.js} +117 -78
  90. package/dist/tools/sdk-utils/{constants.d.ts → bstack/frameworks.d.ts} +1 -1
  91. package/dist/tools/sdk-utils/bstack/frameworks.js +57 -0
  92. package/dist/tools/sdk-utils/bstack/index.d.ts +4 -0
  93. package/dist/tools/sdk-utils/bstack/index.js +5 -0
  94. package/dist/tools/sdk-utils/bstack/sdkHandler.d.ts +4 -0
  95. package/dist/tools/sdk-utils/bstack/sdkHandler.js +74 -0
  96. package/dist/tools/sdk-utils/common/constants.d.ts +10 -0
  97. package/dist/tools/sdk-utils/common/constants.js +86 -0
  98. package/dist/tools/sdk-utils/common/formatUtils.d.ts +5 -0
  99. package/dist/tools/sdk-utils/common/formatUtils.js +27 -0
  100. package/dist/tools/sdk-utils/common/index.d.ts +3 -0
  101. package/dist/tools/sdk-utils/common/index.js +4 -0
  102. package/dist/tools/sdk-utils/common/instructionUtils.d.ts +8 -0
  103. package/dist/tools/sdk-utils/common/instructionUtils.js +20 -0
  104. package/dist/tools/sdk-utils/common/schema.d.ts +73 -0
  105. package/dist/tools/sdk-utils/common/schema.js +51 -0
  106. package/dist/tools/sdk-utils/common/types.d.ts +66 -0
  107. package/dist/tools/sdk-utils/{types.js → common/types.js} +15 -2
  108. package/dist/tools/sdk-utils/common/utils.d.ts +25 -0
  109. package/dist/tools/sdk-utils/common/utils.js +90 -0
  110. package/dist/tools/sdk-utils/handler.d.ts +4 -0
  111. package/dist/tools/sdk-utils/handler.js +119 -0
  112. package/dist/tools/sdk-utils/percy-automate/constants.d.ts +11 -0
  113. package/dist/tools/sdk-utils/percy-automate/constants.js +338 -0
  114. package/dist/tools/sdk-utils/percy-automate/frameworks.d.ts +8 -0
  115. package/dist/tools/sdk-utils/percy-automate/frameworks.js +50 -0
  116. package/dist/tools/sdk-utils/percy-automate/handler.d.ts +3 -0
  117. package/dist/tools/sdk-utils/percy-automate/handler.js +30 -0
  118. package/dist/tools/sdk-utils/percy-automate/index.d.ts +1 -0
  119. package/dist/tools/sdk-utils/percy-automate/index.js +2 -0
  120. package/dist/tools/sdk-utils/percy-automate/types.d.ts +13 -0
  121. package/dist/tools/sdk-utils/percy-automate/types.js +1 -0
  122. package/dist/tools/sdk-utils/percy-bstack/constants.d.ts +4 -0
  123. package/dist/tools/sdk-utils/{percy → percy-bstack}/constants.js +13 -39
  124. package/dist/tools/sdk-utils/percy-bstack/frameworks.d.ts +2 -0
  125. package/dist/tools/sdk-utils/percy-bstack/frameworks.js +27 -0
  126. package/dist/tools/sdk-utils/percy-bstack/handler.d.ts +4 -0
  127. package/dist/tools/sdk-utils/percy-bstack/handler.js +99 -0
  128. package/dist/tools/sdk-utils/percy-bstack/index.d.ts +4 -0
  129. package/dist/tools/sdk-utils/percy-bstack/index.js +4 -0
  130. package/dist/tools/sdk-utils/percy-bstack/instructions.d.ts +7 -0
  131. package/dist/tools/sdk-utils/{percy → percy-bstack}/instructions.js +5 -9
  132. package/dist/tools/sdk-utils/percy-bstack/types.d.ts +13 -0
  133. package/dist/tools/sdk-utils/percy-bstack/types.js +5 -0
  134. package/dist/tools/sdk-utils/percy-web/constants.d.ts +41 -0
  135. package/dist/tools/sdk-utils/percy-web/constants.js +883 -0
  136. package/dist/tools/sdk-utils/percy-web/fetchPercyToken.d.ts +4 -0
  137. package/dist/tools/sdk-utils/percy-web/fetchPercyToken.js +32 -0
  138. package/dist/tools/sdk-utils/percy-web/frameworks.d.ts +7 -0
  139. package/dist/tools/sdk-utils/percy-web/frameworks.js +103 -0
  140. package/dist/tools/sdk-utils/percy-web/handler.d.ts +4 -0
  141. package/dist/tools/sdk-utils/percy-web/handler.js +27 -0
  142. package/dist/tools/sdk-utils/percy-web/index.d.ts +4 -0
  143. package/dist/tools/sdk-utils/percy-web/index.js +4 -0
  144. package/dist/tools/sdk-utils/percy-web/types.d.ts +12 -0
  145. package/dist/tools/sdk-utils/percy-web/types.js +1 -0
  146. package/dist/tools/testmanagement-utils/create-testrun.d.ts +4 -4
  147. package/dist/tools/testmanagement-utils/update-testrun.d.ts +4 -4
  148. package/package.json +2 -1
  149. package/dist/tools/appautomate-utils/types.d.ts +0 -5
  150. package/dist/tools/sdk-utils/commands.js +0 -65
  151. package/dist/tools/sdk-utils/instructions.d.ts +0 -6
  152. package/dist/tools/sdk-utils/instructions.js +0 -99
  153. package/dist/tools/sdk-utils/percy/constants.d.ts +0 -3
  154. package/dist/tools/sdk-utils/percy/instructions.d.ts +0 -10
  155. package/dist/tools/sdk-utils/percy/types.d.ts +0 -5
  156. package/dist/tools/sdk-utils/types.d.ts +0 -40
  157. /package/dist/tools/{sdk-utils/percy → percy-snapshot-utils}/types.js +0 -0
@@ -0,0 +1,4 @@
1
+ export declare function updateFileAndStep(file: string, idx: number, total: number, instruction: string): Promise<{
2
+ type: "text";
3
+ text: string;
4
+ }[]>;
@@ -0,0 +1,30 @@
1
+ const content = [];
2
+ export async function updateFileAndStep(file, idx, total, instruction) {
3
+ content.length = 0;
4
+ const nextIndex = idx + 1;
5
+ content.push({
6
+ type: "text",
7
+ text: `Complete all steps in order. If a tool call is requested, update the file first, then call the tool. Follow instructions exactly— do not skip any steps to ensure all files are updated.`,
8
+ });
9
+ content.push({
10
+ type: "text",
11
+ text: `Step 1 : You need to add percy snapshot commands in some key test cases in the file ${file} use the following instructions: \n${instruction}`,
12
+ });
13
+ content.push({
14
+ type: "text",
15
+ text: `Step 2 : Confirm that Percy snapshot commands have been added at all key points of visual change in the file ${file}.`,
16
+ });
17
+ if (nextIndex < total) {
18
+ content.push({
19
+ type: "text",
20
+ text: `Step 3 : Call the tool updateTestFileWithInstructions with index as ${nextIndex} out of ${total}`,
21
+ });
22
+ }
23
+ if (nextIndex === total) {
24
+ content.push({
25
+ type: "text",
26
+ text: `Step 3: Percy snapshot commands have been added to all files. You can now run the tool runPercyScan to run the percy scan.`,
27
+ });
28
+ }
29
+ return content;
30
+ }
@@ -0,0 +1,13 @@
1
+ import { z } from "zod";
2
+ import { TestStatus } from "./types.js";
3
+ export declare const FETCH_RCA_PARAMS: {
4
+ testId: z.ZodArray<z.ZodString, "many">;
5
+ };
6
+ export declare const GET_BUILD_ID_PARAMS: {
7
+ browserStackProjectName: z.ZodString;
8
+ browserStackBuildName: z.ZodString;
9
+ };
10
+ export declare const LIST_TEST_IDS_PARAMS: {
11
+ buildId: z.ZodString;
12
+ status: z.ZodNativeEnum<typeof TestStatus>;
13
+ };
@@ -0,0 +1,24 @@
1
+ import { z } from "zod";
2
+ import { TestStatus } from "./types.js";
3
+ export const FETCH_RCA_PARAMS = {
4
+ testId: z
5
+ .array(z.string())
6
+ .max(3)
7
+ .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."),
8
+ };
9
+ export const GET_BUILD_ID_PARAMS = {
10
+ browserStackProjectName: z
11
+ .string()
12
+ .describe("The BrowserStack project name used during test run creation. Action: First, check browserstack.yml or any equivalent project configuration files. If the project name is found, extract and return it. If it is not found or if there is any uncertainty, immediately prompt the user to provide the value. Do not infer, guess, or assume a default."),
13
+ browserStackBuildName: z
14
+ .string()
15
+ .describe("The BrowserStack build name used during test run creation. Action: First, check browserstack.yml or any equivalent project configuration files. If the build name is found, extract and return it. If it is not found or if there is any uncertainty, immediately prompt the user to provide the value. Do not infer, guess, or assume a default."),
16
+ };
17
+ export const LIST_TEST_IDS_PARAMS = {
18
+ buildId: z
19
+ .string()
20
+ .describe("The Browserstack Build ID of the test run. If not known, use the getBuildId tool to fetch it using project and build name"),
21
+ status: z
22
+ .nativeEnum(TestStatus)
23
+ .describe("Filter tests by status. If not provided, all tests are returned. Example for RCA usecase always use failed status"),
24
+ };
@@ -0,0 +1 @@
1
+ export declare function formatRCAData(rcaData: any): string;
@@ -0,0 +1,37 @@
1
+ // Utility function to format RCA data for better readability
2
+ export function formatRCAData(rcaData) {
3
+ if (!rcaData || !rcaData.testCases || rcaData.testCases.length === 0) {
4
+ return "No RCA data available.";
5
+ }
6
+ let output = "## Root Cause Analysis Report\n\n";
7
+ rcaData.testCases.forEach((testCase, index) => {
8
+ // Show test case ID with smaller heading
9
+ output += `### Test Case ${index + 1}\n`;
10
+ output += `**Test ID:** ${testCase.id}\n`;
11
+ output += `**Status:** ${testCase.state}\n\n`;
12
+ // Access RCA data from the correct path
13
+ const rca = testCase.rcaData?.rcaData;
14
+ if (rca) {
15
+ if (rca.root_cause) {
16
+ output += `**Root Cause:** ${rca.root_cause}\n\n`;
17
+ }
18
+ if (rca.failure_type) {
19
+ output += `**Failure Type:** ${rca.failure_type}\n\n`;
20
+ }
21
+ if (rca.description) {
22
+ output += `**Detailed Analysis:**\n${rca.description}\n\n`;
23
+ }
24
+ if (rca.possible_fix) {
25
+ output += `**Recommended Fix:**\n${rca.possible_fix}\n\n`;
26
+ }
27
+ }
28
+ else if (testCase.rcaData?.error) {
29
+ output += `**Error:** ${testCase.rcaData.error}\n\n`;
30
+ }
31
+ else if (testCase.state === "failed") {
32
+ output += `**Note:** RCA analysis failed or is not available for this test case.\n\n`;
33
+ }
34
+ output += "---\n\n";
35
+ });
36
+ return output;
37
+ }
@@ -0,0 +1 @@
1
+ export declare function getBuildId(projectName: string, buildName: string, username: string, accessKey: string): Promise<string>;
@@ -0,0 +1,18 @@
1
+ export async function getBuildId(projectName, buildName, username, accessKey) {
2
+ const url = new URL("https://api-automation.browserstack.com/ext/v1/builds/latest");
3
+ url.searchParams.append("project_name", projectName);
4
+ url.searchParams.append("build_name", buildName);
5
+ url.searchParams.append("user_name", username);
6
+ const authHeader = "Basic " + Buffer.from(`${username}:${accessKey}`).toString("base64");
7
+ const response = await fetch(url.toString(), {
8
+ headers: {
9
+ Authorization: authHeader,
10
+ "Content-Type": "application/json",
11
+ },
12
+ });
13
+ if (!response.ok) {
14
+ throw new Error(`Failed to fetch build ID: ${response.status} ${response.statusText}`);
15
+ }
16
+ const data = await response.json();
17
+ return data.build_id;
18
+ }
@@ -0,0 +1,2 @@
1
+ import { TestStatus, FailedTestInfo } from "./types.js";
2
+ export declare function getTestIds(buildId: string, authString: string, status?: TestStatus): Promise<FailedTestInfo[]>;
@@ -0,0 +1,69 @@
1
+ import logger from "../../logger.js";
2
+ export async function getTestIds(buildId, authString, status) {
3
+ const baseUrl = `https://api-automation.browserstack.com/ext/v1/builds/${buildId}/testRuns`;
4
+ let url = status ? `${baseUrl}?test_statuses=${status}` : baseUrl;
5
+ let allFailedTests = [];
6
+ let requestNumber = 0;
7
+ // Construct Basic auth header
8
+ const encodedCredentials = Buffer.from(authString).toString("base64");
9
+ const authHeader = `Basic ${encodedCredentials}`;
10
+ try {
11
+ while (true) {
12
+ requestNumber++;
13
+ const response = await fetch(url, {
14
+ headers: {
15
+ Authorization: authHeader,
16
+ "Content-Type": "application/json",
17
+ },
18
+ });
19
+ if (!response.ok) {
20
+ throw new Error(`Failed to fetch test runs: ${response.status} ${response.statusText}`);
21
+ }
22
+ const data = (await response.json());
23
+ // Extract failed IDs from current page
24
+ if (data.hierarchy && data.hierarchy.length > 0) {
25
+ const currentFailedTests = extractFailedTestIds(data.hierarchy, status);
26
+ allFailedTests = allFailedTests.concat(currentFailedTests);
27
+ }
28
+ // Check for pagination termination conditions
29
+ if (!data.pagination?.has_next ||
30
+ !data.pagination.next_page ||
31
+ requestNumber >= 5) {
32
+ break;
33
+ }
34
+ const params = {
35
+ next_page: data.pagination.next_page,
36
+ };
37
+ if (status)
38
+ params.test_statuses = status;
39
+ url = `${baseUrl}?${new URLSearchParams(params).toString()}`;
40
+ }
41
+ // Return unique failed test IDs
42
+ return allFailedTests;
43
+ }
44
+ catch (error) {
45
+ logger.error("Error fetching failed tests:", error);
46
+ throw error;
47
+ }
48
+ }
49
+ // Recursive function to extract failed test IDs from hierarchy
50
+ function extractFailedTestIds(hierarchy, status) {
51
+ let failedTests = [];
52
+ for (const node of hierarchy) {
53
+ if (node.details?.status === status && node.details?.run_count) {
54
+ if (node.details?.observability_url) {
55
+ const idMatch = node.details.observability_url.match(/details=(\d+)/);
56
+ if (idMatch) {
57
+ failedTests.push({
58
+ test_id: idMatch[1],
59
+ test_name: node.display_name || `Test ${idMatch[1]}`,
60
+ });
61
+ }
62
+ }
63
+ }
64
+ if (node.children && node.children.length > 0) {
65
+ failedTests = failedTests.concat(extractFailedTestIds(node.children, status));
66
+ }
67
+ }
68
+ return failedTests;
69
+ }
@@ -0,0 +1,9 @@
1
+ import { RCAResponse } from "./types.js";
2
+ interface ScanProgressContext {
3
+ sendNotification: (notification: any) => Promise<void>;
4
+ _meta?: {
5
+ progressToken?: string | number;
6
+ };
7
+ }
8
+ export declare function getRCAData(testIds: string[], authString: string, context?: ScanProgressContext): Promise<RCAResponse>;
9
+ export {};
@@ -0,0 +1,196 @@
1
+ import { RCAState } from "./types.js";
2
+ const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
3
+ function isInProgressState(state) {
4
+ return [
5
+ RCAState.PENDING,
6
+ RCAState.FETCHING_LOGS,
7
+ RCAState.GENERATING_RCA,
8
+ RCAState.GENERATED_RCA,
9
+ ].includes(state);
10
+ }
11
+ function isFailedState(state) {
12
+ return [
13
+ RCAState.FAILED,
14
+ RCAState.LLM_SERVICE_ERROR,
15
+ RCAState.LOG_FETCH_ERROR,
16
+ RCAState.UNKNOWN_ERROR,
17
+ RCAState.TIMEOUT,
18
+ ].includes(state);
19
+ }
20
+ function calculateProgress(resolvedCount, totalCount, baseProgress = 10) {
21
+ if (totalCount === 0)
22
+ return 100; // ✅ fix divide by zero
23
+ const progressRange = 90 - baseProgress;
24
+ const completionProgress = (resolvedCount / totalCount) * progressRange;
25
+ return Math.min(100, baseProgress + completionProgress);
26
+ }
27
+ // ✅ centralized mapping function
28
+ function mapApiState(apiState) {
29
+ const state = apiState?.toLowerCase();
30
+ switch (state) {
31
+ case "completed":
32
+ return RCAState.COMPLETED;
33
+ case "pending":
34
+ return RCAState.PENDING;
35
+ case "fetching_logs":
36
+ return RCAState.FETCHING_LOGS;
37
+ case "generating_rca":
38
+ return RCAState.GENERATING_RCA;
39
+ case "generated_rca":
40
+ return RCAState.GENERATED_RCA;
41
+ case "error":
42
+ return RCAState.UNKNOWN_ERROR;
43
+ default:
44
+ return RCAState.UNKNOWN_ERROR;
45
+ }
46
+ }
47
+ async function notifyProgress(context, message, progress) {
48
+ if (!context?.sendNotification)
49
+ return;
50
+ await context.sendNotification({
51
+ method: "notifications/progress",
52
+ params: {
53
+ progressToken: context._meta?.progressToken?.toString(),
54
+ message,
55
+ progress,
56
+ total: 100,
57
+ },
58
+ });
59
+ }
60
+ async function updateProgress(context, testCases, message) {
61
+ const inProgressCases = testCases.filter((tc) => isInProgressState(tc.state));
62
+ const resolvedCount = testCases.length - inProgressCases.length;
63
+ await notifyProgress(context, message ??
64
+ `RCA analysis in progress (${resolvedCount}/${testCases.length} resolved)`, inProgressCases.length === 0
65
+ ? 100
66
+ : calculateProgress(resolvedCount, testCases.length));
67
+ }
68
+ async function fetchInitialRCA(testId, headers, baseUrl) {
69
+ const url = baseUrl.replace("{testId}", testId);
70
+ try {
71
+ const response = await fetch(url, { headers });
72
+ if (!response.ok) {
73
+ return {
74
+ id: testId,
75
+ testRunId: testId,
76
+ state: RCAState.LOG_FETCH_ERROR,
77
+ rcaData: {
78
+ error: `HTTP ${response.status}: Failed to start RCA analysis`,
79
+ },
80
+ };
81
+ }
82
+ const data = await response.json();
83
+ const resultState = mapApiState(data.state);
84
+ return {
85
+ id: testId,
86
+ testRunId: testId,
87
+ state: resultState,
88
+ ...(resultState === RCAState.COMPLETED && { rcaData: data }),
89
+ ...(isFailedState(resultState) &&
90
+ data.state && {
91
+ rcaData: {
92
+ error: `API returned state: ${data.state}`,
93
+ originalResponse: data,
94
+ },
95
+ }),
96
+ };
97
+ }
98
+ catch (error) {
99
+ return {
100
+ id: testId,
101
+ testRunId: testId,
102
+ state: RCAState.LLM_SERVICE_ERROR,
103
+ rcaData: {
104
+ error: error instanceof Error ? error.message : "Network or parsing error",
105
+ },
106
+ };
107
+ }
108
+ }
109
+ async function pollRCAResults(testCases, headers, baseUrl, context, pollInterval, timeout, initialDelay) {
110
+ const startTime = Date.now();
111
+ await delay(initialDelay);
112
+ try {
113
+ while (true) {
114
+ const inProgressCases = testCases.filter((tc) => isInProgressState(tc.state));
115
+ await updateProgress(context, testCases);
116
+ if (inProgressCases.length === 0)
117
+ break;
118
+ if (Date.now() - startTime >= timeout) {
119
+ inProgressCases.forEach((tc) => {
120
+ tc.state = RCAState.TIMEOUT;
121
+ tc.rcaData = { error: `Timeout after ${timeout}ms` };
122
+ });
123
+ await updateProgress(context, testCases, "RCA analysis timed out");
124
+ break;
125
+ }
126
+ await Promise.allSettled(inProgressCases.map(async (tc) => {
127
+ try {
128
+ const pollUrl = baseUrl.replace("{testId}", tc.id);
129
+ const response = await fetch(pollUrl, { headers });
130
+ if (!response.ok) {
131
+ const errorText = await response.text();
132
+ tc.state = RCAState.LOG_FETCH_ERROR;
133
+ tc.rcaData = {
134
+ error: `HTTP ${response.status}: Polling failed - ${errorText}`,
135
+ };
136
+ return;
137
+ }
138
+ const data = await response.json();
139
+ if (!isFailedState(tc.state)) {
140
+ const mappedState = mapApiState(data.state);
141
+ tc.state = mappedState;
142
+ if (mappedState === RCAState.COMPLETED) {
143
+ tc.rcaData = data;
144
+ }
145
+ else if (mappedState === RCAState.UNKNOWN_ERROR) {
146
+ tc.rcaData = {
147
+ error: `API returned state: ${data.state}`,
148
+ originalResponse: data,
149
+ };
150
+ }
151
+ }
152
+ }
153
+ catch (err) {
154
+ if (!isFailedState(tc.state)) {
155
+ tc.state = RCAState.LLM_SERVICE_ERROR;
156
+ tc.rcaData = {
157
+ error: err instanceof Error
158
+ ? err.message
159
+ : "Network or parsing error",
160
+ };
161
+ }
162
+ }
163
+ }));
164
+ await delay(pollInterval);
165
+ }
166
+ }
167
+ catch (err) {
168
+ testCases
169
+ .filter((tc) => isInProgressState(tc.state))
170
+ .forEach((tc) => {
171
+ tc.state = RCAState.UNKNOWN_ERROR;
172
+ tc.rcaData = {
173
+ error: err instanceof Error ? err.message : "Unexpected error",
174
+ };
175
+ });
176
+ await updateProgress(context, testCases, "RCA analysis failed due to unexpected error");
177
+ }
178
+ return { testCases };
179
+ }
180
+ export async function getRCAData(testIds, authString, context) {
181
+ const pollInterval = 5000;
182
+ const timeout = 40000;
183
+ const initialDelay = 20000;
184
+ const baseUrl = "https://api-observability.browserstack.com/ext/v1/testRun/{testId}/testRca";
185
+ const headers = {
186
+ Authorization: `Basic ${Buffer.from(authString).toString("base64")}`,
187
+ "Content-Type": "application/json",
188
+ };
189
+ await notifyProgress(context, "Starting RCA analysis for test cases...", 0);
190
+ const testCases = await Promise.all(testIds.map((testId) => fetchInitialRCA(testId, headers, baseUrl)));
191
+ const inProgressCount = testCases.filter((tc) => isInProgressState(tc.state)).length;
192
+ await notifyProgress(context, `Initial RCA requests completed. ${inProgressCount} cases pending analysis...`, 10);
193
+ if (inProgressCount === 0)
194
+ return { testCases };
195
+ return await pollRCAResults(testCases, headers, baseUrl, context, pollInterval, timeout, initialDelay);
196
+ }
@@ -0,0 +1,48 @@
1
+ export declare enum TestStatus {
2
+ PASSED = "passed",
3
+ FAILED = "failed",
4
+ PENDING = "pending",
5
+ SKIPPED = "skipped"
6
+ }
7
+ export interface TestDetails {
8
+ status: TestStatus;
9
+ details: any;
10
+ children?: TestDetails[];
11
+ display_name?: string;
12
+ }
13
+ export interface TestRun {
14
+ hierarchy: TestDetails[];
15
+ pagination?: {
16
+ has_next: boolean;
17
+ next_page: string | null;
18
+ };
19
+ }
20
+ export interface FailedTestInfo {
21
+ test_id: string;
22
+ test_name: string;
23
+ }
24
+ export declare enum RCAState {
25
+ PENDING = "pending",
26
+ FETCHING_LOGS = "fetching_logs",
27
+ GENERATING_RCA = "generating_rca",
28
+ GENERATED_RCA = "generated_rca",
29
+ COMPLETED = "completed",
30
+ FAILED = "failed",
31
+ LLM_SERVICE_ERROR = "LLM_SERVICE_ERROR",
32
+ LOG_FETCH_ERROR = "LOG_FETCH_ERROR",
33
+ UNKNOWN_ERROR = "UNKNOWN_ERROR",
34
+ TIMEOUT = "TIMEOUT"
35
+ }
36
+ export interface RCATestCase {
37
+ id: string;
38
+ testRunId: string;
39
+ state: RCAState;
40
+ rcaData?: any;
41
+ }
42
+ export interface RCAResponse {
43
+ testCases: RCATestCase[];
44
+ }
45
+ export interface BuildIdArgs {
46
+ browserStackProjectName: string;
47
+ browserStackBuildName: string;
48
+ }
@@ -0,0 +1,20 @@
1
+ export var TestStatus;
2
+ (function (TestStatus) {
3
+ TestStatus["PASSED"] = "passed";
4
+ TestStatus["FAILED"] = "failed";
5
+ TestStatus["PENDING"] = "pending";
6
+ TestStatus["SKIPPED"] = "skipped";
7
+ })(TestStatus || (TestStatus = {}));
8
+ export var RCAState;
9
+ (function (RCAState) {
10
+ RCAState["PENDING"] = "pending";
11
+ RCAState["FETCHING_LOGS"] = "fetching_logs";
12
+ RCAState["GENERATING_RCA"] = "generating_rca";
13
+ RCAState["GENERATED_RCA"] = "generated_rca";
14
+ RCAState["COMPLETED"] = "completed";
15
+ RCAState["FAILED"] = "failed";
16
+ RCAState["LLM_SERVICE_ERROR"] = "LLM_SERVICE_ERROR";
17
+ RCAState["LOG_FETCH_ERROR"] = "LOG_FETCH_ERROR";
18
+ RCAState["UNKNOWN_ERROR"] = "UNKNOWN_ERROR";
19
+ RCAState["TIMEOUT"] = "TIMEOUT";
20
+ })(RCAState || (RCAState = {}));
@@ -0,0 +1,14 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
3
+ import { BrowserStackConfig } from "../lib/types.js";
4
+ import { TestStatus } from "./rca-agent-utils/types.js";
5
+ import { BuildIdArgs } from "./rca-agent-utils/types.js";
6
+ export declare function getBuildIdTool(args: BuildIdArgs, config: BrowserStackConfig): Promise<CallToolResult>;
7
+ export declare function fetchRCADataTool(args: {
8
+ testId: string[];
9
+ }, config: BrowserStackConfig): Promise<CallToolResult>;
10
+ export declare function listTestIdsTool(args: {
11
+ buildId: string;
12
+ status?: TestStatus;
13
+ }, config: BrowserStackConfig): Promise<CallToolResult>;
14
+ export default function addRCATools(server: McpServer, config: BrowserStackConfig): Record<string, any>;
@@ -0,0 +1,119 @@
1
+ import logger from "../logger.js";
2
+ import { getBrowserStackAuth } from "../lib/get-auth.js";
3
+ import { getBuildId } from "./rca-agent-utils/get-build-id.js";
4
+ import { getTestIds } from "./rca-agent-utils/get-failed-test-id.js";
5
+ import { getRCAData } from "./rca-agent-utils/rca-data.js";
6
+ import { formatRCAData } from "./rca-agent-utils/format-rca.js";
7
+ import { handleMCPError } from "../lib/utils.js";
8
+ import { trackMCP } from "../index.js";
9
+ import { FETCH_RCA_PARAMS, GET_BUILD_ID_PARAMS, LIST_TEST_IDS_PARAMS, } from "./rca-agent-utils/constants.js";
10
+ // Tool function to fetch build ID
11
+ export async function getBuildIdTool(args, config) {
12
+ try {
13
+ const { browserStackProjectName, browserStackBuildName } = args;
14
+ const authString = getBrowserStackAuth(config);
15
+ const [username, accessKey] = authString.split(":");
16
+ const buildId = await getBuildId(browserStackProjectName, browserStackBuildName, username, accessKey);
17
+ return {
18
+ content: [
19
+ {
20
+ type: "text",
21
+ text: buildId,
22
+ },
23
+ ],
24
+ };
25
+ }
26
+ catch (error) {
27
+ logger.error("Error fetching build ID", error);
28
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
29
+ return {
30
+ content: [
31
+ {
32
+ type: "text",
33
+ text: `Error fetching build ID: ${errorMessage}`,
34
+ },
35
+ ],
36
+ };
37
+ }
38
+ }
39
+ // Tool function that fetches RCA data
40
+ export async function fetchRCADataTool(args, config) {
41
+ try {
42
+ const authString = getBrowserStackAuth(config);
43
+ // Limit to first 3 test IDs for performance
44
+ const testIds = args.testId;
45
+ const rcaData = await getRCAData(testIds, authString);
46
+ const formattedData = formatRCAData(rcaData);
47
+ return {
48
+ content: [
49
+ {
50
+ type: "text",
51
+ text: formattedData,
52
+ },
53
+ ],
54
+ };
55
+ }
56
+ catch (error) {
57
+ logger.error("Error fetching RCA data", error);
58
+ throw error;
59
+ }
60
+ }
61
+ export async function listTestIdsTool(args, config) {
62
+ try {
63
+ const { buildId, status } = args;
64
+ const authString = getBrowserStackAuth(config);
65
+ // Get test IDs
66
+ const testIds = await getTestIds(buildId, authString, status);
67
+ return {
68
+ content: [
69
+ {
70
+ type: "text",
71
+ text: JSON.stringify(testIds, null, 2),
72
+ },
73
+ ],
74
+ };
75
+ }
76
+ catch (error) {
77
+ logger.error("Error listing test IDs", error);
78
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
79
+ return {
80
+ content: [
81
+ {
82
+ type: "text",
83
+ text: `Error listing test IDs: ${errorMessage}`,
84
+ },
85
+ ],
86
+ };
87
+ }
88
+ }
89
+ export default function addRCATools(server, config) {
90
+ const tools = {};
91
+ tools.fetchRCA = server.tool("fetchRCA", "Retrieves AI-RCA (Root Cause Analysis) data for a BrowserStack Automate and App-Automate session and provides insights into test failures.", FETCH_RCA_PARAMS, async (args) => {
92
+ try {
93
+ trackMCP("fetchRCA", server.server.getClientVersion(), config);
94
+ return await fetchRCADataTool(args, config);
95
+ }
96
+ catch (error) {
97
+ return handleMCPError("fetchRCA", server, config, error);
98
+ }
99
+ });
100
+ tools.getBuildId = server.tool("getBuildId", "Get the BrowserStack build ID for a given project and build name.", GET_BUILD_ID_PARAMS, async (args) => {
101
+ try {
102
+ trackMCP("getBuildId", server.server.getClientVersion(), config);
103
+ return await getBuildIdTool(args, config);
104
+ }
105
+ catch (error) {
106
+ return handleMCPError("getBuildId", server, config, error);
107
+ }
108
+ });
109
+ tools.listTestIds = server.tool("listTestIds", "List test IDs from a BrowserStack Automate build, optionally filtered by status", LIST_TEST_IDS_PARAMS, async (args) => {
110
+ try {
111
+ trackMCP("listTestIds", server.server.getClientVersion(), config);
112
+ return await listTestIdsTool(args, config);
113
+ }
114
+ catch (error) {
115
+ return handleMCPError("listTestIds", server, config, error);
116
+ }
117
+ });
118
+ return tools;
119
+ }
@@ -0,0 +1,7 @@
1
+ export declare function getPercyBuildCount(percyToken: string): Promise<{
2
+ noBuilds: boolean;
3
+ isFirstBuild: boolean;
4
+ lastBuildId: string | undefined;
5
+ orgId: string | undefined;
6
+ browserIds: string[];
7
+ }>;