@wdio/mcp 3.0.0 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +111 -0
- package/lib/server.js +345 -52
- package/lib/server.js.map +1 -1
- package/package.json +17 -19
package/README.md
CHANGED
|
@@ -216,6 +216,117 @@ appium
|
|
|
216
216
|
# Server runs at http://127.0.0.1:4723 by default
|
|
217
217
|
```
|
|
218
218
|
|
|
219
|
+
## BrowserStack
|
|
220
|
+
|
|
221
|
+
Run browser and mobile app tests on [BrowserStack](https://www.browserstack.com/) real devices and browsers without any local setup.
|
|
222
|
+
|
|
223
|
+
### Prerequisites
|
|
224
|
+
|
|
225
|
+
Set your credentials as environment variables:
|
|
226
|
+
|
|
227
|
+
```bash
|
|
228
|
+
export BROWSERSTACK_USERNAME=your_username
|
|
229
|
+
export BROWSERSTACK_ACCESS_KEY=your_access_key
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
Or add them to your MCP client config:
|
|
233
|
+
|
|
234
|
+
```json
|
|
235
|
+
{
|
|
236
|
+
"mcpServers": {
|
|
237
|
+
"wdio-mcp": {
|
|
238
|
+
"command": "npx",
|
|
239
|
+
"args": ["-y", "@wdio/mcp@latest"],
|
|
240
|
+
"env": {
|
|
241
|
+
"BROWSERSTACK_USERNAME": "your_username",
|
|
242
|
+
"BROWSERSTACK_ACCESS_KEY": "your_access_key"
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### Browser Sessions
|
|
250
|
+
|
|
251
|
+
Run a browser on a specific OS/version combination:
|
|
252
|
+
|
|
253
|
+
```javascript
|
|
254
|
+
start_session({
|
|
255
|
+
provider: 'browserstack',
|
|
256
|
+
platform: 'browser',
|
|
257
|
+
browser: 'chrome', // chrome | firefox | edge | safari
|
|
258
|
+
browserVersion: 'latest', // default: latest
|
|
259
|
+
os: 'Windows', // e.g. "Windows", "OS X"
|
|
260
|
+
osVersion: '11', // e.g. "11", "Sequoia"
|
|
261
|
+
reporting: {
|
|
262
|
+
project: 'My Project',
|
|
263
|
+
build: 'v1.2.0',
|
|
264
|
+
session: 'Login flow'
|
|
265
|
+
}
|
|
266
|
+
})
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
### Mobile App Sessions
|
|
270
|
+
|
|
271
|
+
Test on BrowserStack real devices. First upload your app (or use an existing `bs://` URL):
|
|
272
|
+
|
|
273
|
+
```javascript
|
|
274
|
+
// Upload a local .apk or .ipa (returns a bs:// URL)
|
|
275
|
+
upload_app({ path: '/path/to/app.apk' })
|
|
276
|
+
|
|
277
|
+
// Start a session with the returned URL
|
|
278
|
+
start_session({
|
|
279
|
+
provider: 'browserstack',
|
|
280
|
+
platform: 'android', // android | ios
|
|
281
|
+
app: 'bs://abc123...', // bs:// URL or custom_id from upload
|
|
282
|
+
deviceName: 'Samsung Galaxy S23',
|
|
283
|
+
platformVersion: '13.0',
|
|
284
|
+
reporting: {
|
|
285
|
+
project: 'My Project',
|
|
286
|
+
build: 'v1.2.0',
|
|
287
|
+
session: 'Checkout flow'
|
|
288
|
+
}
|
|
289
|
+
})
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
Use `list_apps` to see previously uploaded apps:
|
|
293
|
+
|
|
294
|
+
```javascript
|
|
295
|
+
list_apps() // own uploads, sorted by date
|
|
296
|
+
list_apps({ sortBy: 'app_name' })
|
|
297
|
+
list_apps({ organizationWide: true }) // all uploads in your org
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
### BrowserStack Local
|
|
301
|
+
|
|
302
|
+
To test against URLs that are only accessible on your local machine or internal network, enable the BrowserStack Local tunnel:
|
|
303
|
+
|
|
304
|
+
```javascript
|
|
305
|
+
start_session({
|
|
306
|
+
provider: 'browserstack',
|
|
307
|
+
platform: 'browser',
|
|
308
|
+
browser: 'chrome',
|
|
309
|
+
browserstackLocal: true // starts tunnel automatically
|
|
310
|
+
})
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
### Reporting Labels
|
|
314
|
+
|
|
315
|
+
All session types support `reporting` labels that appear in the BrowserStack Automate dashboard:
|
|
316
|
+
|
|
317
|
+
| Field | Description |
|
|
318
|
+
|---------------------------|--------------------------------------------|
|
|
319
|
+
| `reporting.project` | Group sessions under a project name |
|
|
320
|
+
| `reporting.build` | Tag sessions with a build/version label |
|
|
321
|
+
| `reporting.session` | Name for the individual test session |
|
|
322
|
+
|
|
323
|
+
### BrowserStack Tools
|
|
324
|
+
|
|
325
|
+
| Tool | Description |
|
|
326
|
+
|---------------|----------------------------------------------------------------------------------|
|
|
327
|
+
| `upload_app` | Upload a local `.apk` or `.ipa` to BrowserStack; returns a `bs://` URL |
|
|
328
|
+
| `list_apps` | List apps previously uploaded to your BrowserStack account |
|
|
329
|
+
|
|
219
330
|
## Features
|
|
220
331
|
|
|
221
332
|
### Browser Automation
|
package/lib/server.js
CHANGED
|
@@ -46,7 +46,7 @@ var package_default = {
|
|
|
46
46
|
type: "git",
|
|
47
47
|
url: "git://github.com/webdriverio/mcp.git"
|
|
48
48
|
},
|
|
49
|
-
version: "
|
|
49
|
+
version: "3.0.0",
|
|
50
50
|
description: "MCP server with WebdriverIO for browser and mobile app automation (iOS/Android via Appium)",
|
|
51
51
|
main: "./lib/server.js",
|
|
52
52
|
module: "./lib/server.js",
|
|
@@ -77,40 +77,38 @@ var package_default = {
|
|
|
77
77
|
prebundle: "rimraf lib --glob ./*.tgz",
|
|
78
78
|
bundle: "tsup && shx chmod +x lib/server.js",
|
|
79
79
|
postbundle: "npm pack",
|
|
80
|
-
lint: "
|
|
81
|
-
"lint:src": "eslint src/ --fix && tsc --noEmit",
|
|
82
|
-
"lint:tests": "eslint tests/ --fix && tsc -p tsconfig.test.json --noEmit",
|
|
80
|
+
lint: "eslint src/ tests/ --fix && tsc --noEmit",
|
|
83
81
|
start: "node lib/server.js",
|
|
84
82
|
dev: "tsx --watch src/server.ts",
|
|
85
83
|
prepare: "husky",
|
|
86
84
|
test: "vitest run"
|
|
87
85
|
},
|
|
88
86
|
dependencies: {
|
|
89
|
-
"@modelcontextprotocol/sdk": "1.27",
|
|
87
|
+
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
90
88
|
"@toon-format/toon": "^2.1.0",
|
|
91
|
-
"@wdio/protocols": "^9.
|
|
92
|
-
"@xmldom/xmldom": "^0.8.
|
|
93
|
-
"puppeteer-core": "^24.
|
|
89
|
+
"@wdio/protocols": "^9.27.0",
|
|
90
|
+
"@xmldom/xmldom": "^0.8.12",
|
|
91
|
+
"puppeteer-core": "^24.40.0",
|
|
94
92
|
sharp: "^0.34.5",
|
|
95
|
-
webdriverio: "9.
|
|
93
|
+
webdriverio: "^9.27.0",
|
|
96
94
|
xpath: "^0.0.34",
|
|
97
|
-
zod: "^4.3.
|
|
95
|
+
zod: "^4.3.6"
|
|
98
96
|
},
|
|
99
97
|
devDependencies: {
|
|
100
|
-
"@release-it/conventional-changelog": "^10.0.
|
|
101
|
-
"@types/node": "^20.
|
|
98
|
+
"@release-it/conventional-changelog": "^10.0.6",
|
|
99
|
+
"@types/node": "^20.19.37",
|
|
102
100
|
"@wdio/eslint": "^0.1.3",
|
|
103
|
-
"@wdio/types": "^9.
|
|
104
|
-
eslint: "^9.39.
|
|
105
|
-
"happy-dom": "^20.
|
|
101
|
+
"@wdio/types": "^9.27.0",
|
|
102
|
+
eslint: "^9.39.4",
|
|
103
|
+
"happy-dom": "^20.8.9",
|
|
106
104
|
husky: "^9.1.7",
|
|
107
|
-
"release-it": "^19.2.
|
|
108
|
-
rimraf: "^6.1.
|
|
105
|
+
"release-it": "^19.2.4",
|
|
106
|
+
rimraf: "^6.1.3",
|
|
109
107
|
shx: "^0.4.0",
|
|
110
108
|
tsup: "^8.5.1",
|
|
111
109
|
tsx: "^4.21.0",
|
|
112
|
-
typescript: "5.9",
|
|
113
|
-
vitest: "^4.
|
|
110
|
+
typescript: "~5.9.3",
|
|
111
|
+
vitest: "^4.1.2"
|
|
114
112
|
},
|
|
115
113
|
packageManager: "pnpm@10.32.1"
|
|
116
114
|
};
|
|
@@ -2073,6 +2071,86 @@ function withRecording(toolName, callback) {
|
|
|
2073
2071
|
};
|
|
2074
2072
|
}
|
|
2075
2073
|
|
|
2074
|
+
// src/resources/browserstack-local.resource.ts
|
|
2075
|
+
function getLocalBinaryInfo() {
|
|
2076
|
+
const platform2 = process.platform;
|
|
2077
|
+
const arch = process.arch;
|
|
2078
|
+
if (platform2 === "darwin") {
|
|
2079
|
+
return {
|
|
2080
|
+
downloadUrl: "https://local-downloads.browserstack.com/BrowserStackLocal-darwin-x64.zip",
|
|
2081
|
+
platform: "macOS",
|
|
2082
|
+
arch: arch === "arm64" ? "Apple Silicon (via Rosetta 2)" : "Intel x64",
|
|
2083
|
+
binaryName: "BrowserStackLocal",
|
|
2084
|
+
note: arch === "arm64" ? "macOS binary is Intel-only. Rosetta 2 must be installed (it is on most Apple Silicon Macs by default)." : void 0
|
|
2085
|
+
};
|
|
2086
|
+
}
|
|
2087
|
+
if (platform2 === "win32") {
|
|
2088
|
+
return {
|
|
2089
|
+
downloadUrl: "https://local-downloads.browserstack.com/BrowserStackLocal-win32.zip",
|
|
2090
|
+
platform: "Windows",
|
|
2091
|
+
arch: "x86/x64",
|
|
2092
|
+
binaryName: "BrowserStackLocal.exe"
|
|
2093
|
+
};
|
|
2094
|
+
}
|
|
2095
|
+
if (arch === "arm64") {
|
|
2096
|
+
return {
|
|
2097
|
+
downloadUrl: "https://local-downloads.browserstack.com/BrowserStackLocal-linux-arm64.zip",
|
|
2098
|
+
platform: "Linux",
|
|
2099
|
+
arch: "ARM64",
|
|
2100
|
+
binaryName: "BrowserStackLocal"
|
|
2101
|
+
};
|
|
2102
|
+
}
|
|
2103
|
+
if (arch === "ia32") {
|
|
2104
|
+
return {
|
|
2105
|
+
downloadUrl: "https://local-downloads.browserstack.com/BrowserStackLocal-linux-ia32.zip",
|
|
2106
|
+
platform: "Linux",
|
|
2107
|
+
arch: "x86 32-bit",
|
|
2108
|
+
binaryName: "BrowserStackLocal"
|
|
2109
|
+
};
|
|
2110
|
+
}
|
|
2111
|
+
return {
|
|
2112
|
+
downloadUrl: "https://local-downloads.browserstack.com/BrowserStackLocal-linux-x64.zip",
|
|
2113
|
+
platform: "Linux",
|
|
2114
|
+
arch: "x64",
|
|
2115
|
+
binaryName: "BrowserStackLocal"
|
|
2116
|
+
};
|
|
2117
|
+
}
|
|
2118
|
+
var browserstackLocalBinaryResource = {
|
|
2119
|
+
name: "browserstack-local-binary",
|
|
2120
|
+
uri: "wdio://browserstack/local-binary",
|
|
2121
|
+
description: "BrowserStack Local binary download URL and daemon setup instructions for the current platform. MUST be read and followed before using browserstackLocal: true in start_session.",
|
|
2122
|
+
handler: async () => {
|
|
2123
|
+
const info = getLocalBinaryInfo();
|
|
2124
|
+
const accessKey = process.env.BROWSERSTACK_ACCESS_KEY ?? "<BROWSERSTACK_ACCESS_KEY>";
|
|
2125
|
+
const content = {
|
|
2126
|
+
requirement: "MUST start the BrowserStack Local daemon BEFORE calling start_session with browserstackLocal: true. Without it, all navigation to local/internal URLs will fail with ERR_TUNNEL_CONNECTION_FAILED.",
|
|
2127
|
+
platform: info.platform,
|
|
2128
|
+
arch: info.arch,
|
|
2129
|
+
downloadUrl: info.downloadUrl,
|
|
2130
|
+
...info.note ? { note: info.note } : {},
|
|
2131
|
+
setup: [
|
|
2132
|
+
`1. Download: curl -O ${info.downloadUrl}`,
|
|
2133
|
+
`2. Unzip: unzip ${info.downloadUrl.split("/").pop()}`,
|
|
2134
|
+
`3. Make executable (macOS/Linux): chmod +x ${info.binaryName}`,
|
|
2135
|
+
`4. Start daemon: ./${info.binaryName} --key ${accessKey} --force-local --daemon start`
|
|
2136
|
+
],
|
|
2137
|
+
commands: {
|
|
2138
|
+
start: `./${info.binaryName} --key ${accessKey} --force-local --daemon start`,
|
|
2139
|
+
stop: `./${info.binaryName} --key ${accessKey} --daemon stop`,
|
|
2140
|
+
status: `./${info.binaryName} --daemon list`
|
|
2141
|
+
},
|
|
2142
|
+
afterDaemonIsRunning: "Call start_session with browserstackLocal: true to route BrowserStack traffic through the tunnel."
|
|
2143
|
+
};
|
|
2144
|
+
return {
|
|
2145
|
+
contents: [{
|
|
2146
|
+
uri: "wdio://browserstack/local-binary",
|
|
2147
|
+
mimeType: "application/json",
|
|
2148
|
+
text: JSON.stringify(content, null, 2)
|
|
2149
|
+
}]
|
|
2150
|
+
};
|
|
2151
|
+
}
|
|
2152
|
+
};
|
|
2153
|
+
|
|
2076
2154
|
// src/resources/sessions.resource.ts
|
|
2077
2155
|
import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp";
|
|
2078
2156
|
|
|
@@ -2266,13 +2344,41 @@ var sessionCodeResource = {
|
|
|
2266
2344
|
}
|
|
2267
2345
|
};
|
|
2268
2346
|
|
|
2347
|
+
// src/resources/capabilities.resource.ts
|
|
2348
|
+
init_state();
|
|
2349
|
+
var capabilitiesResource = {
|
|
2350
|
+
name: "session-current-capabilities",
|
|
2351
|
+
uri: "wdio://session/current/capabilities",
|
|
2352
|
+
description: "Raw capabilities returned by the WebDriver/Appium server for the current session. Use for debugging \u2014 shows the actual values the driver accepted, including defaults applied by BrowserStack or Appium.",
|
|
2353
|
+
handler: async () => {
|
|
2354
|
+
try {
|
|
2355
|
+
const browser = getBrowser();
|
|
2356
|
+
return {
|
|
2357
|
+
contents: [{
|
|
2358
|
+
uri: "wdio://session/current/capabilities",
|
|
2359
|
+
mimeType: "application/json",
|
|
2360
|
+
text: JSON.stringify(browser.capabilities, null, 2)
|
|
2361
|
+
}]
|
|
2362
|
+
};
|
|
2363
|
+
} catch (e) {
|
|
2364
|
+
return {
|
|
2365
|
+
contents: [{
|
|
2366
|
+
uri: "wdio://session/current/capabilities",
|
|
2367
|
+
mimeType: "application/json",
|
|
2368
|
+
text: JSON.stringify({ error: String(e) })
|
|
2369
|
+
}]
|
|
2370
|
+
};
|
|
2371
|
+
}
|
|
2372
|
+
}
|
|
2373
|
+
};
|
|
2374
|
+
|
|
2269
2375
|
// src/resources/elements.resource.ts
|
|
2270
2376
|
init_state();
|
|
2271
2377
|
import { encode as encode2 } from "@toon-format/toon";
|
|
2272
2378
|
var elementsResource = {
|
|
2273
2379
|
name: "session-current-elements",
|
|
2274
2380
|
uri: "wdio://session/current/elements",
|
|
2275
|
-
description: "Interactable elements on the current page",
|
|
2381
|
+
description: "Interactable elements on the current page. Prefer this over screenshot \u2014 returns ready-to-use selectors, faster, and far fewer tokens. Only use screenshot for visual verification or debugging.",
|
|
2276
2382
|
handler: async () => {
|
|
2277
2383
|
try {
|
|
2278
2384
|
const browser = getBrowser();
|
|
@@ -3047,10 +3153,11 @@ function buildAndroidCapabilities(appPath, options) {
|
|
|
3047
3153
|
var LocalAppiumProvider = class {
|
|
3048
3154
|
name = "local-appium";
|
|
3049
3155
|
getConnectionConfig(options) {
|
|
3156
|
+
const appiumConfig = options.appiumConfig;
|
|
3050
3157
|
const config = getAppiumServerConfig({
|
|
3051
|
-
hostname:
|
|
3052
|
-
port:
|
|
3053
|
-
path:
|
|
3158
|
+
hostname: appiumConfig?.host,
|
|
3159
|
+
port: appiumConfig?.port,
|
|
3160
|
+
path: appiumConfig?.path
|
|
3054
3161
|
});
|
|
3055
3162
|
return { protocol: "http", ...config };
|
|
3056
3163
|
}
|
|
@@ -3112,6 +3219,82 @@ var LocalAppiumProvider = class {
|
|
|
3112
3219
|
};
|
|
3113
3220
|
var localAppiumProvider = new LocalAppiumProvider();
|
|
3114
3221
|
|
|
3222
|
+
// src/providers/cloud/browserstack.provider.ts
|
|
3223
|
+
var BrowserStackProvider = class {
|
|
3224
|
+
name = "browserstack";
|
|
3225
|
+
getConnectionConfig(_options) {
|
|
3226
|
+
return {
|
|
3227
|
+
protocol: "https",
|
|
3228
|
+
hostname: "hub.browserstack.com",
|
|
3229
|
+
port: 443,
|
|
3230
|
+
path: "/wd/hub",
|
|
3231
|
+
user: process.env.BROWSERSTACK_USERNAME,
|
|
3232
|
+
key: process.env.BROWSERSTACK_ACCESS_KEY
|
|
3233
|
+
};
|
|
3234
|
+
}
|
|
3235
|
+
buildCapabilities(options) {
|
|
3236
|
+
const platform2 = options.platform;
|
|
3237
|
+
const userCapabilities = options.capabilities ?? {};
|
|
3238
|
+
const browserstackLocal = options.browserstackLocal;
|
|
3239
|
+
if (platform2 === "browser") {
|
|
3240
|
+
const bstackOptions2 = {
|
|
3241
|
+
browserVersion: options.browserVersion ?? "latest"
|
|
3242
|
+
};
|
|
3243
|
+
if (options.os) bstackOptions2.os = options.os;
|
|
3244
|
+
if (options.osVersion) bstackOptions2.osVersion = options.osVersion;
|
|
3245
|
+
if (browserstackLocal) bstackOptions2.local = true;
|
|
3246
|
+
const reporting2 = options.reporting;
|
|
3247
|
+
if (reporting2?.project) bstackOptions2.projectName = reporting2.project;
|
|
3248
|
+
if (reporting2?.build) bstackOptions2.buildName = reporting2.build;
|
|
3249
|
+
if (reporting2?.session) bstackOptions2.sessionName = reporting2.session;
|
|
3250
|
+
return {
|
|
3251
|
+
browserName: options.browser ?? "chrome",
|
|
3252
|
+
"bstack:options": bstackOptions2,
|
|
3253
|
+
...userCapabilities
|
|
3254
|
+
};
|
|
3255
|
+
}
|
|
3256
|
+
const bstackOptions = {
|
|
3257
|
+
platformName: platform2,
|
|
3258
|
+
deviceName: options.deviceName,
|
|
3259
|
+
platformVersion: options.platformVersion,
|
|
3260
|
+
deviceType: "phone",
|
|
3261
|
+
appiumVersion: "3.1.0"
|
|
3262
|
+
};
|
|
3263
|
+
if (browserstackLocal) bstackOptions.local = true;
|
|
3264
|
+
const reporting = options.reporting;
|
|
3265
|
+
if (reporting?.project) bstackOptions.projectName = reporting.project;
|
|
3266
|
+
if (reporting?.build) bstackOptions.buildName = reporting.build;
|
|
3267
|
+
if (reporting?.session) bstackOptions.sessionName = reporting.session;
|
|
3268
|
+
const autoAcceptAlerts = options.autoAcceptAlerts;
|
|
3269
|
+
const autoDismissAlerts = options.autoDismissAlerts;
|
|
3270
|
+
return {
|
|
3271
|
+
platformName: platform2,
|
|
3272
|
+
"appium:app": options.app,
|
|
3273
|
+
"appium:autoGrantPermissions": options.autoGrantPermissions ?? true,
|
|
3274
|
+
"appium:autoAcceptAlerts": autoDismissAlerts ? void 0 : autoAcceptAlerts ?? true,
|
|
3275
|
+
"appium:autoDismissAlerts": autoDismissAlerts,
|
|
3276
|
+
"appium:newCommandTimeout": options.newCommandTimeout ?? 300,
|
|
3277
|
+
"bstack:options": bstackOptions,
|
|
3278
|
+
...userCapabilities
|
|
3279
|
+
};
|
|
3280
|
+
}
|
|
3281
|
+
getSessionType(options) {
|
|
3282
|
+
const platform2 = options.platform;
|
|
3283
|
+
if (platform2 === "browser") return "browser";
|
|
3284
|
+
return platform2;
|
|
3285
|
+
}
|
|
3286
|
+
shouldAutoDetach(_options) {
|
|
3287
|
+
return false;
|
|
3288
|
+
}
|
|
3289
|
+
};
|
|
3290
|
+
var browserStackProvider = new BrowserStackProvider();
|
|
3291
|
+
|
|
3292
|
+
// src/providers/registry.ts
|
|
3293
|
+
function getProvider(providerName, platform2) {
|
|
3294
|
+
if (providerName === "browserstack") return browserStackProvider;
|
|
3295
|
+
return platform2 === "browser" ? localBrowserProvider : localAppiumProvider;
|
|
3296
|
+
}
|
|
3297
|
+
|
|
3115
3298
|
// src/tools/session.tool.ts
|
|
3116
3299
|
var platformEnum = z14.enum(["browser", "ios", "android"]);
|
|
3117
3300
|
var browserEnum = z14.enum(["chrome", "firefox", "edge", "safari"]);
|
|
@@ -3120,8 +3303,18 @@ var startSessionToolDefinition = {
|
|
|
3120
3303
|
name: "start_session",
|
|
3121
3304
|
description: "Starts a browser or mobile app session. For local browser, use browser platform. For mobile apps, use ios or android platform. Use attach mode to connect to an existing Chrome instance.",
|
|
3122
3305
|
inputSchema: {
|
|
3306
|
+
provider: z14.enum(["local", "browserstack"]).optional().default("local").describe("Session provider (default: local)"),
|
|
3123
3307
|
platform: platformEnum.describe("Session platform type"),
|
|
3124
3308
|
browser: browserEnum.optional().describe("Browser to launch (required for browser platform)"),
|
|
3309
|
+
browserVersion: z14.string().optional().describe("Browser version (BrowserStack only, default: latest)"),
|
|
3310
|
+
os: z14.string().optional().describe('Operating system (BrowserStack browser only, e.g. "Windows", "OS X")'),
|
|
3311
|
+
osVersion: z14.string().optional().describe('OS version (BrowserStack browser only, e.g. "11", "Sequoia")'),
|
|
3312
|
+
app: z14.string().optional().describe("BrowserStack app URL (bs://...) or custom_id for mobile sessions"),
|
|
3313
|
+
reporting: z14.object({
|
|
3314
|
+
project: z14.string().optional(),
|
|
3315
|
+
build: z14.string().optional(),
|
|
3316
|
+
session: z14.string().optional()
|
|
3317
|
+
}).optional().describe("BrowserStack reporting labels (project, build, session)"),
|
|
3125
3318
|
headless: coerceBoolean.optional().default(true).describe("Run browser in headless mode (default: true)"),
|
|
3126
3319
|
windowWidth: z14.number().min(400).max(3840).optional().default(1920).describe("Browser window width"),
|
|
3127
3320
|
windowHeight: z14.number().min(400).max(2160).optional().default(1080).describe("Browser window height"),
|
|
@@ -3138,11 +3331,16 @@ var startSessionToolDefinition = {
|
|
|
3138
3331
|
fullReset: coerceBoolean.optional().describe("Uninstall app before/after session"),
|
|
3139
3332
|
newCommandTimeout: z14.number().min(0).optional().default(300).describe("Appium command timeout in seconds"),
|
|
3140
3333
|
attach: coerceBoolean.optional().default(false).describe("Attach to existing Chrome instead of launching"),
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
3334
|
+
attachConfig: z14.object({
|
|
3335
|
+
port: z14.number().optional().default(9222),
|
|
3336
|
+
host: z14.string().optional().default("localhost")
|
|
3337
|
+
}).optional().describe("Chrome remote debugging connection (attach mode only, defaults: port 9222, host localhost)"),
|
|
3338
|
+
appiumConfig: z14.object({
|
|
3339
|
+
host: z14.string().optional(),
|
|
3340
|
+
port: z14.number().optional(),
|
|
3341
|
+
path: z14.string().optional()
|
|
3342
|
+
}).optional().describe("Appium server connection (local provider only)"),
|
|
3343
|
+
browserstackLocal: coerceBoolean.optional().default(false).describe("Enable BrowserStack Local tunnel for testing against local/internal URLs (BrowserStack only, default: false). IMPORTANT: The BrowserStack Local binary daemon MUST already be running before calling start_session, otherwise all navigation to local/internal URLs will fail with ERR_TUNNEL_CONNECTION_FAILED. Read the wdio://browserstack/local-binary resource for the platform-specific download URL and the exact daemon start command. Do not set this to true without first confirming the daemon is running."),
|
|
3146
3344
|
navigationUrl: z14.string().optional().describe("URL to navigate to after starting"),
|
|
3147
3345
|
capabilities: z14.record(z14.string(), z14.unknown()).optional().describe("Additional capabilities to merge")
|
|
3148
3346
|
}
|
|
@@ -3202,12 +3400,14 @@ async function waitForCDP2(host, port, timeoutMs = 1e4) {
|
|
|
3202
3400
|
throw new Error(`Chrome did not expose CDP on ${host}:${port} within ${timeoutMs}ms`);
|
|
3203
3401
|
}
|
|
3204
3402
|
async function startBrowserSession(args) {
|
|
3205
|
-
const
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3403
|
+
const {
|
|
3404
|
+
browser = "chrome",
|
|
3405
|
+
headless = true,
|
|
3406
|
+
windowWidth = 1920,
|
|
3407
|
+
windowHeight = 1080,
|
|
3408
|
+
navigationUrl,
|
|
3409
|
+
capabilities: userCapabilities = {}
|
|
3410
|
+
} = args;
|
|
3211
3411
|
const browserDisplayNames = {
|
|
3212
3412
|
chrome: "Chrome",
|
|
3213
3413
|
firefox: "Firefox",
|
|
@@ -3216,14 +3416,17 @@ async function startBrowserSession(args) {
|
|
|
3216
3416
|
};
|
|
3217
3417
|
const headlessSupported = browser !== "safari";
|
|
3218
3418
|
const effectiveHeadless = headless && headlessSupported;
|
|
3219
|
-
const
|
|
3419
|
+
const provider = getProvider(args.provider ?? "local", "browser");
|
|
3420
|
+
const connectionConfig = provider.getConnectionConfig(args);
|
|
3421
|
+
const mergedCapabilities = provider.buildCapabilities({
|
|
3422
|
+
...args,
|
|
3220
3423
|
browser,
|
|
3221
3424
|
headless,
|
|
3222
3425
|
windowWidth,
|
|
3223
3426
|
windowHeight,
|
|
3224
3427
|
capabilities: userCapabilities
|
|
3225
3428
|
});
|
|
3226
|
-
const wdioBrowser = await remote({ capabilities: mergedCapabilities });
|
|
3429
|
+
const wdioBrowser = await remote({ ...connectionConfig, capabilities: mergedCapabilities });
|
|
3227
3430
|
const { sessionId } = wdioBrowser;
|
|
3228
3431
|
const sessionMetadata = {
|
|
3229
3432
|
type: "browser",
|
|
@@ -3258,8 +3461,8 @@ Note: Unable to set window size (${windowWidth}x${windowHeight}). ${e}`;
|
|
|
3258
3461
|
};
|
|
3259
3462
|
}
|
|
3260
3463
|
async function startMobileSession(args) {
|
|
3261
|
-
const { platform: platform2, appPath, deviceName, noReset } = args;
|
|
3262
|
-
if (!appPath && noReset !== true) {
|
|
3464
|
+
const { platform: platform2, appPath, app, deviceName, noReset } = args;
|
|
3465
|
+
if (!appPath && !app && noReset !== true) {
|
|
3263
3466
|
return {
|
|
3264
3467
|
content: [{
|
|
3265
3468
|
type: "text",
|
|
@@ -3267,18 +3470,13 @@ async function startMobileSession(args) {
|
|
|
3267
3470
|
}]
|
|
3268
3471
|
};
|
|
3269
3472
|
}
|
|
3270
|
-
const
|
|
3271
|
-
const
|
|
3272
|
-
const
|
|
3273
|
-
|
|
3274
|
-
hostname: serverConfig.hostname,
|
|
3275
|
-
port: serverConfig.port,
|
|
3276
|
-
path: serverConfig.path,
|
|
3277
|
-
capabilities: mergedCapabilities
|
|
3278
|
-
});
|
|
3473
|
+
const provider = getProvider(args.provider ?? "local", args.platform);
|
|
3474
|
+
const serverConfig = provider.getConnectionConfig(args);
|
|
3475
|
+
const mergedCapabilities = provider.buildCapabilities(args);
|
|
3476
|
+
const browser = await remote({ ...serverConfig, capabilities: mergedCapabilities });
|
|
3279
3477
|
const { sessionId } = browser;
|
|
3280
|
-
const shouldAutoDetach =
|
|
3281
|
-
const sessionType =
|
|
3478
|
+
const shouldAutoDetach = provider.shouldAutoDetach(args);
|
|
3479
|
+
const sessionType = provider.getSessionType(args);
|
|
3282
3480
|
const metadata = {
|
|
3283
3481
|
type: sessionType,
|
|
3284
3482
|
capabilities: mergedCapabilities,
|
|
@@ -3307,9 +3505,8 @@ Appium Server: ${serverConfig.hostname}:${serverConfig.port}${serverConfig.path}
|
|
|
3307
3505
|
};
|
|
3308
3506
|
}
|
|
3309
3507
|
async function attachBrowserSession(args) {
|
|
3310
|
-
const port = args.
|
|
3311
|
-
const
|
|
3312
|
-
const navigationUrl = args.navigationUrl;
|
|
3508
|
+
const { port = 9222, host = "localhost" } = args.attachConfig ?? {};
|
|
3509
|
+
const { navigationUrl } = args;
|
|
3313
3510
|
await waitForCDP2(host, port);
|
|
3314
3511
|
const { activeTabUrl, allTabUrls } = await closeStaleMappers(host, port);
|
|
3315
3512
|
const capabilities = {
|
|
@@ -3419,6 +3616,98 @@ var switchTabTool = async ({ handle, index }) => {
|
|
|
3419
3616
|
}
|
|
3420
3617
|
};
|
|
3421
3618
|
|
|
3619
|
+
// src/tools/browserstack.tool.ts
|
|
3620
|
+
import { existsSync as existsSync2, createReadStream } from "fs";
|
|
3621
|
+
import { z as z16 } from "zod";
|
|
3622
|
+
var BS_API = "https://api-cloud.browserstack.com";
|
|
3623
|
+
function getAuth() {
|
|
3624
|
+
const user = process.env.BROWSERSTACK_USERNAME;
|
|
3625
|
+
const key = process.env.BROWSERSTACK_ACCESS_KEY;
|
|
3626
|
+
if (!user || !key) return null;
|
|
3627
|
+
return Buffer.from(`${user}:${key}`).toString("base64");
|
|
3628
|
+
}
|
|
3629
|
+
function formatAppList(apps) {
|
|
3630
|
+
if (apps.length === 0) return "No apps found.";
|
|
3631
|
+
return apps.map((a) => {
|
|
3632
|
+
const id = a.custom_id ? ` [${a.custom_id}]` : "";
|
|
3633
|
+
return `${a.app_name} v${a.app_version}${id} \u2014 ${a.app_url} (${a.uploaded_at})`;
|
|
3634
|
+
}).join("\n");
|
|
3635
|
+
}
|
|
3636
|
+
var listAppsToolDefinition = {
|
|
3637
|
+
name: "list_apps",
|
|
3638
|
+
description: "List apps uploaded to BrowserStack App Automate. Reads BROWSERSTACK_USERNAME and BROWSERSTACK_ACCESS_KEY from environment.",
|
|
3639
|
+
inputSchema: {
|
|
3640
|
+
sortBy: z16.enum(["app_name", "uploaded_at"]).optional().default("uploaded_at").describe("Sort order for results"),
|
|
3641
|
+
organizationWide: coerceBoolean.optional().default(false).describe("List apps uploaded by all users in the organization (uses recent_group_apps endpoint). Defaults to false (own uploads only)."),
|
|
3642
|
+
limit: z16.number().int().min(1).optional().default(20).describe("Maximum number of apps to return (only applies when organizationWide is true, default 20)")
|
|
3643
|
+
}
|
|
3644
|
+
};
|
|
3645
|
+
var listAppsTool = async ({ sortBy = "uploaded_at", organizationWide = false, limit = 20 }) => {
|
|
3646
|
+
const auth = getAuth();
|
|
3647
|
+
if (!auth) {
|
|
3648
|
+
return { isError: true, content: [{ type: "text", text: "Missing credentials: set BROWSERSTACK_USERNAME and BROWSERSTACK_ACCESS_KEY environment variables." }] };
|
|
3649
|
+
}
|
|
3650
|
+
try {
|
|
3651
|
+
let url = `${BS_API}/app-automate/${organizationWide ? "recent_group_apps" : "recent_apps"}`;
|
|
3652
|
+
if (organizationWide && limit) url += `?limit=${limit}`;
|
|
3653
|
+
const res = await fetch(url, {
|
|
3654
|
+
headers: { Authorization: `Basic ${auth}` }
|
|
3655
|
+
});
|
|
3656
|
+
if (!res.ok) {
|
|
3657
|
+
const body = await res.text();
|
|
3658
|
+
return { isError: true, content: [{ type: "text", text: `BrowserStack API error ${res.status}: ${body}` }] };
|
|
3659
|
+
}
|
|
3660
|
+
const raw = await res.json();
|
|
3661
|
+
let apps = Array.isArray(raw) ? raw : [];
|
|
3662
|
+
apps = sortBy === "app_name" ? apps.sort((a, b) => a.app_name.localeCompare(b.app_name)) : apps.sort((a, b) => new Date(b.uploaded_at).getTime() - new Date(a.uploaded_at).getTime());
|
|
3663
|
+
return { content: [{ type: "text", text: formatAppList(apps) }] };
|
|
3664
|
+
} catch (e) {
|
|
3665
|
+
return { isError: true, content: [{ type: "text", text: `Error listing apps: ${e}` }] };
|
|
3666
|
+
}
|
|
3667
|
+
};
|
|
3668
|
+
var uploadAppToolDefinition = {
|
|
3669
|
+
name: "upload_app",
|
|
3670
|
+
description: "Upload a local .apk or .ipa to BrowserStack App Automate. Returns a bs:// URL for use in start_session.",
|
|
3671
|
+
inputSchema: {
|
|
3672
|
+
path: z16.string().describe("Absolute path to the .apk or .ipa file"),
|
|
3673
|
+
customId: z16.string().optional().describe("Optional custom ID for the app (used to reference it later)")
|
|
3674
|
+
}
|
|
3675
|
+
};
|
|
3676
|
+
var uploadAppTool = async ({ path, customId }) => {
|
|
3677
|
+
const auth = getAuth();
|
|
3678
|
+
if (!auth) {
|
|
3679
|
+
return { isError: true, content: [{ type: "text", text: "Missing credentials: set BROWSERSTACK_USERNAME and BROWSERSTACK_ACCESS_KEY environment variables." }] };
|
|
3680
|
+
}
|
|
3681
|
+
if (!existsSync2(path)) {
|
|
3682
|
+
return { isError: true, content: [{ type: "text", text: `File not found: ${path}` }] };
|
|
3683
|
+
}
|
|
3684
|
+
try {
|
|
3685
|
+
const form = new FormData();
|
|
3686
|
+
const stream = createReadStream(path);
|
|
3687
|
+
const fileName = path.split("/").pop() ?? "app";
|
|
3688
|
+
form.append("file", new Blob([stream]), fileName);
|
|
3689
|
+
if (customId) form.append("custom_id", customId);
|
|
3690
|
+
const res = await fetch(`${BS_API}/app-automate/upload`, {
|
|
3691
|
+
method: "POST",
|
|
3692
|
+
headers: { Authorization: `Basic ${auth}` },
|
|
3693
|
+
body: form
|
|
3694
|
+
});
|
|
3695
|
+
if (!res.ok) {
|
|
3696
|
+
const body = await res.text();
|
|
3697
|
+
return { isError: true, content: [{ type: "text", text: `Upload failed ${res.status}: ${body}` }] };
|
|
3698
|
+
}
|
|
3699
|
+
const data = await res.json();
|
|
3700
|
+
const customIdNote = data.custom_id ? `
|
|
3701
|
+
Custom ID: ${data.custom_id}` : "";
|
|
3702
|
+
return { content: [{ type: "text", text: `Upload successful.
|
|
3703
|
+
App URL: ${data.app_url}${customIdNote}
|
|
3704
|
+
|
|
3705
|
+
Use this URL as the "app" parameter in start_session with provider: "browserstack".` }] };
|
|
3706
|
+
} catch (e) {
|
|
3707
|
+
return { isError: true, content: [{ type: "text", text: `Error uploading app: ${e}` }] };
|
|
3708
|
+
}
|
|
3709
|
+
};
|
|
3710
|
+
|
|
3422
3711
|
// src/server.ts
|
|
3423
3712
|
console.log = (...args) => console.error("[LOG]", ...args);
|
|
3424
3713
|
console.info = (...args) => console.error("[INFO]", ...args);
|
|
@@ -3478,11 +3767,15 @@ registerTool(hideKeyboardToolDefinition, hideKeyboardTool);
|
|
|
3478
3767
|
registerTool(setGeolocationToolDefinition, setGeolocationTool);
|
|
3479
3768
|
registerTool(executeScriptToolDefinition, withRecording("execute_script", executeScriptTool));
|
|
3480
3769
|
registerTool(getElementsToolDefinition, getElementsTool);
|
|
3770
|
+
registerTool(listAppsToolDefinition, listAppsTool);
|
|
3771
|
+
registerTool(uploadAppToolDefinition, uploadAppTool);
|
|
3481
3772
|
registerResource(sessionsIndexResource);
|
|
3482
3773
|
registerResource(sessionCurrentStepsResource);
|
|
3483
3774
|
registerResource(sessionCurrentCodeResource);
|
|
3484
3775
|
registerResource(sessionStepsResource);
|
|
3485
3776
|
registerResource(sessionCodeResource);
|
|
3777
|
+
registerResource(browserstackLocalBinaryResource);
|
|
3778
|
+
registerResource(capabilitiesResource);
|
|
3486
3779
|
registerResource(elementsResource);
|
|
3487
3780
|
registerResource(accessibilityResource);
|
|
3488
3781
|
registerResource(screenshotResource);
|