@browserstack/mcp-server 1.1.3 → 1.1.5
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/lib/local.js +9 -5
- package/dist/tools/accessiblity-utils/report-parser.js +1 -1
- package/dist/tools/accessiblity-utils/scanner.js +43 -1
- package/dist/tools/applive-utils/device-search.js +22 -0
- package/dist/tools/applive-utils/start-session.js +37 -110
- package/dist/tools/applive-utils/types.js +1 -0
- package/dist/tools/applive-utils/version-utils.js +13 -0
- package/dist/tools/live-utils/desktop-filter.js +1 -1
- package/dist/tools/live-utils/mobile-filter.js +1 -1
- package/package.json +1 -1
- package/dist/tools/applive-utils/fuzzy-search.js +0 -8
- /package/dist/{tools/live-utils → lib}/version-resolver.js +0 -0
package/dist/lib/local.js
CHANGED
|
@@ -65,15 +65,19 @@ export async function killExistingBrowserStackLocalProcesses() {
|
|
|
65
65
|
// Continue execution as there may not be any processes running
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
|
-
export async function ensureLocalBinarySetup() {
|
|
68
|
+
export async function ensureLocalBinarySetup(localIdentifier) {
|
|
69
69
|
logger.info("Ensuring local binary setup as it is required for private URLs...");
|
|
70
70
|
const localBinary = new Local();
|
|
71
71
|
await killExistingBrowserStackLocalProcesses();
|
|
72
|
+
const requestBody = {
|
|
73
|
+
key: config.browserstackAccessKey,
|
|
74
|
+
username: config.browserstackUsername,
|
|
75
|
+
};
|
|
76
|
+
if (localIdentifier) {
|
|
77
|
+
requestBody.localIdentifier = localIdentifier;
|
|
78
|
+
}
|
|
72
79
|
return await new Promise((resolve, reject) => {
|
|
73
|
-
localBinary.start({
|
|
74
|
-
key: config.browserstackAccessKey,
|
|
75
|
-
username: config.browserstackUsername,
|
|
76
|
-
}, (error) => {
|
|
80
|
+
localBinary.start(requestBody, (error) => {
|
|
77
81
|
if (error) {
|
|
78
82
|
logger.error(`Unable to start BrowserStack Local... please check your credentials and try again. Error: ${error}`);
|
|
79
83
|
reject(new Error(`Unable to configure local tunnel binary, please check your credentials and try again. Error: ${error}`));
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fetch from "node-fetch";
|
|
2
2
|
import { parse } from "csv-parse/sync";
|
|
3
|
-
export async function parseAccessibilityReportFromCSV(reportLink, { maxCharacterLength =
|
|
3
|
+
export async function parseAccessibilityReportFromCSV(reportLink, { maxCharacterLength = 5000, nextPage = 0 } = {}) {
|
|
4
4
|
// 1) Download & parse
|
|
5
5
|
const res = await fetch(reportLink);
|
|
6
6
|
if (!res.ok)
|
|
@@ -1,13 +1,55 @@
|
|
|
1
1
|
import axios from "axios";
|
|
2
2
|
import config from "../../config.js";
|
|
3
|
+
import logger from "../../logger.js";
|
|
4
|
+
import { isLocalURL, ensureLocalBinarySetup, killExistingBrowserStackLocalProcesses, } from "../../lib/local.js";
|
|
3
5
|
export class AccessibilityScanner {
|
|
4
6
|
auth = {
|
|
5
7
|
username: config.browserstackUsername,
|
|
6
8
|
password: config.browserstackAccessKey,
|
|
7
9
|
};
|
|
8
10
|
async startScan(name, urlList) {
|
|
11
|
+
// Check if any URL is local
|
|
12
|
+
const hasLocal = urlList.some(isLocalURL);
|
|
13
|
+
const localIdentifier = crypto.randomUUID();
|
|
14
|
+
const localHosts = new Set(["127.0.0.1", "localhost", "0.0.0.0"]);
|
|
15
|
+
const BS_LOCAL_DOMAIN = "bs-local.com";
|
|
16
|
+
if (hasLocal) {
|
|
17
|
+
await ensureLocalBinarySetup(localIdentifier);
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
await killExistingBrowserStackLocalProcesses();
|
|
21
|
+
}
|
|
22
|
+
const transformedUrlList = urlList.map((url) => {
|
|
23
|
+
try {
|
|
24
|
+
const parsed = new URL(url);
|
|
25
|
+
if (localHosts.has(parsed.hostname)) {
|
|
26
|
+
parsed.hostname = BS_LOCAL_DOMAIN;
|
|
27
|
+
return parsed.toString();
|
|
28
|
+
}
|
|
29
|
+
return url;
|
|
30
|
+
}
|
|
31
|
+
catch (e) {
|
|
32
|
+
logger.warn(`[AccessibilityScan] Invalid URL skipped: ${e}`);
|
|
33
|
+
return url;
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
const baseRequestBody = {
|
|
37
|
+
name,
|
|
38
|
+
urlList: transformedUrlList,
|
|
39
|
+
recurring: false,
|
|
40
|
+
};
|
|
41
|
+
let requestBody = baseRequestBody;
|
|
42
|
+
if (hasLocal) {
|
|
43
|
+
const localConfig = {
|
|
44
|
+
localTestingInfo: {
|
|
45
|
+
localIdentifier,
|
|
46
|
+
localEnabled: true,
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
requestBody = { ...baseRequestBody, ...localConfig };
|
|
50
|
+
}
|
|
9
51
|
try {
|
|
10
|
-
const { data } = await axios.post("https://api-accessibility.browserstack.com/api/website-scanner/v1/scans",
|
|
52
|
+
const { data } = await axios.post("https://api-accessibility.browserstack.com/api/website-scanner/v1/scans", requestBody, { auth: this.auth });
|
|
11
53
|
if (!data.success)
|
|
12
54
|
throw new Error(`Unable to start scan: ${data.errors?.join(", ")}`);
|
|
13
55
|
return data;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { customFuzzySearch } from "../../lib/fuzzy.js";
|
|
2
|
+
/**
|
|
3
|
+
* Find matching devices by name with exact match preference.
|
|
4
|
+
* Throws if none or multiple exact matches.
|
|
5
|
+
*/
|
|
6
|
+
export function findDeviceByName(devices, desiredPhone) {
|
|
7
|
+
const matches = customFuzzySearch(devices, ["display_name"], desiredPhone, 5);
|
|
8
|
+
if (matches.length === 0) {
|
|
9
|
+
const options = [...new Set(devices.map((d) => d.display_name))].join(", ");
|
|
10
|
+
throw new Error(`No devices matching "${desiredPhone}". Available devices: ${options}`);
|
|
11
|
+
}
|
|
12
|
+
// Exact-case-insensitive filter
|
|
13
|
+
const exact = matches.filter((m) => m.display_name.toLowerCase() === desiredPhone.toLowerCase());
|
|
14
|
+
if (exact)
|
|
15
|
+
return exact;
|
|
16
|
+
// If no exact but multiple fuzzy, ask user
|
|
17
|
+
if (matches.length > 1) {
|
|
18
|
+
const names = matches.map((d) => d.display_name).join(", ");
|
|
19
|
+
throw new Error(`Alternative Device/Device's found : ${names}. Please Select one.`);
|
|
20
|
+
}
|
|
21
|
+
return matches;
|
|
22
|
+
}
|
|
@@ -1,129 +1,56 @@
|
|
|
1
|
-
import childProcess from "child_process";
|
|
2
1
|
import logger from "../../logger.js";
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
2
|
+
import childProcess from "child_process";
|
|
3
|
+
import { getDevicesAndBrowsers, BrowserStackProducts, } from "../../lib/device-cache.js";
|
|
5
4
|
import { sanitizeUrlParam } from "../../lib/utils.js";
|
|
6
5
|
import { uploadApp } from "./upload-app.js";
|
|
6
|
+
import { findDeviceByName } from "./device-search.js";
|
|
7
|
+
import { pickVersion } from "./version-utils.js";
|
|
7
8
|
/**
|
|
8
|
-
*
|
|
9
|
-
* @param args - The arguments for starting the session.
|
|
10
|
-
* @returns The launch URL for the session.
|
|
11
|
-
* @throws Will throw an error if no devices are found or if the app URL is invalid.
|
|
9
|
+
* Start an App Live session: filter, select, upload, and open.
|
|
12
10
|
*/
|
|
13
11
|
export async function startSession(args) {
|
|
14
|
-
const { appPath, desiredPlatform, desiredPhone } = args;
|
|
15
|
-
|
|
12
|
+
const { appPath, desiredPlatform, desiredPhone, desiredPlatformVersion } = args;
|
|
13
|
+
// 1) Fetch devices for APP_LIVE
|
|
16
14
|
const data = await getDevicesAndBrowsers(BrowserStackProducts.APP_LIVE);
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const { app_url } = await uploadApp(appPath);
|
|
23
|
-
validateAppUrl(app_url);
|
|
24
|
-
const launchUrl = constructLaunchUrl(app_url, selectedDevice, desiredPlatform, desiredPlatformVersion);
|
|
25
|
-
openBrowser(launchUrl);
|
|
26
|
-
return launchUrl;
|
|
27
|
-
}
|
|
28
|
-
/**
|
|
29
|
-
* Resolves the platform version based on the desired platform and version.
|
|
30
|
-
* @param allDevices - The list of all devices.
|
|
31
|
-
* @param desiredPlatform - The desired platform (android or ios).
|
|
32
|
-
* @param desiredPlatformVersion - The desired platform version.
|
|
33
|
-
* @returns The resolved platform version.
|
|
34
|
-
* @throws Will throw an error if the platform version is not valid.
|
|
35
|
-
*/
|
|
36
|
-
function resolvePlatformVersion(allDevices, desiredPlatform, desiredPlatformVersion) {
|
|
37
|
-
if (desiredPlatformVersion === "latest" ||
|
|
38
|
-
desiredPlatformVersion === "oldest") {
|
|
39
|
-
const filtered = allDevices.filter((d) => d.os === desiredPlatform);
|
|
40
|
-
filtered.sort((a, b) => {
|
|
41
|
-
const versionA = parseFloat(a.os_version);
|
|
42
|
-
const versionB = parseFloat(b.os_version);
|
|
43
|
-
return desiredPlatformVersion === "latest"
|
|
44
|
-
? versionB - versionA
|
|
45
|
-
: versionA - versionB;
|
|
46
|
-
});
|
|
47
|
-
return filtered[0].os_version;
|
|
15
|
+
const all = data.mobile.flatMap((grp) => grp.devices.map((dev) => ({ ...dev, os: grp.os })));
|
|
16
|
+
// 2) Filter by OS
|
|
17
|
+
const osMatches = all.filter((d) => d.os === desiredPlatform);
|
|
18
|
+
if (!osMatches.length) {
|
|
19
|
+
throw new Error(`No devices for OS "${desiredPlatform}"`);
|
|
48
20
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
*/
|
|
59
|
-
function filterDevicesByPlatformAndVersion(allDevices, desiredPlatform, desiredPlatformVersion) {
|
|
60
|
-
return allDevices.filter((d) => {
|
|
61
|
-
if (d.os !== desiredPlatform)
|
|
62
|
-
return false;
|
|
63
|
-
try {
|
|
64
|
-
const versionA = parseFloat(d.os_version);
|
|
65
|
-
const versionB = parseFloat(desiredPlatformVersion);
|
|
66
|
-
return versionA === versionB;
|
|
67
|
-
}
|
|
68
|
-
catch {
|
|
69
|
-
return d.os_version === desiredPlatformVersion;
|
|
70
|
-
}
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
|
-
/**
|
|
74
|
-
* Validates the selected device and handles multiple matches.
|
|
75
|
-
* @param matches - The list of device matches.
|
|
76
|
-
* @param desiredPhone - The desired phone name.
|
|
77
|
-
* @param desiredPlatform - The desired platform (android or ios).
|
|
78
|
-
* @param desiredPlatformVersion - The desired platform version.
|
|
79
|
-
* @returns The selected device entry.
|
|
80
|
-
*/
|
|
81
|
-
function validateAndSelectDevice(matches, desiredPhone, desiredPlatform, desiredPlatformVersion) {
|
|
82
|
-
if (matches.length === 0) {
|
|
83
|
-
throw new Error(`No devices found matching "${desiredPhone}" for ${desiredPlatform} ${desiredPlatformVersion}`);
|
|
84
|
-
}
|
|
85
|
-
const exactMatch = matches.find((d) => d.display_name.toLowerCase() === desiredPhone.toLowerCase());
|
|
86
|
-
if (exactMatch) {
|
|
87
|
-
return exactMatch;
|
|
21
|
+
// 3) Select by name
|
|
22
|
+
const nameMatches = findDeviceByName(osMatches, desiredPhone);
|
|
23
|
+
// 4) Resolve version
|
|
24
|
+
const versions = [...new Set(nameMatches.map((d) => d.os_version))];
|
|
25
|
+
const version = pickVersion(versions, desiredPlatformVersion);
|
|
26
|
+
// 5) Final candidates for version
|
|
27
|
+
const final = nameMatches.filter((d) => d.os_version === version);
|
|
28
|
+
if (!final.length) {
|
|
29
|
+
throw new Error(`No devices for version "${version}" on ${desiredPlatform}`);
|
|
88
30
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
96
|
-
return matches[0];
|
|
97
|
-
}
|
|
98
|
-
/**
|
|
99
|
-
* Validates the app URL.
|
|
100
|
-
* @param appUrl - The app URL to validate.
|
|
101
|
-
* @throws Will throw an error if the app URL is not valid.
|
|
102
|
-
*/
|
|
103
|
-
function validateAppUrl(appUrl) {
|
|
104
|
-
if (!appUrl.match("bs://")) {
|
|
105
|
-
throw new Error("The app path is not a valid BrowserStack app URL.");
|
|
31
|
+
const selected = final[0];
|
|
32
|
+
let note = "";
|
|
33
|
+
if (version != desiredPlatformVersion &&
|
|
34
|
+
desiredPlatformVersion !== "latest" &&
|
|
35
|
+
desiredPlatformVersion !== "oldest") {
|
|
36
|
+
note = `\n Note: The requested version "${desiredPlatformVersion}" is not available. Using "${version}" instead.`;
|
|
106
37
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
* @param desiredPlatform - The desired platform (android or ios).
|
|
113
|
-
* @param desiredPlatformVersion - The desired platform version.
|
|
114
|
-
* @returns The constructed launch URL.
|
|
115
|
-
*/
|
|
116
|
-
function constructLaunchUrl(appUrl, device, desiredPlatform, desiredPlatformVersion) {
|
|
117
|
-
const deviceParam = sanitizeUrlParam(device.display_name.replace(/\s+/g, "+"));
|
|
38
|
+
// 6) Upload app
|
|
39
|
+
const { app_url } = await uploadApp(appPath);
|
|
40
|
+
logger.info(`App uploaded: ${app_url}`);
|
|
41
|
+
// 7) Build URL & open
|
|
42
|
+
const deviceParam = sanitizeUrlParam(selected.display_name.replace(/\s+/g, "+"));
|
|
118
43
|
const params = new URLSearchParams({
|
|
119
44
|
os: desiredPlatform,
|
|
120
|
-
os_version:
|
|
121
|
-
app_hashed_id:
|
|
45
|
+
os_version: version,
|
|
46
|
+
app_hashed_id: app_url.split("bs://").pop() || "",
|
|
122
47
|
scale_to_fit: "true",
|
|
123
48
|
speed: "1",
|
|
124
49
|
start: "true",
|
|
125
50
|
});
|
|
126
|
-
|
|
51
|
+
const launchUrl = `https://app-live.browserstack.com/dashboard#${params.toString()}&device=${deviceParam}`;
|
|
52
|
+
openBrowser(launchUrl);
|
|
53
|
+
return launchUrl + note;
|
|
127
54
|
}
|
|
128
55
|
/**
|
|
129
56
|
* Opens the launch URL in the default browser.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { resolveVersion } from "../../lib/version-resolver.js";
|
|
2
|
+
/**
|
|
3
|
+
* Resolve desired version against available list
|
|
4
|
+
*/
|
|
5
|
+
export function pickVersion(available, requested) {
|
|
6
|
+
try {
|
|
7
|
+
return resolveVersion(requested, available);
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
const opts = available.join(", ");
|
|
11
|
+
throw new Error(`Version "${requested}" not found. Available versions: ${opts}`);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getDevicesAndBrowsers, BrowserStackProducts, } from "../../lib/device-cache.js";
|
|
2
|
-
import { resolveVersion } from "
|
|
2
|
+
import { resolveVersion } from "../../lib/version-resolver.js";
|
|
3
3
|
import { customFuzzySearch } from "../../lib/fuzzy.js";
|
|
4
4
|
export async function filterDesktop(args) {
|
|
5
5
|
const data = await getDevicesAndBrowsers(BrowserStackProducts.LIVE);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getDevicesAndBrowsers, BrowserStackProducts, } from "../../lib/device-cache.js";
|
|
2
|
-
import { resolveVersion } from "
|
|
2
|
+
import { resolveVersion } from "../../lib/version-resolver.js";
|
|
3
3
|
import { customFuzzySearch } from "../../lib/fuzzy.js";
|
|
4
4
|
// Extract all mobile entries from the data
|
|
5
5
|
function getAllMobileEntries(data) {
|
package/package.json
CHANGED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import { customFuzzySearch } from "../../lib/fuzzy.js";
|
|
2
|
-
/**
|
|
3
|
-
* Fuzzy searches App Live device entries by name.
|
|
4
|
-
*/
|
|
5
|
-
export async function fuzzySearchDevices(devices, query, limit = 5) {
|
|
6
|
-
const top_match = customFuzzySearch(devices, ["device", "display_name"], query, limit);
|
|
7
|
-
return top_match;
|
|
8
|
-
}
|
|
File without changes
|