@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/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 usesLocalTunnel = isBrowserStack ? bstackOptions?.local === true : sauceOptions?.tunnelName !== void 0;
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 join4 } from "path";
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 ?? "Linux",
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 { ...caps, ...userCapabilities };
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 = join4(process.cwd(), ".trace");
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 = join4(traceDir, `${timestamp}-${sessionId.slice(0, 8)}.zip`);
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 (cloud providers only, e.g. "Windows", "OS X")'),
4089
- osVersion: z14.string().optional().describe('OS version (cloud providers only, e.g. "11", "Sequoia")'),
4090
- app: z14.string().optional().describe("App URL (bs://...) for BrowserStack or storage:filename= for Sauce Labs mobile sessions"),
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
- if (!appPath && !app && noReset !== true) {
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 appInfo = appPath ? `
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} app session started with sessionId: ${sessionId}
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 or Sauce Labs App Storage). Reads provider-specific credentials from environment.",
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
- const res = await fetch(url, {
4572
- headers: { Authorization: `Basic ${auth}` }
4573
- });
4574
- if (!res.ok) {
4575
- const body = await res.text();
4576
- return { isError: true, content: [{ type: "text", text: `${config.name} API error ${res.status}: ${body}` }] };
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 App Automate or Sauce Labs App Storage). Returns the app URL for use in start_session.",
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);