@browserstack/mcp-server 1.1.8 → 1.2.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/README.md +71 -35
- package/dist/config.d.ts +13 -0
- package/dist/config.js +10 -6
- package/dist/index.d.ts +4 -0
- package/dist/index.js +20 -30
- 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 +12 -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 +3 -0
- package/dist/server-factory.js +37 -0
- package/dist/tools/accessibility.d.ts +3 -0
- package/dist/tools/accessibility.js +17 -10
- 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 +46 -24
- package/dist/tools/appautomate-utils/appautomate.d.ts +42 -0
- package/dist/tools/appautomate-utils/appautomate.js +95 -9
- package/dist/tools/appautomate-utils/types.d.ts +5 -0
- package/dist/tools/appautomate-utils/types.js +6 -0
- package/dist/tools/appautomate.d.ts +3 -0
- package/dist/tools/appautomate.js +109 -13
- 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 +6 -6
- 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 +6 -6
- package/dist/tools/bstack-sdk.d.ts +17 -0
- package/dist/tools/bstack-sdk.js +47 -20
- 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 +11 -11
- 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 +11 -11
- 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 +20 -5
- package/dist/tools/sdk-utils/constants.d.ts +2 -0
- package/dist/tools/sdk-utils/constants.js +284 -160
- package/dist/tools/sdk-utils/instructions.d.ts +6 -0
- package/dist/tools/sdk-utils/instructions.js +28 -6
- package/dist/tools/sdk-utils/percy/constants.d.ts +3 -0
- package/dist/tools/sdk-utils/percy/constants.js +36 -25
- 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 +3 -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 +6 -6
- 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 +51 -53
- package/package.json +1 -1
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
|
-
import axios from "axios";
|
|
3
|
-
import config from "../../config.js";
|
|
4
2
|
import FormData from "form-data";
|
|
3
|
+
import { apiClient } from "../../lib/apiClient.js";
|
|
5
4
|
import { customFuzzySearch } from "../../lib/fuzzy.js";
|
|
6
5
|
/**
|
|
7
6
|
* Finds devices that exactly match the provided display name.
|
|
@@ -70,26 +69,113 @@ export function validateArgs(args) {
|
|
|
70
69
|
/**
|
|
71
70
|
* Uploads an application file to AppAutomate and returns the app URL
|
|
72
71
|
*/
|
|
73
|
-
export async function uploadApp(appPath) {
|
|
72
|
+
export async function uploadApp(appPath, username, password) {
|
|
74
73
|
const filePath = appPath;
|
|
75
74
|
if (!fs.existsSync(filePath)) {
|
|
76
75
|
throw new Error(`File not found at path: ${filePath}`);
|
|
77
76
|
}
|
|
78
77
|
const formData = new FormData();
|
|
79
78
|
formData.append("file", fs.createReadStream(filePath));
|
|
80
|
-
const response = await
|
|
79
|
+
const response = await apiClient.post({
|
|
80
|
+
url: "https://api-cloud.browserstack.com/app-automate/upload",
|
|
81
81
|
headers: {
|
|
82
82
|
...formData.getHeaders(),
|
|
83
|
+
Authorization: "Basic " + Buffer.from(`${username}:${password}`).toString("base64"),
|
|
83
84
|
},
|
|
84
|
-
|
|
85
|
-
username: config.browserstackUsername,
|
|
86
|
-
password: config.browserstackAccessKey,
|
|
87
|
-
},
|
|
85
|
+
body: formData,
|
|
88
86
|
});
|
|
89
87
|
if (response.data.app_url) {
|
|
90
88
|
return response.data.app_url;
|
|
91
89
|
}
|
|
92
90
|
else {
|
|
93
|
-
throw new Error(`Failed to upload app: ${response.data}`);
|
|
91
|
+
throw new Error(`Failed to upload app: ${JSON.stringify(response.data)}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// Helper to upload a file to a given BrowserStack endpoint and return a specific property from the response.
|
|
95
|
+
async function uploadFileToBrowserStack(filePath, endpoint, responseKey, config) {
|
|
96
|
+
if (!fs.existsSync(filePath)) {
|
|
97
|
+
throw new Error(`File not found at path: ${filePath}`);
|
|
98
|
+
}
|
|
99
|
+
const formData = new FormData();
|
|
100
|
+
formData.append("file", fs.createReadStream(filePath));
|
|
101
|
+
const authHeader = "Basic " +
|
|
102
|
+
Buffer.from(`${config["browserstack-username"]}:${config["browserstack-access-key"]}`).toString("base64");
|
|
103
|
+
const response = await apiClient.post({
|
|
104
|
+
url: endpoint,
|
|
105
|
+
headers: {
|
|
106
|
+
...formData.getHeaders(),
|
|
107
|
+
Authorization: authHeader,
|
|
108
|
+
},
|
|
109
|
+
body: formData,
|
|
110
|
+
});
|
|
111
|
+
if (response.data[responseKey]) {
|
|
112
|
+
return response.data[responseKey];
|
|
113
|
+
}
|
|
114
|
+
throw new Error(`Failed to upload file: ${JSON.stringify(response.data)}`);
|
|
115
|
+
}
|
|
116
|
+
//Uploads an Android app (.apk or .aab) to BrowserStack Espresso endpoint and returns the app_url
|
|
117
|
+
export async function uploadEspressoApp(appPath, config) {
|
|
118
|
+
return uploadFileToBrowserStack(appPath, "https://api-cloud.browserstack.com/app-automate/espresso/v2/app", "app_url", config);
|
|
119
|
+
}
|
|
120
|
+
//Uploads an Espresso test suite (.apk) to BrowserStack and returns the test_suite_url
|
|
121
|
+
export async function uploadEspressoTestSuite(testSuitePath, config) {
|
|
122
|
+
return uploadFileToBrowserStack(testSuitePath, "https://api-cloud.browserstack.com/app-automate/espresso/v2/test-suite", "test_suite_url", config);
|
|
123
|
+
}
|
|
124
|
+
//Uploads an iOS app (.ipa) to BrowserStack XCUITest endpoint and returns the app_url
|
|
125
|
+
export async function uploadXcuiApp(appPath, config) {
|
|
126
|
+
return uploadFileToBrowserStack(appPath, "https://api-cloud.browserstack.com/app-automate/xcuitest/v2/app", "app_url", config);
|
|
127
|
+
}
|
|
128
|
+
//Uploads an XCUITest test suite (.zip) to BrowserStack and returns the test_suite_url
|
|
129
|
+
export async function uploadXcuiTestSuite(testSuitePath, config) {
|
|
130
|
+
return uploadFileToBrowserStack(testSuitePath, "https://api-cloud.browserstack.com/app-automate/xcuitest/v2/test-suite", "test_suite_url", config);
|
|
131
|
+
}
|
|
132
|
+
// Triggers an Espresso test run on BrowserStack and returns the build_id
|
|
133
|
+
export async function triggerEspressoBuild(app_url, test_suite_url, devices, project) {
|
|
134
|
+
const auth = {
|
|
135
|
+
username: process.env.BROWSERSTACK_USERNAME || "",
|
|
136
|
+
password: process.env.BROWSERSTACK_ACCESS_KEY || "",
|
|
137
|
+
};
|
|
138
|
+
const response = await apiClient.post({
|
|
139
|
+
url: "https://api-cloud.browserstack.com/app-automate/espresso/v2/build",
|
|
140
|
+
headers: {
|
|
141
|
+
Authorization: "Basic " +
|
|
142
|
+
Buffer.from(`${auth.username}:${auth.password}`).toString("base64"),
|
|
143
|
+
"Content-Type": "application/json",
|
|
144
|
+
},
|
|
145
|
+
body: {
|
|
146
|
+
app: app_url,
|
|
147
|
+
testSuite: test_suite_url,
|
|
148
|
+
devices,
|
|
149
|
+
project,
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
if (response.data.build_id) {
|
|
153
|
+
return response.data.build_id;
|
|
154
|
+
}
|
|
155
|
+
throw new Error(`Failed to trigger Espresso build: ${JSON.stringify(response.data)}`);
|
|
156
|
+
}
|
|
157
|
+
// Triggers an XCUITest run on BrowserStack and returns the build_id
|
|
158
|
+
export async function triggerXcuiBuild(app_url, test_suite_url, devices, project, config) {
|
|
159
|
+
const auth = {
|
|
160
|
+
username: config["browserstack-username"],
|
|
161
|
+
password: config["browserstack-access-key"],
|
|
162
|
+
};
|
|
163
|
+
const response = await apiClient.post({
|
|
164
|
+
url: "https://api-cloud.browserstack.com/app-automate/xcuitest/v2/build",
|
|
165
|
+
headers: {
|
|
166
|
+
Authorization: "Basic " +
|
|
167
|
+
Buffer.from(`${auth.username}:${auth.password}`).toString("base64"),
|
|
168
|
+
"Content-Type": "application/json",
|
|
169
|
+
},
|
|
170
|
+
body: {
|
|
171
|
+
app: app_url,
|
|
172
|
+
testSuite: test_suite_url,
|
|
173
|
+
devices,
|
|
174
|
+
project,
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
if (response.data.build_id) {
|
|
178
|
+
return response.data.build_id;
|
|
94
179
|
}
|
|
180
|
+
throw new Error(`Failed to trigger XCUITest build: ${JSON.stringify(response.data)}`);
|
|
95
181
|
}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import logger from "../logger.js";
|
|
3
|
-
import
|
|
3
|
+
import { getBrowserStackAuth } from "../lib/get-auth.js";
|
|
4
4
|
import { trackMCP } from "../lib/instrumentation.js";
|
|
5
5
|
import { maybeCompressBase64 } from "../lib/utils.js";
|
|
6
6
|
import { remote } from "webdriverio";
|
|
7
|
+
import { AppTestPlatform } from "./appautomate-utils/types.js";
|
|
7
8
|
import { getDevicesAndBrowsers, BrowserStackProducts, } from "../lib/device-cache.js";
|
|
8
|
-
import { findMatchingDevice, getDeviceVersions, resolveVersion, validateArgs, uploadApp, } from "./appautomate-utils/appautomate.js";
|
|
9
|
+
import { findMatchingDevice, getDeviceVersions, resolveVersion, validateArgs, uploadApp, uploadEspressoApp, uploadEspressoTestSuite, triggerEspressoBuild, uploadXcuiApp, uploadXcuiTestSuite, triggerXcuiBuild, } from "./appautomate-utils/appautomate.js";
|
|
9
10
|
var Platform;
|
|
10
11
|
(function (Platform) {
|
|
11
12
|
Platform["ANDROID"] = "android";
|
|
@@ -18,7 +19,7 @@ async function takeAppScreenshot(args) {
|
|
|
18
19
|
let driver;
|
|
19
20
|
try {
|
|
20
21
|
validateArgs(args);
|
|
21
|
-
const { desiredPlatform, desiredPhone, appPath } = args;
|
|
22
|
+
const { desiredPlatform, desiredPhone, appPath, config } = args;
|
|
22
23
|
let { desiredPlatformVersion } = args;
|
|
23
24
|
const platforms = (await getDevicesAndBrowsers(BrowserStackProducts.APP_AUTOMATE)).mobile;
|
|
24
25
|
const platformData = platforms.find((p) => p.os === desiredPlatform.toLowerCase());
|
|
@@ -32,7 +33,9 @@ async function takeAppScreenshot(args) {
|
|
|
32
33
|
if (!selectedDevice) {
|
|
33
34
|
throw new Error(`Device "${desiredPhone}" with version ${desiredPlatformVersion} not found.`);
|
|
34
35
|
}
|
|
35
|
-
const
|
|
36
|
+
const authString = getBrowserStackAuth(config);
|
|
37
|
+
const [username, password] = authString.split(":");
|
|
38
|
+
const app_url = await uploadApp(appPath, username, password);
|
|
36
39
|
logger.info(`App uploaded. URL: ${app_url}`);
|
|
37
40
|
const capabilities = {
|
|
38
41
|
platformName: desiredPlatform,
|
|
@@ -41,8 +44,8 @@ async function takeAppScreenshot(args) {
|
|
|
41
44
|
"appium:app": app_url,
|
|
42
45
|
"appium:autoGrantPermissions": true,
|
|
43
46
|
"bstack:options": {
|
|
44
|
-
userName:
|
|
45
|
-
accessKey:
|
|
47
|
+
userName: username,
|
|
48
|
+
accessKey: password,
|
|
46
49
|
appiumVersion: "2.0.1",
|
|
47
50
|
},
|
|
48
51
|
};
|
|
@@ -84,10 +87,52 @@ async function takeAppScreenshot(args) {
|
|
|
84
87
|
}
|
|
85
88
|
}
|
|
86
89
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
90
|
+
//Runs AppAutomate tests on BrowserStack by uploading app and test suite, then triggering a test run.
|
|
91
|
+
async function runAppTestsOnBrowserStack(args, config) {
|
|
92
|
+
switch (args.detectedAutomationFramework) {
|
|
93
|
+
case AppTestPlatform.ESPRESSO: {
|
|
94
|
+
try {
|
|
95
|
+
const app_url = await uploadEspressoApp(args.appPath, config);
|
|
96
|
+
const test_suite_url = await uploadEspressoTestSuite(args.testSuitePath, config);
|
|
97
|
+
const build_id = await triggerEspressoBuild(app_url, test_suite_url, args.devices, args.project);
|
|
98
|
+
return {
|
|
99
|
+
content: [
|
|
100
|
+
{
|
|
101
|
+
type: "text",
|
|
102
|
+
text: `✅ Espresso run started successfully!\n\n🔧 Build ID: ${build_id}\n🔗 View your build: https://app-automate.browserstack.com/builds/${build_id}`,
|
|
103
|
+
},
|
|
104
|
+
],
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
logger.error("Error running App Automate test", err);
|
|
109
|
+
throw err;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
case AppTestPlatform.XCUITEST: {
|
|
113
|
+
try {
|
|
114
|
+
const app_url = await uploadXcuiApp(args.appPath, config);
|
|
115
|
+
const test_suite_url = await uploadXcuiTestSuite(args.testSuitePath, config);
|
|
116
|
+
const build_id = await triggerXcuiBuild(app_url, test_suite_url, args.devices, args.project, config);
|
|
117
|
+
return {
|
|
118
|
+
content: [
|
|
119
|
+
{
|
|
120
|
+
type: "text",
|
|
121
|
+
text: `✅ XCUITest run started successfully!\n\n🔧 Build ID: ${build_id}\n🔗 View your build: https://app-automate.browserstack.com/builds/${build_id}`,
|
|
122
|
+
},
|
|
123
|
+
],
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
catch (err) {
|
|
127
|
+
logger.error("Error running XCUITest App Automate test", err);
|
|
128
|
+
throw err;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
default:
|
|
132
|
+
throw new Error(`Unsupported automation framework: ${args.detectedAutomationFramework}. If you need support for this framework, please open an issue at Github`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
export default function addAppAutomationTools(server, config) {
|
|
91
136
|
server.tool("takeAppScreenshot", "Use this tool to take a screenshot of an app running on a BrowserStack device. This is useful for visual testing and debugging.", {
|
|
92
137
|
desiredPhone: z
|
|
93
138
|
.string()
|
|
@@ -103,11 +148,11 @@ export default function addAppAutomationTools(server) {
|
|
|
103
148
|
.describe("The path to the .apk or .ipa file. Required for app installation."),
|
|
104
149
|
}, async (args) => {
|
|
105
150
|
try {
|
|
106
|
-
trackMCP("takeAppScreenshot", server.server.getClientVersion());
|
|
107
|
-
return await takeAppScreenshot(args);
|
|
151
|
+
trackMCP("takeAppScreenshot", server.server.getClientVersion(), undefined, config);
|
|
152
|
+
return await takeAppScreenshot({ ...args, config });
|
|
108
153
|
}
|
|
109
154
|
catch (error) {
|
|
110
|
-
trackMCP("takeAppScreenshot", server.server.getClientVersion(), error);
|
|
155
|
+
trackMCP("takeAppScreenshot", server.server.getClientVersion(), error, config);
|
|
111
156
|
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
112
157
|
return {
|
|
113
158
|
content: [
|
|
@@ -119,4 +164,55 @@ export default function addAppAutomationTools(server) {
|
|
|
119
164
|
};
|
|
120
165
|
}
|
|
121
166
|
});
|
|
167
|
+
server.tool("runAppTestsOnBrowserStack", "Run AppAutomate tests on BrowserStack by uploading app and test suite. If running from Android Studio or Xcode, the tool will help export app and test files automatically. For other environments, you'll need to provide the paths to your pre-built app and test files.", {
|
|
168
|
+
appPath: z
|
|
169
|
+
.string()
|
|
170
|
+
.describe("Path to your application file:\n" +
|
|
171
|
+
"If in development IDE directory:\n" +
|
|
172
|
+
"• For Android: 'gradle assembleDebug'\n" +
|
|
173
|
+
"• For iOS:\n" +
|
|
174
|
+
" xcodebuild clean -scheme YOUR_SCHEME && \\\n" +
|
|
175
|
+
" xcodebuild archive -scheme YOUR_SCHEME -configuration Release -archivePath build/app.xcarchive && \\\n" +
|
|
176
|
+
" xcodebuild -exportArchive -archivePath build/app.xcarchive -exportPath build/ipa -exportOptionsPlist exportOptions.plist\n\n" +
|
|
177
|
+
"If in other directory, provide existing app path"),
|
|
178
|
+
testSuitePath: z
|
|
179
|
+
.string()
|
|
180
|
+
.describe("Path to your test suite file:\n" +
|
|
181
|
+
"If in development IDE directory:\n" +
|
|
182
|
+
"• For Android: 'gradle assembleAndroidTest'\n" +
|
|
183
|
+
"• For iOS:\n" +
|
|
184
|
+
" xcodebuild test-without-building -scheme YOUR_SCHEME -destination 'generic/platform=iOS' && \\\n" +
|
|
185
|
+
" cd ~/Library/Developer/Xcode/DerivedData/*/Build/Products/Debug-iphonesimulator/ && \\\n" +
|
|
186
|
+
" zip -r Tests.zip *.xctestrun *-Runner.app\n\n" +
|
|
187
|
+
"If in other directory, provide existing test file path"),
|
|
188
|
+
devices: z
|
|
189
|
+
.array(z.string())
|
|
190
|
+
.describe("List of devices to run the test on, e.g., ['Samsung Galaxy S20-10.0', 'iPhone 12 Pro-16.0']."),
|
|
191
|
+
project: z
|
|
192
|
+
.string()
|
|
193
|
+
.optional()
|
|
194
|
+
.default("BStack-AppAutomate-Suite")
|
|
195
|
+
.describe("Project name for organizing test runs on BrowserStack."),
|
|
196
|
+
detectedAutomationFramework: z
|
|
197
|
+
.string()
|
|
198
|
+
.describe("The automation framework used in the project, such as 'espresso' (Android) or 'xcuitest' (iOS)."),
|
|
199
|
+
}, async (args) => {
|
|
200
|
+
try {
|
|
201
|
+
trackMCP("runAppTestsOnBrowserStack", server.server.getClientVersion(), undefined, config);
|
|
202
|
+
return await runAppTestsOnBrowserStack(args, config);
|
|
203
|
+
}
|
|
204
|
+
catch (error) {
|
|
205
|
+
trackMCP("runAppTestsOnBrowserStack", server.server.getClientVersion(), error, config);
|
|
206
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
207
|
+
return {
|
|
208
|
+
content: [
|
|
209
|
+
{
|
|
210
|
+
type: "text",
|
|
211
|
+
text: `Error running App Automate test: ${errorMessage}`,
|
|
212
|
+
},
|
|
213
|
+
],
|
|
214
|
+
isError: true,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
});
|
|
122
218
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { BrowserStackConfig } from "../../lib/types.js";
|
|
2
|
+
interface StartSessionArgs {
|
|
3
|
+
appPath: string;
|
|
4
|
+
desiredPlatform: "android" | "ios";
|
|
5
|
+
desiredPhone: string;
|
|
6
|
+
desiredPlatformVersion: string;
|
|
7
|
+
}
|
|
8
|
+
interface StartSessionOptions {
|
|
9
|
+
config: BrowserStackConfig;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Start an App Live session: filter, select, upload, and open.
|
|
13
|
+
*/
|
|
14
|
+
export declare function startSession(args: StartSessionArgs, options: StartSessionOptions): Promise<string>;
|
|
15
|
+
export {};
|
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
import logger from "../../logger.js";
|
|
2
|
-
import childProcess from "child_process";
|
|
3
2
|
import { getDevicesAndBrowsers, BrowserStackProducts, } from "../../lib/device-cache.js";
|
|
4
3
|
import { sanitizeUrlParam } from "../../lib/utils.js";
|
|
5
4
|
import { uploadApp } from "./upload-app.js";
|
|
5
|
+
import { getBrowserStackAuth } from "../../lib/get-auth.js";
|
|
6
6
|
import { findDeviceByName } from "./device-search.js";
|
|
7
7
|
import { pickVersion } from "./version-utils.js";
|
|
8
|
+
import childProcess from "child_process";
|
|
9
|
+
import envConfig from "../../config.js";
|
|
8
10
|
/**
|
|
9
11
|
* Start an App Live session: filter, select, upload, and open.
|
|
10
12
|
*/
|
|
11
|
-
export async function startSession(args) {
|
|
13
|
+
export async function startSession(args, options) {
|
|
12
14
|
const { appPath, desiredPlatform, desiredPhone, desiredPlatformVersion } = args;
|
|
15
|
+
const { config } = options;
|
|
13
16
|
// 1) Fetch devices for APP_LIVE
|
|
14
17
|
const data = await getDevicesAndBrowsers(BrowserStackProducts.APP_LIVE);
|
|
15
18
|
const all = data.mobile.flatMap((grp) => grp.devices.map((dev) => ({ ...dev, os: grp.os })));
|
|
@@ -36,7 +39,9 @@ export async function startSession(args) {
|
|
|
36
39
|
note = `\n Note: The requested version "${desiredPlatformVersion}" is not available. Using "${version}" instead.`;
|
|
37
40
|
}
|
|
38
41
|
// 6) Upload app
|
|
39
|
-
const
|
|
42
|
+
const authString = getBrowserStackAuth(config);
|
|
43
|
+
const [username, password] = authString.split(":");
|
|
44
|
+
const { app_url } = await uploadApp(appPath, username, password);
|
|
40
45
|
logger.info(`App uploaded: ${app_url}`);
|
|
41
46
|
if (!app_url) {
|
|
42
47
|
throw new Error("Failed to upload app. Please try again.");
|
|
@@ -52,7 +57,9 @@ export async function startSession(args) {
|
|
|
52
57
|
start: "true",
|
|
53
58
|
});
|
|
54
59
|
const launchUrl = `https://app-live.browserstack.com/dashboard#${params.toString()}&device=${deviceParam}`;
|
|
55
|
-
|
|
60
|
+
if (!envConfig.REMOTE_MCP) {
|
|
61
|
+
openBrowser(launchUrl);
|
|
62
|
+
}
|
|
56
63
|
return launchUrl + note;
|
|
57
64
|
}
|
|
58
65
|
/**
|
|
@@ -1,29 +1,25 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { apiClient } from "../../lib/apiClient.js";
|
|
2
2
|
import FormData from "form-data";
|
|
3
3
|
import fs from "fs";
|
|
4
|
-
|
|
5
|
-
export async function uploadApp(filePath) {
|
|
4
|
+
export async function uploadApp(filePath, username, password) {
|
|
6
5
|
if (!fs.existsSync(filePath)) {
|
|
7
6
|
throw new Error(`File not found at path: ${filePath}`);
|
|
8
7
|
}
|
|
9
8
|
const formData = new FormData();
|
|
10
9
|
formData.append("file", fs.createReadStream(filePath));
|
|
11
10
|
try {
|
|
12
|
-
const response = await
|
|
11
|
+
const response = await apiClient.post({
|
|
12
|
+
url: "https://api-cloud.browserstack.com/app-live/upload",
|
|
13
13
|
headers: {
|
|
14
14
|
...formData.getHeaders(),
|
|
15
|
+
Authorization: "Basic " + Buffer.from(`${username}:${password}`).toString("base64"),
|
|
15
16
|
},
|
|
16
|
-
|
|
17
|
-
username: config.browserstackUsername,
|
|
18
|
-
password: config.browserstackAccessKey,
|
|
19
|
-
},
|
|
17
|
+
body: formData,
|
|
20
18
|
});
|
|
21
19
|
return response.data;
|
|
22
20
|
}
|
|
23
21
|
catch (error) {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
27
|
-
throw error;
|
|
22
|
+
const msg = error?.response?.data?.message || error?.message || String(error);
|
|
23
|
+
throw new Error(`Failed to upload app: ${msg}`);
|
|
28
24
|
}
|
|
29
25
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
3
|
+
import { BrowserStackConfig } from "../lib/types.js";
|
|
4
|
+
/**
|
|
5
|
+
* Launches an App Live Session on BrowserStack.
|
|
6
|
+
*/
|
|
7
|
+
export declare function startAppLiveSession(args: {
|
|
8
|
+
desiredPlatform: string;
|
|
9
|
+
desiredPlatformVersion: string;
|
|
10
|
+
appPath: string;
|
|
11
|
+
desiredPhone: string;
|
|
12
|
+
}, config: BrowserStackConfig): Promise<CallToolResult>;
|
|
13
|
+
export default function addAppLiveTools(server: McpServer, config: BrowserStackConfig): void;
|
package/dist/tools/applive.js
CHANGED
|
@@ -6,7 +6,7 @@ import { trackMCP } from "../lib/instrumentation.js";
|
|
|
6
6
|
/**
|
|
7
7
|
* Launches an App Live Session on BrowserStack.
|
|
8
8
|
*/
|
|
9
|
-
export async function startAppLiveSession(args) {
|
|
9
|
+
export async function startAppLiveSession(args, config) {
|
|
10
10
|
if (!args.desiredPlatform) {
|
|
11
11
|
throw new Error("You must provide a desiredPlatform.");
|
|
12
12
|
}
|
|
@@ -38,7 +38,7 @@ export async function startAppLiveSession(args) {
|
|
|
38
38
|
desiredPlatform: args.desiredPlatform,
|
|
39
39
|
desiredPhone: args.desiredPhone,
|
|
40
40
|
desiredPlatformVersion: args.desiredPlatformVersion,
|
|
41
|
-
});
|
|
41
|
+
}, { config });
|
|
42
42
|
return {
|
|
43
43
|
content: [
|
|
44
44
|
{
|
|
@@ -48,7 +48,7 @@ export async function startAppLiveSession(args) {
|
|
|
48
48
|
],
|
|
49
49
|
};
|
|
50
50
|
}
|
|
51
|
-
export default function addAppLiveTools(server) {
|
|
51
|
+
export default function addAppLiveTools(server, config) {
|
|
52
52
|
server.tool("runAppLiveSession", "Use this tool when user wants to manually check their app on a particular mobile device using BrowserStack's cloud infrastructure. Can be used to debug crashes, slow performance, etc.", {
|
|
53
53
|
desiredPhone: z
|
|
54
54
|
.string()
|
|
@@ -64,12 +64,12 @@ export default function addAppLiveTools(server) {
|
|
|
64
64
|
.describe("The path to the .ipa or .apk file to install on the device. Always ask the user for the app path, do not assume it."),
|
|
65
65
|
}, async (args) => {
|
|
66
66
|
try {
|
|
67
|
-
trackMCP("runAppLiveSession", server.server.getClientVersion());
|
|
68
|
-
return await startAppLiveSession(args);
|
|
67
|
+
trackMCP("runAppLiveSession", server.server.getClientVersion(), undefined, config);
|
|
68
|
+
return await startAppLiveSession(args, config);
|
|
69
69
|
}
|
|
70
70
|
catch (error) {
|
|
71
71
|
logger.error("App live session failed: %s", error);
|
|
72
|
-
trackMCP("runAppLiveSession", server.server.getClientVersion(), error);
|
|
72
|
+
trackMCP("runAppLiveSession", server.server.getClientVersion(), error, config);
|
|
73
73
|
return {
|
|
74
74
|
content: [
|
|
75
75
|
{
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { SessionType } from "../../lib/constants.js";
|
|
2
|
+
import { BrowserStackConfig } from "../../lib/types.js";
|
|
3
|
+
export declare function fetchAutomationScreenshots(sessionId: string, sessionType: SessionType | undefined, config: BrowserStackConfig): Promise<{
|
|
4
|
+
url: string;
|
|
5
|
+
base64: string;
|
|
6
|
+
}[]>;
|
|
@@ -1,20 +1,24 @@
|
|
|
1
|
-
import config from "../../config.js";
|
|
2
1
|
import { assertOkResponse, maybeCompressBase64 } from "../../lib/utils.js";
|
|
3
2
|
import { SessionType } from "../../lib/constants.js";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const
|
|
3
|
+
import { getBrowserStackAuth } from "../../lib/get-auth.js";
|
|
4
|
+
import { apiClient } from "../../lib/apiClient.js";
|
|
5
|
+
async function extractScreenshotUrls(sessionId, sessionType, config) {
|
|
6
|
+
const authString = getBrowserStackAuth(config);
|
|
7
|
+
const auth = Buffer.from(authString).toString("base64");
|
|
8
8
|
const baseUrl = `https://api.browserstack.com/${sessionType === SessionType.Automate ? "automate" : "app-automate"}`;
|
|
9
9
|
const url = `${baseUrl}/sessions/${sessionId}/logs`;
|
|
10
|
-
const response = await
|
|
10
|
+
const response = await apiClient.get({
|
|
11
|
+
url,
|
|
11
12
|
headers: {
|
|
12
13
|
"Content-Type": "application/json",
|
|
13
14
|
Authorization: `Basic ${auth}`,
|
|
14
15
|
},
|
|
16
|
+
raise_error: false,
|
|
15
17
|
});
|
|
16
18
|
await assertOkResponse(response, "Session");
|
|
17
|
-
const text =
|
|
19
|
+
const text = typeof response.data === "string"
|
|
20
|
+
? response.data
|
|
21
|
+
: JSON.stringify(response.data);
|
|
18
22
|
const urls = [];
|
|
19
23
|
const SCREENSHOT_PATTERN = /REQUEST.*GET.*\/screenshot/;
|
|
20
24
|
const RESPONSE_VALUE_PATTERN = /"value"\s*:\s*"([^"]+)"/;
|
|
@@ -35,9 +39,9 @@ async function extractScreenshotUrls(sessionId, sessionType) {
|
|
|
35
39
|
//Converts screenshot URLs to base64 encoded images
|
|
36
40
|
async function convertUrlsToBase64(urls) {
|
|
37
41
|
const screenshots = await Promise.all(urls.map(async (url) => {
|
|
38
|
-
const response = await
|
|
39
|
-
|
|
40
|
-
const base64 = Buffer.from(
|
|
42
|
+
const response = await apiClient.get({ url });
|
|
43
|
+
// Axios returns response.data as a Buffer for binary data
|
|
44
|
+
const base64 = Buffer.from(response.data).toString("base64");
|
|
41
45
|
// Compress the base64 image if needed
|
|
42
46
|
const compressedBase64 = await maybeCompressBase64(base64);
|
|
43
47
|
return {
|
|
@@ -48,8 +52,8 @@ async function convertUrlsToBase64(urls) {
|
|
|
48
52
|
return screenshots;
|
|
49
53
|
}
|
|
50
54
|
//Fetches and converts screenshot URLs to base64 encoded images
|
|
51
|
-
export async function fetchAutomationScreenshots(sessionId, sessionType = SessionType.Automate) {
|
|
52
|
-
const urls = await extractScreenshotUrls(sessionId, sessionType);
|
|
55
|
+
export async function fetchAutomationScreenshots(sessionId, sessionType = SessionType.Automate, config) {
|
|
56
|
+
const urls = await extractScreenshotUrls(sessionId, sessionType, config);
|
|
53
57
|
if (urls.length === 0) {
|
|
54
58
|
return [];
|
|
55
59
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
3
|
+
import { SessionType } from "../lib/constants.js";
|
|
4
|
+
import { BrowserStackConfig } from "../lib/types.js";
|
|
5
|
+
export declare function fetchAutomationScreenshotsTool(args: {
|
|
6
|
+
sessionId: string;
|
|
7
|
+
sessionType: SessionType;
|
|
8
|
+
}, config: BrowserStackConfig): Promise<CallToolResult>;
|
|
9
|
+
export default function addAutomationTools(server: McpServer, config: BrowserStackConfig): void;
|
package/dist/tools/automate.js
CHANGED
|
@@ -4,9 +4,9 @@ import { SessionType } from "../lib/constants.js";
|
|
|
4
4
|
import { trackMCP } from "../lib/instrumentation.js";
|
|
5
5
|
import logger from "../logger.js";
|
|
6
6
|
// Tool function that fetches and processes screenshots from BrowserStack Automate session
|
|
7
|
-
export async function fetchAutomationScreenshotsTool(args) {
|
|
7
|
+
export async function fetchAutomationScreenshotsTool(args, config) {
|
|
8
8
|
try {
|
|
9
|
-
const screenshots = await fetchAutomationScreenshots(args.sessionId, args.sessionType);
|
|
9
|
+
const screenshots = await fetchAutomationScreenshots(args.sessionId, args.sessionType, config);
|
|
10
10
|
if (screenshots.length === 0) {
|
|
11
11
|
return {
|
|
12
12
|
content: [
|
|
@@ -41,7 +41,7 @@ export async function fetchAutomationScreenshotsTool(args) {
|
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
43
|
//Registers the fetchAutomationScreenshots tool with the MCP server
|
|
44
|
-
export default function addAutomationTools(server) {
|
|
44
|
+
export default function addAutomationTools(server, config) {
|
|
45
45
|
server.tool("fetchAutomationScreenshots", "Fetch and process screenshots from a BrowserStack Automate session", {
|
|
46
46
|
sessionId: z
|
|
47
47
|
.string()
|
|
@@ -51,11 +51,11 @@ export default function addAutomationTools(server) {
|
|
|
51
51
|
.describe("Type of BrowserStack session"),
|
|
52
52
|
}, async (args) => {
|
|
53
53
|
try {
|
|
54
|
-
trackMCP("fetchAutomationScreenshots", server.server.getClientVersion());
|
|
55
|
-
return await fetchAutomationScreenshotsTool(args);
|
|
54
|
+
trackMCP("fetchAutomationScreenshots", server.server.getClientVersion(), undefined, config);
|
|
55
|
+
return await fetchAutomationScreenshotsTool(args, config);
|
|
56
56
|
}
|
|
57
57
|
catch (error) {
|
|
58
|
-
trackMCP("fetchAutomationScreenshots", server.server.getClientVersion(), error);
|
|
58
|
+
trackMCP("fetchAutomationScreenshots", server.server.getClientVersion(), error, config);
|
|
59
59
|
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
60
60
|
return {
|
|
61
61
|
content: [
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
3
|
+
import { SDKSupportedBrowserAutomationFramework, SDKSupportedLanguage, SDKSupportedTestingFramework } from "./sdk-utils/types.js";
|
|
4
|
+
import { BrowserStackConfig } from "../lib/types.js";
|
|
5
|
+
/**
|
|
6
|
+
* BrowserStack SDK hooks into your test framework to seamlessly run tests on BrowserStack.
|
|
7
|
+
* This tool gives instructions to setup a browserstack.yml file in the project root and installs the necessary dependencies.
|
|
8
|
+
*/
|
|
9
|
+
export declare function bootstrapProjectWithSDK({ detectedBrowserAutomationFramework, detectedTestingFramework, detectedLanguage, desiredPlatforms, enablePercy, config, }: {
|
|
10
|
+
detectedBrowserAutomationFramework: SDKSupportedBrowserAutomationFramework;
|
|
11
|
+
detectedTestingFramework: SDKSupportedTestingFramework;
|
|
12
|
+
detectedLanguage: SDKSupportedLanguage;
|
|
13
|
+
desiredPlatforms: string[];
|
|
14
|
+
enablePercy: boolean;
|
|
15
|
+
config: BrowserStackConfig;
|
|
16
|
+
}): Promise<CallToolResult>;
|
|
17
|
+
export default function addSDKTools(server: McpServer, config: BrowserStackConfig): void;
|