@browserstack/mcp-server 1.2.1 → 1.2.2

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
@@ -138,6 +138,8 @@ Generate test cases from PRDs, convert manual tests to low-code automation, and
138
138
 
139
139
  ## 🛠️ Installation
140
140
 
141
+ [![Install in VS Code](https://img.shields.io/badge/VS_Code-Install_Server-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](http://mcp.browserstack.com/one-click-setup?client=vscode)   [![Install in Cursor](https://img.shields.io/badge/Cursor-Install_Server-24bfa5?style=flat-square&color=000000&logo=visualstudiocode&logoColor=white)](http://mcp.browserstack.com/one-click-setup?client=cursor)
142
+
141
143
  1. **Create a BrowserStack Account**
142
144
 
143
145
  - Sign up for [BrowserStack](https://www.browserstack.com/users/sign_up) if you don't have an account already.
package/dist/index.d.ts CHANGED
@@ -2,3 +2,4 @@
2
2
  import "dotenv/config";
3
3
  export { setLogger } from "./logger.js";
4
4
  export { BrowserStackMcpServer } from "./server-factory.js";
5
+ export { trackMCP } from "./lib/instrumentation.js";
package/dist/index.js CHANGED
@@ -35,3 +35,4 @@ process.on("exit", () => {
35
35
  });
36
36
  export { setLogger } from "./logger.js";
37
37
  export { BrowserStackMcpServer } from "./server-factory.js";
38
+ export { trackMCP } from "./lib/instrumentation.js";
@@ -7,3 +7,4 @@ export declare enum BrowserStackProducts {
7
7
  * Fetches and caches BrowserStack datasets (live + app_live + app_automate) with a shared TTL.
8
8
  */
9
9
  export declare function getDevicesAndBrowsers(type: BrowserStackProducts): Promise<any>;
10
+ export declare function shouldSendStartedEvent(): boolean;
@@ -2,9 +2,11 @@ import fs from "fs";
2
2
  import os from "os";
3
3
  import path from "path";
4
4
  import { apiClient } from "./apiClient.js";
5
+ import config from "../config.js";
5
6
  const CACHE_DIR = path.join(os.homedir(), ".browserstack", "combined_cache");
6
7
  const CACHE_FILE = path.join(CACHE_DIR, "data.json");
7
8
  const TTL_MS = 24 * 60 * 60 * 1000; // 1 day
9
+ const TTL_STARTED_MS = 3 * 60 * 60 * 1000; // 3 Hours
8
10
  export var BrowserStackProducts;
9
11
  (function (BrowserStackProducts) {
10
12
  BrowserStackProducts["LIVE"] = "live";
@@ -49,3 +51,29 @@ export async function getDevicesAndBrowsers(type) {
49
51
  fs.writeFileSync(CACHE_FILE, JSON.stringify(cache), "utf8");
50
52
  return cache[type];
51
53
  }
54
+ // Rate limiter for started event (3H)
55
+ export function shouldSendStartedEvent() {
56
+ try {
57
+ if (config && config.REMOTE_MCP) {
58
+ return false;
59
+ }
60
+ if (!fs.existsSync(CACHE_DIR)) {
61
+ fs.mkdirSync(CACHE_DIR, { recursive: true });
62
+ }
63
+ let cache = {};
64
+ if (fs.existsSync(CACHE_FILE)) {
65
+ const raw = fs.readFileSync(CACHE_FILE, "utf8");
66
+ cache = JSON.parse(raw || "{}");
67
+ const last = parseInt(cache.lastStartedEvent, 10);
68
+ if (!isNaN(last) && Date.now() - last < TTL_STARTED_MS) {
69
+ return false;
70
+ }
71
+ }
72
+ cache.lastStartedEvent = Date.now();
73
+ fs.writeFileSync(CACHE_FILE, JSON.stringify(cache, null, 2), "utf8");
74
+ return true;
75
+ }
76
+ catch {
77
+ return true;
78
+ }
79
+ }
@@ -1,4 +1,5 @@
1
1
  import { trackMCP } from "./lib/instrumentation.js";
2
+ import { shouldSendStartedEvent } from "./lib/device-cache.js";
2
3
  export function setupOnInitialized(server, config) {
3
4
  const nodeVersion = process.versions.node;
4
5
  // Check for Node.js version
@@ -6,6 +7,8 @@ export function setupOnInitialized(server, config) {
6
7
  throw new Error("Node version is not supported. Please upgrade to 18.0.0 or later.");
7
8
  }
8
9
  server.server.oninitialized = () => {
9
- trackMCP("started", server.server.getClientVersion(), undefined, config);
10
+ if (shouldSendStartedEvent()) {
11
+ trackMCP("started", server.server.getClientVersion(), undefined, config);
12
+ }
10
13
  };
11
14
  }
@@ -26,8 +26,9 @@ export declare function resolveVersion(versions: string[], requestedVersion: str
26
26
  export declare function validateArgs(args: {
27
27
  desiredPlatform: string;
28
28
  desiredPlatformVersion: string;
29
- appPath: string;
29
+ appPath?: string;
30
30
  desiredPhone: string;
31
+ browserstackAppUrl?: string;
31
32
  }): void;
32
33
  /**
33
34
  * Uploads an application file to AppAutomate and returns the app URL
@@ -49,21 +49,24 @@ export function resolveVersion(versions, requestedVersion) {
49
49
  * Checks for presence and correctness of platform, device, and file types.
50
50
  */
51
51
  export function validateArgs(args) {
52
- const { desiredPlatform, desiredPlatformVersion, appPath, desiredPhone } = args;
52
+ const { desiredPlatform, desiredPlatformVersion, appPath, desiredPhone, browserstackAppUrl, } = args;
53
53
  if (!desiredPlatform || !desiredPhone) {
54
54
  throw new Error("Missing required arguments: desiredPlatform and desiredPhone are required");
55
55
  }
56
56
  if (!desiredPlatformVersion) {
57
57
  throw new Error("Missing required arguments: desiredPlatformVersion is required");
58
58
  }
59
- if (!appPath) {
60
- throw new Error("You must provide an appPath.");
61
- }
62
- if (desiredPlatform === "android" && !appPath.endsWith(".apk")) {
63
- throw new Error("You must provide a valid Android app path (.apk).");
64
- }
65
- if (desiredPlatform === "ios" && !appPath.endsWith(".ipa")) {
66
- throw new Error("You must provide a valid iOS app path (.ipa).");
59
+ if (!appPath && !browserstackAppUrl) {
60
+ throw new Error("Either appPath or browserstackAppUrl must be provided");
61
+ }
62
+ // Only validate app path format if appPath is provided
63
+ if (appPath) {
64
+ if (desiredPlatform === "android" && !appPath.endsWith(".apk")) {
65
+ throw new Error("You must provide a valid Android app path (.apk).");
66
+ }
67
+ if (desiredPlatform === "ios" && !appPath.endsWith(".ipa")) {
68
+ throw new Error("You must provide a valid iOS app path (.ipa).");
69
+ }
67
70
  }
68
71
  }
69
72
  /**
@@ -19,7 +19,7 @@ async function takeAppScreenshot(args) {
19
19
  let driver;
20
20
  try {
21
21
  validateArgs(args);
22
- const { desiredPlatform, desiredPhone, appPath, config } = args;
22
+ const { desiredPlatform, desiredPhone, appPath, browserstackAppUrl, config, } = args;
23
23
  let { desiredPlatformVersion } = args;
24
24
  const platforms = (await getDevicesAndBrowsers(BrowserStackProducts.APP_AUTOMATE)).mobile;
25
25
  const platformData = platforms.find((p) => p.os === desiredPlatform.toLowerCase());
@@ -35,8 +35,18 @@ async function takeAppScreenshot(args) {
35
35
  }
36
36
  const authString = getBrowserStackAuth(config);
37
37
  const [username, password] = authString.split(":");
38
- const app_url = await uploadApp(appPath, username, password);
39
- logger.info(`App uploaded. URL: ${app_url}`);
38
+ let app_url;
39
+ if (browserstackAppUrl) {
40
+ app_url = browserstackAppUrl;
41
+ logger.info(`Using provided BrowserStack app URL: ${app_url}`);
42
+ }
43
+ else {
44
+ if (!appPath) {
45
+ throw new Error("appPath is required when browserstackAppUrl is not provided");
46
+ }
47
+ app_url = await uploadApp(appPath, username, password);
48
+ logger.info(`App uploaded. URL: ${app_url}`);
49
+ }
40
50
  const capabilities = {
41
51
  platformName: desiredPlatform,
42
52
  "appium:platformVersion": selectedDevice.os_version,
@@ -89,11 +99,34 @@ async function takeAppScreenshot(args) {
89
99
  }
90
100
  //Runs AppAutomate tests on BrowserStack by uploading app and test suite, then triggering a test run.
91
101
  async function runAppTestsOnBrowserStack(args, config) {
102
+ // Validate that either paths or URLs are provided for both app and test suite
103
+ if (!args.browserstackAppUrl && !args.appPath) {
104
+ throw new Error("appPath is required when browserstackAppUrl is not provided");
105
+ }
106
+ if (!args.browserstackTestSuiteUrl && !args.testSuitePath) {
107
+ throw new Error("testSuitePath is required when browserstackTestSuiteUrl is not provided");
108
+ }
92
109
  switch (args.detectedAutomationFramework) {
93
110
  case AppTestPlatform.ESPRESSO: {
94
111
  try {
95
- const app_url = await uploadEspressoApp(args.appPath, config);
96
- const test_suite_url = await uploadEspressoTestSuite(args.testSuitePath, config);
112
+ let app_url;
113
+ if (args.browserstackAppUrl) {
114
+ app_url = args.browserstackAppUrl;
115
+ logger.info(`Using provided BrowserStack app URL: ${app_url}`);
116
+ }
117
+ else {
118
+ app_url = await uploadEspressoApp(args.appPath, config);
119
+ logger.info(`App uploaded. URL: ${app_url}`);
120
+ }
121
+ let test_suite_url;
122
+ if (args.browserstackTestSuiteUrl) {
123
+ test_suite_url = args.browserstackTestSuiteUrl;
124
+ logger.info(`Using provided BrowserStack test suite URL: ${test_suite_url}`);
125
+ }
126
+ else {
127
+ test_suite_url = await uploadEspressoTestSuite(args.testSuitePath, config);
128
+ logger.info(`Test suite uploaded. URL: ${test_suite_url}`);
129
+ }
97
130
  const build_id = await triggerEspressoBuild(app_url, test_suite_url, args.devices, args.project);
98
131
  return {
99
132
  content: [
@@ -111,8 +144,24 @@ async function runAppTestsOnBrowserStack(args, config) {
111
144
  }
112
145
  case AppTestPlatform.XCUITEST: {
113
146
  try {
114
- const app_url = await uploadXcuiApp(args.appPath, config);
115
- const test_suite_url = await uploadXcuiTestSuite(args.testSuitePath, config);
147
+ let app_url;
148
+ if (args.browserstackAppUrl) {
149
+ app_url = args.browserstackAppUrl;
150
+ logger.info(`Using provided BrowserStack app URL: ${app_url}`);
151
+ }
152
+ else {
153
+ app_url = await uploadXcuiApp(args.appPath, config);
154
+ logger.info(`App uploaded. URL: ${app_url}`);
155
+ }
156
+ let test_suite_url;
157
+ if (args.browserstackTestSuiteUrl) {
158
+ test_suite_url = args.browserstackTestSuiteUrl;
159
+ logger.info(`Using provided BrowserStack test suite URL: ${test_suite_url}`);
160
+ }
161
+ else {
162
+ test_suite_url = await uploadXcuiTestSuite(args.testSuitePath, config);
163
+ logger.info(`Test suite uploaded. URL: ${test_suite_url}`);
164
+ }
116
165
  const build_id = await triggerXcuiBuild(app_url, test_suite_url, args.devices, args.project, config);
117
166
  return {
118
167
  content: [
@@ -1,9 +1,10 @@
1
1
  import { BrowserStackConfig } from "../../lib/types.js";
2
2
  interface StartSessionArgs {
3
- appPath: string;
3
+ appPath?: string;
4
4
  desiredPlatform: "android" | "ios";
5
5
  desiredPhone: string;
6
6
  desiredPlatformVersion: string;
7
+ browserstackAppUrl?: string;
7
8
  }
8
9
  interface StartSessionOptions {
9
10
  config: BrowserStackConfig;
@@ -11,7 +11,7 @@ import envConfig from "../../config.js";
11
11
  * Start an App Live session: filter, select, upload, and open.
12
12
  */
13
13
  export async function startSession(args, options) {
14
- const { appPath, desiredPlatform, desiredPhone, desiredPlatformVersion } = args;
14
+ const { appPath, desiredPlatform, desiredPhone, desiredPlatformVersion, browserstackAppUrl, } = args;
15
15
  const { config } = options;
16
16
  // 1) Fetch devices for APP_LIVE
17
17
  const data = await getDevicesAndBrowsers(BrowserStackProducts.APP_LIVE);
@@ -38,11 +38,22 @@ export async function startSession(args, options) {
38
38
  desiredPlatformVersion !== "oldest") {
39
39
  note = `\n Note: The requested version "${desiredPlatformVersion}" is not available. Using "${version}" instead.`;
40
40
  }
41
- // 6) Upload app
42
- const authString = getBrowserStackAuth(config);
43
- const [username, password] = authString.split(":");
44
- const { app_url } = await uploadApp(appPath, username, password);
45
- logger.info(`App uploaded: ${app_url}`);
41
+ // 6) Upload app or use provided URL
42
+ let app_url;
43
+ if (browserstackAppUrl) {
44
+ app_url = browserstackAppUrl;
45
+ logger.info(`Using provided BrowserStack app URL: ${app_url}`);
46
+ }
47
+ else {
48
+ if (!appPath) {
49
+ throw new Error("appPath is required when browserstackAppUrl is not provided");
50
+ }
51
+ const authString = getBrowserStackAuth(config);
52
+ const [username, password] = authString.split(":");
53
+ const result = await uploadApp(appPath, username, password);
54
+ app_url = result.app_url;
55
+ logger.info(`App uploaded: ${app_url}`);
56
+ }
46
57
  if (!app_url) {
47
58
  throw new Error("Failed to upload app. Please try again.");
48
59
  }
@@ -7,7 +7,8 @@ import { BrowserStackConfig } from "../lib/types.js";
7
7
  export declare function startAppLiveSession(args: {
8
8
  desiredPlatform: string;
9
9
  desiredPlatformVersion: string;
10
- appPath: string;
10
+ appPath?: string;
11
11
  desiredPhone: string;
12
+ browserstackAppUrl?: string;
12
13
  }, config: BrowserStackConfig): Promise<CallToolResult>;
13
14
  export default function addAppLiveTools(server: McpServer, config: BrowserStackConfig): Record<string, any>;
@@ -10,34 +10,38 @@ export async function startAppLiveSession(args, config) {
10
10
  if (!args.desiredPlatform) {
11
11
  throw new Error("You must provide a desiredPlatform.");
12
12
  }
13
- if (!args.appPath) {
14
- throw new Error("You must provide a appPath.");
13
+ if (!args.appPath && !args.browserstackAppUrl) {
14
+ throw new Error("You must provide either appPath or browserstackAppUrl.");
15
15
  }
16
16
  if (!args.desiredPhone) {
17
17
  throw new Error("You must provide a desiredPhone.");
18
18
  }
19
- if (args.desiredPlatform === "android" && !args.appPath.endsWith(".apk")) {
20
- throw new Error("You must provide a valid Android app path.");
21
- }
22
- if (args.desiredPlatform === "ios" && !args.appPath.endsWith(".ipa")) {
23
- throw new Error("You must provide a valid iOS app path.");
24
- }
25
- // check if the app path exists && is readable
26
- try {
27
- if (!fs.existsSync(args.appPath)) {
28
- throw new Error("The app path does not exist.");
19
+ // Only validate app path if it's provided (not using browserstackAppUrl)
20
+ if (args.appPath) {
21
+ if (args.desiredPlatform === "android" && !args.appPath.endsWith(".apk")) {
22
+ throw new Error("You must provide a valid Android app path.");
23
+ }
24
+ if (args.desiredPlatform === "ios" && !args.appPath.endsWith(".ipa")) {
25
+ throw new Error("You must provide a valid iOS app path.");
26
+ }
27
+ // check if the app path exists && is readable
28
+ try {
29
+ if (!fs.existsSync(args.appPath)) {
30
+ throw new Error("The app path does not exist.");
31
+ }
32
+ fs.accessSync(args.appPath, fs.constants.R_OK);
33
+ }
34
+ catch (error) {
35
+ logger.error("The app path does not exist or is not readable: %s", error);
36
+ throw new Error("The app path does not exist or is not readable.");
29
37
  }
30
- fs.accessSync(args.appPath, fs.constants.R_OK);
31
- }
32
- catch (error) {
33
- logger.error("The app path does not exist or is not readable: %s", error);
34
- throw new Error("The app path does not exist or is not readable.");
35
38
  }
36
39
  const launchUrl = await startSession({
37
40
  appPath: args.appPath,
38
41
  desiredPlatform: args.desiredPlatform,
39
42
  desiredPhone: args.desiredPhone,
40
43
  desiredPlatformVersion: args.desiredPlatformVersion,
44
+ browserstackAppUrl: args.browserstackAppUrl,
41
45
  }, { config });
42
46
  return {
43
47
  content: [
@@ -426,7 +426,7 @@ exports.config.capabilities.forEach(function (caps) {
426
426
  ---STEP---
427
427
 
428
428
  Run your tests:
429
- You can now run your tests on BrowserStack using your standard WebdriverIO command.
429
+ You can now run your tests on BrowserStack using your standard WebdriverIO command or Use the commands defined in your package.json file to run the tests.
430
430
  `;
431
431
  const cypressInstructions = (username, accessKey) => `
432
432
  ---STEP---
@@ -555,5 +555,8 @@ export const SUPPORTED_CONFIGURATIONS = {
555
555
  cypress: {
556
556
  cypress: { instructions: cypressInstructions },
557
557
  },
558
+ webdriverio: {
559
+ mocha: { instructions: webdriverioInstructions },
560
+ },
558
561
  },
559
562
  };
@@ -8,7 +8,8 @@ export type SDKSupportedLanguage = keyof typeof SDKSupportedLanguageEnum;
8
8
  export declare enum SDKSupportedBrowserAutomationFrameworkEnum {
9
9
  playwright = "playwright",
10
10
  selenium = "selenium",
11
- cypress = "cypress"
11
+ cypress = "cypress",
12
+ webdriverio = "webdriverio"
12
13
  }
13
14
  export type SDKSupportedBrowserAutomationFramework = keyof typeof SDKSupportedBrowserAutomationFrameworkEnum;
14
15
  export declare enum SDKSupportedTestingFrameworkEnum {
@@ -10,6 +10,7 @@ export var SDKSupportedBrowserAutomationFrameworkEnum;
10
10
  SDKSupportedBrowserAutomationFrameworkEnum["playwright"] = "playwright";
11
11
  SDKSupportedBrowserAutomationFrameworkEnum["selenium"] = "selenium";
12
12
  SDKSupportedBrowserAutomationFrameworkEnum["cypress"] = "cypress";
13
+ SDKSupportedBrowserAutomationFrameworkEnum["webdriverio"] = "webdriverio";
13
14
  })(SDKSupportedBrowserAutomationFrameworkEnum || (SDKSupportedBrowserAutomationFrameworkEnum = {}));
14
15
  export var SDKSupportedTestingFrameworkEnum;
15
16
  (function (SDKSupportedTestingFrameworkEnum) {
@@ -21,6 +21,7 @@ export interface TestCaseCreateRequest {
21
21
  issue_tracker?: IssueTracker;
22
22
  tags?: string[];
23
23
  custom_fields?: Record<string, string>;
24
+ automation_status?: string;
24
25
  }
25
26
  export interface TestCaseResponse {
26
27
  data: {
@@ -80,6 +81,7 @@ export declare const CreateTestCaseSchema: z.ZodObject<{
80
81
  }>>;
81
82
  tags: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
82
83
  custom_fields: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
84
+ automation_status: z.ZodOptional<z.ZodString>;
83
85
  }, "strip", z.ZodTypeAny, {
84
86
  name: string;
85
87
  test_case_steps: {
@@ -90,6 +92,7 @@ export declare const CreateTestCaseSchema: z.ZodObject<{
90
92
  folder_id: string;
91
93
  issues?: string[] | undefined;
92
94
  description?: string | undefined;
95
+ automation_status?: string | undefined;
93
96
  tags?: string[] | undefined;
94
97
  custom_fields?: Record<string, string> | undefined;
95
98
  preconditions?: string | undefined;
@@ -108,6 +111,7 @@ export declare const CreateTestCaseSchema: z.ZodObject<{
108
111
  folder_id: string;
109
112
  issues?: string[] | undefined;
110
113
  description?: string | undefined;
114
+ automation_status?: string | undefined;
111
115
  tags?: string[] | undefined;
112
116
  custom_fields?: Record<string, string> | undefined;
113
117
  preconditions?: string | undefined;
@@ -49,6 +49,10 @@ export const CreateTestCaseSchema = z.object({
49
49
  .record(z.string(), z.string())
50
50
  .optional()
51
51
  .describe("Map of custom field names to values."),
52
+ automation_status: z
53
+ .string()
54
+ .optional()
55
+ .describe("Automation status of the test case. Common values include 'not_automated', 'automated', 'automation_not_required'."),
52
56
  });
53
57
  export function sanitizeArgs(args) {
54
58
  const cleaned = { ...args };
@@ -58,6 +62,8 @@ export function sanitizeArgs(args) {
58
62
  delete cleaned.owner;
59
63
  if (cleaned.preconditions === null)
60
64
  delete cleaned.preconditions;
65
+ if (cleaned.automation_status === null)
66
+ delete cleaned.automation_status;
61
67
  if (cleaned.issue_tracker) {
62
68
  if (cleaned.issue_tracker.name === undefined ||
63
69
  cleaned.issue_tracker.host === undefined) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@browserstack/mcp-server",
3
- "version": "1.2.1",
3
+ "version": "1.2.2",
4
4
  "description": "BrowserStack's Official MCP Server",
5
5
  "main": "dist/index.js",
6
6
  "repository": {