@browserstack/mcp-server 1.0.15 → 1.1.0
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/config.js +6 -2
- package/dist/index.js +34 -29
- package/dist/lib/api.js +9 -3
- package/dist/lib/constants.js +6 -3
- package/dist/lib/device-cache.js +21 -14
- package/dist/lib/error.js +6 -3
- package/dist/lib/fuzzy.js +4 -1
- package/dist/lib/inmemory-store.js +4 -1
- package/dist/lib/instrumentation.js +18 -12
- package/dist/lib/local.js +35 -27
- package/dist/lib/utils.js +15 -6
- package/dist/logger.js +6 -4
- package/dist/tools/accessibility.js +16 -13
- package/dist/tools/accessiblity-utils/report-fetcher.js +14 -7
- package/dist/tools/accessiblity-utils/report-parser.js +11 -5
- package/dist/tools/accessiblity-utils/scanner.js +16 -9
- package/dist/tools/appautomate-utils/appautomate.js +27 -17
- package/dist/tools/appautomate.js +35 -29
- package/dist/tools/applive-utils/fuzzy-search.js +6 -3
- package/dist/tools/applive-utils/start-session.js +20 -14
- package/dist/tools/applive-utils/upload-app.js +51 -12
- package/dist/tools/applive.js +25 -18
- package/dist/tools/automate-utils/fetch-screenshots.js +14 -8
- package/dist/tools/automate.js +21 -14
- package/dist/tools/bstack-sdk.js +18 -14
- package/dist/tools/failurelogs-utils/app-automate.js +26 -15
- package/dist/tools/failurelogs-utils/automate.js +23 -13
- package/dist/tools/getFailureLogs.js +49 -42
- package/dist/tools/live-utils/desktop-filter.js +11 -8
- package/dist/tools/live-utils/mobile-filter.js +10 -7
- package/dist/tools/live-utils/start-session.js +23 -17
- package/dist/tools/live-utils/types.js +5 -2
- package/dist/tools/live-utils/version-resolver.js +4 -1
- package/dist/tools/live.js +29 -23
- package/dist/tools/observability.js +19 -12
- package/dist/tools/sdk-utils/constants.js +9 -3
- package/dist/tools/sdk-utils/instructions.js +9 -4
- package/dist/tools/sdk-utils/types.js +2 -1
- package/dist/tools/testmanagement-utils/TCG-utils/api.js +38 -26
- package/dist/tools/testmanagement-utils/TCG-utils/config.js +10 -5
- package/dist/tools/testmanagement-utils/TCG-utils/helpers.js +8 -3
- package/dist/tools/testmanagement-utils/TCG-utils/types.js +8 -5
- package/dist/tools/testmanagement-utils/add-test-result.js +24 -17
- package/dist/tools/testmanagement-utils/create-project-folder.js +28 -21
- package/dist/tools/testmanagement-utils/create-testcase.js +38 -30
- package/dist/tools/testmanagement-utils/create-testrun.js +30 -23
- package/dist/tools/testmanagement-utils/list-testcases.js +22 -15
- package/dist/tools/testmanagement-utils/list-testruns.js +19 -12
- package/dist/tools/testmanagement-utils/testcase-from-file.js +22 -16
- package/dist/tools/testmanagement-utils/update-testrun.js +22 -15
- package/dist/tools/testmanagement-utils/upload-file.js +29 -22
- package/dist/tools/testmanagement.js +76 -61
- package/package.json +1 -1
package/dist/config.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Config = void 0;
|
|
1
4
|
if (!process.env.BROWSERSTACK_ACCESS_KEY ||
|
|
2
5
|
!process.env.BROWSERSTACK_USERNAME) {
|
|
3
6
|
throw new Error("Unable to start MCP server. Please set the BROWSERSTACK_ACCESS_KEY and BROWSERSTACK_USERNAME environment variables. Go to https://www.browserstack.com/accounts/profile/details to access them");
|
|
4
7
|
}
|
|
5
|
-
|
|
8
|
+
class Config {
|
|
6
9
|
browserstackUsername;
|
|
7
10
|
browserstackAccessKey;
|
|
8
11
|
DEV_MODE;
|
|
@@ -12,5 +15,6 @@ export class Config {
|
|
|
12
15
|
this.DEV_MODE = DEV_MODE;
|
|
13
16
|
}
|
|
14
17
|
}
|
|
18
|
+
exports.Config = Config;
|
|
15
19
|
const config = new Config(process.env.BROWSERSTACK_USERNAME, process.env.BROWSERSTACK_ACCESS_KEY, process.env.DEV_MODE === "true");
|
|
16
|
-
|
|
20
|
+
exports.default = config;
|
package/dist/index.js
CHANGED
|
@@ -1,44 +1,49 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
2
|
+
"use strict";
|
|
3
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
|
+
};
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
8
|
+
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
9
|
+
const package_json_1 = __importDefault(require("../package.json"));
|
|
10
|
+
require("dotenv/config");
|
|
11
|
+
const logger_js_1 = __importDefault(require("./logger.js"));
|
|
12
|
+
const bstack_sdk_js_1 = __importDefault(require("./tools/bstack-sdk.js"));
|
|
13
|
+
const applive_js_1 = __importDefault(require("./tools/applive.js"));
|
|
14
|
+
const live_js_1 = __importDefault(require("./tools/live.js"));
|
|
15
|
+
const accessibility_js_1 = __importDefault(require("./tools/accessibility.js"));
|
|
16
|
+
const testmanagement_js_1 = __importDefault(require("./tools/testmanagement.js"));
|
|
17
|
+
const appautomate_js_1 = __importDefault(require("./tools/appautomate.js"));
|
|
18
|
+
const getFailureLogs_js_1 = __importDefault(require("./tools/getFailureLogs.js"));
|
|
19
|
+
const automate_js_1 = __importDefault(require("./tools/automate.js"));
|
|
20
|
+
const instrumentation_js_1 = require("./lib/instrumentation.js");
|
|
16
21
|
function registerTools(server) {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
(0, bstack_sdk_js_1.default)(server);
|
|
23
|
+
(0, applive_js_1.default)(server);
|
|
24
|
+
(0, live_js_1.default)(server);
|
|
25
|
+
(0, accessibility_js_1.default)(server);
|
|
26
|
+
(0, testmanagement_js_1.default)(server);
|
|
27
|
+
(0, appautomate_js_1.default)(server);
|
|
28
|
+
(0, getFailureLogs_js_1.default)(server);
|
|
29
|
+
(0, automate_js_1.default)(server);
|
|
25
30
|
}
|
|
26
31
|
// Create an MCP server
|
|
27
|
-
const server = new McpServer({
|
|
32
|
+
const server = new mcp_js_1.McpServer({
|
|
28
33
|
name: "BrowserStack MCP Server",
|
|
29
|
-
version:
|
|
34
|
+
version: package_json_1.default.version,
|
|
30
35
|
});
|
|
31
36
|
registerTools(server);
|
|
32
37
|
async function main() {
|
|
33
|
-
|
|
38
|
+
logger_js_1.default.info("Launching BrowserStack MCP server, version %s", package_json_1.default.version);
|
|
34
39
|
// Start receiving messages on stdin and sending messages on stdout
|
|
35
|
-
const transport = new StdioServerTransport();
|
|
40
|
+
const transport = new stdio_js_1.StdioServerTransport();
|
|
36
41
|
await server.connect(transport);
|
|
37
|
-
|
|
38
|
-
trackMCP("started", server.server.getClientVersion());
|
|
42
|
+
logger_js_1.default.info("MCP server started successfully");
|
|
43
|
+
(0, instrumentation_js_1.trackMCP)("started", server.server.getClientVersion());
|
|
39
44
|
}
|
|
40
45
|
main().catch(console.error);
|
|
41
46
|
// Ensure logs are flushed before exit
|
|
42
47
|
process.on("exit", () => {
|
|
43
|
-
|
|
48
|
+
logger_js_1.default.flush();
|
|
44
49
|
});
|
package/dist/lib/api.js
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.getLatestO11YBuildInfo = getLatestO11YBuildInfo;
|
|
7
|
+
const config_js_1 = __importDefault(require("../config.js"));
|
|
8
|
+
async function getLatestO11YBuildInfo(buildName, projectName) {
|
|
3
9
|
const buildsUrl = `https://api-observability.browserstack.com/ext/v1/builds/latest?build_name=${encodeURIComponent(buildName)}&project_name=${encodeURIComponent(projectName)}`;
|
|
4
10
|
const buildsResponse = await fetch(buildsUrl, {
|
|
5
11
|
headers: {
|
|
6
|
-
Authorization: `Basic ${Buffer.from(`${
|
|
12
|
+
Authorization: `Basic ${Buffer.from(`${config_js_1.default.browserstackUsername}:${config_js_1.default.browserstackAccessKey}`).toString("base64")}`,
|
|
7
13
|
},
|
|
8
14
|
});
|
|
9
15
|
if (!buildsResponse.ok) {
|
package/dist/lib/constants.js
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
|
-
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AppAutomateLogType = exports.AutomateLogType = exports.SessionType = void 0;
|
|
4
|
+
exports.SessionType = {
|
|
2
5
|
Automate: "automate",
|
|
3
6
|
AppAutomate: "app-automate",
|
|
4
7
|
};
|
|
5
|
-
|
|
8
|
+
exports.AutomateLogType = {
|
|
6
9
|
NetworkLogs: "networkLogs",
|
|
7
10
|
SessionLogs: "sessionLogs",
|
|
8
11
|
ConsoleLogs: "consoleLogs",
|
|
9
12
|
};
|
|
10
|
-
|
|
13
|
+
exports.AppAutomateLogType = {
|
|
11
14
|
DeviceLogs: "deviceLogs",
|
|
12
15
|
AppiumLogs: "appiumLogs",
|
|
13
16
|
CrashLogs: "crashLogs",
|
package/dist/lib/device-cache.js
CHANGED
|
@@ -1,15 +1,22 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.BrowserStackProducts = void 0;
|
|
7
|
+
exports.getDevicesAndBrowsers = getDevicesAndBrowsers;
|
|
8
|
+
const fs_1 = __importDefault(require("fs"));
|
|
9
|
+
const os_1 = __importDefault(require("os"));
|
|
10
|
+
const path_1 = __importDefault(require("path"));
|
|
11
|
+
const CACHE_DIR = path_1.default.join(os_1.default.homedir(), ".browserstack", "combined_cache");
|
|
12
|
+
const CACHE_FILE = path_1.default.join(CACHE_DIR, "data.json");
|
|
6
13
|
const TTL_MS = 24 * 60 * 60 * 1000; // 1 day
|
|
7
|
-
|
|
14
|
+
var BrowserStackProducts;
|
|
8
15
|
(function (BrowserStackProducts) {
|
|
9
16
|
BrowserStackProducts["LIVE"] = "live";
|
|
10
17
|
BrowserStackProducts["APP_LIVE"] = "app_live";
|
|
11
18
|
BrowserStackProducts["APP_AUTOMATE"] = "app_automate";
|
|
12
|
-
})(BrowserStackProducts || (BrowserStackProducts = {}));
|
|
19
|
+
})(BrowserStackProducts || (exports.BrowserStackProducts = BrowserStackProducts = {}));
|
|
13
20
|
const URLS = {
|
|
14
21
|
[BrowserStackProducts.LIVE]: "https://www.browserstack.com/list-of-browsers-and-platforms/live.json",
|
|
15
22
|
[BrowserStackProducts.APP_LIVE]: "https://www.browserstack.com/list-of-browsers-and-platforms/app_live.json",
|
|
@@ -18,16 +25,16 @@ const URLS = {
|
|
|
18
25
|
/**
|
|
19
26
|
* Fetches and caches BrowserStack datasets (live + app_live + app_automate) with a shared TTL.
|
|
20
27
|
*/
|
|
21
|
-
|
|
22
|
-
if (!
|
|
23
|
-
|
|
28
|
+
async function getDevicesAndBrowsers(type) {
|
|
29
|
+
if (!fs_1.default.existsSync(CACHE_DIR)) {
|
|
30
|
+
fs_1.default.mkdirSync(CACHE_DIR, { recursive: true });
|
|
24
31
|
}
|
|
25
32
|
let cache = {};
|
|
26
|
-
if (
|
|
27
|
-
const stats =
|
|
33
|
+
if (fs_1.default.existsSync(CACHE_FILE)) {
|
|
34
|
+
const stats = fs_1.default.statSync(CACHE_FILE);
|
|
28
35
|
if (Date.now() - stats.mtimeMs < TTL_MS) {
|
|
29
36
|
try {
|
|
30
|
-
cache = JSON.parse(
|
|
37
|
+
cache = JSON.parse(fs_1.default.readFileSync(CACHE_FILE, "utf8"));
|
|
31
38
|
if (cache[type]) {
|
|
32
39
|
return cache[type];
|
|
33
40
|
}
|
|
@@ -46,6 +53,6 @@ export async function getDevicesAndBrowsers(type) {
|
|
|
46
53
|
cache = {
|
|
47
54
|
[type]: data,
|
|
48
55
|
};
|
|
49
|
-
|
|
56
|
+
fs_1.default.writeFileSync(CACHE_FILE, JSON.stringify(cache), "utf8");
|
|
50
57
|
return cache[type];
|
|
51
58
|
}
|
package/dist/lib/error.js
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
|
-
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.formatAxiosError = formatAxiosError;
|
|
4
|
+
const axios_1 = require("axios");
|
|
2
5
|
/**
|
|
3
6
|
* Formats an AxiosError into a CallToolResult with an appropriate message.
|
|
4
7
|
* @param err - The error object to format
|
|
5
8
|
* @param defaultText - The fallback error message
|
|
6
9
|
*/
|
|
7
|
-
|
|
10
|
+
function formatAxiosError(err, defaultText) {
|
|
8
11
|
let text = defaultText;
|
|
9
|
-
if (err instanceof AxiosError && err.response?.data) {
|
|
12
|
+
if (err instanceof axios_1.AxiosError && err.response?.data) {
|
|
10
13
|
const message = err.response.data.message ||
|
|
11
14
|
err.response.data.error ||
|
|
12
15
|
err.message ||
|
package/dist/lib/fuzzy.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.customFuzzySearch = customFuzzySearch;
|
|
1
4
|
// 1. Compute Levenshtein distance between two strings
|
|
2
5
|
function levenshtein(a, b) {
|
|
3
6
|
const dp = Array(a.length + 1)
|
|
@@ -36,7 +39,7 @@ function scoreItem(item, keys, queryTokens) {
|
|
|
36
39
|
return best;
|
|
37
40
|
}
|
|
38
41
|
// 3. The search entrypoint
|
|
39
|
-
|
|
42
|
+
function customFuzzySearch(list, keys, query, limit = 5, maxDistance = 0.6) {
|
|
40
43
|
const q = query.toLowerCase().trim();
|
|
41
44
|
const queryTokens = q.split(/\s+/);
|
|
42
45
|
return list
|
|
@@ -1,10 +1,16 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.trackMCP = trackMCP;
|
|
7
|
+
const logger_js_1 = __importDefault(require("../logger.js"));
|
|
8
|
+
const config_js_1 = __importDefault(require("../config.js"));
|
|
9
|
+
const package_json_1 = __importDefault(require("../../package.json"));
|
|
10
|
+
const axios_1 = __importDefault(require("axios"));
|
|
11
|
+
function trackMCP(toolName, clientInfo, error) {
|
|
12
|
+
if (config_js_1.default.DEV_MODE) {
|
|
13
|
+
logger_js_1.default.info("Tracking MCP is disabled in dev mode");
|
|
8
14
|
return;
|
|
9
15
|
}
|
|
10
16
|
const instrumentationEndpoint = "https://api.browserstack.com/sdk/v1/event";
|
|
@@ -12,15 +18,15 @@ export function trackMCP(toolName, clientInfo, error) {
|
|
|
12
18
|
const mcpClient = clientInfo?.name || "unknown";
|
|
13
19
|
// Log client information
|
|
14
20
|
if (clientInfo?.name) {
|
|
15
|
-
|
|
21
|
+
logger_js_1.default.info(`Client connected: ${clientInfo.name} (version: ${clientInfo.version})`);
|
|
16
22
|
}
|
|
17
23
|
else {
|
|
18
|
-
|
|
24
|
+
logger_js_1.default.info("Client connected: unknown client");
|
|
19
25
|
}
|
|
20
26
|
const event = {
|
|
21
27
|
event_type: "MCPInstrumentation",
|
|
22
28
|
event_properties: {
|
|
23
|
-
mcp_version:
|
|
29
|
+
mcp_version: package_json_1.default.version,
|
|
24
30
|
tool_name: toolName,
|
|
25
31
|
mcp_client: mcpClient,
|
|
26
32
|
success: isSuccess,
|
|
@@ -33,11 +39,11 @@ export function trackMCP(toolName, clientInfo, error) {
|
|
|
33
39
|
event.event_properties.error_type =
|
|
34
40
|
error instanceof Error ? error.constructor.name : "Unknown";
|
|
35
41
|
}
|
|
36
|
-
|
|
42
|
+
axios_1.default
|
|
37
43
|
.post(instrumentationEndpoint, event, {
|
|
38
44
|
headers: {
|
|
39
45
|
"Content-Type": "application/json",
|
|
40
|
-
Authorization: `Basic ${Buffer.from(`${
|
|
46
|
+
Authorization: `Basic ${Buffer.from(`${config_js_1.default.browserstackUsername}:${config_js_1.default.browserstackAccessKey}`).toString("base64")}`,
|
|
41
47
|
},
|
|
42
48
|
timeout: 2000,
|
|
43
49
|
})
|
package/dist/lib/local.js
CHANGED
|
@@ -1,39 +1,47 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.killExistingBrowserStackLocalProcesses = killExistingBrowserStackLocalProcesses;
|
|
7
|
+
exports.ensureLocalBinarySetup = ensureLocalBinarySetup;
|
|
8
|
+
exports.isLocalURL = isLocalURL;
|
|
9
|
+
const logger_js_1 = __importDefault(require("../logger.js"));
|
|
10
|
+
const child_process_1 = require("child_process");
|
|
11
|
+
const browserstack_local_1 = require("browserstack-local");
|
|
12
|
+
const config_js_1 = __importDefault(require("../config.js"));
|
|
5
13
|
async function isBrowserStackLocalRunning() {
|
|
6
14
|
// Check if BrowserStackLocal binary is already running
|
|
7
15
|
try {
|
|
8
16
|
if (process.platform === "win32") {
|
|
9
|
-
const result = execSync('tasklist /FI "IMAGENAME eq BrowserStackLocal.exe"', {
|
|
17
|
+
const result = (0, child_process_1.execSync)('tasklist /FI "IMAGENAME eq BrowserStackLocal.exe"', {
|
|
10
18
|
encoding: "utf8",
|
|
11
19
|
});
|
|
12
20
|
if (result.includes("BrowserStackLocal.exe")) {
|
|
13
|
-
|
|
21
|
+
logger_js_1.default.info("BrowserStackLocal binary is already running");
|
|
14
22
|
return true;
|
|
15
23
|
}
|
|
16
24
|
}
|
|
17
25
|
else {
|
|
18
|
-
const result = execSync("pgrep -f BrowserStackLocal", {
|
|
26
|
+
const result = (0, child_process_1.execSync)("pgrep -f BrowserStackLocal", {
|
|
19
27
|
encoding: "utf8",
|
|
20
28
|
stdio: "pipe",
|
|
21
29
|
}).toString();
|
|
22
30
|
if (result) {
|
|
23
|
-
|
|
31
|
+
logger_js_1.default.info("BrowserStackLocal binary is already running");
|
|
24
32
|
return true;
|
|
25
33
|
}
|
|
26
34
|
}
|
|
27
|
-
|
|
35
|
+
logger_js_1.default.info("BrowserStackLocal binary is not running");
|
|
28
36
|
return false;
|
|
29
37
|
}
|
|
30
38
|
catch (error) {
|
|
31
|
-
|
|
39
|
+
logger_js_1.default.info("Error checking BrowserStackLocal status, assuming not running ... " +
|
|
32
40
|
error);
|
|
33
41
|
return false;
|
|
34
42
|
}
|
|
35
43
|
}
|
|
36
|
-
|
|
44
|
+
async function killExistingBrowserStackLocalProcesses() {
|
|
37
45
|
const isRunning = await isBrowserStackLocalRunning();
|
|
38
46
|
if (!isRunning) {
|
|
39
47
|
return;
|
|
@@ -42,50 +50,50 @@ export async function killExistingBrowserStackLocalProcesses() {
|
|
|
42
50
|
try {
|
|
43
51
|
if (process.platform === "win32") {
|
|
44
52
|
// Check if process exists on Windows
|
|
45
|
-
const checkResult = execSync('tasklist /FI "IMAGENAME eq BrowserStackLocal.exe"', { encoding: "utf8" });
|
|
53
|
+
const checkResult = (0, child_process_1.execSync)('tasklist /FI "IMAGENAME eq BrowserStackLocal.exe"', { encoding: "utf8" });
|
|
46
54
|
if (checkResult.includes("BrowserStackLocal.exe")) {
|
|
47
|
-
execSync("taskkill /F /IM BrowserStackLocal.exe", { stdio: "ignore" });
|
|
48
|
-
|
|
55
|
+
(0, child_process_1.execSync)("taskkill /F /IM BrowserStackLocal.exe", { stdio: "ignore" });
|
|
56
|
+
logger_js_1.default.info("Successfully killed existing BrowserStackLocal processes");
|
|
49
57
|
}
|
|
50
58
|
}
|
|
51
59
|
else {
|
|
52
60
|
// Check if process exists on Unix-like systems
|
|
53
|
-
const checkResult = execSync("pgrep -f BrowserStackLocal", {
|
|
61
|
+
const checkResult = (0, child_process_1.execSync)("pgrep -f BrowserStackLocal", {
|
|
54
62
|
encoding: "utf8",
|
|
55
63
|
stdio: "pipe",
|
|
56
64
|
}).toString();
|
|
57
65
|
if (checkResult) {
|
|
58
|
-
execSync("pkill -f BrowserStackLocal", { stdio: "ignore" });
|
|
59
|
-
|
|
66
|
+
(0, child_process_1.execSync)("pkill -f BrowserStackLocal", { stdio: "ignore" });
|
|
67
|
+
logger_js_1.default.info("Successfully killed existing BrowserStackLocal processes");
|
|
60
68
|
}
|
|
61
69
|
}
|
|
62
70
|
}
|
|
63
71
|
catch (error) {
|
|
64
|
-
|
|
72
|
+
logger_js_1.default.info(`Error checking/killing BrowserStackLocal processes: ${error}`);
|
|
65
73
|
// Continue execution as there may not be any processes running
|
|
66
74
|
}
|
|
67
75
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
const localBinary = new Local();
|
|
76
|
+
async function ensureLocalBinarySetup() {
|
|
77
|
+
logger_js_1.default.info("Ensuring local binary setup as it is required for private URLs...");
|
|
78
|
+
const localBinary = new browserstack_local_1.Local();
|
|
71
79
|
await killExistingBrowserStackLocalProcesses();
|
|
72
80
|
return await new Promise((resolve, reject) => {
|
|
73
81
|
localBinary.start({
|
|
74
|
-
key:
|
|
75
|
-
username:
|
|
82
|
+
key: config_js_1.default.browserstackAccessKey,
|
|
83
|
+
username: config_js_1.default.browserstackUsername,
|
|
76
84
|
}, (error) => {
|
|
77
85
|
if (error) {
|
|
78
|
-
|
|
86
|
+
logger_js_1.default.error(`Unable to start BrowserStack Local... please check your credentials and try again. Error: ${error}`);
|
|
79
87
|
reject(new Error(`Unable to configure local tunnel binary, please check your credentials and try again. Error: ${error}`));
|
|
80
88
|
}
|
|
81
89
|
else {
|
|
82
|
-
|
|
90
|
+
logger_js_1.default.info("Successfully started BrowserStack Local");
|
|
83
91
|
resolve();
|
|
84
92
|
}
|
|
85
93
|
});
|
|
86
94
|
});
|
|
87
95
|
}
|
|
88
|
-
|
|
96
|
+
function isLocalURL(url) {
|
|
89
97
|
try {
|
|
90
98
|
const urlObj = new URL(url);
|
|
91
99
|
const hostname = urlObj.hostname.toLowerCase();
|
|
@@ -95,7 +103,7 @@ export function isLocalURL(url) {
|
|
|
95
103
|
hostname.endsWith(".localhost"));
|
|
96
104
|
}
|
|
97
105
|
catch (error) {
|
|
98
|
-
|
|
106
|
+
logger_js_1.default.error(`Error checking if URL is local: ${error}`);
|
|
99
107
|
return false;
|
|
100
108
|
}
|
|
101
109
|
}
|
package/dist/lib/utils.js
CHANGED
|
@@ -1,11 +1,20 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.sanitizeUrlParam = sanitizeUrlParam;
|
|
7
|
+
exports.maybeCompressBase64 = maybeCompressBase64;
|
|
8
|
+
exports.assertOkResponse = assertOkResponse;
|
|
9
|
+
exports.filterLinesByKeywords = filterLinesByKeywords;
|
|
10
|
+
const sharp_1 = __importDefault(require("sharp"));
|
|
11
|
+
function sanitizeUrlParam(param) {
|
|
3
12
|
// Remove any characters that could be used for command injection
|
|
4
13
|
return param.replace(/[;&|`$(){}[\]<>]/g, "");
|
|
5
14
|
}
|
|
6
15
|
const ONE_MB = 1048576;
|
|
7
16
|
//Compresses a base64 image intelligently to keep it under 1 MB if needed.
|
|
8
|
-
|
|
17
|
+
async function maybeCompressBase64(base64) {
|
|
9
18
|
const buffer = Buffer.from(base64, "base64");
|
|
10
19
|
if (buffer.length <= ONE_MB) {
|
|
11
20
|
return base64;
|
|
@@ -13,10 +22,10 @@ export async function maybeCompressBase64(base64) {
|
|
|
13
22
|
const sizeRatio = 1048576 / buffer.length;
|
|
14
23
|
const estimatedQuality = Math.floor(sizeRatio * 100);
|
|
15
24
|
const quality = Math.min(95, Math.max(30, estimatedQuality));
|
|
16
|
-
const compressedBuffer = await
|
|
25
|
+
const compressedBuffer = await (0, sharp_1.default)(buffer).png({ quality }).toBuffer();
|
|
17
26
|
return compressedBuffer.toString("base64");
|
|
18
27
|
}
|
|
19
|
-
|
|
28
|
+
async function assertOkResponse(response, action) {
|
|
20
29
|
if (!response.ok) {
|
|
21
30
|
if (response.status === 404) {
|
|
22
31
|
throw new Error(`Invalid session ID for ${action}`);
|
|
@@ -24,7 +33,7 @@ export async function assertOkResponse(response, action) {
|
|
|
24
33
|
throw new Error(`Failed to fetch logs for ${action}: ${response.statusText}`);
|
|
25
34
|
}
|
|
26
35
|
}
|
|
27
|
-
|
|
36
|
+
function filterLinesByKeywords(logText, keywords) {
|
|
28
37
|
return logText
|
|
29
38
|
.split(/\r?\n/)
|
|
30
39
|
.map((line) => line.trim())
|
package/dist/logger.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const pino_1 = require("pino");
|
|
2
4
|
let logger;
|
|
3
5
|
if (process.env.NODE_ENV === "development") {
|
|
4
|
-
logger = pino({
|
|
6
|
+
logger = (0, pino_1.pino)({
|
|
5
7
|
level: "debug",
|
|
6
8
|
transport: {
|
|
7
9
|
targets: [
|
|
@@ -22,7 +24,7 @@ if (process.env.NODE_ENV === "development") {
|
|
|
22
24
|
}
|
|
23
25
|
else {
|
|
24
26
|
// NULL logger
|
|
25
|
-
logger = pino({
|
|
27
|
+
logger = (0, pino_1.pino)({
|
|
26
28
|
level: "info",
|
|
27
29
|
transport: {
|
|
28
30
|
target: "pino/file",
|
|
@@ -32,4 +34,4 @@ else {
|
|
|
32
34
|
},
|
|
33
35
|
});
|
|
34
36
|
}
|
|
35
|
-
|
|
37
|
+
exports.default = logger;
|
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
const
|
|
7
|
-
const
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.default = addAccessibilityTools;
|
|
4
|
+
const zod_1 = require("zod");
|
|
5
|
+
const scanner_js_1 = require("./accessiblity-utils/scanner.js");
|
|
6
|
+
const report_fetcher_js_1 = require("./accessiblity-utils/report-fetcher.js");
|
|
7
|
+
const instrumentation_js_1 = require("../lib/instrumentation.js");
|
|
8
|
+
const report_parser_js_1 = require("./accessiblity-utils/report-parser.js");
|
|
9
|
+
const scanner = new scanner_js_1.AccessibilityScanner();
|
|
10
|
+
const reportFetcher = new report_fetcher_js_1.AccessibilityReportFetcher();
|
|
8
11
|
async function runAccessibilityScan(name, pageURL, context) {
|
|
9
12
|
// Start scan
|
|
10
13
|
const startResp = await scanner.startScan(name, [pageURL]);
|
|
@@ -36,7 +39,7 @@ async function runAccessibilityScan(name, pageURL, context) {
|
|
|
36
39
|
}
|
|
37
40
|
// Fetch CSV report link
|
|
38
41
|
const reportLink = await reportFetcher.getReportLink(scanId, scanRunId);
|
|
39
|
-
const { records } = await parseAccessibilityReportFromCSV(reportLink);
|
|
42
|
+
const { records } = await (0, report_parser_js_1.parseAccessibilityReportFromCSV)(reportLink);
|
|
40
43
|
return {
|
|
41
44
|
content: [
|
|
42
45
|
{
|
|
@@ -50,17 +53,17 @@ async function runAccessibilityScan(name, pageURL, context) {
|
|
|
50
53
|
],
|
|
51
54
|
};
|
|
52
55
|
}
|
|
53
|
-
|
|
56
|
+
function addAccessibilityTools(server) {
|
|
54
57
|
server.tool("startAccessibilityScan", "Start an accessibility scan via BrowserStack and retrieve a local CSV report path.", {
|
|
55
|
-
name: z.string().describe("Name of the accessibility scan"),
|
|
56
|
-
pageURL: z.string().describe("The URL to scan for accessibility issues"),
|
|
58
|
+
name: zod_1.z.string().describe("Name of the accessibility scan"),
|
|
59
|
+
pageURL: zod_1.z.string().describe("The URL to scan for accessibility issues"),
|
|
57
60
|
}, async (args, context) => {
|
|
58
61
|
try {
|
|
59
|
-
trackMCP("startAccessibilityScan", server.server.getClientVersion());
|
|
62
|
+
(0, instrumentation_js_1.trackMCP)("startAccessibilityScan", server.server.getClientVersion());
|
|
60
63
|
return await runAccessibilityScan(args.name, args.pageURL, context);
|
|
61
64
|
}
|
|
62
65
|
catch (error) {
|
|
63
|
-
trackMCP("startAccessibilityScan", server.server.getClientVersion(), error);
|
|
66
|
+
(0, instrumentation_js_1.trackMCP)("startAccessibilityScan", server.server.getClientVersion(), error);
|
|
64
67
|
return {
|
|
65
68
|
content: [
|
|
66
69
|
{
|
|
@@ -1,14 +1,20 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.AccessibilityReportFetcher = void 0;
|
|
7
|
+
const axios_1 = __importDefault(require("axios"));
|
|
8
|
+
const config_js_1 = __importDefault(require("../../config.js"));
|
|
9
|
+
class AccessibilityReportFetcher {
|
|
4
10
|
auth = {
|
|
5
|
-
username:
|
|
6
|
-
password:
|
|
11
|
+
username: config_js_1.default.browserstackUsername,
|
|
12
|
+
password: config_js_1.default.browserstackAccessKey,
|
|
7
13
|
};
|
|
8
14
|
async getReportLink(scanId, scanRunId) {
|
|
9
15
|
// Initiate CSV link generation
|
|
10
16
|
const initUrl = `https://api-accessibility.browserstack.com/api/website-scanner/v1/scans/${scanId}/scan_runs/issues?scan_run_id=${scanRunId}`;
|
|
11
|
-
const initResp = await
|
|
17
|
+
const initResp = await axios_1.default.get(initUrl, {
|
|
12
18
|
auth: this.auth,
|
|
13
19
|
});
|
|
14
20
|
if (!initResp.data.success) {
|
|
@@ -17,7 +23,7 @@ export class AccessibilityReportFetcher {
|
|
|
17
23
|
const taskId = initResp.data.data.task_id;
|
|
18
24
|
// Fetch the generated CSV link
|
|
19
25
|
const reportUrl = `https://api-accessibility.browserstack.com/api/website-scanner/v1/scans/${scanId}/scan_runs/issues?task_id=${encodeURIComponent(taskId)}`;
|
|
20
|
-
const reportResp = await
|
|
26
|
+
const reportResp = await axios_1.default.get(reportUrl, {
|
|
21
27
|
auth: this.auth,
|
|
22
28
|
});
|
|
23
29
|
if (!reportResp.data.success) {
|
|
@@ -26,3 +32,4 @@ export class AccessibilityReportFetcher {
|
|
|
26
32
|
return reportResp.data.data.reportLink;
|
|
27
33
|
}
|
|
28
34
|
}
|
|
35
|
+
exports.AccessibilityReportFetcher = AccessibilityReportFetcher;
|
|
@@ -1,12 +1,18 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.parseAccessibilityReportFromCSV = parseAccessibilityReportFromCSV;
|
|
7
|
+
const node_fetch_1 = __importDefault(require("node-fetch"));
|
|
8
|
+
const sync_1 = require("csv-parse/sync");
|
|
9
|
+
async function parseAccessibilityReportFromCSV(reportLink, { maxCharacterLength = 10_000, nextPage = 0 } = {}) {
|
|
4
10
|
// 1) Download & parse
|
|
5
|
-
const res = await
|
|
11
|
+
const res = await (0, node_fetch_1.default)(reportLink);
|
|
6
12
|
if (!res.ok)
|
|
7
13
|
throw new Error(`Failed to download report: ${res.statusText}`);
|
|
8
14
|
const text = await res.text();
|
|
9
|
-
const all = parse(text, {
|
|
15
|
+
const all = (0, sync_1.parse)(text, {
|
|
10
16
|
columns: true,
|
|
11
17
|
skip_empty_lines: true,
|
|
12
18
|
}).map((row) => ({
|