@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.
Files changed (39) hide show
  1. package/README.md +9 -5
  2. package/dist/lib/apiClient.d.ts +7 -5
  3. package/dist/lib/apiClient.js +76 -15
  4. package/dist/lib/device-cache.d.ts +3 -1
  5. package/dist/lib/device-cache.js +4 -0
  6. package/dist/lib/instrumentation.js +6 -3
  7. package/dist/lib/utils.d.ts +75 -2
  8. package/dist/lib/utils.js +20 -0
  9. package/dist/lib/version-resolver.js +26 -14
  10. package/dist/tools/appautomate-utils/appium-sdk/config-generator.d.ts +7 -1
  11. package/dist/tools/appautomate-utils/appium-sdk/config-generator.js +46 -26
  12. package/dist/tools/appautomate-utils/appium-sdk/constants.d.ts +1 -1
  13. package/dist/tools/appautomate-utils/appium-sdk/constants.js +24 -3
  14. package/dist/tools/appautomate-utils/appium-sdk/handler.js +16 -2
  15. package/dist/tools/appautomate-utils/appium-sdk/languages/java.d.ts +2 -0
  16. package/dist/tools/appautomate-utils/appium-sdk/languages/java.js +63 -29
  17. package/dist/tools/appautomate-utils/appium-sdk/types.d.ts +2 -1
  18. package/dist/tools/appautomate-utils/appium-sdk/types.js +10 -1
  19. package/dist/tools/appautomate-utils/native-execution/constants.d.ts +2 -1
  20. package/dist/tools/appautomate-utils/native-execution/constants.js +24 -2
  21. package/dist/tools/appautomate.js +15 -2
  22. package/dist/tools/percy-sdk.js +30 -3
  23. package/dist/tools/run-percy-scan.js +1 -1
  24. package/dist/tools/sdk-utils/bstack/configUtils.d.ts +7 -4
  25. package/dist/tools/sdk-utils/bstack/configUtils.js +67 -20
  26. package/dist/tools/sdk-utils/bstack/sdkHandler.d.ts +1 -1
  27. package/dist/tools/sdk-utils/bstack/sdkHandler.js +10 -2
  28. package/dist/tools/sdk-utils/common/constants.d.ts +5 -4
  29. package/dist/tools/sdk-utils/common/constants.js +7 -6
  30. package/dist/tools/sdk-utils/common/device-validator.d.ts +25 -0
  31. package/dist/tools/sdk-utils/common/device-validator.js +368 -0
  32. package/dist/tools/sdk-utils/common/schema.d.ts +24 -4
  33. package/dist/tools/sdk-utils/common/schema.js +57 -3
  34. package/dist/tools/sdk-utils/common/utils.js +2 -1
  35. package/dist/tools/sdk-utils/handler.d.ts +1 -0
  36. package/dist/tools/sdk-utils/handler.js +40 -12
  37. package/dist/tools/sdk-utils/percy-bstack/handler.js +5 -1
  38. package/dist/tools/sdk-utils/percy-web/handler.js +3 -1
  39. 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
- desiredPlatforms: z.ZodArray<z.ZodEnum<["windows", "macos", "android", "ios"]>, "many">;
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
- desiredPlatforms: z.ZodArray<z.ZodEnum<["windows", "macos", "android", "ios"]>, "many">;
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
- desiredPlatforms: ("android" | "windows" | "macos" | "ios")[];
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
- desiredPlatforms: ("android" | "windows" | "macos" | "ios")[];
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
- desiredPlatforms: z
24
- .array(z.enum(["windows", "macos", "android", "ios"]))
25
- .describe("An array of platforms to run tests on."),
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: ${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
- try {
14
- const input = RunTestsOnBrowserStackSchema.parse(rawInput);
15
- // Only handle BrowserStack SDK setup for functional/integration tests.
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
- desiredPlatforms: [],
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
- desiredPlatforms: [],
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(input.desiredPlatforms, true, input.projectName);
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: `Here is percy token if required {${percyToken}}`,
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.4",
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.11.4",
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",