Skip to main content
Glama

BrowserStack MCP server

Official
accessibility.ts14.6 kB
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; import { AccessibilityScanner } from "./accessiblity-utils/scanner.js"; import { AccessibilityReportFetcher } from "./accessiblity-utils/report-fetcher.js"; import { AccessibilityAuthConfig } from "./accessiblity-utils/auth-config.js"; import { trackMCP } from "../lib/instrumentation.js"; import { parseAccessibilityReportFromCSV } from "./accessiblity-utils/report-parser.js"; import { queryAccessibilityRAG } from "./accessiblity-utils/accessibility-rag.js"; import { getBrowserStackAuth } from "../lib/get-auth.js"; import { BrowserStackConfig } from "../lib/types.js"; import logger from "../logger.js"; interface AuthCredentials { username: string; password: string; } interface ScanProgressContext { sendNotification: (notification: any) => Promise<void>; _meta?: { progressToken?: string | number; }; } interface FormAuthArgs { name: string; type: "form"; url: string; username: string; password: string; usernameSelector: string; passwordSelector: string; submitSelector: string; } interface BasicAuthArgs { name: string; type: "basic"; url: string; username: string; password: string; } type AuthConfigArgs = FormAuthArgs | BasicAuthArgs; function setupAuth(config: BrowserStackConfig): AuthCredentials { const authString = getBrowserStackAuth(config); const [username, password] = authString.split(":"); return { username, password }; } function createErrorResponse(message: string, isError = true): CallToolResult { return { content: [ { type: "text", text: message, isError, }, ], isError, }; } function createSuccessResponse(messages: string[]): CallToolResult { return { content: messages.map((text) => ({ type: "text" as const, text, })), }; } function handleMCPError( toolName: string, server: McpServer, config: BrowserStackConfig, error: unknown, ): CallToolResult { trackMCP(toolName, server.server.getClientVersion()!, error, config); const errorMessage = error instanceof Error ? error.message : "Unknown error"; return createErrorResponse( `Failed to ${toolName.replace(/([A-Z])/g, " $1").toLowerCase()}: ${errorMessage}. Please open an issue on GitHub if the problem persists`, ); } async function notifyScanProgress( context: ScanProgressContext, message: string, progress = 0, ): Promise<void> { await context.sendNotification({ method: "notifications/progress", params: { progressToken: context._meta?.progressToken?.toString() ?? "NOT_FOUND", message, progress, total: 100, }, }); } async function initializeScanner( config: BrowserStackConfig, ): Promise<AccessibilityScanner> { const scanner = new AccessibilityScanner(); const auth = setupAuth(config); scanner.setAuth(auth); return scanner; } async function initializeReportFetcher( config: BrowserStackConfig, ): Promise<AccessibilityReportFetcher> { const reportFetcher = new AccessibilityReportFetcher(); const auth = setupAuth(config); reportFetcher.setAuth(auth); return reportFetcher; } async function executeAccessibilityRAG( args: { query: string }, server: McpServer, config: BrowserStackConfig, ): Promise<CallToolResult> { try { trackMCP( "accessibilityExpert", server.server.getClientVersion()!, undefined, config, ); return await queryAccessibilityRAG(args.query, config); } catch (error) { return handleMCPError("accessibilityExpert", server, config, error); } } async function executeFetchAccessibilityIssues( args: { scanId: string; scanRunId: string; cursor?: number }, server: McpServer, config: BrowserStackConfig, ): Promise<CallToolResult> { try { trackMCP( "fetchAccessibilityIssues", server.server.getClientVersion()!, undefined, config, ); return await fetchAccessibilityIssues( args.scanId, args.scanRunId, config, args.cursor, ); } catch (error) { return handleMCPError("fetchAccessibilityIssues", server, config, error); } } async function fetchAccessibilityIssues( scanId: string, scanRunId: string, config: BrowserStackConfig, cursor = 0, ): Promise<CallToolResult> { const reportFetcher = await initializeReportFetcher(config); const reportLink = await reportFetcher.getReportLink(scanId, scanRunId); const { records, page_length, total_issues, next_page } = await parseAccessibilityReportFromCSV(reportLink, { nextPage: cursor }); const currentlyShown = cursor === 0 ? page_length : Math.floor(cursor / JSON.stringify(records[0] || {}).length) + page_length; const remainingIssues = total_issues - currentlyShown; const messages = [ `Retrieved ${page_length} accessibility issues (Total: ${total_issues})`, `Issues: ${JSON.stringify(records, null, 2)}`, ]; if (next_page !== null) { messages.push( `${remainingIssues} more issues available. Use fetchAccessibilityIssues with cursor: ${next_page} to get the next batch.`, ); } else { messages.push(`✅ All issues retrieved.`); } return createSuccessResponse(messages); } async function executeAccessibilityScan( args: { name: string; pageURL: string; authConfigId?: number }, context: ScanProgressContext, server: McpServer, config: BrowserStackConfig, ): Promise<CallToolResult> { try { trackMCP( "startAccessibilityScan", server.server.getClientVersion()!, undefined, config, ); return await runAccessibilityScan( args.name, args.pageURL, context, config, args.authConfigId, ); } catch (error) { return handleMCPError("startAccessibilityScan", server, config, error); } } function validateFormAuthArgs(args: AuthConfigArgs): args is FormAuthArgs { return ( args.type === "form" && "usernameSelector" in args && "passwordSelector" in args && "submitSelector" in args && !!args.usernameSelector && !!args.passwordSelector && !!args.submitSelector ); } async function createAuthConfig( args: AuthConfigArgs, config: BrowserStackConfig, ): Promise<any> { const authConfig = new AccessibilityAuthConfig(); const auth = setupAuth(config); authConfig.setAuth(auth); if (args.type === "form") { if (!validateFormAuthArgs(args)) { throw new Error( "Form authentication requires usernameSelector, passwordSelector, and submitSelector", ); } return await authConfig.createFormAuthConfig(args.name, { username: args.username, usernameSelector: args.usernameSelector, password: args.password, passwordSelector: args.passwordSelector, submitSelector: args.submitSelector, url: args.url, }); } else { return await authConfig.createBasicAuthConfig(args.name, { url: args.url, username: args.username, password: args.password, }); } } async function executeCreateAuthConfig( args: AuthConfigArgs, server: McpServer, config: BrowserStackConfig, ): Promise<CallToolResult> { try { trackMCP( "createAccessibilityAuthConfig", server.server.getClientVersion()!, undefined, config, ); logger.info(`Creating auth config: ${JSON.stringify(args)}`); const result = await createAuthConfig(args, config); return createSuccessResponse([ `✅ Auth config "${args.name}" created successfully with ID: ${result.data?.id}`, `Auth config details: ${JSON.stringify(result.data, null, 2)}`, ]); } catch (error) { if ( error instanceof Error && error.message.includes("Form authentication requires") ) { return createErrorResponse(error.message); } return handleMCPError( "createAccessibilityAuthConfig", server, config, error, ); } } async function executeGetAuthConfig( args: { configId: number }, server: McpServer, config: BrowserStackConfig, ): Promise<CallToolResult> { try { trackMCP( "getAccessibilityAuthConfig", server.server.getClientVersion()!, undefined, config, ); const authConfig = new AccessibilityAuthConfig(); const auth = setupAuth(config); authConfig.setAuth(auth); const result = await authConfig.getAuthConfig(args.configId); return createSuccessResponse([ `✅ Auth config retrieved successfully`, `Auth config details: ${JSON.stringify(result.data, null, 2)}`, ]); } catch (error) { return handleMCPError("getAccessibilityAuthConfig", server, config, error); } } function createScanFailureResponse( name: string, status: string, ): CallToolResult { return createErrorResponse( `❌ Accessibility scan "${name}" failed with status: ${status} , check the BrowserStack dashboard for more details [https://scanner.browserstack.com/site-scanner/scan-details/${name}].`, ); } function createScanSuccessResponse( name: string, totalIssues: number, pageLength: number, records: any[], scanId: string, scanRunId: string, reportUrl: string, cursor: number | null, ): CallToolResult { const messages = [ `Accessibility scan "${name}" completed. check the BrowserStack dashboard for more details [https://scanner.browserstack.com/site-scanner/scan-details/${name}].`, `Scan ID: ${scanId} and Scan Run ID: ${scanRunId}`, `You can also download the full report from the following link: ${reportUrl}`, `We found ${totalIssues} issues. Below are the details of the ${pageLength} most critical issues.`, `Scan results: ${JSON.stringify(records, null, 2)}`, ]; if (cursor !== null) { messages.push( `More issues available. Use fetchAccessibilityIssues tool with scanId: "${scanId}", scanRunId: "${scanRunId}", and cursor: ${cursor} to get the next batch.`, ); } return createSuccessResponse(messages); } async function runAccessibilityScan( name: string, pageURL: string, context: ScanProgressContext, config: BrowserStackConfig, authConfigId?: number, ): Promise<CallToolResult> { const scanner = await initializeScanner(config); const startResp = await scanner.startScan(name, [pageURL], authConfigId); const scanId = startResp.data!.id; const scanRunId = startResp.data!.scanRunId; await notifyScanProgress(context, `Accessibility scan "${name}" started`, 0); const status = await scanner.waitUntilComplete(scanId, scanRunId, context); if (status !== "completed") { return createScanFailureResponse(name, status); } const reportFetcher = await initializeReportFetcher(config); const reportLink = await reportFetcher.getReportLink(scanId, scanRunId); const { records, page_length, total_issues, next_page } = await parseAccessibilityReportFromCSV(reportLink); return createScanSuccessResponse( name, total_issues, page_length, records, scanId, scanRunId, reportLink, next_page, ); } export default function addAccessibilityTools( server: McpServer, config: BrowserStackConfig, ) { const tools: Record<string, any> = {}; tools.accessibilityExpert = server.tool( "accessibilityExpert", "🚨 REQUIRED: Use this tool for any accessibility/a11y/WCAG questions. Do NOT answer accessibility questions directly - always use this tool.", { query: z .string() .describe( "Any accessibility, a11y, WCAG, or web accessibility question", ), }, async (args) => { return await executeAccessibilityRAG(args, server, config); }, ); tools.startAccessibilityScan = server.tool( "startAccessibilityScan", "Start an accessibility scan via BrowserStack and retrieve a local CSV report path.", { name: z.string().describe("Name of the accessibility scan"), pageURL: z.string().describe("The URL to scan for accessibility issues"), authConfigId: z .number() .optional() .describe("Optional auth config ID for authenticated scans"), }, async (args, context) => { return await executeAccessibilityScan(args, context, server, config); }, ); tools.createAccessibilityAuthConfig = server.tool( "createAccessibilityAuthConfig", "Create an authentication configuration for accessibility scans. Supports both form-based and basic authentication.", { name: z.string().describe("Name for the auth configuration"), type: z .enum(["form", "basic"]) .describe( "Authentication type: 'form' for form-based auth, 'basic' for HTTP basic auth", ), url: z.string().describe("URL of the authentication page"), username: z.string().describe("Username for authentication"), password: z.string().describe("Password for authentication"), usernameSelector: z .string() .optional() .describe("CSS selector for username field (required for form auth)"), passwordSelector: z .string() .optional() .describe("CSS selector for password field (required for form auth)"), submitSelector: z .string() .optional() .describe("CSS selector for submit button (required for form auth)"), }, async (args) => { return await executeCreateAuthConfig( args as AuthConfigArgs, server, config, ); }, ); tools.getAccessibilityAuthConfig = server.tool( "getAccessibilityAuthConfig", "Retrieve an existing authentication configuration by ID.", { configId: z.number().describe("ID of the auth configuration to retrieve"), }, async (args) => { return await executeGetAuthConfig(args, server, config); }, ); tools.fetchAccessibilityIssues = server.tool( "fetchAccessibilityIssues", "Fetch accessibility issues from a completed scan with pagination support. Use cursor parameter to get subsequent pages of results.", { scanId: z .string() .describe("The scan ID from a completed accessibility scan"), scanRunId: z .string() .describe("The scan run ID from a completed accessibility scan"), cursor: z .number() .optional() .describe("Character offset for pagination (default: 0)"), }, async (args) => { return await executeFetchAccessibilityIssues(args, server, config); }, ); return tools; }

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