@browserstack/mcp-server 1.1.5 → 1.1.7

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.
package/dist/config.js CHANGED
@@ -1,12 +1,44 @@
1
+ // List of supported BrowserStack Local option names (as per SDK)
2
+ const BROWSERSTACK_LOCAL_OPTION_KEYS = [
3
+ "proxyHost",
4
+ "proxyPort",
5
+ "proxyUser",
6
+ "proxyPass",
7
+ "useCaCertificate",
8
+ "localProxyHost",
9
+ "localProxyPort",
10
+ "localProxyUser",
11
+ "localProxyPass",
12
+ "pacFile",
13
+ "force",
14
+ "forceLocal",
15
+ "onlyAutomate",
16
+ "verbose",
17
+ "logFile",
18
+ "binarypath",
19
+ "f",
20
+ "excludeHosts",
21
+ ];
22
+ // Build browserstackLocalOptions from individual env vars
23
+ const browserstackLocalOptions = {};
24
+ for (const key of BROWSERSTACK_LOCAL_OPTION_KEYS) {
25
+ // Env var name: BROWSERSTACK_LOCAL_OPTION_<UPPERCASE_KEY>
26
+ const envVar = process.env[`BROWSERSTACK_LOCAL_OPTION_${key.toUpperCase()}`];
27
+ if (envVar !== undefined) {
28
+ browserstackLocalOptions[key] = envVar;
29
+ }
30
+ }
1
31
  export class Config {
2
32
  browserstackUsername;
3
33
  browserstackAccessKey;
4
34
  DEV_MODE;
5
- constructor(browserstackUsername, browserstackAccessKey, DEV_MODE) {
35
+ browserstackLocalOptions;
36
+ constructor(browserstackUsername, browserstackAccessKey, DEV_MODE, browserstackLocalOptions) {
6
37
  this.browserstackUsername = browserstackUsername;
7
38
  this.browserstackAccessKey = browserstackAccessKey;
8
39
  this.DEV_MODE = DEV_MODE;
40
+ this.browserstackLocalOptions = browserstackLocalOptions;
9
41
  }
10
42
  }
11
- const config = new Config(process.env.BROWSERSTACK_USERNAME, process.env.BROWSERSTACK_ACCESS_KEY, process.env.DEV_MODE === "true");
43
+ const config = new Config(process.env.BROWSERSTACK_USERNAME, process.env.BROWSERSTACK_ACCESS_KEY, process.env.DEV_MODE === "true", browserstackLocalOptions);
12
44
  export default config;
package/dist/index.js CHANGED
@@ -14,6 +14,7 @@ import addTestManagementTools from "./tools/testmanagement.js";
14
14
  import addAppAutomationTools from "./tools/appautomate.js";
15
15
  import addFailureLogsTools from "./tools/getFailureLogs.js";
16
16
  import addAutomateTools from "./tools/automate.js";
17
+ import addSelfHealTools from "./tools/selfheal.js";
17
18
  import { setupOnInitialized } from "./oninitialized.js";
18
19
  function registerTools(server) {
19
20
  addSDKTools(server);
@@ -24,6 +25,7 @@ function registerTools(server) {
24
25
  addAppAutomationTools(server);
25
26
  addFailureLogsTools(server);
26
27
  addAutomateTools(server);
28
+ addSelfHealTools(server);
27
29
  }
28
30
  // Create an MCP server
29
31
  const server = new McpServer({
package/dist/lib/local.js CHANGED
@@ -69,15 +69,17 @@ export async function ensureLocalBinarySetup(localIdentifier) {
69
69
  logger.info("Ensuring local binary setup as it is required for private URLs...");
70
70
  const localBinary = new Local();
71
71
  await killExistingBrowserStackLocalProcesses();
72
- const requestBody = {
72
+ // Use a single options object from config and extend with required fields
73
+ const bsLocalArgs = {
74
+ ...(config.browserstackLocalOptions || {}),
73
75
  key: config.browserstackAccessKey,
74
76
  username: config.browserstackUsername,
75
77
  };
76
78
  if (localIdentifier) {
77
- requestBody.localIdentifier = localIdentifier;
79
+ bsLocalArgs.localIdentifier = localIdentifier;
78
80
  }
79
81
  return await new Promise((resolve, reject) => {
80
- localBinary.start(requestBody, (error) => {
82
+ localBinary.start(bsLocalArgs, (error) => {
81
83
  if (error) {
82
84
  logger.error(`Unable to start BrowserStack Local... please check your credentials and try again. Error: ${error}`);
83
85
  reject(new Error(`Unable to configure local tunnel binary, please check your credentials and try again. Error: ${error}`));
@@ -56,6 +56,9 @@ export class AccessibilityScanner {
56
56
  }
57
57
  catch (err) {
58
58
  if (axios.isAxiosError(err) && err.response?.data) {
59
+ if (err.response.status === 422) {
60
+ throw new Error("A scan with this name already exists. please update the name and run again.");
61
+ }
59
62
  const msg = err.response.data.error ||
60
63
  err.response.data.message ||
61
64
  err.message;
@@ -47,13 +47,19 @@ async function takeAppScreenshot(args) {
47
47
  },
48
48
  };
49
49
  logger.info("Starting WebDriver session on BrowserStack...");
50
- driver = await remote({
51
- protocol: "https",
52
- hostname: "hub.browserstack.com",
53
- port: 443,
54
- path: "/wd/hub",
55
- capabilities,
56
- });
50
+ try {
51
+ driver = await remote({
52
+ protocol: "https",
53
+ hostname: "hub.browserstack.com",
54
+ port: 443,
55
+ path: "/wd/hub",
56
+ capabilities,
57
+ });
58
+ }
59
+ catch (error) {
60
+ logger.error("Error initializing WebDriver:", error);
61
+ throw new Error("Failed to initialize the WebDriver or a timeout occurred. Please try again.");
62
+ }
57
63
  const screenshotBase64 = await driver.takeScreenshot();
58
64
  const compressed = await maybeCompressBase64(screenshotBase64);
59
65
  return {
@@ -11,7 +11,7 @@ export function findDeviceByName(devices, desiredPhone) {
11
11
  }
12
12
  // Exact-case-insensitive filter
13
13
  const exact = matches.filter((m) => m.display_name.toLowerCase() === desiredPhone.toLowerCase());
14
- if (exact)
14
+ if (exact.length)
15
15
  return exact;
16
16
  // If no exact but multiple fuzzy, ask user
17
17
  if (matches.length > 1) {
@@ -38,6 +38,9 @@ export async function startSession(args) {
38
38
  // 6) Upload app
39
39
  const { app_url } = await uploadApp(appPath);
40
40
  logger.info(`App uploaded: ${app_url}`);
41
+ if (!app_url) {
42
+ throw new Error("Failed to upload app. Please try again.");
43
+ }
41
44
  // 7) Build URL & open
42
45
  const deviceParam = sanitizeUrlParam(selected.display_name.replace(/\s+/g, "+"));
43
46
  const params = new URLSearchParams({
@@ -29,6 +29,43 @@ In order to run tests on BrowserStack, run the following command:
29
29
  browserstack-sdk python <path-to-test-file>
30
30
  \`\`\`
31
31
  `;
32
+ const argsInstruction = '<argLine>-javaagent:"${com.browserstack:browserstack-java-sdk:jar}"</argLine>';
33
+ const javaInstructions = `
34
+ Strictly Add the following dependencies to your \`pom.xml\`:
35
+ \`\`\`xml
36
+ <dependency>
37
+ <groupId>com.browserstack</groupId>
38
+ <artifactId>browserstack-java-sdk</artifactId>
39
+ <version>LATEST</version>
40
+ <scope>compile</scope>
41
+ </dependency>
42
+
43
+ ${argsInstruction}
44
+ \`\`\`
45
+
46
+ For Gradle projects, add to \`build.gradle\`:
47
+ \`\`\`groovy
48
+ dependencies {
49
+ implementation 'com.browserstack:browserstack-java-sdk:LATEST'
50
+ }
51
+ \`\`\`
52
+
53
+ Inform user to export:
54
+ \`\`\`bash
55
+ export BROWSERSTACK_USERNAME=${config.browserstackUsername}
56
+ export BROWSERSTACK_ACCESS_KEY=${config.browserstackAccessKey}
57
+ \`\`\`
58
+
59
+ Run tests using:
60
+ \`\`\`bash
61
+ mvn clean test
62
+ \`\`\`
63
+
64
+ Or for Gradle:
65
+ \`\`\`bash
66
+ gradle clean test
67
+ \`\`\`
68
+ `;
32
69
  export const SUPPORTED_CONFIGURATIONS = {
33
70
  nodejs: {
34
71
  playwright: {
@@ -55,4 +92,12 @@ export const SUPPORTED_CONFIGURATIONS = {
55
92
  behave: { instructions: pythonInstructions },
56
93
  },
57
94
  },
95
+ java: {
96
+ playwright: {},
97
+ selenium: {
98
+ testng: { instructions: javaInstructions },
99
+ cucumber: { instructions: javaInstructions },
100
+ junit: { instructions: javaInstructions },
101
+ },
102
+ },
58
103
  };
@@ -0,0 +1,51 @@
1
+ import { assertOkResponse } from "../../lib/utils.js";
2
+ import config from "../../config.js";
3
+ export async function getSelfHealSelectors(sessionId) {
4
+ const credentials = `${config.browserstackUsername}:${config.browserstackAccessKey}`;
5
+ const auth = Buffer.from(credentials).toString("base64");
6
+ const url = `https://api.browserstack.com/automate/sessions/${sessionId}/logs`;
7
+ const response = await fetch(url, {
8
+ headers: {
9
+ "Content-Type": "application/json",
10
+ Authorization: `Basic ${auth}`,
11
+ },
12
+ });
13
+ await assertOkResponse(response, "session logs");
14
+ const logText = await response.text();
15
+ return extractHealedSelectors(logText);
16
+ }
17
+ function extractHealedSelectors(logText) {
18
+ // Split log text into lines for easier context handling
19
+ const logLines = logText.split("\n");
20
+ // Pattern to match successful SELFHEAL entries only
21
+ const selfhealPattern = /SELFHEAL\s*{\s*"status":"true",\s*"data":\s*{\s*"using":"css selector",\s*"value":"(.*?)"}/;
22
+ // Pattern to match preceding selector requests
23
+ const requestPattern = /POST \/session\/[^/]+\/element.*?"using":"css selector","value":"(.*?)"/;
24
+ // Find all successful healed selectors with their line numbers and context
25
+ const healedMappings = [];
26
+ for (let i = 0; i < logLines.length; i++) {
27
+ const match = logLines[i].match(selfhealPattern);
28
+ if (match) {
29
+ const beforeLine = i > 0 ? logLines[i - 1] : "";
30
+ const afterLine = i < logLines.length - 1 ? logLines[i + 1] : "";
31
+ // Look backwards to find the most recent original selector request
32
+ let originalSelector = "UNKNOWN";
33
+ for (let j = i - 1; j >= 0; j--) {
34
+ const requestMatch = logLines[j].match(requestPattern);
35
+ if (requestMatch) {
36
+ originalSelector = requestMatch[1];
37
+ break;
38
+ }
39
+ }
40
+ healedMappings.push({
41
+ originalSelector,
42
+ healedSelector: match[1],
43
+ context: {
44
+ before: beforeLine,
45
+ after: afterLine,
46
+ },
47
+ });
48
+ }
49
+ }
50
+ return healedMappings;
51
+ }
@@ -0,0 +1,46 @@
1
+ import { z } from "zod";
2
+ import { getSelfHealSelectors } from "./selfheal-utils/selfheal.js";
3
+ import logger from "../logger.js";
4
+ import { trackMCP } from "../lib/instrumentation.js";
5
+ // Tool function that fetches self-healing selectors
6
+ export async function fetchSelfHealSelectorTool(args) {
7
+ try {
8
+ const selectors = await getSelfHealSelectors(args.sessionId);
9
+ return {
10
+ content: [
11
+ {
12
+ type: "text",
13
+ text: "Self-heal selectors fetched successfully" +
14
+ JSON.stringify(selectors),
15
+ },
16
+ ],
17
+ };
18
+ }
19
+ catch (error) {
20
+ logger.error("Error fetching self-heal selector suggestions", error);
21
+ throw error;
22
+ }
23
+ }
24
+ // Registers the fetchSelfHealSelector tool with the MCP server
25
+ export default function addSelfHealTools(server) {
26
+ server.tool("fetchSelfHealedSelectors", "Retrieves AI-generated, self-healed selectors for a BrowserStack Automate session to resolve flaky tests caused by dynamic DOM changes.", {
27
+ sessionId: z.string().describe("The session ID of the test run"),
28
+ }, async (args) => {
29
+ try {
30
+ trackMCP("fetchSelfHealedSelectors", server.server.getClientVersion());
31
+ return await fetchSelfHealSelectorTool(args);
32
+ }
33
+ catch (error) {
34
+ trackMCP("fetchSelfHealedSelectors", server.server.getClientVersion(), error);
35
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
36
+ return {
37
+ content: [
38
+ {
39
+ type: "text",
40
+ text: `Error during fetching self-heal suggestions: ${errorMessage}`,
41
+ },
42
+ ],
43
+ };
44
+ }
45
+ });
46
+ }
@@ -127,7 +127,7 @@ export async function pollScenariosTestDetails(args, traceId, context, documentI
127
127
  progressToken: context._meta?.progressToken ?? traceId,
128
128
  progress: count,
129
129
  total: count,
130
- message: `Fetched ${count} scenarios`,
130
+ message: `Generated ${count} scenarios`,
131
131
  },
132
132
  });
133
133
  }
@@ -157,7 +157,7 @@ export async function pollScenariosTestDetails(args, traceId, context, documentI
157
157
  progressToken: context._meta?.progressToken ?? traceId,
158
158
  progress: iteratorCount,
159
159
  total,
160
- message: `Fetched ${array.length} test cases for scenario ${iteratorCount} out of ${total}`,
160
+ message: `Generated ${array.length} test cases for scenario ${iteratorCount} out of ${total}`,
161
161
  },
162
162
  });
163
163
  }
@@ -214,7 +214,7 @@ export async function bulkCreateTestCases(scenariosMap, projectId, folderId, fie
214
214
  method: "notifications/progress",
215
215
  params: {
216
216
  progressToken: context._meta?.progressToken ?? "bulk-create",
217
- message: `Bulk create done for scenario ${doneCount} of ${total}`,
217
+ message: `Saving and creating test cases...`,
218
218
  total,
219
219
  progress: doneCount,
220
220
  },
@@ -226,7 +226,7 @@ export async function bulkCreateTestCases(scenariosMap, projectId, folderId, fie
226
226
  method: "notifications/progress",
227
227
  params: {
228
228
  progressToken: context._meta?.progressToken ?? traceId,
229
- message: `Bulk create failed for scenario ${id}: ${error instanceof Error ? error.message : "Unknown error"}`,
229
+ message: `Creation failed for scenario ${id}: ${error instanceof Error ? error.message : "Unknown error"}`,
230
230
  total,
231
231
  progress: doneCount,
232
232
  },
@@ -257,3 +257,42 @@ export async function projectIdentifierToId(projectId) {
257
257
  }
258
258
  throw new Error(`Project with identifier ${projectId} not found.`);
259
259
  }
260
+ export async function testCaseIdentifierToDetails(projectId, testCaseIdentifier) {
261
+ const url = `https://test-management.browserstack.com/api/v1/projects/${projectId}/test-cases/search?q[query]=${testCaseIdentifier}`;
262
+ const response = await axios.get(url, {
263
+ headers: {
264
+ "API-TOKEN": `${config.browserstackUsername}:${config.browserstackAccessKey}`,
265
+ accept: "application/json, text/plain, */*",
266
+ },
267
+ });
268
+ if (response.data.success !== true) {
269
+ throw new Error(`Failed to fetch test case details: ${response.statusText}`);
270
+ }
271
+ // Check if test_cases array exists and has items
272
+ if (!response.data.test_cases ||
273
+ !Array.isArray(response.data.test_cases) ||
274
+ response.data.test_cases.length === 0) {
275
+ throw new Error(`No test cases found in response for identifier ${testCaseIdentifier}`);
276
+ }
277
+ for (const testCase of response.data.test_cases) {
278
+ if (testCase.identifier === testCaseIdentifier) {
279
+ // Extract folder ID from the links.folder URL
280
+ // URL format: "/api/v1/projects/1930314/folder/10193436/test-cases"
281
+ let folderId = "";
282
+ if (testCase.links && testCase.links.folder) {
283
+ const folderMatch = testCase.links.folder.match(/\/folder\/(\d+)\//);
284
+ if (folderMatch && folderMatch[1]) {
285
+ folderId = folderMatch[1];
286
+ }
287
+ }
288
+ if (!folderId) {
289
+ throw new Error(`Could not extract folder ID for test case ${testCaseIdentifier}`);
290
+ }
291
+ return {
292
+ testCaseId: testCase.id.toString(),
293
+ folderId: folderId,
294
+ };
295
+ }
296
+ }
297
+ throw new Error(`Test case with identifier ${testCaseIdentifier} not found.`);
298
+ }
@@ -0,0 +1,166 @@
1
+ import { z } from "zod";
2
+ import axios from "axios";
3
+ import config from "../../config.js";
4
+ import { formatAxiosError } from "../../lib/error.js";
5
+ import { projectIdentifierToId, testCaseIdentifierToDetails, } from "./TCG-utils/api.js";
6
+ import { pollLCAStatus } from "./poll-lca-status.js";
7
+ /**
8
+ * Schema for creating LCA steps for a test case
9
+ */
10
+ export const CreateLCAStepsSchema = z.object({
11
+ project_identifier: z
12
+ .string()
13
+ .describe("ID of the project (Starts with 'PR-')"),
14
+ test_case_identifier: z
15
+ .string()
16
+ .describe("Identifier of the test case (e.g., 'TC-12345')"),
17
+ base_url: z.string().describe("Base URL for the test (e.g., 'google.com')"),
18
+ credentials: z
19
+ .object({
20
+ username: z.string().describe("Username for authentication"),
21
+ password: z.string().describe("Password for authentication"),
22
+ })
23
+ .optional()
24
+ .describe("Optional credentials for authentication. Extract from the test case details if provided in it. This is required for the test cases which require authentication."),
25
+ local_enabled: z
26
+ .boolean()
27
+ .optional()
28
+ .default(false)
29
+ .describe("Whether local testing is enabled"),
30
+ test_name: z.string().describe("Name of the test"),
31
+ test_case_details: z
32
+ .object({
33
+ name: z.string().describe("Name of the test case"),
34
+ description: z.string().describe("Description of the test case"),
35
+ preconditions: z.string().describe("Preconditions for the test case"),
36
+ test_case_steps: z
37
+ .array(z.object({
38
+ step: z.string().describe("Test step description"),
39
+ result: z.string().describe("Expected result"),
40
+ }))
41
+ .describe("Array of test case steps with expected results"),
42
+ })
43
+ .describe("Test case details including steps"),
44
+ wait_for_completion: z
45
+ .boolean()
46
+ .optional()
47
+ .default(true)
48
+ .describe("Whether to wait for LCA build completion (default: true)"),
49
+ });
50
+ /**
51
+ * Creates LCA (Low Code Automation) steps for a test case in BrowserStack Test Management
52
+ */
53
+ export async function createLCASteps(args, context) {
54
+ try {
55
+ // Get the project ID from identifier
56
+ const projectId = await projectIdentifierToId(args.project_identifier);
57
+ // Get the test case ID and folder ID from identifier
58
+ const { testCaseId, folderId } = await testCaseIdentifierToDetails(projectId, args.test_case_identifier);
59
+ const url = `https://test-management.browserstack.com/api/v1/projects/${projectId}/test-cases/${testCaseId}/lcnc`;
60
+ const payload = {
61
+ base_url: args.base_url,
62
+ credentials: args.credentials,
63
+ local_enabled: args.local_enabled,
64
+ test_name: args.test_name,
65
+ test_case_details: args.test_case_details,
66
+ version: "v2",
67
+ webhook_path: `https://test-management.browserstack.com/api/v1/projects/${projectId}/test-cases/${testCaseId}/webhooks/lcnc`,
68
+ };
69
+ const response = await axios.post(url, payload, {
70
+ headers: {
71
+ "API-TOKEN": `${config.browserstackUsername}:${config.browserstackAccessKey}`,
72
+ accept: "application/json, text/plain, */*",
73
+ "Content-Type": "application/json",
74
+ },
75
+ });
76
+ if (response.status >= 200 && response.status < 300) {
77
+ // Check if user wants to wait for completion
78
+ if (!args.wait_for_completion) {
79
+ return {
80
+ content: [
81
+ {
82
+ type: "text",
83
+ text: `LCA steps creation initiated for test case ${args.test_case_identifier} (ID: ${testCaseId})`,
84
+ },
85
+ {
86
+ type: "text",
87
+ text: "LCA build started. Check the BrowserStack Lowcode Automation UI for completion status.",
88
+ },
89
+ ],
90
+ };
91
+ }
92
+ // Start polling for LCA build completion
93
+ try {
94
+ const max_wait_minutes = 10; // Maximum wait time in minutes
95
+ const maxWaitMs = max_wait_minutes * 60 * 1000;
96
+ const lcaResult = await pollLCAStatus(projectId, folderId, testCaseId, context, maxWaitMs, // max wait time
97
+ 2 * 60 * 1000, // 2 minutes initial wait
98
+ 10 * 1000);
99
+ if (lcaResult && lcaResult.status === "done") {
100
+ return {
101
+ content: [
102
+ {
103
+ type: "text",
104
+ text: `Successfully created LCA steps for test case ${args.test_case_identifier} (ID: ${testCaseId})`,
105
+ },
106
+ {
107
+ type: "text",
108
+ text: `LCA build completed! Resource URL: ${lcaResult.resource_path}`,
109
+ },
110
+ ],
111
+ };
112
+ }
113
+ else {
114
+ return {
115
+ content: [
116
+ {
117
+ type: "text",
118
+ text: `LCA steps creation initiated for test case ${args.test_case_identifier} (ID: ${testCaseId})`,
119
+ },
120
+ {
121
+ type: "text",
122
+ text: `Warning: LCA build did not complete within ${max_wait_minutes} minutes. You can check the status later in the BrowserStack Test Management UI.`,
123
+ },
124
+ ],
125
+ };
126
+ }
127
+ }
128
+ catch (pollError) {
129
+ console.error("Error during LCA polling:", pollError);
130
+ return {
131
+ content: [
132
+ {
133
+ type: "text",
134
+ text: `LCA steps creation initiated for test case ${args.test_case_identifier} (ID: ${testCaseId})`,
135
+ },
136
+ {
137
+ type: "text",
138
+ text: "Warning: Error occurred while polling for LCA build completion. Check the BrowserStack Test Management UI for status.",
139
+ },
140
+ ],
141
+ };
142
+ }
143
+ }
144
+ else {
145
+ throw new Error(`Unexpected response: ${JSON.stringify(response.data)}`);
146
+ }
147
+ }
148
+ catch (error) {
149
+ // Add more specific error handling
150
+ if (error instanceof Error) {
151
+ if (error.message.includes("not found")) {
152
+ return {
153
+ content: [
154
+ {
155
+ type: "text",
156
+ text: `Error: ${error.message}. Please verify that the project identifier "${args.project_identifier}" and test case identifier "${args.test_case_identifier}" are correct.`,
157
+ isError: true,
158
+ },
159
+ ],
160
+ isError: true,
161
+ };
162
+ }
163
+ }
164
+ return formatAxiosError(error, "Failed to create LCA steps");
165
+ }
166
+ }
@@ -0,0 +1,117 @@
1
+ import axios from "axios";
2
+ import config from "../../config.js";
3
+ /**
4
+ * Poll test case details to check LCA build status
5
+ */
6
+ export async function pollLCAStatus(projectId, folderId, testCaseId, context, maxWaitTimeMs = 10 * 60 * 1000, // 10 minutes default
7
+ initialWaitMs = 2 * 60 * 1000, // 2 minutes initial wait
8
+ pollIntervalMs = 10 * 1000) {
9
+ const url = `https://test-management.browserstack.com/api/v1/projects/${projectId}/folder/${folderId}/test-cases/${testCaseId}`;
10
+ const startTime = Date.now();
11
+ // Send initial notification that polling is starting
12
+ const notificationInterval = Math.min(initialWaitMs, pollIntervalMs);
13
+ const notificationStartTime = Date.now();
14
+ const notificationIntervalId = setInterval(async () => {
15
+ const elapsedTime = Date.now() - notificationStartTime;
16
+ const progressPercentage = Math.min(90, Math.floor((elapsedTime / maxWaitTimeMs) * 90));
17
+ await context.sendNotification({
18
+ method: "notifications/progress",
19
+ params: {
20
+ progressToken: context._meta?.progressToken ?? `lca-${testCaseId}`,
21
+ message: `Generating Low Code Automation Test..`,
22
+ progress: progressPercentage,
23
+ total: 100,
24
+ },
25
+ });
26
+ if (elapsedTime >= initialWaitMs) {
27
+ clearInterval(notificationIntervalId);
28
+ }
29
+ }, notificationInterval);
30
+ // Wait for initial period before starting to poll
31
+ await new Promise((resolve) => setTimeout(resolve, initialWaitMs));
32
+ clearInterval(notificationIntervalId);
33
+ return new Promise((resolve) => {
34
+ // Set up timeout to handle max wait time
35
+ const timeoutId = setTimeout(() => {
36
+ clearInterval(intervalId);
37
+ // Send timeout notification
38
+ context.sendNotification({
39
+ method: "notifications/progress",
40
+ params: {
41
+ progressToken: context._meta?.progressToken ?? `lca-${testCaseId}`,
42
+ message: `LCA build polling timed out after ${Math.floor(maxWaitTimeMs / 60000)} minutes`,
43
+ progress: 100,
44
+ total: 100,
45
+ },
46
+ });
47
+ resolve(null);
48
+ }, maxWaitTimeMs);
49
+ // Set up polling interval
50
+ const intervalId = setInterval(async () => {
51
+ try {
52
+ const response = await axios.get(url, {
53
+ headers: {
54
+ "API-TOKEN": `${config.browserstackUsername}:${config.browserstackAccessKey}`,
55
+ accept: "application/json, text/plain, */*",
56
+ },
57
+ });
58
+ if (response.data.data.success && response.data.data.test_case) {
59
+ const testCase = response.data.data.test_case;
60
+ // Check lcnc_build_map in both possible locations
61
+ const lcncBuildMap = testCase.lcnc_build_map ||
62
+ response.data.data.metadata?.lcnc_build_map;
63
+ if (lcncBuildMap) {
64
+ if (lcncBuildMap.status === "done") {
65
+ // Clean up timers
66
+ clearInterval(intervalId);
67
+ clearTimeout(timeoutId);
68
+ // Send completion notification
69
+ await context.sendNotification({
70
+ method: "notifications/progress",
71
+ params: {
72
+ progressToken: context._meta?.progressToken ?? `lca-${testCaseId}`,
73
+ message: `LCA build completed successfully!`,
74
+ progress: 100,
75
+ total: 100,
76
+ },
77
+ });
78
+ resolve({
79
+ resource_path: lcncBuildMap.resource_path,
80
+ status: lcncBuildMap.status,
81
+ });
82
+ return;
83
+ }
84
+ // Send progress notification with current status
85
+ const elapsedTime = Date.now() - startTime;
86
+ const progressPercentage = Math.min(90, Math.floor((elapsedTime / maxWaitTimeMs) * 90) + 10);
87
+ // Cycle through different numbers of dots (2, 3, 4, 5, then back to 2)
88
+ const dotCount = (Math.floor(elapsedTime / pollIntervalMs) % 4) + 2;
89
+ const dots = ".".repeat(dotCount);
90
+ await context.sendNotification({
91
+ method: "notifications/progress",
92
+ params: {
93
+ progressToken: context._meta?.progressToken ?? `lca-${testCaseId}`,
94
+ message: `Generating Low Code Automation Test${dots}`,
95
+ progress: progressPercentage,
96
+ total: 100,
97
+ },
98
+ });
99
+ }
100
+ }
101
+ }
102
+ catch (error) {
103
+ console.error("Error polling LCA status:", error);
104
+ // Send error notification but continue polling
105
+ await context.sendNotification({
106
+ method: "notifications/progress",
107
+ params: {
108
+ progressToken: context._meta?.progressToken ?? `lca-${testCaseId}`,
109
+ message: `Error occurred while polling, retrying... (${error instanceof Error ? error.message : "Unknown error"})`,
110
+ progress: Math.min(90, Math.floor(((Date.now() - startTime) / maxWaitTimeMs) * 90) + 10),
111
+ total: 100,
112
+ },
113
+ });
114
+ }
115
+ }, pollIntervalMs);
116
+ });
117
+ }
@@ -31,12 +31,17 @@ export async function createTestCasesFromFile(args, context) {
31
31
  const scenariosMap = await pollScenariosTestDetails(args, traceId, context, documentId, source);
32
32
  const resultString = await bulkCreateTestCases(scenariosMap, args.projectReferenceId, args.folderId, fieldMaps, booleanFieldId, traceId, context, documentId);
33
33
  signedUrlMap.delete(args.documentId);
34
+ const dashboardURL = `https://test-management.browserstack.com/projects/${args.projectReferenceId}/folder/${args.folderId}/test-cases`;
34
35
  return {
35
36
  content: [
36
37
  {
37
38
  type: "text",
38
39
  text: resultString,
39
40
  },
41
+ {
42
+ type: "text",
43
+ text: `Dashboard URL: ${dashboardURL}`,
44
+ },
40
45
  ],
41
46
  };
42
47
  }
@@ -11,6 +11,7 @@ import { addTestResult, AddTestResultSchema, } from "./testmanagement-utils/add-
11
11
  import { UploadFileSchema, uploadFile, } from "./testmanagement-utils/upload-file.js";
12
12
  import { createTestCasesFromFile } from "./testmanagement-utils/testcase-from-file.js";
13
13
  import { CreateTestCasesFromFileSchema } from "./testmanagement-utils/TCG-utils/types.js";
14
+ import { createLCASteps, CreateLCAStepsSchema, } from "./testmanagement-utils/create-lca-steps.js";
14
15
  //TODO: Moving the traceMCP and catch block to the parent(server) function
15
16
  /**
16
17
  * Wrapper to call createProjectOrFolder util.
@@ -216,6 +217,28 @@ export async function createTestCasesFromFileTool(args, context) {
216
217
  };
217
218
  }
218
219
  }
220
+ /**
221
+ * Creates LCA (Low Code Automation) steps for a test case in BrowserStack Test Management.
222
+ */
223
+ export async function createLCAStepsTool(args, context) {
224
+ try {
225
+ trackMCP("createLCASteps", serverInstance.server.getClientVersion());
226
+ return await createLCASteps(args, context);
227
+ }
228
+ catch (err) {
229
+ trackMCP("createLCASteps", serverInstance.server.getClientVersion(), err);
230
+ return {
231
+ content: [
232
+ {
233
+ type: "text",
234
+ text: `Failed to create LCA steps: ${err instanceof Error ? err.message : "Unknown error"}. Please open an issue on GitHub if the problem persists`,
235
+ isError: true,
236
+ },
237
+ ],
238
+ isError: true,
239
+ };
240
+ }
241
+ }
219
242
  /**
220
243
  * Registers both project/folder and test-case tools.
221
244
  */
@@ -228,6 +251,7 @@ export default function addTestManagementTools(server) {
228
251
  server.tool("listTestRuns", "List test runs in a project with optional filters (date ranges, assignee, state, etc.)", ListTestRunsSchema.shape, listTestRunsTool);
229
252
  server.tool("updateTestRun", "Update a test run in BrowserStack Test Management.", UpdateTestRunSchema.shape, updateTestRunTool);
230
253
  server.tool("addTestResult", "Add a test result to a specific test run via BrowserStack Test Management API.", AddTestResultSchema.shape, addTestResultTool);
231
- server.tool("uploadProductRequirementFile", "Upload files such as PDRs or PDFs to BrowserStack Test Management and get file mapping ID back. Its Used for generating test cases from file.", UploadFileSchema.shape, uploadProductRequirementFileTool);
232
- server.tool("createTestCasesFromFile", "Create test cases from a file in BrowserStack Test Management.", CreateTestCasesFromFileSchema.shape, createTestCasesFromFileTool);
254
+ server.tool("uploadProductRequirementFile", "Upload files (e.g., PDRs, PDFs) to BrowserStack Test Management and retrieve a file mapping ID. This is utilized for generating test cases from files and is part of the Test Case Generator AI Agent in BrowserStack.", UploadFileSchema.shape, uploadProductRequirementFileTool);
255
+ server.tool("createTestCasesFromFile", "Generate test cases from a file in BrowserStack Test Management using the Test Case Generator AI Agent.", CreateTestCasesFromFileSchema.shape, createTestCasesFromFileTool);
256
+ server.tool("createLCASteps", "Generate Low Code Automation (LCA) steps for a test case in BrowserStack Test Management using the Low Code Automation Agent.", CreateLCAStepsSchema.shape, createLCAStepsTool);
233
257
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@browserstack/mcp-server",
3
- "version": "1.1.5",
3
+ "version": "1.1.7",
4
4
  "description": "BrowserStack's Official MCP Server",
5
5
  "main": "dist/index.js",
6
6
  "repository": {