@wdio/mcp 3.1.0 → 3.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 +60 -49
- package/lib/server.js +206 -62
- package/lib/server.js.map +1 -1
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -353,23 +353,22 @@ All session types support `reporting` labels that appear in the BrowserStack Aut
|
|
|
353
353
|
|
|
354
354
|
### Session Management
|
|
355
355
|
|
|
356
|
-
| Tool
|
|
357
|
-
|
|
358
|
-
| `
|
|
359
|
-
| `
|
|
360
|
-
| `close_session`
|
|
361
|
-
| `
|
|
362
|
-
| `emulate_device` | Emulate a mobile/tablet device preset (viewport, DPR, UA, touch); requires BiDi session |
|
|
356
|
+
| Tool | Description |
|
|
357
|
+
|------------------|------------------------------------------------------------------------------------------|
|
|
358
|
+
| `start_session` | Start a browser or app session. Use `platform: 'browser'` for web, `platform: 'ios'`/`'android'` for mobile, or `attach: true` to connect to a running Chrome instance |
|
|
359
|
+
| `launch_chrome` | Launch a new Chrome instance with remote debugging enabled (for use with `start_session({ attach: true })`) |
|
|
360
|
+
| `close_session` | Close or detach from the current session (supports `detach: true` to disconnect without terminating) |
|
|
361
|
+
| `emulate_device` | Emulate a mobile/tablet device preset (viewport, DPR, UA, touch); requires BiDi session |
|
|
363
362
|
|
|
364
363
|
### Navigation & Page Interaction (Web & Mobile)
|
|
365
364
|
|
|
366
|
-
| Tool
|
|
367
|
-
|
|
368
|
-
| `navigate`
|
|
369
|
-
| `
|
|
370
|
-
| `
|
|
371
|
-
| `
|
|
372
|
-
| `
|
|
365
|
+
| Tool | Description |
|
|
366
|
+
|-----------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|
367
|
+
| `navigate` | Navigate to a URL |
|
|
368
|
+
| `get_elements` | Get visible, interactable elements on the page. Supports `inViewportOnly` (default: true) to filter viewport elements, and `includeContainers` (default: false) to include layout containers on mobile |
|
|
369
|
+
| `scroll` | Scroll in a direction (up/down) by specified pixels |
|
|
370
|
+
| `execute_script` | Execute arbitrary JavaScript in the browser context |
|
|
371
|
+
| `switch_tab` | Switch to a different browser tab by index or URL |
|
|
373
372
|
|
|
374
373
|
### Element Interaction (Web & Mobile)
|
|
375
374
|
|
|
@@ -382,7 +381,6 @@ All session types support `reporting` labels that appear in the BrowserStack Aut
|
|
|
382
381
|
|
|
383
382
|
| Tool | Description |
|
|
384
383
|
|------------------|--------------------------------------------------------|
|
|
385
|
-
| `get_cookies` | Get all cookies or a specific cookie by name |
|
|
386
384
|
| `set_cookie` | Set a cookie with name, value, and optional attributes |
|
|
387
385
|
| `delete_cookies` | Delete all cookies or a specific cookie |
|
|
388
386
|
|
|
@@ -394,27 +392,40 @@ All session types support `reporting` labels that appear in the BrowserStack Aut
|
|
|
394
392
|
| `swipe` | Swipe in a direction (up/down/left/right) |
|
|
395
393
|
| `drag_and_drop` | Drag from one location to another |
|
|
396
394
|
|
|
397
|
-
### App Lifecycle (iOS/Android)
|
|
398
|
-
|
|
399
|
-
| Tool | Description |
|
|
400
|
-
|-----------------|--------------------------------------------------------------|
|
|
401
|
-
| `get_app_state` | Check app state (installed, running, background, foreground) |
|
|
402
|
-
|
|
403
395
|
### Context Switching (Hybrid Apps)
|
|
404
396
|
|
|
405
|
-
| Tool
|
|
406
|
-
|
|
407
|
-
| `
|
|
408
|
-
| `get_current_context` | Show the currently active context |
|
|
409
|
-
| `switch_context` | Switch between native and webview contexts |
|
|
397
|
+
| Tool | Description |
|
|
398
|
+
|------------------|-------------------------------------------------|
|
|
399
|
+
| `switch_context` | Switch between native and webview contexts |
|
|
410
400
|
|
|
411
401
|
### Device Control (iOS/Android)
|
|
412
402
|
|
|
413
|
-
| Tool
|
|
414
|
-
|
|
415
|
-
| `rotate_device`
|
|
416
|
-
| `hide_keyboard`
|
|
417
|
-
| `
|
|
403
|
+
| Tool | Description |
|
|
404
|
+
|-------------------|---------------------------------|
|
|
405
|
+
| `rotate_device` | Rotate to portrait or landscape |
|
|
406
|
+
| `hide_keyboard` | Hide on-screen keyboard |
|
|
407
|
+
| `set_geolocation` | Set device GPS location |
|
|
408
|
+
|
|
409
|
+
### MCP Resources (read-only, no tool call needed)
|
|
410
|
+
|
|
411
|
+
| Resource | Description |
|
|
412
|
+
|-------------------------------------------|--------------------------------------------------------|
|
|
413
|
+
| `wdio://sessions` | Index of all recorded sessions |
|
|
414
|
+
| `wdio://session/current/steps` | Step log for the active session |
|
|
415
|
+
| `wdio://session/current/code` | Generated runnable WebdriverIO JS for the active session |
|
|
416
|
+
| `wdio://session/{id}/steps` | Step log for any past session by ID |
|
|
417
|
+
| `wdio://session/{id}/code` | Generated JS for any past session by ID |
|
|
418
|
+
| `wdio://session/current/elements` | Interactable elements (viewport-only by default) |
|
|
419
|
+
| `wdio://session/current/accessibility` | Accessibility tree |
|
|
420
|
+
| `wdio://session/current/screenshot` | Screenshot (base64) |
|
|
421
|
+
| `wdio://session/current/cookies` | Browser cookies |
|
|
422
|
+
| `wdio://session/current/tabs` | Open browser tabs |
|
|
423
|
+
| `wdio://session/current/contexts` | Native/webview contexts (mobile) |
|
|
424
|
+
| `wdio://session/current/context` | Currently active context (mobile) |
|
|
425
|
+
| `wdio://session/current/app-state` | Mobile app state |
|
|
426
|
+
| `wdio://session/current/geolocation` | Device geolocation |
|
|
427
|
+
| `wdio://session/current/capabilities` | Resolved WebDriver capabilities for the active session |
|
|
428
|
+
| `wdio://browserstack/local-binary` | BrowserStack Local binary download URL and start command |
|
|
418
429
|
|
|
419
430
|
## Usage Examples
|
|
420
431
|
|
|
@@ -458,28 +469,26 @@ You are a Testing expert, and want to assess the basic workflows of a web applic
|
|
|
458
469
|
|
|
459
470
|
```javascript
|
|
460
471
|
// Default settings (headed mode, 1280x1080)
|
|
461
|
-
|
|
472
|
+
start_session({platform: 'browser'})
|
|
462
473
|
|
|
463
474
|
// Firefox
|
|
464
|
-
|
|
475
|
+
start_session({platform: 'browser', browser: 'firefox'})
|
|
465
476
|
|
|
466
477
|
// Edge
|
|
467
|
-
|
|
478
|
+
start_session({platform: 'browser', browser: 'edge'})
|
|
468
479
|
|
|
469
480
|
// Safari (headed only; requires macOS)
|
|
470
|
-
|
|
481
|
+
start_session({platform: 'browser', browser: 'safari'})
|
|
471
482
|
|
|
472
483
|
// Headless mode
|
|
473
|
-
|
|
484
|
+
start_session({platform: 'browser', headless: true})
|
|
474
485
|
|
|
475
486
|
// Custom dimensions
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
// Headless with custom dimensions
|
|
479
|
-
start_browser({headless: true, windowWidth: 1920, windowHeight: 1080})
|
|
487
|
+
start_session({platform: 'browser', windowWidth: 1920, windowHeight: 1080})
|
|
480
488
|
|
|
481
489
|
// Pass custom capabilities (e.g. Chrome extensions, profile, prefs)
|
|
482
|
-
|
|
490
|
+
start_session({
|
|
491
|
+
platform: 'browser',
|
|
483
492
|
headless: false,
|
|
484
493
|
capabilities: {
|
|
485
494
|
'goog:chromeOptions': {
|
|
@@ -504,9 +513,9 @@ start_browser({
|
|
|
504
513
|
// google-chrome --remote-debugging-port=9222 --user-data-dir=/tmp/chrome-debug &
|
|
505
514
|
//
|
|
506
515
|
// Verify it's ready: curl http://localhost:9222/json/version
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
516
|
+
start_session({attach: true})
|
|
517
|
+
start_session({attach: true, port: 9333})
|
|
518
|
+
start_session({attach: true, port: 9222, navigationUrl: 'https://app.example.com'})
|
|
510
519
|
```
|
|
511
520
|
|
|
512
521
|
**Device emulation (requires BiDi session):**
|
|
@@ -647,8 +656,8 @@ Control app state when creating new sessions using the `noReset` and `fullReset`
|
|
|
647
656
|
|
|
648
657
|
```javascript
|
|
649
658
|
// Preserve login state between test runs
|
|
650
|
-
|
|
651
|
-
platform: '
|
|
659
|
+
start_session({
|
|
660
|
+
platform: 'android',
|
|
652
661
|
appPath: '/path/to/app.apk',
|
|
653
662
|
deviceName: 'emulator-5554',
|
|
654
663
|
noReset: true, // Don't reset app state
|
|
@@ -717,10 +726,12 @@ This eliminates the need to manually handle permission popups during automated t
|
|
|
717
726
|
Every tool call is automatically recorded to a session history. You can inspect sessions and export runnable code via MCP resources — no extra tool calls needed:
|
|
718
727
|
|
|
719
728
|
- `wdio://sessions` — lists all recorded sessions with type, timestamps, and step count
|
|
720
|
-
- `wdio://session/current/steps` — step log for the active session
|
|
721
|
-
- `wdio://session/
|
|
729
|
+
- `wdio://session/current/steps` — step log for the active session
|
|
730
|
+
- `wdio://session/current/code` — generated runnable WebdriverIO JS for the active session
|
|
731
|
+
- `wdio://session/{sessionId}/steps` — step log for any past session by ID
|
|
732
|
+
- `wdio://session/{sessionId}/code` — generated JS for any past session by ID
|
|
722
733
|
|
|
723
|
-
The generated script reconstructs the full session — including capabilities, navigation, clicks, and inputs — as a standalone `import { remote } from 'webdriverio'` file.
|
|
734
|
+
The generated script reconstructs the full session — including capabilities, navigation, clicks, and inputs — as a standalone `import { remote } from 'webdriverio'` file. For BrowserStack sessions it includes the full try/catch/finally with automatic session result marking.
|
|
724
735
|
|
|
725
736
|
## Troubleshooting
|
|
726
737
|
|
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: "3.
|
|
49
|
+
version: "3.1.1",
|
|
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",
|
|
@@ -88,6 +88,7 @@ var package_default = {
|
|
|
88
88
|
"@toon-format/toon": "^2.1.0",
|
|
89
89
|
"@wdio/protocols": "^9.27.0",
|
|
90
90
|
"@xmldom/xmldom": "^0.8.12",
|
|
91
|
+
"browserstack-local": "^1.5.12",
|
|
91
92
|
"puppeteer-core": "^24.40.0",
|
|
92
93
|
sharp: "^0.34.5",
|
|
93
94
|
webdriverio: "^9.27.0",
|
|
@@ -114,7 +115,7 @@ var package_default = {
|
|
|
114
115
|
};
|
|
115
116
|
|
|
116
117
|
// src/server.ts
|
|
117
|
-
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp";
|
|
118
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
118
119
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
119
120
|
|
|
120
121
|
// src/tools/navigate.tool.ts
|
|
@@ -2152,7 +2153,7 @@ var browserstackLocalBinaryResource = {
|
|
|
2152
2153
|
};
|
|
2153
2154
|
|
|
2154
2155
|
// src/resources/sessions.resource.ts
|
|
2155
|
-
import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp";
|
|
2156
|
+
import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2156
2157
|
|
|
2157
2158
|
// src/recording/code-generator.ts
|
|
2158
2159
|
function escapeStr(value) {
|
|
@@ -2176,6 +2177,23 @@ function generateStep(step, history) {
|
|
|
2176
2177
|
switch (step.tool) {
|
|
2177
2178
|
case "start_session": {
|
|
2178
2179
|
const platform2 = p.platform;
|
|
2180
|
+
const isBrowserStack = "bstack:options" in history.capabilities;
|
|
2181
|
+
const capJson = indentJson(history.capabilities).split("\n").map((line, i) => i > 0 ? ` ${line}` : line).join("\n");
|
|
2182
|
+
if (isBrowserStack) {
|
|
2183
|
+
const nav = platform2 === "browser" && p.navigationUrl ? `
|
|
2184
|
+
await browser.url('${escapeStr(p.navigationUrl)}');` : "";
|
|
2185
|
+
return [
|
|
2186
|
+
"const browser = await remote({",
|
|
2187
|
+
" protocol: 'https',",
|
|
2188
|
+
" hostname: 'hub.browserstack.com',",
|
|
2189
|
+
" port: 443,",
|
|
2190
|
+
" path: '/wd/hub',",
|
|
2191
|
+
" user: process.env.BS_USER,",
|
|
2192
|
+
" key: process.env.BS_KEY,",
|
|
2193
|
+
` capabilities: ${capJson}`,
|
|
2194
|
+
`});${nav}`
|
|
2195
|
+
].join("\n");
|
|
2196
|
+
}
|
|
2179
2197
|
if (platform2 === "browser") {
|
|
2180
2198
|
const nav = p.navigationUrl ? `
|
|
2181
2199
|
await browser.url('${escapeStr(p.navigationUrl)}');` : "";
|
|
@@ -2225,8 +2243,67 @@ await browser.url('${escapeStr(p.navigationUrl)}');` : "";
|
|
|
2225
2243
|
return `// [unknown tool] ${step.tool}`;
|
|
2226
2244
|
}
|
|
2227
2245
|
}
|
|
2246
|
+
function bsStatusUpdateLines(sessionType) {
|
|
2247
|
+
const apiUrl = sessionType === "browser" ? "https://api.browserstack.com/automate/sessions/" : "https://api-cloud.browserstack.com/app-automate/sessions/";
|
|
2248
|
+
return [
|
|
2249
|
+
" const bsAuth = Buffer.from(`${process.env.BS_USER}:${process.env.BS_KEY}`).toString('base64');",
|
|
2250
|
+
` await fetch('${apiUrl}' + browser.sessionId + '.json', {`,
|
|
2251
|
+
" method: 'PUT',",
|
|
2252
|
+
" headers: { Authorization: 'Basic ' + bsAuth, 'Content-Type': 'application/json' },",
|
|
2253
|
+
" body: JSON.stringify({ status: bsStatus, ...(bsReason ? { reason: bsReason } : {}) })",
|
|
2254
|
+
" });"
|
|
2255
|
+
];
|
|
2256
|
+
}
|
|
2228
2257
|
function generateCode(history) {
|
|
2258
|
+
const bstackOptions = history.capabilities["bstack:options"];
|
|
2259
|
+
const isBrowserStack = bstackOptions !== void 0;
|
|
2260
|
+
const usesBrowserstackLocal = bstackOptions?.local === true;
|
|
2229
2261
|
const steps = history.steps.map((step) => generateStep(step, history)).join("\n").split("\n").map((line) => ` ${line}`).join("\n");
|
|
2262
|
+
if (isBrowserStack) {
|
|
2263
|
+
const bsSteps = steps.replace(/const browser = await remote\(/g, "browser = await remote(");
|
|
2264
|
+
const statusUpdate = bsStatusUpdateLines(history.type).join("\n");
|
|
2265
|
+
const preamble = "let browser;\nlet bsStatus = 'passed';\nlet bsReason;";
|
|
2266
|
+
const catchBlock = "} catch (e) {\n bsStatus = 'failed';\n bsReason = String(e);\n throw e;";
|
|
2267
|
+
const finallyBody = ` if (browser) {
|
|
2268
|
+
${statusUpdate}
|
|
2269
|
+
await browser.deleteSession();
|
|
2270
|
+
}`;
|
|
2271
|
+
if (usesBrowserstackLocal) {
|
|
2272
|
+
const tunnelSetup = [
|
|
2273
|
+
"",
|
|
2274
|
+
"const tunnel = new BrowserstackTunnel();",
|
|
2275
|
+
"const startTunnel = promisify(tunnel.start.bind(tunnel));",
|
|
2276
|
+
"const stopTunnel = promisify(tunnel.stop.bind(tunnel));",
|
|
2277
|
+
"await startTunnel({ key: process.env.BROWSERSTACK_ACCESS_KEY });",
|
|
2278
|
+
""
|
|
2279
|
+
].join("\n");
|
|
2280
|
+
return [
|
|
2281
|
+
"import { remote } from 'webdriverio';",
|
|
2282
|
+
"import { Local as BrowserstackTunnel } from 'browserstack-local';",
|
|
2283
|
+
"import { promisify } from 'node:util';",
|
|
2284
|
+
tunnelSetup,
|
|
2285
|
+
preamble,
|
|
2286
|
+
"try {",
|
|
2287
|
+
bsSteps,
|
|
2288
|
+
catchBlock,
|
|
2289
|
+
"} finally {",
|
|
2290
|
+
finallyBody,
|
|
2291
|
+
" await stopTunnel();",
|
|
2292
|
+
"}"
|
|
2293
|
+
].join("\n");
|
|
2294
|
+
}
|
|
2295
|
+
return [
|
|
2296
|
+
"import { remote } from 'webdriverio';",
|
|
2297
|
+
"",
|
|
2298
|
+
preamble,
|
|
2299
|
+
"try {",
|
|
2300
|
+
bsSteps,
|
|
2301
|
+
catchBlock,
|
|
2302
|
+
"} finally {",
|
|
2303
|
+
finallyBody,
|
|
2304
|
+
"}"
|
|
2305
|
+
].join("\n");
|
|
2306
|
+
}
|
|
2230
2307
|
return `import { remote } from 'webdriverio';
|
|
2231
2308
|
|
|
2232
2309
|
try {
|
|
@@ -2786,7 +2863,7 @@ var cookiesResource = {
|
|
|
2786
2863
|
|
|
2787
2864
|
// src/resources/app-state.resource.ts
|
|
2788
2865
|
init_state();
|
|
2789
|
-
import { ResourceTemplate as ResourceTemplate2 } from "@modelcontextprotocol/sdk/server/mcp";
|
|
2866
|
+
import { ResourceTemplate as ResourceTemplate2 } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2790
2867
|
async function readAppState(bundleId) {
|
|
2791
2868
|
try {
|
|
2792
2869
|
const browser = getBrowser();
|
|
@@ -2917,60 +2994,6 @@ import { z as z14 } from "zod";
|
|
|
2917
2994
|
|
|
2918
2995
|
// src/session/lifecycle.ts
|
|
2919
2996
|
init_state();
|
|
2920
|
-
function handleSessionTransition(newSessionId) {
|
|
2921
|
-
const state2 = getState();
|
|
2922
|
-
if (state2.currentSession && state2.currentSession !== newSessionId) {
|
|
2923
|
-
const outgoing = state2.sessionHistory.get(state2.currentSession);
|
|
2924
|
-
if (outgoing) {
|
|
2925
|
-
outgoing.steps.push({
|
|
2926
|
-
index: outgoing.steps.length + 1,
|
|
2927
|
-
tool: "__session_transition__",
|
|
2928
|
-
params: { newSessionId },
|
|
2929
|
-
status: "ok",
|
|
2930
|
-
durationMs: 0,
|
|
2931
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2932
|
-
});
|
|
2933
|
-
outgoing.endedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2934
|
-
}
|
|
2935
|
-
}
|
|
2936
|
-
}
|
|
2937
|
-
function registerSession(sessionId, browser, metadata, historyEntry) {
|
|
2938
|
-
const state2 = getState();
|
|
2939
|
-
const oldSessionId = state2.currentSession;
|
|
2940
|
-
if (oldSessionId && oldSessionId !== sessionId) {
|
|
2941
|
-
handleSessionTransition(sessionId);
|
|
2942
|
-
}
|
|
2943
|
-
state2.browsers.set(sessionId, browser);
|
|
2944
|
-
state2.sessionMetadata.set(sessionId, metadata);
|
|
2945
|
-
state2.sessionHistory.set(sessionId, historyEntry);
|
|
2946
|
-
state2.currentSession = sessionId;
|
|
2947
|
-
if (oldSessionId && oldSessionId !== sessionId) {
|
|
2948
|
-
const oldBrowser = state2.browsers.get(oldSessionId);
|
|
2949
|
-
if (oldBrowser) {
|
|
2950
|
-
oldBrowser.deleteSession().catch(() => {
|
|
2951
|
-
});
|
|
2952
|
-
state2.browsers.delete(oldSessionId);
|
|
2953
|
-
state2.sessionMetadata.delete(oldSessionId);
|
|
2954
|
-
}
|
|
2955
|
-
}
|
|
2956
|
-
}
|
|
2957
|
-
async function closeSession(sessionId, detach, isAttached, force) {
|
|
2958
|
-
const state2 = getState();
|
|
2959
|
-
const browser = state2.browsers.get(sessionId);
|
|
2960
|
-
if (!browser) return;
|
|
2961
|
-
const history = state2.sessionHistory.get(sessionId);
|
|
2962
|
-
if (history) {
|
|
2963
|
-
history.endedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2964
|
-
}
|
|
2965
|
-
if (force || !detach && !isAttached) {
|
|
2966
|
-
await browser.deleteSession();
|
|
2967
|
-
}
|
|
2968
|
-
state2.browsers.delete(sessionId);
|
|
2969
|
-
state2.sessionMetadata.delete(sessionId);
|
|
2970
|
-
if (state2.currentSession === sessionId) {
|
|
2971
|
-
state2.currentSession = null;
|
|
2972
|
-
}
|
|
2973
|
-
}
|
|
2974
2997
|
|
|
2975
2998
|
// src/providers/local-browser.provider.ts
|
|
2976
2999
|
var LocalBrowserProvider = class {
|
|
@@ -3220,6 +3243,8 @@ var LocalAppiumProvider = class {
|
|
|
3220
3243
|
var localAppiumProvider = new LocalAppiumProvider();
|
|
3221
3244
|
|
|
3222
3245
|
// src/providers/cloud/browserstack.provider.ts
|
|
3246
|
+
import { promisify } from "util";
|
|
3247
|
+
import { Local as BrowserstackTunnel } from "browserstack-local";
|
|
3223
3248
|
var BrowserStackProvider = class {
|
|
3224
3249
|
name = "browserstack";
|
|
3225
3250
|
getConnectionConfig(_options) {
|
|
@@ -3286,6 +3311,40 @@ var BrowserStackProvider = class {
|
|
|
3286
3311
|
shouldAutoDetach(_options) {
|
|
3287
3312
|
return false;
|
|
3288
3313
|
}
|
|
3314
|
+
async startTunnel(_options) {
|
|
3315
|
+
const key = process.env.BROWSERSTACK_ACCESS_KEY ?? "";
|
|
3316
|
+
const tunnel = new BrowserstackTunnel();
|
|
3317
|
+
const start = promisify(tunnel.start.bind(tunnel));
|
|
3318
|
+
try {
|
|
3319
|
+
await start({ key });
|
|
3320
|
+
} catch (e) {
|
|
3321
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
3322
|
+
if (msg.includes("another browserstack local client is running") || msg.includes("server is listening on port")) {
|
|
3323
|
+
console.error("[BrowserStack] Tunnel already running \u2014 reusing existing tunnel");
|
|
3324
|
+
return null;
|
|
3325
|
+
}
|
|
3326
|
+
throw e;
|
|
3327
|
+
}
|
|
3328
|
+
return tunnel;
|
|
3329
|
+
}
|
|
3330
|
+
async onSessionClose(sessionId, sessionType, result, tunnelHandle) {
|
|
3331
|
+
if (tunnelHandle) {
|
|
3332
|
+
const tunnel = tunnelHandle;
|
|
3333
|
+
const stop = promisify(tunnel.stop.bind(tunnel));
|
|
3334
|
+
await stop();
|
|
3335
|
+
}
|
|
3336
|
+
const user = process.env.BROWSERSTACK_USERNAME;
|
|
3337
|
+
const key = process.env.BROWSERSTACK_ACCESS_KEY;
|
|
3338
|
+
if (!user || !key) return;
|
|
3339
|
+
const baseUrl = sessionType === "browser" ? "https://api.browserstack.com/automate/sessions" : "https://api-cloud.browserstack.com/app-automate/sessions";
|
|
3340
|
+
const auth = Buffer.from(`${user}:${key}`).toString("base64");
|
|
3341
|
+
const body = { status: result.status, ...result.reason ? { reason: result.reason } : {} };
|
|
3342
|
+
await fetch(`${baseUrl}/${sessionId}.json`, {
|
|
3343
|
+
method: "PUT",
|
|
3344
|
+
headers: { Authorization: `Basic ${auth}`, "Content-Type": "application/json" },
|
|
3345
|
+
body: JSON.stringify(body)
|
|
3346
|
+
});
|
|
3347
|
+
}
|
|
3289
3348
|
};
|
|
3290
3349
|
var browserStackProvider = new BrowserStackProvider();
|
|
3291
3350
|
|
|
@@ -3295,6 +3354,84 @@ function getProvider(providerName, platform2) {
|
|
|
3295
3354
|
return platform2 === "browser" ? localBrowserProvider : localAppiumProvider;
|
|
3296
3355
|
}
|
|
3297
3356
|
|
|
3357
|
+
// src/session/lifecycle.ts
|
|
3358
|
+
function getSessionResult(history) {
|
|
3359
|
+
const errorStep = history?.steps.find((s) => s.status === "error");
|
|
3360
|
+
return errorStep ? { status: "failed", reason: errorStep.error } : { status: "passed" };
|
|
3361
|
+
}
|
|
3362
|
+
function handleSessionTransition(newSessionId) {
|
|
3363
|
+
const state2 = getState();
|
|
3364
|
+
if (state2.currentSession && state2.currentSession !== newSessionId) {
|
|
3365
|
+
const outgoing = state2.sessionHistory.get(state2.currentSession);
|
|
3366
|
+
if (outgoing) {
|
|
3367
|
+
outgoing.steps.push({
|
|
3368
|
+
index: outgoing.steps.length + 1,
|
|
3369
|
+
tool: "__session_transition__",
|
|
3370
|
+
params: { newSessionId },
|
|
3371
|
+
status: "ok",
|
|
3372
|
+
durationMs: 0,
|
|
3373
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
3374
|
+
});
|
|
3375
|
+
outgoing.endedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3376
|
+
}
|
|
3377
|
+
}
|
|
3378
|
+
}
|
|
3379
|
+
function registerSession(sessionId, browser, metadata, historyEntry) {
|
|
3380
|
+
const state2 = getState();
|
|
3381
|
+
const oldSessionId = state2.currentSession;
|
|
3382
|
+
if (oldSessionId && oldSessionId !== sessionId) {
|
|
3383
|
+
handleSessionTransition(sessionId);
|
|
3384
|
+
}
|
|
3385
|
+
state2.browsers.set(sessionId, browser);
|
|
3386
|
+
state2.sessionMetadata.set(sessionId, metadata);
|
|
3387
|
+
state2.sessionHistory.set(sessionId, historyEntry);
|
|
3388
|
+
state2.currentSession = sessionId;
|
|
3389
|
+
if (oldSessionId && oldSessionId !== sessionId) {
|
|
3390
|
+
const oldBrowser = state2.browsers.get(oldSessionId);
|
|
3391
|
+
const oldMetadata = state2.sessionMetadata.get(oldSessionId);
|
|
3392
|
+
if (oldBrowser) {
|
|
3393
|
+
void (async () => {
|
|
3394
|
+
if (oldMetadata?.provider) {
|
|
3395
|
+
const oldHistory = state2.sessionHistory.get(oldSessionId);
|
|
3396
|
+
const provider = getProvider(oldMetadata.provider, oldMetadata.type);
|
|
3397
|
+
await provider.onSessionClose?.(oldSessionId, oldMetadata.type, getSessionResult(oldHistory), oldMetadata.tunnelHandle).catch(() => {
|
|
3398
|
+
});
|
|
3399
|
+
}
|
|
3400
|
+
await oldBrowser.deleteSession().catch(() => {
|
|
3401
|
+
});
|
|
3402
|
+
})();
|
|
3403
|
+
state2.browsers.delete(oldSessionId);
|
|
3404
|
+
state2.sessionMetadata.delete(oldSessionId);
|
|
3405
|
+
}
|
|
3406
|
+
}
|
|
3407
|
+
}
|
|
3408
|
+
async function closeSession(sessionId, detach, isAttached, force) {
|
|
3409
|
+
const state2 = getState();
|
|
3410
|
+
const browser = state2.browsers.get(sessionId);
|
|
3411
|
+
if (!browser) return;
|
|
3412
|
+
const history = state2.sessionHistory.get(sessionId);
|
|
3413
|
+
if (history) {
|
|
3414
|
+
history.endedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3415
|
+
}
|
|
3416
|
+
const metadata = state2.sessionMetadata.get(sessionId);
|
|
3417
|
+
if (force || !detach && !isAttached) {
|
|
3418
|
+
if (metadata?.provider) {
|
|
3419
|
+
try {
|
|
3420
|
+
const provider = getProvider(metadata.provider, metadata.type);
|
|
3421
|
+
await provider.onSessionClose?.(sessionId, metadata.type, getSessionResult(history), metadata.tunnelHandle);
|
|
3422
|
+
} catch (e) {
|
|
3423
|
+
console.error("[WARN] Failed to run provider onSessionClose:", e);
|
|
3424
|
+
}
|
|
3425
|
+
}
|
|
3426
|
+
await browser.deleteSession();
|
|
3427
|
+
}
|
|
3428
|
+
state2.browsers.delete(sessionId);
|
|
3429
|
+
state2.sessionMetadata.delete(sessionId);
|
|
3430
|
+
if (state2.currentSession === sessionId) {
|
|
3431
|
+
state2.currentSession = null;
|
|
3432
|
+
}
|
|
3433
|
+
}
|
|
3434
|
+
|
|
3298
3435
|
// src/tools/session.tool.ts
|
|
3299
3436
|
var platformEnum = z14.enum(["browser", "ios", "android"]);
|
|
3300
3437
|
var browserEnum = z14.enum(["chrome", "firefox", "edge", "safari"]);
|
|
@@ -3340,7 +3477,7 @@ var startSessionToolDefinition = {
|
|
|
3340
3477
|
port: z14.number().optional(),
|
|
3341
3478
|
path: z14.string().optional()
|
|
3342
3479
|
}).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).
|
|
3480
|
+
browserstackLocal: coerceBoolean.optional().default(false).describe("Enable BrowserStack Local tunnel for testing against local/internal URLs (BrowserStack only, default: false). When true, the tunnel is started automatically before the session and stopped when the session is closed."),
|
|
3344
3481
|
navigationUrl: z14.string().optional().describe("URL to navigate to after starting"),
|
|
3345
3482
|
capabilities: z14.record(z14.string(), z14.unknown()).optional().describe("Additional capabilities to merge")
|
|
3346
3483
|
}
|
|
@@ -3426,12 +3563,15 @@ async function startBrowserSession(args) {
|
|
|
3426
3563
|
windowHeight,
|
|
3427
3564
|
capabilities: userCapabilities
|
|
3428
3565
|
});
|
|
3566
|
+
const tunnelHandle = args.browserstackLocal ? await provider.startTunnel?.(args) : void 0;
|
|
3429
3567
|
const wdioBrowser = await remote({ ...connectionConfig, capabilities: mergedCapabilities });
|
|
3430
3568
|
const { sessionId } = wdioBrowser;
|
|
3431
3569
|
const sessionMetadata = {
|
|
3432
3570
|
type: "browser",
|
|
3433
3571
|
capabilities: mergedCapabilities,
|
|
3434
|
-
isAttached: false
|
|
3572
|
+
isAttached: false,
|
|
3573
|
+
provider: args.provider ?? "local",
|
|
3574
|
+
tunnelHandle
|
|
3435
3575
|
};
|
|
3436
3576
|
registerSession(sessionId, wdioBrowser, sessionMetadata, {
|
|
3437
3577
|
sessionId,
|
|
@@ -3473,6 +3613,7 @@ async function startMobileSession(args) {
|
|
|
3473
3613
|
const provider = getProvider(args.provider ?? "local", args.platform);
|
|
3474
3614
|
const serverConfig = provider.getConnectionConfig(args);
|
|
3475
3615
|
const mergedCapabilities = provider.buildCapabilities(args);
|
|
3616
|
+
const tunnelHandle = args.browserstackLocal ? await provider.startTunnel?.(args) : void 0;
|
|
3476
3617
|
const browser = await remote({ ...serverConfig, capabilities: mergedCapabilities });
|
|
3477
3618
|
const { sessionId } = browser;
|
|
3478
3619
|
const shouldAutoDetach = provider.shouldAutoDetach(args);
|
|
@@ -3480,7 +3621,9 @@ async function startMobileSession(args) {
|
|
|
3480
3621
|
const metadata = {
|
|
3481
3622
|
type: sessionType,
|
|
3482
3623
|
capabilities: mergedCapabilities,
|
|
3483
|
-
isAttached: shouldAutoDetach
|
|
3624
|
+
isAttached: shouldAutoDetach,
|
|
3625
|
+
provider: args.provider ?? "local",
|
|
3626
|
+
tunnelHandle
|
|
3484
3627
|
};
|
|
3485
3628
|
registerSession(sessionId, browser, metadata, {
|
|
3486
3629
|
sessionId,
|
|
@@ -3526,7 +3669,8 @@ async function attachBrowserSession(args) {
|
|
|
3526
3669
|
const sessionMetadata = {
|
|
3527
3670
|
type: "browser",
|
|
3528
3671
|
capabilities,
|
|
3529
|
-
isAttached: true
|
|
3672
|
+
isAttached: true,
|
|
3673
|
+
provider: "local"
|
|
3530
3674
|
};
|
|
3531
3675
|
registerSession(sessionId, browser, sessionMetadata, {
|
|
3532
3676
|
sessionId,
|