@browserstack/mcp-server 1.4.0 → 1.4.1-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/lib/upload-validator.d.ts +3 -2
- package/dist/lib/upload-validator.js +18 -15
- package/dist/tools/appautomate-utils/native-execution/constants.js +4 -2
- package/dist/tools/appautomate.js +3 -1
- package/dist/tools/applive.js +1 -1
- package/dist/tools/bstack-sdk.js +2 -2
- package/dist/tools/build-insights.js +1 -1
- package/dist/tools/percy-sdk.js +9 -9
- package/dist/tools/testmanagement.js +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -292,6 +292,8 @@ Select the “Installed” tab. Click the “Configure MCP Servers” button at
|
|
|
292
292
|
}
|
|
293
293
|
```
|
|
294
294
|
|
|
295
|
+
> **File & app uploads:** tools that upload a local file/app (`uploadProductRequirementFile`, `takeAppScreenshot`, `runAppTestsOnBrowserStack`, `runAppLiveSession`) require the `MCP_UPLOAD_BASE_DIR` env var set to a directory containing those files; uploads are restricted to it.
|
|
296
|
+
|
|
295
297
|
### 💡 List of BrowserStack MCP Tools
|
|
296
298
|
|
|
297
299
|
As of now we support 46 tools.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export interface UploadValidationOptions {
|
|
2
2
|
allowedExtensions: readonly string[];
|
|
3
3
|
maxSizeBytes: number;
|
|
4
|
-
allowedBaseDir
|
|
4
|
+
allowedBaseDir: string | undefined;
|
|
5
5
|
}
|
|
6
6
|
/**
|
|
7
7
|
* Canonicalizes and validates a user-supplied upload path. Returns the resolved
|
|
@@ -13,7 +13,8 @@ export interface UploadValidationOptions {
|
|
|
13
13
|
* - File extension is in `allowedExtensions` (case-insensitive)
|
|
14
14
|
* - No path segment is a hidden dir/file (starts with `.`); blocks ~/.ssh,
|
|
15
15
|
* ~/.aws, .env, etc. even after symlink resolution
|
|
16
|
-
* -
|
|
16
|
+
* - `allowedBaseDir` is mandatory: uploads are refused unless it is configured
|
|
17
|
+
* (via MCP_UPLOAD_BASE_DIR), and the canonical path must live inside it
|
|
17
18
|
*/
|
|
18
19
|
export declare function validateUploadPath(filePath: string, options: UploadValidationOptions): string;
|
|
19
20
|
export declare const APP_BINARY_EXTENSIONS: readonly [".apk", ".aab", ".ipa", ".app", ".zip"];
|
|
@@ -10,7 +10,8 @@ import path from "path";
|
|
|
10
10
|
* - File extension is in `allowedExtensions` (case-insensitive)
|
|
11
11
|
* - No path segment is a hidden dir/file (starts with `.`); blocks ~/.ssh,
|
|
12
12
|
* ~/.aws, .env, etc. even after symlink resolution
|
|
13
|
-
* -
|
|
13
|
+
* - `allowedBaseDir` is mandatory: uploads are refused unless it is configured
|
|
14
|
+
* (via MCP_UPLOAD_BASE_DIR), and the canonical path must live inside it
|
|
14
15
|
*/
|
|
15
16
|
export function validateUploadPath(filePath, options) {
|
|
16
17
|
if (typeof filePath !== "string" || filePath.trim().length === 0) {
|
|
@@ -48,20 +49,22 @@ export function validateUploadPath(filePath, options) {
|
|
|
48
49
|
if (!allowed.includes(ext)) {
|
|
49
50
|
throw new Error(`Upload rejected: file extension "${ext || "(none)"}" is not in the allowed list (${allowed.join(", ")}).`);
|
|
50
51
|
}
|
|
51
|
-
if (options.allowedBaseDir) {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
52
|
+
if (!options.allowedBaseDir) {
|
|
53
|
+
throw new Error("Upload rejected: MCP_UPLOAD_BASE_DIR is not set. Set it to the directory " +
|
|
54
|
+
"containing the files to upload, then restart the MCP server.");
|
|
55
|
+
}
|
|
56
|
+
let baseCanonical;
|
|
57
|
+
try {
|
|
58
|
+
baseCanonical = fs.realpathSync(path.resolve(options.allowedBaseDir));
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
throw new Error(`Upload rejected: configured MCP_UPLOAD_BASE_DIR does not exist (${options.allowedBaseDir}).`);
|
|
62
|
+
}
|
|
63
|
+
const baseWithSep = baseCanonical.endsWith(path.sep)
|
|
64
|
+
? baseCanonical
|
|
65
|
+
: baseCanonical + path.sep;
|
|
66
|
+
if (canonical !== baseCanonical && !canonical.startsWith(baseWithSep)) {
|
|
67
|
+
throw new Error(`Upload rejected: file must be located inside ${baseCanonical}.`);
|
|
65
68
|
}
|
|
66
69
|
return canonical;
|
|
67
70
|
}
|
|
@@ -20,7 +20,8 @@ export const RUN_APP_AUTOMATE_SCHEMA = {
|
|
|
20
20
|
" xcodebuild clean -scheme YOUR_SCHEME && \\\n" +
|
|
21
21
|
" xcodebuild archive -scheme YOUR_SCHEME -configuration Release -archivePath build/app.xcarchive && \\\n" +
|
|
22
22
|
" xcodebuild -exportArchive -archivePath build/app.xcarchive -exportPath build/ipa -exportOptionsPlist exportOptions.plist\n\n" +
|
|
23
|
-
"If in other directory, provide existing app path"
|
|
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."),
|
|
24
25
|
testSuitePath: z
|
|
25
26
|
.string()
|
|
26
27
|
.describe("Path to your test suite file:\n" +
|
|
@@ -30,7 +31,8 @@ export const RUN_APP_AUTOMATE_SCHEMA = {
|
|
|
30
31
|
" xcodebuild test-without-building -scheme YOUR_SCHEME -destination 'generic/platform=iOS' && \\\n" +
|
|
31
32
|
" cd ~/Library/Developer/Xcode/DerivedData/*/Build/Products/Debug-iphonesimulator/ && \\\n" +
|
|
32
33
|
" zip -r Tests.zip *.xctestrun *-Runner.app\n\n" +
|
|
33
|
-
"If in other directory, provide existing test file path"
|
|
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."),
|
|
34
36
|
devices: z
|
|
35
37
|
.array(MobileDeviceSchema)
|
|
36
38
|
.max(3)
|
|
@@ -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("The path to the .apk or .ipa file. Required for app installation."),
|
|
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,
|
|
@@ -266,9 +266,11 @@ export default function addAppAutomationTools(server, config) {
|
|
|
266
266
|
idempotentHint: true,
|
|
267
267
|
}, async (args) => {
|
|
268
268
|
try {
|
|
269
|
+
trackMCP("setupBrowserStackAppAutomateTests", server.server.getClientVersion(), undefined, config);
|
|
269
270
|
return await setupAppAutomateHandler(args, config);
|
|
270
271
|
}
|
|
271
272
|
catch (error) {
|
|
273
|
+
trackMCP("setupBrowserStackAppAutomateTests", server.server.getClientVersion(), error, config);
|
|
272
274
|
const error_message = error instanceof Error ? error.message : "Unknown error";
|
|
273
275
|
return {
|
|
274
276
|
content: [
|
package/dist/tools/applive.js
CHANGED
|
@@ -66,7 +66,7 @@ export default function addAppLiveTools(server, config) {
|
|
|
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("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."),
|
|
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,
|
package/dist/tools/bstack-sdk.js
CHANGED
|
@@ -13,11 +13,11 @@ export function registerRunBrowserStackTestsTool(server, config) {
|
|
|
13
13
|
idempotentHint: true,
|
|
14
14
|
}, async (args) => {
|
|
15
15
|
try {
|
|
16
|
-
trackMCP("
|
|
16
|
+
trackMCP("setupBrowserStackAutomateTests", server.server.getClientVersion(), undefined, config);
|
|
17
17
|
return await runTestsOnBrowserStackHandler(args, config);
|
|
18
18
|
}
|
|
19
19
|
catch (error) {
|
|
20
|
-
return handleMCPError("
|
|
20
|
+
return handleMCPError("setupBrowserStackAutomateTests", server, config, error);
|
|
21
21
|
}
|
|
22
22
|
});
|
|
23
23
|
return tools;
|
|
@@ -108,7 +108,7 @@ export default function addBuildInsightsTools(server, config) {
|
|
|
108
108
|
idempotentHint: true,
|
|
109
109
|
}, async (args) => {
|
|
110
110
|
try {
|
|
111
|
-
trackMCP("fetchBuildInsights", server.server.getClientVersion(), config);
|
|
111
|
+
trackMCP("fetchBuildInsights", server.server.getClientVersion(), undefined, config);
|
|
112
112
|
return await fetchBuildInsightsTool(args, config);
|
|
113
113
|
}
|
|
114
114
|
catch (error) {
|
package/dist/tools/percy-sdk.js
CHANGED
|
@@ -38,11 +38,11 @@ export function registerPercyTools(server, config) {
|
|
|
38
38
|
idempotentHint: true,
|
|
39
39
|
}, async (args) => {
|
|
40
40
|
try {
|
|
41
|
-
trackMCP("
|
|
41
|
+
trackMCP("percyVisualTestIntegrationAgent", server.server.getClientVersion(), undefined, config);
|
|
42
42
|
return simulatePercyChangeHandler(args, config);
|
|
43
43
|
}
|
|
44
44
|
catch (error) {
|
|
45
|
-
return handleMCPError("
|
|
45
|
+
return handleMCPError("percyVisualTestIntegrationAgent", server, config, error);
|
|
46
46
|
}
|
|
47
47
|
});
|
|
48
48
|
tools.setupPercyVisualTesting = server.tool("expandPercyVisualTesting", SETUP_PERCY_DESCRIPTION, SetUpPercyParamsShape, {
|
|
@@ -53,11 +53,11 @@ export function registerPercyTools(server, config) {
|
|
|
53
53
|
idempotentHint: true,
|
|
54
54
|
}, async (args) => {
|
|
55
55
|
try {
|
|
56
|
-
trackMCP("
|
|
56
|
+
trackMCP("expandPercyVisualTesting", server.server.getClientVersion(), undefined, config);
|
|
57
57
|
return setUpPercyHandler(args, config);
|
|
58
58
|
}
|
|
59
59
|
catch (error) {
|
|
60
|
-
return handleMCPError("
|
|
60
|
+
return handleMCPError("expandPercyVisualTesting", server, config, error);
|
|
61
61
|
}
|
|
62
62
|
});
|
|
63
63
|
tools.addPercySnapshotCommands = server.tool("addPercySnapshotCommands", PERCY_SNAPSHOT_COMMANDS_DESCRIPTION, UpdateTestFileWithInstructionsParams, {
|
|
@@ -68,7 +68,7 @@ export function registerPercyTools(server, config) {
|
|
|
68
68
|
idempotentHint: true,
|
|
69
69
|
}, async (args) => {
|
|
70
70
|
try {
|
|
71
|
-
trackMCP("addPercySnapshotCommands", server.server.getClientVersion(), config);
|
|
71
|
+
trackMCP("addPercySnapshotCommands", server.server.getClientVersion(), undefined, config);
|
|
72
72
|
return await updateTestsWithPercyCommands(args);
|
|
73
73
|
}
|
|
74
74
|
catch (error) {
|
|
@@ -83,7 +83,7 @@ export function registerPercyTools(server, config) {
|
|
|
83
83
|
idempotentHint: true,
|
|
84
84
|
}, async () => {
|
|
85
85
|
try {
|
|
86
|
-
trackMCP("listTestFiles", server.server.getClientVersion(), config);
|
|
86
|
+
trackMCP("listTestFiles", server.server.getClientVersion(), undefined, config);
|
|
87
87
|
return addListTestFiles();
|
|
88
88
|
}
|
|
89
89
|
catch (error) {
|
|
@@ -98,7 +98,7 @@ export function registerPercyTools(server, config) {
|
|
|
98
98
|
idempotentHint: true,
|
|
99
99
|
}, async (args) => {
|
|
100
100
|
try {
|
|
101
|
-
trackMCP("runPercyScan", server.server.getClientVersion(), config);
|
|
101
|
+
trackMCP("runPercyScan", server.server.getClientVersion(), undefined, config);
|
|
102
102
|
return runPercyScan(args);
|
|
103
103
|
}
|
|
104
104
|
catch (error) {
|
|
@@ -113,7 +113,7 @@ export function registerPercyTools(server, config) {
|
|
|
113
113
|
idempotentHint: true,
|
|
114
114
|
}, async (args) => {
|
|
115
115
|
try {
|
|
116
|
-
trackMCP("fetchPercyChanges", server.server.getClientVersion(), config);
|
|
116
|
+
trackMCP("fetchPercyChanges", server.server.getClientVersion(), undefined, config);
|
|
117
117
|
return await fetchPercyChanges(args, config);
|
|
118
118
|
}
|
|
119
119
|
catch (error) {
|
|
@@ -128,7 +128,7 @@ export function registerPercyTools(server, config) {
|
|
|
128
128
|
idempotentHint: true,
|
|
129
129
|
}, async (args) => {
|
|
130
130
|
try {
|
|
131
|
-
trackMCP("managePercyBuildApproval", server.server.getClientVersion(), config);
|
|
131
|
+
trackMCP("managePercyBuildApproval", server.server.getClientVersion(), undefined, config);
|
|
132
132
|
return await approveOrDeclinePercyBuild(args, config);
|
|
133
133
|
}
|
|
134
134
|
catch (error) {
|
|
@@ -264,11 +264,11 @@ export async function uploadProductRequirementFileTool(args, config, server) {
|
|
|
264
264
|
*/
|
|
265
265
|
export async function createTestCasesFromFileTool(args, context, config, server) {
|
|
266
266
|
try {
|
|
267
|
-
trackMCP("createTestCasesFromFile", server.server.getClientVersion(), undefined);
|
|
267
|
+
trackMCP("createTestCasesFromFile", server.server.getClientVersion(), undefined, config);
|
|
268
268
|
return await createTestCasesFromFile(args, context, config);
|
|
269
269
|
}
|
|
270
270
|
catch (err) {
|
|
271
|
-
trackMCP("createTestCasesFromFile", server.server.getClientVersion(), err);
|
|
271
|
+
trackMCP("createTestCasesFromFile", server.server.getClientVersion(), err, config);
|
|
272
272
|
return {
|
|
273
273
|
content: [
|
|
274
274
|
{
|