@browserstack/mcp-server 1.0.6 → 1.0.9
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
|
@@ -88,7 +88,7 @@ Use the following prompts to run/debug/fix your **automated tests** on BrowserSt
|
|
|
88
88
|
|
|
89
89
|
1. **Create a BrowserStack Account**
|
|
90
90
|
|
|
91
|
-
- Sign up for [BrowserStack](https://www.browserstack.com/
|
|
91
|
+
- Sign up for [BrowserStack](https://www.browserstack.com/users/sign_up) if you don't have an account already.
|
|
92
92
|
|
|
93
93
|
- ℹ️ If you have an open-source project, we'll be able to provide you with a [free plan](https://www.browserstack.com/open-source).
|
|
94
94
|
<div align="center">
|
|
@@ -154,6 +154,14 @@ Use the following prompts to run/debug/fix your **automated tests** on BrowserSt
|
|
|
154
154
|
}
|
|
155
155
|
```
|
|
156
156
|
|
|
157
|
+
### Installing via Smithery
|
|
158
|
+
|
|
159
|
+
To install BrowserStack Test Platform Server for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@browserstack/mcp-server):
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
npx -y @smithery/cli install @browserstack/mcp-server --client claude
|
|
163
|
+
```
|
|
164
|
+
|
|
157
165
|
## 🤝 Recommended MCP Clients
|
|
158
166
|
|
|
159
167
|
- We recommend using **Github Copilot or Cursor** for automated testing + debugging use cases.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.customFuzzySearch = customFuzzySearch;
|
|
4
|
+
// 1. Compute Levenshtein distance between two strings
|
|
5
|
+
function levenshtein(a, b) {
|
|
6
|
+
const dp = Array(a.length + 1)
|
|
7
|
+
.fill(0)
|
|
8
|
+
.map(() => Array(b.length + 1).fill(0));
|
|
9
|
+
for (let i = 0; i <= a.length; i++)
|
|
10
|
+
dp[i][0] = i;
|
|
11
|
+
for (let j = 0; j <= b.length; j++)
|
|
12
|
+
dp[0][j] = j;
|
|
13
|
+
for (let i = 1; i <= a.length; i++) {
|
|
14
|
+
for (let j = 1; j <= b.length; j++) {
|
|
15
|
+
dp[i][j] = Math.min(dp[i - 1][j] + 1, // deletion
|
|
16
|
+
dp[i][j - 1] + 1, // insertion
|
|
17
|
+
dp[i - 1][j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return dp[a.length][b.length];
|
|
21
|
+
}
|
|
22
|
+
// 2. Score one item against the query (normalized score 0–1)
|
|
23
|
+
function scoreItem(item, keys, queryTokens) {
|
|
24
|
+
let best = Infinity;
|
|
25
|
+
for (const key of keys) {
|
|
26
|
+
const field = String(item[key] ?? "").toLowerCase();
|
|
27
|
+
const fieldTokens = field.split(/\s+/);
|
|
28
|
+
const tokenScores = queryTokens.map((qt) => {
|
|
29
|
+
const minNormalized = Math.min(...fieldTokens.map((ft) => {
|
|
30
|
+
const rawDist = levenshtein(ft, qt);
|
|
31
|
+
const maxLen = Math.max(ft.length, qt.length);
|
|
32
|
+
return maxLen === 0 ? 0 : rawDist / maxLen; // normalized 0–1
|
|
33
|
+
}));
|
|
34
|
+
return minNormalized;
|
|
35
|
+
});
|
|
36
|
+
const avg = tokenScores.reduce((a, b) => a + b, 0) / tokenScores.length;
|
|
37
|
+
best = Math.min(best, avg);
|
|
38
|
+
}
|
|
39
|
+
return best;
|
|
40
|
+
}
|
|
41
|
+
// 3. The search entrypoint
|
|
42
|
+
function customFuzzySearch(list, keys, query, limit = 5, maxDistance = 0.6) {
|
|
43
|
+
const q = query.toLowerCase().trim();
|
|
44
|
+
const queryTokens = q.split(/\s+/);
|
|
45
|
+
return list
|
|
46
|
+
.map((item) => ({ item, score: scoreItem(item, keys, queryTokens) }))
|
|
47
|
+
.filter((x) => x.score <= maxDistance)
|
|
48
|
+
.sort((a, b) => a.score - b.score)
|
|
49
|
+
.slice(0, limit)
|
|
50
|
+
.map((x) => x.item);
|
|
51
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.getAppLiveData = getAppLiveData;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const os_1 = __importDefault(require("os"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const CACHE_DIR = path_1.default.join(os_1.default.homedir(), ".browserstack", "app_live_cache");
|
|
11
|
+
const CACHE_FILE = path_1.default.join(CACHE_DIR, "app_live.json");
|
|
12
|
+
const TTL_MS = 24 * 60 * 60 * 1000; // 1 day
|
|
13
|
+
/**
|
|
14
|
+
* Fetches and caches the App Live devices JSON with a 1-day TTL.
|
|
15
|
+
*/
|
|
16
|
+
async function getAppLiveData() {
|
|
17
|
+
if (!fs_1.default.existsSync(CACHE_DIR)) {
|
|
18
|
+
fs_1.default.mkdirSync(CACHE_DIR, { recursive: true });
|
|
19
|
+
}
|
|
20
|
+
if (fs_1.default.existsSync(CACHE_FILE)) {
|
|
21
|
+
const stats = fs_1.default.statSync(CACHE_FILE);
|
|
22
|
+
if (Date.now() - stats.mtimeMs < TTL_MS) {
|
|
23
|
+
return JSON.parse(fs_1.default.readFileSync(CACHE_FILE, "utf8"));
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
const response = await fetch("https://www.browserstack.com/list-of-browsers-and-platforms/app_live.json");
|
|
27
|
+
if (!response.ok) {
|
|
28
|
+
throw new Error(`Failed to fetch app live list: ${response.statusText}`);
|
|
29
|
+
}
|
|
30
|
+
const data = await response.json();
|
|
31
|
+
fs_1.default.writeFileSync(CACHE_FILE, JSON.stringify(data), "utf8");
|
|
32
|
+
return data;
|
|
33
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.fuzzySearchDevices = fuzzySearchDevices;
|
|
4
|
+
const fuzzy_1 = require("../../lib/fuzzy");
|
|
5
|
+
/**
|
|
6
|
+
* Fuzzy searches App Live device entries by name.
|
|
7
|
+
*/
|
|
8
|
+
async function fuzzySearchDevices(devices, query, limit = 5) {
|
|
9
|
+
const top_match = (0, fuzzy_1.customFuzzySearch)(devices, ["device", "display_name"], query, limit);
|
|
10
|
+
return top_match;
|
|
11
|
+
}
|
|
@@ -6,30 +6,138 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.startSession = startSession;
|
|
7
7
|
const child_process_1 = __importDefault(require("child_process"));
|
|
8
8
|
const logger_1 = __importDefault(require("../../logger"));
|
|
9
|
+
const device_cache_1 = require("./device-cache");
|
|
10
|
+
const fuzzy_search_1 = require("./fuzzy-search");
|
|
9
11
|
const utils_1 = require("../../lib/utils");
|
|
12
|
+
const upload_app_1 = require("./upload-app");
|
|
13
|
+
/**
|
|
14
|
+
* Starts an App Live session after filtering, fuzzy matching, and launching.
|
|
15
|
+
* @param args - The arguments for starting the session.
|
|
16
|
+
* @returns The launch URL for the session.
|
|
17
|
+
* @throws Will throw an error if no devices are found or if the app URL is invalid.
|
|
18
|
+
*/
|
|
10
19
|
async function startSession(args) {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
const { appPath, desiredPlatform, desiredPhone } = args;
|
|
21
|
+
let { desiredPlatformVersion } = args;
|
|
22
|
+
const data = await (0, device_cache_1.getAppLiveData)();
|
|
23
|
+
const allDevices = data.mobile.flatMap((group) => group.devices.map((dev) => ({ ...dev, os: group.os })));
|
|
24
|
+
desiredPlatformVersion = resolvePlatformVersion(allDevices, desiredPlatform, desiredPlatformVersion);
|
|
25
|
+
const filteredDevices = filterDevicesByPlatformAndVersion(allDevices, desiredPlatform, desiredPlatformVersion);
|
|
26
|
+
const matches = await (0, fuzzy_search_1.fuzzySearchDevices)(filteredDevices, desiredPhone);
|
|
27
|
+
const selectedDevice = validateAndSelectDevice(matches, desiredPhone, desiredPlatform, desiredPlatformVersion);
|
|
28
|
+
const { app_url } = await (0, upload_app_1.uploadApp)(appPath);
|
|
29
|
+
validateAppUrl(app_url);
|
|
30
|
+
const launchUrl = constructLaunchUrl(app_url, selectedDevice, desiredPlatform, desiredPlatformVersion);
|
|
31
|
+
openBrowser(launchUrl);
|
|
32
|
+
return launchUrl;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Resolves the platform version based on the desired platform and version.
|
|
36
|
+
* @param allDevices - The list of all devices.
|
|
37
|
+
* @param desiredPlatform - The desired platform (android or ios).
|
|
38
|
+
* @param desiredPlatformVersion - The desired platform version.
|
|
39
|
+
* @returns The resolved platform version.
|
|
40
|
+
* @throws Will throw an error if the platform version is not valid.
|
|
41
|
+
*/
|
|
42
|
+
function resolvePlatformVersion(allDevices, desiredPlatform, desiredPlatformVersion) {
|
|
43
|
+
if (desiredPlatformVersion === "latest" ||
|
|
44
|
+
desiredPlatformVersion === "oldest") {
|
|
45
|
+
const filtered = allDevices.filter((d) => d.os === desiredPlatform);
|
|
46
|
+
filtered.sort((a, b) => {
|
|
47
|
+
const versionA = parseFloat(a.os_version);
|
|
48
|
+
const versionB = parseFloat(b.os_version);
|
|
49
|
+
return desiredPlatformVersion === "latest"
|
|
50
|
+
? versionB - versionA
|
|
51
|
+
: versionA - versionB;
|
|
52
|
+
});
|
|
53
|
+
return filtered[0].os_version;
|
|
54
|
+
}
|
|
55
|
+
return desiredPlatformVersion;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Filters devices based on the desired platform and version.
|
|
59
|
+
* @param allDevices - The list of all devices.
|
|
60
|
+
* @param desiredPlatform - The desired platform (android or ios).
|
|
61
|
+
* @param desiredPlatformVersion - The desired platform version.
|
|
62
|
+
* @returns The filtered list of devices.
|
|
63
|
+
* @throws Will throw an error if the platform version is not valid.
|
|
64
|
+
*/
|
|
65
|
+
function filterDevicesByPlatformAndVersion(allDevices, desiredPlatform, desiredPlatformVersion) {
|
|
66
|
+
return allDevices.filter((d) => {
|
|
67
|
+
if (d.os !== desiredPlatform)
|
|
68
|
+
return false;
|
|
69
|
+
try {
|
|
70
|
+
const versionA = parseFloat(d.os_version);
|
|
71
|
+
const versionB = parseFloat(desiredPlatformVersion);
|
|
72
|
+
return versionA === versionB;
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return d.os_version === desiredPlatformVersion;
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Validates the selected device and handles multiple matches.
|
|
81
|
+
* @param matches - The list of device matches.
|
|
82
|
+
* @param desiredPhone - The desired phone name.
|
|
83
|
+
* @param desiredPlatform - The desired platform (android or ios).
|
|
84
|
+
* @param desiredPlatformVersion - The desired platform version.
|
|
85
|
+
* @returns The selected device entry.
|
|
86
|
+
*/
|
|
87
|
+
function validateAndSelectDevice(matches, desiredPhone, desiredPlatform, desiredPlatformVersion) {
|
|
88
|
+
if (matches.length === 0) {
|
|
89
|
+
throw new Error(`No devices found matching "${desiredPhone}" for ${desiredPlatform} ${desiredPlatformVersion}`);
|
|
90
|
+
}
|
|
91
|
+
const exactMatch = matches.find((d) => d.display_name.toLowerCase() === desiredPhone.toLowerCase());
|
|
92
|
+
if (exactMatch) {
|
|
93
|
+
return exactMatch;
|
|
94
|
+
}
|
|
95
|
+
else if (matches.length >= 1) {
|
|
96
|
+
const names = matches.map((d) => d.display_name).join(", ");
|
|
97
|
+
const error_message = matches.length === 1
|
|
98
|
+
? `Alternative device found: ${names}. Would you like to use it?`
|
|
99
|
+
: `Multiple devices found: ${names}. Please select one.`;
|
|
100
|
+
throw new Error(`${error_message}`);
|
|
101
|
+
}
|
|
102
|
+
return matches[0];
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Validates the app URL.
|
|
106
|
+
* @param appUrl - The app URL to validate.
|
|
107
|
+
* @throws Will throw an error if the app URL is not valid.
|
|
108
|
+
*/
|
|
109
|
+
function validateAppUrl(appUrl) {
|
|
110
|
+
if (!appUrl.match("bs://")) {
|
|
111
|
+
throw new Error("The app path is not a valid BrowserStack app URL.");
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Constructs the launch URL for the App Live session.
|
|
116
|
+
* @param appUrl - The app URL.
|
|
117
|
+
* @param device - The selected device entry.
|
|
118
|
+
* @param desiredPlatform - The desired platform (android or ios).
|
|
119
|
+
* @param desiredPlatformVersion - The desired platform version.
|
|
120
|
+
* @returns The constructed launch URL.
|
|
121
|
+
*/
|
|
122
|
+
function constructLaunchUrl(appUrl, device, desiredPlatform, desiredPlatformVersion) {
|
|
123
|
+
const deviceParam = (0, utils_1.sanitizeUrlParam)(device.display_name.replace(/\s+/g, "+"));
|
|
22
124
|
const params = new URLSearchParams({
|
|
23
|
-
os:
|
|
24
|
-
os_version:
|
|
25
|
-
app_hashed_id:
|
|
125
|
+
os: desiredPlatform,
|
|
126
|
+
os_version: desiredPlatformVersion,
|
|
127
|
+
app_hashed_id: appUrl.split("bs://").pop() || "",
|
|
26
128
|
scale_to_fit: "true",
|
|
27
129
|
speed: "1",
|
|
28
130
|
start: "true",
|
|
29
131
|
});
|
|
30
|
-
|
|
132
|
+
return `https://app-live.browserstack.com/dashboard#${params.toString()}&device=${deviceParam}`;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Opens the launch URL in the default browser.
|
|
136
|
+
* @param launchUrl - The URL to open.
|
|
137
|
+
* @throws Will throw an error if the browser fails to open.
|
|
138
|
+
*/
|
|
139
|
+
function openBrowser(launchUrl) {
|
|
31
140
|
try {
|
|
32
|
-
// Use platform-specific commands with proper escaping
|
|
33
141
|
const command = process.platform === "darwin"
|
|
34
142
|
? ["open", launchUrl]
|
|
35
143
|
: process.platform === "win32"
|
|
@@ -40,16 +148,12 @@ async function startSession(args) {
|
|
|
40
148
|
stdio: "ignore",
|
|
41
149
|
detached: true,
|
|
42
150
|
});
|
|
43
|
-
// Handle process errors
|
|
44
151
|
child.on("error", (error) => {
|
|
45
152
|
logger_1.default.error(`Failed to open browser automatically: ${error}. Please open this URL manually: ${launchUrl}`);
|
|
46
153
|
});
|
|
47
|
-
// Unref the child process to allow the parent to exit
|
|
48
154
|
child.unref();
|
|
49
|
-
return launchUrl;
|
|
50
155
|
}
|
|
51
156
|
catch (error) {
|
|
52
157
|
logger_1.default.error(`Failed to open browser automatically: ${error}. Please open this URL manually: ${launchUrl}`);
|
|
53
|
-
return launchUrl;
|
|
54
158
|
}
|
|
55
159
|
}
|
package/dist/tools/applive.js
CHANGED
|
@@ -7,7 +7,6 @@ exports.startAppLiveSession = startAppLiveSession;
|
|
|
7
7
|
exports.default = addAppLiveTools;
|
|
8
8
|
const zod_1 = require("zod");
|
|
9
9
|
const fs_1 = __importDefault(require("fs"));
|
|
10
|
-
const upload_app_1 = require("./applive-utils/upload-app");
|
|
11
10
|
const start_session_1 = require("./applive-utils/start-session");
|
|
12
11
|
const logger_1 = __importDefault(require("../logger"));
|
|
13
12
|
/**
|
|
@@ -40,12 +39,8 @@ async function startAppLiveSession(args) {
|
|
|
40
39
|
logger_1.default.error("The app path does not exist or is not readable: %s", error);
|
|
41
40
|
throw new Error("The app path does not exist or is not readable.");
|
|
42
41
|
}
|
|
43
|
-
const { app_url } = await (0, upload_app_1.uploadApp)(args.appPath);
|
|
44
|
-
if (!app_url.match("bs://")) {
|
|
45
|
-
throw new Error("The app path is not a valid BrowserStack app URL.");
|
|
46
|
-
}
|
|
47
42
|
const launchUrl = await (0, start_session_1.startSession)({
|
|
48
|
-
|
|
43
|
+
appPath: args.appPath,
|
|
49
44
|
desiredPlatform: args.desiredPlatform,
|
|
50
45
|
desiredPhone: args.desiredPhone,
|
|
51
46
|
desiredPlatformVersion: args.desiredPlatformVersion,
|
|
@@ -66,7 +61,7 @@ function addAppLiveTools(server) {
|
|
|
66
61
|
.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. "),
|
|
67
62
|
desiredPlatformVersion: zod_1.z
|
|
68
63
|
.string()
|
|
69
|
-
.describe("
|
|
64
|
+
.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'."),
|
|
70
65
|
desiredPlatform: zod_1.z
|
|
71
66
|
.enum(["android", "ios"])
|
|
72
67
|
.describe("Which platform to run on, examples: 'android', 'ios'. Set this based on the app path provided."),
|