@wdio/mcp 3.0.0 → 3.1.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 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: "2.5.3",
49
+ version: "3.1.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,46 +77,44 @@ 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: "npm run lint:src && npm run lint:tests",
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.16.2",
92
- "@xmldom/xmldom": "^0.8.11",
93
- "puppeteer-core": "^24.35.0",
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.24",
93
+ webdriverio: "^9.27.0",
96
94
  xpath: "^0.0.34",
97
- zod: "^4.3.5"
95
+ zod: "^4.3.6"
98
96
  },
99
97
  devDependencies: {
100
- "@release-it/conventional-changelog": "^10.0.4",
101
- "@types/node": "^20.11.0",
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.20.0",
104
- eslint: "^9.39.2",
105
- "happy-dom": "^20.7.0",
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.3",
108
- rimraf: "^6.1.2",
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.0.18"
110
+ typescript: "~5.9.3",
111
+ vitest: "^4.1.2"
114
112
  },
115
113
  packageManager: "pnpm@10.32.1"
116
114
  };
117
115
 
118
116
  // src/server.ts
119
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp";
117
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
120
118
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
121
119
 
122
120
  // src/tools/navigate.tool.ts
@@ -2073,8 +2071,88 @@ 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
- import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp";
2155
+ import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
2078
2156
 
2079
2157
  // src/recording/code-generator.ts
2080
2158
  function escapeStr(value) {
@@ -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();
@@ -2680,7 +2786,7 @@ var cookiesResource = {
2680
2786
 
2681
2787
  // src/resources/app-state.resource.ts
2682
2788
  init_state();
2683
- import { ResourceTemplate as ResourceTemplate2 } from "@modelcontextprotocol/sdk/server/mcp";
2789
+ import { ResourceTemplate as ResourceTemplate2 } from "@modelcontextprotocol/sdk/server/mcp.js";
2684
2790
  async function readAppState(bundleId) {
2685
2791
  try {
2686
2792
  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: options.appiumHost,
3052
- port: options.appiumPort,
3053
- path: options.appiumPath
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
- port: z14.number().optional().default(9222).describe("Chrome remote debugging port (for attach mode)"),
3142
- host: z14.string().optional().default("localhost").describe("Chrome host (for attach mode)"),
3143
- appiumHost: z14.string().optional().describe("Appium server hostname"),
3144
- appiumPort: z14.number().optional().describe("Appium server port"),
3145
- appiumPath: z14.string().optional().describe("Appium server path"),
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 browser = args.browser ?? "chrome";
3206
- const headless = args.headless ?? true;
3207
- const windowWidth = args.windowWidth ?? 1920;
3208
- const windowHeight = args.windowHeight ?? 1080;
3209
- const navigationUrl = args.navigationUrl;
3210
- const userCapabilities = args.capabilities ?? {};
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 mergedCapabilities = localBrowserProvider.buildCapabilities({
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 serverConfig = localAppiumProvider.getConnectionConfig(args);
3271
- const mergedCapabilities = localAppiumProvider.buildCapabilities(args);
3272
- const browser = await remote({
3273
- protocol: serverConfig.protocol,
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 = localAppiumProvider.shouldAutoDetach(args);
3281
- const sessionType = localAppiumProvider.getSessionType(args);
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.port ?? 9222;
3311
- const host = args.host ?? "localhost";
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);