@browserstack/mcp-server 1.0.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/LICENSE +661 -0
- package/README.md +117 -0
- package/dist/config.js +18 -0
- package/dist/index.js +40 -0
- package/dist/lib/api.js +22 -0
- package/dist/lib/local.js +109 -0
- package/dist/lib/utils.js +7 -0
- package/dist/logger.js +40 -0
- package/dist/tools/accessibility.js +43 -0
- package/dist/tools/accessiblity-utils/accessibility.js +82 -0
- package/dist/tools/applive-utils/constants.js +82 -0
- package/dist/tools/applive-utils/start-session.js +55 -0
- package/dist/tools/applive-utils/types.js +2 -0
- package/dist/tools/applive-utils/upload-app.js +68 -0
- package/dist/tools/applive.js +93 -0
- package/dist/tools/bstack-sdk.js +64 -0
- package/dist/tools/live-utils/start-session.js +58 -0
- package/dist/tools/live.js +97 -0
- package/dist/tools/observability.js +58 -0
- package/dist/tools/sdk-utils/constants.js +63 -0
- package/dist/tools/sdk-utils/instructions.js +64 -0
- package/dist/tools/sdk-utils/types.js +2 -0
- package/package.json +58 -0
package/README.md
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# BrowserStack MCP Server
|
|
2
|
+
<div style="display: flex; justify-content: space-between; padding: 20px;">
|
|
3
|
+
<img src="assets/browserstack-logo.png" alt="BrowserStack Logo" height="100"> <img src="assets/mcp-logo.png" alt="MCP Server Logo" width="100">
|
|
4
|
+
</div>
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
*See it in action, check out the video below.*
|
|
8
|
+
|
|
9
|
+
[](https://www.youtube.com/watch?v=vy1sx0J7sTk)
|
|
10
|
+
|
|
11
|
+
## ๐ Overview
|
|
12
|
+
BrowserStack MCP Server brings the power of BrowserStack's [Test Platform](https://www.browserstack.com/test-platform) directly into your development workflow. It enables you to run tests, debug applications, and perform cross-browser testing through any [MCP-compliant client](https://modelcontextprotocol.io/clients#feature-support-matrix).
|
|
13
|
+
|
|
14
|
+
## โจ Features - More Coming Soon!
|
|
15
|
+
|
|
16
|
+
### ๐ฑ App Testing
|
|
17
|
+
- **Real Device Testing**: Test your mobile apps on BrowserStack's extensive cloud of real devices
|
|
18
|
+
- **Performance Testing**: Unlike emulators, test your app's real-world performance on actual devices
|
|
19
|
+
- **Live Debugging**: Debug crashes and performance issues in real-time
|
|
20
|
+
- **Comprehensive Device Coverage**: Access all major devices and OS versions from our [device grid](https://www.browserstack.com/list-of-browsers-and-platforms/app_live)
|
|
21
|
+
|
|
22
|
+
### ๐ Web Testing
|
|
23
|
+
- **Local Testing**: Seamlessly test websites hosted on localhost
|
|
24
|
+
- **Cross-Browser Testing**: Test websites across different browsers and devices
|
|
25
|
+
- **Screenshot Testing**: Capture and compare screenshots across different environments
|
|
26
|
+
|
|
27
|
+
### ๐งช Automated Testing
|
|
28
|
+
- **Fix Test Failures**: Leverage AI to identify and fix test failures reported by your CI/CD pipeline by utilising our industry leading [Test Observability](https://www.browserstack.com/docs/test-observability) features. Find more info [here](https://www.browserstack.com/docs/test-observability/features/smart-tags).
|
|
29
|
+
- **Run on BrowserStack**: Easily run tests written in Jest, Playwright, Selenium, and more on BrowserStack's [Test Platform](https://www.browserstack.com/test-platform)
|
|
30
|
+
- **Accessibility Testing**: Ensure WCAG and ADA compliance with our [Accessibility Testing](https://www.browserstack.com/accessibility-testing) tool
|
|
31
|
+
|
|
32
|
+
## ๐ ๏ธ Installation
|
|
33
|
+
|
|
34
|
+
1. **Create a BrowserStack Account**
|
|
35
|
+
- Sign up at [BrowserStack](https://www.browserstack.com/signup)
|
|
36
|
+
- Get your credentials from [Account Settings](https://www.browserstack.com/accounts/profile/details)
|
|
37
|
+
|
|
38
|
+
2. **Install the MCP Server**
|
|
39
|
+
- Sample MCP Server config for Cursor: `.cursor/mcp.json`:
|
|
40
|
+
```json
|
|
41
|
+
{
|
|
42
|
+
"mcpServers": {
|
|
43
|
+
"browserstack_mcp_server": {
|
|
44
|
+
"command": "npx",
|
|
45
|
+
"args": ["@browserstack/mcp-server"],
|
|
46
|
+
"env": {
|
|
47
|
+
"BROWSERSTACK_USERNAME": "<username>",
|
|
48
|
+
"BROWSERSTACK_ACCESS_KEY": "<access_key>"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
- For Github Copilot users, ensure you are using the latest version of the VSCode with support for Copilot agent mode.
|
|
55
|
+
|
|
56
|
+
## ๐ก Usage Examples
|
|
57
|
+
|
|
58
|
+
### App Testing
|
|
59
|
+
```bash
|
|
60
|
+
# Open app on specific device
|
|
61
|
+
"open my app on a iPhone 15 Pro Max"
|
|
62
|
+
|
|
63
|
+
# Debug app crashes
|
|
64
|
+
"My app crashed on Android 14 device, can you help me debug?"
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Web Testing
|
|
68
|
+
```bash
|
|
69
|
+
# Test local website
|
|
70
|
+
"open my website hosted on localhost:3001 on a Windows Edge browser and take a screenshot"
|
|
71
|
+
|
|
72
|
+
# Check website compatibility
|
|
73
|
+
"open test.com on Samsung Browser and check for readability issues"
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Automated Testing
|
|
77
|
+
```bash
|
|
78
|
+
# Run test suite
|
|
79
|
+
"run my test suite on BrowserStack"
|
|
80
|
+
|
|
81
|
+
# Debug test failures
|
|
82
|
+
"My test suite failed, can you help me fix the new failures?"
|
|
83
|
+
|
|
84
|
+
# Accessibility testing
|
|
85
|
+
"check for accessibility issues on my www.mywebsite.com"
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## ๐ค Recommended MCP Clients
|
|
89
|
+
|
|
90
|
+
- **Github Copilot or Cursor** (Recommended for automated testing + debugging)
|
|
91
|
+
- **Claude Desktop** (Recommended for manual testing)
|
|
92
|
+
|
|
93
|
+
## โ ๏ธ Important Notes
|
|
94
|
+
- The BrowserStack MCP Server is under active development and currently supports a subset of the MCP protocol, i.e. `tools`. More features will be added soon.
|
|
95
|
+
- As tool invocation relies on the MCP Client which in turn rely on LLMs for tool calling. There can be some non-deterministic behaviour and can lead to unexpected results. If you have any suggestions or feedback, please open an issue to discuss.
|
|
96
|
+
|
|
97
|
+
## ๐ Contributing
|
|
98
|
+
|
|
99
|
+
We welcome contributions! Please read our [Contributing Guidelines](CONTRIBUTING.md) for details on our code of conduct and the process for submitting pull requests.
|
|
100
|
+
|
|
101
|
+
## ๐ License
|
|
102
|
+
|
|
103
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
104
|
+
|
|
105
|
+
## ๐ Support
|
|
106
|
+
|
|
107
|
+
For support, please:
|
|
108
|
+
- Check our [documentation](https://www.browserstack.com/docs)
|
|
109
|
+
- Open an issue in our [GitHub repository](https://github.com/browserstack/mcp-server)
|
|
110
|
+
- Contact our [support team](https://www.browserstack.com/contact)
|
|
111
|
+
|
|
112
|
+
## ๐ Resources
|
|
113
|
+
|
|
114
|
+
- [BrowserStack Test Platform](https://www.browserstack.com/test-platform)
|
|
115
|
+
- [MCP Protocol Documentation](https://modelcontextprotocol.io)
|
|
116
|
+
- [Device Grid](https://www.browserstack.com/list-of-browsers-and-platforms/app_live)
|
|
117
|
+
- [Accessibility Testing](https://www.browserstack.com/accessibility-testing)
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Config = void 0;
|
|
4
|
+
if (!process.env.BROWSERSTACK_ACCESS_KEY ||
|
|
5
|
+
!process.env.BROWSERSTACK_USERNAME) {
|
|
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");
|
|
7
|
+
}
|
|
8
|
+
class Config {
|
|
9
|
+
browserstackUsername;
|
|
10
|
+
browserstackAccessKey;
|
|
11
|
+
constructor(browserstackUsername, browserstackAccessKey) {
|
|
12
|
+
this.browserstackUsername = browserstackUsername;
|
|
13
|
+
this.browserstackAccessKey = browserstackAccessKey;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
exports.Config = Config;
|
|
17
|
+
const config = new Config(process.env.BROWSERSTACK_USERNAME, process.env.BROWSERSTACK_ACCESS_KEY);
|
|
18
|
+
exports.default = config;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
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
|
+
const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
7
|
+
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
8
|
+
const package_json_1 = __importDefault(require("../package.json"));
|
|
9
|
+
require("dotenv/config");
|
|
10
|
+
const logger_1 = __importDefault(require("./logger"));
|
|
11
|
+
const bstack_sdk_1 = __importDefault(require("./tools/bstack-sdk"));
|
|
12
|
+
const applive_1 = __importDefault(require("./tools/applive"));
|
|
13
|
+
const observability_1 = __importDefault(require("./tools/observability"));
|
|
14
|
+
const live_1 = __importDefault(require("./tools/live"));
|
|
15
|
+
const accessibility_1 = __importDefault(require("./tools/accessibility"));
|
|
16
|
+
function registerTools(server) {
|
|
17
|
+
(0, bstack_sdk_1.default)(server);
|
|
18
|
+
(0, applive_1.default)(server);
|
|
19
|
+
(0, live_1.default)(server);
|
|
20
|
+
(0, observability_1.default)(server);
|
|
21
|
+
(0, accessibility_1.default)(server);
|
|
22
|
+
}
|
|
23
|
+
// Create an MCP server
|
|
24
|
+
const server = new mcp_js_1.McpServer({
|
|
25
|
+
name: "BrowserStack MCP Server",
|
|
26
|
+
version: package_json_1.default.version,
|
|
27
|
+
});
|
|
28
|
+
registerTools(server);
|
|
29
|
+
async function main() {
|
|
30
|
+
logger_1.default.info("Launching BrowserStack MCP server, version %s", package_json_1.default.version);
|
|
31
|
+
// Start receiving messages on stdin and sending messages on stdout
|
|
32
|
+
const transport = new stdio_js_1.StdioServerTransport();
|
|
33
|
+
await server.connect(transport);
|
|
34
|
+
logger_1.default.info("MCP server started successfully");
|
|
35
|
+
}
|
|
36
|
+
main().catch(console.error);
|
|
37
|
+
// Ensure logs are flushed before exit
|
|
38
|
+
process.on("exit", () => {
|
|
39
|
+
logger_1.default.flush();
|
|
40
|
+
});
|
package/dist/lib/api.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
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_1 = __importDefault(require("../config"));
|
|
8
|
+
async function getLatestO11YBuildInfo(buildName, projectName) {
|
|
9
|
+
const buildsUrl = `https://api-observability.browserstack.com/ext/v1/builds/latest?build_name=${encodeURIComponent(buildName)}&project_name=${encodeURIComponent(projectName)}`;
|
|
10
|
+
const buildsResponse = await fetch(buildsUrl, {
|
|
11
|
+
headers: {
|
|
12
|
+
Authorization: `Basic ${Buffer.from(`${config_1.default.browserstackUsername}:${config_1.default.browserstackAccessKey}`).toString("base64")}`,
|
|
13
|
+
},
|
|
14
|
+
});
|
|
15
|
+
if (!buildsResponse.ok) {
|
|
16
|
+
if (buildsResponse.statusText === "Unauthorized") {
|
|
17
|
+
throw new Error(`Failed to fetch builds: ${buildsResponse.statusText}. Please check if the BrowserStack credentials are correctly configured when installing the MCP server.`);
|
|
18
|
+
}
|
|
19
|
+
throw new Error(`Failed to fetch builds: ${buildsResponse.statusText}`);
|
|
20
|
+
}
|
|
21
|
+
return buildsResponse.json();
|
|
22
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
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_1 = __importDefault(require("../logger"));
|
|
10
|
+
const child_process_1 = require("child_process");
|
|
11
|
+
const browserstack_local_1 = require("browserstack-local");
|
|
12
|
+
const config_1 = __importDefault(require("../config"));
|
|
13
|
+
async function isBrowserStackLocalRunning() {
|
|
14
|
+
// Check if BrowserStackLocal binary is already running
|
|
15
|
+
try {
|
|
16
|
+
if (process.platform === "win32") {
|
|
17
|
+
const result = (0, child_process_1.execSync)('tasklist /FI "IMAGENAME eq BrowserStackLocal.exe"', {
|
|
18
|
+
encoding: "utf8",
|
|
19
|
+
});
|
|
20
|
+
if (result.includes("BrowserStackLocal.exe")) {
|
|
21
|
+
logger_1.default.info("BrowserStackLocal binary is already running");
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
const result = (0, child_process_1.execSync)("pgrep -f BrowserStackLocal", {
|
|
27
|
+
encoding: "utf8",
|
|
28
|
+
stdio: "pipe",
|
|
29
|
+
}).toString();
|
|
30
|
+
if (result) {
|
|
31
|
+
logger_1.default.info("BrowserStackLocal binary is already running");
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
logger_1.default.info("BrowserStackLocal binary is not running");
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
logger_1.default.info("Error checking BrowserStackLocal status, assuming not running ... " +
|
|
40
|
+
error);
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
async function killExistingBrowserStackLocalProcesses() {
|
|
45
|
+
const isRunning = await isBrowserStackLocalRunning();
|
|
46
|
+
if (!isRunning) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
// Check and kill any existing BrowserStackLocal processes before starting new one
|
|
50
|
+
try {
|
|
51
|
+
if (process.platform === "win32") {
|
|
52
|
+
// Check if process exists on Windows
|
|
53
|
+
const checkResult = (0, child_process_1.execSync)('tasklist /FI "IMAGENAME eq BrowserStackLocal.exe"', { encoding: "utf8" });
|
|
54
|
+
if (checkResult.includes("BrowserStackLocal.exe")) {
|
|
55
|
+
(0, child_process_1.execSync)("taskkill /F /IM BrowserStackLocal.exe", { stdio: "ignore" });
|
|
56
|
+
logger_1.default.info("Successfully killed existing BrowserStackLocal processes");
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
// Check if process exists on Unix-like systems
|
|
61
|
+
const checkResult = (0, child_process_1.execSync)("pgrep -f BrowserStackLocal", {
|
|
62
|
+
encoding: "utf8",
|
|
63
|
+
stdio: "pipe",
|
|
64
|
+
}).toString();
|
|
65
|
+
if (checkResult) {
|
|
66
|
+
(0, child_process_1.execSync)("pkill -f BrowserStackLocal", { stdio: "ignore" });
|
|
67
|
+
logger_1.default.info("Successfully killed existing BrowserStackLocal processes");
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
logger_1.default.info(`Error checking/killing BrowserStackLocal processes: ${error}`);
|
|
73
|
+
// Continue execution as there may not be any processes running
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
async function ensureLocalBinarySetup() {
|
|
77
|
+
logger_1.default.info("Ensuring local binary setup as it is required for private URLs...");
|
|
78
|
+
const localBinary = new browserstack_local_1.Local();
|
|
79
|
+
await killExistingBrowserStackLocalProcesses();
|
|
80
|
+
return await new Promise((resolve, reject) => {
|
|
81
|
+
localBinary.start({
|
|
82
|
+
key: config_1.default.browserstackAccessKey,
|
|
83
|
+
username: config_1.default.browserstackUsername,
|
|
84
|
+
}, (error) => {
|
|
85
|
+
if (error) {
|
|
86
|
+
logger_1.default.error(`Unable to start BrowserStack Local... please check your credentials and try again. Error: ${error}`);
|
|
87
|
+
reject(new Error(`Unable to configure local tunnel binary, please check your credentials and try again. Error: ${error}`));
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
logger_1.default.info("Successfully started BrowserStack Local");
|
|
91
|
+
resolve();
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
function isLocalURL(url) {
|
|
97
|
+
try {
|
|
98
|
+
const urlObj = new URL(url);
|
|
99
|
+
const hostname = urlObj.hostname.toLowerCase();
|
|
100
|
+
return (hostname === "localhost" ||
|
|
101
|
+
hostname === "127.0.0.1" ||
|
|
102
|
+
hostname.endsWith(".local") ||
|
|
103
|
+
hostname.endsWith(".localhost"));
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
logger_1.default.error(`Error checking if URL is local: ${error}`);
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.sanitizeUrlParam = sanitizeUrlParam;
|
|
4
|
+
function sanitizeUrlParam(param) {
|
|
5
|
+
// Remove any characters that could be used for command injection
|
|
6
|
+
return param.replace(/[;&|`$(){}[\]<>]/g, "");
|
|
7
|
+
}
|
package/dist/logger.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
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
|
+
const pino_1 = __importDefault(require("pino"));
|
|
7
|
+
let logger;
|
|
8
|
+
if (process.env.NODE_ENV === "development") {
|
|
9
|
+
logger = (0, pino_1.default)({
|
|
10
|
+
level: "debug",
|
|
11
|
+
transport: {
|
|
12
|
+
targets: [
|
|
13
|
+
{
|
|
14
|
+
level: "debug",
|
|
15
|
+
target: "pino-pretty",
|
|
16
|
+
options: {
|
|
17
|
+
colorize: true,
|
|
18
|
+
levelFirst: true,
|
|
19
|
+
destination: process.platform === "win32"
|
|
20
|
+
? "C:\\Windows\\Temp\\browserstack-mcp-server.log"
|
|
21
|
+
: "/tmp/browserstack-mcp-server.log",
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
],
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
// NULL logger
|
|
30
|
+
logger = (0, pino_1.default)({
|
|
31
|
+
level: "info",
|
|
32
|
+
transport: {
|
|
33
|
+
target: "pino/file",
|
|
34
|
+
options: {
|
|
35
|
+
destination: process.platform === "win32" ? "NUL" : "/dev/null",
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
exports.default = logger;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.default = addAccessibilityTools;
|
|
4
|
+
const zod_1 = require("zod");
|
|
5
|
+
const accessibility_1 = require("./accessiblity-utils/accessibility");
|
|
6
|
+
async function runAccessibilityScan(name, pageURL) {
|
|
7
|
+
try {
|
|
8
|
+
const response = await (0, accessibility_1.startAccessibilityScan)(name, [pageURL]);
|
|
9
|
+
const scanId = response.data?.id;
|
|
10
|
+
const scanRunId = response.data?.scanRunId;
|
|
11
|
+
if (!scanId || !scanRunId) {
|
|
12
|
+
throw new Error("Unable to start a accessibility scan, please try again later or open an issue on GitHub if the problem persists");
|
|
13
|
+
}
|
|
14
|
+
return {
|
|
15
|
+
content: [
|
|
16
|
+
{
|
|
17
|
+
type: "text",
|
|
18
|
+
text: `Successfully queued accessibility scan, you will get a report via email within 5 minutes.`,
|
|
19
|
+
},
|
|
20
|
+
],
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
return {
|
|
25
|
+
content: [
|
|
26
|
+
{
|
|
27
|
+
type: "text",
|
|
28
|
+
text: `Failed to start accessibility scan: ${error instanceof Error ? error.message : "Unknown error"}. Please open an issue on GitHub if the problem persists`,
|
|
29
|
+
isError: true,
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
isError: true,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function addAccessibilityTools(server) {
|
|
37
|
+
server.tool("startAccessibilityScan", "Use this tool to start an accessibility scan for a list of URLs on BrowserStack.", {
|
|
38
|
+
name: zod_1.z.string().describe("Name of the accessibility scan"),
|
|
39
|
+
pageURL: zod_1.z.string().describe("The URL to scan for accessibility issues"),
|
|
40
|
+
}, async (args) => {
|
|
41
|
+
return runAccessibilityScan(args.name, args.pageURL);
|
|
42
|
+
});
|
|
43
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
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.startAccessibilityScan = startAccessibilityScan;
|
|
7
|
+
exports.pollScanStatus = pollScanStatus;
|
|
8
|
+
exports.waitUntilScanComplete = waitUntilScanComplete;
|
|
9
|
+
const axios_1 = __importDefault(require("axios"));
|
|
10
|
+
const config_js_1 = __importDefault(require("../../config.js"));
|
|
11
|
+
const axios_2 = require("axios");
|
|
12
|
+
async function startAccessibilityScan(name, urlList) {
|
|
13
|
+
try {
|
|
14
|
+
const response = await axios_1.default.post("https://api-accessibility.browserstack.com/api/website-scanner/v1/scans", {
|
|
15
|
+
name,
|
|
16
|
+
urlList,
|
|
17
|
+
recurring: false,
|
|
18
|
+
}, {
|
|
19
|
+
auth: {
|
|
20
|
+
username: config_js_1.default.browserstackUsername,
|
|
21
|
+
password: config_js_1.default.browserstackAccessKey,
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
if (!response.data.success) {
|
|
25
|
+
throw new Error(`Unable to create an accessibility scan: ${response.data.errors?.join(", ")}`);
|
|
26
|
+
}
|
|
27
|
+
return response.data;
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
if (error instanceof axios_2.AxiosError) {
|
|
31
|
+
if (error.response?.data?.error) {
|
|
32
|
+
throw new Error(`Failed to start accessibility scan: ${error.response?.data?.error}`);
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
throw new Error(`Failed to start accessibility scan: ${error.response?.data?.message || error.message}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
async function pollScanStatus(scanId, scanRunId) {
|
|
42
|
+
try {
|
|
43
|
+
const response = await axios_1.default.get(`https://api-accessibility.browserstack.com/api/website-scanner/v1/scans/${scanId}/scan_runs/${scanRunId}/status`, {
|
|
44
|
+
auth: {
|
|
45
|
+
username: config_js_1.default.browserstackUsername,
|
|
46
|
+
password: config_js_1.default.browserstackAccessKey,
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
if (!response.data.success) {
|
|
50
|
+
throw new Error(`Failed to get scan status: ${response.data.errors?.join(", ")}`);
|
|
51
|
+
}
|
|
52
|
+
return response.data.data?.status || "unknown";
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
if (error instanceof axios_2.AxiosError) {
|
|
56
|
+
throw new Error(`Failed to get scan status: ${error.response?.data?.message || error.message}`);
|
|
57
|
+
}
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
async function waitUntilScanComplete(scanId, scanRunId) {
|
|
62
|
+
return new Promise((resolve, reject) => {
|
|
63
|
+
const interval = setInterval(async () => {
|
|
64
|
+
try {
|
|
65
|
+
const status = await pollScanStatus(scanId, scanRunId);
|
|
66
|
+
if (status === "completed") {
|
|
67
|
+
clearInterval(interval);
|
|
68
|
+
resolve();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
clearInterval(interval);
|
|
73
|
+
reject(error);
|
|
74
|
+
}
|
|
75
|
+
}, 5000); // Poll every 5 seconds
|
|
76
|
+
// Set a timeout of 5 minutes
|
|
77
|
+
setTimeout(() => {
|
|
78
|
+
clearInterval(interval);
|
|
79
|
+
reject(new Error("Scan timed out after 5 minutes"));
|
|
80
|
+
}, 5 * 60 * 1000);
|
|
81
|
+
});
|
|
82
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SUPPORTED_LIVE_CONFIGURATIONS = void 0;
|
|
4
|
+
exports.SUPPORTED_LIVE_CONFIGURATIONS = {
|
|
5
|
+
android: {
|
|
6
|
+
chrome: {
|
|
7
|
+
version: "100",
|
|
8
|
+
},
|
|
9
|
+
firefox: {
|
|
10
|
+
version: "100",
|
|
11
|
+
},
|
|
12
|
+
safari: {
|
|
13
|
+
version: "100",
|
|
14
|
+
},
|
|
15
|
+
"samsung browser": {
|
|
16
|
+
version: "100",
|
|
17
|
+
},
|
|
18
|
+
"internet-explorer": {
|
|
19
|
+
version: "100",
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
ios: {
|
|
23
|
+
chrome: {
|
|
24
|
+
version: "100",
|
|
25
|
+
},
|
|
26
|
+
firefox: {
|
|
27
|
+
version: "100",
|
|
28
|
+
},
|
|
29
|
+
edge: {
|
|
30
|
+
version: "100",
|
|
31
|
+
},
|
|
32
|
+
safari: {
|
|
33
|
+
version: "100",
|
|
34
|
+
},
|
|
35
|
+
"samsung browser": {
|
|
36
|
+
version: "100",
|
|
37
|
+
},
|
|
38
|
+
"internet-explorer": {
|
|
39
|
+
version: "100",
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
windows: {
|
|
43
|
+
chrome: {
|
|
44
|
+
version: "100",
|
|
45
|
+
},
|
|
46
|
+
firefox: {
|
|
47
|
+
version: "100",
|
|
48
|
+
},
|
|
49
|
+
edge: {
|
|
50
|
+
version: "100",
|
|
51
|
+
},
|
|
52
|
+
safari: {
|
|
53
|
+
version: "100",
|
|
54
|
+
},
|
|
55
|
+
"samsung browser": {
|
|
56
|
+
version: "100",
|
|
57
|
+
},
|
|
58
|
+
"internet-explorer": {
|
|
59
|
+
version: "100",
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
macos: {
|
|
63
|
+
chrome: {
|
|
64
|
+
version: "100",
|
|
65
|
+
},
|
|
66
|
+
firefox: {
|
|
67
|
+
version: "100",
|
|
68
|
+
},
|
|
69
|
+
edge: {
|
|
70
|
+
version: "100",
|
|
71
|
+
},
|
|
72
|
+
safari: {
|
|
73
|
+
version: "100",
|
|
74
|
+
},
|
|
75
|
+
"samsung browser": {
|
|
76
|
+
version: "100",
|
|
77
|
+
},
|
|
78
|
+
"internet-explorer": {
|
|
79
|
+
version: "100",
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
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.startSession = startSession;
|
|
7
|
+
const child_process_1 = __importDefault(require("child_process"));
|
|
8
|
+
const logger_1 = __importDefault(require("../../logger"));
|
|
9
|
+
const utils_1 = require("../../lib/utils");
|
|
10
|
+
async function startSession(args) {
|
|
11
|
+
// Sanitize all input parameters
|
|
12
|
+
const sanitizedArgs = {
|
|
13
|
+
appUrl: (0, utils_1.sanitizeUrlParam)(args.appUrl),
|
|
14
|
+
desiredPlatform: (0, utils_1.sanitizeUrlParam)(args.desiredPlatform),
|
|
15
|
+
desiredPhone: (0, utils_1.sanitizeUrlParam)(args.desiredPhone),
|
|
16
|
+
desiredPlatformVersion: (0, utils_1.sanitizeUrlParam)(args.desiredPlatformVersion),
|
|
17
|
+
};
|
|
18
|
+
// Get app hash ID and format phone name
|
|
19
|
+
const appHashedId = sanitizedArgs.appUrl.split("bs://").pop();
|
|
20
|
+
const desiredPhoneWithSpaces = sanitizedArgs.desiredPhone.replace(/\s+/g, "+");
|
|
21
|
+
// Construct URL with encoded parameters
|
|
22
|
+
const params = new URLSearchParams({
|
|
23
|
+
os: sanitizedArgs.desiredPlatform,
|
|
24
|
+
os_version: sanitizedArgs.desiredPlatformVersion,
|
|
25
|
+
app_hashed_id: appHashedId || "",
|
|
26
|
+
scale_to_fit: "true",
|
|
27
|
+
speed: "1",
|
|
28
|
+
start: "true",
|
|
29
|
+
});
|
|
30
|
+
const launchUrl = `https://app-live.browserstack.com/dashboard#${params.toString()}&device=${desiredPhoneWithSpaces}`;
|
|
31
|
+
try {
|
|
32
|
+
// Use platform-specific commands with proper escaping
|
|
33
|
+
const command = process.platform === "darwin"
|
|
34
|
+
? ["open", launchUrl]
|
|
35
|
+
: process.platform === "win32"
|
|
36
|
+
? ["cmd", "/c", "start", launchUrl]
|
|
37
|
+
: ["xdg-open", launchUrl];
|
|
38
|
+
// nosemgrep:javascript.lang.security.detect-child-process.detect-child-process
|
|
39
|
+
const child = child_process_1.default.spawn(command[0], command.slice(1), {
|
|
40
|
+
stdio: "ignore",
|
|
41
|
+
detached: true,
|
|
42
|
+
});
|
|
43
|
+
// Handle process errors
|
|
44
|
+
child.on("error", (error) => {
|
|
45
|
+
logger_1.default.error(`Failed to open browser automatically: ${error}. Please open this URL manually: ${launchUrl}`);
|
|
46
|
+
});
|
|
47
|
+
// Unref the child process to allow the parent to exit
|
|
48
|
+
child.unref();
|
|
49
|
+
return launchUrl;
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
logger_1.default.error(`Failed to open browser automatically: ${error}. Please open this URL manually: ${launchUrl}`);
|
|
53
|
+
return launchUrl;
|
|
54
|
+
}
|
|
55
|
+
}
|