@browserstack/mcp-server 2.2.2-beta.2 → 2.2.3-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/error.d.ts +10 -0
- package/dist/lib/error.js +64 -12
- package/dist/lib/utils.js +13 -2
- package/dist/tools/accessibility.js +3 -2
- package/dist/tools/appautomate-utils/appium-sdk/constants.d.ts +1 -1
- package/dist/tools/appautomate-utils/appium-sdk/constants.js +3 -3
- package/dist/tools/appautomate-utils/native-execution/constants.d.ts +1 -1
- package/dist/tools/appautomate-utils/native-execution/constants.js +20 -4
- package/dist/tools/appautomate.js +3 -3
- package/dist/tools/applive.js +4 -4
- package/dist/tools/ask-browserstack/register.js +13 -6
- package/dist/tools/automate-utils/fetch-screenshots.d.ts +6 -2
- package/dist/tools/automate-utils/fetch-screenshots.js +88 -19
- package/dist/tools/automate.js +2 -2
- package/dist/tools/capability-registry/register.js +90 -14
- package/dist/tools/get-failure-logs.js +1 -1
- package/dist/tools/live.js +2 -2
- package/dist/tools/rca-agent-utils/constants.js +2 -2
- package/dist/tools/rca-agent.js +1 -1
- package/dist/tools/sdk-utils/common/constants.d.ts +1 -1
- package/dist/tools/sdk-utils/common/constants.js +1 -1
- package/dist/tools/sdk-utils/common/schema.js +1 -1
- package/dist/tools/selfheal.js +16 -2
- package/dist/tools/testmanagement-utils/create-testcase.js +3 -3
- package/dist/tools/testmanagement-utils/list-testcases.js +1 -1
- package/dist/tools/testmanagement-utils/update-testcase.js +8 -6
- package/dist/tools/testmanagement.js +16 -12
- package/dist/tools/tool-handoff.d.ts +2 -2
- package/dist/tools/tool-handoff.js +16 -10
- package/package.json +1 -1
package/dist/lib/error.d.ts
CHANGED
|
@@ -1,4 +1,14 @@
|
|
|
1
1
|
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Returns the most specific human-readable text for an error raised by an
|
|
4
|
+
* API call, capped at 500 characters. For an AxiosError with a response body
|
|
5
|
+
* it prefers the body's `message`, `error` or `errors` field, serialising
|
|
6
|
+
* objects, and uses the whole body when none of those fields exist. An empty,
|
|
7
|
+
* whitespace-only or HTML-document body falls back to the error's own message.
|
|
8
|
+
* @param err - The error object to describe
|
|
9
|
+
* @param defaultText - The fallback error message
|
|
10
|
+
*/
|
|
11
|
+
export declare function errorText(err: unknown, defaultText: string): string;
|
|
2
12
|
/**
|
|
3
13
|
* Formats an AxiosError into a CallToolResult with an appropriate message.
|
|
4
14
|
* @param err - The error object to format
|
package/dist/lib/error.js
CHANGED
|
@@ -1,23 +1,75 @@
|
|
|
1
1
|
import { AxiosError } from "axios";
|
|
2
|
+
const MAX_ERROR_TEXT = 500;
|
|
3
|
+
const BODY_FIELDS = ["message", "error", "errors"];
|
|
4
|
+
function clip(text) {
|
|
5
|
+
return text.length > MAX_ERROR_TEXT ? text.slice(0, MAX_ERROR_TEXT) : text;
|
|
6
|
+
}
|
|
7
|
+
const HTML_DOCUMENT = /^\s*<(!doctype|html)\b/i;
|
|
8
|
+
function stringify(value) {
|
|
9
|
+
if (typeof value === "string") {
|
|
10
|
+
return HTML_DOCUMENT.test(value) ? "" : value.trim();
|
|
11
|
+
}
|
|
12
|
+
if (value === undefined || value === null)
|
|
13
|
+
return "";
|
|
14
|
+
try {
|
|
15
|
+
const json = JSON.stringify(value);
|
|
16
|
+
return json === "{}" || json === "[]" ? "" : (json ?? "");
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return "";
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Text taken from an API error body: the first non-empty `message`, `error`
|
|
24
|
+
* or `errors` field when any of them is present, otherwise the whole body.
|
|
25
|
+
* Returns undefined when the body carries no usable text.
|
|
26
|
+
*/
|
|
27
|
+
function bodyText(data) {
|
|
28
|
+
if (typeof data !== "object" || data === null) {
|
|
29
|
+
const text = stringify(data);
|
|
30
|
+
return text ? clip(text) : undefined;
|
|
31
|
+
}
|
|
32
|
+
const record = data;
|
|
33
|
+
const named = BODY_FIELDS.filter((key) => key in record);
|
|
34
|
+
if (named.length > 0) {
|
|
35
|
+
for (const key of named) {
|
|
36
|
+
const text = stringify(record[key]);
|
|
37
|
+
if (text)
|
|
38
|
+
return clip(text);
|
|
39
|
+
}
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
const text = stringify(record);
|
|
43
|
+
return text ? clip(text) : undefined;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Returns the most specific human-readable text for an error raised by an
|
|
47
|
+
* API call, capped at 500 characters. For an AxiosError with a response body
|
|
48
|
+
* it prefers the body's `message`, `error` or `errors` field, serialising
|
|
49
|
+
* objects, and uses the whole body when none of those fields exist. An empty,
|
|
50
|
+
* whitespace-only or HTML-document body falls back to the error's own message.
|
|
51
|
+
* @param err - The error object to describe
|
|
52
|
+
* @param defaultText - The fallback error message
|
|
53
|
+
*/
|
|
54
|
+
export function errorText(err, defaultText) {
|
|
55
|
+
if (err instanceof AxiosError) {
|
|
56
|
+
const text = bodyText(err.response?.data);
|
|
57
|
+
if (text)
|
|
58
|
+
return text;
|
|
59
|
+
return clip(err.message || defaultText);
|
|
60
|
+
}
|
|
61
|
+
if (err instanceof Error)
|
|
62
|
+
return clip(err.message || defaultText);
|
|
63
|
+
return defaultText;
|
|
64
|
+
}
|
|
2
65
|
/**
|
|
3
66
|
* Formats an AxiosError into a CallToolResult with an appropriate message.
|
|
4
67
|
* @param err - The error object to format
|
|
5
68
|
* @param defaultText - The fallback error message
|
|
6
69
|
*/
|
|
7
70
|
export function formatAxiosError(err, defaultText) {
|
|
8
|
-
let text = defaultText;
|
|
9
|
-
if (err instanceof AxiosError && err.response?.data) {
|
|
10
|
-
const message = err.response.data.message ||
|
|
11
|
-
err.response.data.error ||
|
|
12
|
-
err.message ||
|
|
13
|
-
defaultText;
|
|
14
|
-
text = message;
|
|
15
|
-
}
|
|
16
|
-
else if (err instanceof Error) {
|
|
17
|
-
text = err.message;
|
|
18
|
-
}
|
|
19
71
|
return {
|
|
20
|
-
content: [{ type: "text", text }],
|
|
72
|
+
content: [{ type: "text", text: errorText(err, defaultText) }],
|
|
21
73
|
isError: true,
|
|
22
74
|
};
|
|
23
75
|
}
|
package/dist/lib/utils.js
CHANGED
|
@@ -7,6 +7,9 @@ export function sanitizeUrlParam(param) {
|
|
|
7
7
|
return param.replace(/[;&|`$(){}[\]<>]/g, "");
|
|
8
8
|
}
|
|
9
9
|
const ONE_MB = 1048576;
|
|
10
|
+
// sharp's default (~268M px) lets one image decode to ~1 GB of RGBA; 25M px caps it near 100 MB.
|
|
11
|
+
const MAX_INPUT_PIXELS = 25_000_000;
|
|
12
|
+
const MAX_IMAGE_DIMENSION = 1920;
|
|
10
13
|
let sharpConfigured = false;
|
|
11
14
|
//Compresses a base64 image intelligently to keep it under 1 MB if needed.
|
|
12
15
|
export async function maybeCompressBase64(base64) {
|
|
@@ -25,11 +28,19 @@ export async function maybeCompressBase64(base64) {
|
|
|
25
28
|
sharp.concurrency(1);
|
|
26
29
|
sharpConfigured = true;
|
|
27
30
|
}
|
|
28
|
-
const
|
|
31
|
+
const image = globalConfig.REMOTE_MCP
|
|
32
|
+
? sharp(buffer, { limitInputPixels: MAX_INPUT_PIXELS }).resize({
|
|
33
|
+
width: MAX_IMAGE_DIMENSION,
|
|
34
|
+
height: MAX_IMAGE_DIMENSION,
|
|
35
|
+
fit: "inside",
|
|
36
|
+
withoutEnlargement: true,
|
|
37
|
+
})
|
|
38
|
+
: sharp(buffer);
|
|
39
|
+
const compressedBuffer = await image.png({ quality }).toBuffer();
|
|
29
40
|
return compressedBuffer.toString("base64");
|
|
30
41
|
}
|
|
31
42
|
catch (err) {
|
|
32
|
-
logger.warn("Image compression
|
|
43
|
+
logger.warn("Image compression failed; returning the uncompressed image. %s", err instanceof Error ? err.message : String(err));
|
|
33
44
|
return base64;
|
|
34
45
|
}
|
|
35
46
|
}
|
|
@@ -305,7 +305,8 @@ export default function addAccessibilityTools(server, config) {
|
|
|
305
305
|
return handleMCPError("createAccessibilityAuthConfig", server, config, error);
|
|
306
306
|
}
|
|
307
307
|
});
|
|
308
|
-
tools.getAccessibilityAuthConfig = server.tool("getAccessibilityAuthConfig", "
|
|
308
|
+
tools.getAccessibilityAuthConfig = server.tool("getAccessibilityAuthConfig", "Retrieve an existing authentication configuration by ID." +
|
|
309
|
+
NEEDS_A11Y_CONFIG_ID, {
|
|
309
310
|
configId: z.number().describe("ID of the auth configuration to retrieve"),
|
|
310
311
|
}, {
|
|
311
312
|
title: "Get Accessibility Auth Config",
|
|
@@ -316,7 +317,7 @@ export default function addAccessibilityTools(server, config) {
|
|
|
316
317
|
}, async (args) => {
|
|
317
318
|
return await executeGetAuthConfig(args, server, config);
|
|
318
319
|
});
|
|
319
|
-
tools.fetchAccessibilityIssues = server.tool("fetchAccessibilityIssues", "Fetch a completed scan
|
|
320
|
+
tools.fetchAccessibilityIssues = server.tool("fetchAccessibilityIssues", "Fetch accessibility issues from a completed scan with pagination support. Use cursor parameter to get subsequent pages of results." +
|
|
320
321
|
NEEDS_A11Y_SCAN_ID, {
|
|
321
322
|
scanId: z
|
|
322
323
|
.string()
|
|
@@ -21,7 +21,7 @@ export declare const MobileDeviceSchema: z.ZodObject<{
|
|
|
21
21
|
deviceName: z.ZodString;
|
|
22
22
|
osVersion: z.ZodString;
|
|
23
23
|
}, z.core.$strip>;
|
|
24
|
-
export declare const SETUP_APP_AUTOMATE_DESCRIPTION = "Set up
|
|
24
|
+
export declare const SETUP_APP_AUTOMATE_DESCRIPTION = "Set up BrowserStack App Automate SDK integration for Appium-based mobile app testing. ONLY for Appium based framework . This tool configures SDK for various languages with appium. For pre-built Espresso or XCUITest test suites, use 'runAppTestsOnBrowserStack' instead.";
|
|
25
25
|
export declare const SETUP_APP_AUTOMATE_SCHEMA: {
|
|
26
26
|
detectedFramework: z.ZodEnum<typeof AppSDKSupportedFrameworkEnum>;
|
|
27
27
|
detectedTestingFramework: z.ZodEnum<typeof AppSDKSupportedTestingFrameworkEnum>;
|
|
@@ -31,14 +31,14 @@ export const MobileDeviceSchema = z.object({
|
|
|
31
31
|
osVersion: z.string().describe("OS version, e.g. '14', '16', '17', 'latest'"),
|
|
32
32
|
});
|
|
33
33
|
// Tool description and schema for setupBrowserStackAppAutomateTests
|
|
34
|
-
export const SETUP_APP_AUTOMATE_DESCRIPTION = "Set up
|
|
34
|
+
export const SETUP_APP_AUTOMATE_DESCRIPTION = "Set up BrowserStack App Automate SDK integration for Appium-based mobile app testing. ONLY for Appium based framework . This tool configures SDK for various languages with appium. For pre-built Espresso or XCUITest test suites, use 'runAppTestsOnBrowserStack' instead.";
|
|
35
35
|
export const SETUP_APP_AUTOMATE_SCHEMA = {
|
|
36
36
|
detectedFramework: z
|
|
37
37
|
.nativeEnum(AppSDKSupportedFrameworkEnum)
|
|
38
38
|
.describe("The mobile automation framework configured in the project. Example: 'appium'"),
|
|
39
39
|
detectedTestingFramework: z
|
|
40
40
|
.nativeEnum(AppSDKSupportedTestingFrameworkEnum)
|
|
41
|
-
.describe("
|
|
41
|
+
.describe("The testing framework used in the project. Be precise with framework selection Example: 'testng', 'behave', 'pytest', 'robot'"),
|
|
42
42
|
detectedLanguage: z
|
|
43
43
|
.nativeEnum(AppSDKSupportedLanguageEnum)
|
|
44
44
|
.describe("The programming language used in the project. Supports Java and C#. Example: 'java', 'csharp'"),
|
|
@@ -46,7 +46,7 @@ export const SETUP_APP_AUTOMATE_SCHEMA = {
|
|
|
46
46
|
.array(MobileDeviceSchema)
|
|
47
47
|
.max(3)
|
|
48
48
|
.default([])
|
|
49
|
-
.describe("
|
|
49
|
+
.describe("Tuples describing target mobile devices. Add device only when user asks explicitly for it. Defaults to [] . Example: [['android', 'Samsung Galaxy S24', '14'], ['ios', 'iPhone 15', '17']]"),
|
|
50
50
|
appPath: z
|
|
51
51
|
.string()
|
|
52
52
|
.describe("Path to the mobile app file (.apk for Android, .ipa for iOS). Can be a local file path or a BrowserStack app URL (bs://)"),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { AppTestPlatform } from "./types.js";
|
|
3
|
-
export declare const RUN_APP_AUTOMATE_DESCRIPTION = "
|
|
3
|
+
export declare const RUN_APP_AUTOMATE_DESCRIPTION = "Execute pre-built native mobile test suites (Espresso for Android, XCUITest for iOS) by direct upload to BrowserStack. ONLY for compiled .apk/.ipa test files. This is NOT for SDK integration or Appium tests. For Appium-based testing with SDK setup, use 'setupBrowserStackAppAutomateTests' instead.";
|
|
4
4
|
export declare const MobileDeviceSchema: z.ZodObject<{
|
|
5
5
|
platform: z.ZodEnum<{
|
|
6
6
|
android: "android";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { AppTestPlatform } from "./types.js";
|
|
3
|
-
export const RUN_APP_AUTOMATE_DESCRIPTION = `
|
|
3
|
+
export const RUN_APP_AUTOMATE_DESCRIPTION = `Execute pre-built native mobile test suites (Espresso for Android, XCUITest for iOS) by direct upload to BrowserStack. ONLY for compiled .apk/.ipa test files. This is NOT for SDK integration or Appium tests. For Appium-based testing with SDK setup, use 'setupBrowserStackAppAutomateTests' instead.`;
|
|
4
4
|
export const MobileDeviceSchema = z.object({
|
|
5
5
|
platform: z
|
|
6
6
|
.enum(["android", "ios"])
|
|
@@ -13,15 +13,31 @@ export const MobileDeviceSchema = z.object({
|
|
|
13
13
|
export const RUN_APP_AUTOMATE_SCHEMA = {
|
|
14
14
|
appPath: z
|
|
15
15
|
.string()
|
|
16
|
-
.describe("
|
|
16
|
+
.describe("Path to your application file:\n" +
|
|
17
|
+
"If in development IDE directory:\n" +
|
|
18
|
+
"• For Android: 'gradle assembleDebug'\n" +
|
|
19
|
+
"• For iOS:\n" +
|
|
20
|
+
" xcodebuild clean -scheme YOUR_SCHEME && \\\n" +
|
|
21
|
+
" xcodebuild archive -scheme YOUR_SCHEME -configuration Release -archivePath build/app.xcarchive && \\\n" +
|
|
22
|
+
" xcodebuild -exportArchive -archivePath build/app.xcarchive -exportPath build/ipa -exportOptionsPlist exportOptions.plist\n\n" +
|
|
23
|
+
"If in other directory, provide existing app path.\n" +
|
|
24
|
+
"The resolved file must be located inside the directory set in MCP_UPLOAD_BASE_DIR."),
|
|
17
25
|
testSuitePath: z
|
|
18
26
|
.string()
|
|
19
|
-
.describe("
|
|
27
|
+
.describe("Path to your test suite file:\n" +
|
|
28
|
+
"If in development IDE directory:\n" +
|
|
29
|
+
"• For Android: 'gradle assembleAndroidTest'\n" +
|
|
30
|
+
"• For iOS:\n" +
|
|
31
|
+
" xcodebuild test-without-building -scheme YOUR_SCHEME -destination 'generic/platform=iOS' && \\\n" +
|
|
32
|
+
" cd ~/Library/Developer/Xcode/DerivedData/*/Build/Products/Debug-iphonesimulator/ && \\\n" +
|
|
33
|
+
" zip -r Tests.zip *.xctestrun *-Runner.app\n\n" +
|
|
34
|
+
"If in other directory, provide existing test file path.\n" +
|
|
35
|
+
"The resolved file must be located inside the directory set in MCP_UPLOAD_BASE_DIR."),
|
|
20
36
|
devices: z
|
|
21
37
|
.array(MobileDeviceSchema)
|
|
22
38
|
.max(3)
|
|
23
39
|
.default([])
|
|
24
|
-
.describe("
|
|
40
|
+
.describe("Tuples describing target mobile devices. Add device only when user asks explicitly for it. Defaults to [] . Example: [['android', 'Samsung Galaxy S24', '14'], ['ios', 'iPhone 15', '17']]"),
|
|
25
41
|
project: z
|
|
26
42
|
.string()
|
|
27
43
|
.optional()
|
|
@@ -194,10 +194,10 @@ async function runAppTestsOnBrowserStack(args, config) {
|
|
|
194
194
|
}
|
|
195
195
|
export default function addAppAutomationTools(server, config) {
|
|
196
196
|
const tools = {};
|
|
197
|
-
tools.takeAppScreenshot = server.tool("takeAppScreenshot", "
|
|
197
|
+
tools.takeAppScreenshot = server.tool("takeAppScreenshot", "Use this tool to take a screenshot of an app running on a BrowserStack device. This is useful for visual testing and debugging.", {
|
|
198
198
|
desiredPhone: z
|
|
199
199
|
.string()
|
|
200
|
-
.describe("
|
|
200
|
+
.describe("The full name of the device to run the app on. Example: 'iPhone 12 Pro' or 'Samsung Galaxy S20'. Always ask the user for the device they want to use."),
|
|
201
201
|
desiredPlatformVersion: z
|
|
202
202
|
.string()
|
|
203
203
|
.describe("The platform version to run the app on. Use 'latest' or 'oldest' for dynamic resolution."),
|
|
@@ -206,7 +206,7 @@ export default function addAppAutomationTools(server, config) {
|
|
|
206
206
|
.describe("Platform to run the app on. Either 'android' or 'ios'."),
|
|
207
207
|
appPath: z
|
|
208
208
|
.string()
|
|
209
|
-
.describe("
|
|
209
|
+
.describe("The path to the .apk or .ipa file. Required for app installation. Must be located inside the directory set in MCP_UPLOAD_BASE_DIR."),
|
|
210
210
|
}, {
|
|
211
211
|
title: "Take App Screenshot",
|
|
212
212
|
readOnlyHint: false,
|
package/dist/tools/applive.js
CHANGED
|
@@ -54,19 +54,19 @@ export async function startAppLiveSession(args, config) {
|
|
|
54
54
|
}
|
|
55
55
|
export default function addAppLiveTools(server, config) {
|
|
56
56
|
const tools = {};
|
|
57
|
-
tools.runAppLiveSession = server.tool("runAppLiveSession", "
|
|
57
|
+
tools.runAppLiveSession = server.tool("runAppLiveSession", "Use this tool when user wants to manually check their app on a particular mobile device using BrowserStack's cloud infrastructure. Can be used to debug crashes, slow performance, etc.", {
|
|
58
58
|
desiredPhone: z
|
|
59
59
|
.string()
|
|
60
|
-
.describe("
|
|
60
|
+
.describe("The full name of the device to run the app on. Example: 'iPhone 12 Pro' or 'Samsung Galaxy S20' or 'Google Pixel 6'. Always ask the user for the device they want to use, do not assume it. "),
|
|
61
61
|
desiredPlatformVersion: z
|
|
62
62
|
.string()
|
|
63
|
-
.describe("
|
|
63
|
+
.describe("Specifies the platform version to run the app on. For example, use '12.0' for Android or '16.0' for iOS. If the user says 'latest', 'newest', or similar, normalize it to 'latest'. Likewise, convert terms like 'earliest' or 'oldest' to 'oldest'."),
|
|
64
64
|
desiredPlatform: z
|
|
65
65
|
.enum(["android", "ios"])
|
|
66
66
|
.describe("Which platform to run on, examples: 'android', 'ios'. Set this based on the app path provided."),
|
|
67
67
|
appPath: z
|
|
68
68
|
.string()
|
|
69
|
-
.describe("
|
|
69
|
+
.describe("The path to the .ipa or .apk file to install on the device. Always ask the user for the app path, do not assume it. Must be located inside the directory set in MCP_UPLOAD_BASE_DIR."),
|
|
70
70
|
}, {
|
|
71
71
|
title: "Run App Live Session",
|
|
72
72
|
readOnlyHint: false,
|
|
@@ -53,12 +53,19 @@ const DESCRIPTION =
|
|
|
53
53
|
// tool that is not enabled for the account cannot do the job at all — so "there is a
|
|
54
54
|
// per-account gate, fall back to the individual tools" is the most useful thing to say
|
|
55
55
|
// first. Parenthesised so it reads as a status note, not as the tool's purpose.
|
|
56
|
-
"(Alpha, limited availability.
|
|
57
|
-
"the
|
|
58
|
-
"
|
|
59
|
-
"
|
|
60
|
-
"
|
|
61
|
-
"
|
|
56
|
+
"(Alpha, limited availability. Enabled per account and per product; if it is not enabled " +
|
|
57
|
+
"the call returns an entitlement error, nothing runs, and you should complete the task " +
|
|
58
|
+
"with the individual tools instead. To request access, the user should contact their " +
|
|
59
|
+
"BrowserStack account owner.) " +
|
|
60
|
+
"Use this when no other BrowserStack tool here fits the task, or when the ones you tried " +
|
|
61
|
+
"did not get you there. Prefer a specific tool whenever one fits: it is faster and more " +
|
|
62
|
+
"predictable than handing the job to an agent. " +
|
|
63
|
+
"Otherwise, describe what you want in plain language and BrowserStack's agent decides " +
|
|
64
|
+
"which calls to make, then returns its answer plus the steps it took. Anything that would " +
|
|
65
|
+
"change data pauses and asks you to confirm it first; deletes are refused outright. " +
|
|
66
|
+
"Put that confirmation to the user and answer with what THEY say — never decide on " +
|
|
67
|
+
"their behalf. If it cannot be put to anyone, the run is read-only and everything it " +
|
|
68
|
+
"wanted to change comes back in `needs_approval` instead. One task per call.";
|
|
62
69
|
/**
|
|
63
70
|
* `isError` marks a call that FAILED, not one that was refused.
|
|
64
71
|
*
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { SessionType } from "../../lib/constants.js";
|
|
2
2
|
import { BrowserStackConfig } from "../../lib/types.js";
|
|
3
|
-
export declare
|
|
3
|
+
export declare const MAX_SCREENSHOT_BYTES: number | undefined;
|
|
4
|
+
export declare function parseScreenshotUrls(text: string): string[];
|
|
5
|
+
export type Screenshot = {
|
|
4
6
|
url: string;
|
|
5
7
|
base64: string;
|
|
6
|
-
|
|
8
|
+
mimeType: string;
|
|
9
|
+
};
|
|
10
|
+
export declare function fetchAutomationScreenshots(sessionId: string, sessionType: SessionType | undefined, config: BrowserStackConfig): Promise<Screenshot[]>;
|
|
@@ -3,6 +3,10 @@ import { SessionType } from "../../lib/constants.js";
|
|
|
3
3
|
import { getBrowserStackAuth } from "../../lib/get-auth.js";
|
|
4
4
|
import { apiClient } from "../../lib/apiClient.js";
|
|
5
5
|
import globalConfig from "../../config.js";
|
|
6
|
+
import { MAX_LOG_BYTES } from "../failurelogs-utils/utils.js";
|
|
7
|
+
export const MAX_SCREENSHOT_BYTES = globalConfig.REMOTE_MCP
|
|
8
|
+
? 10 * 1024 * 1024
|
|
9
|
+
: undefined;
|
|
6
10
|
async function extractScreenshotUrls(sessionId, sessionType, config) {
|
|
7
11
|
const authString = getBrowserStackAuth(config);
|
|
8
12
|
const auth = Buffer.from(authString).toString("base64");
|
|
@@ -14,47 +18,112 @@ async function extractScreenshotUrls(sessionId, sessionType, config) {
|
|
|
14
18
|
"Content-Type": "application/json",
|
|
15
19
|
Authorization: `Basic ${auth}`,
|
|
16
20
|
},
|
|
21
|
+
maxContentLength: MAX_LOG_BYTES,
|
|
17
22
|
raise_error: false,
|
|
18
23
|
});
|
|
24
|
+
if (response.status === 413) {
|
|
25
|
+
throw new Error("The logs for this session are too large (over 5 MB) to find screenshots here. Open the session in the BrowserStack dashboard to view them.");
|
|
26
|
+
}
|
|
19
27
|
await assertOkResponse(response, "Session");
|
|
20
28
|
const text = typeof response.data === "string"
|
|
21
29
|
? response.data
|
|
22
30
|
: JSON.stringify(response.data);
|
|
31
|
+
return parseScreenshotUrls(text);
|
|
32
|
+
}
|
|
33
|
+
const LOG_PREFIX = String.raw `^\d{4}-\d{2}-\d{2} \d{1,2}:\d{1,2}:\d{1,2}:\d{1,3} `;
|
|
34
|
+
const LINE_TYPE_PATTERN = new RegExp(`${LOG_PREFIX}([A-Z_]+) `);
|
|
35
|
+
const SCREENSHOT_REQUEST_PATTERN = new RegExp(String.raw `${LOG_PREFIX}REQUEST \[[^\]]+\] GET /session/[^/\s]+/screenshot(\s|$)`);
|
|
36
|
+
const RESPONSE_VALUE_PATTERN = new RegExp(String.raw `${LOG_PREFIX}RESPONSE \{"value"\s*:\s*"(https://[^"]+)"`);
|
|
37
|
+
const DEBUG_SCREENSHOT_PATTERN = new RegExp(String.raw `${LOG_PREFIX}DEBUG (https://\S+)$`);
|
|
38
|
+
const TRUSTED_HOST_PATTERN = /(^|\.)browserstack\.com$|\.s3([.-][a-z0-9-]+)*\.amazonaws\.com$|\.backblazeb2\.com$/;
|
|
39
|
+
function isTrustedScreenshotUrl(value) {
|
|
40
|
+
try {
|
|
41
|
+
const url = new URL(value);
|
|
42
|
+
return url.protocol === "https:" && TRUSTED_HOST_PATTERN.test(url.hostname);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export function parseScreenshotUrls(text) {
|
|
23
49
|
const urls = [];
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
50
|
+
const lines = text.split("\n").map((line) => line.trimEnd());
|
|
51
|
+
for (let i = 0; i < lines.length; i++) {
|
|
52
|
+
const line = lines[i];
|
|
53
|
+
const debugMatch = line.match(DEBUG_SCREENSHOT_PATTERN);
|
|
54
|
+
if (debugMatch) {
|
|
55
|
+
urls.push(debugMatch[1]);
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (!SCREENSHOT_REQUEST_PATTERN.test(line)) {
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
62
|
+
const lineType = lines[j].match(LINE_TYPE_PATTERN)?.[1];
|
|
63
|
+
if (lineType === "REQUEST") {
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
if (lineType === "RESPONSE") {
|
|
67
|
+
const valueMatch = lines[j].match(RESPONSE_VALUE_PATTERN);
|
|
68
|
+
if (valueMatch) {
|
|
69
|
+
urls.push(valueMatch[1]);
|
|
70
|
+
}
|
|
71
|
+
break;
|
|
35
72
|
}
|
|
36
73
|
}
|
|
37
74
|
}
|
|
38
|
-
return urls;
|
|
75
|
+
return [...new Set(urls)].filter(isTrustedScreenshotUrl);
|
|
76
|
+
}
|
|
77
|
+
function detectImageMimeType(buffer) {
|
|
78
|
+
if (buffer[0] === 0xff && buffer[1] === 0xd8 && buffer[2] === 0xff) {
|
|
79
|
+
return "image/jpeg";
|
|
80
|
+
}
|
|
81
|
+
return "image/png";
|
|
39
82
|
}
|
|
83
|
+
// Returns null for a screenshot over MAX_SCREENSHOT_BYTES so the rest still load.
|
|
40
84
|
async function fetchAndCompress(url) {
|
|
41
|
-
const response = await apiClient.get({
|
|
85
|
+
const response = await apiClient.get({
|
|
86
|
+
url,
|
|
87
|
+
responseType: "arraybuffer",
|
|
88
|
+
maxContentLength: MAX_SCREENSHOT_BYTES,
|
|
89
|
+
raise_error: false,
|
|
90
|
+
});
|
|
91
|
+
if (response.status === 413) {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
if (!response.ok) {
|
|
95
|
+
throw new Error(`Failed to download screenshot: ${response.status} ${response.statusText}`);
|
|
96
|
+
}
|
|
42
97
|
// Axios returns response.data as a Buffer for binary data
|
|
43
|
-
const
|
|
44
|
-
|
|
98
|
+
const buffer = Buffer.from(response.data);
|
|
99
|
+
const base64 = await maybeCompressBase64(buffer.toString("base64"));
|
|
100
|
+
// Detect AFTER compression: sharp re-encodes anything over 1 MB to PNG, so
|
|
101
|
+
// reading the original bytes would label a large JPEG with the wrong type.
|
|
102
|
+
return {
|
|
103
|
+
url,
|
|
104
|
+
base64,
|
|
105
|
+
mimeType: detectImageMimeType(Buffer.from(base64, "base64")),
|
|
106
|
+
};
|
|
45
107
|
}
|
|
46
108
|
//Converts screenshot URLs to base64 encoded images
|
|
47
109
|
async function convertUrlsToBase64(urls) {
|
|
110
|
+
let results;
|
|
48
111
|
// Hosted pods are memory-constrained: process one image at a time. Local runs
|
|
49
112
|
// keep the concurrent path.
|
|
50
113
|
if (globalConfig.REMOTE_MCP) {
|
|
51
|
-
|
|
114
|
+
results = [];
|
|
52
115
|
for (const url of urls) {
|
|
53
|
-
|
|
116
|
+
results.push(await fetchAndCompress(url));
|
|
54
117
|
}
|
|
55
|
-
return screenshots;
|
|
56
118
|
}
|
|
57
|
-
|
|
119
|
+
else {
|
|
120
|
+
results = await Promise.all(urls.map(fetchAndCompress));
|
|
121
|
+
}
|
|
122
|
+
const screenshots = results.filter((s) => s !== null);
|
|
123
|
+
if (urls.length > 0 && screenshots.length === 0) {
|
|
124
|
+
throw new Error("The screenshots in this session are too large (over 10 MB each) to fetch here. Open the session in the BrowserStack dashboard to view them.");
|
|
125
|
+
}
|
|
126
|
+
return screenshots;
|
|
58
127
|
}
|
|
59
128
|
//Fetches and converts screenshot URLs to base64 encoded images
|
|
60
129
|
export async function fetchAutomationScreenshots(sessionId, sessionType = SessionType.Automate, config) {
|
package/dist/tools/automate.js
CHANGED
|
@@ -14,7 +14,7 @@ export async function fetchAutomationScreenshotsTool(args, config) {
|
|
|
14
14
|
content: [
|
|
15
15
|
{
|
|
16
16
|
type: "text",
|
|
17
|
-
text: "No screenshots found in the session
|
|
17
|
+
text: "No screenshots found in the session logs. Run with browserstack.debug: true to capture visual-log screenshots.",
|
|
18
18
|
},
|
|
19
19
|
],
|
|
20
20
|
isError: true,
|
|
@@ -23,7 +23,7 @@ export async function fetchAutomationScreenshotsTool(args, config) {
|
|
|
23
23
|
const results = screenshots.map((screenshot, index) => ({
|
|
24
24
|
type: "image",
|
|
25
25
|
data: screenshot.base64,
|
|
26
|
-
mimeType:
|
|
26
|
+
mimeType: screenshot.mimeType,
|
|
27
27
|
_meta: { url: screenshot.url, index: index + 1 },
|
|
28
28
|
}));
|
|
29
29
|
return {
|
|
@@ -189,11 +189,24 @@ export function addCapabilityRegistryTools(server, deps, config) {
|
|
|
189
189
|
}
|
|
190
190
|
return failed(message, extra);
|
|
191
191
|
};
|
|
192
|
-
tools.listProducts = server.tool("listProducts", "List BrowserStack products
|
|
192
|
+
tools.listProducts = server.tool("listProducts", "List the BrowserStack products this surface can reach: what each one does, the " +
|
|
193
|
+
"entities it models, and a line saying what each entity is. START HERE — " +
|
|
194
|
+
"searchCapability needs a product, and this is what tells you which one. If two " +
|
|
195
|
+
"products could both fit the task, ask the user rather than choosing for them. " +
|
|
196
|
+
"ALWAYS pass the user's request, in their own words, as `query`: the answer then " +
|
|
197
|
+
"says outright whether the products clash over the words in it. WHEN IT REPORTS A " +
|
|
198
|
+
"CLASH — two products claiming the same word for different things — ASK THE USER " +
|
|
199
|
+
"WHICH PRODUCT THEY MEAN AND WAIT FOR THEIR ANSWER BEFORE SEARCHING OR INVOKING " +
|
|
200
|
+
"ANYTHING. The response carries the question to put to them and what the shared " +
|
|
201
|
+
"word means in each product. Choosing for them, or searching each product in turn " +
|
|
202
|
+
"and merging the results, is the failure this call exists to prevent.", {
|
|
193
203
|
query: z
|
|
194
204
|
.string()
|
|
195
205
|
.optional()
|
|
196
|
-
.describe("The user's request, in their own words
|
|
206
|
+
.describe("The user's request, in their own words. With it the response carries a " +
|
|
207
|
+
"`routing` block saying whether the products share the vocabulary this " +
|
|
208
|
+
"request uses — and, when they do, the question to put to the user before " +
|
|
209
|
+
"searching either product."),
|
|
197
210
|
}, {
|
|
198
211
|
title: "List Capability Products",
|
|
199
212
|
readOnlyHint: true,
|
|
@@ -336,7 +349,9 @@ export function addCapabilityRegistryTools(server, deps, config) {
|
|
|
336
349
|
// each. That is the right trade while routing is the problem it solves (you cannot
|
|
337
350
|
// choose between products by looking at one of them), but past a dozen products it may
|
|
338
351
|
// need a names-only default with aliases on request.
|
|
339
|
-
tools.describeEntity = server.tool("describeEntity", "Describe
|
|
352
|
+
tools.describeEntity = server.tool("describeEntity", "Describe one entity: what it is, what identifies it, what it relates to, and the " +
|
|
353
|
+
"vocabulary the product uses for it. Read this before filtering or writing, because " +
|
|
354
|
+
"ids and field values usually have to be resolved first.", {
|
|
340
355
|
product: productArg().describe(`Which product the entity belongs to: ${productList}.`),
|
|
341
356
|
entity: z.string().describe("Entity name, as listProducts returns it."),
|
|
342
357
|
}, {
|
|
@@ -356,10 +371,38 @@ export function addCapabilityRegistryTools(server, deps, config) {
|
|
|
356
371
|
}
|
|
357
372
|
return ok({ product, entity, ...doc });
|
|
358
373
|
});
|
|
359
|
-
tools.searchCapability = server.tool("searchCapability", "Find
|
|
374
|
+
tools.searchCapability = server.tool("searchCapability", "Find endpoints this surface can call, by plain language, within ONE product. " +
|
|
375
|
+
"CALL listProducts FIRST, passing the user's request as its `query`: it names the " +
|
|
376
|
+
"products, says whether they clash over the words in that request, and gives you " +
|
|
377
|
+
"the question to ask when they do. You need it anyway — `product` here takes a name " +
|
|
378
|
+
"exactly as listProducts spells it. Where two products use the same word for " +
|
|
379
|
+
"different things, ask the USER and wait for an answer rather than picking. " +
|
|
380
|
+
"Search matches the product's OWN words, not synonyms. When your words are not the " +
|
|
381
|
+
"product's, the response says so: `weak_match: true` with a `suggested_vocabulary` " +
|
|
382
|
+
"map of the product's entities and their aliases. Results are still returned, but " +
|
|
383
|
+
"treat them as unconfirmed — pick the closest entity from that map and search again " +
|
|
384
|
+
"using its vocabulary, or call describeEntity on it for the fuller picture. That one " +
|
|
385
|
+
"extra round trip is far cheaper than invoking the wrong capability. " +
|
|
386
|
+
"Narrowing further with `entity` sharpens results. " +
|
|
387
|
+
"THIS IS A SHORTLIST, NOT A CONTRACT. Each result carries only what you need to " +
|
|
388
|
+
"CHOOSE: `name` (the handle), `product`, `mode` (whether it writes), `intent` and " +
|
|
389
|
+
"`guidance` (what it does and what goes wrong), and `method`/`path` for products " +
|
|
390
|
+
"that publish no name yet. " +
|
|
391
|
+
"It does NOT carry parameters or response shapes. Once you have picked one, call " +
|
|
392
|
+
"describeCapability for its full contract, then invokeCapability. Fetching the " +
|
|
393
|
+
"contract only for the one you chose is the difference between ~1k and ~8.6k tokens " +
|
|
394
|
+
"a search. Results are ranked and capped. " +
|
|
395
|
+
"TO SEE EVERYTHING rather than search, pass query='*' — that lists the product " +
|
|
396
|
+
"(honouring `entity` and `mode`) in a stable order instead of ranking it, which is " +
|
|
397
|
+
"what you want when the task is 'what can this product do' rather than a lookup. " +
|
|
398
|
+
"TO PAGE, send `offset`: every response carries `offset` and, when more matched, " +
|
|
399
|
+
"`next_offset` — resend the SAME query with that value. `next_offset` is absent on " +
|
|
400
|
+
"the last page, so walk until it stops rather than comparing counts.", {
|
|
360
401
|
query: z
|
|
361
402
|
.string()
|
|
362
|
-
.describe("
|
|
403
|
+
.describe("What you are trying to do, in plain language. Pass '*' (or 'all') to BROWSE " +
|
|
404
|
+
"instead of search: every capability in the product, in a stable order, " +
|
|
405
|
+
"paginated — use it when you want to see the surface rather than find one thing."),
|
|
363
406
|
entity: z
|
|
364
407
|
.string()
|
|
365
408
|
.optional()
|
|
@@ -387,7 +430,9 @@ export function addCapabilityRegistryTools(server, deps, config) {
|
|
|
387
430
|
// refused here instead, by a message that names listProducts.
|
|
388
431
|
product: z
|
|
389
432
|
.string()
|
|
390
|
-
.describe("
|
|
433
|
+
.describe("Which product to search, named exactly as listProducts spells it. Call " +
|
|
434
|
+
"listProducts with the user's request to get the name — and to find out " +
|
|
435
|
+
"whether the products clash over it, in which case ask the user first."),
|
|
391
436
|
// `product_choice` USED TO LIVE HERE, AND WAS ITS OWN DEFEAT.
|
|
392
437
|
//
|
|
393
438
|
// A required enum of 'user_confirmed' | 'not_asked' publishes, in the schema every
|
|
@@ -400,11 +445,18 @@ export function addCapabilityRegistryTools(server, deps, config) {
|
|
|
400
445
|
routing_token: z
|
|
401
446
|
.string()
|
|
402
447
|
.optional()
|
|
403
|
-
.describe("
|
|
448
|
+
.describe("The `routing_token` from the last listProducts call or product_ambiguous " +
|
|
449
|
+
"refusal — it carries what that step concluded about the user's request, " +
|
|
450
|
+
"signed. Always pass the most recent one. When it reports a clash, send the " +
|
|
451
|
+
"user's answer in `user_words` alongside it; neither works alone."),
|
|
404
452
|
user_words: z
|
|
405
453
|
.string()
|
|
406
454
|
.optional()
|
|
407
|
-
.describe("The
|
|
455
|
+
.describe("The USER'S OWN words, verbatim, that settle which product this is — either " +
|
|
456
|
+
"the product's name or a term only one product uses ('quality gate', 'test " +
|
|
457
|
+
"plan'). Quote them; do not paraphrase, and do not write words the user did " +
|
|
458
|
+
"not say. When a query could mean either product this is what lifts the " +
|
|
459
|
+
"refusal, because it is checked against the index rather than taken on trust."),
|
|
408
460
|
mode: z
|
|
409
461
|
.enum(["read", "write", "destructive"])
|
|
410
462
|
.optional()
|
|
@@ -413,7 +465,8 @@ export function addCapabilityRegistryTools(server, deps, config) {
|
|
|
413
465
|
offset: z
|
|
414
466
|
.number()
|
|
415
467
|
.optional()
|
|
416
|
-
.describe("`next_offset` from the previous
|
|
468
|
+
.describe("Skip this many results — send the `next_offset` from the previous response " +
|
|
469
|
+
"with the SAME query to get the next page. Omit for the first page."),
|
|
417
470
|
}, {
|
|
418
471
|
title: "Search Capabilities",
|
|
419
472
|
readOnlyHint: true,
|
|
@@ -670,7 +723,16 @@ export function addCapabilityRegistryTools(server, deps, config) {
|
|
|
670
723
|
: {}),
|
|
671
724
|
});
|
|
672
725
|
});
|
|
673
|
-
tools.describeCapability = server.tool("describeCapability", "
|
|
726
|
+
tools.describeCapability = server.tool("describeCapability", "The full contract for ONE capability you picked from searchCapability: its " +
|
|
727
|
+
"parameters grouped into path_params / query / body under the spec's own names, " +
|
|
728
|
+
"what it returns, and its declared response shapes fully expanded. Call this after " +
|
|
729
|
+
"search and before invokeCapability — search deliberately omits all of it, because " +
|
|
730
|
+
"it is only needed for the one capability you actually call. " +
|
|
731
|
+
"Identify it by `name`, exactly as search returned it; only when a result carries " +
|
|
732
|
+
"no `name` (some products publish none yet) pass `method` and `path` instead. " +
|
|
733
|
+
"Every parameter lists its type, whether it is required, its allowed values where " +
|
|
734
|
+
"the set is closed, and any limits the product declares — obey those before calling " +
|
|
735
|
+
"rather than discovering them from a rejected request.", {
|
|
674
736
|
name: z
|
|
675
737
|
.string()
|
|
676
738
|
.optional()
|
|
@@ -685,11 +747,14 @@ export function addCapabilityRegistryTools(server, deps, config) {
|
|
|
685
747
|
.describe("Path with {placeholders} intact — only for capabilities returned without a `name`."),
|
|
686
748
|
product: productArg()
|
|
687
749
|
.optional()
|
|
688
|
-
.describe(
|
|
750
|
+
.describe(`Which product owns it (${productList}). searchCapability returns it on every ` +
|
|
751
|
+
"result; required only when two products share a name or a path."),
|
|
689
752
|
include_responses: z
|
|
690
753
|
.enum(["success", "all", "none"])
|
|
691
754
|
.optional()
|
|
692
|
-
.describe("'success' (default
|
|
755
|
+
.describe("Which declared responses to expand: 'success' (default, the 2xx shape), 'all' " +
|
|
756
|
+
"(adds the error shapes — several times larger, and near-identical across " +
|
|
757
|
+
"endpoints), or 'none'."),
|
|
693
758
|
}, {
|
|
694
759
|
title: "Describe Capability",
|
|
695
760
|
readOnlyHint: true,
|
|
@@ -752,7 +817,17 @@ export function addCapabilityRegistryTools(server, deps, config) {
|
|
|
752
817
|
return refuse("describeCapability", "unexpected_error", "that capability could not be described", undefined, identity);
|
|
753
818
|
}
|
|
754
819
|
});
|
|
755
|
-
tools.invokeCapability = server.tool("invokeCapability", "
|
|
820
|
+
tools.invokeCapability = server.tool("invokeCapability", "Call a capability whose contract you have from describeCapability. Pass `name` exactly " +
|
|
821
|
+
"as given — that is the handle. Only when a result carries no `name` (some products " +
|
|
822
|
+
"do not publish them yet) pass `method` and `path` instead, exactly as returned. " +
|
|
823
|
+
"Arguments go in path_params / query / body under the spec's own names. One call " +
|
|
824
|
+
"makes exactly one request and returns the product's own response untouched; when " +
|
|
825
|
+
"`completed` is false there is another page, which you fetch by sending the " +
|
|
826
|
+
"capability's own page parameter. If the mode is 'write' you MUST ask the user " +
|
|
827
|
+
"first, then resend with user_permission='granted' and a change_summary; both are " +
|
|
828
|
+
"recorded. Capabilities whose mode is 'destructive' (deletes) are refused outright — " +
|
|
829
|
+
"archiving, closing and merging are ordinary writes and DO run, so read the mode and " +
|
|
830
|
+
"intent before confirming with the user.", {
|
|
756
831
|
name: z
|
|
757
832
|
.string()
|
|
758
833
|
.optional()
|
|
@@ -780,7 +855,8 @@ export function addCapabilityRegistryTools(server, deps, config) {
|
|
|
780
855
|
.describe("Body fields, under the spec's names."),
|
|
781
856
|
product: productArg()
|
|
782
857
|
.optional()
|
|
783
|
-
.describe(
|
|
858
|
+
.describe(`Which product owns the capability (${productList}). searchCapability returns it ` +
|
|
859
|
+
"on every result; required only when two products share a name or a path."),
|
|
784
860
|
user_permission: z
|
|
785
861
|
.enum(PERMISSION_VALUES)
|
|
786
862
|
.optional()
|
|
@@ -115,7 +115,7 @@ export async function getFailureLogs(args, config) {
|
|
|
115
115
|
// Register tool with the MCP server
|
|
116
116
|
export default function registerGetFailureLogs(server, config) {
|
|
117
117
|
const tools = {};
|
|
118
|
-
tools.getFailureLogs = server.tool("getFailureLogs", "Fetch logs or the video URL for an Automate/App Automate session." +
|
|
118
|
+
tools.getFailureLogs = server.tool("getFailureLogs", "Fetch logs, or the session video URL, for an Automate/App Automate session." +
|
|
119
119
|
NEEDS_SESSION_ID, {
|
|
120
120
|
sessionType: z
|
|
121
121
|
.enum([SessionType.Automate, SessionType.AppAutomate])
|
package/dist/tools/live.js
CHANGED
|
@@ -15,14 +15,14 @@ const LiveArgsShape = {
|
|
|
15
15
|
.describe("Desktop OS ('Windows' or 'OS X') or mobile OS ('android','ios','winphone')"),
|
|
16
16
|
desiredOSVersion: z
|
|
17
17
|
.string()
|
|
18
|
-
.describe("
|
|
18
|
+
.describe("The OS version must be specified as a version number (e.g., '10', '14.0') or as a keyword such as 'latest' or 'oldest'. Normalize variations like 'newest' or 'most recent' to 'latest', and terms like 'earliest' or 'first' to 'oldest'. For macOS, version names (e.g., 'Sequoia') must be used instead of numeric versions."),
|
|
19
19
|
desiredBrowser: z
|
|
20
20
|
.enum(["chrome", "firefox", "safari", "edge", "ie"])
|
|
21
21
|
.describe("Browser for desktop (Chrome, IE, Firefox, Safari, Edge)"),
|
|
22
22
|
desiredBrowserVersion: z
|
|
23
23
|
.string()
|
|
24
24
|
.optional()
|
|
25
|
-
.describe("
|
|
25
|
+
.describe("Browser version for desktop (e.g. '133.2', 'latest'). If the user says 'latest', 'newest', or similar, normalize it to 'latest'. Likewise, convert terms like 'earliest' or 'oldest' to 'oldest'."),
|
|
26
26
|
desiredDevice: z.string().optional().describe("Device name for mobile"),
|
|
27
27
|
};
|
|
28
28
|
const LiveArgsSchema = z.object(LiveArgsShape);
|
|
@@ -8,7 +8,7 @@ export const FETCH_RCA_PARAMS = {
|
|
|
8
8
|
testId: z
|
|
9
9
|
.array(z.number().int())
|
|
10
10
|
.max(3)
|
|
11
|
-
.describe("
|
|
11
|
+
.describe("Array of integer test IDs to fetch RCA data for (maximum 3 IDs). These must be numeric test IDs, not session IDs or strings. If not provided, use the listTestIds tool to get all failed testcases. If more than 3 IDs are provided, only the first 3 will be processed."),
|
|
12
12
|
};
|
|
13
13
|
export const GET_BUILD_ID_PARAMS = {
|
|
14
14
|
browserStackProjectName: z
|
|
@@ -21,7 +21,7 @@ export const GET_BUILD_ID_PARAMS = {
|
|
|
21
21
|
export const LIST_TEST_IDS_PARAMS = {
|
|
22
22
|
buildId: z
|
|
23
23
|
.string()
|
|
24
|
-
.describe("Build
|
|
24
|
+
.describe("The Browserstack Build ID of the test run. If not known, use the getBuildId tool to fetch it using project and build name"),
|
|
25
25
|
status: z
|
|
26
26
|
.nativeEnum(TestStatus)
|
|
27
27
|
.describe("Filter tests by status. If not provided, all tests are returned. Example for RCA usecase always use failed status"),
|
package/dist/tools/rca-agent.js
CHANGED
|
@@ -131,7 +131,7 @@ export async function listTestIdsTool(args, config) {
|
|
|
131
131
|
}
|
|
132
132
|
export default function addRCATools(server, config) {
|
|
133
133
|
const tools = {};
|
|
134
|
-
tools.fetchRCA = server.tool("fetchRCA", "AI
|
|
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
135
|
NEEDS_TEST_IDS, FETCH_RCA_PARAMS, {
|
|
136
136
|
title: "Fetch Root Cause Analysis",
|
|
137
137
|
readOnlyHint: true,
|
|
@@ -2,7 +2,7 @@ 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.";
|
|
5
|
-
export declare const RUN_ON_BROWSERSTACK_DESCRIPTION = "Set up and run web tests on BrowserStack
|
|
5
|
+
export declare const RUN_ON_BROWSERSTACK_DESCRIPTION = "Set up and run automated web-based tests on BrowserStack using the BrowserStack SDK. Use this tool for functional or integration test setup on BrowserStack only. For any visual testing or Percy integration, use the dedicated Percy setup tool. Example prompts: run this test on browserstack; set up this project for browserstack.";
|
|
6
6
|
export declare const SIMULATE_PERCY_CHANGE_DESCRIPTION = "Integrate Percy visual testing into new projects and demonstrate visual change detection through a step-by-step simulation. This tool handles initial Percy integration, setup, and creates a working demonstration for new users. Primary tool for prompts like: \"Integrate percy for this project\", \"Integrate percy in this project {project_name}\"";
|
|
7
7
|
export declare const PERCY_REPLACE_REGEX: RegExp;
|
|
8
8
|
export declare const PERCY_SNAPSHOT_INSTRUCTION = "\nInvoke listTestFiles() with the provided directories from user to gather all test files in memory ---STEP---\nProcess files in STRICT sequential order using tool addPercySnapshotCommands() with below instructions:\n- Start with index 0\n- Then index 1 \n- Then index 2\n- Continue sequentially until the last index\n- DO NOT skip any index numbers\n- DO NOT jump to random indices like 10, 20, 30\n- DO NOT worry about time consuming and any constraints.\n- DO NOT STOP until you add commands in all the files or you reach end of the files.\n";
|
|
@@ -3,7 +3,7 @@ export const IMPORTANT_SETUP_WARNING = "IMPORTANT: DO NOT SKIP ANY STEP. All the
|
|
|
3
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}";
|
|
4
4
|
export const LIST_TEST_FILES_DESCRIPTION = "Lists all test files for a given set of directories.";
|
|
5
5
|
export const PERCY_SNAPSHOT_COMMANDS_DESCRIPTION = "Adds Percy snapshot commands to the specified test files.";
|
|
6
|
-
export const RUN_ON_BROWSERSTACK_DESCRIPTION = "Set up and run web tests on BrowserStack
|
|
6
|
+
export const RUN_ON_BROWSERSTACK_DESCRIPTION = "Set up and run automated web-based tests on BrowserStack using the BrowserStack SDK. Use this tool for functional or integration test setup on BrowserStack only. For any visual testing or Percy integration, use the dedicated Percy setup tool. Example prompts: run this test on browserstack; set up this project for browserstack.";
|
|
7
7
|
export const SIMULATE_PERCY_CHANGE_DESCRIPTION = `Integrate Percy visual testing into new projects and demonstrate visual change detection through a step-by-step simulation. This tool handles initial Percy integration, setup, and creates a working demonstration for new users. Primary tool for prompts like: "Integrate percy for this project", "Integrate percy in this project {project_name}"`;
|
|
8
8
|
export const PERCY_REPLACE_REGEX = /Invoke listTestFiles\(\) with the provided directories[\s\S]*?- DO NOT STOP until you add commands in all the files or you reach end of the files\./;
|
|
9
9
|
export const PERCY_SNAPSHOT_INSTRUCTION = `
|
|
@@ -63,7 +63,7 @@ export const RunTestsOnBrowserStackParamsShape = {
|
|
|
63
63
|
.array(DeviceSchema)
|
|
64
64
|
.max(3)
|
|
65
65
|
.default([])
|
|
66
|
-
.describe("
|
|
66
|
+
.describe("Device objects array. Use the object format directly - no transformation needed. Add only when user explicitly requests devices. Examples: [{ platform: 'windows', osVersion: '11', browser: 'chrome', browserVersion: 'latest' }] or [{ platform: 'android', deviceName: 'Samsung Galaxy S24', osVersion: '14', browser: 'chrome' }]."),
|
|
67
67
|
};
|
|
68
68
|
export const SetUpPercySchema = z.object(SetUpPercyParamsShape);
|
|
69
69
|
export const RunTestsOnBrowserStackSchema = z.object(RunTestsOnBrowserStackParamsShape);
|
package/dist/tools/selfheal.js
CHANGED
|
@@ -417,7 +417,11 @@ export async function prepareSelfHealingPlanTool(args, config) {
|
|
|
417
417
|
// Registers the fetchSelfHealSelector tool with the MCP server
|
|
418
418
|
export default function addSelfHealTools(server, config) {
|
|
419
419
|
const tools = {};
|
|
420
|
-
tools.fetchSelfHealedSelectors = server.tool("fetchSelfHealedSelectors", "Fetch self-healed selectors
|
|
420
|
+
tools.fetchSelfHealedSelectors = server.tool("fetchSelfHealedSelectors", "Fetch BrowserStack self-healed selectors plus the test source code for " +
|
|
421
|
+
"the run. Provide exactly one of `sessionId` (single Automate / " +
|
|
422
|
+
"App-Automate session) or `buildUuid` (full self-healing report for a " +
|
|
423
|
+
"build). Pass the returned locator pairs to `prepareSelfHealingPlan` " +
|
|
424
|
+
"to plan edits." +
|
|
421
425
|
NEEDS_SESSION_ID, {
|
|
422
426
|
sessionId: z
|
|
423
427
|
.string()
|
|
@@ -477,7 +481,17 @@ export default function addSelfHealTools(server, config) {
|
|
|
477
481
|
// Flexible entry point for the `sessions` argument. See normalizers above
|
|
478
482
|
// for the accepted alternate shapes.
|
|
479
483
|
const sessionsFieldSchema = z.preprocess(normalizeSessionsInput, z.array(sessionPayloadSchema).min(1));
|
|
480
|
-
tools.prepareSelfHealingPlan = server.tool("prepareSelfHealingPlan", "Build
|
|
484
|
+
tools.prepareSelfHealingPlan = server.tool("prepareSelfHealingPlan", "Build a self-healing edit plan that bundles locator pairs with test " +
|
|
485
|
+
"source code for the calling LLM to apply with its own editing tools. " +
|
|
486
|
+
"This tool does NOT modify files — it returns structured context so " +
|
|
487
|
+
"the LLM can edit only the relevant call sites (avoids blind " +
|
|
488
|
+
"find/replace across shared ids/classes). `sessions` accepts the " +
|
|
489
|
+
"canonical shape `[{sessionId, locators: [{original, healed, " +
|
|
490
|
+
"thought?}]}]`, a single session object, an envelope `{sessions: " +
|
|
491
|
+
"[...]}`, the raw report `{healing_logs: [...]}` (with " +
|
|
492
|
+
"`healed_selectors` aliasing `locators`), and snake_case keys " +
|
|
493
|
+
"(`session_id`, `original_locator`, `healed_locator`, " +
|
|
494
|
+
"`healing_thought`)." +
|
|
481
495
|
NEEDS_SESSION_ID, {
|
|
482
496
|
sessions: sessionsFieldSchema.describe("Sessions to plan edits for. See tool description for accepted shapes."),
|
|
483
497
|
}, {
|
|
@@ -15,10 +15,10 @@ export const customFieldValueSchema = z.union([
|
|
|
15
15
|
export const CreateTestCaseSchema = z.object({
|
|
16
16
|
project_identifier: z
|
|
17
17
|
.string()
|
|
18
|
-
.describe("
|
|
18
|
+
.describe("The ID of the BrowserStack project where the test case should be created. If no project identifier is provided, ask the user if they would like to create a new project using the createProjectOrFolder tool."),
|
|
19
19
|
folder_id: z
|
|
20
20
|
.string()
|
|
21
|
-
.describe("
|
|
21
|
+
.describe("The ID of the folder within the project where the test case should be created. If not provided, ask the user if they would like to create a new folder using the createProjectOrFolder tool."),
|
|
22
22
|
name: z.string().describe("Name of the test case."),
|
|
23
23
|
description: z
|
|
24
24
|
.string()
|
|
@@ -66,7 +66,7 @@ export const CreateTestCaseSchema = z.object({
|
|
|
66
66
|
priority: z
|
|
67
67
|
.string()
|
|
68
68
|
.optional()
|
|
69
|
-
.describe("Priority
|
|
69
|
+
.describe("Priority of the test case. Accepts either display name (e.g. 'Critical', 'High', 'Medium', 'Low') or internal name (e.g. 'medium'). If omitted, the project default (usually 'Medium') is applied. Valid values are per-project and discoverable via the form-fields endpoint."),
|
|
70
70
|
case_type: z
|
|
71
71
|
.string()
|
|
72
72
|
.optional()
|
|
@@ -13,7 +13,7 @@ export const ListTestCasesSchema = z.object({
|
|
|
13
13
|
folder_id: z
|
|
14
14
|
.string()
|
|
15
15
|
.optional()
|
|
16
|
-
.describe("
|
|
16
|
+
.describe("Optional. If provided, only return test cases in this folder. If omitted, returns all test cases in the project. Folder ids can be discovered via listFolders."),
|
|
17
17
|
case_type: z
|
|
18
18
|
.string()
|
|
19
19
|
.optional()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { apiClient } from "../../lib/apiClient.js";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
-
import {
|
|
3
|
+
import { errorText } from "../../lib/error.js";
|
|
4
4
|
import { fetchFormFields, normalizeDefaultFields as normalizeDefaultFieldsFromForm, projectIdentifierToId, } from "./TCG-utils/api.js";
|
|
5
5
|
import { getTMBaseURL } from "../../lib/tm-base-url.js";
|
|
6
6
|
import { getBrowserStackAuth } from "../../lib/get-auth.js";
|
|
@@ -37,15 +37,15 @@ export const UpdateTestCaseSchema = z.object({
|
|
|
37
37
|
priority: z
|
|
38
38
|
.string()
|
|
39
39
|
.optional()
|
|
40
|
-
.describe("
|
|
40
|
+
.describe("Updated priority. Accepts either display name (e.g. 'Medium', 'Critical', 'High', 'Low') or internal name (e.g. 'medium'). Valid values are per-project and discoverable via the form-fields endpoint."),
|
|
41
41
|
case_type: z
|
|
42
42
|
.string()
|
|
43
43
|
.optional()
|
|
44
|
-
.describe("
|
|
44
|
+
.describe("Updated test case type. Accepts either display name (e.g. 'Functional', 'Regression', 'Smoke & Sanity') or internal name (e.g. 'functional', 'smoke_sanity'). Valid values are per-project."),
|
|
45
45
|
automation_status: z
|
|
46
46
|
.string()
|
|
47
47
|
.optional()
|
|
48
|
-
.describe("
|
|
48
|
+
.describe("Updated automation status. Use internal name such as 'not_automated', 'automated', 'automation_not_required', 'cannot_be_automated', or 'obsolete'."),
|
|
49
49
|
status: z
|
|
50
50
|
.string()
|
|
51
51
|
.optional()
|
|
@@ -218,12 +218,14 @@ Error details: ${JSON.stringify(err.response?.data || err.message)}`,
|
|
|
218
218
|
isError: true,
|
|
219
219
|
};
|
|
220
220
|
}
|
|
221
|
-
const
|
|
221
|
+
const status = err.response?.status;
|
|
222
|
+
const detail = errorText(err, "unknown error");
|
|
223
|
+
const hint = status === 401 ? " Check the BrowserStack credentials in use." : "";
|
|
222
224
|
return {
|
|
223
225
|
content: [
|
|
224
226
|
{
|
|
225
227
|
type: "text",
|
|
226
|
-
text: `Failed to update test case
|
|
228
|
+
text: `Failed to update test case${status ? ` (HTTP ${status})` : ""}: ${detail}.${hint}`,
|
|
227
229
|
},
|
|
228
230
|
],
|
|
229
231
|
isError: true,
|
|
@@ -441,14 +441,15 @@ export default function addTestManagementTools(server, config) {
|
|
|
441
441
|
destructiveHint: false,
|
|
442
442
|
idempotentHint: false,
|
|
443
443
|
}, (args) => createProjectOrFolderTool(args, config, server));
|
|
444
|
-
tools.createTestCase = server.tool("createTestCase", "
|
|
444
|
+
tools.createTestCase = server.tool("createTestCase", "Use this tool to create a test case in BrowserStack Test Management." +
|
|
445
|
+
NEEDS_PROJECT_ID, CreateTestCaseSchema.shape, {
|
|
445
446
|
title: "Create Test Case",
|
|
446
447
|
readOnlyHint: false,
|
|
447
448
|
openWorldHint: false,
|
|
448
449
|
destructiveHint: false,
|
|
449
450
|
idempotentHint: false,
|
|
450
451
|
}, (args) => createTestCaseTool(args, config, server));
|
|
451
|
-
tools.updateTestCase = server.tool("updateTestCase", "Update an existing test case
|
|
452
|
+
tools.updateTestCase = server.tool("updateTestCase", "Update an existing test case in BrowserStack Test Management. Any subset of the following fields may be changed: name, description, preconditions, test_case_steps, owner, priority, case_type, automation_status, status, tags, issues, custom_fields. Only the supplied fields are modified." +
|
|
452
453
|
NEEDS_PROJECT_ID, UpdateTestCaseSchema.shape, {
|
|
453
454
|
title: "Update Test Case",
|
|
454
455
|
readOnlyHint: false,
|
|
@@ -456,7 +457,7 @@ export default function addTestManagementTools(server, config) {
|
|
|
456
457
|
destructiveHint: true,
|
|
457
458
|
idempotentHint: true,
|
|
458
459
|
}, (args) => updateTestCaseTool(args, config, server));
|
|
459
|
-
tools.listTestCases = server.tool("listTestCases", "List test cases in a project, optionally
|
|
460
|
+
tools.listTestCases = server.tool("listTestCases", "List test cases in a project, optionally scoped to a specific folder. Omit folder_id to list all test cases in the project; provide folder_id (discoverable via listFolders) to list only that folder's cases. Supports filters: case_type, priority, pagination." +
|
|
460
461
|
NEEDS_PROJECT_ID, ListTestCasesSchema.shape, {
|
|
461
462
|
title: "List Test Cases",
|
|
462
463
|
readOnlyHint: true,
|
|
@@ -464,7 +465,7 @@ export default function addTestManagementTools(server, config) {
|
|
|
464
465
|
destructiveHint: false,
|
|
465
466
|
idempotentHint: true,
|
|
466
467
|
}, (args) => listTestCasesTool(args, config, server));
|
|
467
|
-
tools.listFolders = server.tool("listFolders", "List folders
|
|
468
|
+
tools.listFolders = server.tool("listFolders", "List folders in a BrowserStack Test Management project, returning each folder's id and name (plus case counts and sub-folder counts). Pass parent_id to list sub-folders under a specific folder instead of top-level folders." +
|
|
468
469
|
NEEDS_PROJECT_ID, ListFoldersSchema.shape, {
|
|
469
470
|
title: "List Folders",
|
|
470
471
|
readOnlyHint: true,
|
|
@@ -486,21 +487,24 @@ export default function addTestManagementTools(server, config) {
|
|
|
486
487
|
destructiveHint: false,
|
|
487
488
|
idempotentHint: false,
|
|
488
489
|
}, (args) => createTestRunTool(args, config, server));
|
|
489
|
-
tools.listTestRuns = server.tool("listTestRuns", "List test runs with filters (
|
|
490
|
+
tools.listTestRuns = server.tool("listTestRuns", "List test runs in a project with optional filters (date ranges, assignee, state, etc.)" +
|
|
491
|
+
NEEDS_PROJECT_ID, ListTestRunsSchema.shape, {
|
|
490
492
|
title: "List Test Runs",
|
|
491
493
|
readOnlyHint: true,
|
|
492
494
|
openWorldHint: false,
|
|
493
495
|
destructiveHint: false,
|
|
494
496
|
idempotentHint: true,
|
|
495
497
|
}, (args) => listTestRunsTool(args, config, server));
|
|
496
|
-
tools.updateTestRun = server.tool("updateTestRun", "Update a test run or add test cases to it." +
|
|
498
|
+
tools.updateTestRun = server.tool("updateTestRun", "Update a test run's metadata and/or add test cases to it." +
|
|
499
|
+
NEEDS_PROJECT_ID, UpdateTestRunSchema.shape, {
|
|
497
500
|
title: "Update Test Run",
|
|
498
501
|
readOnlyHint: false,
|
|
499
502
|
openWorldHint: false,
|
|
500
503
|
destructiveHint: true,
|
|
501
504
|
idempotentHint: true,
|
|
502
505
|
}, (args) => updateTestRunTool(args, config, server));
|
|
503
|
-
tools.addTestResult = server.tool("addTestResult", "Add a test result to a test run
|
|
506
|
+
tools.addTestResult = server.tool("addTestResult", "Add a test result to a specific test run via BrowserStack Test Management API." +
|
|
507
|
+
NEEDS_PROJECT_ID, AddTestResultSchema.shape, {
|
|
504
508
|
title: "Add Test Result",
|
|
505
509
|
readOnlyHint: false,
|
|
506
510
|
openWorldHint: false,
|
|
@@ -522,7 +526,7 @@ export default function addTestManagementTools(server, config) {
|
|
|
522
526
|
destructiveHint: false,
|
|
523
527
|
idempotentHint: false,
|
|
524
528
|
}, (args, context) => createTestCasesFromFileTool(args, context, config, server));
|
|
525
|
-
tools.createLCASteps = server.tool("createLCASteps", "Generate Low Code Automation (LCA) steps for a test case." +
|
|
529
|
+
tools.createLCASteps = server.tool("createLCASteps", "Generate Low Code Automation (LCA) steps for a test case in BrowserStack Test Management using the Low Code Automation Agent." +
|
|
526
530
|
NEEDS_PROJECT_ID, CreateLCAStepsSchema.shape, {
|
|
527
531
|
title: "Create LCA Steps",
|
|
528
532
|
readOnlyHint: false,
|
|
@@ -530,7 +534,7 @@ export default function addTestManagementTools(server, config) {
|
|
|
530
534
|
destructiveHint: false,
|
|
531
535
|
idempotentHint: false,
|
|
532
536
|
}, (args, context) => createLCAStepsTool(args, context, config, server));
|
|
533
|
-
tools.listTestPlans = server.tool("listTestPlans", "List test plans (TP-*)
|
|
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." +
|
|
534
538
|
NEEDS_PROJECT_ID +
|
|
535
539
|
PLAN_WRITES_VIA_CAPABILITIES, ListTestPlansSchema.shape, {
|
|
536
540
|
title: "List Test Plans",
|
|
@@ -539,7 +543,7 @@ export default function addTestManagementTools(server, config) {
|
|
|
539
543
|
destructiveHint: false,
|
|
540
544
|
idempotentHint: true,
|
|
541
545
|
}, (args) => listTestPlansTool(args, config, server));
|
|
542
|
-
tools.getTestPlan = server.tool("getTestPlan", "
|
|
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." +
|
|
543
547
|
NEEDS_PROJECT_ID +
|
|
544
548
|
NEEDS_TEST_PLAN_ID +
|
|
545
549
|
PLAN_WRITES_VIA_CAPABILITIES, GetTestPlanSchema.shape, {
|
|
@@ -549,7 +553,7 @@ export default function addTestManagementTools(server, config) {
|
|
|
549
553
|
destructiveHint: false,
|
|
550
554
|
idempotentHint: true,
|
|
551
555
|
}, (args) => getTestPlanTool(args, config, server));
|
|
552
|
-
tools.listSubTestPlans = server.tool("listSubTestPlans", "List sub-plans under a test plan (TP-*)
|
|
556
|
+
tools.listSubTestPlans = server.tool("listSubTestPlans", "List sub-test-plans under a parent test plan (TP-*) in a Test Management project. Supports pagination." +
|
|
553
557
|
NEEDS_PROJECT_ID +
|
|
554
558
|
NEEDS_TEST_PLAN_ID +
|
|
555
559
|
PLAN_WRITES_VIA_CAPABILITIES, ListSubTestPlansSchema.shape, {
|
|
@@ -559,7 +563,7 @@ export default function addTestManagementTools(server, config) {
|
|
|
559
563
|
destructiveHint: false,
|
|
560
564
|
idempotentHint: true,
|
|
561
565
|
}, (args) => listSubTestPlansTool(args, config, server));
|
|
562
|
-
tools.getSubTestPlan = server.tool("getSubTestPlan", "
|
|
566
|
+
tools.getSubTestPlan = server.tool("getSubTestPlan", "Fetch a sub-test-plan (STP-*) under a parent plan (TP-*). Returns metadata and linked test runs." +
|
|
563
567
|
NEEDS_PROJECT_ID +
|
|
564
568
|
NEEDS_TEST_PLAN_ID +
|
|
565
569
|
PLAN_WRITES_VIA_CAPABILITIES, GetSubTestPlanSchema.shape, {
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
* twenty hand-written variants drift into twenty different signals.
|
|
24
24
|
*/
|
|
25
25
|
/** No tool lists projects, so this genuinely has to go to the agent. */
|
|
26
|
-
export declare const NEEDS_PROJECT_ID
|
|
26
|
+
export declare const NEEDS_PROJECT_ID: string;
|
|
27
27
|
/** A sibling tool can produce the id — prefer it over the agent. */
|
|
28
28
|
export declare function needsIdFrom(idLabel: string, sourceTool: string): string;
|
|
29
29
|
/**
|
|
@@ -60,7 +60,7 @@ export declare const NEEDS_BUILD_ID: string;
|
|
|
60
60
|
* this branch was open. This used to send the model to askBrowserStackAI for want of a tool;
|
|
61
61
|
* pointing at the agent when a real tool exists is exactly what the header above forbids.
|
|
62
62
|
*/
|
|
63
|
-
export declare const NEEDS_SESSION_ID
|
|
63
|
+
export declare const NEEDS_SESSION_ID: string;
|
|
64
64
|
/** A completed scan's ids come from startAccessibilityScan, or from the agent. */
|
|
65
65
|
export declare const NEEDS_A11Y_SCAN_ID: string;
|
|
66
66
|
/** Auth-config ids are not listable by any tool here. */
|
|
@@ -23,7 +23,9 @@
|
|
|
23
23
|
* twenty hand-written variants drift into twenty different signals.
|
|
24
24
|
*/
|
|
25
25
|
/** No tool lists projects, so this genuinely has to go to the agent. */
|
|
26
|
-
export const NEEDS_PROJECT_ID = "
|
|
26
|
+
export const NEEDS_PROJECT_ID = " Requires a project identifier (PR-*). No tool here lists projects, so if you do not " +
|
|
27
|
+
"have one, use the capability functions to list the Test Management projects, then " +
|
|
28
|
+
"retry this tool with the identifier they return.";
|
|
27
29
|
/** A sibling tool can produce the id — prefer it over the agent. */
|
|
28
30
|
export function needsIdFrom(idLabel, sourceTool) {
|
|
29
31
|
return ` Requires ${idLabel}. Call ${sourceTool} first if you do not have it.`;
|
|
@@ -34,8 +36,9 @@ export function needsIdFrom(idLabel, sourceTool) {
|
|
|
34
36
|
* tool read "Requires a project identifier ... call askBrowserStackAI", which routed
|
|
35
37
|
* "create me a project" through the agent before letting the tool run.
|
|
36
38
|
*/
|
|
37
|
-
export const PROJECT_ID_ONLY_FOR_FOLDER = " Creating a project needs no identifier
|
|
38
|
-
"
|
|
39
|
+
export const PROJECT_ID_ONLY_FOR_FOLDER = " Creating a project needs no identifier. Creating a folder inside an EXISTING project " +
|
|
40
|
+
"needs that project's identifier (PR-*); no tool here lists projects, so use the " +
|
|
41
|
+
"capability functions to look it up.";
|
|
39
42
|
/** A test plan id (TP-*) comes from listTestPlans. */
|
|
40
43
|
export const NEEDS_TEST_PLAN_ID = needsIdFrom("a test plan identifier (TP-*)", "listTestPlans");
|
|
41
44
|
/**
|
|
@@ -55,8 +58,9 @@ export const NEEDS_TEST_PLAN_ID = needsIdFrom("a test plan identifier (TP-*)", "
|
|
|
55
58
|
* completes on a client that can show a prompt. On one that cannot, the intended write is
|
|
56
59
|
* refused rather than performed.
|
|
57
60
|
*/
|
|
58
|
-
export const PLAN_WRITES_VIA_CAPABILITIES = " Creating a test plan or sub-plan, and linking or unlinking test runs, are not " +
|
|
59
|
-
"available as tools here: use the capability functions
|
|
61
|
+
export const PLAN_WRITES_VIA_CAPABILITIES = " Creating a test plan or sub-plan, and linking or unlinking test runs on one, are not " +
|
|
62
|
+
"available as tools here: use the capability functions, which reach Test Management's " +
|
|
63
|
+
"own API for these operations. They ask you to confirm before changing anything.";
|
|
60
64
|
/** A build id comes from either build-lookup tool. */
|
|
61
65
|
export const NEEDS_BUILD_ID = needsIdFrom("a BrowserStack build id", "getBuildId or listBuildId");
|
|
62
66
|
/**
|
|
@@ -64,12 +68,14 @@ export const NEEDS_BUILD_ID = needsIdFrom("a BrowserStack build id", "getBuildId
|
|
|
64
68
|
* this branch was open. This used to send the model to askBrowserStackAI for want of a tool;
|
|
65
69
|
* pointing at the agent when a real tool exists is exactly what the header above forbids.
|
|
66
70
|
*/
|
|
67
|
-
export const NEEDS_SESSION_ID = "
|
|
71
|
+
export const NEEDS_SESSION_ID = " Requires a session id. listSessions lists them for a build; if you do not have the " +
|
|
72
|
+
"build either, getBuildId or listBuildId resolves one from a project and build name.";
|
|
68
73
|
/** A completed scan's ids come from startAccessibilityScan, or from the agent. */
|
|
69
|
-
export const NEEDS_A11Y_SCAN_ID = "
|
|
70
|
-
"
|
|
74
|
+
export const NEEDS_A11Y_SCAN_ID = " Requires the ids of a completed scan, which are returned by startAccessibilityScan. " +
|
|
75
|
+
"No tool here lists past scans, so if you do not have the ids, start a new scan rather " +
|
|
76
|
+
"than guessing.";
|
|
71
77
|
/** Auth-config ids are not listable by any tool here. */
|
|
72
|
-
export const NEEDS_A11Y_CONFIG_ID = "
|
|
73
|
-
"one rather than guessing.";
|
|
78
|
+
export const NEEDS_A11Y_CONFIG_ID = " Requires the numeric id returned by createAccessibilityAuthConfig. No tool here lists " +
|
|
79
|
+
"existing configurations, so if you do not have the id, create one rather than guessing.";
|
|
74
80
|
/** Test ids come from listTestIds, which itself needs a build id. */
|
|
75
81
|
export const NEEDS_TEST_IDS = needsIdFrom("test ids", "listTestIds");
|