@browserstack/mcp-server 1.2.3-beta.1 → 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.
- package/README.md +88 -2
- package/dist/lib/device-cache.js +20 -17
- package/dist/lib/inmemory-store.d.ts +1 -0
- package/dist/lib/inmemory-store.js +1 -0
- package/dist/lib/utils.d.ts +5 -0
- package/dist/lib/utils.js +27 -0
- package/dist/server-factory.js +6 -0
- package/dist/tools/add-percy-snapshots.d.ts +5 -0
- package/dist/tools/add-percy-snapshots.js +17 -0
- package/dist/tools/appautomate-utils/appium-sdk/types.d.ts +2 -2
- package/dist/tools/appautomate-utils/appium-sdk/types.js +2 -9
- package/dist/tools/appautomate-utils/appium-sdk/utils.js +3 -0
- package/dist/tools/bstack-sdk.d.ts +2 -15
- package/dist/tools/bstack-sdk.js +10 -119
- package/dist/tools/build-insights.d.ts +7 -0
- package/dist/tools/build-insights.js +67 -0
- package/dist/tools/list-test-files.d.ts +2 -0
- package/dist/tools/list-test-files.js +36 -0
- package/dist/tools/percy-sdk.d.ts +4 -0
- package/dist/tools/percy-sdk.js +71 -0
- package/dist/tools/percy-snapshot-utils/constants.d.ts +16 -0
- package/dist/tools/percy-snapshot-utils/constants.js +500 -0
- package/dist/tools/percy-snapshot-utils/detect-test-files.d.ts +10 -0
- package/dist/tools/percy-snapshot-utils/detect-test-files.js +175 -0
- package/dist/tools/percy-snapshot-utils/types.d.ts +15 -0
- package/dist/tools/percy-snapshot-utils/utils.d.ts +4 -0
- package/dist/tools/percy-snapshot-utils/utils.js +30 -0
- package/dist/tools/rca-agent-utils/constants.d.ts +13 -0
- package/dist/tools/rca-agent-utils/constants.js +24 -0
- package/dist/tools/rca-agent-utils/format-rca.d.ts +1 -0
- package/dist/tools/rca-agent-utils/format-rca.js +37 -0
- package/dist/tools/rca-agent-utils/get-build-id.d.ts +1 -0
- package/dist/tools/rca-agent-utils/get-build-id.js +18 -0
- package/dist/tools/rca-agent-utils/get-failed-test-id.d.ts +2 -0
- package/dist/tools/rca-agent-utils/get-failed-test-id.js +69 -0
- package/dist/tools/rca-agent-utils/rca-data.d.ts +9 -0
- package/dist/tools/rca-agent-utils/rca-data.js +196 -0
- package/dist/tools/rca-agent-utils/types.d.ts +48 -0
- package/dist/tools/rca-agent-utils/types.js +20 -0
- package/dist/tools/rca-agent.d.ts +14 -0
- package/dist/tools/rca-agent.js +119 -0
- package/dist/tools/review-agent-utils/build-counts.d.ts +7 -0
- package/dist/tools/review-agent-utils/build-counts.js +44 -0
- package/dist/tools/review-agent-utils/percy-approve-reject.d.ts +6 -0
- package/dist/tools/review-agent-utils/percy-approve-reject.js +39 -0
- package/dist/tools/review-agent-utils/percy-diffs.d.ts +9 -0
- package/dist/tools/review-agent-utils/percy-diffs.js +35 -0
- package/dist/tools/review-agent-utils/percy-snapshots.d.ts +11 -0
- package/dist/tools/review-agent-utils/percy-snapshots.js +58 -0
- package/dist/tools/review-agent.d.ts +5 -0
- package/dist/tools/review-agent.js +56 -0
- package/dist/tools/run-percy-scan.d.ts +8 -0
- package/dist/tools/run-percy-scan.js +37 -0
- package/dist/tools/sdk-utils/{commands.d.ts → bstack/commands.d.ts} +1 -1
- package/dist/tools/sdk-utils/bstack/commands.js +88 -0
- package/dist/tools/sdk-utils/bstack/configUtils.d.ts +4 -0
- package/dist/tools/sdk-utils/bstack/configUtils.js +66 -0
- package/dist/tools/sdk-utils/bstack/constants.d.ts +58 -0
- package/dist/tools/sdk-utils/{constants.js → bstack/constants.js} +117 -78
- package/dist/tools/sdk-utils/{constants.d.ts → bstack/frameworks.d.ts} +1 -1
- package/dist/tools/sdk-utils/bstack/frameworks.js +57 -0
- package/dist/tools/sdk-utils/bstack/index.d.ts +4 -0
- package/dist/tools/sdk-utils/bstack/index.js +5 -0
- package/dist/tools/sdk-utils/bstack/sdkHandler.d.ts +4 -0
- package/dist/tools/sdk-utils/bstack/sdkHandler.js +74 -0
- package/dist/tools/sdk-utils/common/constants.d.ts +10 -0
- package/dist/tools/sdk-utils/common/constants.js +86 -0
- package/dist/tools/sdk-utils/common/formatUtils.d.ts +5 -0
- package/dist/tools/sdk-utils/common/formatUtils.js +27 -0
- package/dist/tools/sdk-utils/common/index.d.ts +3 -0
- package/dist/tools/sdk-utils/common/index.js +4 -0
- package/dist/tools/sdk-utils/common/instructionUtils.d.ts +8 -0
- package/dist/tools/sdk-utils/common/instructionUtils.js +20 -0
- package/dist/tools/sdk-utils/common/schema.d.ts +73 -0
- package/dist/tools/sdk-utils/common/schema.js +51 -0
- package/dist/tools/sdk-utils/common/types.d.ts +66 -0
- package/dist/tools/sdk-utils/{types.js → common/types.js} +15 -2
- package/dist/tools/sdk-utils/common/utils.d.ts +25 -0
- package/dist/tools/sdk-utils/common/utils.js +90 -0
- package/dist/tools/sdk-utils/handler.d.ts +4 -0
- package/dist/tools/sdk-utils/handler.js +119 -0
- package/dist/tools/sdk-utils/percy-automate/constants.d.ts +11 -0
- package/dist/tools/sdk-utils/percy-automate/constants.js +338 -0
- package/dist/tools/sdk-utils/percy-automate/frameworks.d.ts +8 -0
- package/dist/tools/sdk-utils/percy-automate/frameworks.js +50 -0
- package/dist/tools/sdk-utils/percy-automate/handler.d.ts +3 -0
- package/dist/tools/sdk-utils/percy-automate/handler.js +30 -0
- package/dist/tools/sdk-utils/percy-automate/index.d.ts +1 -0
- package/dist/tools/sdk-utils/percy-automate/index.js +2 -0
- package/dist/tools/sdk-utils/percy-automate/types.d.ts +13 -0
- package/dist/tools/sdk-utils/percy-automate/types.js +1 -0
- package/dist/tools/sdk-utils/percy-bstack/constants.d.ts +4 -0
- package/dist/tools/sdk-utils/{percy → percy-bstack}/constants.js +13 -39
- package/dist/tools/sdk-utils/percy-bstack/frameworks.d.ts +2 -0
- package/dist/tools/sdk-utils/percy-bstack/frameworks.js +27 -0
- package/dist/tools/sdk-utils/percy-bstack/handler.d.ts +4 -0
- package/dist/tools/sdk-utils/percy-bstack/handler.js +99 -0
- package/dist/tools/sdk-utils/percy-bstack/index.d.ts +4 -0
- package/dist/tools/sdk-utils/percy-bstack/index.js +4 -0
- package/dist/tools/sdk-utils/percy-bstack/instructions.d.ts +7 -0
- package/dist/tools/sdk-utils/{percy → percy-bstack}/instructions.js +5 -9
- package/dist/tools/sdk-utils/percy-bstack/types.d.ts +13 -0
- package/dist/tools/sdk-utils/percy-bstack/types.js +5 -0
- package/dist/tools/sdk-utils/percy-web/constants.d.ts +41 -0
- package/dist/tools/sdk-utils/percy-web/constants.js +883 -0
- package/dist/tools/sdk-utils/percy-web/fetchPercyToken.d.ts +4 -0
- package/dist/tools/sdk-utils/percy-web/fetchPercyToken.js +32 -0
- package/dist/tools/sdk-utils/percy-web/frameworks.d.ts +7 -0
- package/dist/tools/sdk-utils/percy-web/frameworks.js +103 -0
- package/dist/tools/sdk-utils/percy-web/handler.d.ts +4 -0
- package/dist/tools/sdk-utils/percy-web/handler.js +27 -0
- package/dist/tools/sdk-utils/percy-web/index.d.ts +4 -0
- package/dist/tools/sdk-utils/percy-web/index.js +4 -0
- package/dist/tools/sdk-utils/percy-web/types.d.ts +12 -0
- package/dist/tools/sdk-utils/percy-web/types.js +1 -0
- package/dist/tools/testmanagement-utils/create-testrun.d.ts +4 -4
- package/dist/tools/testmanagement-utils/update-testrun.d.ts +4 -4
- package/package.json +2 -1
- package/dist/tools/sdk-utils/commands.js +0 -65
- package/dist/tools/sdk-utils/instructions.d.ts +0 -6
- package/dist/tools/sdk-utils/instructions.js +0 -99
- package/dist/tools/sdk-utils/percy/constants.d.ts +0 -3
- package/dist/tools/sdk-utils/percy/instructions.d.ts +0 -10
- package/dist/tools/sdk-utils/percy/types.d.ts +0 -5
- package/dist/tools/sdk-utils/types.d.ts +0 -40
- /package/dist/tools/{sdk-utils/percy → percy-snapshot-utils}/types.js +0 -0
|
@@ -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,44 @@
|
|
|
1
|
+
// Utility for fetching the count of Percy builds and orgId.
|
|
2
|
+
export async function getPercyBuildCount(percyToken) {
|
|
3
|
+
const apiUrl = `https://percy.io/api/v1/builds`;
|
|
4
|
+
const response = await fetch(apiUrl, {
|
|
5
|
+
headers: {
|
|
6
|
+
Authorization: `Token token=${percyToken}`,
|
|
7
|
+
"Content-Type": "application/json",
|
|
8
|
+
},
|
|
9
|
+
});
|
|
10
|
+
if (!response.ok) {
|
|
11
|
+
throw new Error(`Failed to fetch Percy builds: ${response.statusText}`);
|
|
12
|
+
}
|
|
13
|
+
const data = await response.json();
|
|
14
|
+
const builds = data.data ?? [];
|
|
15
|
+
const included = data.included ?? [];
|
|
16
|
+
let isFirstBuild = false;
|
|
17
|
+
let lastBuildId;
|
|
18
|
+
let orgId;
|
|
19
|
+
let browserIds = [];
|
|
20
|
+
if (builds.length === 0) {
|
|
21
|
+
return {
|
|
22
|
+
noBuilds: true,
|
|
23
|
+
isFirstBuild: false,
|
|
24
|
+
lastBuildId: undefined,
|
|
25
|
+
orgId,
|
|
26
|
+
browserIds: [],
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
isFirstBuild = builds.length === 1;
|
|
31
|
+
lastBuildId = builds[0].id;
|
|
32
|
+
}
|
|
33
|
+
// Extract browserIds from the latest build if available
|
|
34
|
+
browserIds =
|
|
35
|
+
builds[0]?.relationships?.browsers?.data
|
|
36
|
+
?.map((b) => b.id)
|
|
37
|
+
?.filter((id) => typeof id === "string") ?? [];
|
|
38
|
+
// Extract orgId from the `included` projects block
|
|
39
|
+
const project = included.find((item) => item.type === "projects");
|
|
40
|
+
if (project?.relationships?.organization?.data?.id) {
|
|
41
|
+
orgId = project.relationships.organization.data.id;
|
|
42
|
+
}
|
|
43
|
+
return { noBuilds: false, isFirstBuild, lastBuildId, orgId, browserIds };
|
|
44
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { BrowserStackConfig } from "../../lib/types.js";
|
|
2
|
+
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
3
|
+
export declare function approveOrDeclinePercyBuild(args: {
|
|
4
|
+
buildId: string;
|
|
5
|
+
action: "approve" | "unapprove" | "reject";
|
|
6
|
+
}, config: BrowserStackConfig): Promise<CallToolResult>;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { getBrowserStackAuth } from "../../lib/get-auth.js";
|
|
2
|
+
export async function approveOrDeclinePercyBuild(args, config) {
|
|
3
|
+
const { buildId, action } = args;
|
|
4
|
+
// Get Basic Auth credentials
|
|
5
|
+
const authString = getBrowserStackAuth(config);
|
|
6
|
+
const auth = Buffer.from(authString).toString("base64");
|
|
7
|
+
// Prepare request body
|
|
8
|
+
const body = {
|
|
9
|
+
data: {
|
|
10
|
+
type: "reviews",
|
|
11
|
+
attributes: { action },
|
|
12
|
+
relationships: {
|
|
13
|
+
build: { data: { type: "builds", id: buildId } },
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
// Send request to Percy API
|
|
18
|
+
const response = await fetch("https://percy.io/api/v1/reviews", {
|
|
19
|
+
method: "POST",
|
|
20
|
+
headers: {
|
|
21
|
+
"Content-Type": "application/json",
|
|
22
|
+
Authorization: `Basic ${auth}`,
|
|
23
|
+
},
|
|
24
|
+
body: JSON.stringify(body),
|
|
25
|
+
});
|
|
26
|
+
if (!response.ok) {
|
|
27
|
+
const errorText = await response.text();
|
|
28
|
+
throw new Error(`Percy build ${action} failed: ${response.status} ${errorText}`);
|
|
29
|
+
}
|
|
30
|
+
const result = await response.json();
|
|
31
|
+
return {
|
|
32
|
+
content: [
|
|
33
|
+
{
|
|
34
|
+
type: "text",
|
|
35
|
+
text: `Percy build ${buildId} was ${result.data.attributes["review-state"]} by ${result.data.attributes["action-performed-by"].user_name}`,
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface PercySnapshotDiff {
|
|
2
|
+
id: string;
|
|
3
|
+
name: string | null;
|
|
4
|
+
title: string;
|
|
5
|
+
description: string | null;
|
|
6
|
+
coordinates: any;
|
|
7
|
+
}
|
|
8
|
+
export declare function getPercySnapshotDiff(snapshotId: string, percyToken: string): Promise<PercySnapshotDiff[]>;
|
|
9
|
+
export declare function getPercySnapshotDiffs(snapshotIds: string[], percyToken: string): Promise<PercySnapshotDiff[]>;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export async function getPercySnapshotDiff(snapshotId, percyToken) {
|
|
2
|
+
const apiUrl = `https://percy.io/api/v1/snapshots/${snapshotId}`;
|
|
3
|
+
const response = await fetch(apiUrl, {
|
|
4
|
+
headers: {
|
|
5
|
+
Authorization: `Token token=${percyToken}`,
|
|
6
|
+
"Content-Type": "application/json",
|
|
7
|
+
},
|
|
8
|
+
});
|
|
9
|
+
if (!response.ok) {
|
|
10
|
+
throw new Error(`Failed to fetch Percy snapshot ${snapshotId}: ${response.statusText}`);
|
|
11
|
+
}
|
|
12
|
+
const data = await response.json();
|
|
13
|
+
const pageUrl = data.data.attributes?.name || null;
|
|
14
|
+
const changes = [];
|
|
15
|
+
const comparisons = data.included?.filter((item) => item.type === "comparisons") ?? [];
|
|
16
|
+
for (const comparison of comparisons) {
|
|
17
|
+
const appliedRegions = comparison.attributes?.["applied-regions"] ?? [];
|
|
18
|
+
for (const region of appliedRegions) {
|
|
19
|
+
if (region.ignored)
|
|
20
|
+
continue;
|
|
21
|
+
changes.push({
|
|
22
|
+
id: String(region.id),
|
|
23
|
+
name: pageUrl,
|
|
24
|
+
title: region.change_title,
|
|
25
|
+
description: region.change_description ?? null,
|
|
26
|
+
coordinates: region.coordinates ?? null,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return changes;
|
|
31
|
+
}
|
|
32
|
+
export async function getPercySnapshotDiffs(snapshotIds, percyToken) {
|
|
33
|
+
const allDiffs = await Promise.all(snapshotIds.map((id) => getPercySnapshotDiff(id, percyToken)));
|
|
34
|
+
return allDiffs.flat();
|
|
35
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { BrowserStackConfig } from "../../lib/types.js";
|
|
2
|
+
export declare function getChangedPercySnapshotIds(buildId: string, config: BrowserStackConfig, orgId: string | undefined, browserIds: string[]): Promise<string[]>;
|
|
3
|
+
export declare function constructPercyBuildItemsUrl({ buildId, orgId, category, subcategories, browserIds, widths, groupSnapshotsBy, }: {
|
|
4
|
+
buildId: string;
|
|
5
|
+
orgId: string;
|
|
6
|
+
category?: string[];
|
|
7
|
+
subcategories?: string[];
|
|
8
|
+
browserIds?: string[];
|
|
9
|
+
widths?: string[];
|
|
10
|
+
groupSnapshotsBy?: string;
|
|
11
|
+
}): string;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { getBrowserStackAuth } from "../../lib/get-auth.js";
|
|
2
|
+
import { sanitizeUrlParam } from "../../lib/utils.js";
|
|
3
|
+
// Utility for fetching only the IDs of changed Percy snapshots for a given build.
|
|
4
|
+
export async function getChangedPercySnapshotIds(buildId, config, orgId, browserIds) {
|
|
5
|
+
if (!buildId || !orgId) {
|
|
6
|
+
throw new Error("Failed to fetch AI Summary: Missing build ID or organization ID");
|
|
7
|
+
}
|
|
8
|
+
const urlStr = constructPercyBuildItemsUrl({
|
|
9
|
+
buildId,
|
|
10
|
+
orgId,
|
|
11
|
+
category: ["changed"],
|
|
12
|
+
subcategories: ["unreviewed", "approved", "changes_requested"],
|
|
13
|
+
groupSnapshotsBy: "similar_diff",
|
|
14
|
+
browserIds,
|
|
15
|
+
widths: ["375", "1280", "1920"],
|
|
16
|
+
});
|
|
17
|
+
const authString = getBrowserStackAuth(config);
|
|
18
|
+
const auth = Buffer.from(authString).toString("base64");
|
|
19
|
+
const response = await fetch(urlStr, {
|
|
20
|
+
headers: {
|
|
21
|
+
Authorization: `Basic ${auth}`,
|
|
22
|
+
"Content-Type": "application/json",
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
if (!response.ok) {
|
|
26
|
+
throw new Error(`Failed to fetch changed Percy snapshots: ${response.status} ${response.statusText}`);
|
|
27
|
+
}
|
|
28
|
+
const responseData = await response.json();
|
|
29
|
+
const buildItems = responseData.data ?? [];
|
|
30
|
+
if (buildItems.length === 0) {
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
const snapshotIds = buildItems
|
|
34
|
+
.flatMap((item) => item.attributes?.["snapshot-ids"] ?? [])
|
|
35
|
+
.map((id) => String(id));
|
|
36
|
+
return snapshotIds;
|
|
37
|
+
}
|
|
38
|
+
export function constructPercyBuildItemsUrl({ buildId, orgId, category = [], subcategories = [], browserIds = [], widths = [], groupSnapshotsBy, }) {
|
|
39
|
+
const url = new URL("https://percy.io/api/v1/build-items");
|
|
40
|
+
url.searchParams.set("filter[build-id]", sanitizeUrlParam(buildId));
|
|
41
|
+
url.searchParams.set("filter[organization-id]", sanitizeUrlParam(orgId));
|
|
42
|
+
if (category && category.length > 0) {
|
|
43
|
+
category.forEach((cat) => url.searchParams.append("filter[category][]", sanitizeUrlParam(cat)));
|
|
44
|
+
}
|
|
45
|
+
if (subcategories && subcategories.length > 0) {
|
|
46
|
+
subcategories.forEach((sub) => url.searchParams.append("filter[subcategories][]", sanitizeUrlParam(sub)));
|
|
47
|
+
}
|
|
48
|
+
if (browserIds && browserIds.length > 0) {
|
|
49
|
+
browserIds.forEach((id) => url.searchParams.append("filter[browser_ids][]", sanitizeUrlParam(id)));
|
|
50
|
+
}
|
|
51
|
+
if (widths && widths.length > 0) {
|
|
52
|
+
widths.forEach((w) => url.searchParams.append("filter[widths][]", sanitizeUrlParam(w)));
|
|
53
|
+
}
|
|
54
|
+
if (groupSnapshotsBy) {
|
|
55
|
+
url.searchParams.set("filter[group_snapshots_by]", sanitizeUrlParam(groupSnapshotsBy));
|
|
56
|
+
}
|
|
57
|
+
return url.toString();
|
|
58
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { getBrowserStackAuth } from "../lib/get-auth.js";
|
|
2
|
+
import { getPercyBuildCount } from "./review-agent-utils/build-counts.js";
|
|
3
|
+
import { getChangedPercySnapshotIds } from "./review-agent-utils/percy-snapshots.js";
|
|
4
|
+
import { PercyIntegrationTypeEnum } from "./sdk-utils/common/types.js";
|
|
5
|
+
import { fetchPercyToken } from "./sdk-utils/percy-web/fetchPercyToken.js";
|
|
6
|
+
import { getPercySnapshotDiffs, } from "./review-agent-utils/percy-diffs.js";
|
|
7
|
+
export async function fetchPercyChanges(args, config) {
|
|
8
|
+
const { project_name } = args;
|
|
9
|
+
const authorization = getBrowserStackAuth(config);
|
|
10
|
+
// Get Percy token for the project
|
|
11
|
+
const percyToken = await fetchPercyToken(project_name, authorization, {
|
|
12
|
+
type: PercyIntegrationTypeEnum.WEB,
|
|
13
|
+
});
|
|
14
|
+
// Get build info (noBuilds, isFirstBuild, lastBuildId)
|
|
15
|
+
const { noBuilds, isFirstBuild, lastBuildId, orgId, browserIds } = await getPercyBuildCount(percyToken);
|
|
16
|
+
if (noBuilds) {
|
|
17
|
+
return {
|
|
18
|
+
content: [
|
|
19
|
+
{
|
|
20
|
+
type: "text",
|
|
21
|
+
text: "No Percy builds found. Please run your first Percy scan to start visual testing.",
|
|
22
|
+
},
|
|
23
|
+
],
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
if (isFirstBuild || !lastBuildId) {
|
|
27
|
+
return {
|
|
28
|
+
content: [
|
|
29
|
+
{
|
|
30
|
+
type: "text",
|
|
31
|
+
text: "This is the first Percy build. No baseline exists to compare changes.",
|
|
32
|
+
},
|
|
33
|
+
],
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
// Get snapshot IDs for the latest build
|
|
37
|
+
const snapshotIds = await getChangedPercySnapshotIds(lastBuildId, config, orgId, browserIds);
|
|
38
|
+
// Fetch all diffs concurrently and flatten results
|
|
39
|
+
const allDiffs = await getPercySnapshotDiffs(snapshotIds, percyToken);
|
|
40
|
+
if (allDiffs.length === 0) {
|
|
41
|
+
return {
|
|
42
|
+
content: [
|
|
43
|
+
{
|
|
44
|
+
type: "text",
|
|
45
|
+
text: "AI Summary is not yet available for this build/framework. There may still be visual changes—please review the build on the dashboard.",
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
content: allDiffs.map((diff) => ({
|
|
52
|
+
type: "text",
|
|
53
|
+
text: `${diff.name} → ${diff.title}: ${diff.description ?? ""}`,
|
|
54
|
+
})),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
2
|
+
import { PercyIntegrationTypeEnum } from "./sdk-utils/common/types.js";
|
|
3
|
+
import { BrowserStackConfig } from "../lib/types.js";
|
|
4
|
+
export declare function runPercyScan(args: {
|
|
5
|
+
projectName: string;
|
|
6
|
+
integrationType: PercyIntegrationTypeEnum;
|
|
7
|
+
instruction?: string;
|
|
8
|
+
}, config: BrowserStackConfig): Promise<CallToolResult>;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { getBrowserStackAuth } from "../lib/get-auth.js";
|
|
2
|
+
import { fetchPercyToken } from "./sdk-utils/percy-web/fetchPercyToken.js";
|
|
3
|
+
export async function runPercyScan(args, config) {
|
|
4
|
+
const { projectName, integrationType, instruction } = args;
|
|
5
|
+
const authorization = getBrowserStackAuth(config);
|
|
6
|
+
const percyToken = await fetchPercyToken(projectName, authorization, {
|
|
7
|
+
type: integrationType,
|
|
8
|
+
});
|
|
9
|
+
const steps = [generatePercyTokenInstructions(percyToken)];
|
|
10
|
+
if (instruction) {
|
|
11
|
+
steps.push(`Use the provided test command with Percy:\n${instruction}`, `If this command fails or is incorrect, fall back to the default approach below.`);
|
|
12
|
+
}
|
|
13
|
+
steps.push(`Attempt to infer the project's test command from context (high confidence commands first):
|
|
14
|
+
- Java → mvn test
|
|
15
|
+
- Python → pytest
|
|
16
|
+
- Node.js → npm test or yarn test
|
|
17
|
+
- Cypress → cypress run
|
|
18
|
+
or from package.json scripts`, `Wrap the inferred command with Percy:\nnpx percy exec -- <test command>`, `If the test command cannot be inferred confidently, ask the user directly for the correct test command.`);
|
|
19
|
+
const instructionContext = steps
|
|
20
|
+
.map((step, index) => `${index + 1}. ${step}`)
|
|
21
|
+
.join("\n\n");
|
|
22
|
+
return {
|
|
23
|
+
content: [
|
|
24
|
+
{
|
|
25
|
+
type: "text",
|
|
26
|
+
text: instructionContext,
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
function generatePercyTokenInstructions(percyToken) {
|
|
32
|
+
return `Set the environment variable for your project:
|
|
33
|
+
|
|
34
|
+
export PERCY_TOKEN="${percyToken}"
|
|
35
|
+
|
|
36
|
+
(For Windows: use 'setx PERCY_TOKEN "${percyToken}"' or 'set PERCY_TOKEN=${percyToken}' as appropriate.)`;
|
|
37
|
+
}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { SDKSupportedLanguage } from "
|
|
1
|
+
import { SDKSupportedLanguage } from "../common/types.js";
|
|
2
2
|
export declare function getSDKPrefixCommand(language: SDKSupportedLanguage, framework: string, username: string, accessKey: string): string;
|
|
3
3
|
export declare function getJavaFrameworkForMaven(framework: string): string;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// Constants
|
|
2
|
+
const MAVEN_ARCHETYPE_GROUP_ID = "com.browserstack";
|
|
3
|
+
const MAVEN_ARCHETYPE_ARTIFACT_ID = "browserstack-sdk-archetype-integrate";
|
|
4
|
+
const MAVEN_ARCHETYPE_VERSION = "1.0";
|
|
5
|
+
// Mapping of test frameworks to their corresponding Maven archetype framework names
|
|
6
|
+
const JAVA_FRAMEWORK_MAP = {
|
|
7
|
+
testng: "testng",
|
|
8
|
+
junit5: "junit5",
|
|
9
|
+
junit4: "junit4",
|
|
10
|
+
cucumber: "cucumber-testng",
|
|
11
|
+
};
|
|
12
|
+
// Template for Node.js SDK setup instructions
|
|
13
|
+
const NODEJS_SDK_INSTRUCTIONS = (username, accessKey) => `---STEP---
|
|
14
|
+
Install BrowserStack Node SDK using command:
|
|
15
|
+
\`\`\`bash
|
|
16
|
+
npm i -D browserstack-node-sdk@latest
|
|
17
|
+
\`\`\`
|
|
18
|
+
---STEP---
|
|
19
|
+
Run the following command to setup browserstack sdk:
|
|
20
|
+
\`\`\`bash
|
|
21
|
+
npx setup --username ${username} --key ${accessKey}
|
|
22
|
+
\`\`\``;
|
|
23
|
+
// Template for Gradle setup instructions (platform-independent)
|
|
24
|
+
const GRADLE_SETUP_INSTRUCTIONS = `
|
|
25
|
+
**For Gradle setup:**
|
|
26
|
+
1. Add browserstack-java-sdk to dependencies:
|
|
27
|
+
compileOnly 'com.browserstack:browserstack-java-sdk:latest.release'
|
|
28
|
+
|
|
29
|
+
2. Add browserstackSDK path variable:
|
|
30
|
+
def browserstackSDKArtifact = configurations.compileClasspath.resolvedConfiguration.resolvedArtifacts.find { it.name == 'browserstack-java-sdk' }
|
|
31
|
+
|
|
32
|
+
3. Add javaagent to gradle tasks:
|
|
33
|
+
jvmArgs "-javaagent:\${browserstackSDKArtifact.file}"
|
|
34
|
+
`;
|
|
35
|
+
// Generates Maven archetype command for Windows platform
|
|
36
|
+
function getMavenCommandForWindows(framework, mavenFramework) {
|
|
37
|
+
return (`mvn archetype:generate -B ` +
|
|
38
|
+
`-DarchetypeGroupId="${MAVEN_ARCHETYPE_GROUP_ID}" ` +
|
|
39
|
+
`-DarchetypeArtifactId="${MAVEN_ARCHETYPE_ARTIFACT_ID}" ` +
|
|
40
|
+
`-DarchetypeVersion="${MAVEN_ARCHETYPE_VERSION}" ` +
|
|
41
|
+
`-DgroupId="${MAVEN_ARCHETYPE_GROUP_ID}" ` +
|
|
42
|
+
`-DartifactId="${MAVEN_ARCHETYPE_ARTIFACT_ID}" ` +
|
|
43
|
+
`-Dversion="${MAVEN_ARCHETYPE_VERSION}" ` +
|
|
44
|
+
`-DBROWSERSTACK_USERNAME="${process.env.BROWSERSTACK_USERNAME}" ` +
|
|
45
|
+
`-DBROWSERSTACK_ACCESS_KEY="${process.env.BROWSERSTACK_ACCESS_KEY}" ` +
|
|
46
|
+
`-DBROWSERSTACK_FRAMEWORK="${mavenFramework}"`);
|
|
47
|
+
}
|
|
48
|
+
// Generates Maven archetype command for Unix-like platforms (macOS/Linux)
|
|
49
|
+
function getMavenCommandForUnix(username, accessKey, mavenFramework) {
|
|
50
|
+
return `mvn archetype:generate -B -DarchetypeGroupId=${MAVEN_ARCHETYPE_GROUP_ID} \\
|
|
51
|
+
-DarchetypeArtifactId=${MAVEN_ARCHETYPE_ARTIFACT_ID} -DarchetypeVersion=${MAVEN_ARCHETYPE_VERSION} \\
|
|
52
|
+
-DgroupId=${MAVEN_ARCHETYPE_GROUP_ID} -DartifactId=${MAVEN_ARCHETYPE_ARTIFACT_ID} -Dversion=${MAVEN_ARCHETYPE_VERSION} \\
|
|
53
|
+
-DBROWSERSTACK_USERNAME="${username}" \\
|
|
54
|
+
-DBROWSERSTACK_ACCESS_KEY="${accessKey}" \\
|
|
55
|
+
-DBROWSERSTACK_FRAMEWORK="${mavenFramework}"`;
|
|
56
|
+
}
|
|
57
|
+
// Generates Java SDK setup instructions with Maven/Gradle options
|
|
58
|
+
function getJavaSDKInstructions(framework, username, accessKey) {
|
|
59
|
+
const mavenFramework = getJavaFrameworkForMaven(framework);
|
|
60
|
+
const isWindows = process.platform === "win32";
|
|
61
|
+
const platformLabel = isWindows ? "Windows" : "macOS/Linux";
|
|
62
|
+
const mavenCommand = isWindows
|
|
63
|
+
? getMavenCommandForWindows(framework, mavenFramework)
|
|
64
|
+
: getMavenCommandForUnix(username, accessKey, mavenFramework);
|
|
65
|
+
return `---STEP---
|
|
66
|
+
Install BrowserStack Java SDK
|
|
67
|
+
|
|
68
|
+
**Maven command for ${framework} (${platformLabel}):**
|
|
69
|
+
Run the command, it is required to generate the browserstack-sdk-archetype-integrate project:
|
|
70
|
+
${mavenCommand}
|
|
71
|
+
|
|
72
|
+
Alternative setup for Gradle users:
|
|
73
|
+
${GRADLE_SETUP_INSTRUCTIONS}`;
|
|
74
|
+
}
|
|
75
|
+
// Main function to get SDK setup commands based on language and framework
|
|
76
|
+
export function getSDKPrefixCommand(language, framework, username, accessKey) {
|
|
77
|
+
switch (language) {
|
|
78
|
+
case "nodejs":
|
|
79
|
+
return NODEJS_SDK_INSTRUCTIONS(username, accessKey);
|
|
80
|
+
case "java":
|
|
81
|
+
return getJavaSDKInstructions(framework, username, accessKey);
|
|
82
|
+
default:
|
|
83
|
+
return "";
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
export function getJavaFrameworkForMaven(framework) {
|
|
87
|
+
return JAVA_FRAMEWORK_MAP[framework] || framework;
|
|
88
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utilities for generating BrowserStack configuration files.
|
|
3
|
+
*/
|
|
4
|
+
export function generateBrowserStackYMLInstructions(desiredPlatforms, enablePercy = false, projectName) {
|
|
5
|
+
let ymlContent = `
|
|
6
|
+
# ======================
|
|
7
|
+
# BrowserStack Reporting
|
|
8
|
+
# ======================
|
|
9
|
+
# A single name for your project to organize all your tests. This is required for Percy.
|
|
10
|
+
projectName: ${projectName}
|
|
11
|
+
# TODO: Replace these sample values with your actual project details
|
|
12
|
+
buildName: Sample-Build
|
|
13
|
+
|
|
14
|
+
# =======================================
|
|
15
|
+
# Platforms (Browsers / Devices to test)
|
|
16
|
+
# =======================================
|
|
17
|
+
# Platforms object contains all the browser / device combinations you want to test on.
|
|
18
|
+
# Generate this on the basis of the following platforms requested by the user:
|
|
19
|
+
# Requested platforms: ${desiredPlatforms}
|
|
20
|
+
platforms:
|
|
21
|
+
- os: Windows
|
|
22
|
+
osVersion: 11
|
|
23
|
+
browserName: chrome
|
|
24
|
+
browserVersion: latest
|
|
25
|
+
|
|
26
|
+
# =======================
|
|
27
|
+
# Parallels per Platform
|
|
28
|
+
# =======================
|
|
29
|
+
# The number of parallel threads to be used for each platform set.
|
|
30
|
+
# BrowserStack's SDK runner will select the best strategy based on the configured value
|
|
31
|
+
#
|
|
32
|
+
# Example 1 - If you have configured 3 platforms and set \`parallelsPerPlatform\` as 2, a total of 6 (2 * 3) parallel threads will be used on BrowserStack
|
|
33
|
+
#
|
|
34
|
+
# Example 2 - If you have configured 1 platform and set \`parallelsPerPlatform\` as 5, a total of 5 (1 * 5) parallel threads will be used on BrowserStack
|
|
35
|
+
parallelsPerPlatform: 1
|
|
36
|
+
|
|
37
|
+
# =================
|
|
38
|
+
# Local Testing
|
|
39
|
+
# =================
|
|
40
|
+
# Set to true to test local
|
|
41
|
+
browserstackLocal: true
|
|
42
|
+
|
|
43
|
+
# ===================
|
|
44
|
+
# Debugging features
|
|
45
|
+
# ===================
|
|
46
|
+
debug: true # Visual logs, text logs, etc.
|
|
47
|
+
testObservability: true # For Test Observability`;
|
|
48
|
+
if (enablePercy) {
|
|
49
|
+
ymlContent += `
|
|
50
|
+
|
|
51
|
+
# =====================
|
|
52
|
+
# Percy Visual Testing
|
|
53
|
+
# =====================
|
|
54
|
+
# Set percy to true to enable visual testing.
|
|
55
|
+
# Set percyCaptureMode to 'manual' to control when screenshots are taken.
|
|
56
|
+
percy: true
|
|
57
|
+
percyCaptureMode: manual`;
|
|
58
|
+
}
|
|
59
|
+
return `
|
|
60
|
+
---STEP---
|
|
61
|
+
Create a browserstack.yml file in the project root. The file should be in the following format:
|
|
62
|
+
|
|
63
|
+
\`\`\`yaml${ymlContent}
|
|
64
|
+
\`\`\`
|
|
65
|
+
\n`;
|
|
66
|
+
}
|