@browserstack/mcp-server 1.2.36 → 1.3.1-beta.1
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/README.md +2 -0
- package/dist/config.d.ts +4 -1
- package/dist/config.js +23 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/lib/tm-base-url.js +7 -1
- package/dist/tools/accessibility.js +30 -0
- package/dist/tools/appautomate.js +20 -2
- package/dist/tools/applive.js +6 -0
- package/dist/tools/automate.js +6 -0
- package/dist/tools/bstack-sdk.js +7 -1
- package/dist/tools/build-insights.js +16 -3
- package/dist/tools/get-failure-logs.js +6 -0
- package/dist/tools/live.js +7 -1
- package/dist/tools/observability.js +6 -0
- package/dist/tools/percy-sdk.js +49 -7
- package/dist/tools/rca-agent-utils/constants.d.ts +2 -0
- package/dist/tools/rca-agent-utils/constants.js +8 -0
- package/dist/tools/rca-agent-utils/get-failed-test-id.d.ts +3 -2
- package/dist/tools/rca-agent-utils/get-failed-test-id.js +94 -25
- package/dist/tools/rca-agent-utils/types.d.ts +10 -0
- package/dist/tools/rca-agent.d.ts +1 -0
- package/dist/tools/rca-agent.js +30 -6
- package/dist/tools/selfheal.js +12 -0
- package/dist/tools/testmanagement.js +119 -17
- package/dist/tools/tfa-rca-collaboration.d.ts +15 -0
- package/dist/tools/tfa-rca-collaboration.js +162 -0
- package/dist/tools/tfa-rca-utils/build-failure-themes.d.ts +61 -0
- package/dist/tools/tfa-rca-utils/build-failure-themes.js +133 -0
- package/dist/tools/tfa-rca-utils/constants.d.ts +67 -0
- package/dist/tools/tfa-rca-utils/constants.js +103 -0
- package/dist/tools/tfa-rca-utils/submit-turn.d.ts +29 -0
- package/dist/tools/tfa-rca-utils/submit-turn.js +188 -0
- package/dist/tools/tfa-rca-utils/trigger-report.d.ts +22 -0
- package/dist/tools/tfa-rca-utils/trigger-report.js +61 -0
- package/dist/tools/tfa-rca-utils/turn-result.d.ts +14 -0
- package/dist/tools/tfa-rca-utils/turn-result.js +131 -0
- package/dist/tools/tfa-rca-utils/types.d.ts +61 -0
- package/dist/tools/tfa-rca-utils/types.js +9 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -41,6 +41,8 @@ Click on the buttons below to install MCP in your respective IDE:
|
|
|
41
41
|
|
|
42
42
|
<a href="http://mcp.browserstack.com/one-click-setup?client=vscode"><img src="assets/one-click-vs-code.png" alt="Install in VS Code" width="160" height="80"></a> <a href="http://mcp.browserstack.com/one-click-setup?client=cursor"><img src="assets/one-click-cursor.png" alt="Install in Cursor" width="150" height="70"></a>
|
|
43
43
|
|
|
44
|
+
> **Note:** The hosted Remote MCP server (`mcp.browserstack.com`) is now fully stateless over Streamable HTTP — any Streamable-HTTP client (Claude, Cursor, VS Code, ChatGPT) connects with no configuration change.
|
|
45
|
+
|
|
44
46
|
#### Note : Ensure you are using Node version >= `18.0`
|
|
45
47
|
- Check your node version using `node --version`. Recommended version: `v22.15.0` (LTS)
|
|
46
48
|
- To Upgrade Node :
|
package/dist/config.d.ts
CHANGED
|
@@ -8,7 +8,10 @@ export declare class Config {
|
|
|
8
8
|
readonly USE_OWN_LOCAL_BINARY_PROCESS: boolean;
|
|
9
9
|
readonly REMOTE_MCP: boolean;
|
|
10
10
|
readonly UPLOAD_BASE_DIR: string | undefined;
|
|
11
|
-
|
|
11
|
+
readonly O11Y_TFA_RCA_BASE_URL: string;
|
|
12
|
+
readonly BROWSERSTACK_AUTOMATION_BASE_URL: string;
|
|
13
|
+
readonly BROWSERSTACK_O11Y_UI_BASE_URL: string;
|
|
14
|
+
constructor(DEV_MODE: boolean, browserstackLocalOptions: Record<string, any>, USE_OWN_LOCAL_BINARY_PROCESS: boolean, REMOTE_MCP: boolean, UPLOAD_BASE_DIR: string | undefined, O11Y_TFA_RCA_BASE_URL: string, BROWSERSTACK_AUTOMATION_BASE_URL: string, BROWSERSTACK_O11Y_UI_BASE_URL: string);
|
|
12
15
|
}
|
|
13
16
|
declare const config: Config;
|
|
14
17
|
export default config;
|
package/dist/config.js
CHANGED
|
@@ -28,6 +28,12 @@ for (const key of BROWSERSTACK_LOCAL_OPTION_KEYS) {
|
|
|
28
28
|
browserstackLocalOptions[key] = envVar;
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
|
+
// Overridable via O11Y_TFA_RCA_BASE_URL to target a staging tenant.
|
|
32
|
+
const DEFAULT_O11Y_TFA_RCA_BASE_URL = "https://api-automation.browserstack.com";
|
|
33
|
+
// Overridable via BROWSERSTACK_AUTOMATION_BASE_URL to target a non-prod env.
|
|
34
|
+
const DEFAULT_BROWSERSTACK_AUTOMATION_BASE_URL = "https://api-automation.browserstack.com";
|
|
35
|
+
// Overridable via BROWSERSTACK_O11Y_UI_BASE_URL to point at a staging UI.
|
|
36
|
+
const DEFAULT_BROWSERSTACK_O11Y_UI_BASE_URL = "https://automation.browserstack.com";
|
|
31
37
|
/**
|
|
32
38
|
* USE_OWN_LOCAL_BINARY_PROCESS:
|
|
33
39
|
* If true, the system will not start a new local binary process, but will use the user's own process.
|
|
@@ -38,15 +44,30 @@ export class Config {
|
|
|
38
44
|
USE_OWN_LOCAL_BINARY_PROCESS;
|
|
39
45
|
REMOTE_MCP;
|
|
40
46
|
UPLOAD_BASE_DIR;
|
|
41
|
-
|
|
47
|
+
O11Y_TFA_RCA_BASE_URL;
|
|
48
|
+
BROWSERSTACK_AUTOMATION_BASE_URL;
|
|
49
|
+
BROWSERSTACK_O11Y_UI_BASE_URL;
|
|
50
|
+
constructor(DEV_MODE, browserstackLocalOptions, USE_OWN_LOCAL_BINARY_PROCESS, REMOTE_MCP, UPLOAD_BASE_DIR, O11Y_TFA_RCA_BASE_URL, BROWSERSTACK_AUTOMATION_BASE_URL, BROWSERSTACK_O11Y_UI_BASE_URL) {
|
|
42
51
|
this.DEV_MODE = DEV_MODE;
|
|
43
52
|
this.browserstackLocalOptions = browserstackLocalOptions;
|
|
44
53
|
this.USE_OWN_LOCAL_BINARY_PROCESS = USE_OWN_LOCAL_BINARY_PROCESS;
|
|
45
54
|
this.REMOTE_MCP = REMOTE_MCP;
|
|
46
55
|
this.UPLOAD_BASE_DIR = UPLOAD_BASE_DIR;
|
|
56
|
+
this.O11Y_TFA_RCA_BASE_URL = O11Y_TFA_RCA_BASE_URL;
|
|
57
|
+
this.BROWSERSTACK_AUTOMATION_BASE_URL = BROWSERSTACK_AUTOMATION_BASE_URL;
|
|
58
|
+
this.BROWSERSTACK_O11Y_UI_BASE_URL = BROWSERSTACK_O11Y_UI_BASE_URL;
|
|
47
59
|
}
|
|
48
60
|
}
|
|
49
61
|
const config = new Config(process.env.DEV_MODE === "true", browserstackLocalOptions, process.env.USE_OWN_LOCAL_BINARY_PROCESS === "true", process.env.REMOTE_MCP === "true", process.env.MCP_UPLOAD_BASE_DIR && process.env.MCP_UPLOAD_BASE_DIR.length > 0
|
|
50
62
|
? process.env.MCP_UPLOAD_BASE_DIR
|
|
51
|
-
: undefined
|
|
63
|
+
: undefined, process.env.O11Y_TFA_RCA_BASE_URL &&
|
|
64
|
+
process.env.O11Y_TFA_RCA_BASE_URL.length > 0
|
|
65
|
+
? process.env.O11Y_TFA_RCA_BASE_URL
|
|
66
|
+
: DEFAULT_O11Y_TFA_RCA_BASE_URL, process.env.BROWSERSTACK_AUTOMATION_BASE_URL &&
|
|
67
|
+
process.env.BROWSERSTACK_AUTOMATION_BASE_URL.length > 0
|
|
68
|
+
? process.env.BROWSERSTACK_AUTOMATION_BASE_URL
|
|
69
|
+
: DEFAULT_BROWSERSTACK_AUTOMATION_BASE_URL, process.env.BROWSERSTACK_O11Y_UI_BASE_URL &&
|
|
70
|
+
process.env.BROWSERSTACK_O11Y_UI_BASE_URL.length > 0
|
|
71
|
+
? process.env.BROWSERSTACK_O11Y_UI_BASE_URL
|
|
72
|
+
: DEFAULT_BROWSERSTACK_O11Y_UI_BASE_URL);
|
|
52
73
|
export default config;
|
package/dist/index.d.ts
CHANGED
|
@@ -3,4 +3,5 @@ import "dotenv/config";
|
|
|
3
3
|
export { setLogger } from "./logger.js";
|
|
4
4
|
export { BrowserStackMcpServer } from "./server-factory.js";
|
|
5
5
|
export { trackMCP } from "./lib/instrumentation.js";
|
|
6
|
+
export { default as addTfaRcaCollaborationTools } from "./tools/tfa-rca-collaboration.js";
|
|
6
7
|
export declare const PackageJsonVersion: any;
|
package/dist/index.js
CHANGED
|
@@ -36,4 +36,5 @@ process.on("exit", () => {
|
|
|
36
36
|
export { setLogger } from "./logger.js";
|
|
37
37
|
export { BrowserStackMcpServer } from "./server-factory.js";
|
|
38
38
|
export { trackMCP } from "./lib/instrumentation.js";
|
|
39
|
+
export { default as addTfaRcaCollaborationTools } from "./tools/tfa-rca-collaboration.js";
|
|
39
40
|
export const PackageJsonVersion = packageJson.version;
|
package/dist/lib/tm-base-url.js
CHANGED
|
@@ -20,6 +20,7 @@ export async function getTMBaseURL(config) {
|
|
|
20
20
|
const authString = getBrowserStackAuth(config);
|
|
21
21
|
const [username, password] = authString.split(":");
|
|
22
22
|
const authHeader = "Basic " + Buffer.from(`${username}:${password}`).toString("base64");
|
|
23
|
+
const failures = [];
|
|
23
24
|
for (const baseUrl of TM_BASE_URLS) {
|
|
24
25
|
try {
|
|
25
26
|
const res = await apiClient.get({
|
|
@@ -27,6 +28,9 @@ export async function getTMBaseURL(config) {
|
|
|
27
28
|
headers: { Authorization: authHeader },
|
|
28
29
|
raise_error: false,
|
|
29
30
|
});
|
|
31
|
+
if (!res.ok) {
|
|
32
|
+
failures.push(`${baseUrl}: HTTP ${res.status}`);
|
|
33
|
+
}
|
|
30
34
|
if (res.ok) {
|
|
31
35
|
// Only populate the cache in single-tenant (stdio) mode; in remote mode
|
|
32
36
|
// the cache must stay empty so each user discovers their own region.
|
|
@@ -38,8 +42,10 @@ export async function getTMBaseURL(config) {
|
|
|
38
42
|
}
|
|
39
43
|
}
|
|
40
44
|
catch (err) {
|
|
45
|
+
const code = err?.code ?? err?.message;
|
|
46
|
+
failures.push(`${baseUrl}: ${code}`);
|
|
41
47
|
logger.debug(`Failed TM base URL: ${baseUrl} (${err})`);
|
|
42
48
|
}
|
|
43
49
|
}
|
|
44
|
-
throw new Error(
|
|
50
|
+
throw new Error(`Unable to connect to BrowserStack Test Management. Please check your credentials and network connection.Please open an issue on GitHub if the problem persists. Details: ${failures.join("; ")}`);
|
|
45
51
|
}
|
|
@@ -213,6 +213,12 @@ export default function addAccessibilityTools(server, config) {
|
|
|
213
213
|
query: z
|
|
214
214
|
.string()
|
|
215
215
|
.describe("Any accessibility, a11y, WCAG, or web accessibility question"),
|
|
216
|
+
}, {
|
|
217
|
+
title: "Accessibility Expert",
|
|
218
|
+
readOnlyHint: true,
|
|
219
|
+
openWorldHint: false,
|
|
220
|
+
destructiveHint: false,
|
|
221
|
+
idempotentHint: true,
|
|
216
222
|
}, async (args) => {
|
|
217
223
|
return await executeAccessibilityRAG(args, server, config);
|
|
218
224
|
});
|
|
@@ -227,6 +233,12 @@ export default function addAccessibilityTools(server, config) {
|
|
|
227
233
|
.boolean()
|
|
228
234
|
.optional()
|
|
229
235
|
.describe("Enable advanced accessibility rules in the scan settings"),
|
|
236
|
+
}, {
|
|
237
|
+
title: "Start Accessibility Scan",
|
|
238
|
+
readOnlyHint: false,
|
|
239
|
+
openWorldHint: false,
|
|
240
|
+
destructiveHint: false,
|
|
241
|
+
idempotentHint: false,
|
|
230
242
|
}, async (args, context) => {
|
|
231
243
|
return await executeAccessibilityScan(args, context, server, config);
|
|
232
244
|
});
|
|
@@ -256,6 +268,12 @@ export default function addAccessibilityTools(server, config) {
|
|
|
256
268
|
.string()
|
|
257
269
|
.optional()
|
|
258
270
|
.describe("CSS selector for submit button (required for form auth)"),
|
|
271
|
+
}, {
|
|
272
|
+
title: "Create Accessibility Auth Config",
|
|
273
|
+
readOnlyHint: false,
|
|
274
|
+
openWorldHint: false,
|
|
275
|
+
destructiveHint: false,
|
|
276
|
+
idempotentHint: false,
|
|
259
277
|
}, async (args) => {
|
|
260
278
|
try {
|
|
261
279
|
const creds = await elicitCredentialsIfSupported(server, { username: args.username, password: args.password }, [
|
|
@@ -287,6 +305,12 @@ export default function addAccessibilityTools(server, config) {
|
|
|
287
305
|
});
|
|
288
306
|
tools.getAccessibilityAuthConfig = server.tool("getAccessibilityAuthConfig", "Retrieve an existing authentication configuration by ID.", {
|
|
289
307
|
configId: z.number().describe("ID of the auth configuration to retrieve"),
|
|
308
|
+
}, {
|
|
309
|
+
title: "Get Accessibility Auth Config",
|
|
310
|
+
readOnlyHint: true,
|
|
311
|
+
openWorldHint: false,
|
|
312
|
+
destructiveHint: false,
|
|
313
|
+
idempotentHint: true,
|
|
290
314
|
}, async (args) => {
|
|
291
315
|
return await executeGetAuthConfig(args, server, config);
|
|
292
316
|
});
|
|
@@ -301,6 +325,12 @@ export default function addAccessibilityTools(server, config) {
|
|
|
301
325
|
.number()
|
|
302
326
|
.optional()
|
|
303
327
|
.describe("Character offset for pagination (default: 0)"),
|
|
328
|
+
}, {
|
|
329
|
+
title: "Fetch Accessibility Issues",
|
|
330
|
+
readOnlyHint: true,
|
|
331
|
+
openWorldHint: false,
|
|
332
|
+
destructiveHint: false,
|
|
333
|
+
idempotentHint: true,
|
|
304
334
|
}, async (args) => {
|
|
305
335
|
return await executeFetchAccessibilityIssues(args, server, config);
|
|
306
336
|
});
|
|
@@ -207,6 +207,12 @@ export default function addAppAutomationTools(server, config) {
|
|
|
207
207
|
appPath: z
|
|
208
208
|
.string()
|
|
209
209
|
.describe("The path to the .apk or .ipa file. Required for app installation."),
|
|
210
|
+
}, {
|
|
211
|
+
title: "Take App Screenshot",
|
|
212
|
+
readOnlyHint: false,
|
|
213
|
+
openWorldHint: false,
|
|
214
|
+
destructiveHint: false,
|
|
215
|
+
idempotentHint: false,
|
|
210
216
|
}, async (args) => {
|
|
211
217
|
try {
|
|
212
218
|
trackMCP("takeAppScreenshot", server.server.getClientVersion(), undefined, config);
|
|
@@ -226,7 +232,13 @@ export default function addAppAutomationTools(server, config) {
|
|
|
226
232
|
};
|
|
227
233
|
}
|
|
228
234
|
});
|
|
229
|
-
tools.runAppTestsOnBrowserStack = server.tool("runAppTestsOnBrowserStack", RUN_APP_AUTOMATE_DESCRIPTION, RUN_APP_AUTOMATE_SCHEMA,
|
|
235
|
+
tools.runAppTestsOnBrowserStack = server.tool("runAppTestsOnBrowserStack", RUN_APP_AUTOMATE_DESCRIPTION, RUN_APP_AUTOMATE_SCHEMA, {
|
|
236
|
+
title: "Run App Tests on BrowserStack",
|
|
237
|
+
readOnlyHint: false,
|
|
238
|
+
openWorldHint: false,
|
|
239
|
+
destructiveHint: false,
|
|
240
|
+
idempotentHint: false,
|
|
241
|
+
}, async (args) => {
|
|
230
242
|
try {
|
|
231
243
|
trackMCP("runAppTestsOnBrowserStack", server.server.getClientVersion(), undefined, config);
|
|
232
244
|
const devicesAsArrays = args.devices.map((device) => [device.platform, device.deviceName, device.osVersion]);
|
|
@@ -246,7 +258,13 @@ export default function addAppAutomationTools(server, config) {
|
|
|
246
258
|
};
|
|
247
259
|
}
|
|
248
260
|
});
|
|
249
|
-
tools.setupBrowserStackAppAutomateTests = server.tool("setupBrowserStackAppAutomateTests", SETUP_APP_AUTOMATE_DESCRIPTION, SETUP_APP_AUTOMATE_SCHEMA,
|
|
261
|
+
tools.setupBrowserStackAppAutomateTests = server.tool("setupBrowserStackAppAutomateTests", SETUP_APP_AUTOMATE_DESCRIPTION, SETUP_APP_AUTOMATE_SCHEMA, {
|
|
262
|
+
title: "Set Up App Automate Tests",
|
|
263
|
+
readOnlyHint: true,
|
|
264
|
+
openWorldHint: false,
|
|
265
|
+
destructiveHint: false,
|
|
266
|
+
idempotentHint: true,
|
|
267
|
+
}, async (args) => {
|
|
250
268
|
try {
|
|
251
269
|
return await setupAppAutomateHandler(args, config);
|
|
252
270
|
}
|
package/dist/tools/applive.js
CHANGED
|
@@ -67,6 +67,12 @@ export default function addAppLiveTools(server, config) {
|
|
|
67
67
|
appPath: z
|
|
68
68
|
.string()
|
|
69
69
|
.describe("The path to the .ipa or .apk file to install on the device. Always ask the user for the app path, do not assume it."),
|
|
70
|
+
}, {
|
|
71
|
+
title: "Run App Live Session",
|
|
72
|
+
readOnlyHint: false,
|
|
73
|
+
openWorldHint: false,
|
|
74
|
+
destructiveHint: false,
|
|
75
|
+
idempotentHint: false,
|
|
70
76
|
}, async (args) => {
|
|
71
77
|
try {
|
|
72
78
|
trackMCP("runAppLiveSession", server.server.getClientVersion(), undefined, config);
|
package/dist/tools/automate.js
CHANGED
|
@@ -58,6 +58,12 @@ export default function addAutomationTools(server, config) {
|
|
|
58
58
|
sessionType: z
|
|
59
59
|
.enum([SessionType.Automate, SessionType.AppAutomate])
|
|
60
60
|
.describe("Type of BrowserStack session"),
|
|
61
|
+
}, {
|
|
62
|
+
title: "Fetch Automation Screenshots",
|
|
63
|
+
readOnlyHint: true,
|
|
64
|
+
openWorldHint: false,
|
|
65
|
+
destructiveHint: false,
|
|
66
|
+
idempotentHint: true,
|
|
61
67
|
}, async (args) => {
|
|
62
68
|
try {
|
|
63
69
|
trackMCP("fetchAutomationScreenshots", server.server.getClientVersion(), undefined, config);
|
package/dist/tools/bstack-sdk.js
CHANGED
|
@@ -5,7 +5,13 @@ import { handleMCPError } from "../lib/utils.js";
|
|
|
5
5
|
import { trackMCP } from "../lib/instrumentation.js";
|
|
6
6
|
export function registerRunBrowserStackTestsTool(server, config) {
|
|
7
7
|
const tools = {};
|
|
8
|
-
tools.setupBrowserStackAutomateTests = server.tool("setupBrowserStackAutomateTests", RUN_ON_BROWSERSTACK_DESCRIPTION, RunTestsOnBrowserStackParamsShape,
|
|
8
|
+
tools.setupBrowserStackAutomateTests = server.tool("setupBrowserStackAutomateTests", RUN_ON_BROWSERSTACK_DESCRIPTION, RunTestsOnBrowserStackParamsShape, {
|
|
9
|
+
title: "Set Up Automate Tests",
|
|
10
|
+
readOnlyHint: true,
|
|
11
|
+
openWorldHint: false,
|
|
12
|
+
destructiveHint: false,
|
|
13
|
+
idempotentHint: true,
|
|
14
|
+
}, async (args) => {
|
|
9
15
|
try {
|
|
10
16
|
trackMCP("runTestsOnBrowserStack", server.server.getClientVersion(), config);
|
|
11
17
|
return await runTestsOnBrowserStackHandler(args, config);
|
|
@@ -7,9 +7,13 @@ export async function fetchBuildInsightsTool(args, config) {
|
|
|
7
7
|
try {
|
|
8
8
|
const buildUrl = `https://api-automation.browserstack.com/ext/v1/builds/${args.buildId}`;
|
|
9
9
|
const qualityGateUrl = `https://api-automation.browserstack.com/ext/v1/quality-gates/${args.buildId}`;
|
|
10
|
+
// Quality gate data is optional — a failure there should not block build insights
|
|
10
11
|
const [buildData, qualityData] = await Promise.all([
|
|
11
12
|
fetchFromBrowserStackAPI(buildUrl, config),
|
|
12
|
-
fetchFromBrowserStackAPI(qualityGateUrl, config)
|
|
13
|
+
fetchFromBrowserStackAPI(qualityGateUrl, config).catch((error) => {
|
|
14
|
+
logger.warn("Failed to fetch quality gate data", error);
|
|
15
|
+
return null;
|
|
16
|
+
}),
|
|
13
17
|
]);
|
|
14
18
|
// Select useful fields for users
|
|
15
19
|
const insights = {
|
|
@@ -25,9 +29,12 @@ export async function fetchBuildInsightsTool(args, config) {
|
|
|
25
29
|
unique_errors: buildData.unique_errors?.overview,
|
|
26
30
|
observability_url: buildData?.observability_url,
|
|
27
31
|
ci_build_url: buildData.ci_info?.build_url,
|
|
28
|
-
|
|
32
|
+
branch: buildData.vcs_info?.branch,
|
|
33
|
+
commit_sha: buildData.vcs_info?.sha,
|
|
34
|
+
vcs_name: buildData.vcs_info?.name,
|
|
35
|
+
quality_gate_result: qualityData?.quality_gate_result,
|
|
29
36
|
};
|
|
30
|
-
const qualityProfiles = qualityData
|
|
37
|
+
const qualityProfiles = qualityData?.quality_profiles?.map((profile) => ({
|
|
31
38
|
name: profile.name,
|
|
32
39
|
result: profile.result,
|
|
33
40
|
}));
|
|
@@ -54,6 +61,12 @@ export default function addBuildInsightsTools(server, config) {
|
|
|
54
61
|
const tools = {};
|
|
55
62
|
tools.fetchBuildInsights = server.tool("fetchBuildInsights", "Fetches insights about a BrowserStack build by combining build details and quality gate results.", {
|
|
56
63
|
buildId: z.string().describe("The build UUID of the BrowserStack build"),
|
|
64
|
+
}, {
|
|
65
|
+
title: "Fetch Build Insights",
|
|
66
|
+
readOnlyHint: true,
|
|
67
|
+
openWorldHint: false,
|
|
68
|
+
destructiveHint: false,
|
|
69
|
+
idempotentHint: true,
|
|
57
70
|
}, async (args) => {
|
|
58
71
|
try {
|
|
59
72
|
trackMCP("fetchBuildInsights", server.server.getClientVersion(), config);
|
|
@@ -119,6 +119,12 @@ export default function registerGetFailureLogs(server, config) {
|
|
|
119
119
|
AppAutomateLogType.CrashLogs,
|
|
120
120
|
]))
|
|
121
121
|
.describe("The types of logs to fetch."),
|
|
122
|
+
}, {
|
|
123
|
+
title: "Get Failure Logs",
|
|
124
|
+
readOnlyHint: true,
|
|
125
|
+
openWorldHint: false,
|
|
126
|
+
destructiveHint: false,
|
|
127
|
+
idempotentHint: true,
|
|
122
128
|
}, async (args) => {
|
|
123
129
|
try {
|
|
124
130
|
trackMCP("getFailureLogs", server.server.getClientVersion(), undefined, config);
|
package/dist/tools/live.js
CHANGED
|
@@ -88,7 +88,13 @@ async function runBrowserSession(rawArgs, config) {
|
|
|
88
88
|
}
|
|
89
89
|
export default function addBrowserLiveTools(server, config) {
|
|
90
90
|
const tools = {};
|
|
91
|
-
tools.runBrowserLiveSession = server.tool("runBrowserLiveSession", "Launch a BrowserStack Live session (desktop or mobile).", LiveArgsShape,
|
|
91
|
+
tools.runBrowserLiveSession = server.tool("runBrowserLiveSession", "Launch a BrowserStack Live session (desktop or mobile).", LiveArgsShape, {
|
|
92
|
+
title: "Run Browser Live Session",
|
|
93
|
+
readOnlyHint: false,
|
|
94
|
+
openWorldHint: true,
|
|
95
|
+
destructiveHint: false,
|
|
96
|
+
idempotentHint: false,
|
|
97
|
+
}, async (args) => {
|
|
92
98
|
try {
|
|
93
99
|
trackMCP("runBrowserLiveSession", server.server.getClientVersion(), undefined, config);
|
|
94
100
|
return await runBrowserSession(args, config);
|
|
@@ -39,6 +39,12 @@ export default function addObservabilityTools(server, config) {
|
|
|
39
39
|
projectName: z
|
|
40
40
|
.string()
|
|
41
41
|
.describe("Name of the project to get failures for. This is the 'projectName' key in the browserstack.yml file. If not sure, ask the user for the project name."),
|
|
42
|
+
}, {
|
|
43
|
+
title: "Get Failures in Last Run",
|
|
44
|
+
readOnlyHint: true,
|
|
45
|
+
openWorldHint: false,
|
|
46
|
+
destructiveHint: false,
|
|
47
|
+
idempotentHint: true,
|
|
42
48
|
}, async (args) => {
|
|
43
49
|
try {
|
|
44
50
|
trackMCP("getFailuresInLastRun", server.server.getClientVersion(), undefined, config);
|
package/dist/tools/percy-sdk.js
CHANGED
|
@@ -30,7 +30,13 @@ export function registerPercyTools(server, config) {
|
|
|
30
30
|
],
|
|
31
31
|
};
|
|
32
32
|
});
|
|
33
|
-
tools.percyVisualTestIntegrationAgent = server.tool("percyVisualTestIntegrationAgent", SIMULATE_PERCY_CHANGE_DESCRIPTION, SetUpPercyParamsShape,
|
|
33
|
+
tools.percyVisualTestIntegrationAgent = server.tool("percyVisualTestIntegrationAgent", SIMULATE_PERCY_CHANGE_DESCRIPTION, SetUpPercyParamsShape, {
|
|
34
|
+
title: "Percy Visual Test Integration Agent",
|
|
35
|
+
readOnlyHint: true,
|
|
36
|
+
openWorldHint: false,
|
|
37
|
+
destructiveHint: false,
|
|
38
|
+
idempotentHint: true,
|
|
39
|
+
}, async (args) => {
|
|
34
40
|
try {
|
|
35
41
|
trackMCP("VisualTestIntegrationAgent", server.server.getClientVersion(), config);
|
|
36
42
|
return simulatePercyChangeHandler(args, config);
|
|
@@ -39,7 +45,13 @@ export function registerPercyTools(server, config) {
|
|
|
39
45
|
return handleMCPError("VisualTestIntegrationAgent", server, config, error);
|
|
40
46
|
}
|
|
41
47
|
});
|
|
42
|
-
tools.setupPercyVisualTesting = server.tool("expandPercyVisualTesting", SETUP_PERCY_DESCRIPTION, SetUpPercyParamsShape,
|
|
48
|
+
tools.setupPercyVisualTesting = server.tool("expandPercyVisualTesting", SETUP_PERCY_DESCRIPTION, SetUpPercyParamsShape, {
|
|
49
|
+
title: "Expand Percy Visual Testing",
|
|
50
|
+
readOnlyHint: true,
|
|
51
|
+
openWorldHint: false,
|
|
52
|
+
destructiveHint: false,
|
|
53
|
+
idempotentHint: true,
|
|
54
|
+
}, async (args) => {
|
|
43
55
|
try {
|
|
44
56
|
trackMCP("setupPercyVisualTesting", server.server.getClientVersion(), config);
|
|
45
57
|
return setUpPercyHandler(args, config);
|
|
@@ -48,7 +60,13 @@ export function registerPercyTools(server, config) {
|
|
|
48
60
|
return handleMCPError("setupPercyVisualTesting", server, config, error);
|
|
49
61
|
}
|
|
50
62
|
});
|
|
51
|
-
tools.addPercySnapshotCommands = server.tool("addPercySnapshotCommands", PERCY_SNAPSHOT_COMMANDS_DESCRIPTION, UpdateTestFileWithInstructionsParams,
|
|
63
|
+
tools.addPercySnapshotCommands = server.tool("addPercySnapshotCommands", PERCY_SNAPSHOT_COMMANDS_DESCRIPTION, UpdateTestFileWithInstructionsParams, {
|
|
64
|
+
title: "Add Percy Snapshot Commands",
|
|
65
|
+
readOnlyHint: true,
|
|
66
|
+
openWorldHint: false,
|
|
67
|
+
destructiveHint: false,
|
|
68
|
+
idempotentHint: true,
|
|
69
|
+
}, async (args) => {
|
|
52
70
|
try {
|
|
53
71
|
trackMCP("addPercySnapshotCommands", server.server.getClientVersion(), config);
|
|
54
72
|
return await updateTestsWithPercyCommands(args);
|
|
@@ -57,7 +75,13 @@ export function registerPercyTools(server, config) {
|
|
|
57
75
|
return handleMCPError("addPercySnapshotCommands", server, config, error);
|
|
58
76
|
}
|
|
59
77
|
});
|
|
60
|
-
tools.listTestFiles = server.tool("listTestFiles", LIST_TEST_FILES_DESCRIPTION, {},
|
|
78
|
+
tools.listTestFiles = server.tool("listTestFiles", LIST_TEST_FILES_DESCRIPTION, {}, {
|
|
79
|
+
title: "List Test Files",
|
|
80
|
+
readOnlyHint: true,
|
|
81
|
+
openWorldHint: false,
|
|
82
|
+
destructiveHint: false,
|
|
83
|
+
idempotentHint: true,
|
|
84
|
+
}, async () => {
|
|
61
85
|
try {
|
|
62
86
|
trackMCP("listTestFiles", server.server.getClientVersion(), config);
|
|
63
87
|
return addListTestFiles();
|
|
@@ -66,7 +90,13 @@ export function registerPercyTools(server, config) {
|
|
|
66
90
|
return handleMCPError("listTestFiles", server, config, error);
|
|
67
91
|
}
|
|
68
92
|
});
|
|
69
|
-
tools.runPercyScan = server.tool("runPercyScan", "Run a Percy visual test scan. Example prompts : Run this Percy build/scan. Never run percy scan/build without this tool", RunPercyScanParamsShape,
|
|
93
|
+
tools.runPercyScan = server.tool("runPercyScan", "Run a Percy visual test scan. Example prompts : Run this Percy build/scan. Never run percy scan/build without this tool", RunPercyScanParamsShape, {
|
|
94
|
+
title: "Run Percy Scan",
|
|
95
|
+
readOnlyHint: true,
|
|
96
|
+
openWorldHint: false,
|
|
97
|
+
destructiveHint: false,
|
|
98
|
+
idempotentHint: true,
|
|
99
|
+
}, async (args) => {
|
|
70
100
|
try {
|
|
71
101
|
trackMCP("runPercyScan", server.server.getClientVersion(), config);
|
|
72
102
|
return runPercyScan(args);
|
|
@@ -75,7 +105,13 @@ export function registerPercyTools(server, config) {
|
|
|
75
105
|
return handleMCPError("runPercyScan", server, config, error);
|
|
76
106
|
}
|
|
77
107
|
});
|
|
78
|
-
tools.fetchPercyChanges = server.tool("fetchPercyChanges", "Retrieves and summarizes all visual changes detected by Percy AI between the latest and previous builds, helping quickly review what has changed in your project.", FetchPercyChangesParamsShape,
|
|
108
|
+
tools.fetchPercyChanges = server.tool("fetchPercyChanges", "Retrieves and summarizes all visual changes detected by Percy AI between the latest and previous builds, helping quickly review what has changed in your project.", FetchPercyChangesParamsShape, {
|
|
109
|
+
title: "Fetch Percy Changes",
|
|
110
|
+
readOnlyHint: true,
|
|
111
|
+
openWorldHint: false,
|
|
112
|
+
destructiveHint: false,
|
|
113
|
+
idempotentHint: true,
|
|
114
|
+
}, async (args) => {
|
|
79
115
|
try {
|
|
80
116
|
trackMCP("fetchPercyChanges", server.server.getClientVersion(), config);
|
|
81
117
|
return await fetchPercyChanges(args, config);
|
|
@@ -84,7 +120,13 @@ export function registerPercyTools(server, config) {
|
|
|
84
120
|
return handleMCPError("fetchPercyChanges", server, config, error);
|
|
85
121
|
}
|
|
86
122
|
});
|
|
87
|
-
tools.managePercyBuildApproval = server.tool("managePercyBuildApproval", "Approve or reject a Percy build", ManagePercyBuildApprovalParamsShape,
|
|
123
|
+
tools.managePercyBuildApproval = server.tool("managePercyBuildApproval", "Approve or reject a Percy build", ManagePercyBuildApprovalParamsShape, {
|
|
124
|
+
title: "Manage Percy Build Approval",
|
|
125
|
+
readOnlyHint: false,
|
|
126
|
+
openWorldHint: false,
|
|
127
|
+
destructiveHint: true,
|
|
128
|
+
idempotentHint: true,
|
|
129
|
+
}, async (args) => {
|
|
88
130
|
try {
|
|
89
131
|
trackMCP("managePercyBuildApproval", server.server.getClientVersion(), config);
|
|
90
132
|
return await approveOrDeclinePercyBuild(args, config);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { TestStatus } from "./types.js";
|
|
3
|
+
export declare function getAutomationBaseUrl(): string;
|
|
3
4
|
export declare const FETCH_RCA_PARAMS: {
|
|
4
5
|
testId: z.ZodArray<z.ZodNumber>;
|
|
5
6
|
};
|
|
@@ -10,4 +11,5 @@ export declare const GET_BUILD_ID_PARAMS: {
|
|
|
10
11
|
export declare const LIST_TEST_IDS_PARAMS: {
|
|
11
12
|
buildId: z.ZodString;
|
|
12
13
|
status: z.ZodEnum<typeof TestStatus>;
|
|
14
|
+
includeFailureDetail: z.ZodOptional<z.ZodBoolean>;
|
|
13
15
|
};
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import appConfig from "../../config.js";
|
|
2
3
|
import { TestStatus } from "./types.js";
|
|
4
|
+
export function getAutomationBaseUrl() {
|
|
5
|
+
return appConfig.BROWSERSTACK_AUTOMATION_BASE_URL;
|
|
6
|
+
}
|
|
3
7
|
export const FETCH_RCA_PARAMS = {
|
|
4
8
|
testId: z
|
|
5
9
|
.array(z.number().int())
|
|
@@ -21,4 +25,8 @@ export const LIST_TEST_IDS_PARAMS = {
|
|
|
21
25
|
status: z
|
|
22
26
|
.nativeEnum(TestStatus)
|
|
23
27
|
.describe("Filter tests by status. If not provided, all tests are returned. Example for RCA usecase always use failed status"),
|
|
28
|
+
includeFailureDetail: z
|
|
29
|
+
.boolean()
|
|
30
|
+
.optional()
|
|
31
|
+
.describe("Add per-test failure signature for clustering. Default false."),
|
|
24
32
|
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
import { TestStatus, FailedTestInfo, TestDetails } from "./types.js";
|
|
2
|
-
export declare function getTestIds(buildId: string, authString: string, status?: TestStatus): Promise<FailedTestInfo[]>;
|
|
3
|
-
export declare function
|
|
2
|
+
export declare function getTestIds(buildId: string, authString: string, status?: TestStatus, includeFailureDetail?: boolean): Promise<FailedTestInfo[]>;
|
|
3
|
+
export declare function extractTestIds(hierarchy: TestDetails[], status?: TestStatus, includeFailureDetail?: boolean): FailedTestInfo[];
|
|
4
|
+
export declare const extractFailedTestIds: typeof extractTestIds;
|
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
import logger from "../../logger.js";
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
import { getAutomationBaseUrl } from "./constants.js";
|
|
3
|
+
import { TestStatus, } from "./types.js";
|
|
4
|
+
// Keeps the response payload lean — never full stack traces.
|
|
5
|
+
const ERROR_SUMMARY_MAX = 200;
|
|
6
|
+
// Safety bound on pagination; hitting it is logged, never silent.
|
|
7
|
+
const MAX_PAGES = 100;
|
|
8
|
+
export async function getTestIds(buildId, authString, status, includeFailureDetail = false) {
|
|
9
|
+
// No `status` → returns ALL tests; a `status` narrows the query.
|
|
10
|
+
const baseUrl = `${getAutomationBaseUrl()}/ext/v1/builds/${buildId}/testRuns`;
|
|
4
11
|
let url = status ? `${baseUrl}?test_statuses=${status}` : baseUrl;
|
|
5
|
-
let
|
|
12
|
+
let allTests = [];
|
|
6
13
|
let requestNumber = 0;
|
|
7
14
|
// Construct Basic auth header
|
|
8
15
|
const encodedCredentials = Buffer.from(authString).toString("base64");
|
|
@@ -20,15 +27,16 @@ export async function getTestIds(buildId, authString, status) {
|
|
|
20
27
|
throw new Error(`Failed to fetch test runs: ${response.status} ${response.statusText}`);
|
|
21
28
|
}
|
|
22
29
|
const data = (await response.json());
|
|
23
|
-
// Extract
|
|
30
|
+
// Extract test IDs from the current page (all tests unless narrowed).
|
|
24
31
|
if (data.hierarchy && data.hierarchy.length > 0) {
|
|
25
|
-
const
|
|
26
|
-
|
|
32
|
+
const currentTests = extractTestIds(data.hierarchy, status, includeFailureDetail);
|
|
33
|
+
allTests = allTests.concat(currentTests);
|
|
27
34
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
35
|
+
if (!data.pagination?.has_next || !data.pagination.next_page) {
|
|
36
|
+
break;
|
|
37
|
+
}
|
|
38
|
+
if (requestNumber >= MAX_PAGES) {
|
|
39
|
+
logger.warn(`listTestIds: hit MAX_PAGES (${MAX_PAGES}) for build ${buildId}; result may be partial`);
|
|
32
40
|
break;
|
|
33
41
|
}
|
|
34
42
|
const params = {
|
|
@@ -38,31 +46,92 @@ export async function getTestIds(buildId, authString, status) {
|
|
|
38
46
|
params.test_statuses = status;
|
|
39
47
|
url = `${baseUrl}?${new URLSearchParams(params).toString()}`;
|
|
40
48
|
}
|
|
41
|
-
|
|
42
|
-
return allFailedTests;
|
|
49
|
+
return allTests;
|
|
43
50
|
}
|
|
44
51
|
catch (error) {
|
|
45
|
-
logger.error("Error fetching
|
|
52
|
+
logger.error("Error fetching test runs:", error);
|
|
46
53
|
throw error;
|
|
47
54
|
}
|
|
48
55
|
}
|
|
49
|
-
export function
|
|
50
|
-
let
|
|
56
|
+
export function extractTestIds(hierarchy, status, includeFailureDetail = false) {
|
|
57
|
+
let tests = [];
|
|
51
58
|
for (const node of hierarchy) {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
59
|
+
// observability_url `details=<id>` filters out suite/hook nodes. run_count
|
|
60
|
+
// is not required — JUnit-uploaded builds report run_count=0 for real tests too.
|
|
61
|
+
const nodeStatus = node.details?.status;
|
|
62
|
+
const statusMatches = status === undefined || nodeStatus === status;
|
|
63
|
+
if (statusMatches && node.details?.observability_url) {
|
|
64
|
+
const idMatch = node.details.observability_url.match(/details=(\d+)/);
|
|
65
|
+
if (idMatch) {
|
|
66
|
+
const entry = {
|
|
67
|
+
test_id: idMatch[1],
|
|
68
|
+
test_name: node.display_name || `Test ${idMatch[1]}`,
|
|
69
|
+
status: nodeStatus,
|
|
70
|
+
};
|
|
71
|
+
// Failure signatures only exist for failed tests; include when asked.
|
|
72
|
+
if (includeFailureDetail && nodeStatus === TestStatus.FAILED) {
|
|
73
|
+
const signature = buildFailureSignature(node.details);
|
|
74
|
+
if (signature)
|
|
75
|
+
entry.failure = signature;
|
|
60
76
|
}
|
|
77
|
+
tests.push(entry);
|
|
61
78
|
}
|
|
62
79
|
}
|
|
63
80
|
if (node.children && node.children.length > 0) {
|
|
64
|
-
|
|
81
|
+
tests = tests.concat(extractTestIds(node.children, status, includeFailureDetail));
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return tests;
|
|
85
|
+
}
|
|
86
|
+
// Back-compat alias — prefer extractTestIds. Kept so existing imports/tests
|
|
87
|
+
// referencing the old name continue to resolve.
|
|
88
|
+
export const extractFailedTestIds = extractTestIds;
|
|
89
|
+
// Build a trimmed failure signature from a test node's `details`. Returns
|
|
90
|
+
// undefined when no signal is available so the field is simply omitted.
|
|
91
|
+
function buildFailureSignature(details) {
|
|
92
|
+
if (!details)
|
|
93
|
+
return undefined;
|
|
94
|
+
const signature = {};
|
|
95
|
+
if (details.failure_categories != null) {
|
|
96
|
+
signature.category = Array.isArray(details.failure_categories)
|
|
97
|
+
? details.failure_categories.filter(Boolean).join(", ")
|
|
98
|
+
: String(details.failure_categories);
|
|
99
|
+
}
|
|
100
|
+
const errorSummary = extractFirstFailureLine(details);
|
|
101
|
+
if (errorSummary)
|
|
102
|
+
signature.error_summary = errorSummary;
|
|
103
|
+
if (details.file_path)
|
|
104
|
+
signature.file_path = String(details.file_path);
|
|
105
|
+
if (typeof details.is_flaky === "boolean")
|
|
106
|
+
signature.is_flaky = details.is_flaky;
|
|
107
|
+
if (typeof details.is_always_failing === "boolean")
|
|
108
|
+
signature.is_always_failing = details.is_always_failing;
|
|
109
|
+
if (typeof details.is_new_failure === "boolean")
|
|
110
|
+
signature.is_new_failure = details.is_new_failure;
|
|
111
|
+
return Object.keys(signature).length > 0 ? signature : undefined;
|
|
112
|
+
}
|
|
113
|
+
// First non-empty line of the first retry's TEST_FAILURE log, capped. Handles
|
|
114
|
+
// both string entries and object entries ({ message } / { text }).
|
|
115
|
+
function extractFirstFailureLine(details) {
|
|
116
|
+
const retries = details?.retries;
|
|
117
|
+
if (!Array.isArray(retries))
|
|
118
|
+
return undefined;
|
|
119
|
+
for (const retry of retries) {
|
|
120
|
+
const failures = retry?.logs?.TEST_FAILURE;
|
|
121
|
+
if (!failures)
|
|
122
|
+
continue;
|
|
123
|
+
const entries = Array.isArray(failures) ? failures : [failures];
|
|
124
|
+
for (const failure of entries) {
|
|
125
|
+
const text = typeof failure === "string"
|
|
126
|
+
? failure
|
|
127
|
+
: (failure?.message ?? failure?.text ?? "");
|
|
128
|
+
const firstLine = String(text)
|
|
129
|
+
.split("\n")
|
|
130
|
+
.map((line) => line.trim())
|
|
131
|
+
.find((line) => line.length > 0);
|
|
132
|
+
if (firstLine)
|
|
133
|
+
return firstLine.slice(0, ERROR_SUMMARY_MAX);
|
|
65
134
|
}
|
|
66
135
|
}
|
|
67
|
-
return
|
|
136
|
+
return undefined;
|
|
68
137
|
}
|