@browserstack/mcp-server 1.4.0-beta.2 → 1.4.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 +43 -29
- package/dist/config.d.ts +4 -1
- package/dist/config.js +23 -2
- package/dist/lib/constants.d.ts +3 -0
- package/dist/lib/constants.js +3 -0
- package/dist/lib/untrusted-content.d.ts +15 -0
- package/dist/lib/untrusted-content.js +24 -0
- 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/tools/accessibility.js +4 -3
- package/dist/tools/accessiblity-utils/accessibility-rag.js +3 -1
- package/dist/tools/ask-browserstack/central-oauth.d.ts +7 -1
- package/dist/tools/ask-browserstack/central-oauth.js +25 -19
- package/dist/tools/ask-browserstack/config.d.ts +7 -1
- package/dist/tools/ask-browserstack/config.js +11 -5
- package/dist/tools/ask-browserstack/register.js +14 -1
- package/dist/tools/ask-browserstack/stream.js +9 -10
- package/dist/tools/ask-browserstack/types.d.ts +1 -1
- package/dist/tools/ask-browserstack/types.js +5 -1
- 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/build-insights.js +40 -1
- 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 +28 -11
- package/dist/tools/observability.js +3 -1
- 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 +1 -1
- 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 +1 -1
- 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 +10 -6
- package/dist/tools/tool-handoff.d.ts +30 -1
- package/dist/tools/tool-handoff.js +40 -9
- package/package.json +1 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { getBrowserStackAuth } from "../../lib/get-auth.js";
|
|
2
|
+
import { wrapUntrusted } from "../../lib/untrusted-content.js";
|
|
2
3
|
import { filterLinesByKeywords, validateLogResponse, } from "./utils.js";
|
|
3
4
|
import { apiClient } from "../../lib/apiClient.js";
|
|
4
5
|
// NETWORK LOGS
|
|
@@ -22,7 +23,7 @@ export async function retrieveNetworkFailures(sessionId, config) {
|
|
|
22
23
|
entry.response.status >= 400 ||
|
|
23
24
|
entry.response._error !== undefined);
|
|
24
25
|
return failureEntries.length > 0
|
|
25
|
-
? `Network Failures (${failureEntries.length} found):\n${JSON.stringify(failureEntries.map((entry) => ({
|
|
26
|
+
? `Network Failures (${failureEntries.length} found):\n${wrapUntrusted("network logs", JSON.stringify(failureEntries.map((entry) => ({
|
|
26
27
|
startedDateTime: entry.startedDateTime,
|
|
27
28
|
request: {
|
|
28
29
|
method: entry.request?.method,
|
|
@@ -36,7 +37,7 @@ export async function retrieveNetworkFailures(sessionId, config) {
|
|
|
36
37
|
},
|
|
37
38
|
serverIPAddress: entry.serverIPAddress,
|
|
38
39
|
time: entry.time,
|
|
39
|
-
})), null, 2)}`
|
|
40
|
+
})), null, 2))}`
|
|
40
41
|
: "No network failures found";
|
|
41
42
|
}
|
|
42
43
|
// SESSION LOGS
|
|
@@ -60,7 +61,7 @@ export async function retrieveSessionFailures(sessionId, config) {
|
|
|
60
61
|
: JSON.stringify(response.data);
|
|
61
62
|
const logs = filterSessionFailures(logText);
|
|
62
63
|
return logs.length > 0
|
|
63
|
-
? `Session Failures (${logs.length} found):\n${JSON.stringify(logs, null, 2)}`
|
|
64
|
+
? `Session Failures (${logs.length} found):\n${wrapUntrusted("session logs", JSON.stringify(logs, null, 2))}`
|
|
64
65
|
: "No session failures found";
|
|
65
66
|
}
|
|
66
67
|
// CONSOLE LOGS
|
|
@@ -84,7 +85,7 @@ export async function retrieveConsoleFailures(sessionId, config) {
|
|
|
84
85
|
: JSON.stringify(response.data);
|
|
85
86
|
const logs = filterConsoleFailures(logText);
|
|
86
87
|
return logs.length > 0
|
|
87
|
-
? `Console Failures (${logs.length} found):\n${JSON.stringify(logs, null, 2)}`
|
|
88
|
+
? `Console Failures (${logs.length} found):\n${wrapUntrusted("console logs", JSON.stringify(logs, null, 2))}`
|
|
88
89
|
: "No console failures found";
|
|
89
90
|
}
|
|
90
91
|
// FILTER: session logs
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { SessionType } from "../../lib/constants.js";
|
|
2
|
+
import { resolveBuildIdFromSession } from "../automate-utils/resolve-hashed-build-id.js";
|
|
3
|
+
export async function resolveAppAutomateBuildId(sessionId, config) {
|
|
4
|
+
return resolveBuildIdFromSession(sessionId, SessionType.AppAutomate, config);
|
|
5
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { getBrowserStackAuth } from "../../lib/get-auth.js";
|
|
2
|
+
import { apiClient } from "../../lib/apiClient.js";
|
|
3
|
+
import { SessionType } from "../../lib/constants.js";
|
|
4
|
+
import { validateLogResponse } from "./utils.js";
|
|
5
|
+
export async function retrieveSessionVideo(sessionId, sessionType, config) {
|
|
6
|
+
const product = sessionType === SessionType.AppAutomate ? "app-automate" : "automate";
|
|
7
|
+
const url = `https://api.browserstack.com/${product}/sessions/${encodeURIComponent(sessionId)}.json`;
|
|
8
|
+
const authString = getBrowserStackAuth(config);
|
|
9
|
+
const auth = Buffer.from(authString).toString("base64");
|
|
10
|
+
const response = await apiClient.get({
|
|
11
|
+
url,
|
|
12
|
+
headers: {
|
|
13
|
+
"Content-Type": "application/json",
|
|
14
|
+
Authorization: `Basic ${auth}`,
|
|
15
|
+
},
|
|
16
|
+
raise_error: false,
|
|
17
|
+
});
|
|
18
|
+
const validationError = validateLogResponse(response, "session video");
|
|
19
|
+
if (validationError)
|
|
20
|
+
return validationError.message;
|
|
21
|
+
const videoUrl = response.data?.automation_session?.video_url;
|
|
22
|
+
return typeof videoUrl === "string" && videoUrl.trim()
|
|
23
|
+
? `Session video: ${videoUrl.trim()}`
|
|
24
|
+
: "No session video available for this session";
|
|
25
|
+
}
|
|
@@ -3,7 +3,9 @@ import { trackMCP } from "../lib/instrumentation.js";
|
|
|
3
3
|
import { NEEDS_SESSION_ID } from "./tool-handoff.js";
|
|
4
4
|
import { retrieveNetworkFailures, retrieveSessionFailures, retrieveConsoleFailures, } from "./failurelogs-utils/automate.js";
|
|
5
5
|
import { retrieveDeviceLogs, retrieveAppiumLogs, retrieveCrashLogs, } from "./failurelogs-utils/app-automate.js";
|
|
6
|
-
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";
|
|
7
9
|
// Main log fetcher function
|
|
8
10
|
export async function getFailureLogs(args, config) {
|
|
9
11
|
const results = [];
|
|
@@ -12,17 +14,16 @@ export async function getFailureLogs(args, config) {
|
|
|
12
14
|
if (!args.sessionId) {
|
|
13
15
|
throw new Error("Session ID is required");
|
|
14
16
|
}
|
|
15
|
-
if (args.sessionType === SessionType.AppAutomate && !args.buildId) {
|
|
16
|
-
throw new Error("Build ID is required for app-automate sessions");
|
|
17
|
-
}
|
|
18
17
|
// Validate log types and collect errors
|
|
19
18
|
validLogTypes = args.logTypes.filter((logType) => {
|
|
20
19
|
const isAutomate = Object.values(AutomateLogType).includes(logType);
|
|
21
20
|
const isAppAutomate = Object.values(AppAutomateLogType).includes(logType);
|
|
22
21
|
if (!isAutomate && !isAppAutomate) {
|
|
23
22
|
errors.push(`Invalid log type '${logType}'. Valid log types are: ${[
|
|
24
|
-
...
|
|
25
|
-
|
|
23
|
+
...new Set([
|
|
24
|
+
...Object.values(AutomateLogType),
|
|
25
|
+
...Object.values(AppAutomateLogType),
|
|
26
|
+
]),
|
|
26
27
|
].join(", ")}`);
|
|
27
28
|
return false;
|
|
28
29
|
}
|
|
@@ -47,11 +48,26 @@ export async function getFailureLogs(args, config) {
|
|
|
47
48
|
isError: true,
|
|
48
49
|
};
|
|
49
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
|
+
}
|
|
50
61
|
let response;
|
|
51
62
|
// eslint-disable-next-line no-useless-catch
|
|
52
63
|
try {
|
|
53
64
|
for (const logType of validLogTypes) {
|
|
54
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
|
+
}
|
|
55
71
|
case AutomateLogType.NetworkLogs: {
|
|
56
72
|
response = await retrieveNetworkFailures(args.sessionId, config);
|
|
57
73
|
results.push({ type: "text", text: response });
|
|
@@ -68,17 +84,17 @@ export async function getFailureLogs(args, config) {
|
|
|
68
84
|
break;
|
|
69
85
|
}
|
|
70
86
|
case AppAutomateLogType.DeviceLogs: {
|
|
71
|
-
response = await retrieveDeviceLogs(args.sessionId,
|
|
87
|
+
response = await retrieveDeviceLogs(args.sessionId, buildId, config);
|
|
72
88
|
results.push({ type: "text", text: response });
|
|
73
89
|
break;
|
|
74
90
|
}
|
|
75
91
|
case AppAutomateLogType.AppiumLogs: {
|
|
76
|
-
response = await retrieveAppiumLogs(args.sessionId,
|
|
92
|
+
response = await retrieveAppiumLogs(args.sessionId, buildId, config);
|
|
77
93
|
results.push({ type: "text", text: response });
|
|
78
94
|
break;
|
|
79
95
|
}
|
|
80
96
|
case AppAutomateLogType.CrashLogs: {
|
|
81
|
-
response = await retrieveCrashLogs(args.sessionId,
|
|
97
|
+
response = await retrieveCrashLogs(args.sessionId, buildId, config);
|
|
82
98
|
results.push({ type: "text", text: response });
|
|
83
99
|
break;
|
|
84
100
|
}
|
|
@@ -99,7 +115,7 @@ export async function getFailureLogs(args, config) {
|
|
|
99
115
|
// Register tool with the MCP server
|
|
100
116
|
export default function registerGetFailureLogs(server, config) {
|
|
101
117
|
const tools = {};
|
|
102
|
-
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." +
|
|
103
119
|
NEEDS_SESSION_ID, {
|
|
104
120
|
sessionType: z
|
|
105
121
|
.enum([SessionType.Automate, SessionType.AppAutomate])
|
|
@@ -110,7 +126,7 @@ export default function registerGetFailureLogs(server, config) {
|
|
|
110
126
|
buildId: z
|
|
111
127
|
.string()
|
|
112
128
|
.optional()
|
|
113
|
-
.describe("
|
|
129
|
+
.describe("App Automate build ID. Optional — resolved from the session if omitted."),
|
|
114
130
|
logTypes: z
|
|
115
131
|
.array(z.enum([
|
|
116
132
|
AutomateLogType.NetworkLogs,
|
|
@@ -119,6 +135,7 @@ export default function registerGetFailureLogs(server, config) {
|
|
|
119
135
|
AppAutomateLogType.DeviceLogs,
|
|
120
136
|
AppAutomateLogType.AppiumLogs,
|
|
121
137
|
AppAutomateLogType.CrashLogs,
|
|
138
|
+
SessionVideoLogType,
|
|
122
139
|
]))
|
|
123
140
|
.describe("The types of logs to fetch."),
|
|
124
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
|
};
|
|
@@ -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
|
@@ -177,7 +177,7 @@ export default function addRCATools(server, config) {
|
|
|
177
177
|
return handleMCPError("listBuildId", server, config, error);
|
|
178
178
|
}
|
|
179
179
|
});
|
|
180
|
-
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
181
|
NEEDS_BUILD_ID, LIST_TEST_IDS_PARAMS, {
|
|
182
182
|
title: "List Test IDs",
|
|
183
183
|
readOnlyHint: true,
|
|
@@ -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
|
@@ -506,7 +506,7 @@ export default function addSelfHealTools(server, config) {
|
|
|
506
506
|
sessions: sessionsFieldSchema.describe("Sessions to plan edits for. See tool description for accepted shapes."),
|
|
507
507
|
}, {
|
|
508
508
|
title: "Prepare Self-Healing Plan",
|
|
509
|
-
readOnlyHint:
|
|
509
|
+
readOnlyHint: false,
|
|
510
510
|
openWorldHint: false,
|
|
511
511
|
destructiveHint: false,
|
|
512
512
|
idempotentHint: true,
|
|
@@ -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,
|
|
@@ -19,7 +19,7 @@ import { getTestPlan, GetTestPlanSchema, } from "./testmanagement-utils/get-test
|
|
|
19
19
|
import { listSubTestPlans, ListSubTestPlansSchema, } from "./testmanagement-utils/list-sub-testplans.js";
|
|
20
20
|
import { getSubTestPlan, GetSubTestPlanSchema, } from "./testmanagement-utils/get-sub-testplan.js";
|
|
21
21
|
import { elicitCredentialsIfSupported } from "../lib/elicit-credentials.js";
|
|
22
|
-
import { NEEDS_PROJECT_ID, NEEDS_TEST_PLAN_ID } from "./tool-handoff.js";
|
|
22
|
+
import { NEEDS_PROJECT_ID, NEEDS_TEST_PLAN_ID, PLAN_WRITES_VIA_AGENT, PROJECT_ID_ONLY_FOR_FOLDER, } from "./tool-handoff.js";
|
|
23
23
|
//TODO: Moving the traceMCP and catch block to the parent(server) function
|
|
24
24
|
/**
|
|
25
25
|
* Wrapper to call createProjectOrFolder util.
|
|
@@ -434,7 +434,7 @@ export async function getSubTestPlanTool(args, config, server) {
|
|
|
434
434
|
export default function addTestManagementTools(server, config) {
|
|
435
435
|
const tools = {};
|
|
436
436
|
tools.createProjectOrFolder = server.tool("createProjectOrFolder", "Create a project and/or folder in BrowserStack Test Management." +
|
|
437
|
-
|
|
437
|
+
PROJECT_ID_ONLY_FOR_FOLDER, CreateProjFoldSchema.shape, {
|
|
438
438
|
title: "Create Project or Folder",
|
|
439
439
|
readOnlyHint: false,
|
|
440
440
|
openWorldHint: false,
|
|
@@ -535,7 +535,8 @@ export default function addTestManagementTools(server, config) {
|
|
|
535
535
|
idempotentHint: false,
|
|
536
536
|
}, (args, context) => createLCAStepsTool(args, context, config, server));
|
|
537
537
|
tools.listTestPlans = server.tool("listTestPlans", "List test plans in a BrowserStack Test Management project. Returns each plan's identifier (TP-*), name, status, description, dates, and active/closed test-run counts. Supports pagination." +
|
|
538
|
-
NEEDS_PROJECT_ID
|
|
538
|
+
NEEDS_PROJECT_ID +
|
|
539
|
+
PLAN_WRITES_VIA_AGENT, ListTestPlansSchema.shape, {
|
|
539
540
|
title: "List Test Plans",
|
|
540
541
|
readOnlyHint: true,
|
|
541
542
|
openWorldHint: false,
|
|
@@ -544,7 +545,8 @@ export default function addTestManagementTools(server, config) {
|
|
|
544
545
|
}, (args) => listTestPlansTool(args, config, server));
|
|
545
546
|
tools.getTestPlan = server.tool("getTestPlan", "Fetch a test plan by identifier (TP-*) from BrowserStack Test Management. Returns plan metadata, the full list of linked test runs, total test-case count across runs, and a status summary — suitable for generating test documentation or QA status reports." +
|
|
546
547
|
NEEDS_PROJECT_ID +
|
|
547
|
-
NEEDS_TEST_PLAN_ID
|
|
548
|
+
NEEDS_TEST_PLAN_ID +
|
|
549
|
+
PLAN_WRITES_VIA_AGENT, GetTestPlanSchema.shape, {
|
|
548
550
|
title: "Get Test Plan",
|
|
549
551
|
readOnlyHint: true,
|
|
550
552
|
openWorldHint: false,
|
|
@@ -553,7 +555,8 @@ export default function addTestManagementTools(server, config) {
|
|
|
553
555
|
}, (args) => getTestPlanTool(args, config, server));
|
|
554
556
|
tools.listSubTestPlans = server.tool("listSubTestPlans", "List sub-test-plans under a parent test plan (TP-*) in a Test Management project. Supports pagination." +
|
|
555
557
|
NEEDS_PROJECT_ID +
|
|
556
|
-
NEEDS_TEST_PLAN_ID
|
|
558
|
+
NEEDS_TEST_PLAN_ID +
|
|
559
|
+
PLAN_WRITES_VIA_AGENT, ListSubTestPlansSchema.shape, {
|
|
557
560
|
title: "List Sub Test Plans",
|
|
558
561
|
readOnlyHint: true,
|
|
559
562
|
openWorldHint: false,
|
|
@@ -562,7 +565,8 @@ export default function addTestManagementTools(server, config) {
|
|
|
562
565
|
}, (args) => listSubTestPlansTool(args, config, server));
|
|
563
566
|
tools.getSubTestPlan = server.tool("getSubTestPlan", "Fetch a sub-test-plan (STP-*) under a parent plan (TP-*). Returns metadata and linked test runs." +
|
|
564
567
|
NEEDS_PROJECT_ID +
|
|
565
|
-
NEEDS_TEST_PLAN_ID
|
|
568
|
+
NEEDS_TEST_PLAN_ID +
|
|
569
|
+
PLAN_WRITES_VIA_AGENT, GetSubTestPlanSchema.shape, {
|
|
566
570
|
title: "Get Sub Test Plan",
|
|
567
571
|
readOnlyHint: true,
|
|
568
572
|
openWorldHint: false,
|
|
@@ -23,11 +23,40 @@
|
|
|
23
23
|
export declare const NEEDS_PROJECT_ID: string;
|
|
24
24
|
/** A sibling tool can produce the id — prefer it over the agent. */
|
|
25
25
|
export declare function needsIdFrom(idLabel: string, sourceTool: string): string;
|
|
26
|
+
/**
|
|
27
|
+
* createProjectOrFolder must NOT carry NEEDS_PROJECT_ID: `project_identifier` is optional
|
|
28
|
+
* there, and the create-a-PROJECT half needs no id at all. With the generic constant the
|
|
29
|
+
* tool read "Requires a project identifier ... call askBrowserStackAI", which routed
|
|
30
|
+
* "create me a project" through the agent before letting the tool run.
|
|
31
|
+
*/
|
|
32
|
+
export declare const PROJECT_ID_ONLY_FOR_FOLDER: string;
|
|
26
33
|
/** A test plan id (TP-*) comes from listTestPlans. */
|
|
27
34
|
export declare const NEEDS_TEST_PLAN_ID: string;
|
|
35
|
+
/**
|
|
36
|
+
* The ONLY capability handoff here: every other constant points at a tool that produces a
|
|
37
|
+
* missing *id*, but plan WRITES have no tool at all — the surface is `listTestPlans`,
|
|
38
|
+
* `getTestPlan`, `listSubTestPlans`, `getSubTestPlan` and nothing else. Atlas can do them
|
|
39
|
+
* (the tm harness allows POST /api/v1/projects/{id}/test-plans plus /update, /delete,
|
|
40
|
+
* /clone, /test-runs and /test-runs/unlink), so without this line the model reads the four
|
|
41
|
+
* read tools, finds no create, and reports the capability as absent — which is exactly what
|
|
42
|
+
* a QA eval concluded.
|
|
43
|
+
*
|
|
44
|
+
* Deliberately narrow: it names the specific operations that are missing rather than
|
|
45
|
+
* inviting the model to route plan work to the agent generally, because the tool
|
|
46
|
+
* descriptions otherwise say to prefer a specific tool whenever one fits.
|
|
47
|
+
*
|
|
48
|
+
* Caveat worth knowing: askBrowserStackAI pins every write to human approval, so this path
|
|
49
|
+
* only completes on a client that can show a prompt. On one that cannot, the intended write
|
|
50
|
+
* comes back in `needs_approval` instead of happening.
|
|
51
|
+
*/
|
|
52
|
+
export declare const PLAN_WRITES_VIA_AGENT: string;
|
|
28
53
|
/** A build id comes from either build-lookup tool. */
|
|
29
54
|
export declare const NEEDS_BUILD_ID: string;
|
|
30
|
-
/**
|
|
55
|
+
/**
|
|
56
|
+
* Session ids ARE listable now — PR #395 added `listSessions`, which merged into main while
|
|
57
|
+
* this branch was open. This used to send the model to askBrowserStackAI for want of a tool;
|
|
58
|
+
* pointing at the agent when a real tool exists is exactly what the header above forbids.
|
|
59
|
+
*/
|
|
31
60
|
export declare const NEEDS_SESSION_ID: string;
|
|
32
61
|
/** A completed scan's ids come from startAccessibilityScan, or from the agent. */
|
|
33
62
|
export declare const NEEDS_A11Y_SCAN_ID: string;
|
|
@@ -27,21 +27,52 @@ export const NEEDS_PROJECT_ID = " Requires a project identifier (PR-*). No tool
|
|
|
27
27
|
export function needsIdFrom(idLabel, sourceTool) {
|
|
28
28
|
return ` Requires ${idLabel}. Call ${sourceTool} first if you do not have it.`;
|
|
29
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* createProjectOrFolder must NOT carry NEEDS_PROJECT_ID: `project_identifier` is optional
|
|
32
|
+
* there, and the create-a-PROJECT half needs no id at all. With the generic constant the
|
|
33
|
+
* tool read "Requires a project identifier ... call askBrowserStackAI", which routed
|
|
34
|
+
* "create me a project" through the agent before letting the tool run.
|
|
35
|
+
*/
|
|
36
|
+
export const PROJECT_ID_ONLY_FOR_FOLDER = " Creating a project needs no identifier. Creating a folder inside an EXISTING project " +
|
|
37
|
+
"needs that project's identifier (PR-*); no tool here lists projects, so ask " +
|
|
38
|
+
'askBrowserStackAI with product "tm" for it.';
|
|
30
39
|
/** A test plan id (TP-*) comes from listTestPlans. */
|
|
31
40
|
export const NEEDS_TEST_PLAN_ID = needsIdFrom("a test plan identifier (TP-*)", "listTestPlans");
|
|
41
|
+
/**
|
|
42
|
+
* The ONLY capability handoff here: every other constant points at a tool that produces a
|
|
43
|
+
* missing *id*, but plan WRITES have no tool at all — the surface is `listTestPlans`,
|
|
44
|
+
* `getTestPlan`, `listSubTestPlans`, `getSubTestPlan` and nothing else. Atlas can do them
|
|
45
|
+
* (the tm harness allows POST /api/v1/projects/{id}/test-plans plus /update, /delete,
|
|
46
|
+
* /clone, /test-runs and /test-runs/unlink), so without this line the model reads the four
|
|
47
|
+
* read tools, finds no create, and reports the capability as absent — which is exactly what
|
|
48
|
+
* a QA eval concluded.
|
|
49
|
+
*
|
|
50
|
+
* Deliberately narrow: it names the specific operations that are missing rather than
|
|
51
|
+
* inviting the model to route plan work to the agent generally, because the tool
|
|
52
|
+
* descriptions otherwise say to prefer a specific tool whenever one fits.
|
|
53
|
+
*
|
|
54
|
+
* Caveat worth knowing: askBrowserStackAI pins every write to human approval, so this path
|
|
55
|
+
* only completes on a client that can show a prompt. On one that cannot, the intended write
|
|
56
|
+
* comes back in `needs_approval` instead of happening.
|
|
57
|
+
*/
|
|
58
|
+
export const PLAN_WRITES_VIA_AGENT = " Creating a test plan or sub-plan, and linking or unlinking test runs on one, are not " +
|
|
59
|
+
'available as tools here: call askBrowserStackAI with product "tm" and describe what you ' +
|
|
60
|
+
"want. It asks you to confirm before changing anything.";
|
|
32
61
|
/** A build id comes from either build-lookup tool. */
|
|
33
62
|
export const NEEDS_BUILD_ID = needsIdFrom("a BrowserStack build id", "getBuildId or listBuildId");
|
|
34
|
-
/**
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
63
|
+
/**
|
|
64
|
+
* Session ids ARE listable now — PR #395 added `listSessions`, which merged into main while
|
|
65
|
+
* this branch was open. This used to send the model to askBrowserStackAI for want of a tool;
|
|
66
|
+
* pointing at the agent when a real tool exists is exactly what the header above forbids.
|
|
67
|
+
*/
|
|
68
|
+
export const NEEDS_SESSION_ID = " Requires a session id. listSessions lists them for a build; if you do not have the " +
|
|
69
|
+
"build either, getBuildId or listBuildId resolves one from a project and build name.";
|
|
38
70
|
/** A completed scan's ids come from startAccessibilityScan, or from the agent. */
|
|
39
|
-
export const NEEDS_A11Y_SCAN_ID = " Requires the ids of a completed scan
|
|
40
|
-
|
|
41
|
-
"
|
|
71
|
+
export const NEEDS_A11Y_SCAN_ID = " Requires the ids of a completed scan, which are returned by startAccessibilityScan. " +
|
|
72
|
+
"No tool here lists past scans, so if you do not have the ids, start a new scan rather " +
|
|
73
|
+
"than guessing.";
|
|
42
74
|
/** Auth-config ids are not listable by any tool here. */
|
|
43
75
|
export const NEEDS_A11Y_CONFIG_ID = " Requires the numeric id returned by createAccessibilityAuthConfig. No tool here lists " +
|
|
44
|
-
"existing configurations, so if you do not have the id,
|
|
45
|
-
'product "a11y".';
|
|
76
|
+
"existing configurations, so if you do not have the id, create one rather than guessing.";
|
|
46
77
|
/** Test ids come from listTestIds, which itself needs a build id. */
|
|
47
78
|
export const NEEDS_TEST_IDS = needsIdFrom("test ids", "listTestIds");
|