@browserstack/mcp-server 1.1.9 → 1.2.1
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/README.md +4 -1
- package/dist/config.d.ts +13 -0
- package/dist/config.js +4 -6
- package/dist/index.d.ts +4 -0
- package/dist/index.js +21 -31
- package/dist/lib/api.d.ts +2 -0
- package/dist/lib/api.js +10 -5
- package/dist/lib/apiClient.d.ts +31 -0
- package/dist/lib/apiClient.js +108 -0
- package/dist/lib/constants.d.ts +17 -0
- package/dist/lib/device-cache.d.ts +9 -0
- package/dist/lib/device-cache.js +3 -3
- package/dist/lib/error.d.ts +7 -0
- package/dist/lib/fuzzy.d.ts +1 -0
- package/dist/lib/get-auth.d.ts +2 -0
- package/dist/lib/get-auth.js +8 -0
- package/dist/lib/inmemory-store.d.ts +1 -0
- package/dist/lib/instrumentation.d.ts +4 -0
- package/dist/lib/instrumentation.js +8 -7
- package/dist/lib/local.d.ts +3 -0
- package/dist/lib/local.js +3 -3
- package/dist/lib/types.d.ts +4 -0
- package/dist/lib/types.js +1 -0
- package/dist/lib/utils.d.ts +4 -0
- package/dist/lib/version-resolver.d.ts +6 -0
- package/dist/logger.d.ts +3 -0
- package/dist/logger.js +20 -4
- package/dist/oninitialized.d.ts +2 -0
- package/dist/oninitialized.js +2 -7
- package/dist/server-factory.d.ts +25 -0
- package/dist/server-factory.js +70 -0
- package/dist/tools/accessibility.d.ts +3 -0
- package/dist/tools/accessibility.js +21 -12
- package/dist/tools/accessiblity-utils/accessibility-rag.d.ts +12 -0
- package/dist/tools/accessiblity-utils/accessibility-rag.js +22 -12
- package/dist/tools/accessiblity-utils/report-fetcher.d.ts +8 -0
- package/dist/tools/accessiblity-utils/report-fetcher.js +26 -16
- package/dist/tools/accessiblity-utils/report-parser.d.ts +23 -0
- package/dist/tools/accessiblity-utils/report-parser.js +3 -3
- package/dist/tools/accessiblity-utils/scanner.d.ts +25 -0
- package/dist/tools/accessiblity-utils/scanner.js +43 -24
- package/dist/tools/appautomate-utils/appautomate.d.ts +42 -0
- package/dist/tools/appautomate-utils/appautomate.js +63 -38
- package/dist/tools/appautomate-utils/types.d.ts +5 -0
- package/dist/tools/appautomate.d.ts +3 -0
- package/dist/tools/appautomate.js +24 -21
- package/dist/tools/applive-utils/device-search.d.ts +6 -0
- package/dist/tools/applive-utils/start-session.d.ts +15 -0
- package/dist/tools/applive-utils/start-session.js +11 -4
- package/dist/tools/applive-utils/types.d.ts +7 -0
- package/dist/tools/applive-utils/upload-app.d.ts +5 -0
- package/dist/tools/applive-utils/upload-app.js +8 -12
- package/dist/tools/applive-utils/version-utils.d.ts +4 -0
- package/dist/tools/applive.d.ts +13 -0
- package/dist/tools/applive.js +9 -7
- package/dist/tools/automate-utils/fetch-screenshots.d.ts +6 -0
- package/dist/tools/automate-utils/fetch-screenshots.js +16 -12
- package/dist/tools/automate.d.ts +9 -0
- package/dist/tools/automate.js +9 -7
- package/dist/tools/bstack-sdk.d.ts +17 -0
- package/dist/tools/bstack-sdk.js +15 -8
- package/dist/tools/failurelogs-utils/app-automate.d.ts +7 -0
- package/dist/tools/failurelogs-utils/app-automate.js +29 -11
- package/dist/tools/failurelogs-utils/automate.d.ts +6 -0
- package/dist/tools/failurelogs-utils/automate.js +27 -12
- package/dist/tools/failurelogs-utils/utils.d.ts +30 -0
- package/dist/tools/getFailureLogs.d.ts +14 -0
- package/dist/tools/getFailureLogs.js +15 -16
- package/dist/tools/live-utils/desktop-filter.d.ts +2 -0
- package/dist/tools/live-utils/mobile-filter.d.ts +2 -0
- package/dist/tools/live-utils/start-session.d.ts +6 -0
- package/dist/tools/live-utils/start-session.js +18 -5
- package/dist/tools/live-utils/types.d.ts +33 -0
- package/dist/tools/live.d.ts +3 -0
- package/dist/tools/live.js +28 -16
- package/dist/tools/observability.d.ts +5 -0
- package/dist/tools/observability.js +14 -11
- package/dist/tools/sdk-utils/commands.d.ts +3 -0
- package/dist/tools/sdk-utils/commands.js +5 -4
- package/dist/tools/sdk-utils/constants.d.ts +2 -0
- package/dist/tools/sdk-utils/constants.js +82 -21
- package/dist/tools/sdk-utils/instructions.d.ts +6 -0
- package/dist/tools/sdk-utils/instructions.js +8 -6
- package/dist/tools/sdk-utils/percy/constants.d.ts +3 -0
- package/dist/tools/sdk-utils/percy/constants.js +1 -0
- package/dist/tools/sdk-utils/percy/instructions.d.ts +10 -0
- package/dist/tools/sdk-utils/percy/types.d.ts +5 -0
- package/dist/tools/sdk-utils/types.d.ts +39 -0
- package/dist/tools/sdk-utils/types.js +1 -0
- package/dist/tools/selfheal-utils/selfheal.d.ts +11 -0
- package/dist/tools/selfheal-utils/selfheal.js +10 -6
- package/dist/tools/selfheal.d.ts +7 -0
- package/dist/tools/selfheal.js +9 -7
- package/dist/tools/testmanagement-utils/TCG-utils/api.d.ts +34 -0
- package/dist/tools/testmanagement-utils/TCG-utils/api.js +57 -44
- package/dist/tools/testmanagement-utils/TCG-utils/config.d.ts +5 -0
- package/dist/tools/testmanagement-utils/TCG-utils/helpers.d.ts +13 -0
- package/dist/tools/testmanagement-utils/TCG-utils/helpers.js +2 -1
- package/dist/tools/testmanagement-utils/TCG-utils/types.d.ts +26 -0
- package/dist/tools/testmanagement-utils/add-test-result.d.ts +42 -0
- package/dist/tools/testmanagement-utils/add-test-result.js +23 -10
- package/dist/tools/testmanagement-utils/create-lca-steps.d.ts +100 -0
- package/dist/tools/testmanagement-utils/create-lca-steps.js +64 -55
- package/dist/tools/testmanagement-utils/create-project-folder.d.ts +31 -0
- package/dist/tools/testmanagement-utils/create-project-folder.js +31 -21
- package/dist/tools/testmanagement-utils/create-testcase.d.ts +122 -0
- package/dist/tools/testmanagement-utils/create-testcase.js +13 -10
- package/dist/tools/testmanagement-utils/create-testrun.d.ts +82 -0
- package/dist/tools/testmanagement-utils/create-testrun.js +11 -8
- package/dist/tools/testmanagement-utils/list-testcases.d.ts +30 -0
- package/dist/tools/testmanagement-utils/list-testcases.js +9 -7
- package/dist/tools/testmanagement-utils/list-testruns.d.ts +22 -0
- package/dist/tools/testmanagement-utils/list-testruns.js +9 -7
- package/dist/tools/testmanagement-utils/poll-lca-status.d.ts +11 -0
- package/dist/tools/testmanagement-utils/poll-lca-status.js +12 -8
- package/dist/tools/testmanagement-utils/testcase-from-file.d.ts +4 -0
- package/dist/tools/testmanagement-utils/testcase-from-file.js +6 -6
- package/dist/tools/testmanagement-utils/update-testrun.d.ts +40 -0
- package/dist/tools/testmanagement-utils/update-testrun.js +11 -7
- package/dist/tools/testmanagement-utils/upload-file.d.ts +20 -0
- package/dist/tools/testmanagement-utils/upload-file.js +8 -6
- package/dist/tools/testmanagement.d.ts +60 -0
- package/dist/tools/testmanagement.js +53 -53
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -25,10 +25,13 @@
|
|
|
25
25
|
<img src="assets/thumbnail.webp">
|
|
26
26
|
</a>
|
|
27
27
|
</div>
|
|
28
|
+
|
|
28
29
|
|
|
29
30
|
Manage test cases, execute manual or automated tests, debug issues, and even fix code—directly within tools like Cursor, Claude, or any MCP-enabled client, using plain English.
|
|
30
31
|
#### Test from anywhere:
|
|
31
32
|
Easily connect the BrowserStack Test Platform to your favourite AI tools, such as IDEs, LLMs, or agentic workflows.
|
|
33
|
+
#### Test with natural language:
|
|
34
|
+
Manage, execute, debug tests, and even fix code using plain English prompts.
|
|
32
35
|
#### Reduced context switching:
|
|
33
36
|
Stay in flow—keep all project context in one place and trigger actions directly from your IDE or LLM.
|
|
34
37
|
|
|
@@ -115,7 +118,7 @@ Create and manage test cases, create test plans and trigger test runs using natu
|
|
|
115
118
|
"update test results as passed for Login tests test run from My Demo Project"
|
|
116
119
|
```
|
|
117
120
|
|
|
118
|
-
### 🧪 Access BrowserStack AI
|
|
121
|
+
### 🧪 Access BrowserStack AI agents
|
|
119
122
|
|
|
120
123
|
Generate test cases from PRDs, convert manual tests to low-code automation, and auto-heal flaky scripts powered by BrowserStack’s AI agents, seamlessly integrated into your workflow. Below are few example prompts to access Browserstack AI agents
|
|
121
124
|
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* USE_OWN_LOCAL_BINARY_PROCESS:
|
|
3
|
+
* If true, the system will not start a new local binary process, but will use the user's own process.
|
|
4
|
+
*/
|
|
5
|
+
export declare class Config {
|
|
6
|
+
readonly DEV_MODE: boolean;
|
|
7
|
+
readonly browserstackLocalOptions: Record<string, any>;
|
|
8
|
+
readonly USE_OWN_LOCAL_BINARY_PROCESS: boolean;
|
|
9
|
+
readonly REMOTE_MCP: boolean;
|
|
10
|
+
constructor(DEV_MODE: boolean, browserstackLocalOptions: Record<string, any>, USE_OWN_LOCAL_BINARY_PROCESS: boolean, REMOTE_MCP: boolean);
|
|
11
|
+
}
|
|
12
|
+
declare const config: Config;
|
|
13
|
+
export default config;
|
package/dist/config.js
CHANGED
|
@@ -33,18 +33,16 @@ for (const key of BROWSERSTACK_LOCAL_OPTION_KEYS) {
|
|
|
33
33
|
* If true, the system will not start a new local binary process, but will use the user's own process.
|
|
34
34
|
*/
|
|
35
35
|
export class Config {
|
|
36
|
-
browserstackUsername;
|
|
37
|
-
browserstackAccessKey;
|
|
38
36
|
DEV_MODE;
|
|
39
37
|
browserstackLocalOptions;
|
|
40
38
|
USE_OWN_LOCAL_BINARY_PROCESS;
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
this.browserstackAccessKey = browserstackAccessKey;
|
|
39
|
+
REMOTE_MCP;
|
|
40
|
+
constructor(DEV_MODE, browserstackLocalOptions, USE_OWN_LOCAL_BINARY_PROCESS, REMOTE_MCP) {
|
|
44
41
|
this.DEV_MODE = DEV_MODE;
|
|
45
42
|
this.browserstackLocalOptions = browserstackLocalOptions;
|
|
46
43
|
this.USE_OWN_LOCAL_BINARY_PROCESS = USE_OWN_LOCAL_BINARY_PROCESS;
|
|
44
|
+
this.REMOTE_MCP = REMOTE_MCP;
|
|
47
45
|
}
|
|
48
46
|
}
|
|
49
|
-
const config = new Config(process.env.
|
|
47
|
+
const config = new Config(process.env.DEV_MODE === "true", browserstackLocalOptions, process.env.USE_OWN_LOCAL_BINARY_PROCESS === "true", process.env.REMOTE_MCP === "true");
|
|
50
48
|
export default config;
|
package/dist/index.d.ts
ADDED
package/dist/index.js
CHANGED
|
@@ -1,47 +1,37 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
2
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
3
|
import { createRequire } from "module";
|
|
5
4
|
const require = createRequire(import.meta.url);
|
|
6
5
|
const packageJson = require("../package.json");
|
|
7
6
|
import "dotenv/config";
|
|
8
7
|
import logger from "./logger.js";
|
|
9
|
-
import
|
|
10
|
-
import addAppLiveTools from "./tools/applive.js";
|
|
11
|
-
import addBrowserLiveTools from "./tools/live.js";
|
|
12
|
-
import addAccessibilityTools from "./tools/accessibility.js";
|
|
13
|
-
import addTestManagementTools from "./tools/testmanagement.js";
|
|
14
|
-
import addAppAutomationTools from "./tools/appautomate.js";
|
|
15
|
-
import addFailureLogsTools from "./tools/getFailureLogs.js";
|
|
16
|
-
import addAutomateTools from "./tools/automate.js";
|
|
17
|
-
import addSelfHealTools from "./tools/selfheal.js";
|
|
18
|
-
import { setupOnInitialized } from "./oninitialized.js";
|
|
19
|
-
function registerTools(server) {
|
|
20
|
-
addAccessibilityTools(server);
|
|
21
|
-
addSDKTools(server);
|
|
22
|
-
addAppLiveTools(server);
|
|
23
|
-
addBrowserLiveTools(server);
|
|
24
|
-
addTestManagementTools(server);
|
|
25
|
-
addAppAutomationTools(server);
|
|
26
|
-
addFailureLogsTools(server);
|
|
27
|
-
addAutomateTools(server);
|
|
28
|
-
addSelfHealTools(server);
|
|
29
|
-
}
|
|
30
|
-
// Create an MCP server
|
|
31
|
-
const server = new McpServer({
|
|
32
|
-
name: "BrowserStack MCP Server",
|
|
33
|
-
version: packageJson.version,
|
|
34
|
-
});
|
|
35
|
-
setupOnInitialized(server);
|
|
36
|
-
registerTools(server);
|
|
8
|
+
import { BrowserStackMcpServer } from "./server-factory.js";
|
|
37
9
|
async function main() {
|
|
38
10
|
logger.info("Launching BrowserStack MCP server, version %s", packageJson.version);
|
|
39
|
-
|
|
11
|
+
const remoteMCP = process.env.REMOTE_MCP === "true";
|
|
12
|
+
if (remoteMCP) {
|
|
13
|
+
logger.info("Running in remote MCP mode");
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
const username = process.env.BROWSERSTACK_USERNAME;
|
|
17
|
+
const accessKey = process.env.BROWSERSTACK_ACCESS_KEY;
|
|
18
|
+
if (!username) {
|
|
19
|
+
throw new Error("BROWSERSTACK_USERNAME environment variable is required");
|
|
20
|
+
}
|
|
21
|
+
if (!accessKey) {
|
|
22
|
+
throw new Error("BROWSERSTACK_ACCESS_KEY environment variable is required");
|
|
23
|
+
}
|
|
40
24
|
const transport = new StdioServerTransport();
|
|
41
|
-
|
|
25
|
+
const mcpServer = new BrowserStackMcpServer({
|
|
26
|
+
"browserstack-username": username,
|
|
27
|
+
"browserstack-access-key": accessKey,
|
|
28
|
+
});
|
|
29
|
+
await mcpServer.getInstance().connect(transport);
|
|
42
30
|
}
|
|
43
31
|
main().catch(console.error);
|
|
44
32
|
// Ensure logs are flushed before exit
|
|
45
33
|
process.on("exit", () => {
|
|
46
34
|
logger.flush();
|
|
47
35
|
});
|
|
36
|
+
export { setLogger } from "./logger.js";
|
|
37
|
+
export { BrowserStackMcpServer } from "./server-factory.js";
|
package/dist/lib/api.js
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
1
|
+
import { getBrowserStackAuth } from "./get-auth.js";
|
|
2
|
+
import { apiClient } from "./apiClient.js";
|
|
3
|
+
export async function getLatestO11YBuildInfo(buildName, projectName, config) {
|
|
3
4
|
const buildsUrl = `https://api-observability.browserstack.com/ext/v1/builds/latest?build_name=${encodeURIComponent(buildName)}&project_name=${encodeURIComponent(projectName)}`;
|
|
4
|
-
const
|
|
5
|
+
const authString = getBrowserStackAuth(config);
|
|
6
|
+
const auth = Buffer.from(authString).toString("base64");
|
|
7
|
+
const buildsResponse = await apiClient.get({
|
|
8
|
+
url: buildsUrl,
|
|
5
9
|
headers: {
|
|
6
|
-
Authorization: `Basic ${
|
|
10
|
+
Authorization: `Basic ${auth}`,
|
|
7
11
|
},
|
|
12
|
+
raise_error: false,
|
|
8
13
|
});
|
|
9
14
|
if (!buildsResponse.ok) {
|
|
10
15
|
if (buildsResponse.statusText === "Unauthorized") {
|
|
@@ -12,5 +17,5 @@ export async function getLatestO11YBuildInfo(buildName, projectName) {
|
|
|
12
17
|
}
|
|
13
18
|
throw new Error(`Failed to fetch builds: ${buildsResponse.statusText}`);
|
|
14
19
|
}
|
|
15
|
-
return buildsResponse
|
|
20
|
+
return buildsResponse;
|
|
16
21
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { AxiosRequestConfig, AxiosResponse } from "axios";
|
|
2
|
+
type RequestOptions = {
|
|
3
|
+
url: string;
|
|
4
|
+
headers?: Record<string, string>;
|
|
5
|
+
params?: Record<string, string | number>;
|
|
6
|
+
body?: any;
|
|
7
|
+
raise_error?: boolean;
|
|
8
|
+
};
|
|
9
|
+
declare class ApiResponse<T = any> {
|
|
10
|
+
private _response;
|
|
11
|
+
constructor(response: AxiosResponse<T>);
|
|
12
|
+
get data(): T;
|
|
13
|
+
get status(): number;
|
|
14
|
+
get statusText(): string;
|
|
15
|
+
get headers(): Record<string, string>;
|
|
16
|
+
get config(): AxiosRequestConfig;
|
|
17
|
+
get url(): string | undefined;
|
|
18
|
+
get ok(): boolean;
|
|
19
|
+
}
|
|
20
|
+
declare class ApiClient {
|
|
21
|
+
private instance;
|
|
22
|
+
private get axiosAgent();
|
|
23
|
+
private requestWrapper;
|
|
24
|
+
get<T = any>({ url, headers, params, raise_error, }: RequestOptions): Promise<ApiResponse<T>>;
|
|
25
|
+
post<T = any>({ url, headers, body, raise_error, }: RequestOptions): Promise<ApiResponse<T>>;
|
|
26
|
+
put<T = any>({ url, headers, body, raise_error, }: RequestOptions): Promise<ApiResponse<T>>;
|
|
27
|
+
patch<T = any>({ url, headers, body, raise_error, }: RequestOptions): Promise<ApiResponse<T>>;
|
|
28
|
+
delete<T = any>({ url, headers, params, raise_error, }: RequestOptions): Promise<ApiResponse<T>>;
|
|
29
|
+
}
|
|
30
|
+
export declare const apiClient: ApiClient;
|
|
31
|
+
export type { ApiResponse, RequestOptions };
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import axios from "axios";
|
|
2
|
+
import httpsProxyAgentPkg from "https-proxy-agent";
|
|
3
|
+
const { HttpsProxyAgent } = httpsProxyAgentPkg;
|
|
4
|
+
import * as https from "https";
|
|
5
|
+
import * as fs from "fs";
|
|
6
|
+
import config from "../config.js";
|
|
7
|
+
class ApiResponse {
|
|
8
|
+
_response;
|
|
9
|
+
constructor(response) {
|
|
10
|
+
this._response = response;
|
|
11
|
+
}
|
|
12
|
+
get data() {
|
|
13
|
+
return this._response.data;
|
|
14
|
+
}
|
|
15
|
+
get status() {
|
|
16
|
+
return this._response.status;
|
|
17
|
+
}
|
|
18
|
+
get statusText() {
|
|
19
|
+
return this._response.statusText;
|
|
20
|
+
}
|
|
21
|
+
get headers() {
|
|
22
|
+
const raw = this._response.headers;
|
|
23
|
+
const sanitized = {};
|
|
24
|
+
for (const key in raw) {
|
|
25
|
+
const value = raw[key];
|
|
26
|
+
if (typeof value === "string") {
|
|
27
|
+
sanitized[key] = value;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return sanitized;
|
|
31
|
+
}
|
|
32
|
+
get config() {
|
|
33
|
+
return this._response.config;
|
|
34
|
+
}
|
|
35
|
+
get url() {
|
|
36
|
+
return this._response.config.url;
|
|
37
|
+
}
|
|
38
|
+
get ok() {
|
|
39
|
+
return this._response.status >= 200 && this._response.status < 300;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
// Utility to create HTTPS agent if needed (proxy/CA)
|
|
43
|
+
function getAxiosAgent() {
|
|
44
|
+
const proxyHost = config.browserstackLocalOptions.proxyHost;
|
|
45
|
+
const proxyPort = config.browserstackLocalOptions.proxyPort;
|
|
46
|
+
const caCertPath = config.browserstackLocalOptions.useCaCertificate;
|
|
47
|
+
// If both proxy host and port are defined
|
|
48
|
+
if (proxyHost && proxyPort) {
|
|
49
|
+
const proxyUrl = `http://${proxyHost}:${proxyPort}`;
|
|
50
|
+
if (caCertPath && fs.existsSync(caCertPath)) {
|
|
51
|
+
// Proxy + CA cert
|
|
52
|
+
const ca = fs.readFileSync(caCertPath);
|
|
53
|
+
return new HttpsProxyAgent({
|
|
54
|
+
host: proxyHost,
|
|
55
|
+
port: Number(proxyPort),
|
|
56
|
+
ca,
|
|
57
|
+
rejectUnauthorized: false, // Set to true if you want strict SSL
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
// Proxy only
|
|
62
|
+
return new HttpsProxyAgent(proxyUrl);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
else if (caCertPath && fs.existsSync(caCertPath)) {
|
|
66
|
+
// CA only
|
|
67
|
+
return new https.Agent({
|
|
68
|
+
ca: fs.readFileSync(caCertPath),
|
|
69
|
+
rejectUnauthorized: false, // Set to true for strict SSL
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
// Default agent (no proxy, no CA)
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
class ApiClient {
|
|
76
|
+
instance = axios.create();
|
|
77
|
+
get axiosAgent() {
|
|
78
|
+
return getAxiosAgent();
|
|
79
|
+
}
|
|
80
|
+
async requestWrapper(fn, raise_error = true) {
|
|
81
|
+
try {
|
|
82
|
+
const res = await fn(this.axiosAgent);
|
|
83
|
+
return new ApiResponse(res);
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
if (error.response && !raise_error) {
|
|
87
|
+
return new ApiResponse(error.response);
|
|
88
|
+
}
|
|
89
|
+
throw error;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
async get({ url, headers, params, raise_error = true, }) {
|
|
93
|
+
return this.requestWrapper((agent) => this.instance.get(url, { headers, params, httpsAgent: agent }), raise_error);
|
|
94
|
+
}
|
|
95
|
+
async post({ url, headers, body, raise_error = true, }) {
|
|
96
|
+
return this.requestWrapper((agent) => this.instance.post(url, body, { headers, httpsAgent: agent }), raise_error);
|
|
97
|
+
}
|
|
98
|
+
async put({ url, headers, body, raise_error = true, }) {
|
|
99
|
+
return this.requestWrapper((agent) => this.instance.put(url, body, { headers, httpsAgent: agent }), raise_error);
|
|
100
|
+
}
|
|
101
|
+
async patch({ url, headers, body, raise_error = true, }) {
|
|
102
|
+
return this.requestWrapper((agent) => this.instance.patch(url, body, { headers, httpsAgent: agent }), raise_error);
|
|
103
|
+
}
|
|
104
|
+
async delete({ url, headers, params, raise_error = true, }) {
|
|
105
|
+
return this.requestWrapper((agent) => this.instance.delete(url, { headers, params, httpsAgent: agent }), raise_error);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
export const apiClient = new ApiClient();
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export declare const SessionType: {
|
|
2
|
+
readonly Automate: "automate";
|
|
3
|
+
readonly AppAutomate: "app-automate";
|
|
4
|
+
};
|
|
5
|
+
export declare const AutomateLogType: {
|
|
6
|
+
readonly NetworkLogs: "networkLogs";
|
|
7
|
+
readonly SessionLogs: "sessionLogs";
|
|
8
|
+
readonly ConsoleLogs: "consoleLogs";
|
|
9
|
+
};
|
|
10
|
+
export declare const AppAutomateLogType: {
|
|
11
|
+
readonly DeviceLogs: "deviceLogs";
|
|
12
|
+
readonly AppiumLogs: "appiumLogs";
|
|
13
|
+
readonly CrashLogs: "crashLogs";
|
|
14
|
+
};
|
|
15
|
+
export type SessionType = (typeof SessionType)[keyof typeof SessionType];
|
|
16
|
+
export type AutomateLogType = (typeof AutomateLogType)[keyof typeof AutomateLogType];
|
|
17
|
+
export type AppAutomateLogType = (typeof AppAutomateLogType)[keyof typeof AppAutomateLogType];
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare enum BrowserStackProducts {
|
|
2
|
+
LIVE = "live",
|
|
3
|
+
APP_LIVE = "app_live",
|
|
4
|
+
APP_AUTOMATE = "app_automate"
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Fetches and caches BrowserStack datasets (live + app_live + app_automate) with a shared TTL.
|
|
8
|
+
*/
|
|
9
|
+
export declare function getDevicesAndBrowsers(type: BrowserStackProducts): Promise<any>;
|
package/dist/lib/device-cache.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
2
|
import os from "os";
|
|
3
3
|
import path from "path";
|
|
4
|
+
import { apiClient } from "./apiClient.js";
|
|
4
5
|
const CACHE_DIR = path.join(os.homedir(), ".browserstack", "combined_cache");
|
|
5
6
|
const CACHE_FILE = path.join(CACHE_DIR, "data.json");
|
|
6
7
|
const TTL_MS = 24 * 60 * 60 * 1000; // 1 day
|
|
@@ -38,13 +39,12 @@ export async function getDevicesAndBrowsers(type) {
|
|
|
38
39
|
}
|
|
39
40
|
}
|
|
40
41
|
}
|
|
41
|
-
const liveRes = await
|
|
42
|
+
const liveRes = await apiClient.get({ url: URLS[type], raise_error: false });
|
|
42
43
|
if (!liveRes.ok) {
|
|
43
44
|
throw new Error(`Failed to fetch configuration from BrowserStack : ${type}=${liveRes.statusText}`);
|
|
44
45
|
}
|
|
45
|
-
const data = await liveRes.json();
|
|
46
46
|
cache = {
|
|
47
|
-
[type]: data,
|
|
47
|
+
[type]: liveRes.data,
|
|
48
48
|
};
|
|
49
49
|
fs.writeFileSync(CACHE_FILE, JSON.stringify(cache), "utf8");
|
|
50
50
|
return cache[type];
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Formats an AxiosError into a CallToolResult with an appropriate message.
|
|
4
|
+
* @param err - The error object to format
|
|
5
|
+
* @param defaultText - The fallback error message
|
|
6
|
+
*/
|
|
7
|
+
export declare function formatAxiosError(err: unknown, defaultText: string): CallToolResult;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function customFuzzySearch<T>(list: T[], keys: Array<keyof T | string>, query: string, limit?: number, maxDistance?: number): T[];
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export function getBrowserStackAuth(config) {
|
|
2
|
+
const username = config["browserstack-username"];
|
|
3
|
+
const accessKey = config["browserstack-access-key"];
|
|
4
|
+
if (!username || !accessKey) {
|
|
5
|
+
throw new Error("BrowserStack credentials not set on server.authHeaders");
|
|
6
|
+
}
|
|
7
|
+
return `${username}:${accessKey}`;
|
|
8
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const signedUrlMap: Map<string, object>;
|
|
@@ -1,14 +1,10 @@
|
|
|
1
1
|
import logger from "../logger.js";
|
|
2
|
-
import
|
|
2
|
+
import { getBrowserStackAuth } from "./get-auth.js";
|
|
3
3
|
import { createRequire } from "module";
|
|
4
4
|
const require = createRequire(import.meta.url);
|
|
5
5
|
const packageJson = require("../../package.json");
|
|
6
6
|
import axios from "axios";
|
|
7
|
-
export function trackMCP(toolName, clientInfo, error) {
|
|
8
|
-
if (config.DEV_MODE) {
|
|
9
|
-
logger.info("Tracking MCP is disabled in dev mode");
|
|
10
|
-
return;
|
|
11
|
-
}
|
|
7
|
+
export function trackMCP(toolName, clientInfo, error, config) {
|
|
12
8
|
const instrumentationEndpoint = "https://api.browserstack.com/sdk/v1/event";
|
|
13
9
|
const isSuccess = !error;
|
|
14
10
|
const mcpClient = clientInfo?.name || "unknown";
|
|
@@ -35,11 +31,16 @@ export function trackMCP(toolName, clientInfo, error) {
|
|
|
35
31
|
event.event_properties.error_type =
|
|
36
32
|
error instanceof Error ? error.constructor.name : "Unknown";
|
|
37
33
|
}
|
|
34
|
+
let authHeader = undefined;
|
|
35
|
+
if (config) {
|
|
36
|
+
const authString = getBrowserStackAuth(config);
|
|
37
|
+
authHeader = `Basic ${Buffer.from(authString).toString("base64")}`;
|
|
38
|
+
}
|
|
38
39
|
axios
|
|
39
40
|
.post(instrumentationEndpoint, event, {
|
|
40
41
|
headers: {
|
|
41
42
|
"Content-Type": "application/json",
|
|
42
|
-
Authorization:
|
|
43
|
+
...(authHeader ? { Authorization: authHeader } : {}),
|
|
43
44
|
},
|
|
44
45
|
timeout: 2000,
|
|
45
46
|
})
|
package/dist/lib/local.js
CHANGED
|
@@ -65,7 +65,7 @@ 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(localIdentifier) {
|
|
68
|
+
export async function ensureLocalBinarySetup(username, password, localIdentifier) {
|
|
69
69
|
logger.info("Ensuring local binary setup as it is required for private URLs...");
|
|
70
70
|
if (config.USE_OWN_LOCAL_BINARY_PROCESS) {
|
|
71
71
|
logger.info("Using user's own BrowserStack Local binary process, checking if it's running...");
|
|
@@ -81,8 +81,8 @@ export async function ensureLocalBinarySetup(localIdentifier) {
|
|
|
81
81
|
// Use a single options object from config and extend with required fields
|
|
82
82
|
const bsLocalArgs = {
|
|
83
83
|
...(config.browserstackLocalOptions || {}),
|
|
84
|
-
key:
|
|
85
|
-
username
|
|
84
|
+
key: password,
|
|
85
|
+
username,
|
|
86
86
|
};
|
|
87
87
|
if (localIdentifier) {
|
|
88
88
|
bsLocalArgs.localIdentifier = localIdentifier;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ApiResponse } from "./apiClient.js";
|
|
2
|
+
export declare function sanitizeUrlParam(param: string): string;
|
|
3
|
+
export declare function maybeCompressBase64(base64: string): Promise<string>;
|
|
4
|
+
export declare function assertOkResponse(response: Response | ApiResponse, action: string): Promise<void>;
|
package/dist/logger.d.ts
ADDED
package/dist/logger.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { pino } from "pino";
|
|
2
|
-
|
|
2
|
+
// 1. The actual logger instance, swapped out as needed
|
|
3
|
+
let currentLogger;
|
|
3
4
|
if (process.env.NODE_ENV === "development") {
|
|
4
|
-
|
|
5
|
+
currentLogger = pino({
|
|
5
6
|
level: "debug",
|
|
6
7
|
transport: {
|
|
7
8
|
targets: [
|
|
@@ -21,8 +22,8 @@ if (process.env.NODE_ENV === "development") {
|
|
|
21
22
|
});
|
|
22
23
|
}
|
|
23
24
|
else {
|
|
24
|
-
//
|
|
25
|
-
|
|
25
|
+
// Null logger (logs go to /dev/null or NUL)
|
|
26
|
+
currentLogger = pino({
|
|
26
27
|
level: "info",
|
|
27
28
|
transport: {
|
|
28
29
|
target: "pino/file",
|
|
@@ -32,4 +33,19 @@ else {
|
|
|
32
33
|
},
|
|
33
34
|
});
|
|
34
35
|
}
|
|
36
|
+
// 2. Proxy logger: always delegates to the currentLogger
|
|
37
|
+
const logger = new Proxy({}, {
|
|
38
|
+
get(_target, prop) {
|
|
39
|
+
// Forward function calls to currentLogger
|
|
40
|
+
if (typeof currentLogger[prop] === "function") {
|
|
41
|
+
return (...args) => currentLogger[prop](...args);
|
|
42
|
+
}
|
|
43
|
+
// Forward property gets
|
|
44
|
+
return currentLogger[prop];
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
// 3. Setter to update the logger instance everywhere
|
|
48
|
+
export function setLogger(customLogger) {
|
|
49
|
+
currentLogger = customLogger;
|
|
50
|
+
}
|
|
35
51
|
export default logger;
|
package/dist/oninitialized.js
CHANGED
|
@@ -1,16 +1,11 @@
|
|
|
1
|
-
import config from "./config.js";
|
|
2
1
|
import { trackMCP } from "./lib/instrumentation.js";
|
|
3
|
-
export function setupOnInitialized(server) {
|
|
2
|
+
export function setupOnInitialized(server, config) {
|
|
4
3
|
const nodeVersion = process.versions.node;
|
|
5
4
|
// Check for Node.js version
|
|
6
5
|
if (nodeVersion < "18.0.0") {
|
|
7
6
|
throw new Error("Node version is not supported. Please upgrade to 18.0.0 or later.");
|
|
8
7
|
}
|
|
9
|
-
// Check for BrowserStack credentials
|
|
10
|
-
if (!config.browserstackUsername || !config.browserstackAccessKey) {
|
|
11
|
-
throw new Error("BrowserStack credentials are missing. Please provide a valid username and access key.");
|
|
12
|
-
}
|
|
13
8
|
server.server.oninitialized = () => {
|
|
14
|
-
trackMCP("started", server.server.getClientVersion());
|
|
9
|
+
trackMCP("started", server.server.getClientVersion(), undefined, config);
|
|
15
10
|
};
|
|
16
11
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { McpServer, RegisteredTool } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { BrowserStackConfig } from "./lib/types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Wrapper class for BrowserStack MCP Server
|
|
5
|
+
* Stores a map of registered tools by name
|
|
6
|
+
*/
|
|
7
|
+
export declare class BrowserStackMcpServer {
|
|
8
|
+
private config;
|
|
9
|
+
server: McpServer;
|
|
10
|
+
tools: Record<string, RegisteredTool>;
|
|
11
|
+
constructor(config: BrowserStackConfig);
|
|
12
|
+
/**
|
|
13
|
+
* Calls each tool-adder function and collects their returned tools
|
|
14
|
+
*/
|
|
15
|
+
private registerTools;
|
|
16
|
+
/**
|
|
17
|
+
* Expose the underlying MCP server instance
|
|
18
|
+
*/
|
|
19
|
+
getInstance(): McpServer;
|
|
20
|
+
/**
|
|
21
|
+
* Get all registered tools
|
|
22
|
+
*/
|
|
23
|
+
getTools(): Record<string, RegisteredTool>;
|
|
24
|
+
getTool(name: string): RegisteredTool | undefined;
|
|
25
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { McpServer, } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { createRequire } from "module";
|
|
3
|
+
const require = createRequire(import.meta.url);
|
|
4
|
+
const packageJson = require("../package.json");
|
|
5
|
+
import logger from "./logger.js";
|
|
6
|
+
import addSDKTools from "./tools/bstack-sdk.js";
|
|
7
|
+
import addBrowserLiveTools from "./tools/live.js";
|
|
8
|
+
import addAccessibilityTools from "./tools/accessibility.js";
|
|
9
|
+
import addTestManagementTools from "./tools/testmanagement.js";
|
|
10
|
+
import addAppAutomationTools from "./tools/appautomate.js";
|
|
11
|
+
import addFailureLogsTools from "./tools/getFailureLogs.js";
|
|
12
|
+
import addAutomateTools from "./tools/automate.js";
|
|
13
|
+
import addSelfHealTools from "./tools/selfheal.js";
|
|
14
|
+
import addAppLiveTools from "./tools/applive.js";
|
|
15
|
+
import { setupOnInitialized } from "./oninitialized.js";
|
|
16
|
+
/**
|
|
17
|
+
* Wrapper class for BrowserStack MCP Server
|
|
18
|
+
* Stores a map of registered tools by name
|
|
19
|
+
*/
|
|
20
|
+
export class BrowserStackMcpServer {
|
|
21
|
+
config;
|
|
22
|
+
server;
|
|
23
|
+
tools = {};
|
|
24
|
+
constructor(config) {
|
|
25
|
+
this.config = config;
|
|
26
|
+
logger.info("Creating BrowserStack MCP Server, version %s", packageJson.version);
|
|
27
|
+
this.server = new McpServer({
|
|
28
|
+
name: "BrowserStack MCP Server",
|
|
29
|
+
version: packageJson.version,
|
|
30
|
+
});
|
|
31
|
+
setupOnInitialized(this.server, this.config);
|
|
32
|
+
this.registerTools();
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Calls each tool-adder function and collects their returned tools
|
|
36
|
+
*/
|
|
37
|
+
registerTools() {
|
|
38
|
+
const toolAdders = [
|
|
39
|
+
addAccessibilityTools,
|
|
40
|
+
addSDKTools,
|
|
41
|
+
addAppLiveTools,
|
|
42
|
+
addBrowserLiveTools,
|
|
43
|
+
addTestManagementTools,
|
|
44
|
+
addAppAutomationTools,
|
|
45
|
+
addFailureLogsTools,
|
|
46
|
+
addAutomateTools,
|
|
47
|
+
addSelfHealTools,
|
|
48
|
+
];
|
|
49
|
+
toolAdders.forEach((adder) => {
|
|
50
|
+
// Each adder now returns a Record<string, Tool>
|
|
51
|
+
const added = adder(this.server, this.config);
|
|
52
|
+
Object.assign(this.tools, added);
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Expose the underlying MCP server instance
|
|
57
|
+
*/
|
|
58
|
+
getInstance() {
|
|
59
|
+
return this.server;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Get all registered tools
|
|
63
|
+
*/
|
|
64
|
+
getTools() {
|
|
65
|
+
return this.tools;
|
|
66
|
+
}
|
|
67
|
+
getTool(name) {
|
|
68
|
+
return this.tools[name];
|
|
69
|
+
}
|
|
70
|
+
}
|