@browserstack/mcp-server 1.0.14 → 1.0.15

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.
package/dist/index.js CHANGED
@@ -6,7 +6,6 @@ import "dotenv/config";
6
6
  import logger from "./logger.js";
7
7
  import addSDKTools from "./tools/bstack-sdk.js";
8
8
  import addAppLiveTools from "./tools/applive.js";
9
- import addObservabilityTools from "./tools/observability.js";
10
9
  import addBrowserLiveTools from "./tools/live.js";
11
10
  import addAccessibilityTools from "./tools/accessibility.js";
12
11
  import addTestManagementTools from "./tools/testmanagement.js";
@@ -18,7 +17,6 @@ function registerTools(server) {
18
17
  addSDKTools(server);
19
18
  addAppLiveTools(server);
20
19
  addBrowserLiveTools(server);
21
- addObservabilityTools(server);
22
20
  addAccessibilityTools(server);
23
21
  addTestManagementTools(server);
24
22
  addAppAutomationTools(server);
@@ -1,30 +1,63 @@
1
1
  import { z } from "zod";
2
- import { startAccessibilityScan, } from "./accessiblity-utils/accessibility.js";
2
+ import { AccessibilityScanner } from "./accessiblity-utils/scanner.js";
3
+ import { AccessibilityReportFetcher } from "./accessiblity-utils/report-fetcher.js";
3
4
  import { trackMCP } from "../lib/instrumentation.js";
4
- async function runAccessibilityScan(name, pageURL) {
5
- const response = await startAccessibilityScan(name, [pageURL]);
6
- const scanId = response.data?.id;
7
- const scanRunId = response.data?.scanRunId;
8
- if (!scanId || !scanRunId) {
9
- throw new Error("Unable to start a accessibility scan, please try again later or open an issue on GitHub if the problem persists");
5
+ import { parseAccessibilityReportFromCSV } from "./accessiblity-utils/report-parser.js";
6
+ const scanner = new AccessibilityScanner();
7
+ const reportFetcher = new AccessibilityReportFetcher();
8
+ async function runAccessibilityScan(name, pageURL, context) {
9
+ // Start scan
10
+ const startResp = await scanner.startScan(name, [pageURL]);
11
+ const scanId = startResp.data.id;
12
+ const scanRunId = startResp.data.scanRunId;
13
+ // Notify scan start
14
+ await context.sendNotification({
15
+ method: "notifications/progress",
16
+ params: {
17
+ progressToken: context._meta?.progressToken ?? "NOT_FOUND",
18
+ message: `Accessibility scan "${name}" started`,
19
+ progress: 0,
20
+ total: 100,
21
+ },
22
+ });
23
+ // Wait until scan completes
24
+ const status = await scanner.waitUntilComplete(scanId, scanRunId, context);
25
+ if (status !== "completed") {
26
+ return {
27
+ content: [
28
+ {
29
+ type: "text",
30
+ text: `❌ Accessibility scan "${name}" failed with status: ${status} , check the BrowserStack dashboard for more details [https://scanner.browserstack.com/site-scanner/scan-details/${name}].`,
31
+ isError: true,
32
+ },
33
+ ],
34
+ isError: true,
35
+ };
10
36
  }
37
+ // Fetch CSV report link
38
+ const reportLink = await reportFetcher.getReportLink(scanId, scanRunId);
39
+ const { records } = await parseAccessibilityReportFromCSV(reportLink);
11
40
  return {
12
41
  content: [
13
42
  {
14
43
  type: "text",
15
- text: `Successfully queued accessibility scan, you will get a report via email within 5 minutes.`,
44
+ text: `✅ Accessibility scan "${name}" completed. check the BrowserStack dashboard for more details [https://scanner.browserstack.com/site-scanner/scan-details/${name}].`,
45
+ },
46
+ {
47
+ type: "text",
48
+ text: `Scan results: ${JSON.stringify(records, null, 2)}`,
16
49
  },
17
50
  ],
18
51
  };
19
52
  }
20
53
  export default function addAccessibilityTools(server) {
21
- server.tool("startAccessibilityScan", "Use this tool to start an accessibility scan for a list of URLs on BrowserStack.", {
54
+ server.tool("startAccessibilityScan", "Start an accessibility scan via BrowserStack and retrieve a local CSV report path.", {
22
55
  name: z.string().describe("Name of the accessibility scan"),
23
56
  pageURL: z.string().describe("The URL to scan for accessibility issues"),
24
- }, async (args) => {
57
+ }, async (args, context) => {
25
58
  try {
26
59
  trackMCP("startAccessibilityScan", server.server.getClientVersion());
27
- return await runAccessibilityScan(args.name, args.pageURL);
60
+ return await runAccessibilityScan(args.name, args.pageURL, context);
28
61
  }
29
62
  catch (error) {
30
63
  trackMCP("startAccessibilityScan", server.server.getClientVersion(), error);
@@ -0,0 +1,28 @@
1
+ import axios from "axios";
2
+ import config from "../../config.js";
3
+ export class AccessibilityReportFetcher {
4
+ auth = {
5
+ username: config.browserstackUsername,
6
+ password: config.browserstackAccessKey,
7
+ };
8
+ async getReportLink(scanId, scanRunId) {
9
+ // Initiate CSV link generation
10
+ const initUrl = `https://api-accessibility.browserstack.com/api/website-scanner/v1/scans/${scanId}/scan_runs/issues?scan_run_id=${scanRunId}`;
11
+ const initResp = await axios.get(initUrl, {
12
+ auth: this.auth,
13
+ });
14
+ if (!initResp.data.success) {
15
+ throw new Error(`Failed to initiate report: ${initResp.data.error || initResp.data.data.message}`);
16
+ }
17
+ const taskId = initResp.data.data.task_id;
18
+ // Fetch the generated CSV link
19
+ const reportUrl = `https://api-accessibility.browserstack.com/api/website-scanner/v1/scans/${scanId}/scan_runs/issues?task_id=${encodeURIComponent(taskId)}`;
20
+ const reportResp = await axios.get(reportUrl, {
21
+ auth: this.auth,
22
+ });
23
+ if (!reportResp.data.success) {
24
+ throw new Error(`Failed to fetch report: ${reportResp.data.error}`);
25
+ }
26
+ return reportResp.data.data.reportLink;
27
+ }
28
+ }
@@ -0,0 +1,51 @@
1
+ import fetch from "node-fetch";
2
+ import { parse } from "csv-parse/sync";
3
+ export async function parseAccessibilityReportFromCSV(reportLink, { maxCharacterLength = 10_000, nextPage = 0 } = {}) {
4
+ // 1) Download & parse
5
+ const res = await fetch(reportLink);
6
+ if (!res.ok)
7
+ throw new Error(`Failed to download report: ${res.statusText}`);
8
+ const text = await res.text();
9
+ const all = parse(text, {
10
+ columns: true,
11
+ skip_empty_lines: true,
12
+ }).map((row) => ({
13
+ issue_type: row["Issue type"],
14
+ component: row["Component"],
15
+ issue_description: row["Issue description"],
16
+ HTML_snippet: row["HTML snippet"],
17
+ how_to_fix: row["How to fix this issue"],
18
+ severity: (row["Severity"] || "unknown").trim(),
19
+ }));
20
+ // 2) Sort by severity
21
+ const order = {
22
+ critical: 0,
23
+ serious: 1,
24
+ moderate: 2,
25
+ minor: 3,
26
+ };
27
+ all.sort((a, b) => (order[a.severity] ?? 99) - (order[b.severity] ?? 99));
28
+ // 3) Walk to the starting offset
29
+ let charCursor = 0;
30
+ let idx = 0;
31
+ for (; idx < all.length; idx++) {
32
+ const len = JSON.stringify(all[idx]).length;
33
+ if (charCursor + len > nextPage)
34
+ break;
35
+ charCursor += len;
36
+ }
37
+ // 4) Collect up to maxCharacterLength
38
+ const page = [];
39
+ for (let i = idx; i < all.length; i++) {
40
+ const recStr = JSON.stringify(all[i]);
41
+ if (charCursor - nextPage + recStr.length > maxCharacterLength)
42
+ break;
43
+ page.push(all[i]);
44
+ charCursor += recStr.length;
45
+ }
46
+ const hasMore = idx + page.length < all.length;
47
+ return {
48
+ records: page,
49
+ next_page: hasMore ? charCursor : null,
50
+ };
51
+ }
@@ -0,0 +1,80 @@
1
+ import axios from "axios";
2
+ import config from "../../config.js";
3
+ export class AccessibilityScanner {
4
+ auth = {
5
+ username: config.browserstackUsername,
6
+ password: config.browserstackAccessKey,
7
+ };
8
+ async startScan(name, urlList) {
9
+ try {
10
+ const { data } = await axios.post("https://api-accessibility.browserstack.com/api/website-scanner/v1/scans", { name, urlList, recurring: false }, { auth: this.auth });
11
+ if (!data.success)
12
+ throw new Error(`Unable to start scan: ${data.errors?.join(", ")}`);
13
+ return data;
14
+ }
15
+ catch (err) {
16
+ if (axios.isAxiosError(err) && err.response?.data) {
17
+ const msg = err.response.data.error ||
18
+ err.response.data.message ||
19
+ err.message;
20
+ throw new Error(`Failed to start scan: ${msg}`);
21
+ }
22
+ throw err;
23
+ }
24
+ }
25
+ async pollStatus(scanId, scanRunId) {
26
+ try {
27
+ const { data } = await axios.get(`https://api-accessibility.browserstack.com/api/website-scanner/v1/scans/${scanId}/scan_runs/${scanRunId}/status`, { auth: this.auth });
28
+ if (!data.success)
29
+ throw new Error(`Failed to get status: ${data.errors?.join(", ")}`);
30
+ return data;
31
+ }
32
+ catch (err) {
33
+ if (axios.isAxiosError(err) && err.response?.data) {
34
+ const msg = err.response.data.message || err.message;
35
+ throw new Error(`Failed to get scan status: ${msg}`);
36
+ }
37
+ throw err;
38
+ }
39
+ }
40
+ async waitUntilComplete(scanId, scanRunId, context) {
41
+ return new Promise((resolve, reject) => {
42
+ let timepercent = 0;
43
+ let dotCount = 1;
44
+ const interval = setInterval(async () => {
45
+ try {
46
+ const statusResp = await this.pollStatus(scanId, scanRunId);
47
+ const status = statusResp.data.status;
48
+ timepercent += 1.67;
49
+ const progress = status === "completed" ? 100 : timepercent;
50
+ const dots = ".".repeat(dotCount);
51
+ dotCount = (dotCount % 4) + 1;
52
+ const message = status === "completed" || status === "failed"
53
+ ? `Scan completed with status: ${status}`
54
+ : `Scan in progress${dots}`;
55
+ await context.sendNotification({
56
+ method: "notifications/progress",
57
+ params: {
58
+ progressToken: context._meta?.progressToken ?? "NOT_FOUND",
59
+ message: message,
60
+ progress: progress,
61
+ total: 100,
62
+ },
63
+ });
64
+ if (status === "completed" || status === "failed") {
65
+ clearInterval(interval);
66
+ resolve(status);
67
+ }
68
+ }
69
+ catch (e) {
70
+ clearInterval(interval);
71
+ reject(e);
72
+ }
73
+ }, 5000);
74
+ setTimeout(() => {
75
+ clearInterval(interval);
76
+ reject(new Error("Scan timed out after 5 minutes"));
77
+ }, 5 * 60 * 1000);
78
+ });
79
+ }
80
+ }
@@ -175,13 +175,13 @@ export async function addTestResultTool(args) {
175
175
  /**
176
176
  * Uploads files such as PDRs or screenshots to BrowserStack Test Management and get file mapping ID back.
177
177
  */
178
- export async function uploadFileTestManagementTool(args) {
178
+ export async function uploadProductRequirementFileTool(args) {
179
179
  try {
180
- trackMCP("uploadFileTestManagement", serverInstance.server.getClientVersion());
180
+ trackMCP("uploadProductRequirementFile", serverInstance.server.getClientVersion());
181
181
  return await uploadFile(args);
182
182
  }
183
183
  catch (err) {
184
- trackMCP("uploadFileTestManagement", serverInstance.server.getClientVersion(), err);
184
+ trackMCP("uploadProductRequirementFile", serverInstance.server.getClientVersion(), err);
185
185
  return {
186
186
  content: [
187
187
  {
@@ -228,6 +228,6 @@ export default function addTestManagementTools(server) {
228
228
  server.tool("listTestRuns", "List test runs in a project with optional filters (date ranges, assignee, state, etc.)", ListTestRunsSchema.shape, listTestRunsTool);
229
229
  server.tool("updateTestRun", "Update a test run in BrowserStack Test Management.", UpdateTestRunSchema.shape, updateTestRunTool);
230
230
  server.tool("addTestResult", "Add a test result to a specific test run via BrowserStack Test Management API.", AddTestResultSchema.shape, addTestResultTool);
231
- server.tool("uploadFileTestManagement", "Upload files such as PDRs or PDFs to BrowserStack Test Management and get file mapping ID back. Its Used for generating test cases from file.", UploadFileSchema.shape, uploadFileTestManagementTool);
231
+ server.tool("uploadProductRequirementFile", "Upload files such as PDRs or PDFs to BrowserStack Test Management and get file mapping ID back. Its Used for generating test cases from file.", UploadFileSchema.shape, uploadProductRequirementFileTool);
232
232
  server.tool("createTestCasesFromFile", "Create test cases from a file in BrowserStack Test Management.", CreateTestCasesFromFileSchema.shape, createTestCasesFromFileTool);
233
233
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@browserstack/mcp-server",
3
- "version": "1.0.14",
3
+ "version": "1.0.15",
4
4
  "description": "BrowserStack's Official MCP Server",
5
5
  "main": "dist/index.js",
6
6
  "repository": {
@@ -34,10 +34,11 @@
34
34
  "author": "",
35
35
  "license": "ISC",
36
36
  "dependencies": {
37
- "@modelcontextprotocol/sdk": "^1.10.1",
37
+ "@modelcontextprotocol/sdk": "^1.11.4",
38
38
  "@types/form-data": "^2.5.2",
39
39
  "axios": "^1.8.4",
40
40
  "browserstack-local": "^1.5.6",
41
+ "csv-parse": "^5.6.0",
41
42
  "dotenv": "^16.5.0",
42
43
  "form-data": "^4.0.2",
43
44
  "pino": "^9.6.0",
@@ -49,6 +50,7 @@
49
50
  },
50
51
  "devDependencies": {
51
52
  "@eslint/js": "^9.25.0",
53
+ "@types/csv-parse": "^1.1.12",
52
54
  "@types/node": "^22.14.1",
53
55
  "@types/uuid": "^10.0.0",
54
56
  "eslint": "^9.25.0",
@@ -1,74 +0,0 @@
1
- import axios from "axios";
2
- import config from "../../config.js";
3
- import { AxiosError } from "axios";
4
- export async function startAccessibilityScan(name, urlList) {
5
- try {
6
- const response = await axios.post("https://api-accessibility.browserstack.com/api/website-scanner/v1/scans", {
7
- name,
8
- urlList,
9
- recurring: false,
10
- }, {
11
- auth: {
12
- username: config.browserstackUsername,
13
- password: config.browserstackAccessKey,
14
- },
15
- });
16
- if (!response.data.success) {
17
- throw new Error(`Unable to create an accessibility scan: ${response.data.errors?.join(", ")}`);
18
- }
19
- return response.data;
20
- }
21
- catch (error) {
22
- if (error instanceof AxiosError) {
23
- if (error.response?.data?.error) {
24
- throw new Error(`Failed to start accessibility scan: ${error.response?.data?.error}`);
25
- }
26
- else {
27
- throw new Error(`Failed to start accessibility scan: ${error.response?.data?.message || error.message}`);
28
- }
29
- }
30
- throw error;
31
- }
32
- }
33
- export async function pollScanStatus(scanId, scanRunId) {
34
- try {
35
- const response = await axios.get(`https://api-accessibility.browserstack.com/api/website-scanner/v1/scans/${scanId}/scan_runs/${scanRunId}/status`, {
36
- auth: {
37
- username: config.browserstackUsername,
38
- password: config.browserstackAccessKey,
39
- },
40
- });
41
- if (!response.data.success) {
42
- throw new Error(`Failed to get scan status: ${response.data.errors?.join(", ")}`);
43
- }
44
- return response.data.data?.status || "unknown";
45
- }
46
- catch (error) {
47
- if (error instanceof AxiosError) {
48
- throw new Error(`Failed to get scan status: ${error.response?.data?.message || error.message}`);
49
- }
50
- throw error;
51
- }
52
- }
53
- export async function waitUntilScanComplete(scanId, scanRunId) {
54
- return new Promise((resolve, reject) => {
55
- const interval = setInterval(async () => {
56
- try {
57
- const status = await pollScanStatus(scanId, scanRunId);
58
- if (status === "completed") {
59
- clearInterval(interval);
60
- resolve();
61
- }
62
- }
63
- catch (error) {
64
- clearInterval(interval);
65
- reject(error);
66
- }
67
- }, 5000); // Poll every 5 seconds
68
- // Set a timeout of 5 minutes
69
- setTimeout(() => {
70
- clearInterval(interval);
71
- reject(new Error("Scan timed out after 5 minutes"));
72
- }, 5 * 60 * 1000);
73
- });
74
- }