@wdio/mcp 3.5.1 → 3.6.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 +201 -39
- package/lib/server.js +812 -77
- package/lib/server.js.map +1 -1
- package/lib/trace.js +3 -1
- package/lib/trace.js.map +1 -1
- package/package.json +3 -1
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.5.
|
|
49
|
+
version: "3.5.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",
|
|
@@ -91,12 +91,14 @@ var package_default = {
|
|
|
91
91
|
test: "vitest run"
|
|
92
92
|
},
|
|
93
93
|
dependencies: {
|
|
94
|
+
"@lambdatest/node-tunnel": "^4.0.11",
|
|
94
95
|
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
95
96
|
"@toon-format/toon": "^2.1.0",
|
|
96
97
|
"@wdio/protocols": "^9.27.0",
|
|
97
98
|
"@xmldom/xmldom": "^0.9.10",
|
|
98
99
|
"browserstack-local": "^1.5.12",
|
|
99
100
|
"puppeteer-core": "^24.40.0",
|
|
101
|
+
saucelabs: "^9.0.2",
|
|
100
102
|
sharp: "^0.34.5",
|
|
101
103
|
webdriverio: "^9.27.0",
|
|
102
104
|
xpath: "^0.0.34",
|
|
@@ -2427,6 +2429,134 @@ var browserstackLocalBinaryResource = {
|
|
|
2427
2429
|
}
|
|
2428
2430
|
};
|
|
2429
2431
|
|
|
2432
|
+
// src/resources/saucelabs-local.resource.ts
|
|
2433
|
+
function getLocalBinaryInfo2() {
|
|
2434
|
+
const platform2 = process.platform;
|
|
2435
|
+
const arch = process.arch;
|
|
2436
|
+
if (platform2 === "darwin") {
|
|
2437
|
+
return {
|
|
2438
|
+
downloadUrl: "https://saucelabs.com/downloads/sc-4.9.2-osx.zip",
|
|
2439
|
+
platform: "macOS",
|
|
2440
|
+
arch: arch === "arm64" ? "Apple Silicon" : "Intel x64",
|
|
2441
|
+
binaryName: "sc"
|
|
2442
|
+
};
|
|
2443
|
+
}
|
|
2444
|
+
if (platform2 === "win32") {
|
|
2445
|
+
return {
|
|
2446
|
+
downloadUrl: "https://saucelabs.com/downloads/sc-4.9.2-win32.zip",
|
|
2447
|
+
platform: "Windows",
|
|
2448
|
+
arch: "x86/x64",
|
|
2449
|
+
binaryName: "sc.exe"
|
|
2450
|
+
};
|
|
2451
|
+
}
|
|
2452
|
+
return {
|
|
2453
|
+
downloadUrl: "https://saucelabs.com/downloads/sc-4.9.2-linux.tar.gz",
|
|
2454
|
+
platform: "Linux",
|
|
2455
|
+
arch: arch === "arm64" ? "ARM64" : "x64",
|
|
2456
|
+
binaryName: "sc"
|
|
2457
|
+
};
|
|
2458
|
+
}
|
|
2459
|
+
var saucelabsLocalBinaryResource = {
|
|
2460
|
+
name: "saucelabs-local-binary",
|
|
2461
|
+
uri: "wdio://saucelabs/local-binary",
|
|
2462
|
+
description: "Sauce Connect Proxy binary download URL and daemon setup instructions for the current platform. Only needed for tunnel: 'external' \u2014 when tunnel: true the SDK auto-manages Sauce Connect for you.",
|
|
2463
|
+
handler: async () => {
|
|
2464
|
+
const info = getLocalBinaryInfo2();
|
|
2465
|
+
const username = process.env.SAUCE_USERNAME ?? "<SAUCE_USERNAME>";
|
|
2466
|
+
const accessKey = process.env.SAUCE_ACCESS_KEY ?? "<SAUCE_ACCESS_KEY>";
|
|
2467
|
+
const region = process.env.SAUCE_REGION ?? "eu-central-1";
|
|
2468
|
+
const content = {
|
|
2469
|
+
requirement: "ONLY needed for tunnel: 'external'. Use tunnel: true instead \u2014 the SDK starts and stops Sauce Connect automatically. With tunnel: 'external', you MUST start the daemon manually BEFORE calling start_session, and set tunnelName to match the running tunnel.",
|
|
2470
|
+
platform: info.platform,
|
|
2471
|
+
arch: info.arch,
|
|
2472
|
+
downloadUrl: info.downloadUrl,
|
|
2473
|
+
...info.note ? { note: info.note } : {},
|
|
2474
|
+
setup: [
|
|
2475
|
+
`1. Download: curl -O ${info.downloadUrl}`,
|
|
2476
|
+
`2. Unzip: ${info.downloadUrl.endsWith(".tar.gz") ? `tar -xzf ${info.downloadUrl.split("/").pop()}` : `unzip ${info.downloadUrl.split("/").pop()}`}`,
|
|
2477
|
+
`3. Make executable (macOS/Linux): chmod +x ${info.binaryName}`,
|
|
2478
|
+
`4. Start daemon: ./${info.binaryName} -u ${username} -k ${accessKey} --region ${region}`
|
|
2479
|
+
],
|
|
2480
|
+
commands: {
|
|
2481
|
+
start: `./${info.binaryName} -u ${username} -k ${accessKey} --region ${region}`,
|
|
2482
|
+
stop: `./${info.binaryName} --stop`,
|
|
2483
|
+
status: `./${info.binaryName} --status`
|
|
2484
|
+
},
|
|
2485
|
+
afterDaemonIsRunning: "Call start_session with tunnel: 'external' and tunnelName matching this daemon to route traffic through it."
|
|
2486
|
+
};
|
|
2487
|
+
return {
|
|
2488
|
+
contents: [{
|
|
2489
|
+
uri: "wdio://saucelabs/local-binary",
|
|
2490
|
+
mimeType: "application/json",
|
|
2491
|
+
text: JSON.stringify(content, null, 2)
|
|
2492
|
+
}]
|
|
2493
|
+
};
|
|
2494
|
+
}
|
|
2495
|
+
};
|
|
2496
|
+
|
|
2497
|
+
// src/resources/testmu-local.resource.ts
|
|
2498
|
+
function getLocalBinaryInfo3() {
|
|
2499
|
+
const platform2 = process.platform;
|
|
2500
|
+
const arch = process.arch;
|
|
2501
|
+
if (platform2 === "darwin") {
|
|
2502
|
+
return {
|
|
2503
|
+
downloadUrl: "https://downloads.lambdatest.com/tunnel/v4/darwin/64bit/LT_Darwin.zip",
|
|
2504
|
+
platform: "macOS",
|
|
2505
|
+
arch: arch === "arm64" ? "Apple Silicon (via Rosetta 2)" : "Intel x64",
|
|
2506
|
+
binaryName: "LT"
|
|
2507
|
+
};
|
|
2508
|
+
}
|
|
2509
|
+
if (platform2 === "win32") {
|
|
2510
|
+
return {
|
|
2511
|
+
downloadUrl: "https://downloads.lambdatest.com/tunnel/v4/win/64bit/LT_Windows.zip",
|
|
2512
|
+
platform: "Windows",
|
|
2513
|
+
arch: "x86/x64",
|
|
2514
|
+
binaryName: "LT.exe"
|
|
2515
|
+
};
|
|
2516
|
+
}
|
|
2517
|
+
return {
|
|
2518
|
+
downloadUrl: "https://downloads.lambdatest.com/tunnel/v4/linux/64bit/LT_Linux.zip",
|
|
2519
|
+
platform: "Linux",
|
|
2520
|
+
arch: arch === "arm64" ? "ARM64" : "x64",
|
|
2521
|
+
binaryName: "LT"
|
|
2522
|
+
};
|
|
2523
|
+
}
|
|
2524
|
+
var testmuLocalBinaryResource = {
|
|
2525
|
+
name: "testmu-local-binary",
|
|
2526
|
+
uri: "wdio://testmu/local-binary",
|
|
2527
|
+
description: "TestMu Tunnel binary download URL and daemon setup instructions for the current platform. MUST be read and followed before using tunnel: true in start_session with provider: testmu.",
|
|
2528
|
+
handler: async () => {
|
|
2529
|
+
const info = getLocalBinaryInfo3();
|
|
2530
|
+
const username = process.env.TESTMU_USERNAME ?? "<TESTMU_USERNAME>";
|
|
2531
|
+
const accessKey = process.env.TESTMU_ACCESS_KEY ?? "<TESTMU_ACCESS_KEY>";
|
|
2532
|
+
const content = {
|
|
2533
|
+
requirement: "MUST start the TestMu Tunnel daemon BEFORE calling start_session with tunnel: true. Without it, all navigation to local/internal URLs will fail.",
|
|
2534
|
+
platform: info.platform,
|
|
2535
|
+
arch: info.arch,
|
|
2536
|
+
downloadUrl: info.downloadUrl,
|
|
2537
|
+
setup: [
|
|
2538
|
+
`1. Download: curl -O ${info.downloadUrl}`,
|
|
2539
|
+
`2. Unzip: unzip ${info.downloadUrl.split("/").pop()}`,
|
|
2540
|
+
`3. Make executable (macOS/Linux): chmod +x ${info.binaryName}`,
|
|
2541
|
+
`4. Start daemon: ./${info.binaryName} --user ${username} --key ${accessKey}`
|
|
2542
|
+
],
|
|
2543
|
+
commands: {
|
|
2544
|
+
start: `./${info.binaryName} --user ${username} --key ${accessKey}`,
|
|
2545
|
+
stop: `./${info.binaryName} --user ${username} --key ${accessKey} --stop`,
|
|
2546
|
+
status: `./${info.binaryName} --status`
|
|
2547
|
+
},
|
|
2548
|
+
afterDaemonIsRunning: "Call start_session with tunnel: true and provider: testmu to route TestMu traffic through the tunnel."
|
|
2549
|
+
};
|
|
2550
|
+
return {
|
|
2551
|
+
contents: [{
|
|
2552
|
+
uri: "wdio://testmu/local-binary",
|
|
2553
|
+
mimeType: "application/json",
|
|
2554
|
+
text: JSON.stringify(content, null, 2)
|
|
2555
|
+
}]
|
|
2556
|
+
};
|
|
2557
|
+
}
|
|
2558
|
+
};
|
|
2559
|
+
|
|
2430
2560
|
// src/resources/sessions.resource.ts
|
|
2431
2561
|
import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2432
2562
|
|
|
@@ -2453,6 +2583,8 @@ function generateStep(step, history) {
|
|
|
2453
2583
|
case "start_session": {
|
|
2454
2584
|
const platform2 = p.platform;
|
|
2455
2585
|
const isBrowserStack = "bstack:options" in history.capabilities;
|
|
2586
|
+
const isSauceLabs = "sauce:options" in history.capabilities;
|
|
2587
|
+
const isLambdaTest = "lt:options" in history.capabilities;
|
|
2456
2588
|
const capJson = indentJson(history.capabilities).split("\n").map((line, i) => i > 0 ? ` ${line}` : line).join("\n");
|
|
2457
2589
|
if (isBrowserStack) {
|
|
2458
2590
|
const nav = platform2 === "browser" && p.navigationUrl ? `
|
|
@@ -2469,6 +2601,39 @@ await browser.url('${escapeStr(p.navigationUrl)}');` : "";
|
|
|
2469
2601
|
`});${nav}`
|
|
2470
2602
|
].join("\n");
|
|
2471
2603
|
}
|
|
2604
|
+
if (isSauceLabs) {
|
|
2605
|
+
const region = history.capabilities["sauce:options"]?.region ?? "eu-central-1";
|
|
2606
|
+
const nav = platform2 === "browser" && p.navigationUrl ? `
|
|
2607
|
+
await browser.url('${escapeStr(p.navigationUrl)}');` : "";
|
|
2608
|
+
return [
|
|
2609
|
+
"const browser = await remote({",
|
|
2610
|
+
" protocol: 'https',",
|
|
2611
|
+
` hostname: 'ondemand.${region}.saucelabs.com',`,
|
|
2612
|
+
" port: 443,",
|
|
2613
|
+
" path: '/wd/hub',",
|
|
2614
|
+
" user: process.env.SAUCE_USERNAME,",
|
|
2615
|
+
" key: process.env.SAUCE_ACCESS_KEY,",
|
|
2616
|
+
` capabilities: ${capJson}`,
|
|
2617
|
+
`});${nav}`
|
|
2618
|
+
].join("\n");
|
|
2619
|
+
}
|
|
2620
|
+
if (isLambdaTest) {
|
|
2621
|
+
const isBrowser = platform2 === "browser";
|
|
2622
|
+
const hostname = isBrowser ? "hub.lambdatest.com" : "mobile-hub.lambdatest.com";
|
|
2623
|
+
const nav = platform2 === "browser" && p.navigationUrl ? `
|
|
2624
|
+
await browser.url('${escapeStr(p.navigationUrl)}');` : "";
|
|
2625
|
+
return [
|
|
2626
|
+
"const browser = await remote({",
|
|
2627
|
+
" protocol: 'https',",
|
|
2628
|
+
` hostname: '${hostname}',`,
|
|
2629
|
+
" port: 443,",
|
|
2630
|
+
" path: '/wd/hub',",
|
|
2631
|
+
" user: process.env.TESTMU_USERNAME,",
|
|
2632
|
+
" key: process.env.TESTMU_ACCESS_KEY,",
|
|
2633
|
+
` capabilities: ${capJson}`,
|
|
2634
|
+
`});${nav}`
|
|
2635
|
+
].join("\n");
|
|
2636
|
+
}
|
|
2472
2637
|
if (platform2 === "browser") {
|
|
2473
2638
|
const nav = p.navigationUrl ? `
|
|
2474
2639
|
await browser.url('${escapeStr(p.navigationUrl)}');` : "";
|
|
@@ -2531,8 +2696,12 @@ function bsStatusUpdateLines(sessionType) {
|
|
|
2531
2696
|
}
|
|
2532
2697
|
function generateCode(history) {
|
|
2533
2698
|
const bstackOptions = history.capabilities["bstack:options"];
|
|
2699
|
+
const sauceOptions = history.capabilities["sauce:options"];
|
|
2700
|
+
const ltOptions = history.capabilities["lt:options"];
|
|
2534
2701
|
const isBrowserStack = bstackOptions !== void 0;
|
|
2535
|
-
const
|
|
2702
|
+
const isSauceLabs = sauceOptions !== void 0;
|
|
2703
|
+
const isLambdaTest = ltOptions !== void 0;
|
|
2704
|
+
const usesLocalTunnel = isBrowserStack ? bstackOptions?.local === true : isSauceLabs ? sauceOptions?.tunnelName !== void 0 : ltOptions?.tunnel === true;
|
|
2536
2705
|
const steps = history.steps.map((step) => generateStep(step, history)).join("\n").split("\n").map((line) => ` ${line}`).join("\n");
|
|
2537
2706
|
if (isBrowserStack) {
|
|
2538
2707
|
const bsSteps = steps.replace(/const browser = await remote\(/g, "browser = await remote(");
|
|
@@ -2543,7 +2712,7 @@ function generateCode(history) {
|
|
|
2543
2712
|
${statusUpdate}
|
|
2544
2713
|
await browser.deleteSession();
|
|
2545
2714
|
}`;
|
|
2546
|
-
if (
|
|
2715
|
+
if (usesLocalTunnel) {
|
|
2547
2716
|
const tunnelSetup = [
|
|
2548
2717
|
"",
|
|
2549
2718
|
"const tunnel = new BrowserstackTunnel();",
|
|
@@ -2579,6 +2748,119 @@ ${statusUpdate}
|
|
|
2579
2748
|
"}"
|
|
2580
2749
|
].join("\n");
|
|
2581
2750
|
}
|
|
2751
|
+
if (isSauceLabs) {
|
|
2752
|
+
const slSteps = steps.replace(/const browser = await remote\(/g, "browser = await remote(");
|
|
2753
|
+
const preamble = "let browser;\nlet slStatus = 'passed';\nlet slReason;";
|
|
2754
|
+
const catchBlock = "} catch (e) {\n slStatus = 'failed';\n slReason = String(e);\n throw e;";
|
|
2755
|
+
const slRegion = sauceOptions?.region ?? "eu-central-1";
|
|
2756
|
+
const statusUpdate = [
|
|
2757
|
+
" const slAuth = Buffer.from(`${process.env.SAUCE_USERNAME}:${process.env.SAUCE_ACCESS_KEY}`).toString('base64');",
|
|
2758
|
+
` await fetch(\`https://api.${slRegion}.saucelabs.com/rest/v1/\${process.env.SAUCE_USERNAME}/jobs/\` + browser.sessionId, {`,
|
|
2759
|
+
" method: 'PUT',",
|
|
2760
|
+
" headers: { Authorization: 'Basic ' + slAuth, 'Content-Type': 'application/json' },",
|
|
2761
|
+
" body: JSON.stringify({ passed: slStatus === 'passed' })",
|
|
2762
|
+
" });"
|
|
2763
|
+
].join("\n");
|
|
2764
|
+
const finallyLines = [
|
|
2765
|
+
" if (browser) {",
|
|
2766
|
+
statusUpdate,
|
|
2767
|
+
" await browser.deleteSession();",
|
|
2768
|
+
" }"
|
|
2769
|
+
];
|
|
2770
|
+
if (usesLocalTunnel) {
|
|
2771
|
+
const tunnelSetup = [
|
|
2772
|
+
"",
|
|
2773
|
+
"import SauceLabs from 'saucelabs';",
|
|
2774
|
+
"",
|
|
2775
|
+
"const sl = new SauceLabs({",
|
|
2776
|
+
" user: process.env.SAUCE_USERNAME,",
|
|
2777
|
+
" key: process.env.SAUCE_ACCESS_KEY,",
|
|
2778
|
+
` region: '${slRegion}',`,
|
|
2779
|
+
"});",
|
|
2780
|
+
"const sc = await sl.startSauceConnect({ tunnelName: 'wdio-mcp-tunnel' });",
|
|
2781
|
+
"const stopTunnel = () => sc.close();",
|
|
2782
|
+
""
|
|
2783
|
+
].join("\n");
|
|
2784
|
+
return [
|
|
2785
|
+
"import { remote } from 'webdriverio';",
|
|
2786
|
+
tunnelSetup,
|
|
2787
|
+
preamble,
|
|
2788
|
+
"try {",
|
|
2789
|
+
slSteps,
|
|
2790
|
+
catchBlock,
|
|
2791
|
+
"} finally {",
|
|
2792
|
+
...finallyLines,
|
|
2793
|
+
" await stopTunnel();",
|
|
2794
|
+
"}"
|
|
2795
|
+
].join("\n");
|
|
2796
|
+
}
|
|
2797
|
+
return [
|
|
2798
|
+
"import { remote } from 'webdriverio';",
|
|
2799
|
+
"",
|
|
2800
|
+
preamble,
|
|
2801
|
+
"try {",
|
|
2802
|
+
slSteps,
|
|
2803
|
+
catchBlock,
|
|
2804
|
+
"} finally {",
|
|
2805
|
+
...finallyLines,
|
|
2806
|
+
"}"
|
|
2807
|
+
].join("\n");
|
|
2808
|
+
}
|
|
2809
|
+
if (isLambdaTest) {
|
|
2810
|
+
const ltSteps = steps.replace(/const browser = await remote\(/g, "browser = await remote(");
|
|
2811
|
+
const preamble = "let browser;\nlet ltStatus = 'passed';";
|
|
2812
|
+
const catchBlock = "} catch (e) {\n ltStatus = 'failed';\n throw e;";
|
|
2813
|
+
const isMobile = history.type !== "browser";
|
|
2814
|
+
const statusUpdate = isMobile ? " await browser.execute('lambda-status=' + ltStatus);" : [
|
|
2815
|
+
" const ltAuth = Buffer.from(`${process.env.TESTMU_USERNAME}:${process.env.TESTMU_ACCESS_KEY}`).toString('base64');",
|
|
2816
|
+
" await fetch('https://api.lambdatest.com/automation/api/v1/sessions/' + browser.sessionId, {",
|
|
2817
|
+
" method: 'PATCH',",
|
|
2818
|
+
" headers: { Authorization: 'Basic ' + ltAuth, 'Content-Type': 'application/json' },",
|
|
2819
|
+
" body: JSON.stringify({ status_ind: ltStatus })",
|
|
2820
|
+
" });"
|
|
2821
|
+
].join("\n");
|
|
2822
|
+
const finallyLines = [
|
|
2823
|
+
" if (browser) {",
|
|
2824
|
+
statusUpdate,
|
|
2825
|
+
" await browser.deleteSession();",
|
|
2826
|
+
" }"
|
|
2827
|
+
];
|
|
2828
|
+
if (usesLocalTunnel) {
|
|
2829
|
+
const tunnelName = ltOptions?.tunnelName ?? "wdio-mcp-tunnel";
|
|
2830
|
+
const tunnelSetup = [
|
|
2831
|
+
"",
|
|
2832
|
+
"import LambdaTunnel from '@lambdatest/node-tunnel';",
|
|
2833
|
+
"",
|
|
2834
|
+
"const tunnel = new LambdaTunnel();",
|
|
2835
|
+
`await tunnel.start({ user: process.env.TESTMU_USERNAME, key: process.env.TESTMU_ACCESS_KEY, tunnelName: '${escapeStr(tunnelName)}' });`,
|
|
2836
|
+
"const stopTunnel = () => tunnel.stop();",
|
|
2837
|
+
""
|
|
2838
|
+
].join("\n");
|
|
2839
|
+
return [
|
|
2840
|
+
"import { remote } from 'webdriverio';",
|
|
2841
|
+
tunnelSetup,
|
|
2842
|
+
preamble,
|
|
2843
|
+
"try {",
|
|
2844
|
+
ltSteps,
|
|
2845
|
+
catchBlock,
|
|
2846
|
+
"} finally {",
|
|
2847
|
+
...finallyLines,
|
|
2848
|
+
" await stopTunnel();",
|
|
2849
|
+
"}"
|
|
2850
|
+
].join("\n");
|
|
2851
|
+
}
|
|
2852
|
+
return [
|
|
2853
|
+
"import { remote } from 'webdriverio';",
|
|
2854
|
+
"",
|
|
2855
|
+
preamble,
|
|
2856
|
+
"try {",
|
|
2857
|
+
ltSteps,
|
|
2858
|
+
catchBlock,
|
|
2859
|
+
"} finally {",
|
|
2860
|
+
...finallyLines,
|
|
2861
|
+
"}"
|
|
2862
|
+
].join("\n");
|
|
2863
|
+
}
|
|
2582
2864
|
return `import { remote } from 'webdriverio';
|
|
2583
2865
|
|
|
2584
2866
|
try {
|
|
@@ -3276,7 +3558,7 @@ import { z as z14 } from "zod";
|
|
|
3276
3558
|
// src/session/lifecycle.ts
|
|
3277
3559
|
init_state();
|
|
3278
3560
|
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
3279
|
-
import { join as
|
|
3561
|
+
import { join as join5 } from "path";
|
|
3280
3562
|
|
|
3281
3563
|
// src/providers/local-browser.provider.ts
|
|
3282
3564
|
var LocalBrowserProvider = class {
|
|
@@ -3531,6 +3813,13 @@ import { promisify } from "util";
|
|
|
3531
3813
|
import { tmpdir as tmpdir2 } from "os";
|
|
3532
3814
|
import { join as join2 } from "path";
|
|
3533
3815
|
import { Local as BrowserstackTunnel } from "browserstack-local";
|
|
3816
|
+
|
|
3817
|
+
// src/utils/auth.ts
|
|
3818
|
+
function basicAuth(user, key) {
|
|
3819
|
+
return Buffer.from(`${user}:${key}`).toString("base64");
|
|
3820
|
+
}
|
|
3821
|
+
|
|
3822
|
+
// src/providers/cloud/browserstack.provider.ts
|
|
3534
3823
|
var BrowserStackProvider = class {
|
|
3535
3824
|
name = "browserstack";
|
|
3536
3825
|
getConnectionConfig(options) {
|
|
@@ -3548,7 +3837,7 @@ var BrowserStackProvider = class {
|
|
|
3548
3837
|
buildCapabilities(options) {
|
|
3549
3838
|
const platform2 = options.platform;
|
|
3550
3839
|
const userCapabilities = options.capabilities ?? {};
|
|
3551
|
-
const browserstackLocal = options.browserstackLocal;
|
|
3840
|
+
const browserstackLocal = options.tunnel ?? options.browserstackLocal;
|
|
3552
3841
|
if (platform2 === "browser") {
|
|
3553
3842
|
const bstackOptions2 = {
|
|
3554
3843
|
browserVersion: options.browserVersion ?? "latest"
|
|
@@ -3561,9 +3850,9 @@ var BrowserStackProvider = class {
|
|
|
3561
3850
|
if (reporting2?.build) bstackOptions2.buildName = reporting2.build;
|
|
3562
3851
|
if (reporting2?.session) bstackOptions2.sessionName = reporting2.session;
|
|
3563
3852
|
return {
|
|
3853
|
+
...userCapabilities,
|
|
3564
3854
|
browserName: options.browser ?? "chrome",
|
|
3565
|
-
"bstack:options": bstackOptions2
|
|
3566
|
-
...userCapabilities
|
|
3855
|
+
"bstack:options": bstackOptions2
|
|
3567
3856
|
};
|
|
3568
3857
|
}
|
|
3569
3858
|
const bstackOptions = {
|
|
@@ -3581,14 +3870,14 @@ var BrowserStackProvider = class {
|
|
|
3581
3870
|
const autoAcceptAlerts = options.autoAcceptAlerts;
|
|
3582
3871
|
const autoDismissAlerts = options.autoDismissAlerts;
|
|
3583
3872
|
return {
|
|
3873
|
+
...userCapabilities,
|
|
3584
3874
|
platformName: platform2,
|
|
3585
3875
|
"appium:app": options.app,
|
|
3586
3876
|
"appium:autoGrantPermissions": options.autoGrantPermissions ?? true,
|
|
3587
3877
|
"appium:autoAcceptAlerts": autoDismissAlerts ? void 0 : autoAcceptAlerts ?? true,
|
|
3588
3878
|
"appium:autoDismissAlerts": autoDismissAlerts,
|
|
3589
3879
|
"appium:newCommandTimeout": options.newCommandTimeout ?? 300,
|
|
3590
|
-
"bstack:options": bstackOptions
|
|
3591
|
-
...userCapabilities
|
|
3880
|
+
"bstack:options": bstackOptions
|
|
3592
3881
|
};
|
|
3593
3882
|
}
|
|
3594
3883
|
getSessionType(options) {
|
|
@@ -3626,7 +3915,7 @@ var BrowserStackProvider = class {
|
|
|
3626
3915
|
const key = process.env.BROWSERSTACK_ACCESS_KEY;
|
|
3627
3916
|
if (!user || !key) return;
|
|
3628
3917
|
const baseUrl = sessionType === "browser" ? "https://api.browserstack.com/automate/sessions" : "https://api-cloud.browserstack.com/app-automate/sessions";
|
|
3629
|
-
const auth =
|
|
3918
|
+
const auth = basicAuth(user, key);
|
|
3630
3919
|
const body = { status: result.status, ...result.reason ? { reason: result.reason } : {} };
|
|
3631
3920
|
await fetch(`${baseUrl}/${sessionId}.json`, {
|
|
3632
3921
|
method: "PUT",
|
|
@@ -3637,11 +3926,305 @@ var BrowserStackProvider = class {
|
|
|
3637
3926
|
};
|
|
3638
3927
|
var browserStackProvider = new BrowserStackProvider();
|
|
3639
3928
|
|
|
3929
|
+
// src/providers/cloud/saucelabs.provider.ts
|
|
3930
|
+
import SauceLabs from "saucelabs";
|
|
3931
|
+
import { tmpdir as tmpdir3 } from "os";
|
|
3932
|
+
import { join as join3 } from "path";
|
|
3933
|
+
var SauceLabsProvider = class {
|
|
3934
|
+
name = "saucelabs";
|
|
3935
|
+
resolveRegion(options) {
|
|
3936
|
+
return options.region ?? "eu-central-1";
|
|
3937
|
+
}
|
|
3938
|
+
getConnectionConfig(options) {
|
|
3939
|
+
const region = this.resolveRegion(options);
|
|
3940
|
+
return {
|
|
3941
|
+
protocol: "https",
|
|
3942
|
+
hostname: `ondemand.${region}.saucelabs.com`,
|
|
3943
|
+
port: 443,
|
|
3944
|
+
path: "/wd/hub",
|
|
3945
|
+
user: process.env.SAUCE_USERNAME,
|
|
3946
|
+
key: process.env.SAUCE_ACCESS_KEY
|
|
3947
|
+
};
|
|
3948
|
+
}
|
|
3949
|
+
buildCapabilities(options) {
|
|
3950
|
+
const platform2 = options.platform;
|
|
3951
|
+
const region = this.resolveRegion(options);
|
|
3952
|
+
const userCapabilities = options.capabilities ?? {};
|
|
3953
|
+
const saucelabsLocal = options.tunnel ?? options.saucelabsLocal;
|
|
3954
|
+
const reporting = options.reporting;
|
|
3955
|
+
const sauceOptions = { region };
|
|
3956
|
+
if (reporting?.build) sauceOptions.build = reporting.build;
|
|
3957
|
+
if (reporting?.session) sauceOptions.name = reporting.session;
|
|
3958
|
+
else if (reporting?.project) sauceOptions.name = reporting.project;
|
|
3959
|
+
if (saucelabsLocal) {
|
|
3960
|
+
sauceOptions.tunnelName = options.tunnelName;
|
|
3961
|
+
}
|
|
3962
|
+
if (platform2 === "browser") {
|
|
3963
|
+
return {
|
|
3964
|
+
...userCapabilities,
|
|
3965
|
+
browserName: options.browser ?? "chrome",
|
|
3966
|
+
browserVersion: options.browserVersion ?? "latest",
|
|
3967
|
+
platformName: options.os ? [options.os, options.osVersion].filter(Boolean).join(" ") : "Linux",
|
|
3968
|
+
"sauce:options": sauceOptions
|
|
3969
|
+
};
|
|
3970
|
+
}
|
|
3971
|
+
const mobileBrowser = options.browser;
|
|
3972
|
+
if (mobileBrowser) {
|
|
3973
|
+
if (options.deviceOrientation) sauceOptions.deviceOrientation = options.deviceOrientation;
|
|
3974
|
+
const caps = {
|
|
3975
|
+
platformName: platform2,
|
|
3976
|
+
browserName: mobileBrowser,
|
|
3977
|
+
"appium:deviceName": options.deviceName,
|
|
3978
|
+
"appium:platformVersion": options.platformVersion,
|
|
3979
|
+
"appium:automationName": options.automationName ?? (platform2 === "ios" ? "XCUITest" : "UiAutomator2"),
|
|
3980
|
+
"appium:newCommandTimeout": options.newCommandTimeout ?? 300,
|
|
3981
|
+
"sauce:options": sauceOptions
|
|
3982
|
+
};
|
|
3983
|
+
return { ...userCapabilities, ...caps };
|
|
3984
|
+
}
|
|
3985
|
+
sauceOptions.appiumVersion = "latest";
|
|
3986
|
+
const autoAcceptAlerts = options.autoAcceptAlerts;
|
|
3987
|
+
const autoDismissAlerts = options.autoDismissAlerts;
|
|
3988
|
+
return {
|
|
3989
|
+
...userCapabilities,
|
|
3990
|
+
platformName: platform2,
|
|
3991
|
+
"appium:app": options.app,
|
|
3992
|
+
"appium:deviceName": options.deviceName,
|
|
3993
|
+
"appium:platformVersion": options.platformVersion,
|
|
3994
|
+
"appium:automationName": options.automationName ?? (platform2 === "ios" ? "XCUITest" : "UiAutomator2"),
|
|
3995
|
+
"appium:autoGrantPermissions": options.autoGrantPermissions ?? true,
|
|
3996
|
+
"appium:autoAcceptAlerts": autoDismissAlerts ? void 0 : autoAcceptAlerts ?? true,
|
|
3997
|
+
"appium:autoDismissAlerts": autoDismissAlerts,
|
|
3998
|
+
"appium:newCommandTimeout": options.newCommandTimeout ?? 300,
|
|
3999
|
+
"sauce:options": sauceOptions
|
|
4000
|
+
};
|
|
4001
|
+
}
|
|
4002
|
+
getSessionType(options) {
|
|
4003
|
+
const platform2 = options.platform;
|
|
4004
|
+
if (platform2 === "browser") return "browser";
|
|
4005
|
+
return platform2;
|
|
4006
|
+
}
|
|
4007
|
+
shouldAutoDetach(_options) {
|
|
4008
|
+
return false;
|
|
4009
|
+
}
|
|
4010
|
+
async startTunnel(options) {
|
|
4011
|
+
const region = this.resolveRegion(options);
|
|
4012
|
+
const tunnelName = options.tunnelName ?? `wdio-mcp-${Date.now()}`;
|
|
4013
|
+
const logFile = join3(tmpdir3(), "sauce-connect.log");
|
|
4014
|
+
console.error(`[SauceLabs] Starting tunnel "${tunnelName}" (region: ${region})`);
|
|
4015
|
+
try {
|
|
4016
|
+
const api = new SauceLabs({ user: process.env.SAUCE_USERNAME ?? "", key: process.env.SAUCE_ACCESS_KEY ?? "", region });
|
|
4017
|
+
return api.startSauceConnect({
|
|
4018
|
+
tunnelName,
|
|
4019
|
+
logFile,
|
|
4020
|
+
logger: (msg) => console.error(`[SauceConnect] ${msg}`)
|
|
4021
|
+
});
|
|
4022
|
+
} catch (e) {
|
|
4023
|
+
const msg = (e !== null && typeof e === "object" ? e.message : void 0) ?? String(e);
|
|
4024
|
+
if (msg.includes("already running") || msg.includes("another instance")) {
|
|
4025
|
+
console.error("[SauceLabs] Tunnel already running \u2014 reusing existing tunnel");
|
|
4026
|
+
return null;
|
|
4027
|
+
}
|
|
4028
|
+
throw e;
|
|
4029
|
+
}
|
|
4030
|
+
}
|
|
4031
|
+
async onSessionClose(sessionId, _sessionType, result, _tunnelHandle, _browser, region) {
|
|
4032
|
+
const effectiveRegion = region ?? "eu-central-1";
|
|
4033
|
+
const user = process.env.SAUCE_USERNAME;
|
|
4034
|
+
const key = process.env.SAUCE_ACCESS_KEY;
|
|
4035
|
+
if (user && key) {
|
|
4036
|
+
try {
|
|
4037
|
+
const auth = basicAuth(user, key);
|
|
4038
|
+
const body = { passed: result.status === "passed" };
|
|
4039
|
+
const apiUrl = `https://api.${effectiveRegion}.saucelabs.com/rest/v1/${user}/jobs/${sessionId}`;
|
|
4040
|
+
console.error(`[SauceLabs] Setting job status for ${sessionId}: ${result.status}`);
|
|
4041
|
+
await fetch(apiUrl, {
|
|
4042
|
+
method: "PUT",
|
|
4043
|
+
headers: {
|
|
4044
|
+
Authorization: `Basic ${auth}`,
|
|
4045
|
+
"Content-Type": "application/json"
|
|
4046
|
+
},
|
|
4047
|
+
body: JSON.stringify(body)
|
|
4048
|
+
});
|
|
4049
|
+
console.error("[SauceLabs] Job result set successfully via REST API");
|
|
4050
|
+
} catch (e) {
|
|
4051
|
+
console.error("[SauceLabs] Failed to set job result via REST API:", e);
|
|
4052
|
+
}
|
|
4053
|
+
}
|
|
4054
|
+
}
|
|
4055
|
+
async stopTunnel(tunnelHandle) {
|
|
4056
|
+
if (tunnelHandle) {
|
|
4057
|
+
const sc = tunnelHandle;
|
|
4058
|
+
await sc.close();
|
|
4059
|
+
}
|
|
4060
|
+
}
|
|
4061
|
+
};
|
|
4062
|
+
var sauceLabsProvider = new SauceLabsProvider();
|
|
4063
|
+
|
|
4064
|
+
// src/providers/cloud/testmu.provider.ts
|
|
4065
|
+
import LambdaTunnel from "@lambdatest/node-tunnel";
|
|
4066
|
+
import { tmpdir as tmpdir4 } from "os";
|
|
4067
|
+
import { join as join4 } from "path";
|
|
4068
|
+
var TestMuProvider = class {
|
|
4069
|
+
name = "testmu";
|
|
4070
|
+
getConnectionConfig(options) {
|
|
4071
|
+
const platform2 = options.platform;
|
|
4072
|
+
const isBrowser = platform2 === "browser";
|
|
4073
|
+
return {
|
|
4074
|
+
protocol: "https",
|
|
4075
|
+
hostname: isBrowser ? "hub.lambdatest.com" : "mobile-hub.lambdatest.com",
|
|
4076
|
+
port: 443,
|
|
4077
|
+
path: "/wd/hub",
|
|
4078
|
+
user: process.env.TESTMU_USERNAME,
|
|
4079
|
+
key: process.env.TESTMU_ACCESS_KEY
|
|
4080
|
+
};
|
|
4081
|
+
}
|
|
4082
|
+
buildCapabilities(options) {
|
|
4083
|
+
const platform2 = options.platform;
|
|
4084
|
+
const userCapabilities = options.capabilities ?? {};
|
|
4085
|
+
const tunnel = options.tunnel ?? options.testmuLocal;
|
|
4086
|
+
const reporting = options.reporting;
|
|
4087
|
+
const ltOptions = { w3c: true };
|
|
4088
|
+
if (reporting?.project) ltOptions.project = reporting.project;
|
|
4089
|
+
if (reporting?.build) ltOptions.build = reporting.build;
|
|
4090
|
+
if (reporting?.session) ltOptions.name = reporting.session;
|
|
4091
|
+
else if (reporting?.project) ltOptions.name = reporting.project;
|
|
4092
|
+
if (tunnel) {
|
|
4093
|
+
ltOptions.tunnel = true;
|
|
4094
|
+
if (options.tunnelName) ltOptions.tunnelName = options.tunnelName;
|
|
4095
|
+
}
|
|
4096
|
+
if (platform2 === "browser") {
|
|
4097
|
+
return {
|
|
4098
|
+
...userCapabilities,
|
|
4099
|
+
browserName: options.browser ?? "chrome",
|
|
4100
|
+
browserVersion: options.browserVersion ?? "latest",
|
|
4101
|
+
platformName: options.os ? [options.os, options.osVersion].filter(Boolean).join(" ") : "Linux",
|
|
4102
|
+
"lt:options": ltOptions
|
|
4103
|
+
};
|
|
4104
|
+
}
|
|
4105
|
+
const mobileBrowser = options.browser;
|
|
4106
|
+
if (mobileBrowser) {
|
|
4107
|
+
ltOptions.isRealMobile = false;
|
|
4108
|
+
if (options.deviceOrientation) ltOptions.deviceOrientation = options.deviceOrientation;
|
|
4109
|
+
const caps2 = {
|
|
4110
|
+
platformName: platform2,
|
|
4111
|
+
browserName: mobileBrowser,
|
|
4112
|
+
"appium:deviceName": options.deviceName,
|
|
4113
|
+
"appium:platformVersion": options.platformVersion,
|
|
4114
|
+
"appium:automationName": options.automationName ?? (platform2 === "ios" ? "XCUITest" : "UiAutomator2"),
|
|
4115
|
+
"appium:newCommandTimeout": options.newCommandTimeout ?? 300,
|
|
4116
|
+
"lt:options": ltOptions
|
|
4117
|
+
};
|
|
4118
|
+
return { ...userCapabilities, ...caps2 };
|
|
4119
|
+
}
|
|
4120
|
+
ltOptions.appiumVersion = "latest";
|
|
4121
|
+
ltOptions.isRealMobile = true;
|
|
4122
|
+
const autoAcceptAlerts = options.autoAcceptAlerts;
|
|
4123
|
+
const autoDismissAlerts = options.autoDismissAlerts;
|
|
4124
|
+
const caps = {
|
|
4125
|
+
platformName: platform2,
|
|
4126
|
+
"appium:app": options.app,
|
|
4127
|
+
"appium:deviceName": options.deviceName,
|
|
4128
|
+
"appium:platformVersion": options.platformVersion,
|
|
4129
|
+
"appium:automationName": options.automationName ?? (platform2 === "ios" ? "XCUITest" : "UiAutomator2"),
|
|
4130
|
+
"appium:autoGrantPermissions": options.autoGrantPermissions ?? true,
|
|
4131
|
+
"appium:autoAcceptAlerts": autoDismissAlerts ? void 0 : autoAcceptAlerts ?? true,
|
|
4132
|
+
"appium:autoDismissAlerts": autoDismissAlerts,
|
|
4133
|
+
"appium:newCommandTimeout": options.newCommandTimeout ?? 300,
|
|
4134
|
+
"lt:options": ltOptions
|
|
4135
|
+
};
|
|
4136
|
+
return { ...userCapabilities, ...caps };
|
|
4137
|
+
}
|
|
4138
|
+
getSessionType(options) {
|
|
4139
|
+
const platform2 = options.platform;
|
|
4140
|
+
if (platform2 === "browser") return "browser";
|
|
4141
|
+
return platform2;
|
|
4142
|
+
}
|
|
4143
|
+
shouldAutoDetach(_options) {
|
|
4144
|
+
return false;
|
|
4145
|
+
}
|
|
4146
|
+
async startTunnel(options) {
|
|
4147
|
+
const tunnelName = options.tunnelName ?? `wdio-mcp-testmu-${Date.now()}`;
|
|
4148
|
+
const logFile = join4(tmpdir4(), "testmu-tunnel.log");
|
|
4149
|
+
console.error(`[TestMu] Starting tunnel "${tunnelName}"`);
|
|
4150
|
+
try {
|
|
4151
|
+
const tunnel = new LambdaTunnel();
|
|
4152
|
+
await tunnel.start({
|
|
4153
|
+
user: process.env.TESTMU_USERNAME ?? "",
|
|
4154
|
+
key: process.env.TESTMU_ACCESS_KEY ?? "",
|
|
4155
|
+
tunnelName,
|
|
4156
|
+
logFile
|
|
4157
|
+
});
|
|
4158
|
+
console.error(`[TestMu] Tunnel started: "${tunnelName}"`);
|
|
4159
|
+
return tunnel;
|
|
4160
|
+
} catch (e) {
|
|
4161
|
+
const msg = (e !== null && typeof e === "object" ? e.message : void 0) ?? String(e);
|
|
4162
|
+
if (msg.includes("already running") || msg.includes("another tunnel") || msg.includes("already in use")) {
|
|
4163
|
+
console.error("[TestMu] Tunnel already running \u2014 reusing existing tunnel");
|
|
4164
|
+
return null;
|
|
4165
|
+
}
|
|
4166
|
+
throw e;
|
|
4167
|
+
}
|
|
4168
|
+
}
|
|
4169
|
+
async onSessionClose(sessionId, sessionType, result, _tunnelHandle, browser, _region) {
|
|
4170
|
+
const user = process.env.TESTMU_USERNAME;
|
|
4171
|
+
const key = process.env.TESTMU_ACCESS_KEY;
|
|
4172
|
+
if (!user || !key) return;
|
|
4173
|
+
const status = result.status === "passed" ? "passed" : "failed";
|
|
4174
|
+
if (sessionType !== "browser") {
|
|
4175
|
+
try {
|
|
4176
|
+
console.error(`[TestMu] Setting mobile session status for ${sessionId}: ${status}`);
|
|
4177
|
+
await browser?.execute("lambda-status=" + status);
|
|
4178
|
+
console.error("[TestMu] Mobile session status set successfully via execute");
|
|
4179
|
+
} catch (e) {
|
|
4180
|
+
console.error("[TestMu] Failed to set mobile session status via execute:", e);
|
|
4181
|
+
}
|
|
4182
|
+
return;
|
|
4183
|
+
}
|
|
4184
|
+
try {
|
|
4185
|
+
const auth = basicAuth(user, key);
|
|
4186
|
+
const body = { status_ind: status };
|
|
4187
|
+
const apiUrl = `https://api.lambdatest.com/automation/api/v1/sessions/${sessionId}`;
|
|
4188
|
+
console.error(`[TestMu] Setting session status for ${sessionId}: ${body.status_ind}`);
|
|
4189
|
+
const res = await fetch(apiUrl, {
|
|
4190
|
+
method: "PATCH",
|
|
4191
|
+
headers: {
|
|
4192
|
+
Authorization: `Basic ${auth}`,
|
|
4193
|
+
"Content-Type": "application/json"
|
|
4194
|
+
},
|
|
4195
|
+
body: JSON.stringify(body)
|
|
4196
|
+
});
|
|
4197
|
+
if (res.ok) {
|
|
4198
|
+
console.error("[TestMu] Session status set successfully via REST API");
|
|
4199
|
+
} else {
|
|
4200
|
+
const resBody = await res.text();
|
|
4201
|
+
console.error(`[TestMu] Failed to set session status: HTTP ${res.status} \u2014 ${resBody}`);
|
|
4202
|
+
}
|
|
4203
|
+
} catch (e) {
|
|
4204
|
+
console.error("[TestMu] Failed to set session status via REST API:", e);
|
|
4205
|
+
}
|
|
4206
|
+
}
|
|
4207
|
+
async stopTunnel(tunnelHandle) {
|
|
4208
|
+
if (tunnelHandle) {
|
|
4209
|
+
const tunnel = tunnelHandle;
|
|
4210
|
+
await tunnel.stop();
|
|
4211
|
+
}
|
|
4212
|
+
}
|
|
4213
|
+
};
|
|
4214
|
+
var testMuProvider = new TestMuProvider();
|
|
4215
|
+
|
|
3640
4216
|
// src/providers/registry.ts
|
|
3641
|
-
|
|
3642
|
-
|
|
4217
|
+
var providers = /* @__PURE__ */ new Map([
|
|
4218
|
+
["browserstack", browserStackProvider],
|
|
4219
|
+
["saucelabs", sauceLabsProvider],
|
|
4220
|
+
["testmu", testMuProvider]
|
|
4221
|
+
]);
|
|
4222
|
+
function getDefaultProvider(platform2) {
|
|
3643
4223
|
return platform2 === "browser" ? localBrowserProvider : localAppiumProvider;
|
|
3644
4224
|
}
|
|
4225
|
+
function getProvider(providerName, platform2) {
|
|
4226
|
+
return providers.get(providerName) ?? getDefaultProvider(platform2);
|
|
4227
|
+
}
|
|
3645
4228
|
|
|
3646
4229
|
// src/trace/zip-writer.ts
|
|
3647
4230
|
import yazl from "yazl";
|
|
@@ -3671,10 +4254,10 @@ async function finalizeTrace(sessionId, browser) {
|
|
|
3671
4254
|
if (!traceSession) return;
|
|
3672
4255
|
try {
|
|
3673
4256
|
await traceSession.screenshotChain;
|
|
3674
|
-
const traceDir =
|
|
4257
|
+
const traceDir = join5(process.cwd(), ".trace");
|
|
3675
4258
|
mkdirSync2(traceDir, { recursive: true });
|
|
3676
4259
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
3677
|
-
const outPath =
|
|
4260
|
+
const outPath = join5(traceDir, `${timestamp}-${sessionId.slice(0, 8)}.zip`);
|
|
3678
4261
|
const zipBuffer = await buildTraceZip(traceSession);
|
|
3679
4262
|
writeFileSync2(outPath, zipBuffer);
|
|
3680
4263
|
console.error(`[TRACE] Saved to ${outPath}`);
|
|
@@ -3726,11 +4309,16 @@ function registerSession(sessionId, browser, metadata, historyEntry) {
|
|
|
3726
4309
|
if (oldMetadata?.provider) {
|
|
3727
4310
|
const oldHistory = state2.sessionHistory.get(oldSessionId);
|
|
3728
4311
|
const provider = getProvider(oldMetadata.provider, oldMetadata.type);
|
|
3729
|
-
await provider.onSessionClose?.(oldSessionId, oldMetadata.type, getSessionResult(oldHistory), oldMetadata.tunnelHandle).catch(() => {
|
|
4312
|
+
await provider.onSessionClose?.(oldSessionId, oldMetadata.type, getSessionResult(oldHistory), oldMetadata.tunnelHandle, oldBrowser, oldMetadata.region).catch(() => {
|
|
3730
4313
|
});
|
|
3731
4314
|
}
|
|
3732
4315
|
await oldBrowser.deleteSession().catch(() => {
|
|
3733
4316
|
});
|
|
4317
|
+
if (oldMetadata?.provider && oldMetadata?.tunnelHandle) {
|
|
4318
|
+
const provider = getProvider(oldMetadata.provider, oldMetadata.type);
|
|
4319
|
+
await provider.stopTunnel?.(oldMetadata.tunnelHandle).catch(() => {
|
|
4320
|
+
});
|
|
4321
|
+
}
|
|
3734
4322
|
})();
|
|
3735
4323
|
state2.browsers.delete(oldSessionId);
|
|
3736
4324
|
state2.sessionMetadata.delete(oldSessionId);
|
|
@@ -3753,12 +4341,20 @@ async function closeSession(sessionId, detach, isAttached, force) {
|
|
|
3753
4341
|
if (metadata?.provider) {
|
|
3754
4342
|
try {
|
|
3755
4343
|
const provider = getProvider(metadata.provider, metadata.type);
|
|
3756
|
-
await provider.onSessionClose?.(sessionId, metadata.type, getSessionResult(history), metadata.tunnelHandle);
|
|
4344
|
+
await provider.onSessionClose?.(sessionId, metadata.type, getSessionResult(history), metadata.tunnelHandle, browser, metadata.region);
|
|
3757
4345
|
} catch (e) {
|
|
3758
4346
|
console.error("[WARN] Failed to run provider onSessionClose:", e);
|
|
3759
4347
|
}
|
|
3760
4348
|
}
|
|
3761
4349
|
await browser.deleteSession();
|
|
4350
|
+
if (metadata?.provider && metadata?.tunnelHandle) {
|
|
4351
|
+
try {
|
|
4352
|
+
const provider = getProvider(metadata.provider, metadata.type);
|
|
4353
|
+
await provider.stopTunnel?.(metadata.tunnelHandle);
|
|
4354
|
+
} catch (e) {
|
|
4355
|
+
console.error("[WARN] Failed to stop tunnel:", e);
|
|
4356
|
+
}
|
|
4357
|
+
}
|
|
3762
4358
|
}
|
|
3763
4359
|
state2.browsers.delete(sessionId);
|
|
3764
4360
|
state2.sessionMetadata.delete(sessionId);
|
|
@@ -3776,23 +4372,23 @@ var startSessionToolDefinition = {
|
|
|
3776
4372
|
description: 'Starts a browser or mobile automation session. Only one active session at a time \u2014 starting a new one closes the existing session first. Use platform "browser" with a browser name, or "ios"/"android" with deviceName. Set attach: true to connect to a running Chrome via CDP instead of launching a new browser.',
|
|
3777
4373
|
annotations: { title: "Start Session", destructiveHint: false },
|
|
3778
4374
|
inputSchema: {
|
|
3779
|
-
provider: z14.enum(["local", "browserstack"]).optional().default("local").describe("Session provider (default: local)"),
|
|
4375
|
+
provider: z14.enum(["local", "browserstack", "saucelabs", "testmu"]).optional().default("local").describe("Session provider (default: local)"),
|
|
3780
4376
|
platform: platformEnum.describe("Session platform type"),
|
|
3781
4377
|
browser: browserEnum.optional().describe("Browser to launch (required for browser platform)"),
|
|
3782
|
-
browserVersion: z14.string().optional().describe("Browser version (
|
|
3783
|
-
os: z14.string().optional().describe('Operating system
|
|
3784
|
-
osVersion: z14.string().optional().describe('OS version
|
|
3785
|
-
app: z14.string().optional().describe("
|
|
4378
|
+
browserVersion: z14.string().optional().describe("Browser version (cloud providers only, default: latest)"),
|
|
4379
|
+
os: z14.string().optional().describe('Operating system for cloud provider browser sessions (e.g. "Windows", "Mac", "macOS", "Linux"). BrowserStack: sets bstack:options.os separately. TestMu/Sauce Labs: combined with osVersion into W3C platformName. Browser platform only.'),
|
|
4380
|
+
osVersion: z14.string().optional().describe('OS version for cloud provider browser sessions (e.g. "11", "15", "Monterey"). BrowserStack: sets bstack:options.osVersion separately. TestMu/Sauce Labs: combined with os into W3C platformName. Browser platform only.'),
|
|
4381
|
+
app: z14.string().optional().describe("App URL (bs://... for BrowserStack, storage:filename= for Sauce Labs, lt://... for TestMu mobile sessions)"),
|
|
3786
4382
|
reporting: z14.object({
|
|
3787
4383
|
project: z14.string().optional(),
|
|
3788
4384
|
build: z14.string().optional(),
|
|
3789
4385
|
session: z14.string().optional()
|
|
3790
|
-
}).optional().describe("
|
|
4386
|
+
}).optional().describe("Cloud provider reporting labels (project, build, session)"),
|
|
3791
4387
|
headless: coerceBoolean.optional().default(true).describe("Run browser in headless mode (default: true)"),
|
|
3792
4388
|
windowWidth: z14.number().min(400).max(3840).optional().default(1920).describe("Browser window width"),
|
|
3793
4389
|
windowHeight: z14.number().min(400).max(2160).optional().default(1080).describe("Browser window height"),
|
|
3794
4390
|
deviceName: z14.string().optional().describe("Mobile device/emulator/simulator name (required for ios/android)"),
|
|
3795
|
-
platformVersion: z14.string().optional().describe('OS version (e.g., "17.0", "14")'),
|
|
4391
|
+
platformVersion: z14.string().optional().describe('OS version for mobile sessions (e.g., "17.0", "14"). Mobile (ios/android) only.'),
|
|
3796
4392
|
appPath: z14.string().optional().describe("Path to app file (.app/.apk/.ipa)"),
|
|
3797
4393
|
automationName: automationEnum.optional().describe("Automation driver"),
|
|
3798
4394
|
autoGrantPermissions: coerceBoolean.optional().describe("Auto-grant app permissions (default: true)"),
|
|
@@ -3815,7 +4411,12 @@ var startSessionToolDefinition = {
|
|
|
3815
4411
|
path: z14.string().optional(),
|
|
3816
4412
|
protocol: z14.string().optional()
|
|
3817
4413
|
}).optional().describe("Appium server connection (local provider only)"),
|
|
3818
|
-
|
|
4414
|
+
region: z14.enum(["us-west-1", "eu-central-1", "apac-southeast-1"]).optional().default("eu-central-1").describe('Sauce Labs region (default: eu-central-1). Only used with provider: "saucelabs".'),
|
|
4415
|
+
tunnel: z14.union([z14.literal("external"), coerceBoolean]).optional().default(false).describe('Enable local tunnel routing (cloud providers only, default: false). true = auto-start tunnel before session and stop on close. "external" = tunnel already running externally.'),
|
|
4416
|
+
tunnelName: z14.string().optional().describe('Tunnel identifier name. With tunnel: "external" this must match the running tunnel. With tunnel: true a unique name is auto-generated if not provided.'),
|
|
4417
|
+
browserstackLocal: z14.union([z14.literal("external"), coerceBoolean]).optional().describe('Deprecated: use "tunnel" instead. Enable BrowserStack Local tunnel routing.'),
|
|
4418
|
+
saucelabsLocal: z14.union([z14.literal("external"), coerceBoolean]).optional().describe('Deprecated: use "tunnel" instead. Enable Sauce Connect tunnel routing.'),
|
|
4419
|
+
testmuLocal: z14.union([z14.literal("external"), coerceBoolean]).optional().describe('Deprecated: use "tunnel" instead. Enable TestMu Tunnel routing.'),
|
|
3819
4420
|
navigationUrl: z14.string().optional().describe("URL to navigate to after starting"),
|
|
3820
4421
|
capabilities: z14.record(z14.string(), z14.unknown()).optional().describe("Additional capabilities to merge")
|
|
3821
4422
|
}
|
|
@@ -3894,15 +4495,19 @@ async function startBrowserSession(args) {
|
|
|
3894
4495
|
const effectiveHeadless = headless && headlessSupported;
|
|
3895
4496
|
const provider = getProvider(args.provider ?? "local", "browser");
|
|
3896
4497
|
const connectionConfig = provider.getConnectionConfig(args);
|
|
4498
|
+
const effectiveTunnel = args.tunnel ?? args.browserstackLocal ?? args.saucelabsLocal ?? args.testmuLocal ?? false;
|
|
4499
|
+
const tunnelEnabled = effectiveTunnel === true;
|
|
4500
|
+
const tunnelName = tunnelEnabled && !args.tunnelName ? `wdio-mcp-${Date.now()}` : args.tunnelName;
|
|
3897
4501
|
const mergedCapabilities = provider.buildCapabilities({
|
|
3898
4502
|
...args,
|
|
3899
4503
|
browser,
|
|
3900
4504
|
headless,
|
|
3901
4505
|
windowWidth,
|
|
3902
4506
|
windowHeight,
|
|
3903
|
-
capabilities: userCapabilities
|
|
4507
|
+
capabilities: userCapabilities,
|
|
4508
|
+
tunnelName
|
|
3904
4509
|
});
|
|
3905
|
-
const tunnelHandle =
|
|
4510
|
+
const tunnelHandle = tunnelEnabled ? await provider.startTunnel?.({ ...args, tunnelName }) : void 0;
|
|
3906
4511
|
const wdioBrowser = await remote({ ...connectionConfig, capabilities: mergedCapabilities });
|
|
3907
4512
|
const { sessionId } = wdioBrowser;
|
|
3908
4513
|
const sessionMetadata = {
|
|
@@ -3910,6 +4515,8 @@ async function startBrowserSession(args) {
|
|
|
3910
4515
|
capabilities: mergedCapabilities,
|
|
3911
4516
|
isAttached: false,
|
|
3912
4517
|
provider: args.provider ?? "local",
|
|
4518
|
+
region: args.region,
|
|
4519
|
+
tunnelName,
|
|
3913
4520
|
tunnelHandle,
|
|
3914
4521
|
trace: args.trace ?? false
|
|
3915
4522
|
};
|
|
@@ -3948,7 +4555,8 @@ Note: Unable to set window size (${windowWidth}x${windowHeight}). ${e}`;
|
|
|
3948
4555
|
}
|
|
3949
4556
|
async function startMobileSession(args) {
|
|
3950
4557
|
const { platform: platform2, appPath, app, deviceName, noReset } = args;
|
|
3951
|
-
|
|
4558
|
+
const isMobileBrowser = args.browser !== void 0;
|
|
4559
|
+
if (!isMobileBrowser && !appPath && !app && noReset !== true) {
|
|
3952
4560
|
return {
|
|
3953
4561
|
content: [{
|
|
3954
4562
|
type: "text",
|
|
@@ -3958,8 +4566,11 @@ async function startMobileSession(args) {
|
|
|
3958
4566
|
}
|
|
3959
4567
|
const provider = getProvider(args.provider ?? "local", args.platform);
|
|
3960
4568
|
const serverConfig = provider.getConnectionConfig(args);
|
|
3961
|
-
const
|
|
3962
|
-
const
|
|
4569
|
+
const effectiveTunnel = args.tunnel ?? args.browserstackLocal ?? args.saucelabsLocal ?? args.testmuLocal ?? false;
|
|
4570
|
+
const tunnelEnabled = effectiveTunnel === true;
|
|
4571
|
+
const tunnelName = tunnelEnabled && !args.tunnelName ? `wdio-mcp-${Date.now()}` : args.tunnelName;
|
|
4572
|
+
const mergedCapabilities = provider.buildCapabilities({ ...args, tunnelName });
|
|
4573
|
+
const tunnelHandle = tunnelEnabled ? await provider.startTunnel?.({ ...args, tunnelName }) : void 0;
|
|
3963
4574
|
const browser = await remote({ ...serverConfig, capabilities: mergedCapabilities });
|
|
3964
4575
|
const { sessionId } = browser;
|
|
3965
4576
|
const shouldAutoDetach = provider.shouldAutoDetach(args);
|
|
@@ -3969,6 +4580,8 @@ async function startMobileSession(args) {
|
|
|
3969
4580
|
capabilities: mergedCapabilities,
|
|
3970
4581
|
isAttached: shouldAutoDetach,
|
|
3971
4582
|
provider: args.provider ?? "local",
|
|
4583
|
+
region: args.region,
|
|
4584
|
+
tunnelName,
|
|
3972
4585
|
tunnelHandle,
|
|
3973
4586
|
trace: args.trace ?? false
|
|
3974
4587
|
};
|
|
@@ -3983,14 +4596,16 @@ async function startMobileSession(args) {
|
|
|
3983
4596
|
if (args.trace) {
|
|
3984
4597
|
startTrace(sessionId, mergedCapabilities, sessionType);
|
|
3985
4598
|
}
|
|
3986
|
-
const
|
|
4599
|
+
const sessionKind = isMobileBrowser ? "mobile browser" : "app";
|
|
4600
|
+
const appInfo = isMobileBrowser ? `
|
|
4601
|
+
Browser: ${args.browser}` : appPath ? `
|
|
3987
4602
|
App: ${appPath}` : "\nApp: (connected to running app)";
|
|
3988
4603
|
const detachNote = shouldAutoDetach ? "\n\n(Auto-detach enabled: session will be preserved on close. Use close_session({ detach: false }) to force terminate.)" : "";
|
|
3989
4604
|
return {
|
|
3990
4605
|
content: [
|
|
3991
4606
|
{
|
|
3992
4607
|
type: "text",
|
|
3993
|
-
text: `${platform2}
|
|
4608
|
+
text: `${platform2} ${sessionKind} session started with sessionId: ${sessionId}
|
|
3994
4609
|
Device: ${deviceName}${appInfo}
|
|
3995
4610
|
Appium Server: ${serverConfig.hostname}:${serverConfig.port}${serverConfig.path}${detachNote}`
|
|
3996
4611
|
}
|
|
@@ -4152,51 +4767,163 @@ var switchFrameTool = async ({
|
|
|
4152
4767
|
}
|
|
4153
4768
|
};
|
|
4154
4769
|
|
|
4155
|
-
// src/tools/
|
|
4156
|
-
import { existsSync as existsSync2,
|
|
4770
|
+
// src/tools/cloud-provider.tool.ts
|
|
4771
|
+
import { existsSync as existsSync2, readFileSync } from "fs";
|
|
4157
4772
|
import { z as z17 } from "zod";
|
|
4158
|
-
var BS_API = "https://api-cloud.browserstack.com";
|
|
4159
|
-
function getAuth() {
|
|
4160
|
-
const user = process.env.BROWSERSTACK_USERNAME;
|
|
4161
|
-
const key = process.env.BROWSERSTACK_ACCESS_KEY;
|
|
4162
|
-
if (!user || !key) return null;
|
|
4163
|
-
return Buffer.from(`${user}:${key}`).toString("base64");
|
|
4164
|
-
}
|
|
4165
4773
|
function formatAppList(apps) {
|
|
4166
4774
|
if (apps.length === 0) return "No apps found.";
|
|
4167
4775
|
return apps.map((a) => {
|
|
4168
|
-
const id = a.
|
|
4169
|
-
|
|
4776
|
+
const id = a.customId ? ` [${a.customId}]` : "";
|
|
4777
|
+
const ts = a.uploadedAt ?? "unknown";
|
|
4778
|
+
return `${a.name}${id} \u2014 ${a.ref} (${ts})`;
|
|
4170
4779
|
}).join("\n");
|
|
4171
4780
|
}
|
|
4781
|
+
var PROVIDER_CONFIGS = {
|
|
4782
|
+
browserstack: {
|
|
4783
|
+
name: "BrowserStack",
|
|
4784
|
+
apiBase: "https://api-cloud.browserstack.com",
|
|
4785
|
+
credsEnvNames: ["BROWSERSTACK_USERNAME", "BROWSERSTACK_ACCESS_KEY"],
|
|
4786
|
+
listPath: "/app-automate/recent_apps",
|
|
4787
|
+
supportsOrgWide: true,
|
|
4788
|
+
parseListResponse: (raw) => {
|
|
4789
|
+
const apps = Array.isArray(raw) ? raw : [];
|
|
4790
|
+
return apps.map((a) => ({
|
|
4791
|
+
name: `${a.app_name}`,
|
|
4792
|
+
ref: `${a.app_url}`,
|
|
4793
|
+
uploadedAt: `${a.uploaded_at}`,
|
|
4794
|
+
customId: a.custom_id
|
|
4795
|
+
}));
|
|
4796
|
+
},
|
|
4797
|
+
uploadPath: "/app-automate/upload",
|
|
4798
|
+
uploadField: "file",
|
|
4799
|
+
parseUploadResponse: (raw, fileName) => {
|
|
4800
|
+
const data = raw;
|
|
4801
|
+
return { appRef: data.app_url, appName: fileName };
|
|
4802
|
+
}
|
|
4803
|
+
},
|
|
4804
|
+
saucelabs: {
|
|
4805
|
+
name: "Sauce Labs",
|
|
4806
|
+
apiBase: "https://api.eu-central-1.saucelabs.com",
|
|
4807
|
+
// region overridden via param
|
|
4808
|
+
credsEnvNames: ["SAUCE_USERNAME", "SAUCE_ACCESS_KEY"],
|
|
4809
|
+
listPath: "/v1/storage/files",
|
|
4810
|
+
supportsOrgWide: false,
|
|
4811
|
+
parseListResponse: (raw) => {
|
|
4812
|
+
const data = raw;
|
|
4813
|
+
return (data.items ?? []).map((a) => ({
|
|
4814
|
+
name: a.name,
|
|
4815
|
+
ref: `storage:filename=${a.name}`,
|
|
4816
|
+
uploadedAt: a.uploadTimestamp ? new Date(a.uploadTimestamp).toISOString() : void 0,
|
|
4817
|
+
customId: a.customId
|
|
4818
|
+
}));
|
|
4819
|
+
},
|
|
4820
|
+
uploadPath: "/v1/storage/upload",
|
|
4821
|
+
uploadField: "payload",
|
|
4822
|
+
parseUploadResponse: (raw, fileName) => {
|
|
4823
|
+
const data = raw;
|
|
4824
|
+
const name = data.item?.name ?? fileName;
|
|
4825
|
+
return { appRef: `storage:filename=${name}`, appName: name };
|
|
4826
|
+
}
|
|
4827
|
+
},
|
|
4828
|
+
testmu: {
|
|
4829
|
+
name: "TestMu",
|
|
4830
|
+
apiBase: "https://manual-api.lambdatest.com",
|
|
4831
|
+
credsEnvNames: ["TESTMU_USERNAME", "TESTMU_ACCESS_KEY"],
|
|
4832
|
+
listPath: "/app/data",
|
|
4833
|
+
supportsOrgWide: false,
|
|
4834
|
+
parseListResponse: (raw) => {
|
|
4835
|
+
if (raw === null || raw === void 0 || typeof raw !== "object") return [];
|
|
4836
|
+
const data = raw;
|
|
4837
|
+
const apps = data.data ?? (Array.isArray(raw) ? raw : []);
|
|
4838
|
+
return apps.map((a) => ({
|
|
4839
|
+
name: a.name ?? a.app_name ?? "unknown",
|
|
4840
|
+
ref: a.app_id ? `lt://${a.app_id}` : `lt://${a.custom_id ?? "unknown"}`,
|
|
4841
|
+
uploadedAt: a.updated_at,
|
|
4842
|
+
customId: a.custom_id
|
|
4843
|
+
}));
|
|
4844
|
+
},
|
|
4845
|
+
uploadPath: "/app/upload/realDevice",
|
|
4846
|
+
uploadField: "appFile",
|
|
4847
|
+
parseUploadResponse: (raw, fileName) => {
|
|
4848
|
+
const data = raw;
|
|
4849
|
+
const ref = data.app_id ? `lt://${data.app_id}` : data.app_url ?? "unknown";
|
|
4850
|
+
return { appRef: ref, appName: data.name ?? fileName };
|
|
4851
|
+
}
|
|
4852
|
+
}
|
|
4853
|
+
};
|
|
4854
|
+
function getProviderConfig(provider, region) {
|
|
4855
|
+
const base = PROVIDER_CONFIGS[provider];
|
|
4856
|
+
if (!base) return { error: `Unknown provider: ${provider}` };
|
|
4857
|
+
const [userEnv, keyEnv] = base.credsEnvNames;
|
|
4858
|
+
const user = process.env[userEnv];
|
|
4859
|
+
const key = process.env[keyEnv];
|
|
4860
|
+
if (!user || !key) {
|
|
4861
|
+
const vars = base.credsEnvNames.join(" and ");
|
|
4862
|
+
return { error: `Missing credentials: set ${vars} environment variables.` };
|
|
4863
|
+
}
|
|
4864
|
+
const config = provider === "saucelabs" ? { ...base, apiBase: `https://api.${region ?? "eu-central-1"}.saucelabs.com` } : base;
|
|
4865
|
+
return { config, auth: basicAuth(user, key) };
|
|
4866
|
+
}
|
|
4172
4867
|
var listAppsToolDefinition = {
|
|
4173
4868
|
name: "list_apps",
|
|
4174
|
-
description: "List apps uploaded to BrowserStack App Automate. Reads
|
|
4175
|
-
annotations: { title: "List
|
|
4869
|
+
description: "List apps uploaded to a cloud provider (BrowserStack App Automate, Sauce Labs App Storage, or TestMu Real Device Cloud). Reads provider-specific credentials from environment.",
|
|
4870
|
+
annotations: { title: "List Cloud Provider Apps", readOnlyHint: true, idempotentHint: true },
|
|
4176
4871
|
inputSchema: {
|
|
4872
|
+
provider: z17.enum(["browserstack", "saucelabs", "testmu"]).describe("Cloud provider"),
|
|
4177
4873
|
sortBy: z17.enum(["app_name", "uploaded_at"]).optional().default("uploaded_at").describe("Sort order for results"),
|
|
4178
|
-
organizationWide: coerceBoolean.optional().default(false).describe("List apps uploaded by all users in the organization
|
|
4179
|
-
limit: z17.number().int().min(1).optional().default(20).describe("Maximum number of apps to return (only applies when organizationWide is true, default 20)")
|
|
4874
|
+
organizationWide: coerceBoolean.optional().default(false).describe("(BrowserStack only) List apps uploaded by all users in the organization. Defaults to false (own uploads only)."),
|
|
4875
|
+
limit: z17.number().int().min(1).optional().default(20).describe("Maximum number of apps to return (only applies when organizationWide is true, default 20)"),
|
|
4876
|
+
region: z17.enum(["us-west-1", "eu-central-1", "apac-southeast-1"]).optional().default("eu-central-1").describe("Sauce Labs region (default: eu-central-1)")
|
|
4180
4877
|
}
|
|
4181
4878
|
};
|
|
4182
|
-
var listAppsTool = async (
|
|
4183
|
-
const
|
|
4184
|
-
|
|
4185
|
-
|
|
4879
|
+
var listAppsTool = async (args) => {
|
|
4880
|
+
const { provider, sortBy = "uploaded_at", organizationWide = false, limit = 20, region = "eu-central-1" } = args;
|
|
4881
|
+
const resolved = getProviderConfig(provider, region);
|
|
4882
|
+
if ("error" in resolved) {
|
|
4883
|
+
return { isError: true, content: [{ type: "text", text: resolved.error }] };
|
|
4186
4884
|
}
|
|
4885
|
+
const { config, auth } = resolved;
|
|
4187
4886
|
try {
|
|
4188
|
-
let url = `${
|
|
4189
|
-
if (
|
|
4190
|
-
|
|
4191
|
-
|
|
4192
|
-
|
|
4193
|
-
if (
|
|
4194
|
-
const
|
|
4195
|
-
|
|
4887
|
+
let url = `${config.apiBase}${config.listPath}`;
|
|
4888
|
+
if (config.supportsOrgWide && organizationWide) {
|
|
4889
|
+
url = `${config.apiBase}/app-automate/recent_group_apps?limit=${limit}`;
|
|
4890
|
+
}
|
|
4891
|
+
let apps = [];
|
|
4892
|
+
if (provider === "testmu") {
|
|
4893
|
+
const errors = [];
|
|
4894
|
+
for (const platform2 of ["android", "ios"]) {
|
|
4895
|
+
try {
|
|
4896
|
+
const res = await fetch(`${url}?type=${platform2}`, {
|
|
4897
|
+
headers: { Authorization: `Basic ${auth}` }
|
|
4898
|
+
});
|
|
4899
|
+
if (res.ok) {
|
|
4900
|
+
const raw = await res.json();
|
|
4901
|
+
apps.push(...config.parseListResponse(raw));
|
|
4902
|
+
} else {
|
|
4903
|
+
const body = await res.text();
|
|
4904
|
+
errors.push(`${config.name} API error ${res.status} (${platform2}): ${body}`);
|
|
4905
|
+
}
|
|
4906
|
+
} catch (e) {
|
|
4907
|
+
errors.push(String(e));
|
|
4908
|
+
}
|
|
4909
|
+
}
|
|
4910
|
+
if (apps.length === 0 && errors.length > 0) {
|
|
4911
|
+
const message = errors.length === 1 ? errors[0] : `All platform fetches failed:
|
|
4912
|
+
${errors.map((e) => ` - ${e}`).join("\n")}`;
|
|
4913
|
+
return { isError: true, content: [{ type: "text", text: `Error listing apps: ${message}` }] };
|
|
4914
|
+
}
|
|
4915
|
+
} else {
|
|
4916
|
+
const res = await fetch(url, {
|
|
4917
|
+
headers: { Authorization: `Basic ${auth}` }
|
|
4918
|
+
});
|
|
4919
|
+
if (!res.ok) {
|
|
4920
|
+
const body = await res.text();
|
|
4921
|
+
return { isError: true, content: [{ type: "text", text: `${config.name} API error ${res.status}: ${body}` }] };
|
|
4922
|
+
}
|
|
4923
|
+
const raw = await res.json();
|
|
4924
|
+
apps = config.parseListResponse(raw);
|
|
4196
4925
|
}
|
|
4197
|
-
|
|
4198
|
-
let apps = Array.isArray(raw) ? raw : [];
|
|
4199
|
-
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());
|
|
4926
|
+
apps = sortBy === "app_name" ? apps.sort((a, b) => a.name.localeCompare(b.name)) : apps.sort((a, b) => (b.uploadedAt ?? "").localeCompare(a.uploadedAt ?? ""));
|
|
4200
4927
|
return { content: [{ type: "text", text: formatAppList(apps) }] };
|
|
4201
4928
|
} catch (e) {
|
|
4202
4929
|
return { isError: true, content: [{ type: "text", text: `Error listing apps: ${e}` }] };
|
|
@@ -4204,28 +4931,33 @@ var listAppsTool = async ({ sortBy = "uploaded_at", organizationWide = false, li
|
|
|
4204
4931
|
};
|
|
4205
4932
|
var uploadAppToolDefinition = {
|
|
4206
4933
|
name: "upload_app",
|
|
4207
|
-
description: "Upload a local .apk or .ipa to BrowserStack
|
|
4208
|
-
annotations: { title: "Upload App to
|
|
4934
|
+
description: "Upload a local .apk or .ipa to a cloud provider (BrowserStack, Sauce Labs, or TestMu). Returns the app URL for use in start_session.",
|
|
4935
|
+
annotations: { title: "Upload App to Cloud Provider", destructiveHint: false },
|
|
4209
4936
|
inputSchema: {
|
|
4937
|
+
provider: z17.enum(["browserstack", "saucelabs", "testmu"]).describe("Cloud provider"),
|
|
4210
4938
|
path: z17.string().describe("Absolute path to the .apk or .ipa file"),
|
|
4211
|
-
customId: z17.string().optional().describe("Optional custom ID for the app (used to reference it later)")
|
|
4939
|
+
customId: z17.string().optional().describe("Optional custom ID for the app (used to reference it later)"),
|
|
4940
|
+
region: z17.enum(["us-west-1", "eu-central-1", "apac-southeast-1"]).optional().default("eu-central-1").describe("Sauce Labs region (default: eu-central-1)")
|
|
4212
4941
|
}
|
|
4213
4942
|
};
|
|
4214
|
-
var uploadAppTool = async (
|
|
4215
|
-
const
|
|
4216
|
-
|
|
4217
|
-
|
|
4943
|
+
var uploadAppTool = async (args) => {
|
|
4944
|
+
const { provider, path, customId, region = "eu-central-1" } = args;
|
|
4945
|
+
const resolved = getProviderConfig(provider, region);
|
|
4946
|
+
if ("error" in resolved) {
|
|
4947
|
+
return { isError: true, content: [{ type: "text", text: resolved.error }] };
|
|
4218
4948
|
}
|
|
4949
|
+
const { config, auth } = resolved;
|
|
4219
4950
|
if (!existsSync2(path)) {
|
|
4220
4951
|
return { isError: true, content: [{ type: "text", text: `File not found: ${path}` }] };
|
|
4221
4952
|
}
|
|
4953
|
+
const fileName = path.split("/").pop() ?? "app";
|
|
4222
4954
|
try {
|
|
4223
4955
|
const form = new FormData();
|
|
4224
|
-
const
|
|
4225
|
-
const
|
|
4226
|
-
form.append(
|
|
4956
|
+
const fileBuffer = readFileSync(path);
|
|
4957
|
+
const fileBlob = new Blob([fileBuffer], { type: "application/octet-stream" });
|
|
4958
|
+
form.append(config.uploadField, fileBlob, fileName);
|
|
4227
4959
|
if (customId) form.append("custom_id", customId);
|
|
4228
|
-
const res = await fetch(`${
|
|
4960
|
+
const res = await fetch(`${config.apiBase}${config.uploadPath}`, {
|
|
4229
4961
|
method: "POST",
|
|
4230
4962
|
headers: { Authorization: `Basic ${auth}` },
|
|
4231
4963
|
body: form
|
|
@@ -4234,13 +4966,14 @@ var uploadAppTool = async ({ path, customId }) => {
|
|
|
4234
4966
|
const body = await res.text();
|
|
4235
4967
|
return { isError: true, content: [{ type: "text", text: `Upload failed ${res.status}: ${body}` }] };
|
|
4236
4968
|
}
|
|
4237
|
-
const
|
|
4238
|
-
const
|
|
4239
|
-
|
|
4969
|
+
const raw = await res.json();
|
|
4970
|
+
const { appRef } = config.parseUploadResponse(raw, fileName);
|
|
4971
|
+
const customIdNote = customId ? `
|
|
4972
|
+
Custom ID: ${customId}` : "";
|
|
4240
4973
|
return { content: [{ type: "text", text: `Upload successful.
|
|
4241
|
-
App
|
|
4974
|
+
App: ${appRef}${customIdNote}
|
|
4242
4975
|
|
|
4243
|
-
Use
|
|
4976
|
+
Use "${appRef}" as the "app" parameter in start_session with provider: "${provider}".` }] };
|
|
4244
4977
|
} catch (e) {
|
|
4245
4978
|
return { isError: true, content: [{ type: "text", text: `Error uploading app: ${e}` }] };
|
|
4246
4979
|
}
|
|
@@ -4431,6 +5164,8 @@ function createServer() {
|
|
|
4431
5164
|
registerResource(sessionStepsResource);
|
|
4432
5165
|
registerResource(sessionCodeResource);
|
|
4433
5166
|
registerResource(browserstackLocalBinaryResource);
|
|
5167
|
+
registerResource(saucelabsLocalBinaryResource);
|
|
5168
|
+
registerResource(testmuLocalBinaryResource);
|
|
4434
5169
|
registerResource(capabilitiesResource);
|
|
4435
5170
|
registerResource(elementsResource);
|
|
4436
5171
|
registerResource(accessibilityResource);
|