@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 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?: string;
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
- * - If `allowedBaseDir` is set, the canonical path must live inside it
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
- * - If `allowedBaseDir` is set, the canonical path must live inside it
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
- let baseCanonical;
53
- try {
54
- baseCanonical = fs.realpathSync(path.resolve(options.allowedBaseDir));
55
- }
56
- catch {
57
- throw new Error(`Upload rejected: configured MCP_UPLOAD_BASE_DIR does not exist (${options.allowedBaseDir}).`);
58
- }
59
- const baseWithSep = baseCanonical.endsWith(path.sep)
60
- ? baseCanonical
61
- : baseCanonical + path.sep;
62
- if (canonical !== baseCanonical && !canonical.startsWith(baseWithSep)) {
63
- throw new Error(`Upload rejected: file must be located inside ${baseCanonical}.`);
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: [
@@ -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,
@@ -13,11 +13,11 @@ export function registerRunBrowserStackTestsTool(server, config) {
13
13
  idempotentHint: true,
14
14
  }, async (args) => {
15
15
  try {
16
- trackMCP("runTestsOnBrowserStack", server.server.getClientVersion(), config);
16
+ trackMCP("setupBrowserStackAutomateTests", server.server.getClientVersion(), undefined, config);
17
17
  return await runTestsOnBrowserStackHandler(args, config);
18
18
  }
19
19
  catch (error) {
20
- return handleMCPError("runTestsOnBrowserStack", server, config, error);
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) {
@@ -38,11 +38,11 @@ export function registerPercyTools(server, config) {
38
38
  idempotentHint: true,
39
39
  }, async (args) => {
40
40
  try {
41
- trackMCP("VisualTestIntegrationAgent", server.server.getClientVersion(), config);
41
+ trackMCP("percyVisualTestIntegrationAgent", server.server.getClientVersion(), undefined, config);
42
42
  return simulatePercyChangeHandler(args, config);
43
43
  }
44
44
  catch (error) {
45
- return handleMCPError("VisualTestIntegrationAgent", server, config, error);
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("setupPercyVisualTesting", server.server.getClientVersion(), config);
56
+ trackMCP("expandPercyVisualTesting", server.server.getClientVersion(), undefined, config);
57
57
  return setUpPercyHandler(args, config);
58
58
  }
59
59
  catch (error) {
60
- return handleMCPError("setupPercyVisualTesting", server, config, error);
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
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@browserstack/mcp-server",
3
- "version": "1.4.0",
3
+ "version": "1.4.1-beta.1",
4
4
  "description": "BrowserStack's Official MCP Server",
5
5
  "mcpName": "io.github.browserstack/mcp-server",
6
6
  "main": "dist/index.js",