@browserstack/mcp-server 1.2.4 → 1.2.5
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 +9 -5
- package/dist/lib/apiClient.d.ts +7 -5
- package/dist/lib/apiClient.js +76 -15
- package/dist/lib/device-cache.d.ts +3 -1
- package/dist/lib/device-cache.js +4 -0
- package/dist/lib/instrumentation.js +6 -3
- package/dist/lib/utils.d.ts +75 -2
- package/dist/lib/utils.js +20 -0
- package/dist/lib/version-resolver.js +26 -14
- package/dist/tools/appautomate-utils/appium-sdk/config-generator.d.ts +7 -1
- package/dist/tools/appautomate-utils/appium-sdk/config-generator.js +46 -26
- package/dist/tools/appautomate-utils/appium-sdk/constants.d.ts +1 -1
- package/dist/tools/appautomate-utils/appium-sdk/constants.js +24 -3
- package/dist/tools/appautomate-utils/appium-sdk/handler.js +16 -2
- package/dist/tools/appautomate-utils/appium-sdk/languages/java.d.ts +2 -0
- package/dist/tools/appautomate-utils/appium-sdk/languages/java.js +63 -29
- package/dist/tools/appautomate-utils/appium-sdk/types.d.ts +2 -1
- package/dist/tools/appautomate-utils/appium-sdk/types.js +10 -1
- package/dist/tools/appautomate-utils/native-execution/constants.d.ts +2 -1
- package/dist/tools/appautomate-utils/native-execution/constants.js +24 -2
- package/dist/tools/appautomate.js +15 -2
- package/dist/tools/percy-sdk.js +30 -3
- package/dist/tools/run-percy-scan.js +1 -1
- package/dist/tools/sdk-utils/bstack/configUtils.d.ts +7 -4
- package/dist/tools/sdk-utils/bstack/configUtils.js +67 -20
- package/dist/tools/sdk-utils/bstack/sdkHandler.d.ts +1 -1
- package/dist/tools/sdk-utils/bstack/sdkHandler.js +10 -2
- package/dist/tools/sdk-utils/common/constants.d.ts +5 -4
- package/dist/tools/sdk-utils/common/constants.js +7 -6
- package/dist/tools/sdk-utils/common/device-validator.d.ts +25 -0
- package/dist/tools/sdk-utils/common/device-validator.js +368 -0
- package/dist/tools/sdk-utils/common/schema.d.ts +24 -4
- package/dist/tools/sdk-utils/common/schema.js +57 -3
- package/dist/tools/sdk-utils/common/utils.js +2 -1
- package/dist/tools/sdk-utils/handler.d.ts +1 -0
- package/dist/tools/sdk-utils/handler.js +40 -12
- package/dist/tools/sdk-utils/percy-bstack/handler.js +5 -1
- package/dist/tools/sdk-utils/percy-web/handler.js +3 -1
- package/package.json +2 -2
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
import { getDevicesAndBrowsers, BrowserStackProducts, } from "../../../lib/device-cache.js";
|
|
2
|
+
import { resolveVersion } from "../../../lib/version-resolver.js";
|
|
3
|
+
import { customFuzzySearch } from "../../../lib/fuzzy.js";
|
|
4
|
+
import { SDKSupportedBrowserAutomationFrameworkEnum } from "./types.js";
|
|
5
|
+
const DEFAULTS = {
|
|
6
|
+
windows: { browser: "chrome" },
|
|
7
|
+
macos: { browser: "safari" },
|
|
8
|
+
android: { device: "Samsung Galaxy S24", browser: "chrome" },
|
|
9
|
+
ios: { device: "iPhone 15", browser: "safari" },
|
|
10
|
+
};
|
|
11
|
+
// ============================================================================
|
|
12
|
+
// AUTOMATE SECTION (Desktop + Mobile for BrowserStack SDK)
|
|
13
|
+
// ============================================================================
|
|
14
|
+
// Helper functions to build device entries and eliminate duplication
|
|
15
|
+
function buildDesktopEntries(automateData) {
|
|
16
|
+
if (!automateData.desktop) {
|
|
17
|
+
return [];
|
|
18
|
+
}
|
|
19
|
+
return automateData.desktop.flatMap((platform) => platform.browsers.map((browser) => ({
|
|
20
|
+
os: platform.os,
|
|
21
|
+
os_version: platform.os_version,
|
|
22
|
+
browser: browser.browser,
|
|
23
|
+
browser_version: browser.browser_version,
|
|
24
|
+
})));
|
|
25
|
+
}
|
|
26
|
+
function buildMobileEntries(appAutomateData, platform) {
|
|
27
|
+
if (!appAutomateData.mobile) {
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
return appAutomateData.mobile
|
|
31
|
+
.filter((group) => group.os === platform)
|
|
32
|
+
.flatMap((group) => group.devices.map((device) => ({
|
|
33
|
+
os: group.os,
|
|
34
|
+
os_version: device.os_version,
|
|
35
|
+
display_name: device.display_name,
|
|
36
|
+
browsers: device.browsers || [
|
|
37
|
+
{
|
|
38
|
+
browser: device.browser || (platform === "android" ? "chrome" : "safari"),
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
})));
|
|
42
|
+
}
|
|
43
|
+
// Performance optimization: Create indexed maps for faster lookups
|
|
44
|
+
function createDesktopIndex(entries) {
|
|
45
|
+
const byOS = new Map();
|
|
46
|
+
const byOSVersion = new Map();
|
|
47
|
+
const byBrowser = new Map();
|
|
48
|
+
const nested = new Map();
|
|
49
|
+
for (const entry of entries) {
|
|
50
|
+
// Index by OS
|
|
51
|
+
if (!byOS.has(entry.os)) {
|
|
52
|
+
byOS.set(entry.os, []);
|
|
53
|
+
}
|
|
54
|
+
byOS.get(entry.os).push(entry);
|
|
55
|
+
// Index by OS version
|
|
56
|
+
if (!byOSVersion.has(entry.os_version)) {
|
|
57
|
+
byOSVersion.set(entry.os_version, []);
|
|
58
|
+
}
|
|
59
|
+
byOSVersion.get(entry.os_version).push(entry);
|
|
60
|
+
// Index by browser
|
|
61
|
+
if (!byBrowser.has(entry.browser)) {
|
|
62
|
+
byBrowser.set(entry.browser, []);
|
|
63
|
+
}
|
|
64
|
+
byBrowser.get(entry.browser).push(entry);
|
|
65
|
+
// Build nested index: Map<os, Map<os_version, Map<browser, DesktopBrowserEntry[]>>>
|
|
66
|
+
if (!nested.has(entry.os)) {
|
|
67
|
+
nested.set(entry.os, new Map());
|
|
68
|
+
}
|
|
69
|
+
const osMap = nested.get(entry.os);
|
|
70
|
+
if (!osMap.has(entry.os_version)) {
|
|
71
|
+
osMap.set(entry.os_version, new Map());
|
|
72
|
+
}
|
|
73
|
+
const osVersionMap = osMap.get(entry.os_version);
|
|
74
|
+
if (!osVersionMap.has(entry.browser)) {
|
|
75
|
+
osVersionMap.set(entry.browser, []);
|
|
76
|
+
}
|
|
77
|
+
osVersionMap.get(entry.browser).push(entry);
|
|
78
|
+
}
|
|
79
|
+
return { byOS, byOSVersion, byBrowser, nested };
|
|
80
|
+
}
|
|
81
|
+
function createMobileIndex(entries) {
|
|
82
|
+
const byPlatform = new Map();
|
|
83
|
+
const byDeviceName = new Map();
|
|
84
|
+
const byOSVersion = new Map();
|
|
85
|
+
for (const entry of entries) {
|
|
86
|
+
// Index by platform
|
|
87
|
+
if (!byPlatform.has(entry.os)) {
|
|
88
|
+
byPlatform.set(entry.os, []);
|
|
89
|
+
}
|
|
90
|
+
byPlatform.get(entry.os).push(entry);
|
|
91
|
+
// Index by device name (case-insensitive)
|
|
92
|
+
const deviceKey = entry.display_name.toLowerCase();
|
|
93
|
+
if (!byDeviceName.has(deviceKey)) {
|
|
94
|
+
byDeviceName.set(deviceKey, []);
|
|
95
|
+
}
|
|
96
|
+
byDeviceName.get(deviceKey).push(entry);
|
|
97
|
+
// Index by OS version
|
|
98
|
+
if (!byOSVersion.has(entry.os_version)) {
|
|
99
|
+
byOSVersion.set(entry.os_version, []);
|
|
100
|
+
}
|
|
101
|
+
byOSVersion.get(entry.os_version).push(entry);
|
|
102
|
+
}
|
|
103
|
+
return { byPlatform, byDeviceName, byOSVersion };
|
|
104
|
+
}
|
|
105
|
+
export async function validateDevices(devices, framework) {
|
|
106
|
+
const validatedEnvironments = [];
|
|
107
|
+
if (!devices || devices.length === 0) {
|
|
108
|
+
// Use centralized default fallback
|
|
109
|
+
return [
|
|
110
|
+
{
|
|
111
|
+
platform: "windows",
|
|
112
|
+
osVersion: "11",
|
|
113
|
+
browser: DEFAULTS.windows.browser,
|
|
114
|
+
browserVersion: "latest",
|
|
115
|
+
},
|
|
116
|
+
];
|
|
117
|
+
}
|
|
118
|
+
// Determine what data we need to fetch
|
|
119
|
+
const needsDesktop = devices.some((env) => ["windows", "macos"].includes((env[0] || "").toLowerCase()));
|
|
120
|
+
const needsMobile = devices.some((env) => ["android", "ios"].includes((env[0] || "").toLowerCase()));
|
|
121
|
+
// Fetch data using framework-specific endpoint for both desktop and mobile
|
|
122
|
+
let deviceData = null;
|
|
123
|
+
try {
|
|
124
|
+
if (needsDesktop || needsMobile) {
|
|
125
|
+
if (framework === SDKSupportedBrowserAutomationFrameworkEnum.playwright) {
|
|
126
|
+
deviceData = (await getDevicesAndBrowsers(BrowserStackProducts.PLAYWRIGHT_AUTOMATE));
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
deviceData = (await getDevicesAndBrowsers(BrowserStackProducts.SELENIUM_AUTOMATE));
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
throw new Error(`Failed to fetch device data: ${error instanceof Error ? error.message : String(error)}`);
|
|
135
|
+
}
|
|
136
|
+
// Preprocess data into indexed maps for better performance
|
|
137
|
+
let desktopIndex = null;
|
|
138
|
+
let androidIndex = null;
|
|
139
|
+
let iosIndex = null;
|
|
140
|
+
if (needsDesktop && deviceData) {
|
|
141
|
+
const desktopEntries = buildDesktopEntries(deviceData);
|
|
142
|
+
desktopIndex = createDesktopIndex(desktopEntries);
|
|
143
|
+
}
|
|
144
|
+
if (needsMobile && deviceData) {
|
|
145
|
+
const androidEntries = buildMobileEntries(deviceData, "android");
|
|
146
|
+
const iosEntries = buildMobileEntries(deviceData, "ios");
|
|
147
|
+
androidIndex = createMobileIndex(androidEntries);
|
|
148
|
+
iosIndex = createMobileIndex(iosEntries);
|
|
149
|
+
}
|
|
150
|
+
for (const env of devices) {
|
|
151
|
+
const discriminator = (env[0] || "").toLowerCase();
|
|
152
|
+
let validatedEnv;
|
|
153
|
+
if (discriminator === "windows") {
|
|
154
|
+
validatedEnv = validateDesktopEnvironment(env, desktopIndex, "windows", DEFAULTS.windows.browser);
|
|
155
|
+
}
|
|
156
|
+
else if (discriminator === "macos") {
|
|
157
|
+
validatedEnv = validateDesktopEnvironment(env, desktopIndex, "macos", DEFAULTS.macos.browser);
|
|
158
|
+
}
|
|
159
|
+
else if (discriminator === "android") {
|
|
160
|
+
validatedEnv = validateMobileEnvironment(env, androidIndex, "android", DEFAULTS.android.device, DEFAULTS.android.browser);
|
|
161
|
+
}
|
|
162
|
+
else if (discriminator === "ios") {
|
|
163
|
+
validatedEnv = validateMobileEnvironment(env, iosIndex, "ios", DEFAULTS.ios.device, DEFAULTS.ios.browser);
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
throw new Error(`Unsupported platform: ${discriminator}`);
|
|
167
|
+
}
|
|
168
|
+
validatedEnvironments.push(validatedEnv);
|
|
169
|
+
}
|
|
170
|
+
return validatedEnvironments;
|
|
171
|
+
}
|
|
172
|
+
// Optimized desktop validation using nested indexed maps for O(1) lookups
|
|
173
|
+
function validateDesktopEnvironment(env, index, platform, defaultBrowser) {
|
|
174
|
+
const [, osVersion, browser, browserVersion] = env;
|
|
175
|
+
const osKey = platform === "windows" ? "Windows" : "OS X";
|
|
176
|
+
// Use nested index for O(1) lookup instead of filtering
|
|
177
|
+
const osMap = index.nested.get(osKey);
|
|
178
|
+
if (!osMap) {
|
|
179
|
+
throw new Error(`No ${platform} devices available`);
|
|
180
|
+
}
|
|
181
|
+
// Get available OS versions for this platform
|
|
182
|
+
const availableOSVersions = Array.from(osMap.keys());
|
|
183
|
+
const validatedOSVersion = resolveVersion(osVersion || "latest", availableOSVersions);
|
|
184
|
+
// Use nested index for O(1) lookup
|
|
185
|
+
const osVersionMap = osMap.get(validatedOSVersion);
|
|
186
|
+
if (!osVersionMap) {
|
|
187
|
+
throw new Error(`OS version "${validatedOSVersion}" not available for ${platform}`);
|
|
188
|
+
}
|
|
189
|
+
// Get available browsers for this OS version
|
|
190
|
+
const availableBrowsers = Array.from(osVersionMap.keys());
|
|
191
|
+
const validatedBrowser = validateBrowserExact(browser || defaultBrowser, availableBrowsers);
|
|
192
|
+
// Use nested index for O(1) lookup
|
|
193
|
+
const browserEntries = osVersionMap.get(validatedBrowser);
|
|
194
|
+
if (!browserEntries || browserEntries.length === 0) {
|
|
195
|
+
throw new Error(`Browser "${validatedBrowser}" not available for ${platform} ${validatedOSVersion}`);
|
|
196
|
+
}
|
|
197
|
+
const availableBrowserVersions = [
|
|
198
|
+
...new Set(browserEntries.map((e) => e.browser_version)),
|
|
199
|
+
];
|
|
200
|
+
const validatedBrowserVersion = resolveVersion(browserVersion || "latest", availableBrowserVersions);
|
|
201
|
+
return {
|
|
202
|
+
platform,
|
|
203
|
+
osVersion: validatedOSVersion,
|
|
204
|
+
browser: validatedBrowser,
|
|
205
|
+
browserVersion: validatedBrowserVersion,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
// Optimized mobile validation using indexed maps
|
|
209
|
+
function validateMobileEnvironment(env, index, platform, defaultDevice, defaultBrowser) {
|
|
210
|
+
const [, deviceName, osVersion, browser] = env;
|
|
211
|
+
const platformEntries = index.byPlatform.get(platform) || [];
|
|
212
|
+
if (platformEntries.length === 0) {
|
|
213
|
+
throw new Error(`No ${platform} devices available`);
|
|
214
|
+
}
|
|
215
|
+
// Use fuzzy search only for device names (as suggested in feedback)
|
|
216
|
+
const deviceMatches = customFuzzySearch(platformEntries, ["display_name"], deviceName || defaultDevice, 5);
|
|
217
|
+
if (deviceMatches.length === 0) {
|
|
218
|
+
throw new Error(`No ${platform} devices matching "${deviceName}". Available devices: ${platformEntries
|
|
219
|
+
.map((d) => d.display_name || "unknown")
|
|
220
|
+
.slice(0, 5)
|
|
221
|
+
.join(", ")}`);
|
|
222
|
+
}
|
|
223
|
+
// Try to find exact match first
|
|
224
|
+
const exactMatch = deviceMatches.find((m) => m.display_name.toLowerCase() === (deviceName || "").toLowerCase());
|
|
225
|
+
// If no exact match, throw error instead of using fuzzy match
|
|
226
|
+
if (!exactMatch) {
|
|
227
|
+
const suggestions = deviceMatches.map((m) => m.display_name).join(", ");
|
|
228
|
+
throw new Error(`Device "${deviceName}" not found exactly for ${platform}. Available similar devices: ${suggestions}. Please use the exact device name.`);
|
|
229
|
+
}
|
|
230
|
+
// Use index for faster filtering
|
|
231
|
+
const deviceKey = exactMatch.display_name.toLowerCase();
|
|
232
|
+
const deviceFiltered = index.byDeviceName.get(deviceKey) || [];
|
|
233
|
+
const availableOSVersions = [
|
|
234
|
+
...new Set(deviceFiltered.map((d) => d.os_version)),
|
|
235
|
+
];
|
|
236
|
+
const validatedOSVersion = resolveVersion(osVersion || "latest", availableOSVersions);
|
|
237
|
+
// Use index for faster filtering
|
|
238
|
+
const osVersionEntries = index.byOSVersion.get(validatedOSVersion) || [];
|
|
239
|
+
const osFiltered = osVersionEntries.filter((d) => d.display_name.toLowerCase() === deviceKey);
|
|
240
|
+
// Validate browser if provided - use exact matching for browsers
|
|
241
|
+
let validatedBrowser = browser || defaultBrowser;
|
|
242
|
+
if (browser && osFiltered.length > 0) {
|
|
243
|
+
// Extract browsers more carefully - handle different possible structures
|
|
244
|
+
const availableBrowsers = [
|
|
245
|
+
...new Set(osFiltered.flatMap((d) => {
|
|
246
|
+
if (d.browsers && Array.isArray(d.browsers)) {
|
|
247
|
+
// If browsers is an array of objects with browser property
|
|
248
|
+
return d.browsers
|
|
249
|
+
.map((b) => {
|
|
250
|
+
// Use display_name for user-friendly browser names, fallback to browser field
|
|
251
|
+
return b.display_name || b.browser;
|
|
252
|
+
})
|
|
253
|
+
.filter(Boolean);
|
|
254
|
+
}
|
|
255
|
+
// For mobile devices, provide default browsers if none found
|
|
256
|
+
return platform === "android" ? ["chrome"] : ["safari"];
|
|
257
|
+
})),
|
|
258
|
+
].filter(Boolean);
|
|
259
|
+
if (availableBrowsers.length > 0) {
|
|
260
|
+
try {
|
|
261
|
+
validatedBrowser = validateBrowserExact(browser, availableBrowsers);
|
|
262
|
+
}
|
|
263
|
+
catch (error) {
|
|
264
|
+
// Add more context to browser validation errors
|
|
265
|
+
throw new Error(`Failed to validate browser "${browser}" for ${platform} device "${exactMatch.display_name}" on OS version "${validatedOSVersion}". ${error instanceof Error ? error.message : String(error)}`);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
// For mobile, if no specific browsers found, just use the requested browser
|
|
270
|
+
// as most mobile devices support standard browsers
|
|
271
|
+
validatedBrowser = browser || defaultBrowser;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return {
|
|
275
|
+
platform,
|
|
276
|
+
osVersion: validatedOSVersion,
|
|
277
|
+
deviceName: exactMatch.display_name,
|
|
278
|
+
browser: validatedBrowser,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
// ============================================================================
|
|
282
|
+
// APP AUTOMATE SECTION (Mobile devices for App Automate)
|
|
283
|
+
// ============================================================================
|
|
284
|
+
export async function validateAppAutomateDevices(devices) {
|
|
285
|
+
const validatedDevices = [];
|
|
286
|
+
if (!devices || devices.length === 0) {
|
|
287
|
+
// Use centralized default fallback
|
|
288
|
+
return [
|
|
289
|
+
{
|
|
290
|
+
platform: "android",
|
|
291
|
+
osVersion: "latest",
|
|
292
|
+
deviceName: DEFAULTS.android.device,
|
|
293
|
+
},
|
|
294
|
+
];
|
|
295
|
+
}
|
|
296
|
+
let appAutomateData;
|
|
297
|
+
try {
|
|
298
|
+
// Fetch app automate device data
|
|
299
|
+
appAutomateData = (await getDevicesAndBrowsers(BrowserStackProducts.APP_AUTOMATE));
|
|
300
|
+
}
|
|
301
|
+
catch (error) {
|
|
302
|
+
// Only wrap fetch-related errors
|
|
303
|
+
throw new Error(`Failed to fetch device data: ${error instanceof Error ? error.message : String(error)}`);
|
|
304
|
+
}
|
|
305
|
+
for (const device of devices) {
|
|
306
|
+
// Parse device array in format ["android", "Device Name", "OS Version"]
|
|
307
|
+
const [platform, deviceName, osVersion] = device;
|
|
308
|
+
// Find matching device in the data
|
|
309
|
+
let validatedDevice = null;
|
|
310
|
+
if (!appAutomateData.mobile) {
|
|
311
|
+
throw new Error("No mobile device data available");
|
|
312
|
+
}
|
|
313
|
+
// Filter by platform first
|
|
314
|
+
const platformGroup = appAutomateData.mobile.find((group) => group.os === platform.toLowerCase());
|
|
315
|
+
if (!platformGroup) {
|
|
316
|
+
throw new Error(`Platform "${platform}" not supported for App Automate`);
|
|
317
|
+
}
|
|
318
|
+
const platformDevices = platformGroup.devices;
|
|
319
|
+
// Find exact device name match (case-insensitive)
|
|
320
|
+
const exactMatch = platformDevices.find((d) => d.display_name.toLowerCase() === deviceName.toLowerCase());
|
|
321
|
+
if (exactMatch) {
|
|
322
|
+
// Check if the OS version is available for this device
|
|
323
|
+
const deviceVersions = platformDevices
|
|
324
|
+
.filter((d) => d.display_name === exactMatch.display_name)
|
|
325
|
+
.map((d) => d.os_version);
|
|
326
|
+
const validatedOSVersion = resolveVersion(osVersion || "latest", deviceVersions);
|
|
327
|
+
validatedDevice = {
|
|
328
|
+
platform: platformGroup.os,
|
|
329
|
+
osVersion: validatedOSVersion,
|
|
330
|
+
deviceName: exactMatch.display_name,
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
if (!validatedDevice) {
|
|
334
|
+
// If no exact match found, suggest similar devices from the SAME platform only
|
|
335
|
+
const platformDevicesForSearch = platformDevices.map((d) => ({
|
|
336
|
+
...d,
|
|
337
|
+
platform: platformGroup.os,
|
|
338
|
+
}));
|
|
339
|
+
// Try fuzzy search with a more lenient threshold
|
|
340
|
+
const deviceMatches = customFuzzySearch(platformDevicesForSearch, ["display_name"], deviceName, 5, 0.8);
|
|
341
|
+
const suggestions = deviceMatches
|
|
342
|
+
.map((m) => `${m.display_name}`)
|
|
343
|
+
.join(", ");
|
|
344
|
+
// If no fuzzy matches, show some available devices as fallback
|
|
345
|
+
const fallbackDevices = platformDevicesForSearch
|
|
346
|
+
.slice(0, 5)
|
|
347
|
+
.map((d) => d.display_name)
|
|
348
|
+
.join(", ");
|
|
349
|
+
const errorMessage = suggestions
|
|
350
|
+
? `Device "${deviceName}" not found for platform "${platform}".\nAvailable similar devices: ${suggestions}`
|
|
351
|
+
: `Device "${deviceName}" not found for platform "${platform}".\nAvailable devices: ${fallbackDevices}`;
|
|
352
|
+
throw new Error(errorMessage);
|
|
353
|
+
}
|
|
354
|
+
validatedDevices.push(validatedDevice);
|
|
355
|
+
}
|
|
356
|
+
return validatedDevices;
|
|
357
|
+
}
|
|
358
|
+
// ============================================================================
|
|
359
|
+
// SHARED UTILITY FUNCTIONS
|
|
360
|
+
// ============================================================================
|
|
361
|
+
// Exact browser validation (preferred for structured fields)
|
|
362
|
+
function validateBrowserExact(requestedBrowser, availableBrowsers) {
|
|
363
|
+
const exactMatch = availableBrowsers.find((b) => b.toLowerCase() === requestedBrowser.toLowerCase());
|
|
364
|
+
if (exactMatch) {
|
|
365
|
+
return exactMatch;
|
|
366
|
+
}
|
|
367
|
+
throw new Error(`Browser "${requestedBrowser}" not found. Available options: ${availableBrowsers.join(", ")}`);
|
|
368
|
+
}
|
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { PercyIntegrationTypeEnum } from "./types.js";
|
|
3
3
|
import { SDKSupportedBrowserAutomationFrameworkEnum, SDKSupportedTestingFrameworkEnum, SDKSupportedLanguageEnum } from "./types.js";
|
|
4
|
+
export declare const PlatformEnum: {
|
|
5
|
+
readonly WINDOWS: "windows";
|
|
6
|
+
readonly MACOS: "macos";
|
|
7
|
+
readonly ANDROID: "android";
|
|
8
|
+
readonly IOS: "ios";
|
|
9
|
+
};
|
|
10
|
+
export declare const WindowsPlatformEnum: {
|
|
11
|
+
readonly WINDOWS: "windows";
|
|
12
|
+
};
|
|
13
|
+
export declare const MacOSPlatformEnum: {
|
|
14
|
+
readonly MACOS: "macos";
|
|
15
|
+
};
|
|
4
16
|
export declare const SetUpPercyParamsShape: {
|
|
5
17
|
projectName: z.ZodString;
|
|
6
18
|
detectedLanguage: z.ZodNativeEnum<typeof SDKSupportedLanguageEnum>;
|
|
@@ -14,7 +26,11 @@ export declare const RunTestsOnBrowserStackParamsShape: {
|
|
|
14
26
|
detectedLanguage: z.ZodNativeEnum<typeof SDKSupportedLanguageEnum>;
|
|
15
27
|
detectedBrowserAutomationFramework: z.ZodNativeEnum<typeof SDKSupportedBrowserAutomationFrameworkEnum>;
|
|
16
28
|
detectedTestingFramework: z.ZodNativeEnum<typeof SDKSupportedTestingFrameworkEnum>;
|
|
17
|
-
|
|
29
|
+
devices: z.ZodDefault<z.ZodArray<z.ZodUnion<[z.ZodTuple<[z.ZodNativeEnum<{
|
|
30
|
+
readonly WINDOWS: "windows";
|
|
31
|
+
}>, z.ZodString, z.ZodString, z.ZodString], null>, z.ZodTuple<[z.ZodLiteral<"android">, z.ZodString, z.ZodString, z.ZodString], null>, z.ZodTuple<[z.ZodLiteral<"ios">, z.ZodString, z.ZodString, z.ZodString], null>, z.ZodTuple<[z.ZodNativeEnum<{
|
|
32
|
+
readonly MACOS: "macos";
|
|
33
|
+
}>, z.ZodString, z.ZodString, z.ZodString], null>]>, "many">>;
|
|
18
34
|
};
|
|
19
35
|
export declare const SetUpPercySchema: z.ZodObject<{
|
|
20
36
|
projectName: z.ZodString;
|
|
@@ -43,19 +59,23 @@ export declare const RunTestsOnBrowserStackSchema: z.ZodObject<{
|
|
|
43
59
|
detectedLanguage: z.ZodNativeEnum<typeof SDKSupportedLanguageEnum>;
|
|
44
60
|
detectedBrowserAutomationFramework: z.ZodNativeEnum<typeof SDKSupportedBrowserAutomationFrameworkEnum>;
|
|
45
61
|
detectedTestingFramework: z.ZodNativeEnum<typeof SDKSupportedTestingFrameworkEnum>;
|
|
46
|
-
|
|
62
|
+
devices: z.ZodDefault<z.ZodArray<z.ZodUnion<[z.ZodTuple<[z.ZodNativeEnum<{
|
|
63
|
+
readonly WINDOWS: "windows";
|
|
64
|
+
}>, z.ZodString, z.ZodString, z.ZodString], null>, z.ZodTuple<[z.ZodLiteral<"android">, z.ZodString, z.ZodString, z.ZodString], null>, z.ZodTuple<[z.ZodLiteral<"ios">, z.ZodString, z.ZodString, z.ZodString], null>, z.ZodTuple<[z.ZodNativeEnum<{
|
|
65
|
+
readonly MACOS: "macos";
|
|
66
|
+
}>, z.ZodString, z.ZodString, z.ZodString], null>]>, "many">>;
|
|
47
67
|
}, "strip", z.ZodTypeAny, {
|
|
48
68
|
projectName: string;
|
|
49
69
|
detectedLanguage: SDKSupportedLanguageEnum;
|
|
50
70
|
detectedBrowserAutomationFramework: SDKSupportedBrowserAutomationFrameworkEnum;
|
|
51
71
|
detectedTestingFramework: SDKSupportedTestingFrameworkEnum;
|
|
52
|
-
|
|
72
|
+
devices: (["windows", string, string, string] | ["android", string, string, string] | ["ios", string, string, string] | ["macos", string, string, string])[];
|
|
53
73
|
}, {
|
|
54
74
|
projectName: string;
|
|
55
75
|
detectedLanguage: SDKSupportedLanguageEnum;
|
|
56
76
|
detectedBrowserAutomationFramework: SDKSupportedBrowserAutomationFrameworkEnum;
|
|
57
77
|
detectedTestingFramework: SDKSupportedTestingFrameworkEnum;
|
|
58
|
-
|
|
78
|
+
devices?: (["windows", string, string, string] | ["android", string, string, string] | ["ios", string, string, string] | ["macos", string, string, string])[] | undefined;
|
|
59
79
|
}>;
|
|
60
80
|
export type SetUpPercyInput = z.infer<typeof SetUpPercySchema>;
|
|
61
81
|
export type RunTestsOnBrowserStackInput = z.infer<typeof RunTestsOnBrowserStackSchema>;
|
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { PercyIntegrationTypeEnum } from "./types.js";
|
|
3
3
|
import { SDKSupportedBrowserAutomationFrameworkEnum, SDKSupportedTestingFrameworkEnum, SDKSupportedLanguageEnum, } from "./types.js";
|
|
4
|
+
// Platform enums for better validation
|
|
5
|
+
export const PlatformEnum = {
|
|
6
|
+
WINDOWS: "windows",
|
|
7
|
+
MACOS: "macos",
|
|
8
|
+
ANDROID: "android",
|
|
9
|
+
IOS: "ios",
|
|
10
|
+
};
|
|
11
|
+
export const WindowsPlatformEnum = {
|
|
12
|
+
WINDOWS: "windows",
|
|
13
|
+
};
|
|
14
|
+
export const MacOSPlatformEnum = {
|
|
15
|
+
MACOS: "macos",
|
|
16
|
+
};
|
|
4
17
|
export const SetUpPercyParamsShape = {
|
|
5
18
|
projectName: z.string().describe("A unique name for your Percy project."),
|
|
6
19
|
detectedLanguage: z.nativeEnum(SDKSupportedLanguageEnum),
|
|
@@ -20,9 +33,50 @@ export const RunTestsOnBrowserStackParamsShape = {
|
|
|
20
33
|
detectedLanguage: z.nativeEnum(SDKSupportedLanguageEnum),
|
|
21
34
|
detectedBrowserAutomationFramework: z.nativeEnum(SDKSupportedBrowserAutomationFrameworkEnum),
|
|
22
35
|
detectedTestingFramework: z.nativeEnum(SDKSupportedTestingFrameworkEnum),
|
|
23
|
-
|
|
24
|
-
.array(z.
|
|
25
|
-
|
|
36
|
+
devices: z
|
|
37
|
+
.array(z.union([
|
|
38
|
+
// Windows: [windows, osVersion, browser, browserVersion]
|
|
39
|
+
z.tuple([
|
|
40
|
+
z
|
|
41
|
+
.nativeEnum(WindowsPlatformEnum)
|
|
42
|
+
.describe("Platform identifier: 'windows'"),
|
|
43
|
+
z.string().describe("Windows version, e.g. '10', '11'"),
|
|
44
|
+
z.string().describe("Browser name, e.g. 'chrome', 'firefox', 'edge'"),
|
|
45
|
+
z
|
|
46
|
+
.string()
|
|
47
|
+
.describe("Browser version, e.g. '132', 'latest', 'oldest'"),
|
|
48
|
+
]),
|
|
49
|
+
// Android: [android, name, model, osVersion, browser]
|
|
50
|
+
z.tuple([
|
|
51
|
+
z
|
|
52
|
+
.literal(PlatformEnum.ANDROID)
|
|
53
|
+
.describe("Platform identifier: 'android'"),
|
|
54
|
+
z
|
|
55
|
+
.string()
|
|
56
|
+
.describe("Device name, e.g. 'Samsung Galaxy S24', 'Google Pixel 8'"),
|
|
57
|
+
z.string().describe("Android version, e.g. '14', '16', 'latest'"),
|
|
58
|
+
z.string().describe("Browser name, e.g. 'chrome', 'samsung browser'"),
|
|
59
|
+
]),
|
|
60
|
+
// iOS: [ios, name, model, osVersion, browser]
|
|
61
|
+
z.tuple([
|
|
62
|
+
z.literal(PlatformEnum.IOS).describe("Platform identifier: 'ios'"),
|
|
63
|
+
z.string().describe("Device name, e.g. 'iPhone 12 Pro'"),
|
|
64
|
+
z.string().describe("iOS version, e.g. '17', 'latest'"),
|
|
65
|
+
z.string().describe("Browser name, typically 'safari'"),
|
|
66
|
+
]),
|
|
67
|
+
// macOS: [mac|macos, name, model, browser, browserVersion]
|
|
68
|
+
z.tuple([
|
|
69
|
+
z
|
|
70
|
+
.nativeEnum(MacOSPlatformEnum)
|
|
71
|
+
.describe("Platform identifier: 'mac' or 'macos'"),
|
|
72
|
+
z.string().describe("macOS version name, e.g. 'Sequoia', 'Ventura'"),
|
|
73
|
+
z.string().describe("Browser name, e.g. 'safari', 'chrome'"),
|
|
74
|
+
z.string().describe("Browser version, e.g. 'latest'"),
|
|
75
|
+
]),
|
|
76
|
+
]))
|
|
77
|
+
.max(3)
|
|
78
|
+
.default([])
|
|
79
|
+
.describe("Preferred tuples of target devices.Add device only when user asks explicitly for it. Defaults to [] . Example: [['windows', '11', 'chrome', 'latest']]"),
|
|
26
80
|
};
|
|
27
81
|
export const SetUpPercySchema = z.object(SetUpPercyParamsShape);
|
|
28
82
|
export const RunTestsOnBrowserStackSchema = z.object(RunTestsOnBrowserStackParamsShape);
|
|
@@ -69,8 +69,9 @@ export function getPercyAutomateNotImplementedMessage(type, input, supported) {
|
|
|
69
69
|
}
|
|
70
70
|
}
|
|
71
71
|
export function getBootstrapFailedMessage(error, context) {
|
|
72
|
+
const error_message = error instanceof Error ? error.message : "unknown error";
|
|
72
73
|
return `Failed to bootstrap project with BrowserStack SDK.
|
|
73
|
-
Error: ${
|
|
74
|
+
Error: ${error_message}
|
|
74
75
|
Percy Mode: ${context.percyMode ?? "automate"}
|
|
75
76
|
SDK Version: ${context.sdkVersion ?? "N/A"}
|
|
76
77
|
Please open an issue on GitHub if the problem persists.`;
|
|
@@ -2,3 +2,4 @@ import { BrowserStackConfig } from "../../lib/types.js";
|
|
|
2
2
|
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
3
3
|
export declare function runTestsOnBrowserStackHandler(rawInput: unknown, config: BrowserStackConfig): Promise<CallToolResult>;
|
|
4
4
|
export declare function setUpPercyHandler(rawInput: unknown, config: BrowserStackConfig): Promise<CallToolResult>;
|
|
5
|
+
export declare function simulatePercyChangeHandler(rawInput: unknown, config: BrowserStackConfig): Promise<CallToolResult>;
|
|
@@ -9,16 +9,11 @@ import { runPercyWithBrowserstackSDK } from "./percy-bstack/handler.js";
|
|
|
9
9
|
import { checkPercyIntegrationSupport } from "./common/utils.js";
|
|
10
10
|
import { SetUpPercySchema, RunTestsOnBrowserStackSchema, } from "./common/schema.js";
|
|
11
11
|
import { getBootstrapFailedMessage, percyUnsupportedResult, } from "./common/utils.js";
|
|
12
|
+
import { PERCY_SIMULATE_INSTRUCTION, PERCY_REPLACE_REGEX, PERCY_SIMULATION_DRIVER_INSTRUCTION, PERCY_VERIFICATION_REGEX, } from "./common/constants.js";
|
|
12
13
|
export async function runTestsOnBrowserStackHandler(rawInput, config) {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const result = runBstackSDKOnly(input, config);
|
|
17
|
-
return await formatToolResult(result);
|
|
18
|
-
}
|
|
19
|
-
catch (error) {
|
|
20
|
-
throw new Error(getBootstrapFailedMessage(error, { config }));
|
|
21
|
-
}
|
|
14
|
+
const input = RunTestsOnBrowserStackSchema.parse(rawInput);
|
|
15
|
+
const result = await runBstackSDKOnly(input, config);
|
|
16
|
+
return await formatToolResult(result);
|
|
22
17
|
}
|
|
23
18
|
export async function setUpPercyHandler(rawInput, config) {
|
|
24
19
|
try {
|
|
@@ -47,7 +42,7 @@ export async function setUpPercyHandler(rawInput, config) {
|
|
|
47
42
|
// First try Percy with BrowserStack SDK
|
|
48
43
|
const percyWithBrowserstackSDKResult = runPercyWithBrowserstackSDK({
|
|
49
44
|
...percyInput,
|
|
50
|
-
|
|
45
|
+
devices: [],
|
|
51
46
|
}, config);
|
|
52
47
|
const hasPercySDKError = percyWithBrowserstackSDKResult.steps &&
|
|
53
48
|
percyWithBrowserstackSDKResult.steps.some((step) => step.isError);
|
|
@@ -77,9 +72,9 @@ export async function setUpPercyHandler(rawInput, config) {
|
|
|
77
72
|
detectedLanguage: input.detectedLanguage,
|
|
78
73
|
detectedBrowserAutomationFramework: input.detectedBrowserAutomationFramework,
|
|
79
74
|
detectedTestingFramework: input.detectedTestingFramework,
|
|
80
|
-
|
|
75
|
+
devices: [],
|
|
81
76
|
};
|
|
82
|
-
const sdkResult = runBstackSDKOnly(sdkInput, config, true);
|
|
77
|
+
const sdkResult = await runBstackSDKOnly(sdkInput, config, true);
|
|
83
78
|
// Percy Automate instructions
|
|
84
79
|
const percyToken = await fetchPercyToken(input.projectName, authorization, { type: PercyIntegrationTypeEnum.AUTOMATE });
|
|
85
80
|
const percyAutomateResult = runPercyAutomateOnly(percyInput, percyToken);
|
|
@@ -117,3 +112,36 @@ export async function setUpPercyHandler(rawInput, config) {
|
|
|
117
112
|
throw new Error(getBootstrapFailedMessage(error, { config }));
|
|
118
113
|
}
|
|
119
114
|
}
|
|
115
|
+
export async function simulatePercyChangeHandler(rawInput, config) {
|
|
116
|
+
try {
|
|
117
|
+
let percyInstruction;
|
|
118
|
+
try {
|
|
119
|
+
percyInstruction = await setUpPercyHandler(rawInput, config);
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
throw new Error("Failed to set up Percy");
|
|
123
|
+
}
|
|
124
|
+
if (percyInstruction.isError) {
|
|
125
|
+
return percyInstruction;
|
|
126
|
+
}
|
|
127
|
+
if (Array.isArray(percyInstruction.content)) {
|
|
128
|
+
percyInstruction.content = percyInstruction.content.map((item) => {
|
|
129
|
+
if (typeof item.text === "string") {
|
|
130
|
+
const updatedText = item.text
|
|
131
|
+
.replace(PERCY_REPLACE_REGEX, PERCY_SIMULATE_INSTRUCTION)
|
|
132
|
+
.replace(PERCY_VERIFICATION_REGEX, "");
|
|
133
|
+
return { ...item, text: updatedText };
|
|
134
|
+
}
|
|
135
|
+
return item;
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
percyInstruction.content?.push({
|
|
139
|
+
type: "text",
|
|
140
|
+
text: PERCY_SIMULATION_DRIVER_INSTRUCTION,
|
|
141
|
+
});
|
|
142
|
+
return percyInstruction;
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
throw new Error(getBootstrapFailedMessage(error, { config }));
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -63,7 +63,11 @@ export function runPercyWithBrowserstackSDK(input, config) {
|
|
|
63
63
|
content: sdkSetupCommand,
|
|
64
64
|
});
|
|
65
65
|
}
|
|
66
|
-
const ymlInstructions = generateBrowserStackYMLInstructions(
|
|
66
|
+
const ymlInstructions = generateBrowserStackYMLInstructions({
|
|
67
|
+
platforms: input.devices?.map((t) => t.join(" ")) || [],
|
|
68
|
+
enablePercy: true,
|
|
69
|
+
projectName: input.projectName,
|
|
70
|
+
});
|
|
67
71
|
if (ymlInstructions) {
|
|
68
72
|
steps.push({
|
|
69
73
|
type: "instruction",
|
|
@@ -12,7 +12,9 @@ export function runPercyWeb(input, percyToken) {
|
|
|
12
12
|
steps.push({
|
|
13
13
|
type: "instruction",
|
|
14
14
|
title: "Set Percy Token in Environment",
|
|
15
|
-
content: `
|
|
15
|
+
content: `Set the environment variable for your project:
|
|
16
|
+
export PERCY_TOKEN="${percyToken}"
|
|
17
|
+
(For Windows: use 'setx PERCY_TOKEN "${percyToken}"' or 'set PERCY_TOKEN=${percyToken}' as appropriate.)`,
|
|
16
18
|
});
|
|
17
19
|
steps.push({
|
|
18
20
|
type: "instruction",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@browserstack/mcp-server",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.5",
|
|
4
4
|
"description": "BrowserStack's Official MCP Server",
|
|
5
5
|
"mcpName": "io.github.browserstack/mcp-server",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"author": "",
|
|
36
36
|
"license": "ISC",
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
38
|
+
"@modelcontextprotocol/sdk": "^1.18.1",
|
|
39
39
|
"@types/form-data": "^2.5.2",
|
|
40
40
|
"axios": "^1.8.4",
|
|
41
41
|
"browserstack-local": "^1.5.6",
|