From eced35b6269383a20b6509e5c88fa1516084a133 Mon Sep 17 00:00:00 2001 From: Ruturaj-Browserstack Date: Mon, 18 Aug 2025 15:41:04 +0530 Subject: [PATCH 1/6] feat: add authentication configuration support for accessibility scans --- src/tools/accessibility.ts | 438 +++++++++++++++----- src/tools/accessiblity-utils/auth-config.ts | 201 +++++++++ src/tools/accessiblity-utils/scanner.ts | 2 + 3 files changed, 532 insertions(+), 109 deletions(-) create mode 100644 src/tools/accessiblity-utils/auth-config.ts diff --git a/src/tools/accessibility.ts b/src/tools/accessibility.ts index 625bc2a0..ddfb303b 100644 --- a/src/tools/accessibility.ts +++ b/src/tools/accessibility.ts @@ -3,81 +3,309 @@ 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"; -async function runAccessibilityScan( - name: string, - pageURL: string, - context: any, - config: BrowserStackConfig, -): Promise { - // Create scanner and set auth on the go - const scanner = new AccessibilityScanner(); +interface AuthCredentials { + username: string; + password: string; +} + +interface ScanProgressContext { + sendNotification: (notification: any) => Promise; + _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(":"); - scanner.setAuth({ username, password }); + return { username, password }; +} - // Start scan - const startResp = await scanner.startScan(name, [pageURL]); - const scanId = startResp.data!.id; - const scanRunId = startResp.data!.scanRunId; +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`, + ); +} - // Notify scan start +async function notifyScanProgress( + context: ScanProgressContext, + message: string, + progress = 0, +): Promise { await context.sendNotification({ method: "notifications/progress", params: { - progressToken: context._meta?.progressToken ?? "NOT_FOUND", - message: `Accessibility scan "${name}" started`, - progress: 0, + progressToken: context._meta?.progressToken?.toString() ?? "NOT_FOUND", + message, + progress, total: 100, }, }); +} + +async function initializeScanner( + config: BrowserStackConfig, +): Promise { + const scanner = new AccessibilityScanner(); + const auth = setupAuth(config); + scanner.setAuth(auth); + return scanner; +} + +async function initializeReportFetcher( + config: BrowserStackConfig, +): Promise { + const reportFetcher = new AccessibilityReportFetcher(); + const auth = setupAuth(config); + reportFetcher.setAuth(auth); + return reportFetcher; +} + +function trackMCPSuccess( + toolName: string, + server: McpServer, + config: BrowserStackConfig, +): void { + trackMCP(toolName, server.server.getClientVersion()!, undefined, config); +} + +async function executeAccessibilityRAG( + args: { query: string }, + server: McpServer, + config: BrowserStackConfig, +): Promise { + try { + trackMCPSuccess("accessibilityExpert", server, config); + return await queryAccessibilityRAG(args.query, config); + } catch (error) { + return handleMCPError("accessibilityExpert", server, config, error); + } +} + +async function executeAccessibilityScan( + args: { name: string; pageURL: string; authConfigId?: number }, + context: ScanProgressContext, + server: McpServer, + config: BrowserStackConfig, +): Promise { + try { + trackMCPSuccess("startAccessibilityScan", server, 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 { + 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 { + try { + trackMCPSuccess("createAccessibilityAuthConfig", server, 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 { + try { + trackMCPSuccess("getAccessibilityAuthConfig", server, 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[], +): CallToolResult { + return createSuccessResponse([ + `✅ Accessibility scan "${name}" completed. check the BrowserStack dashboard for more details [https://scanner.browserstack.com/site-scanner/scan-details/${name}].`, + `We found ${totalIssues} issues. Below are the details of the ${pageLength} most critical issues.`, + `Scan results: ${JSON.stringify(records, null, 2)}`, + ]); +} + +async function runAccessibilityScan( + name: string, + pageURL: string, + context: ScanProgressContext, + config: BrowserStackConfig, + authConfigId?: number, +): Promise { + 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); - // Wait until scan completes const status = await scanner.waitUntilComplete(scanId, scanRunId, context); if (status !== "completed") { - return { - content: [ - { - type: "text", - text: `❌ Accessibility scan "${name}" failed with status: ${status} , check the BrowserStack dashboard for more details [https://scanner.browserstack.com/site-scanner/scan-details/${name}].`, - isError: true, - }, - ], - isError: true, - }; + return createScanFailureResponse(name, status); } - // Create report fetcher and set auth on the go - const reportFetcher = new AccessibilityReportFetcher(); - reportFetcher.setAuth({ username, password }); - - // Fetch CSV report link + const reportFetcher = await initializeReportFetcher(config); const reportLink = await reportFetcher.getReportLink(scanId, scanRunId); const { records, page_length, total_issues } = await parseAccessibilityReportFromCSV(reportLink); - return { - content: [ - { - type: "text", - text: `✅ Accessibility scan "${name}" completed. check the BrowserStack dashboard for more details [https://scanner.browserstack.com/site-scanner/scan-details/${name}].`, - }, - { - type: "text", - text: `We found ${total_issues} issues. Below are the details of the ${page_length} most critical issues.`, - }, - { - type: "text", - text: `Scan results: ${JSON.stringify(records, null, 2)}`, - }, - ], - }; + return createScanSuccessResponse(name, total_issues, page_length, records); } export default function addAccessibilityTools( @@ -97,33 +325,7 @@ export default function addAccessibilityTools( ), }, async (args) => { - try { - trackMCP( - "accessibilityExpert", - server.server.getClientVersion()!, - undefined, - config, - ); - return await queryAccessibilityRAG(args.query, config); - } catch (error) { - trackMCP( - "accessibilityExpert", - server.server.getClientVersion()!, - error, - config, - ); - return { - content: [ - { - type: "text", - text: `Failed to query accessibility RAG: ${ - error instanceof Error ? error.message : "Unknown error" - }. Please open an issue on GitHub if the problem persists`, - }, - ], - isError: true, - }; - } + return await executeAccessibilityRAG(args, server, config); }, ); @@ -133,41 +335,59 @@ export default function addAccessibilityTools( { 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) => { - try { - trackMCP( - "startAccessibilityScan", - server.server.getClientVersion()!, - undefined, - config, - ); - return await runAccessibilityScan( - args.name, - args.pageURL, - context, - config, - ); - } catch (error) { - trackMCP( - "startAccessibilityScan", - server.server.getClientVersion()!, - error, - config, - ); - return { - content: [ - { - type: "text", - text: `Failed to start accessibility scan: ${ - error instanceof Error ? error.message : "Unknown error" - }. Please open an issue on GitHub if the problem persists`, - isError: true, - }, - ], - isError: true, - }; - } + 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); }, ); diff --git a/src/tools/accessiblity-utils/auth-config.ts b/src/tools/accessiblity-utils/auth-config.ts new file mode 100644 index 00000000..9ac64fc7 --- /dev/null +++ b/src/tools/accessiblity-utils/auth-config.ts @@ -0,0 +1,201 @@ +import { apiClient } from "../../lib/apiClient.js"; +import logger from "../../logger.js"; + +export interface AuthConfigResponse { + success: boolean; + data?: { + id: number; + name: string; + type: string; + username?: string; + password?: string; + url?: string; + usernameSelector?: string; + passwordSelector?: string; + submitSelector?: string; + }; + errors?: string[]; +} + +export interface FormAuthData { + username: string; + usernameSelector: string; + password: string; + passwordSelector: string; + submitSelector: string; + url: string; +} + +export interface BasicAuthData { + url: string; + username: string; + password: string; +} + +export class AccessibilityAuthConfig { + private auth: { username: string; password: string } | undefined; + + public setAuth(auth: { username: string; password: string }): void { + this.auth = auth; + } + + private transformLocalUrl(url: string): string { + try { + const parsed = new URL(url); + const localHosts = new Set(["127.0.0.1", "localhost", "0.0.0.0"]); + const BS_LOCAL_DOMAIN = "bs-local.com"; + + if (localHosts.has(parsed.hostname)) { + parsed.hostname = BS_LOCAL_DOMAIN; + return parsed.toString(); + } + return url; + } catch { + return url; + } + } + + async createFormAuthConfig( + name: string, + authData: FormAuthData, + ): Promise { + if (!this.auth?.username || !this.auth?.password) { + throw new Error( + "BrowserStack credentials are not set for AccessibilityAuthConfig.", + ); + } + + const transformedAuthData = { + ...authData, + url: this.transformLocalUrl(authData.url), + }; + + const requestBody = { + name, + type: "form", + authData: transformedAuthData, + }; + + try { + const response = await apiClient.post({ + url: "https://api-accessibility.browserstack.com/api/website-scanner/v1/auth_configs", + headers: { + Authorization: + "Basic " + + Buffer.from(`${this.auth.username}:${this.auth.password}`).toString( + "base64", + ), + "Content-Type": "application/json", + }, + body: requestBody, + }); + + const data = response.data; + logger.info(`The data returned from the API is: ${JSON.stringify(data)}`); + if (!data.success) { + throw new Error( + `Unable to create auth config: ${data.errors?.join(", ")}`, + ); + } + return data; + } catch (err: any) { + logger.error( + `Error creating form auth config: ${JSON.stringify(err?.response?.data)}`, + ); + const msg = + err?.response?.data?.error || + err?.response?.data?.message || + err?.message || + String(err); + throw new Error(`Failed to create form auth config: ${msg}`); + } + } + + async createBasicAuthConfig( + name: string, + authData: BasicAuthData, + ): Promise { + if (!this.auth?.username || !this.auth?.password) { + throw new Error( + "BrowserStack credentials are not set for AccessibilityAuthConfig.", + ); + } + + const transformedAuthData = { + ...authData, + url: this.transformLocalUrl(authData.url), + }; + + const requestBody = { + name, + type: "basic", + authData: transformedAuthData, + }; + + try { + const response = await apiClient.post({ + url: "https://api-accessibility.browserstack.com/api/website-scanner/v1/auth_configs", + 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 create auth config: ${data.errors?.join(", ")}`, + ); + } + return data; + } catch (err: any) { + const msg = + err?.response?.data?.error || + err?.response?.data?.message || + err?.message || + String(err); + throw new Error(`Failed to create basic auth config: ${msg}`); + } + } + + async getAuthConfig(configId: number): Promise { + if (!this.auth?.username || !this.auth?.password) { + throw new Error( + "BrowserStack credentials are not set for AccessibilityAuthConfig.", + ); + } + + try { + const response = await apiClient.get({ + url: `https://api-accessibility.browserstack.com/api/website-scanner/v1/auth_configs/${configId}`, + headers: { + Authorization: + "Basic " + + Buffer.from(`${this.auth.username}:${this.auth.password}`).toString( + "base64", + ), + }, + }); + + const data = response.data; + if (!data.success) { + throw new Error( + `Unable to get auth config: ${data.errors?.join(", ")}`, + ); + } + return data; + } catch (err: any) { + const msg = + err?.response?.data?.error || + err?.response?.data?.message || + err?.message || + String(err); + throw new Error(`Failed to get auth config: ${msg}`); + } + } +} diff --git a/src/tools/accessiblity-utils/scanner.ts b/src/tools/accessiblity-utils/scanner.ts index 640da0b8..f5e9c12f 100644 --- a/src/tools/accessiblity-utils/scanner.ts +++ b/src/tools/accessiblity-utils/scanner.ts @@ -30,6 +30,7 @@ export class AccessibilityScanner { async startScan( name: string, urlList: string[], + authConfigId?: number, ): Promise { if (!this.auth?.username || !this.auth?.password) { throw new Error( @@ -82,6 +83,7 @@ export class AccessibilityScanner { name, urlList: transformedUrlList, recurring: false, + ...(authConfigId && { authConfigId }), }; let requestBody = baseRequestBody; From 8711b52bc95afde9b2c7acddd735d2532950f8e9 Mon Sep 17 00:00:00 2001 From: Ruturaj-Browserstack Date: Tue, 19 Aug 2025 17:49:30 +0530 Subject: [PATCH 2/6] refactor: replace trackMCPSuccess function calls with direct trackMCP invocations --- src/tools/accessibility.ts | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/src/tools/accessibility.ts b/src/tools/accessibility.ts index ddfb303b..3f2afb60 100644 --- a/src/tools/accessibility.ts +++ b/src/tools/accessibility.ts @@ -119,13 +119,6 @@ async function initializeReportFetcher( return reportFetcher; } -function trackMCPSuccess( - toolName: string, - server: McpServer, - config: BrowserStackConfig, -): void { - trackMCP(toolName, server.server.getClientVersion()!, undefined, config); -} async function executeAccessibilityRAG( args: { query: string }, @@ -133,7 +126,7 @@ async function executeAccessibilityRAG( config: BrowserStackConfig, ): Promise { try { - trackMCPSuccess("accessibilityExpert", server, config); + trackMCP("accessibilityExpert", server.server.getClientVersion()!, undefined, config); return await queryAccessibilityRAG(args.query, config); } catch (error) { return handleMCPError("accessibilityExpert", server, config, error); @@ -147,7 +140,7 @@ async function executeAccessibilityScan( config: BrowserStackConfig, ): Promise { try { - trackMCPSuccess("startAccessibilityScan", server, config); + trackMCP("startAccessibilityScan", server.server.getClientVersion()!, undefined, config); return await runAccessibilityScan( args.name, args.pageURL, @@ -209,7 +202,7 @@ async function executeCreateAuthConfig( config: BrowserStackConfig, ): Promise { try { - trackMCPSuccess("createAccessibilityAuthConfig", server, config); + trackMCP("createAccessibilityAuthConfig", server.server.getClientVersion()!, undefined, config); logger.info(`Creating auth config: ${JSON.stringify(args)}`); const result = await createAuthConfig(args, config); @@ -240,7 +233,7 @@ async function executeGetAuthConfig( config: BrowserStackConfig, ): Promise { try { - trackMCPSuccess("getAccessibilityAuthConfig", server, config); + trackMCP("getAccessibilityAuthConfig", server.server.getClientVersion()!, undefined, config); const authConfig = new AccessibilityAuthConfig(); const auth = setupAuth(config); From 4a51c678907a90090c17c69551dac85153a7f7b7 Mon Sep 17 00:00:00 2001 From: Ruturaj-Browserstack Date: Tue, 19 Aug 2025 17:50:09 +0530 Subject: [PATCH 3/6] refactor: format trackMCP function calls for improved readability --- src/tools/accessibility.ts | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/src/tools/accessibility.ts b/src/tools/accessibility.ts index 3f2afb60..d57728b7 100644 --- a/src/tools/accessibility.ts +++ b/src/tools/accessibility.ts @@ -119,14 +119,18 @@ async function initializeReportFetcher( return reportFetcher; } - async function executeAccessibilityRAG( args: { query: string }, server: McpServer, config: BrowserStackConfig, ): Promise { try { - trackMCP("accessibilityExpert", server.server.getClientVersion()!, undefined, config); + trackMCP( + "accessibilityExpert", + server.server.getClientVersion()!, + undefined, + config, + ); return await queryAccessibilityRAG(args.query, config); } catch (error) { return handleMCPError("accessibilityExpert", server, config, error); @@ -140,7 +144,12 @@ async function executeAccessibilityScan( config: BrowserStackConfig, ): Promise { try { - trackMCP("startAccessibilityScan", server.server.getClientVersion()!, undefined, config); + trackMCP( + "startAccessibilityScan", + server.server.getClientVersion()!, + undefined, + config, + ); return await runAccessibilityScan( args.name, args.pageURL, @@ -202,7 +211,12 @@ async function executeCreateAuthConfig( config: BrowserStackConfig, ): Promise { try { - trackMCP("createAccessibilityAuthConfig", server.server.getClientVersion()!, undefined, config); + trackMCP( + "createAccessibilityAuthConfig", + server.server.getClientVersion()!, + undefined, + config, + ); logger.info(`Creating auth config: ${JSON.stringify(args)}`); const result = await createAuthConfig(args, config); @@ -233,7 +247,12 @@ async function executeGetAuthConfig( config: BrowserStackConfig, ): Promise { try { - trackMCP("getAccessibilityAuthConfig", server.server.getClientVersion()!, undefined, config); + trackMCP( + "getAccessibilityAuthConfig", + server.server.getClientVersion()!, + undefined, + config, + ); const authConfig = new AccessibilityAuthConfig(); const auth = setupAuth(config); From 40588dde9a716ae1d04aeeb8e6c7fb7672916b0a Mon Sep 17 00:00:00 2001 From: Ruturaj-Browserstack Date: Wed, 20 Aug 2025 15:59:10 +0530 Subject: [PATCH 4/6] feat: add fetchAccessibilityIssues function with pagination support --- src/tools/accessibility.ts | 111 +++++++++++++++++++++++++++++++++++-- 1 file changed, 107 insertions(+), 4 deletions(-) diff --git a/src/tools/accessibility.ts b/src/tools/accessibility.ts index d57728b7..5b9358a6 100644 --- a/src/tools/accessibility.ts +++ b/src/tools/accessibility.ts @@ -137,6 +137,64 @@ async function executeAccessibilityRAG( } } +async function executeFetchAccessibilityIssues( + args: { scanId: string; scanRunId: string; nextPage?: number }, + server: McpServer, + config: BrowserStackConfig, +): Promise { + try { + trackMCP( + "fetchAccessibilityIssues", + server.server.getClientVersion()!, + undefined, + config, + ); + return await fetchAccessibilityIssues( + args.scanId, + args.scanRunId, + config, + args.nextPage, + ); + } catch (error) { + return handleMCPError("fetchAccessibilityIssues", server, config, error); + } +} + +async function fetchAccessibilityIssues( + scanId: string, + scanRunId: string, + config: BrowserStackConfig, + nextPage = 0, +): Promise { + const reportFetcher = await initializeReportFetcher(config); + const reportLink = await reportFetcher.getReportLink(scanId, scanRunId); + + const { records, page_length, total_issues, next_page } = + await parseAccessibilityReportFromCSV(reportLink, { nextPage }); + + const currentlyShown = + nextPage === 0 + ? page_length + : Math.floor(nextPage / 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 nextPage: ${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, @@ -283,12 +341,26 @@ function createScanSuccessResponse( totalIssues: number, pageLength: number, records: any[], + scanId: string, + scanRunId: string, + reportUrl: string, + nextPage: number | null, ): CallToolResult { - return createSuccessResponse([ + 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 (nextPage !== null) { + messages.push( + `📄 More issues available. Use fetchAccessibilityIssues tool with scanId: "${scanId}", scanRunId: "${scanRunId}", and nextPage: ${nextPage} to get the next batch.`, + ); + } + + return createSuccessResponse(messages); } async function runAccessibilityScan( @@ -314,10 +386,19 @@ async function runAccessibilityScan( const reportFetcher = await initializeReportFetcher(config); const reportLink = await reportFetcher.getReportLink(scanId, scanRunId); - const { records, page_length, total_issues } = + const { records, page_length, total_issues, next_page } = await parseAccessibilityReportFromCSV(reportLink); - return createScanSuccessResponse(name, total_issues, page_length, records); + return createScanSuccessResponse( + name, + total_issues, + page_length, + records, + scanId, + scanRunId, + reportLink, + next_page, + ); } export default function addAccessibilityTools( @@ -403,5 +484,27 @@ export default function addAccessibilityTools( }, ); + tools.fetchAccessibilityIssues = server.tool( + "fetchAccessibilityIssues", + "Fetch accessibility issues from a completed scan with pagination support. Use nextPage 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"), + nextPage: z + .number() + .optional() + .describe( + "Character offset for pagination (default: 0, use 1 for first page after initial scan results)", + ), + }, + async (args) => { + return await executeFetchAccessibilityIssues(args, server, config); + }, + ); + return tools; } From 8287585d9406e04675fdf9903c75b42e8a7fcde7 Mon Sep 17 00:00:00 2001 From: Ruturaj-Browserstack Date: Wed, 20 Aug 2025 16:15:28 +0530 Subject: [PATCH 5/6] refactor: update fetchAccessibilityIssues function to use cursor instead of nextPage for pagination --- src/tools/accessibility.ts | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/tools/accessibility.ts b/src/tools/accessibility.ts index 5b9358a6..6acd96b7 100644 --- a/src/tools/accessibility.ts +++ b/src/tools/accessibility.ts @@ -138,7 +138,7 @@ async function executeAccessibilityRAG( } async function executeFetchAccessibilityIssues( - args: { scanId: string; scanRunId: string; nextPage?: number }, + args: { scanId: string; scanRunId: string; cursor?: number }, server: McpServer, config: BrowserStackConfig, ): Promise { @@ -153,7 +153,7 @@ async function executeFetchAccessibilityIssues( args.scanId, args.scanRunId, config, - args.nextPage, + args.cursor, ); } catch (error) { return handleMCPError("fetchAccessibilityIssues", server, config, error); @@ -164,29 +164,29 @@ async function fetchAccessibilityIssues( scanId: string, scanRunId: string, config: BrowserStackConfig, - nextPage = 0, + cursor = 0, ): Promise { const reportFetcher = await initializeReportFetcher(config); const reportLink = await reportFetcher.getReportLink(scanId, scanRunId); const { records, page_length, total_issues, next_page } = - await parseAccessibilityReportFromCSV(reportLink, { nextPage }); + await parseAccessibilityReportFromCSV(reportLink, { nextPage: cursor }); const currentlyShown = - nextPage === 0 + cursor === 0 ? page_length - : Math.floor(nextPage / JSON.stringify(records[0] || {}).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})`, + `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 nextPage: ${next_page} to get the next batch.`, + `${remainingIssues} more issues available. Use fetchAccessibilityIssues with cursor: ${next_page} to get the next batch.`, ); } else { messages.push(`✅ All issues retrieved.`); @@ -344,19 +344,19 @@ function createScanSuccessResponse( scanId: string, scanRunId: string, reportUrl: string, - nextPage: number | null, + 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}].`, + `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 (nextPage !== null) { + if (cursor !== null) { messages.push( - `📄 More issues available. Use fetchAccessibilityIssues tool with scanId: "${scanId}", scanRunId: "${scanRunId}", and nextPage: ${nextPage} to get the next batch.`, + `More issues available. Use fetchAccessibilityIssues tool with scanId: "${scanId}", scanRunId: "${scanRunId}", and cursor: ${cursor} to get the next batch.`, ); } @@ -486,7 +486,7 @@ export default function addAccessibilityTools( tools.fetchAccessibilityIssues = server.tool( "fetchAccessibilityIssues", - "Fetch accessibility issues from a completed scan with pagination support. Use nextPage parameter to get subsequent pages of results.", + "Fetch accessibility issues from a completed scan with pagination support. Use cursor parameter to get subsequent pages of results.", { scanId: z .string() @@ -494,7 +494,7 @@ export default function addAccessibilityTools( scanRunId: z .string() .describe("The scan run ID from a completed accessibility scan"), - nextPage: z + cursor: z .number() .optional() .describe( From 7b6ddbf14be671e979d48b467269591f1b1365f4 Mon Sep 17 00:00:00 2001 From: Ruturaj-Browserstack Date: Wed, 20 Aug 2025 16:27:50 +0530 Subject: [PATCH 6/6] refactor: simplify pagination description in addAccessibilityTools function --- src/tools/accessibility.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/tools/accessibility.ts b/src/tools/accessibility.ts index 6acd96b7..5f881b12 100644 --- a/src/tools/accessibility.ts +++ b/src/tools/accessibility.ts @@ -497,9 +497,7 @@ export default function addAccessibilityTools( cursor: z .number() .optional() - .describe( - "Character offset for pagination (default: 0, use 1 for first page after initial scan results)", - ), + .describe("Character offset for pagination (default: 0)"), }, async (args) => { return await executeFetchAccessibilityIssues(args, server, config);