@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 +0 -2
- package/dist/tools/accessibility.js +44 -11
- package/dist/tools/accessiblity-utils/report-fetcher.js +28 -0
- package/dist/tools/accessiblity-utils/report-parser.js +51 -0
- package/dist/tools/accessiblity-utils/scanner.js +80 -0
- package/dist/tools/testmanagement.js +4 -4
- package/package.json +4 -2
- package/dist/tools/accessiblity-utils/accessibility.js +0 -74
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 {
|
|
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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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:
|
|
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", "
|
|
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
|
|
178
|
+
export async function uploadProductRequirementFileTool(args) {
|
|
179
179
|
try {
|
|
180
|
-
trackMCP("
|
|
180
|
+
trackMCP("uploadProductRequirementFile", serverInstance.server.getClientVersion());
|
|
181
181
|
return await uploadFile(args);
|
|
182
182
|
}
|
|
183
183
|
catch (err) {
|
|
184
|
-
trackMCP("
|
|
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("
|
|
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.
|
|
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.
|
|
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
|
-
}
|