Skip to main content
Glama

BrowserStack MCP server

Official
scanner.ts6.14 kB
import { apiClient } from "../../lib/apiClient.js"; import { randomUUID } from "node:crypto"; import logger from "../../logger.js"; import { isLocalURL, ensureLocalBinarySetup, killExistingBrowserStackLocalProcesses, } from "../../lib/local.js"; import config from "../../config.js"; export interface AccessibilityScanResponse { success: boolean; data?: { id: string; scanRunId: string }; errors?: string[]; } export interface AccessibilityScanStatus { success: boolean; data?: { status: string }; errors?: string[]; } export class AccessibilityScanner { private auth: { username: string; password: string } | undefined; public setAuth(auth: { username: string; password: string }): void { this.auth = auth; } async startScan( name: string, urlList: string[], authConfigId?: number, ): Promise<AccessibilityScanResponse> { if (!this.auth?.username || !this.auth?.password) { throw new Error( "BrowserStack credentials are not set for AccessibilityScanner.", ); } // Check if any URL is local const hasLocal = urlList.some(isLocalURL); const localIdentifier = randomUUID(); const localHosts = new Set(["127.0.0.1", "localhost", "0.0.0.0"]); const BS_LOCAL_DOMAIN = "bs-local.com"; if (config.USE_OWN_LOCAL_BINARY_PROCESS && hasLocal) { throw new Error( "Cannot start scan with local URLs when using own BrowserStack Local binary process. Please set USE_OWN_LOCAL_BINARY_PROCESS to false.", ); } if (config.REMOTE_MCP && hasLocal) { throw new Error( "Local URLs are not supported in this remote mcp. Please use a public URL.", ); } if (hasLocal) { await ensureLocalBinarySetup( this.auth.username, this.auth.password, localIdentifier, ); } else { await killExistingBrowserStackLocalProcesses(); } const transformedUrlList = urlList.map((url) => { try { const parsed = new URL(url); if (localHosts.has(parsed.hostname)) { parsed.hostname = BS_LOCAL_DOMAIN; return parsed.toString(); } return url; } catch (e) { logger.warn(`[AccessibilityScan] Invalid URL skipped: ${e}`); return url; } }); const baseRequestBody = { name, urlList: transformedUrlList, recurring: false, ...(authConfigId && { authConfigId }), }; let requestBody = baseRequestBody; if (hasLocal) { const localConfig = { localTestingInfo: { localIdentifier, localEnabled: true, }, }; requestBody = { ...baseRequestBody, ...localConfig }; } try { const response = await apiClient.post<AccessibilityScanResponse>({ url: "https://api-accessibility.browserstack.com/api/website-scanner/v1/scans", headers: { Authorization: "Basic " + Buffer.from(`${this.auth.username}:${this.auth.password}`).toString( "base64", ), "Content-Type": "application/json", }, body: requestBody, }); const data = response.data; if (!data.success) throw new Error(`Unable to start scan: ${data.errors?.join(", ")}`); return data; } catch (err: any) { // apiClient throws generic errors, try to extract message if (err?.response?.status === 422) { throw new Error( "A scan with this name already exists. please update the name and run again.", ); } const msg = err?.response?.data?.error || err?.response?.data?.message || err?.message || String(err); throw new Error(`Failed to start scan: ${msg}`); } } async pollStatus( scanId: string, scanRunId: string, ): Promise<AccessibilityScanStatus> { try { const response = await apiClient.get<AccessibilityScanStatus>({ url: `https://api-accessibility.browserstack.com/api/website-scanner/v1/scans/${scanId}/scan_runs/${scanRunId}/status`, headers: { Authorization: "Basic " + Buffer.from( `${this.auth?.username}:${this.auth?.password}`, ).toString("base64"), }, }); const data = response.data; if (!data.success) throw new Error(`Failed to get status: ${data.errors?.join(", ")}`); return data; } catch (err: any) { const msg = err?.response?.data?.message || err?.message || String(err); throw new Error(`Failed to get scan status: ${msg}`); } } async waitUntilComplete( scanId: string, scanRunId: string, context: any, ): Promise<string> { return new Promise((resolve, reject) => { let timepercent = 0; let dotCount = 1; const interval = setInterval(async () => { try { const statusResp = await this.pollStatus(scanId, scanRunId); const status = statusResp.data!.status; timepercent += 1.67; const progress = status === "completed" ? 100 : timepercent; const dots = ".".repeat(dotCount); dotCount = (dotCount % 4) + 1; const message = status === "completed" || status === "failed" ? `Scan completed with status: ${status}` : `Scan in progress${dots}`; await context.sendNotification({ method: "notifications/progress", params: { progressToken: context._meta?.progressToken ?? "NOT_FOUND", message: message, progress: progress, total: 100, }, }); if (status === "completed" || status === "failed") { clearInterval(interval); resolve(status); } } catch (e) { clearInterval(interval); reject(e); } }, 5000); setTimeout( () => { clearInterval(interval); reject(new Error("Scan timed out after 5 minutes")); }, 5 * 60 * 1000, ); }); } }

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