Skip to main content
Glama

BrowserStack MCP server

Official
device-validator.ts19.1 kB
import { getDevicesAndBrowsers, BrowserStackProducts, } from "../../../lib/device-cache.js"; import { resolveVersion } from "../../../lib/version-resolver.js"; import { customFuzzySearch } from "../../../lib/fuzzy.js"; import { SDKSupportedBrowserAutomationFrameworkEnum } from "./types.js"; // ============================================================================ // SHARED TYPES AND INTERFACES // ============================================================================ // Type definitions for better type safety export interface DesktopBrowserEntry { os: string; os_version: string; browser: string; browser_version: string; } export interface MobileDeviceEntry { os: "android" | "ios"; os_version: string; display_name: string; browsers?: Array<{ browser: string; display_name?: string; }>; } export interface ValidatedEnvironment { platform: string; osVersion: string; browser?: string; browserVersion?: string; deviceName?: string; notes?: string; } // Raw data interfaces for API responses interface RawDesktopPlatform { os: string; os_version: string; browsers: Array<{ browser: string; browser_version: string; }>; } interface RawMobileGroup { os: "android" | "ios"; devices: Array<{ os_version: string; display_name: string; browser?: string; browsers?: Array<{ browser: string; display_name?: string; }>; }>; } interface RawDeviceData { desktop?: RawDesktopPlatform[]; mobile?: RawMobileGroup[]; } const DEFAULTS = { windows: { browser: "chrome" }, macos: { browser: "safari" }, android: { device: "Samsung Galaxy S24", browser: "chrome" }, ios: { device: "iPhone 15", browser: "safari" }, } as const; // Performance optimization: Indexed maps for faster lookups interface DesktopIndex { byOS: Map<string, DesktopBrowserEntry[]>; byOSVersion: Map<string, DesktopBrowserEntry[]>; byBrowser: Map<string, DesktopBrowserEntry[]>; nested: Map<string, Map<string, Map<string, DesktopBrowserEntry[]>>>; } interface MobileIndex { byPlatform: Map<string, MobileDeviceEntry[]>; byDeviceName: Map<string, MobileDeviceEntry[]>; byOSVersion: Map<string, MobileDeviceEntry[]>; } // ============================================================================ // AUTOMATE SECTION (Desktop + Mobile for BrowserStack SDK) // ============================================================================ // Helper functions to build device entries and eliminate duplication function buildDesktopEntries( automateData: RawDeviceData, ): DesktopBrowserEntry[] { if (!automateData.desktop) { return []; } return automateData.desktop.flatMap((platform: RawDesktopPlatform) => platform.browsers.map((browser) => ({ os: platform.os, os_version: platform.os_version, browser: browser.browser, browser_version: browser.browser_version, })), ); } function buildMobileEntries( appAutomateData: RawDeviceData, platform: "android" | "ios", ): MobileDeviceEntry[] { if (!appAutomateData.mobile) { return []; } return appAutomateData.mobile .filter((group: RawMobileGroup) => group.os === platform) .flatMap((group: RawMobileGroup) => group.devices.map((device) => ({ os: group.os, os_version: device.os_version, display_name: device.display_name, browsers: device.browsers || [ { browser: device.browser || (platform === "android" ? "chrome" : "safari"), }, ], })), ); } // Performance optimization: Create indexed maps for faster lookups function createDesktopIndex(entries: DesktopBrowserEntry[]): DesktopIndex { const byOS = new Map<string, DesktopBrowserEntry[]>(); const byOSVersion = new Map<string, DesktopBrowserEntry[]>(); const byBrowser = new Map<string, DesktopBrowserEntry[]>(); const nested = new Map< string, Map<string, Map<string, DesktopBrowserEntry[]>> >(); for (const entry of entries) { // Index by OS if (!byOS.has(entry.os)) { byOS.set(entry.os, []); } byOS.get(entry.os)!.push(entry); // Index by OS version if (!byOSVersion.has(entry.os_version)) { byOSVersion.set(entry.os_version, []); } byOSVersion.get(entry.os_version)!.push(entry); // Index by browser if (!byBrowser.has(entry.browser)) { byBrowser.set(entry.browser, []); } byBrowser.get(entry.browser)!.push(entry); // Build nested index: Map<os, Map<os_version, Map<browser, DesktopBrowserEntry[]>>> if (!nested.has(entry.os)) { nested.set(entry.os, new Map()); } const osMap = nested.get(entry.os)!; if (!osMap.has(entry.os_version)) { osMap.set(entry.os_version, new Map()); } const osVersionMap = osMap.get(entry.os_version)!; if (!osVersionMap.has(entry.browser)) { osVersionMap.set(entry.browser, []); } osVersionMap.get(entry.browser)!.push(entry); } return { byOS, byOSVersion, byBrowser, nested }; } function createMobileIndex(entries: MobileDeviceEntry[]): MobileIndex { const byPlatform = new Map<string, MobileDeviceEntry[]>(); const byDeviceName = new Map<string, MobileDeviceEntry[]>(); const byOSVersion = new Map<string, MobileDeviceEntry[]>(); for (const entry of entries) { // Index by platform if (!byPlatform.has(entry.os)) { byPlatform.set(entry.os, []); } byPlatform.get(entry.os)!.push(entry); // Index by device name (case-insensitive) const deviceKey = entry.display_name.toLowerCase(); if (!byDeviceName.has(deviceKey)) { byDeviceName.set(deviceKey, []); } byDeviceName.get(deviceKey)!.push(entry); // Index by OS version if (!byOSVersion.has(entry.os_version)) { byOSVersion.set(entry.os_version, []); } byOSVersion.get(entry.os_version)!.push(entry); } return { byPlatform, byDeviceName, byOSVersion }; } export async function validateDevices( devices: Array<Array<string>>, framework?: string, ): Promise<ValidatedEnvironment[]> { const validatedEnvironments: ValidatedEnvironment[] = []; if (!devices || devices.length === 0) { // Use centralized default fallback return [ { platform: "windows", osVersion: "11", browser: DEFAULTS.windows.browser, browserVersion: "latest", }, ]; } // Determine what data we need to fetch const needsDesktop = devices.some((env) => ["windows", "macos"].includes((env[0] || "").toLowerCase()), ); const needsMobile = devices.some((env) => ["android", "ios"].includes((env[0] || "").toLowerCase()), ); // Fetch data using framework-specific endpoint for both desktop and mobile let deviceData: RawDeviceData | null = null; try { if (needsDesktop || needsMobile) { if (framework === SDKSupportedBrowserAutomationFrameworkEnum.playwright) { deviceData = (await getDevicesAndBrowsers( BrowserStackProducts.PLAYWRIGHT_AUTOMATE, )) as RawDeviceData; } else { deviceData = (await getDevicesAndBrowsers( BrowserStackProducts.SELENIUM_AUTOMATE, )) as RawDeviceData; } } } catch (error) { throw new Error( `Failed to fetch device data: ${error instanceof Error ? error.message : String(error)}`, ); } // Preprocess data into indexed maps for better performance let desktopIndex: DesktopIndex | null = null; let androidIndex: MobileIndex | null = null; let iosIndex: MobileIndex | null = null; if (needsDesktop && deviceData) { const desktopEntries = buildDesktopEntries(deviceData); desktopIndex = createDesktopIndex(desktopEntries); } if (needsMobile && deviceData) { const androidEntries = buildMobileEntries(deviceData, "android"); const iosEntries = buildMobileEntries(deviceData, "ios"); androidIndex = createMobileIndex(androidEntries); iosIndex = createMobileIndex(iosEntries); } for (const env of devices) { const discriminator = (env[0] || "").toLowerCase(); let validatedEnv: ValidatedEnvironment; if (discriminator === "windows") { validatedEnv = validateDesktopEnvironment( env, desktopIndex!, "windows", DEFAULTS.windows.browser, ); } else if (discriminator === "macos") { validatedEnv = validateDesktopEnvironment( env, desktopIndex!, "macos", DEFAULTS.macos.browser, ); } else if (discriminator === "android") { validatedEnv = validateMobileEnvironment( env, androidIndex!, "android", DEFAULTS.android.device, DEFAULTS.android.browser, ); } else if (discriminator === "ios") { validatedEnv = validateMobileEnvironment( env, iosIndex!, "ios", DEFAULTS.ios.device, DEFAULTS.ios.browser, ); } else { throw new Error(`Unsupported platform: ${discriminator}`); } validatedEnvironments.push(validatedEnv); } if (framework === SDKSupportedBrowserAutomationFrameworkEnum.playwright) { validatedEnvironments.forEach((env) => { if (env.browser) { env.browser = env.browser.toLowerCase(); } }); } return validatedEnvironments; } // Optimized desktop validation using nested indexed maps for O(1) lookups function validateDesktopEnvironment( env: string[], index: DesktopIndex, platform: "windows" | "macos", defaultBrowser: string, ): ValidatedEnvironment { const [, osVersion, browser, browserVersion] = env; const osKey = platform === "windows" ? "Windows" : "OS X"; // Use nested index for O(1) lookup instead of filtering const osMap = index.nested.get(osKey); if (!osMap) { throw new Error(`No ${platform} devices available`); } // Get available OS versions for this platform const availableOSVersions = Array.from(osMap.keys()); const validatedOSVersion = resolveVersion( osVersion || "latest", availableOSVersions, ); // Use nested index for O(1) lookup const osVersionMap = osMap.get(validatedOSVersion); if (!osVersionMap) { throw new Error( `OS version "${validatedOSVersion}" not available for ${platform}`, ); } // Get available browsers for this OS version const availableBrowsers = Array.from(osVersionMap.keys()); const validatedBrowser = validateBrowserExact( browser || defaultBrowser, availableBrowsers, ); // Use nested index for O(1) lookup const browserEntries = osVersionMap.get(validatedBrowser); if (!browserEntries || browserEntries.length === 0) { throw new Error( `Browser "${validatedBrowser}" not available for ${platform} ${validatedOSVersion}`, ); } const availableBrowserVersions = [ ...new Set(browserEntries.map((e) => e.browser_version)), ] as string[]; const validatedBrowserVersion = resolveVersion( browserVersion || "latest", availableBrowserVersions, ); return { platform, osVersion: validatedOSVersion, browser: validatedBrowser, browserVersion: validatedBrowserVersion, }; } // Optimized mobile validation using indexed maps function validateMobileEnvironment( env: string[], index: MobileIndex, platform: "android" | "ios", defaultDevice: string, defaultBrowser: string, ): ValidatedEnvironment { const [, deviceName, osVersion, browser] = env; const platformEntries = index.byPlatform.get(platform) || []; if (platformEntries.length === 0) { throw new Error(`No ${platform} devices available`); } // Use fuzzy search only for device names (as suggested in feedback) const deviceMatches = customFuzzySearch( platformEntries, ["display_name"], deviceName || defaultDevice, 5, ); if (deviceMatches.length === 0) { throw new Error( `No ${platform} devices matching "${deviceName}". Available devices: ${platformEntries .map((d) => d.display_name || "unknown") .slice(0, 5) .join(", ")}`, ); } // Try to find exact match first const exactMatch = deviceMatches.find( (m) => m.display_name.toLowerCase() === (deviceName || "").toLowerCase(), ); // If no exact match, throw error instead of using fuzzy match if (!exactMatch) { const suggestions = deviceMatches.map((m) => m.display_name).join(", "); throw new Error( `Device "${deviceName}" not found exactly for ${platform}. Available similar devices: ${suggestions}. Please use the exact device name.`, ); } // Use index for faster filtering const deviceKey = exactMatch.display_name.toLowerCase(); const deviceFiltered = index.byDeviceName.get(deviceKey) || []; const availableOSVersions = [ ...new Set(deviceFiltered.map((d) => d.os_version)), ] as string[]; const validatedOSVersion = resolveVersion( osVersion || "latest", availableOSVersions, ); // Use index for faster filtering const osVersionEntries = index.byOSVersion.get(validatedOSVersion) || []; const osFiltered = osVersionEntries.filter( (d) => d.display_name.toLowerCase() === deviceKey, ); // Validate browser if provided - use exact matching for browsers let validatedBrowser = browser || defaultBrowser; if (browser && osFiltered.length > 0) { // Extract browsers more carefully - handle different possible structures const availableBrowsers = [ ...new Set( osFiltered.flatMap((d) => { if (d.browsers && Array.isArray(d.browsers)) { // If browsers is an array of objects with browser property return d.browsers .map((b) => { // Use display_name for user-friendly browser names, fallback to browser field return b.display_name || b.browser; }) .filter(Boolean); } // For mobile devices, provide default browsers if none found return platform === "android" ? ["chrome"] : ["safari"]; }), ), ].filter(Boolean) as string[]; if (availableBrowsers.length > 0) { try { validatedBrowser = validateBrowserExact(browser, availableBrowsers); } catch (error) { // Add more context to browser validation errors 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)}`, ); } } else { // For mobile, if no specific browsers found, just use the requested browser // as most mobile devices support standard browsers validatedBrowser = browser || defaultBrowser; } } return { platform, osVersion: validatedOSVersion, deviceName: exactMatch.display_name, browser: validatedBrowser, }; } // ============================================================================ // APP AUTOMATE SECTION (Mobile devices for App Automate) // ============================================================================ export async function validateAppAutomateDevices( devices: Array<Array<string>>, ): Promise<ValidatedEnvironment[]> { const validatedDevices: ValidatedEnvironment[] = []; if (!devices || devices.length === 0) { // Use centralized default fallback return [ { platform: "android", osVersion: "latest", deviceName: DEFAULTS.android.device, }, ]; } let appAutomateData: RawDeviceData; try { // Fetch app automate device data appAutomateData = (await getDevicesAndBrowsers( BrowserStackProducts.APP_AUTOMATE, )) as RawDeviceData; } catch (error) { // Only wrap fetch-related errors throw new Error( `Failed to fetch device data: ${error instanceof Error ? error.message : String(error)}`, ); } for (const device of devices) { // Parse device array in format ["android", "Device Name", "OS Version"] const [platform, deviceName, osVersion] = device; // Find matching device in the data let validatedDevice: ValidatedEnvironment | null = null; if (!appAutomateData.mobile) { throw new Error("No mobile device data available"); } // Filter by platform first const platformGroup = appAutomateData.mobile.find( (group) => group.os === platform.toLowerCase(), ); if (!platformGroup) { throw new Error(`Platform "${platform}" not supported for App Automate`); } const platformDevices = platformGroup.devices; // Find exact device name match (case-insensitive) const exactMatch = platformDevices.find( (d) => d.display_name.toLowerCase() === deviceName.toLowerCase(), ); if (exactMatch) { // Check if the OS version is available for this device const deviceVersions = platformDevices .filter((d) => d.display_name === exactMatch.display_name) .map((d) => d.os_version); const validatedOSVersion = resolveVersion( osVersion || "latest", deviceVersions, ); validatedDevice = { platform: platformGroup.os, osVersion: validatedOSVersion, deviceName: exactMatch.display_name, }; } if (!validatedDevice) { // If no exact match found, suggest similar devices from the SAME platform only const platformDevicesForSearch = platformDevices.map((d) => ({ ...d, platform: platformGroup.os, })); // Try fuzzy search with a more lenient threshold const deviceMatches = customFuzzySearch( platformDevicesForSearch, ["display_name"], deviceName, 5, 0.8, // More lenient threshold ); const suggestions = deviceMatches .map((m) => `${m.display_name}`) .join(", "); // If no fuzzy matches, show some available devices as fallback const fallbackDevices = platformDevicesForSearch .slice(0, 5) .map((d) => d.display_name) .join(", "); const errorMessage = suggestions ? `Device "${deviceName}" not found for platform "${platform}".\nAvailable similar devices: ${suggestions}` : `Device "${deviceName}" not found for platform "${platform}".\nAvailable devices: ${fallbackDevices}`; throw new Error(errorMessage); } validatedDevices.push(validatedDevice); } return validatedDevices; } // ============================================================================ // SHARED UTILITY FUNCTIONS // ============================================================================ // Exact browser validation (preferred for structured fields) function validateBrowserExact( requestedBrowser: string, availableBrowsers: string[], ): string { const exactMatch = availableBrowsers.find( (b) => b.toLowerCase() === requestedBrowser.toLowerCase(), ); if (exactMatch) { return exactMatch; } throw new Error( `Browser "${requestedBrowser}" not found. Available options: ${availableBrowsers.join(", ")}`, ); }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/browserstack/mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server