@wdio/mcp 3.5.2 → 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 +386 -39
- package/lib/server.js.map +1 -1
- package/lib/trace.js +1 -0
- package/lib/trace.js.map +1 -1
- package/package.json +2 -1
package/lib/server.js
CHANGED
|
@@ -91,6 +91,7 @@ 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",
|
|
@@ -2493,6 +2494,69 @@ var saucelabsLocalBinaryResource = {
|
|
|
2493
2494
|
}
|
|
2494
2495
|
};
|
|
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
|
+
|
|
2496
2560
|
// src/resources/sessions.resource.ts
|
|
2497
2561
|
import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2498
2562
|
|
|
@@ -2520,6 +2584,7 @@ function generateStep(step, history) {
|
|
|
2520
2584
|
const platform2 = p.platform;
|
|
2521
2585
|
const isBrowserStack = "bstack:options" in history.capabilities;
|
|
2522
2586
|
const isSauceLabs = "sauce:options" in history.capabilities;
|
|
2587
|
+
const isLambdaTest = "lt:options" in history.capabilities;
|
|
2523
2588
|
const capJson = indentJson(history.capabilities).split("\n").map((line, i) => i > 0 ? ` ${line}` : line).join("\n");
|
|
2524
2589
|
if (isBrowserStack) {
|
|
2525
2590
|
const nav = platform2 === "browser" && p.navigationUrl ? `
|
|
@@ -2552,6 +2617,23 @@ await browser.url('${escapeStr(p.navigationUrl)}');` : "";
|
|
|
2552
2617
|
`});${nav}`
|
|
2553
2618
|
].join("\n");
|
|
2554
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
|
+
}
|
|
2555
2637
|
if (platform2 === "browser") {
|
|
2556
2638
|
const nav = p.navigationUrl ? `
|
|
2557
2639
|
await browser.url('${escapeStr(p.navigationUrl)}');` : "";
|
|
@@ -2615,9 +2697,11 @@ function bsStatusUpdateLines(sessionType) {
|
|
|
2615
2697
|
function generateCode(history) {
|
|
2616
2698
|
const bstackOptions = history.capabilities["bstack:options"];
|
|
2617
2699
|
const sauceOptions = history.capabilities["sauce:options"];
|
|
2700
|
+
const ltOptions = history.capabilities["lt:options"];
|
|
2618
2701
|
const isBrowserStack = bstackOptions !== void 0;
|
|
2619
2702
|
const isSauceLabs = sauceOptions !== void 0;
|
|
2620
|
-
const
|
|
2703
|
+
const isLambdaTest = ltOptions !== void 0;
|
|
2704
|
+
const usesLocalTunnel = isBrowserStack ? bstackOptions?.local === true : isSauceLabs ? sauceOptions?.tunnelName !== void 0 : ltOptions?.tunnel === true;
|
|
2621
2705
|
const steps = history.steps.map((step) => generateStep(step, history)).join("\n").split("\n").map((line) => ` ${line}`).join("\n");
|
|
2622
2706
|
if (isBrowserStack) {
|
|
2623
2707
|
const bsSteps = steps.replace(/const browser = await remote\(/g, "browser = await remote(");
|
|
@@ -2722,6 +2806,61 @@ ${statusUpdate}
|
|
|
2722
2806
|
"}"
|
|
2723
2807
|
].join("\n");
|
|
2724
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
|
+
}
|
|
2725
2864
|
return `import { remote } from 'webdriverio';
|
|
2726
2865
|
|
|
2727
2866
|
try {
|
|
@@ -3419,7 +3558,7 @@ import { z as z14 } from "zod";
|
|
|
3419
3558
|
// src/session/lifecycle.ts
|
|
3420
3559
|
init_state();
|
|
3421
3560
|
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
3422
|
-
import { join as
|
|
3561
|
+
import { join as join5 } from "path";
|
|
3423
3562
|
|
|
3424
3563
|
// src/providers/local-browser.provider.ts
|
|
3425
3564
|
var LocalBrowserProvider = class {
|
|
@@ -3711,9 +3850,9 @@ var BrowserStackProvider = class {
|
|
|
3711
3850
|
if (reporting2?.build) bstackOptions2.buildName = reporting2.build;
|
|
3712
3851
|
if (reporting2?.session) bstackOptions2.sessionName = reporting2.session;
|
|
3713
3852
|
return {
|
|
3853
|
+
...userCapabilities,
|
|
3714
3854
|
browserName: options.browser ?? "chrome",
|
|
3715
|
-
"bstack:options": bstackOptions2
|
|
3716
|
-
...userCapabilities
|
|
3855
|
+
"bstack:options": bstackOptions2
|
|
3717
3856
|
};
|
|
3718
3857
|
}
|
|
3719
3858
|
const bstackOptions = {
|
|
@@ -3731,14 +3870,14 @@ var BrowserStackProvider = class {
|
|
|
3731
3870
|
const autoAcceptAlerts = options.autoAcceptAlerts;
|
|
3732
3871
|
const autoDismissAlerts = options.autoDismissAlerts;
|
|
3733
3872
|
return {
|
|
3873
|
+
...userCapabilities,
|
|
3734
3874
|
platformName: platform2,
|
|
3735
3875
|
"appium:app": options.app,
|
|
3736
3876
|
"appium:autoGrantPermissions": options.autoGrantPermissions ?? true,
|
|
3737
3877
|
"appium:autoAcceptAlerts": autoDismissAlerts ? void 0 : autoAcceptAlerts ?? true,
|
|
3738
3878
|
"appium:autoDismissAlerts": autoDismissAlerts,
|
|
3739
3879
|
"appium:newCommandTimeout": options.newCommandTimeout ?? 300,
|
|
3740
|
-
"bstack:options": bstackOptions
|
|
3741
|
-
...userCapabilities
|
|
3880
|
+
"bstack:options": bstackOptions
|
|
3742
3881
|
};
|
|
3743
3882
|
}
|
|
3744
3883
|
getSessionType(options) {
|
|
@@ -3788,6 +3927,7 @@ var BrowserStackProvider = class {
|
|
|
3788
3927
|
var browserStackProvider = new BrowserStackProvider();
|
|
3789
3928
|
|
|
3790
3929
|
// src/providers/cloud/saucelabs.provider.ts
|
|
3930
|
+
import SauceLabs from "saucelabs";
|
|
3791
3931
|
import { tmpdir as tmpdir3 } from "os";
|
|
3792
3932
|
import { join as join3 } from "path";
|
|
3793
3933
|
var SauceLabsProvider = class {
|
|
@@ -3821,16 +3961,15 @@ var SauceLabsProvider = class {
|
|
|
3821
3961
|
}
|
|
3822
3962
|
if (platform2 === "browser") {
|
|
3823
3963
|
return {
|
|
3964
|
+
...userCapabilities,
|
|
3824
3965
|
browserName: options.browser ?? "chrome",
|
|
3825
3966
|
browserVersion: options.browserVersion ?? "latest",
|
|
3826
|
-
platformName: options.os
|
|
3827
|
-
"sauce:options": sauceOptions
|
|
3828
|
-
...userCapabilities
|
|
3967
|
+
platformName: options.os ? [options.os, options.osVersion].filter(Boolean).join(" ") : "Linux",
|
|
3968
|
+
"sauce:options": sauceOptions
|
|
3829
3969
|
};
|
|
3830
3970
|
}
|
|
3831
3971
|
const mobileBrowser = options.browser;
|
|
3832
3972
|
if (mobileBrowser) {
|
|
3833
|
-
sauceOptions.appiumVersion = "2.11.0";
|
|
3834
3973
|
if (options.deviceOrientation) sauceOptions.deviceOrientation = options.deviceOrientation;
|
|
3835
3974
|
const caps = {
|
|
3836
3975
|
platformName: platform2,
|
|
@@ -3841,12 +3980,13 @@ var SauceLabsProvider = class {
|
|
|
3841
3980
|
"appium:newCommandTimeout": options.newCommandTimeout ?? 300,
|
|
3842
3981
|
"sauce:options": sauceOptions
|
|
3843
3982
|
};
|
|
3844
|
-
return { ...
|
|
3983
|
+
return { ...userCapabilities, ...caps };
|
|
3845
3984
|
}
|
|
3846
3985
|
sauceOptions.appiumVersion = "latest";
|
|
3847
3986
|
const autoAcceptAlerts = options.autoAcceptAlerts;
|
|
3848
3987
|
const autoDismissAlerts = options.autoDismissAlerts;
|
|
3849
3988
|
return {
|
|
3989
|
+
...userCapabilities,
|
|
3850
3990
|
platformName: platform2,
|
|
3851
3991
|
"appium:app": options.app,
|
|
3852
3992
|
"appium:deviceName": options.deviceName,
|
|
@@ -3856,8 +3996,7 @@ var SauceLabsProvider = class {
|
|
|
3856
3996
|
"appium:autoAcceptAlerts": autoDismissAlerts ? void 0 : autoAcceptAlerts ?? true,
|
|
3857
3997
|
"appium:autoDismissAlerts": autoDismissAlerts,
|
|
3858
3998
|
"appium:newCommandTimeout": options.newCommandTimeout ?? 300,
|
|
3859
|
-
"sauce:options": sauceOptions
|
|
3860
|
-
...userCapabilities
|
|
3999
|
+
"sauce:options": sauceOptions
|
|
3861
4000
|
};
|
|
3862
4001
|
}
|
|
3863
4002
|
getSessionType(options) {
|
|
@@ -3874,7 +4013,6 @@ var SauceLabsProvider = class {
|
|
|
3874
4013
|
const logFile = join3(tmpdir3(), "sauce-connect.log");
|
|
3875
4014
|
console.error(`[SauceLabs] Starting tunnel "${tunnelName}" (region: ${region})`);
|
|
3876
4015
|
try {
|
|
3877
|
-
const { default: SauceLabs } = await import("saucelabs");
|
|
3878
4016
|
const api = new SauceLabs({ user: process.env.SAUCE_USERNAME ?? "", key: process.env.SAUCE_ACCESS_KEY ?? "", region });
|
|
3879
4017
|
return api.startSauceConnect({
|
|
3880
4018
|
tunnelName,
|
|
@@ -3923,10 +4061,163 @@ var SauceLabsProvider = class {
|
|
|
3923
4061
|
};
|
|
3924
4062
|
var sauceLabsProvider = new SauceLabsProvider();
|
|
3925
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
|
+
|
|
3926
4216
|
// src/providers/registry.ts
|
|
3927
4217
|
var providers = /* @__PURE__ */ new Map([
|
|
3928
4218
|
["browserstack", browserStackProvider],
|
|
3929
|
-
["saucelabs", sauceLabsProvider]
|
|
4219
|
+
["saucelabs", sauceLabsProvider],
|
|
4220
|
+
["testmu", testMuProvider]
|
|
3930
4221
|
]);
|
|
3931
4222
|
function getDefaultProvider(platform2) {
|
|
3932
4223
|
return platform2 === "browser" ? localBrowserProvider : localAppiumProvider;
|
|
@@ -3963,10 +4254,10 @@ async function finalizeTrace(sessionId, browser) {
|
|
|
3963
4254
|
if (!traceSession) return;
|
|
3964
4255
|
try {
|
|
3965
4256
|
await traceSession.screenshotChain;
|
|
3966
|
-
const traceDir =
|
|
4257
|
+
const traceDir = join5(process.cwd(), ".trace");
|
|
3967
4258
|
mkdirSync2(traceDir, { recursive: true });
|
|
3968
4259
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
3969
|
-
const outPath =
|
|
4260
|
+
const outPath = join5(traceDir, `${timestamp}-${sessionId.slice(0, 8)}.zip`);
|
|
3970
4261
|
const zipBuffer = await buildTraceZip(traceSession);
|
|
3971
4262
|
writeFileSync2(outPath, zipBuffer);
|
|
3972
4263
|
console.error(`[TRACE] Saved to ${outPath}`);
|
|
@@ -4081,13 +4372,13 @@ var startSessionToolDefinition = {
|
|
|
4081
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.',
|
|
4082
4373
|
annotations: { title: "Start Session", destructiveHint: false },
|
|
4083
4374
|
inputSchema: {
|
|
4084
|
-
provider: z14.enum(["local", "browserstack", "saucelabs"]).optional().default("local").describe("Session provider (default: local)"),
|
|
4375
|
+
provider: z14.enum(["local", "browserstack", "saucelabs", "testmu"]).optional().default("local").describe("Session provider (default: local)"),
|
|
4085
4376
|
platform: platformEnum.describe("Session platform type"),
|
|
4086
4377
|
browser: browserEnum.optional().describe("Browser to launch (required for browser platform)"),
|
|
4087
4378
|
browserVersion: z14.string().optional().describe("Browser version (cloud providers only, default: latest)"),
|
|
4088
|
-
os: z14.string().optional().describe('Operating system
|
|
4089
|
-
osVersion: z14.string().optional().describe('OS version
|
|
4090
|
-
app: z14.string().optional().describe("App URL (bs://...
|
|
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)"),
|
|
4091
4382
|
reporting: z14.object({
|
|
4092
4383
|
project: z14.string().optional(),
|
|
4093
4384
|
build: z14.string().optional(),
|
|
@@ -4097,7 +4388,7 @@ var startSessionToolDefinition = {
|
|
|
4097
4388
|
windowWidth: z14.number().min(400).max(3840).optional().default(1920).describe("Browser window width"),
|
|
4098
4389
|
windowHeight: z14.number().min(400).max(2160).optional().default(1080).describe("Browser window height"),
|
|
4099
4390
|
deviceName: z14.string().optional().describe("Mobile device/emulator/simulator name (required for ios/android)"),
|
|
4100
|
-
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.'),
|
|
4101
4392
|
appPath: z14.string().optional().describe("Path to app file (.app/.apk/.ipa)"),
|
|
4102
4393
|
automationName: automationEnum.optional().describe("Automation driver"),
|
|
4103
4394
|
autoGrantPermissions: coerceBoolean.optional().describe("Auto-grant app permissions (default: true)"),
|
|
@@ -4125,6 +4416,7 @@ var startSessionToolDefinition = {
|
|
|
4125
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.'),
|
|
4126
4417
|
browserstackLocal: z14.union([z14.literal("external"), coerceBoolean]).optional().describe('Deprecated: use "tunnel" instead. Enable BrowserStack Local tunnel routing.'),
|
|
4127
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.'),
|
|
4128
4420
|
navigationUrl: z14.string().optional().describe("URL to navigate to after starting"),
|
|
4129
4421
|
capabilities: z14.record(z14.string(), z14.unknown()).optional().describe("Additional capabilities to merge")
|
|
4130
4422
|
}
|
|
@@ -4203,7 +4495,7 @@ async function startBrowserSession(args) {
|
|
|
4203
4495
|
const effectiveHeadless = headless && headlessSupported;
|
|
4204
4496
|
const provider = getProvider(args.provider ?? "local", "browser");
|
|
4205
4497
|
const connectionConfig = provider.getConnectionConfig(args);
|
|
4206
|
-
const effectiveTunnel = args.tunnel ?? args.browserstackLocal ?? args.saucelabsLocal ?? false;
|
|
4498
|
+
const effectiveTunnel = args.tunnel ?? args.browserstackLocal ?? args.saucelabsLocal ?? args.testmuLocal ?? false;
|
|
4207
4499
|
const tunnelEnabled = effectiveTunnel === true;
|
|
4208
4500
|
const tunnelName = tunnelEnabled && !args.tunnelName ? `wdio-mcp-${Date.now()}` : args.tunnelName;
|
|
4209
4501
|
const mergedCapabilities = provider.buildCapabilities({
|
|
@@ -4263,7 +4555,8 @@ Note: Unable to set window size (${windowWidth}x${windowHeight}). ${e}`;
|
|
|
4263
4555
|
}
|
|
4264
4556
|
async function startMobileSession(args) {
|
|
4265
4557
|
const { platform: platform2, appPath, app, deviceName, noReset } = args;
|
|
4266
|
-
|
|
4558
|
+
const isMobileBrowser = args.browser !== void 0;
|
|
4559
|
+
if (!isMobileBrowser && !appPath && !app && noReset !== true) {
|
|
4267
4560
|
return {
|
|
4268
4561
|
content: [{
|
|
4269
4562
|
type: "text",
|
|
@@ -4273,7 +4566,7 @@ async function startMobileSession(args) {
|
|
|
4273
4566
|
}
|
|
4274
4567
|
const provider = getProvider(args.provider ?? "local", args.platform);
|
|
4275
4568
|
const serverConfig = provider.getConnectionConfig(args);
|
|
4276
|
-
const effectiveTunnel = args.tunnel ?? args.browserstackLocal ?? args.saucelabsLocal ?? false;
|
|
4569
|
+
const effectiveTunnel = args.tunnel ?? args.browserstackLocal ?? args.saucelabsLocal ?? args.testmuLocal ?? false;
|
|
4277
4570
|
const tunnelEnabled = effectiveTunnel === true;
|
|
4278
4571
|
const tunnelName = tunnelEnabled && !args.tunnelName ? `wdio-mcp-${Date.now()}` : args.tunnelName;
|
|
4279
4572
|
const mergedCapabilities = provider.buildCapabilities({ ...args, tunnelName });
|
|
@@ -4303,14 +4596,16 @@ async function startMobileSession(args) {
|
|
|
4303
4596
|
if (args.trace) {
|
|
4304
4597
|
startTrace(sessionId, mergedCapabilities, sessionType);
|
|
4305
4598
|
}
|
|
4306
|
-
const
|
|
4599
|
+
const sessionKind = isMobileBrowser ? "mobile browser" : "app";
|
|
4600
|
+
const appInfo = isMobileBrowser ? `
|
|
4601
|
+
Browser: ${args.browser}` : appPath ? `
|
|
4307
4602
|
App: ${appPath}` : "\nApp: (connected to running app)";
|
|
4308
4603
|
const detachNote = shouldAutoDetach ? "\n\n(Auto-detach enabled: session will be preserved on close. Use close_session({ detach: false }) to force terminate.)" : "";
|
|
4309
4604
|
return {
|
|
4310
4605
|
content: [
|
|
4311
4606
|
{
|
|
4312
4607
|
type: "text",
|
|
4313
|
-
text: `${platform2}
|
|
4608
|
+
text: `${platform2} ${sessionKind} session started with sessionId: ${sessionId}
|
|
4314
4609
|
Device: ${deviceName}${appInfo}
|
|
4315
4610
|
Appium Server: ${serverConfig.hostname}:${serverConfig.port}${serverConfig.path}${detachNote}`
|
|
4316
4611
|
}
|
|
@@ -4529,6 +4824,31 @@ var PROVIDER_CONFIGS = {
|
|
|
4529
4824
|
const name = data.item?.name ?? fileName;
|
|
4530
4825
|
return { appRef: `storage:filename=${name}`, appName: name };
|
|
4531
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
|
+
}
|
|
4532
4852
|
}
|
|
4533
4853
|
};
|
|
4534
4854
|
function getProviderConfig(provider, region) {
|
|
@@ -4546,10 +4866,10 @@ function getProviderConfig(provider, region) {
|
|
|
4546
4866
|
}
|
|
4547
4867
|
var listAppsToolDefinition = {
|
|
4548
4868
|
name: "list_apps",
|
|
4549
|
-
description: "List apps uploaded to a cloud provider (BrowserStack App Automate
|
|
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.",
|
|
4550
4870
|
annotations: { title: "List Cloud Provider Apps", readOnlyHint: true, idempotentHint: true },
|
|
4551
4871
|
inputSchema: {
|
|
4552
|
-
provider: z17.enum(["browserstack", "saucelabs"]).describe("Cloud provider"),
|
|
4872
|
+
provider: z17.enum(["browserstack", "saucelabs", "testmu"]).describe("Cloud provider"),
|
|
4553
4873
|
sortBy: z17.enum(["app_name", "uploaded_at"]).optional().default("uploaded_at").describe("Sort order for results"),
|
|
4554
4874
|
organizationWide: coerceBoolean.optional().default(false).describe("(BrowserStack only) List apps uploaded by all users in the organization. Defaults to false (own uploads only)."),
|
|
4555
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)"),
|
|
@@ -4568,15 +4888,41 @@ var listAppsTool = async (args) => {
|
|
|
4568
4888
|
if (config.supportsOrgWide && organizationWide) {
|
|
4569
4889
|
url = `${config.apiBase}/app-automate/recent_group_apps?limit=${limit}`;
|
|
4570
4890
|
}
|
|
4571
|
-
|
|
4572
|
-
|
|
4573
|
-
|
|
4574
|
-
|
|
4575
|
-
|
|
4576
|
-
|
|
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);
|
|
4577
4925
|
}
|
|
4578
|
-
const raw = await res.json();
|
|
4579
|
-
let apps = config.parseListResponse(raw);
|
|
4580
4926
|
apps = sortBy === "app_name" ? apps.sort((a, b) => a.name.localeCompare(b.name)) : apps.sort((a, b) => (b.uploadedAt ?? "").localeCompare(a.uploadedAt ?? ""));
|
|
4581
4927
|
return { content: [{ type: "text", text: formatAppList(apps) }] };
|
|
4582
4928
|
} catch (e) {
|
|
@@ -4585,10 +4931,10 @@ var listAppsTool = async (args) => {
|
|
|
4585
4931
|
};
|
|
4586
4932
|
var uploadAppToolDefinition = {
|
|
4587
4933
|
name: "upload_app",
|
|
4588
|
-
description: "Upload a local .apk or .ipa to a cloud provider (BrowserStack
|
|
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.",
|
|
4589
4935
|
annotations: { title: "Upload App to Cloud Provider", destructiveHint: false },
|
|
4590
4936
|
inputSchema: {
|
|
4591
|
-
provider: z17.enum(["browserstack", "saucelabs"]).describe("Cloud provider"),
|
|
4937
|
+
provider: z17.enum(["browserstack", "saucelabs", "testmu"]).describe("Cloud provider"),
|
|
4592
4938
|
path: z17.string().describe("Absolute path to the .apk or .ipa file"),
|
|
4593
4939
|
customId: z17.string().optional().describe("Optional custom ID for the app (used to reference it later)"),
|
|
4594
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)")
|
|
@@ -4819,6 +5165,7 @@ function createServer() {
|
|
|
4819
5165
|
registerResource(sessionCodeResource);
|
|
4820
5166
|
registerResource(browserstackLocalBinaryResource);
|
|
4821
5167
|
registerResource(saucelabsLocalBinaryResource);
|
|
5168
|
+
registerResource(testmuLocalBinaryResource);
|
|
4822
5169
|
registerResource(capabilitiesResource);
|
|
4823
5170
|
registerResource(elementsResource);
|
|
4824
5171
|
registerResource(accessibilityResource);
|