@browserstack/mcp-server 1.5.0-beta.8 → 1.5.0
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 +48 -32
- package/dist/config.d.ts +4 -1
- package/dist/config.js +23 -2
- package/dist/index.d.ts +2 -1
- package/dist/index.js +7 -3
- package/dist/lib/constants.d.ts +3 -0
- package/dist/lib/constants.js +3 -0
- package/dist/lib/instrumentation.d.ts +14 -2
- package/dist/lib/instrumentation.js +104 -29
- package/dist/lib/node-version-notice.d.ts +1 -0
- package/dist/lib/node-version-notice.js +9 -0
- package/dist/lib/tool-latency.d.ts +4 -0
- package/dist/lib/tool-latency.js +16 -0
- package/dist/lib/untrusted-content.d.ts +15 -0
- package/dist/lib/untrusted-content.js +24 -0
- package/dist/lib/upload-validator.d.ts +3 -2
- package/dist/lib/upload-validator.js +18 -15
- package/dist/lib/utils.js +10 -3
- package/dist/lib/version-resolver.d.ts +0 -5
- package/dist/lib/version-resolver.js +8 -2
- package/dist/logger.js +1 -10
- package/dist/server-factory.js +10 -6
- package/dist/tools/accessibility.js +9 -5
- package/dist/tools/accessiblity-utils/accessibility-rag.js +3 -1
- package/dist/tools/appautomate-utils/native-execution/constants.js +4 -2
- package/dist/tools/appautomate.js +3 -1
- package/dist/tools/applive.js +1 -1
- package/dist/tools/ask-browserstack/central-oauth.d.ts +120 -0
- package/dist/tools/ask-browserstack/central-oauth.js +277 -0
- package/dist/tools/ask-browserstack/config.d.ts +102 -0
- package/dist/tools/ask-browserstack/config.js +140 -0
- package/dist/tools/ask-browserstack/egress.d.ts +34 -0
- package/dist/tools/ask-browserstack/egress.js +31 -0
- package/dist/tools/ask-browserstack/register.d.ts +61 -0
- package/dist/tools/ask-browserstack/register.js +416 -0
- package/dist/tools/ask-browserstack/relay.d.ts +201 -0
- package/dist/tools/ask-browserstack/relay.js +577 -0
- package/dist/tools/ask-browserstack/stream.d.ts +116 -0
- package/dist/tools/ask-browserstack/stream.js +236 -0
- package/dist/tools/ask-browserstack/types.d.ts +196 -0
- package/dist/tools/ask-browserstack/types.js +14 -0
- package/dist/tools/automate-utils/list-session-ids.d.ts +28 -0
- package/dist/tools/automate-utils/list-session-ids.js +87 -0
- package/dist/tools/automate-utils/resolve-hashed-build-id.d.ts +30 -0
- package/dist/tools/automate-utils/resolve-hashed-build-id.js +124 -0
- package/dist/tools/automate.d.ts +7 -0
- package/dist/tools/automate.js +104 -1
- package/dist/tools/bstack-sdk.js +2 -2
- package/dist/tools/build-insights.js +41 -2
- package/dist/tools/failurelogs-utils/app-automate.js +4 -3
- package/dist/tools/failurelogs-utils/automate.js +5 -4
- package/dist/tools/failurelogs-utils/resolve-app-build-id.d.ts +2 -0
- package/dist/tools/failurelogs-utils/resolve-app-build-id.js +5 -0
- package/dist/tools/failurelogs-utils/video.d.ts +3 -0
- package/dist/tools/failurelogs-utils/video.js +25 -0
- package/dist/tools/get-failure-logs.js +30 -11
- package/dist/tools/observability.js +3 -1
- package/dist/tools/percy-sdk.js +9 -9
- package/dist/tools/rca-agent-utils/format-rca.js +5 -4
- package/dist/tools/rca-agent-utils/get-failed-test-id.js +12 -0
- package/dist/tools/rca-agent-utils/types.d.ts +1 -0
- package/dist/tools/rca-agent.js +5 -2
- package/dist/tools/review-agent.js +2 -1
- package/dist/tools/sdk-utils/common/constants.d.ts +1 -1
- package/dist/tools/sdk-utils/common/constants.js +2 -1
- package/dist/tools/selfheal.js +6 -3
- package/dist/tools/testmanagement-utils/TCG-utils/api.js +26 -7
- package/dist/tools/testmanagement-utils/TCG-utils/config.d.ts +2 -0
- package/dist/tools/testmanagement-utils/TCG-utils/config.js +5 -0
- package/dist/tools/testmanagement-utils/testcase-from-file.js +2 -1
- package/dist/tools/testmanagement-utils/upload-file.js +16 -2
- package/dist/tools/testmanagement.js +39 -17
- package/dist/tools/tool-handoff.d.ts +66 -0
- package/dist/tools/tool-handoff.js +78 -0
- package/package.json +4 -5
- package/capability/loadtesting.capability-index.json +0 -1790
- package/capability/tm.capability-index.json +0 -19966
- package/dist/tools/capability-registry/bind.d.ts +0 -29
- package/dist/tools/capability-registry/bind.js +0 -134
- package/dist/tools/capability-registry/config.d.ts +0 -62
- package/dist/tools/capability-registry/config.js +0 -218
- package/dist/tools/capability-registry/discovery.d.ts +0 -44
- package/dist/tools/capability-registry/discovery.js +0 -99
- package/dist/tools/capability-registry/egress.d.ts +0 -44
- package/dist/tools/capability-registry/egress.js +0 -128
- package/dist/tools/capability-registry/index-loader.d.ts +0 -133
- package/dist/tools/capability-registry/index-loader.js +0 -369
- package/dist/tools/capability-registry/register.d.ts +0 -34
- package/dist/tools/capability-registry/register.js +0 -373
- package/dist/tools/capability-registry/resolve.d.ts +0 -38
- package/dist/tools/capability-registry/resolve.js +0 -45
- package/dist/tools/capability-registry/search.d.ts +0 -66
- package/dist/tools/capability-registry/search.js +0 -416
- package/dist/tools/capability-registry/types.d.ts +0 -219
- package/dist/tools/capability-registry/types.js +0 -33
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { trackMCP } from "../lib/instrumentation.js";
|
|
3
|
+
import { NEEDS_SESSION_ID } from "./tool-handoff.js";
|
|
3
4
|
import { retrieveNetworkFailures, retrieveSessionFailures, retrieveConsoleFailures, } from "./failurelogs-utils/automate.js";
|
|
4
5
|
import { retrieveDeviceLogs, retrieveAppiumLogs, retrieveCrashLogs, } from "./failurelogs-utils/app-automate.js";
|
|
5
|
-
import {
|
|
6
|
+
import { resolveAppAutomateBuildId } from "./failurelogs-utils/resolve-app-build-id.js";
|
|
7
|
+
import { retrieveSessionVideo } from "./failurelogs-utils/video.js";
|
|
8
|
+
import { AppAutomateLogType, AutomateLogType, SessionType, SessionVideoLogType, } from "../lib/constants.js";
|
|
6
9
|
// Main log fetcher function
|
|
7
10
|
export async function getFailureLogs(args, config) {
|
|
8
11
|
const results = [];
|
|
@@ -11,17 +14,16 @@ export async function getFailureLogs(args, config) {
|
|
|
11
14
|
if (!args.sessionId) {
|
|
12
15
|
throw new Error("Session ID is required");
|
|
13
16
|
}
|
|
14
|
-
if (args.sessionType === SessionType.AppAutomate && !args.buildId) {
|
|
15
|
-
throw new Error("Build ID is required for app-automate sessions");
|
|
16
|
-
}
|
|
17
17
|
// Validate log types and collect errors
|
|
18
18
|
validLogTypes = args.logTypes.filter((logType) => {
|
|
19
19
|
const isAutomate = Object.values(AutomateLogType).includes(logType);
|
|
20
20
|
const isAppAutomate = Object.values(AppAutomateLogType).includes(logType);
|
|
21
21
|
if (!isAutomate && !isAppAutomate) {
|
|
22
22
|
errors.push(`Invalid log type '${logType}'. Valid log types are: ${[
|
|
23
|
-
...
|
|
24
|
-
|
|
23
|
+
...new Set([
|
|
24
|
+
...Object.values(AutomateLogType),
|
|
25
|
+
...Object.values(AppAutomateLogType),
|
|
26
|
+
]),
|
|
25
27
|
].join(", ")}`);
|
|
26
28
|
return false;
|
|
27
29
|
}
|
|
@@ -46,11 +48,26 @@ export async function getFailureLogs(args, config) {
|
|
|
46
48
|
isError: true,
|
|
47
49
|
};
|
|
48
50
|
}
|
|
51
|
+
let buildId = args.buildId;
|
|
52
|
+
const needsBuildId = validLogTypes.some((logType) => logType !== SessionVideoLogType);
|
|
53
|
+
if (args.sessionType === SessionType.AppAutomate &&
|
|
54
|
+
needsBuildId &&
|
|
55
|
+
!buildId) {
|
|
56
|
+
buildId = await resolveAppAutomateBuildId(args.sessionId, config);
|
|
57
|
+
if (!buildId) {
|
|
58
|
+
throw new Error("Build ID is required for app-automate sessions");
|
|
59
|
+
}
|
|
60
|
+
}
|
|
49
61
|
let response;
|
|
50
62
|
// eslint-disable-next-line no-useless-catch
|
|
51
63
|
try {
|
|
52
64
|
for (const logType of validLogTypes) {
|
|
53
65
|
switch (logType) {
|
|
66
|
+
case SessionVideoLogType: {
|
|
67
|
+
response = await retrieveSessionVideo(args.sessionId, args.sessionType, config);
|
|
68
|
+
results.push({ type: "text", text: response });
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
54
71
|
case AutomateLogType.NetworkLogs: {
|
|
55
72
|
response = await retrieveNetworkFailures(args.sessionId, config);
|
|
56
73
|
results.push({ type: "text", text: response });
|
|
@@ -67,17 +84,17 @@ export async function getFailureLogs(args, config) {
|
|
|
67
84
|
break;
|
|
68
85
|
}
|
|
69
86
|
case AppAutomateLogType.DeviceLogs: {
|
|
70
|
-
response = await retrieveDeviceLogs(args.sessionId,
|
|
87
|
+
response = await retrieveDeviceLogs(args.sessionId, buildId, config);
|
|
71
88
|
results.push({ type: "text", text: response });
|
|
72
89
|
break;
|
|
73
90
|
}
|
|
74
91
|
case AppAutomateLogType.AppiumLogs: {
|
|
75
|
-
response = await retrieveAppiumLogs(args.sessionId,
|
|
92
|
+
response = await retrieveAppiumLogs(args.sessionId, buildId, config);
|
|
76
93
|
results.push({ type: "text", text: response });
|
|
77
94
|
break;
|
|
78
95
|
}
|
|
79
96
|
case AppAutomateLogType.CrashLogs: {
|
|
80
|
-
response = await retrieveCrashLogs(args.sessionId,
|
|
97
|
+
response = await retrieveCrashLogs(args.sessionId, buildId, config);
|
|
81
98
|
results.push({ type: "text", text: response });
|
|
82
99
|
break;
|
|
83
100
|
}
|
|
@@ -98,7 +115,8 @@ export async function getFailureLogs(args, config) {
|
|
|
98
115
|
// Register tool with the MCP server
|
|
99
116
|
export default function registerGetFailureLogs(server, config) {
|
|
100
117
|
const tools = {};
|
|
101
|
-
tools.getFailureLogs = server.tool("getFailureLogs", "Fetch
|
|
118
|
+
tools.getFailureLogs = server.tool("getFailureLogs", "Fetch logs, or the session video URL, for an Automate/App Automate session." +
|
|
119
|
+
NEEDS_SESSION_ID, {
|
|
102
120
|
sessionType: z
|
|
103
121
|
.enum([SessionType.Automate, SessionType.AppAutomate])
|
|
104
122
|
.describe("Type of BrowserStack session. Must be explicitly provided by the user."),
|
|
@@ -108,7 +126,7 @@ export default function registerGetFailureLogs(server, config) {
|
|
|
108
126
|
buildId: z
|
|
109
127
|
.string()
|
|
110
128
|
.optional()
|
|
111
|
-
.describe("
|
|
129
|
+
.describe("App Automate build ID. Optional — resolved from the session if omitted."),
|
|
112
130
|
logTypes: z
|
|
113
131
|
.array(z.enum([
|
|
114
132
|
AutomateLogType.NetworkLogs,
|
|
@@ -117,6 +135,7 @@ export default function registerGetFailureLogs(server, config) {
|
|
|
117
135
|
AppAutomateLogType.DeviceLogs,
|
|
118
136
|
AppAutomateLogType.AppiumLogs,
|
|
119
137
|
AppAutomateLogType.CrashLogs,
|
|
138
|
+
SessionVideoLogType,
|
|
120
139
|
]))
|
|
121
140
|
.describe("The types of logs to fetch."),
|
|
122
141
|
}, {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { wrapUntrusted } from "../lib/untrusted-content.js";
|
|
1
2
|
import { z } from "zod";
|
|
2
3
|
import { getLatestO11YBuildInfo } from "../lib/api.js";
|
|
3
4
|
import { trackMCP } from "../lib/instrumentation.js";
|
|
@@ -26,7 +27,8 @@ export async function getFailuresInLastRun(buildName, projectName, config) {
|
|
|
26
27
|
content: [
|
|
27
28
|
{
|
|
28
29
|
type: "text",
|
|
29
|
-
text: `Observability URL: ${observabilityUrl}\
|
|
30
|
+
text: `Observability URL: ${observabilityUrl}\n` +
|
|
31
|
+
wrapUntrusted("observability failure report", `Overview: ${overview}\nError Details: ${details}`),
|
|
30
32
|
},
|
|
31
33
|
],
|
|
32
34
|
};
|
package/dist/tools/percy-sdk.js
CHANGED
|
@@ -38,11 +38,11 @@ export function registerPercyTools(server, config) {
|
|
|
38
38
|
idempotentHint: true,
|
|
39
39
|
}, async (args) => {
|
|
40
40
|
try {
|
|
41
|
-
trackMCP("
|
|
41
|
+
trackMCP("percyVisualTestIntegrationAgent", server.server.getClientVersion(), undefined, config);
|
|
42
42
|
return simulatePercyChangeHandler(args, config);
|
|
43
43
|
}
|
|
44
44
|
catch (error) {
|
|
45
|
-
return handleMCPError("
|
|
45
|
+
return handleMCPError("percyVisualTestIntegrationAgent", server, config, error);
|
|
46
46
|
}
|
|
47
47
|
});
|
|
48
48
|
tools.setupPercyVisualTesting = server.tool("expandPercyVisualTesting", SETUP_PERCY_DESCRIPTION, SetUpPercyParamsShape, {
|
|
@@ -53,11 +53,11 @@ export function registerPercyTools(server, config) {
|
|
|
53
53
|
idempotentHint: true,
|
|
54
54
|
}, async (args) => {
|
|
55
55
|
try {
|
|
56
|
-
trackMCP("
|
|
56
|
+
trackMCP("expandPercyVisualTesting", server.server.getClientVersion(), undefined, config);
|
|
57
57
|
return setUpPercyHandler(args, config);
|
|
58
58
|
}
|
|
59
59
|
catch (error) {
|
|
60
|
-
return handleMCPError("
|
|
60
|
+
return handleMCPError("expandPercyVisualTesting", server, config, error);
|
|
61
61
|
}
|
|
62
62
|
});
|
|
63
63
|
tools.addPercySnapshotCommands = server.tool("addPercySnapshotCommands", PERCY_SNAPSHOT_COMMANDS_DESCRIPTION, UpdateTestFileWithInstructionsParams, {
|
|
@@ -68,7 +68,7 @@ export function registerPercyTools(server, config) {
|
|
|
68
68
|
idempotentHint: true,
|
|
69
69
|
}, async (args) => {
|
|
70
70
|
try {
|
|
71
|
-
trackMCP("addPercySnapshotCommands", server.server.getClientVersion(), config);
|
|
71
|
+
trackMCP("addPercySnapshotCommands", server.server.getClientVersion(), undefined, config);
|
|
72
72
|
return await updateTestsWithPercyCommands(args);
|
|
73
73
|
}
|
|
74
74
|
catch (error) {
|
|
@@ -83,7 +83,7 @@ export function registerPercyTools(server, config) {
|
|
|
83
83
|
idempotentHint: true,
|
|
84
84
|
}, async () => {
|
|
85
85
|
try {
|
|
86
|
-
trackMCP("listTestFiles", server.server.getClientVersion(), config);
|
|
86
|
+
trackMCP("listTestFiles", server.server.getClientVersion(), undefined, config);
|
|
87
87
|
return addListTestFiles();
|
|
88
88
|
}
|
|
89
89
|
catch (error) {
|
|
@@ -98,7 +98,7 @@ export function registerPercyTools(server, config) {
|
|
|
98
98
|
idempotentHint: true,
|
|
99
99
|
}, async (args) => {
|
|
100
100
|
try {
|
|
101
|
-
trackMCP("runPercyScan", server.server.getClientVersion(), config);
|
|
101
|
+
trackMCP("runPercyScan", server.server.getClientVersion(), undefined, config);
|
|
102
102
|
return runPercyScan(args);
|
|
103
103
|
}
|
|
104
104
|
catch (error) {
|
|
@@ -113,7 +113,7 @@ export function registerPercyTools(server, config) {
|
|
|
113
113
|
idempotentHint: true,
|
|
114
114
|
}, async (args) => {
|
|
115
115
|
try {
|
|
116
|
-
trackMCP("fetchPercyChanges", server.server.getClientVersion(), config);
|
|
116
|
+
trackMCP("fetchPercyChanges", server.server.getClientVersion(), undefined, config);
|
|
117
117
|
return await fetchPercyChanges(args, config);
|
|
118
118
|
}
|
|
119
119
|
catch (error) {
|
|
@@ -128,7 +128,7 @@ export function registerPercyTools(server, config) {
|
|
|
128
128
|
idempotentHint: true,
|
|
129
129
|
}, async (args) => {
|
|
130
130
|
try {
|
|
131
|
-
trackMCP("managePercyBuildApproval", server.server.getClientVersion(), config);
|
|
131
|
+
trackMCP("managePercyBuildApproval", server.server.getClientVersion(), undefined, config);
|
|
132
132
|
return await approveOrDeclinePercyBuild(args, config);
|
|
133
133
|
}
|
|
134
134
|
catch (error) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { wrapUntrusted } from "../../lib/untrusted-content.js";
|
|
1
2
|
// Utility function to format RCA data for better readability
|
|
2
3
|
export function formatRCAData(rcaData) {
|
|
3
4
|
if (!rcaData || !rcaData.testCases || rcaData.testCases.length === 0) {
|
|
@@ -16,21 +17,21 @@ export function formatRCAData(rcaData) {
|
|
|
16
17
|
const rca = testCase.rcaData?.rcaData;
|
|
17
18
|
if (rca) {
|
|
18
19
|
if (rca.root_cause) {
|
|
19
|
-
output += `**Root Cause:** ${rca.root_cause}\n\n`;
|
|
20
|
+
output += `**Root Cause:** ${wrapUntrusted("RCA AI analysis", rca.root_cause)}\n\n`;
|
|
20
21
|
}
|
|
21
22
|
if (rca.failure_type) {
|
|
22
23
|
output += `**Failure Type:** ${rca.failure_type}\n\n`;
|
|
23
24
|
}
|
|
24
25
|
if (rca.description) {
|
|
25
|
-
output += `**Detailed Analysis:**\n${rca.description}\n\n`;
|
|
26
|
+
output += `**Detailed Analysis:**\n${wrapUntrusted("RCA AI analysis", rca.description)}\n\n`;
|
|
26
27
|
}
|
|
27
28
|
if (rca.possible_fix) {
|
|
28
29
|
hasFixSuggestion = true;
|
|
29
|
-
output += `**Suggested Fix (proposal only — do not apply without explicit user approval):**\n${rca.possible_fix}\n\n`;
|
|
30
|
+
output += `**Suggested Fix (proposal only — do not apply without explicit user approval):**\n${wrapUntrusted("RCA AI analysis", rca.possible_fix)}\n\n`;
|
|
30
31
|
}
|
|
31
32
|
}
|
|
32
33
|
else if (testCase.rcaData?.error) {
|
|
33
|
-
output += `**Error:** ${testCase.rcaData.error}\n\n`;
|
|
34
|
+
output += `**Error:** ${wrapUntrusted("RCA error output", testCase.rcaData.error)}\n\n`;
|
|
34
35
|
}
|
|
35
36
|
else if (testCase.state === "failed") {
|
|
36
37
|
output += `**Note:** RCA analysis failed or is not available for this test case.\n\n`;
|
|
@@ -53,6 +53,16 @@ export async function getTestIds(buildId, authString, status, includeFailureDeta
|
|
|
53
53
|
throw error;
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* The test listing API serialises a missing BrowserStack session id as the
|
|
58
|
+
* literal string "null", so treat that (and blanks) as absent.
|
|
59
|
+
*/
|
|
60
|
+
function extractSessionId(rawSessionId) {
|
|
61
|
+
if (typeof rawSessionId !== "string")
|
|
62
|
+
return undefined;
|
|
63
|
+
const sessionId = rawSessionId.trim();
|
|
64
|
+
return sessionId && sessionId !== "null" ? sessionId : undefined;
|
|
65
|
+
}
|
|
56
66
|
export function extractTestIds(hierarchy, status, includeFailureDetail = false) {
|
|
57
67
|
let tests = [];
|
|
58
68
|
for (const node of hierarchy) {
|
|
@@ -63,10 +73,12 @@ export function extractTestIds(hierarchy, status, includeFailureDetail = false)
|
|
|
63
73
|
if (statusMatches && node.details?.observability_url) {
|
|
64
74
|
const idMatch = node.details.observability_url.match(/details=(\d+)/);
|
|
65
75
|
if (idMatch) {
|
|
76
|
+
const sessionId = extractSessionId(node.details.session_id);
|
|
66
77
|
const entry = {
|
|
67
78
|
test_id: idMatch[1],
|
|
68
79
|
test_name: node.display_name || `Test ${idMatch[1]}`,
|
|
69
80
|
status: nodeStatus,
|
|
81
|
+
...(sessionId && { session_id: sessionId }),
|
|
70
82
|
};
|
|
71
83
|
// Failure signatures only exist for failed tests; include when asked.
|
|
72
84
|
if (includeFailureDetail && nodeStatus === TestStatus.FAILED) {
|
package/dist/tools/rca-agent.js
CHANGED
|
@@ -7,6 +7,7 @@ import { getRCAData } from "./rca-agent-utils/rca-data.js";
|
|
|
7
7
|
import { formatRCAData } from "./rca-agent-utils/format-rca.js";
|
|
8
8
|
import { handleMCPError } from "../lib/utils.js";
|
|
9
9
|
import { trackMCP } from "../index.js";
|
|
10
|
+
import { NEEDS_BUILD_ID, NEEDS_TEST_IDS } from "./tool-handoff.js";
|
|
10
11
|
import { FETCH_RCA_PARAMS, GET_BUILD_ID_PARAMS, LIST_TEST_IDS_PARAMS, } from "./rca-agent-utils/constants.js";
|
|
11
12
|
// Tool function to fetch build ID
|
|
12
13
|
export async function getBuildIdTool(args, config) {
|
|
@@ -130,7 +131,8 @@ export async function listTestIdsTool(args, config) {
|
|
|
130
131
|
}
|
|
131
132
|
export default function addRCATools(server, config) {
|
|
132
133
|
const tools = {};
|
|
133
|
-
tools.fetchRCA = server.tool("fetchRCA", "Fetch AI Root Cause Analysis for the current user's failed BrowserStack Automate/App-Automate tests. Suggests fixes only; never auto-apply, require explicit user approval."
|
|
134
|
+
tools.fetchRCA = server.tool("fetchRCA", "Fetch AI Root Cause Analysis for the current user's failed BrowserStack Automate/App-Automate tests. Suggests fixes only; never auto-apply, require explicit user approval." +
|
|
135
|
+
NEEDS_TEST_IDS, FETCH_RCA_PARAMS, {
|
|
134
136
|
title: "Fetch Root Cause Analysis",
|
|
135
137
|
readOnlyHint: true,
|
|
136
138
|
openWorldHint: false,
|
|
@@ -175,7 +177,8 @@ export default function addRCATools(server, config) {
|
|
|
175
177
|
return handleMCPError("listBuildId", server, config, error);
|
|
176
178
|
}
|
|
177
179
|
});
|
|
178
|
-
tools.listTestIds = server.tool("listTestIds", "List all tests of a BrowserStack build (each with its status); optional status filter."
|
|
180
|
+
tools.listTestIds = server.tool("listTestIds", "List all tests of a BrowserStack build (each with its status and session id); optional status filter." +
|
|
181
|
+
NEEDS_BUILD_ID, LIST_TEST_IDS_PARAMS, {
|
|
179
182
|
title: "List Test IDs",
|
|
180
183
|
readOnlyHint: true,
|
|
181
184
|
openWorldHint: false,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { wrapUntrusted } from "../lib/untrusted-content.js";
|
|
1
2
|
import { getBrowserStackAuth } from "../lib/get-auth.js";
|
|
2
3
|
import { getPercyBuildCount } from "./review-agent-utils/build-counts.js";
|
|
3
4
|
import { getChangedPercySnapshotIds } from "./review-agent-utils/percy-snapshots.js";
|
|
@@ -50,7 +51,7 @@ export async function fetchPercyChanges(args, config) {
|
|
|
50
51
|
return {
|
|
51
52
|
content: allDiffs.map((diff) => ({
|
|
52
53
|
type: "text",
|
|
53
|
-
text: `${diff.name} → ${diff.title}: ${diff.description ?? ""}
|
|
54
|
+
text: wrapUntrusted("Percy AI visual-diff description", `${diff.name} → ${diff.title}: ${diff.description ?? ""}`),
|
|
54
55
|
})),
|
|
55
56
|
};
|
|
56
57
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const IMPORTANT_SETUP_WARNING
|
|
1
|
+
export declare const IMPORTANT_SETUP_WARNING: string;
|
|
2
2
|
export declare const SETUP_PERCY_DESCRIPTION = "Set up or expand Percy visual testing configuration with comprehensive coverage for existing projects that might have Percy integrated. This supports both Percy Web Standalone and Percy Automate. Example prompts: Expand percy coverage for this project {project_name}";
|
|
3
3
|
export declare const LIST_TEST_FILES_DESCRIPTION = "Lists all test files for a given set of directories.";
|
|
4
4
|
export declare const PERCY_SNAPSHOT_COMMANDS_DESCRIPTION = "Adds Percy snapshot commands to the specified test files.";
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
export const IMPORTANT_SETUP_WARNING = "IMPORTANT: DO NOT SKIP ANY STEP. All the setup steps described below MUST be executed regardless of any existing configuration or setup. This ensures proper BrowserStack SDK setup."
|
|
1
|
+
export const IMPORTANT_SETUP_WARNING = "IMPORTANT: DO NOT SKIP ANY STEP. All the setup steps described below MUST be executed regardless of any existing configuration or setup. This ensures proper BrowserStack SDK setup. " +
|
|
2
|
+
"If you cannot run commands or edit files in the user's project (e.g. a chat-only client), present every step below to the user in full — including each shell command, the package.json changes, and the complete browserstack.yml contents — instead of summarizing them.";
|
|
2
3
|
export const SETUP_PERCY_DESCRIPTION = "Set up or expand Percy visual testing configuration with comprehensive coverage for existing projects that might have Percy integrated. This supports both Percy Web Standalone and Percy Automate. Example prompts: Expand percy coverage for this project {project_name}";
|
|
3
4
|
export const LIST_TEST_FILES_DESCRIPTION = "Lists all test files for a given set of directories.";
|
|
4
5
|
export const PERCY_SNAPSHOT_COMMANDS_DESCRIPTION = "Adds Percy snapshot commands to the specified test files.";
|
package/dist/tools/selfheal.js
CHANGED
|
@@ -3,6 +3,7 @@ import { getSelfHealSelectors, fetchSelfHealingReportByBuild, } from "./selfheal
|
|
|
3
3
|
import { fetchTestCodeForSessions, formatTestCodeAsContext, describeTestCodeFetchIssues, } from "./selfheal-utils/fetch-test-code.js";
|
|
4
4
|
import logger from "../logger.js";
|
|
5
5
|
import { trackMCP } from "../lib/instrumentation.js";
|
|
6
|
+
import { NEEDS_SESSION_ID } from "./tool-handoff.js";
|
|
6
7
|
// Local helper: returns the server-configured BrowserStack credentials, or
|
|
7
8
|
// null when either is missing. Lives here because the self-heal tools need
|
|
8
9
|
// to degrade gracefully — `getBrowserStackAuth` throws, which is wrong for
|
|
@@ -430,7 +431,8 @@ export default function addSelfHealTools(server, config) {
|
|
|
430
431
|
"the run. Provide exactly one of `sessionId` (single Automate / " +
|
|
431
432
|
"App-Automate session) or `buildUuid` (full self-healing report for a " +
|
|
432
433
|
"build). Pass the returned locator pairs to `prepareSelfHealingPlan` " +
|
|
433
|
-
"to plan edits."
|
|
434
|
+
"to plan edits." +
|
|
435
|
+
NEEDS_SESSION_ID, {
|
|
434
436
|
sessionId: z
|
|
435
437
|
.string()
|
|
436
438
|
.describe("Session ID. Mutually exclusive with buildUuid.")
|
|
@@ -499,11 +501,12 @@ export default function addSelfHealTools(server, config) {
|
|
|
499
501
|
"[...]}`, the raw report `{healing_logs: [...]}` (with " +
|
|
500
502
|
"`healed_selectors` aliasing `locators`), and snake_case keys " +
|
|
501
503
|
"(`session_id`, `original_locator`, `healed_locator`, " +
|
|
502
|
-
"`healing_thought`)."
|
|
504
|
+
"`healing_thought`)." +
|
|
505
|
+
NEEDS_SESSION_ID, {
|
|
503
506
|
sessions: sessionsFieldSchema.describe("Sessions to plan edits for. See tool description for accepted shapes."),
|
|
504
507
|
}, {
|
|
505
508
|
title: "Prepare Self-Healing Plan",
|
|
506
|
-
readOnlyHint:
|
|
509
|
+
readOnlyHint: false,
|
|
507
510
|
openWorldHint: false,
|
|
508
511
|
destructiveHint: false,
|
|
509
512
|
idempotentHint: true,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { apiClient } from "../../../lib/apiClient.js";
|
|
2
|
-
import { TCG_TRIGGER_URL, TCG_POLL_URL, FETCH_DETAILS_URL, FORM_FIELDS_URL, BULK_CREATE_URL, TC_DETAILS_MAX_BATCH, } from "./config.js";
|
|
2
|
+
import { TCG_TRIGGER_URL, TCG_POLL_URL, FETCH_DETAILS_URL, FORM_FIELDS_URL, BULK_CREATE_URL, TC_DETAILS_MAX_BATCH, TCG_POLL_INTERVAL_MS, TCG_POLL_MAX_WAIT_MS, } from "./config.js";
|
|
3
3
|
import { createTestCasePayload } from "./helpers.js";
|
|
4
4
|
import { getBrowserStackAuth } from "../../../lib/get-auth.js";
|
|
5
5
|
import { getTMBaseURL } from "../../../lib/tm-base-url.js";
|
|
@@ -118,9 +118,15 @@ export async function pollTestCaseDetails(traceRequestId, config) {
|
|
|
118
118
|
let done = false;
|
|
119
119
|
const tmBaseUrl = await getTMBaseURL(config);
|
|
120
120
|
const TCG_POLL_URL_VALUE = TCG_POLL_URL(tmBaseUrl);
|
|
121
|
+
const deadline = Date.now() + TCG_POLL_MAX_WAIT_MS;
|
|
121
122
|
while (!done) {
|
|
123
|
+
// Bail out if the backend never sends a "termination" message, so a stuck
|
|
124
|
+
// job cannot keep this loop (and its callers) alive forever.
|
|
125
|
+
if (Date.now() > deadline) {
|
|
126
|
+
throw new Error(`TCG test-case detail polling timed out after ${TCG_POLL_MAX_WAIT_MS}ms (trace ${traceRequestId})`);
|
|
127
|
+
}
|
|
122
128
|
// add a bit of jitter to avoid synchronized polling storms
|
|
123
|
-
await new Promise((r) => setTimeout(r,
|
|
129
|
+
await new Promise((r) => setTimeout(r, TCG_POLL_INTERVAL_MS + Math.random() * 5000));
|
|
124
130
|
const poll = await apiClient.post({
|
|
125
131
|
url: `${TCG_POLL_URL_VALUE}?x-bstack-traceRequestId=${encodeURIComponent(traceRequestId)}`,
|
|
126
132
|
headers: {
|
|
@@ -159,7 +165,20 @@ export async function pollScenariosTestDetails(args, traceId, context, documentI
|
|
|
159
165
|
const TCG_POLL_URL_VALUE = TCG_POLL_URL(tmBaseUrl);
|
|
160
166
|
// Promisify interval-style polling using a wrapper
|
|
161
167
|
await new Promise((resolve, reject) => {
|
|
162
|
-
const
|
|
168
|
+
const timers = {};
|
|
169
|
+
const stop = () => {
|
|
170
|
+
if (timers.interval)
|
|
171
|
+
clearInterval(timers.interval);
|
|
172
|
+
if (timers.timeout)
|
|
173
|
+
clearTimeout(timers.timeout);
|
|
174
|
+
};
|
|
175
|
+
// Hard wall-clock deadline: if the backend never sends a "termination"
|
|
176
|
+
// message, reject instead of letting the interval fire forever.
|
|
177
|
+
timers.timeout = setTimeout(() => {
|
|
178
|
+
stop();
|
|
179
|
+
reject(new Error(`TCG scenario polling timed out after ${TCG_POLL_MAX_WAIT_MS}ms (trace ${traceId})`));
|
|
180
|
+
}, TCG_POLL_MAX_WAIT_MS);
|
|
181
|
+
timers.interval = setInterval(async () => {
|
|
163
182
|
try {
|
|
164
183
|
const poll = await apiClient.post({
|
|
165
184
|
url: `${TCG_POLL_URL_VALUE}?x-bstack-traceRequestId=${encodeURIComponent(traceId)}`,
|
|
@@ -169,7 +188,7 @@ export async function pollScenariosTestDetails(args, traceId, context, documentI
|
|
|
169
188
|
body: {},
|
|
170
189
|
});
|
|
171
190
|
if (poll.status !== 200) {
|
|
172
|
-
|
|
191
|
+
stop();
|
|
173
192
|
reject(new Error(`Polling error: ${poll.statusText || poll.status}`));
|
|
174
193
|
return;
|
|
175
194
|
}
|
|
@@ -221,16 +240,16 @@ export async function pollScenariosTestDetails(args, traceId, context, documentI
|
|
|
221
240
|
}
|
|
222
241
|
}
|
|
223
242
|
if (msg.type === "termination") {
|
|
224
|
-
|
|
243
|
+
stop();
|
|
225
244
|
resolve();
|
|
226
245
|
}
|
|
227
246
|
}
|
|
228
247
|
}
|
|
229
248
|
catch (err) {
|
|
230
|
-
|
|
249
|
+
stop();
|
|
231
250
|
reject(err);
|
|
232
251
|
}
|
|
233
|
-
},
|
|
252
|
+
}, TCG_POLL_INTERVAL_MS);
|
|
234
253
|
});
|
|
235
254
|
// once all detail fetches are triggered, wait for them to complete
|
|
236
255
|
const detailsList = await Promise.all(detailPromises);
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export declare const TC_DETAILS_MAX_BATCH = 10;
|
|
2
|
+
export declare const TCG_POLL_INTERVAL_MS = 10000;
|
|
3
|
+
export declare const TCG_POLL_MAX_WAIT_MS: number;
|
|
2
4
|
export declare const TCG_TRIGGER_URL: (baseUrl: string) => string;
|
|
3
5
|
export declare const TCG_POLL_URL: (baseUrl: string) => string;
|
|
4
6
|
export declare const FETCH_DETAILS_URL: (baseUrl: string) => string;
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
export const TC_DETAILS_MAX_BATCH = 10;
|
|
2
|
+
// Wall-clock bounds for the TCG generation polling loops. Without a hard
|
|
3
|
+
// deadline, a job that never emits a "termination" message keeps the loop
|
|
4
|
+
// (and every caller awaiting it) alive forever.
|
|
5
|
+
export const TCG_POLL_INTERVAL_MS = 10_000;
|
|
6
|
+
export const TCG_POLL_MAX_WAIT_MS = 10 * 60 * 1000; // 10 minutes
|
|
2
7
|
export const TCG_TRIGGER_URL = (baseUrl) => `${baseUrl}/api/v1/integration/tcg/test-generation/suggest-test-cases`;
|
|
3
8
|
export const TCG_POLL_URL = (baseUrl) => `${baseUrl}/api/v1/integration/tcg/test-generation/test-cases-polling`;
|
|
4
9
|
export const FETCH_DETAILS_URL = (baseUrl) => `${baseUrl}/api/v1/integration/tcg/test-generation/fetch-test-case-details`;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { wrapUntrusted } from "../../lib/untrusted-content.js";
|
|
1
2
|
import { fetchFormFields, triggerTestCaseGeneration, pollScenariosTestDetails, bulkCreateTestCases, } from "./TCG-utils/api.js";
|
|
2
3
|
import { buildDefaultFieldMaps, findBooleanFieldId, } from "./TCG-utils/helpers.js";
|
|
3
4
|
import { signedUrlMap } from "../../lib/inmemory-store.js";
|
|
@@ -37,7 +38,7 @@ export async function createTestCasesFromFile(args, context, config) {
|
|
|
37
38
|
content: [
|
|
38
39
|
{
|
|
39
40
|
type: "text",
|
|
40
|
-
text: resultString,
|
|
41
|
+
text: wrapUntrusted("AI-generated test cases from the uploaded document", resultString),
|
|
41
42
|
},
|
|
42
43
|
{
|
|
43
44
|
type: "text",
|
|
@@ -19,16 +19,30 @@ export const UploadFileSchema = z.object({
|
|
|
19
19
|
.describe("ID of the project where the file should be uploaded. Do not assume it, always ask user for it."),
|
|
20
20
|
file_path: z
|
|
21
21
|
.string()
|
|
22
|
-
.describe("Full path to the file that should be uploaded"
|
|
22
|
+
.describe("Full path to the file that should be uploaded. Must be inside the " +
|
|
23
|
+
"directory configured via the MCP_UPLOAD_BASE_DIR environment variable."),
|
|
23
24
|
});
|
|
24
25
|
/**
|
|
25
26
|
* Uploads a file to BrowserStack Test Management and returns the signed URL.
|
|
26
27
|
*/
|
|
27
28
|
export async function uploadFile(args, config) {
|
|
28
29
|
const { project_identifier, file_path } = args;
|
|
30
|
+
if (!appConfig.UPLOAD_BASE_DIR) {
|
|
31
|
+
return {
|
|
32
|
+
content: [
|
|
33
|
+
{
|
|
34
|
+
type: "text",
|
|
35
|
+
text: "File upload is disabled. Set the MCP_UPLOAD_BASE_DIR environment " +
|
|
36
|
+
"variable to a directory that contains the files you want to upload, " +
|
|
37
|
+
"then restart the MCP server. Uploads are restricted to that directory.",
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
isError: true,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
29
43
|
try {
|
|
30
44
|
// Canonicalize path and enforce upload safety rules (extension, size,
|
|
31
|
-
// hidden-directory traversal,
|
|
45
|
+
// hidden-directory traversal, base-dir containment).
|
|
32
46
|
const safePath = validateUploadPath(file_path, {
|
|
33
47
|
allowedExtensions: TEST_MANAGEMENT_ATTACHMENT_EXTENSIONS,
|
|
34
48
|
maxSizeBytes: MAX_ATTACHMENT_UPLOAD_BYTES,
|