@wdio/mcp 3.5.0 → 3.5.2
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 +453 -67
- package/lib/server.js.map +1 -1
- package/lib/trace.js +104 -4
- package/lib/trace.js.map +1 -1
- package/package.json +2 -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.
|
|
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",
|
|
@@ -97,6 +97,7 @@ var package_default = {
|
|
|
97
97
|
"@xmldom/xmldom": "^0.9.10",
|
|
98
98
|
"browserstack-local": "^1.5.12",
|
|
99
99
|
"puppeteer-core": "^24.40.0",
|
|
100
|
+
saucelabs: "^9.0.2",
|
|
100
101
|
sharp: "^0.34.5",
|
|
101
102
|
webdriverio: "^9.27.0",
|
|
102
103
|
xpath: "^0.0.34",
|
|
@@ -2130,9 +2131,7 @@ init_state();
|
|
|
2130
2131
|
import sharp from "sharp";
|
|
2131
2132
|
|
|
2132
2133
|
// src/trace/state.ts
|
|
2133
|
-
|
|
2134
|
-
var require2 = createRequire(import.meta.url);
|
|
2135
|
-
var { version: LIBRARY_VERSION } = require2("../../package.json");
|
|
2134
|
+
var libraryVersion = package_default.version;
|
|
2136
2135
|
var traceSessions = /* @__PURE__ */ new Map();
|
|
2137
2136
|
function createTraceSession(sessionId, browserName, viewport, title, sessionType = "browser") {
|
|
2138
2137
|
const prefix = sessionId.slice(0, 8);
|
|
@@ -2156,7 +2155,7 @@ function createTraceSession(sessionId, browserName, viewport, title, sessionType
|
|
|
2156
2155
|
type: "context-options",
|
|
2157
2156
|
origin: "library",
|
|
2158
2157
|
libraryName: "@wdio/mcp",
|
|
2159
|
-
libraryVersion
|
|
2158
|
+
libraryVersion,
|
|
2160
2159
|
browserName,
|
|
2161
2160
|
platform: process.platform === "darwin" ? "darwin" : process.platform === "win32" ? "windows" : "linux",
|
|
2162
2161
|
wallTime: session.startWallTime,
|
|
@@ -2429,6 +2428,71 @@ var browserstackLocalBinaryResource = {
|
|
|
2429
2428
|
}
|
|
2430
2429
|
};
|
|
2431
2430
|
|
|
2431
|
+
// src/resources/saucelabs-local.resource.ts
|
|
2432
|
+
function getLocalBinaryInfo2() {
|
|
2433
|
+
const platform2 = process.platform;
|
|
2434
|
+
const arch = process.arch;
|
|
2435
|
+
if (platform2 === "darwin") {
|
|
2436
|
+
return {
|
|
2437
|
+
downloadUrl: "https://saucelabs.com/downloads/sc-4.9.2-osx.zip",
|
|
2438
|
+
platform: "macOS",
|
|
2439
|
+
arch: arch === "arm64" ? "Apple Silicon" : "Intel x64",
|
|
2440
|
+
binaryName: "sc"
|
|
2441
|
+
};
|
|
2442
|
+
}
|
|
2443
|
+
if (platform2 === "win32") {
|
|
2444
|
+
return {
|
|
2445
|
+
downloadUrl: "https://saucelabs.com/downloads/sc-4.9.2-win32.zip",
|
|
2446
|
+
platform: "Windows",
|
|
2447
|
+
arch: "x86/x64",
|
|
2448
|
+
binaryName: "sc.exe"
|
|
2449
|
+
};
|
|
2450
|
+
}
|
|
2451
|
+
return {
|
|
2452
|
+
downloadUrl: "https://saucelabs.com/downloads/sc-4.9.2-linux.tar.gz",
|
|
2453
|
+
platform: "Linux",
|
|
2454
|
+
arch: arch === "arm64" ? "ARM64" : "x64",
|
|
2455
|
+
binaryName: "sc"
|
|
2456
|
+
};
|
|
2457
|
+
}
|
|
2458
|
+
var saucelabsLocalBinaryResource = {
|
|
2459
|
+
name: "saucelabs-local-binary",
|
|
2460
|
+
uri: "wdio://saucelabs/local-binary",
|
|
2461
|
+
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.",
|
|
2462
|
+
handler: async () => {
|
|
2463
|
+
const info = getLocalBinaryInfo2();
|
|
2464
|
+
const username = process.env.SAUCE_USERNAME ?? "<SAUCE_USERNAME>";
|
|
2465
|
+
const accessKey = process.env.SAUCE_ACCESS_KEY ?? "<SAUCE_ACCESS_KEY>";
|
|
2466
|
+
const region = process.env.SAUCE_REGION ?? "eu-central-1";
|
|
2467
|
+
const content = {
|
|
2468
|
+
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.",
|
|
2469
|
+
platform: info.platform,
|
|
2470
|
+
arch: info.arch,
|
|
2471
|
+
downloadUrl: info.downloadUrl,
|
|
2472
|
+
...info.note ? { note: info.note } : {},
|
|
2473
|
+
setup: [
|
|
2474
|
+
`1. Download: curl -O ${info.downloadUrl}`,
|
|
2475
|
+
`2. Unzip: ${info.downloadUrl.endsWith(".tar.gz") ? `tar -xzf ${info.downloadUrl.split("/").pop()}` : `unzip ${info.downloadUrl.split("/").pop()}`}`,
|
|
2476
|
+
`3. Make executable (macOS/Linux): chmod +x ${info.binaryName}`,
|
|
2477
|
+
`4. Start daemon: ./${info.binaryName} -u ${username} -k ${accessKey} --region ${region}`
|
|
2478
|
+
],
|
|
2479
|
+
commands: {
|
|
2480
|
+
start: `./${info.binaryName} -u ${username} -k ${accessKey} --region ${region}`,
|
|
2481
|
+
stop: `./${info.binaryName} --stop`,
|
|
2482
|
+
status: `./${info.binaryName} --status`
|
|
2483
|
+
},
|
|
2484
|
+
afterDaemonIsRunning: "Call start_session with tunnel: 'external' and tunnelName matching this daemon to route traffic through it."
|
|
2485
|
+
};
|
|
2486
|
+
return {
|
|
2487
|
+
contents: [{
|
|
2488
|
+
uri: "wdio://saucelabs/local-binary",
|
|
2489
|
+
mimeType: "application/json",
|
|
2490
|
+
text: JSON.stringify(content, null, 2)
|
|
2491
|
+
}]
|
|
2492
|
+
};
|
|
2493
|
+
}
|
|
2494
|
+
};
|
|
2495
|
+
|
|
2432
2496
|
// src/resources/sessions.resource.ts
|
|
2433
2497
|
import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2434
2498
|
|
|
@@ -2455,6 +2519,7 @@ function generateStep(step, history) {
|
|
|
2455
2519
|
case "start_session": {
|
|
2456
2520
|
const platform2 = p.platform;
|
|
2457
2521
|
const isBrowserStack = "bstack:options" in history.capabilities;
|
|
2522
|
+
const isSauceLabs = "sauce:options" in history.capabilities;
|
|
2458
2523
|
const capJson = indentJson(history.capabilities).split("\n").map((line, i) => i > 0 ? ` ${line}` : line).join("\n");
|
|
2459
2524
|
if (isBrowserStack) {
|
|
2460
2525
|
const nav = platform2 === "browser" && p.navigationUrl ? `
|
|
@@ -2471,6 +2536,22 @@ await browser.url('${escapeStr(p.navigationUrl)}');` : "";
|
|
|
2471
2536
|
`});${nav}`
|
|
2472
2537
|
].join("\n");
|
|
2473
2538
|
}
|
|
2539
|
+
if (isSauceLabs) {
|
|
2540
|
+
const region = history.capabilities["sauce:options"]?.region ?? "eu-central-1";
|
|
2541
|
+
const nav = platform2 === "browser" && p.navigationUrl ? `
|
|
2542
|
+
await browser.url('${escapeStr(p.navigationUrl)}');` : "";
|
|
2543
|
+
return [
|
|
2544
|
+
"const browser = await remote({",
|
|
2545
|
+
" protocol: 'https',",
|
|
2546
|
+
` hostname: 'ondemand.${region}.saucelabs.com',`,
|
|
2547
|
+
" port: 443,",
|
|
2548
|
+
" path: '/wd/hub',",
|
|
2549
|
+
" user: process.env.SAUCE_USERNAME,",
|
|
2550
|
+
" key: process.env.SAUCE_ACCESS_KEY,",
|
|
2551
|
+
` capabilities: ${capJson}`,
|
|
2552
|
+
`});${nav}`
|
|
2553
|
+
].join("\n");
|
|
2554
|
+
}
|
|
2474
2555
|
if (platform2 === "browser") {
|
|
2475
2556
|
const nav = p.navigationUrl ? `
|
|
2476
2557
|
await browser.url('${escapeStr(p.navigationUrl)}');` : "";
|
|
@@ -2533,8 +2614,10 @@ function bsStatusUpdateLines(sessionType) {
|
|
|
2533
2614
|
}
|
|
2534
2615
|
function generateCode(history) {
|
|
2535
2616
|
const bstackOptions = history.capabilities["bstack:options"];
|
|
2617
|
+
const sauceOptions = history.capabilities["sauce:options"];
|
|
2536
2618
|
const isBrowserStack = bstackOptions !== void 0;
|
|
2537
|
-
const
|
|
2619
|
+
const isSauceLabs = sauceOptions !== void 0;
|
|
2620
|
+
const usesLocalTunnel = isBrowserStack ? bstackOptions?.local === true : sauceOptions?.tunnelName !== void 0;
|
|
2538
2621
|
const steps = history.steps.map((step) => generateStep(step, history)).join("\n").split("\n").map((line) => ` ${line}`).join("\n");
|
|
2539
2622
|
if (isBrowserStack) {
|
|
2540
2623
|
const bsSteps = steps.replace(/const browser = await remote\(/g, "browser = await remote(");
|
|
@@ -2545,7 +2628,7 @@ function generateCode(history) {
|
|
|
2545
2628
|
${statusUpdate}
|
|
2546
2629
|
await browser.deleteSession();
|
|
2547
2630
|
}`;
|
|
2548
|
-
if (
|
|
2631
|
+
if (usesLocalTunnel) {
|
|
2549
2632
|
const tunnelSetup = [
|
|
2550
2633
|
"",
|
|
2551
2634
|
"const tunnel = new BrowserstackTunnel();",
|
|
@@ -2581,6 +2664,64 @@ ${statusUpdate}
|
|
|
2581
2664
|
"}"
|
|
2582
2665
|
].join("\n");
|
|
2583
2666
|
}
|
|
2667
|
+
if (isSauceLabs) {
|
|
2668
|
+
const slSteps = steps.replace(/const browser = await remote\(/g, "browser = await remote(");
|
|
2669
|
+
const preamble = "let browser;\nlet slStatus = 'passed';\nlet slReason;";
|
|
2670
|
+
const catchBlock = "} catch (e) {\n slStatus = 'failed';\n slReason = String(e);\n throw e;";
|
|
2671
|
+
const slRegion = sauceOptions?.region ?? "eu-central-1";
|
|
2672
|
+
const statusUpdate = [
|
|
2673
|
+
" const slAuth = Buffer.from(`${process.env.SAUCE_USERNAME}:${process.env.SAUCE_ACCESS_KEY}`).toString('base64');",
|
|
2674
|
+
` await fetch(\`https://api.${slRegion}.saucelabs.com/rest/v1/\${process.env.SAUCE_USERNAME}/jobs/\` + browser.sessionId, {`,
|
|
2675
|
+
" method: 'PUT',",
|
|
2676
|
+
" headers: { Authorization: 'Basic ' + slAuth, 'Content-Type': 'application/json' },",
|
|
2677
|
+
" body: JSON.stringify({ passed: slStatus === 'passed' })",
|
|
2678
|
+
" });"
|
|
2679
|
+
].join("\n");
|
|
2680
|
+
const finallyLines = [
|
|
2681
|
+
" if (browser) {",
|
|
2682
|
+
statusUpdate,
|
|
2683
|
+
" await browser.deleteSession();",
|
|
2684
|
+
" }"
|
|
2685
|
+
];
|
|
2686
|
+
if (usesLocalTunnel) {
|
|
2687
|
+
const tunnelSetup = [
|
|
2688
|
+
"",
|
|
2689
|
+
"import SauceLabs from 'saucelabs';",
|
|
2690
|
+
"",
|
|
2691
|
+
"const sl = new SauceLabs({",
|
|
2692
|
+
" user: process.env.SAUCE_USERNAME,",
|
|
2693
|
+
" key: process.env.SAUCE_ACCESS_KEY,",
|
|
2694
|
+
` region: '${slRegion}',`,
|
|
2695
|
+
"});",
|
|
2696
|
+
"const sc = await sl.startSauceConnect({ tunnelName: 'wdio-mcp-tunnel' });",
|
|
2697
|
+
"const stopTunnel = () => sc.close();",
|
|
2698
|
+
""
|
|
2699
|
+
].join("\n");
|
|
2700
|
+
return [
|
|
2701
|
+
"import { remote } from 'webdriverio';",
|
|
2702
|
+
tunnelSetup,
|
|
2703
|
+
preamble,
|
|
2704
|
+
"try {",
|
|
2705
|
+
slSteps,
|
|
2706
|
+
catchBlock,
|
|
2707
|
+
"} finally {",
|
|
2708
|
+
...finallyLines,
|
|
2709
|
+
" await stopTunnel();",
|
|
2710
|
+
"}"
|
|
2711
|
+
].join("\n");
|
|
2712
|
+
}
|
|
2713
|
+
return [
|
|
2714
|
+
"import { remote } from 'webdriverio';",
|
|
2715
|
+
"",
|
|
2716
|
+
preamble,
|
|
2717
|
+
"try {",
|
|
2718
|
+
slSteps,
|
|
2719
|
+
catchBlock,
|
|
2720
|
+
"} finally {",
|
|
2721
|
+
...finallyLines,
|
|
2722
|
+
"}"
|
|
2723
|
+
].join("\n");
|
|
2724
|
+
}
|
|
2584
2725
|
return `import { remote } from 'webdriverio';
|
|
2585
2726
|
|
|
2586
2727
|
try {
|
|
@@ -3278,7 +3419,7 @@ import { z as z14 } from "zod";
|
|
|
3278
3419
|
// src/session/lifecycle.ts
|
|
3279
3420
|
init_state();
|
|
3280
3421
|
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
3281
|
-
import { join as
|
|
3422
|
+
import { join as join4 } from "path";
|
|
3282
3423
|
|
|
3283
3424
|
// src/providers/local-browser.provider.ts
|
|
3284
3425
|
var LocalBrowserProvider = class {
|
|
@@ -3533,6 +3674,13 @@ import { promisify } from "util";
|
|
|
3533
3674
|
import { tmpdir as tmpdir2 } from "os";
|
|
3534
3675
|
import { join as join2 } from "path";
|
|
3535
3676
|
import { Local as BrowserstackTunnel } from "browserstack-local";
|
|
3677
|
+
|
|
3678
|
+
// src/utils/auth.ts
|
|
3679
|
+
function basicAuth(user, key) {
|
|
3680
|
+
return Buffer.from(`${user}:${key}`).toString("base64");
|
|
3681
|
+
}
|
|
3682
|
+
|
|
3683
|
+
// src/providers/cloud/browserstack.provider.ts
|
|
3536
3684
|
var BrowserStackProvider = class {
|
|
3537
3685
|
name = "browserstack";
|
|
3538
3686
|
getConnectionConfig(options) {
|
|
@@ -3550,7 +3698,7 @@ var BrowserStackProvider = class {
|
|
|
3550
3698
|
buildCapabilities(options) {
|
|
3551
3699
|
const platform2 = options.platform;
|
|
3552
3700
|
const userCapabilities = options.capabilities ?? {};
|
|
3553
|
-
const browserstackLocal = options.browserstackLocal;
|
|
3701
|
+
const browserstackLocal = options.tunnel ?? options.browserstackLocal;
|
|
3554
3702
|
if (platform2 === "browser") {
|
|
3555
3703
|
const bstackOptions2 = {
|
|
3556
3704
|
browserVersion: options.browserVersion ?? "latest"
|
|
@@ -3628,7 +3776,7 @@ var BrowserStackProvider = class {
|
|
|
3628
3776
|
const key = process.env.BROWSERSTACK_ACCESS_KEY;
|
|
3629
3777
|
if (!user || !key) return;
|
|
3630
3778
|
const baseUrl = sessionType === "browser" ? "https://api.browserstack.com/automate/sessions" : "https://api-cloud.browserstack.com/app-automate/sessions";
|
|
3631
|
-
const auth =
|
|
3779
|
+
const auth = basicAuth(user, key);
|
|
3632
3780
|
const body = { status: result.status, ...result.reason ? { reason: result.reason } : {} };
|
|
3633
3781
|
await fetch(`${baseUrl}/${sessionId}.json`, {
|
|
3634
3782
|
method: "PUT",
|
|
@@ -3639,11 +3787,153 @@ var BrowserStackProvider = class {
|
|
|
3639
3787
|
};
|
|
3640
3788
|
var browserStackProvider = new BrowserStackProvider();
|
|
3641
3789
|
|
|
3790
|
+
// src/providers/cloud/saucelabs.provider.ts
|
|
3791
|
+
import { tmpdir as tmpdir3 } from "os";
|
|
3792
|
+
import { join as join3 } from "path";
|
|
3793
|
+
var SauceLabsProvider = class {
|
|
3794
|
+
name = "saucelabs";
|
|
3795
|
+
resolveRegion(options) {
|
|
3796
|
+
return options.region ?? "eu-central-1";
|
|
3797
|
+
}
|
|
3798
|
+
getConnectionConfig(options) {
|
|
3799
|
+
const region = this.resolveRegion(options);
|
|
3800
|
+
return {
|
|
3801
|
+
protocol: "https",
|
|
3802
|
+
hostname: `ondemand.${region}.saucelabs.com`,
|
|
3803
|
+
port: 443,
|
|
3804
|
+
path: "/wd/hub",
|
|
3805
|
+
user: process.env.SAUCE_USERNAME,
|
|
3806
|
+
key: process.env.SAUCE_ACCESS_KEY
|
|
3807
|
+
};
|
|
3808
|
+
}
|
|
3809
|
+
buildCapabilities(options) {
|
|
3810
|
+
const platform2 = options.platform;
|
|
3811
|
+
const region = this.resolveRegion(options);
|
|
3812
|
+
const userCapabilities = options.capabilities ?? {};
|
|
3813
|
+
const saucelabsLocal = options.tunnel ?? options.saucelabsLocal;
|
|
3814
|
+
const reporting = options.reporting;
|
|
3815
|
+
const sauceOptions = { region };
|
|
3816
|
+
if (reporting?.build) sauceOptions.build = reporting.build;
|
|
3817
|
+
if (reporting?.session) sauceOptions.name = reporting.session;
|
|
3818
|
+
else if (reporting?.project) sauceOptions.name = reporting.project;
|
|
3819
|
+
if (saucelabsLocal) {
|
|
3820
|
+
sauceOptions.tunnelName = options.tunnelName;
|
|
3821
|
+
}
|
|
3822
|
+
if (platform2 === "browser") {
|
|
3823
|
+
return {
|
|
3824
|
+
browserName: options.browser ?? "chrome",
|
|
3825
|
+
browserVersion: options.browserVersion ?? "latest",
|
|
3826
|
+
platformName: options.os ?? "Linux",
|
|
3827
|
+
"sauce:options": sauceOptions,
|
|
3828
|
+
...userCapabilities
|
|
3829
|
+
};
|
|
3830
|
+
}
|
|
3831
|
+
const mobileBrowser = options.browser;
|
|
3832
|
+
if (mobileBrowser) {
|
|
3833
|
+
sauceOptions.appiumVersion = "2.11.0";
|
|
3834
|
+
if (options.deviceOrientation) sauceOptions.deviceOrientation = options.deviceOrientation;
|
|
3835
|
+
const caps = {
|
|
3836
|
+
platformName: platform2,
|
|
3837
|
+
browserName: mobileBrowser,
|
|
3838
|
+
"appium:deviceName": options.deviceName,
|
|
3839
|
+
"appium:platformVersion": options.platformVersion,
|
|
3840
|
+
"appium:automationName": options.automationName ?? (platform2 === "ios" ? "XCUITest" : "UiAutomator2"),
|
|
3841
|
+
"appium:newCommandTimeout": options.newCommandTimeout ?? 300,
|
|
3842
|
+
"sauce:options": sauceOptions
|
|
3843
|
+
};
|
|
3844
|
+
return { ...caps, ...userCapabilities };
|
|
3845
|
+
}
|
|
3846
|
+
sauceOptions.appiumVersion = "latest";
|
|
3847
|
+
const autoAcceptAlerts = options.autoAcceptAlerts;
|
|
3848
|
+
const autoDismissAlerts = options.autoDismissAlerts;
|
|
3849
|
+
return {
|
|
3850
|
+
platformName: platform2,
|
|
3851
|
+
"appium:app": options.app,
|
|
3852
|
+
"appium:deviceName": options.deviceName,
|
|
3853
|
+
"appium:platformVersion": options.platformVersion,
|
|
3854
|
+
"appium:automationName": options.automationName ?? (platform2 === "ios" ? "XCUITest" : "UiAutomator2"),
|
|
3855
|
+
"appium:autoGrantPermissions": options.autoGrantPermissions ?? true,
|
|
3856
|
+
"appium:autoAcceptAlerts": autoDismissAlerts ? void 0 : autoAcceptAlerts ?? true,
|
|
3857
|
+
"appium:autoDismissAlerts": autoDismissAlerts,
|
|
3858
|
+
"appium:newCommandTimeout": options.newCommandTimeout ?? 300,
|
|
3859
|
+
"sauce:options": sauceOptions,
|
|
3860
|
+
...userCapabilities
|
|
3861
|
+
};
|
|
3862
|
+
}
|
|
3863
|
+
getSessionType(options) {
|
|
3864
|
+
const platform2 = options.platform;
|
|
3865
|
+
if (platform2 === "browser") return "browser";
|
|
3866
|
+
return platform2;
|
|
3867
|
+
}
|
|
3868
|
+
shouldAutoDetach(_options) {
|
|
3869
|
+
return false;
|
|
3870
|
+
}
|
|
3871
|
+
async startTunnel(options) {
|
|
3872
|
+
const region = this.resolveRegion(options);
|
|
3873
|
+
const tunnelName = options.tunnelName ?? `wdio-mcp-${Date.now()}`;
|
|
3874
|
+
const logFile = join3(tmpdir3(), "sauce-connect.log");
|
|
3875
|
+
console.error(`[SauceLabs] Starting tunnel "${tunnelName}" (region: ${region})`);
|
|
3876
|
+
try {
|
|
3877
|
+
const { default: SauceLabs } = await import("saucelabs");
|
|
3878
|
+
const api = new SauceLabs({ user: process.env.SAUCE_USERNAME ?? "", key: process.env.SAUCE_ACCESS_KEY ?? "", region });
|
|
3879
|
+
return api.startSauceConnect({
|
|
3880
|
+
tunnelName,
|
|
3881
|
+
logFile,
|
|
3882
|
+
logger: (msg) => console.error(`[SauceConnect] ${msg}`)
|
|
3883
|
+
});
|
|
3884
|
+
} catch (e) {
|
|
3885
|
+
const msg = (e !== null && typeof e === "object" ? e.message : void 0) ?? String(e);
|
|
3886
|
+
if (msg.includes("already running") || msg.includes("another instance")) {
|
|
3887
|
+
console.error("[SauceLabs] Tunnel already running \u2014 reusing existing tunnel");
|
|
3888
|
+
return null;
|
|
3889
|
+
}
|
|
3890
|
+
throw e;
|
|
3891
|
+
}
|
|
3892
|
+
}
|
|
3893
|
+
async onSessionClose(sessionId, _sessionType, result, _tunnelHandle, _browser, region) {
|
|
3894
|
+
const effectiveRegion = region ?? "eu-central-1";
|
|
3895
|
+
const user = process.env.SAUCE_USERNAME;
|
|
3896
|
+
const key = process.env.SAUCE_ACCESS_KEY;
|
|
3897
|
+
if (user && key) {
|
|
3898
|
+
try {
|
|
3899
|
+
const auth = basicAuth(user, key);
|
|
3900
|
+
const body = { passed: result.status === "passed" };
|
|
3901
|
+
const apiUrl = `https://api.${effectiveRegion}.saucelabs.com/rest/v1/${user}/jobs/${sessionId}`;
|
|
3902
|
+
console.error(`[SauceLabs] Setting job status for ${sessionId}: ${result.status}`);
|
|
3903
|
+
await fetch(apiUrl, {
|
|
3904
|
+
method: "PUT",
|
|
3905
|
+
headers: {
|
|
3906
|
+
Authorization: `Basic ${auth}`,
|
|
3907
|
+
"Content-Type": "application/json"
|
|
3908
|
+
},
|
|
3909
|
+
body: JSON.stringify(body)
|
|
3910
|
+
});
|
|
3911
|
+
console.error("[SauceLabs] Job result set successfully via REST API");
|
|
3912
|
+
} catch (e) {
|
|
3913
|
+
console.error("[SauceLabs] Failed to set job result via REST API:", e);
|
|
3914
|
+
}
|
|
3915
|
+
}
|
|
3916
|
+
}
|
|
3917
|
+
async stopTunnel(tunnelHandle) {
|
|
3918
|
+
if (tunnelHandle) {
|
|
3919
|
+
const sc = tunnelHandle;
|
|
3920
|
+
await sc.close();
|
|
3921
|
+
}
|
|
3922
|
+
}
|
|
3923
|
+
};
|
|
3924
|
+
var sauceLabsProvider = new SauceLabsProvider();
|
|
3925
|
+
|
|
3642
3926
|
// src/providers/registry.ts
|
|
3643
|
-
|
|
3644
|
-
|
|
3927
|
+
var providers = /* @__PURE__ */ new Map([
|
|
3928
|
+
["browserstack", browserStackProvider],
|
|
3929
|
+
["saucelabs", sauceLabsProvider]
|
|
3930
|
+
]);
|
|
3931
|
+
function getDefaultProvider(platform2) {
|
|
3645
3932
|
return platform2 === "browser" ? localBrowserProvider : localAppiumProvider;
|
|
3646
3933
|
}
|
|
3934
|
+
function getProvider(providerName, platform2) {
|
|
3935
|
+
return providers.get(providerName) ?? getDefaultProvider(platform2);
|
|
3936
|
+
}
|
|
3647
3937
|
|
|
3648
3938
|
// src/trace/zip-writer.ts
|
|
3649
3939
|
import yazl from "yazl";
|
|
@@ -3673,10 +3963,10 @@ async function finalizeTrace(sessionId, browser) {
|
|
|
3673
3963
|
if (!traceSession) return;
|
|
3674
3964
|
try {
|
|
3675
3965
|
await traceSession.screenshotChain;
|
|
3676
|
-
const traceDir =
|
|
3966
|
+
const traceDir = join4(process.cwd(), ".trace");
|
|
3677
3967
|
mkdirSync2(traceDir, { recursive: true });
|
|
3678
3968
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
3679
|
-
const outPath =
|
|
3969
|
+
const outPath = join4(traceDir, `${timestamp}-${sessionId.slice(0, 8)}.zip`);
|
|
3680
3970
|
const zipBuffer = await buildTraceZip(traceSession);
|
|
3681
3971
|
writeFileSync2(outPath, zipBuffer);
|
|
3682
3972
|
console.error(`[TRACE] Saved to ${outPath}`);
|
|
@@ -3728,11 +4018,16 @@ function registerSession(sessionId, browser, metadata, historyEntry) {
|
|
|
3728
4018
|
if (oldMetadata?.provider) {
|
|
3729
4019
|
const oldHistory = state2.sessionHistory.get(oldSessionId);
|
|
3730
4020
|
const provider = getProvider(oldMetadata.provider, oldMetadata.type);
|
|
3731
|
-
await provider.onSessionClose?.(oldSessionId, oldMetadata.type, getSessionResult(oldHistory), oldMetadata.tunnelHandle).catch(() => {
|
|
4021
|
+
await provider.onSessionClose?.(oldSessionId, oldMetadata.type, getSessionResult(oldHistory), oldMetadata.tunnelHandle, oldBrowser, oldMetadata.region).catch(() => {
|
|
3732
4022
|
});
|
|
3733
4023
|
}
|
|
3734
4024
|
await oldBrowser.deleteSession().catch(() => {
|
|
3735
4025
|
});
|
|
4026
|
+
if (oldMetadata?.provider && oldMetadata?.tunnelHandle) {
|
|
4027
|
+
const provider = getProvider(oldMetadata.provider, oldMetadata.type);
|
|
4028
|
+
await provider.stopTunnel?.(oldMetadata.tunnelHandle).catch(() => {
|
|
4029
|
+
});
|
|
4030
|
+
}
|
|
3736
4031
|
})();
|
|
3737
4032
|
state2.browsers.delete(oldSessionId);
|
|
3738
4033
|
state2.sessionMetadata.delete(oldSessionId);
|
|
@@ -3755,12 +4050,20 @@ async function closeSession(sessionId, detach, isAttached, force) {
|
|
|
3755
4050
|
if (metadata?.provider) {
|
|
3756
4051
|
try {
|
|
3757
4052
|
const provider = getProvider(metadata.provider, metadata.type);
|
|
3758
|
-
await provider.onSessionClose?.(sessionId, metadata.type, getSessionResult(history), metadata.tunnelHandle);
|
|
4053
|
+
await provider.onSessionClose?.(sessionId, metadata.type, getSessionResult(history), metadata.tunnelHandle, browser, metadata.region);
|
|
3759
4054
|
} catch (e) {
|
|
3760
4055
|
console.error("[WARN] Failed to run provider onSessionClose:", e);
|
|
3761
4056
|
}
|
|
3762
4057
|
}
|
|
3763
4058
|
await browser.deleteSession();
|
|
4059
|
+
if (metadata?.provider && metadata?.tunnelHandle) {
|
|
4060
|
+
try {
|
|
4061
|
+
const provider = getProvider(metadata.provider, metadata.type);
|
|
4062
|
+
await provider.stopTunnel?.(metadata.tunnelHandle);
|
|
4063
|
+
} catch (e) {
|
|
4064
|
+
console.error("[WARN] Failed to stop tunnel:", e);
|
|
4065
|
+
}
|
|
4066
|
+
}
|
|
3764
4067
|
}
|
|
3765
4068
|
state2.browsers.delete(sessionId);
|
|
3766
4069
|
state2.sessionMetadata.delete(sessionId);
|
|
@@ -3778,18 +4081,18 @@ var startSessionToolDefinition = {
|
|
|
3778
4081
|
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.',
|
|
3779
4082
|
annotations: { title: "Start Session", destructiveHint: false },
|
|
3780
4083
|
inputSchema: {
|
|
3781
|
-
provider: z14.enum(["local", "browserstack"]).optional().default("local").describe("Session provider (default: local)"),
|
|
4084
|
+
provider: z14.enum(["local", "browserstack", "saucelabs"]).optional().default("local").describe("Session provider (default: local)"),
|
|
3782
4085
|
platform: platformEnum.describe("Session platform type"),
|
|
3783
4086
|
browser: browserEnum.optional().describe("Browser to launch (required for browser platform)"),
|
|
3784
|
-
browserVersion: z14.string().optional().describe("Browser version (
|
|
3785
|
-
os: z14.string().optional().describe('Operating system (
|
|
3786
|
-
osVersion: z14.string().optional().describe('OS version (
|
|
3787
|
-
app: z14.string().optional().describe("
|
|
4087
|
+
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"),
|
|
3788
4091
|
reporting: z14.object({
|
|
3789
4092
|
project: z14.string().optional(),
|
|
3790
4093
|
build: z14.string().optional(),
|
|
3791
4094
|
session: z14.string().optional()
|
|
3792
|
-
}).optional().describe("
|
|
4095
|
+
}).optional().describe("Cloud provider reporting labels (project, build, session)"),
|
|
3793
4096
|
headless: coerceBoolean.optional().default(true).describe("Run browser in headless mode (default: true)"),
|
|
3794
4097
|
windowWidth: z14.number().min(400).max(3840).optional().default(1920).describe("Browser window width"),
|
|
3795
4098
|
windowHeight: z14.number().min(400).max(2160).optional().default(1080).describe("Browser window height"),
|
|
@@ -3817,7 +4120,11 @@ var startSessionToolDefinition = {
|
|
|
3817
4120
|
path: z14.string().optional(),
|
|
3818
4121
|
protocol: z14.string().optional()
|
|
3819
4122
|
}).optional().describe("Appium server connection (local provider only)"),
|
|
3820
|
-
|
|
4123
|
+
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".'),
|
|
4124
|
+
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.'),
|
|
4125
|
+
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
|
+
browserstackLocal: z14.union([z14.literal("external"), coerceBoolean]).optional().describe('Deprecated: use "tunnel" instead. Enable BrowserStack Local tunnel routing.'),
|
|
4127
|
+
saucelabsLocal: z14.union([z14.literal("external"), coerceBoolean]).optional().describe('Deprecated: use "tunnel" instead. Enable Sauce Connect tunnel routing.'),
|
|
3821
4128
|
navigationUrl: z14.string().optional().describe("URL to navigate to after starting"),
|
|
3822
4129
|
capabilities: z14.record(z14.string(), z14.unknown()).optional().describe("Additional capabilities to merge")
|
|
3823
4130
|
}
|
|
@@ -3896,15 +4203,19 @@ async function startBrowserSession(args) {
|
|
|
3896
4203
|
const effectiveHeadless = headless && headlessSupported;
|
|
3897
4204
|
const provider = getProvider(args.provider ?? "local", "browser");
|
|
3898
4205
|
const connectionConfig = provider.getConnectionConfig(args);
|
|
4206
|
+
const effectiveTunnel = args.tunnel ?? args.browserstackLocal ?? args.saucelabsLocal ?? false;
|
|
4207
|
+
const tunnelEnabled = effectiveTunnel === true;
|
|
4208
|
+
const tunnelName = tunnelEnabled && !args.tunnelName ? `wdio-mcp-${Date.now()}` : args.tunnelName;
|
|
3899
4209
|
const mergedCapabilities = provider.buildCapabilities({
|
|
3900
4210
|
...args,
|
|
3901
4211
|
browser,
|
|
3902
4212
|
headless,
|
|
3903
4213
|
windowWidth,
|
|
3904
4214
|
windowHeight,
|
|
3905
|
-
capabilities: userCapabilities
|
|
4215
|
+
capabilities: userCapabilities,
|
|
4216
|
+
tunnelName
|
|
3906
4217
|
});
|
|
3907
|
-
const tunnelHandle =
|
|
4218
|
+
const tunnelHandle = tunnelEnabled ? await provider.startTunnel?.({ ...args, tunnelName }) : void 0;
|
|
3908
4219
|
const wdioBrowser = await remote({ ...connectionConfig, capabilities: mergedCapabilities });
|
|
3909
4220
|
const { sessionId } = wdioBrowser;
|
|
3910
4221
|
const sessionMetadata = {
|
|
@@ -3912,6 +4223,8 @@ async function startBrowserSession(args) {
|
|
|
3912
4223
|
capabilities: mergedCapabilities,
|
|
3913
4224
|
isAttached: false,
|
|
3914
4225
|
provider: args.provider ?? "local",
|
|
4226
|
+
region: args.region,
|
|
4227
|
+
tunnelName,
|
|
3915
4228
|
tunnelHandle,
|
|
3916
4229
|
trace: args.trace ?? false
|
|
3917
4230
|
};
|
|
@@ -3960,8 +4273,11 @@ async function startMobileSession(args) {
|
|
|
3960
4273
|
}
|
|
3961
4274
|
const provider = getProvider(args.provider ?? "local", args.platform);
|
|
3962
4275
|
const serverConfig = provider.getConnectionConfig(args);
|
|
3963
|
-
const
|
|
3964
|
-
const
|
|
4276
|
+
const effectiveTunnel = args.tunnel ?? args.browserstackLocal ?? args.saucelabsLocal ?? false;
|
|
4277
|
+
const tunnelEnabled = effectiveTunnel === true;
|
|
4278
|
+
const tunnelName = tunnelEnabled && !args.tunnelName ? `wdio-mcp-${Date.now()}` : args.tunnelName;
|
|
4279
|
+
const mergedCapabilities = provider.buildCapabilities({ ...args, tunnelName });
|
|
4280
|
+
const tunnelHandle = tunnelEnabled ? await provider.startTunnel?.({ ...args, tunnelName }) : void 0;
|
|
3965
4281
|
const browser = await remote({ ...serverConfig, capabilities: mergedCapabilities });
|
|
3966
4282
|
const { sessionId } = browser;
|
|
3967
4283
|
const shouldAutoDetach = provider.shouldAutoDetach(args);
|
|
@@ -3971,6 +4287,8 @@ async function startMobileSession(args) {
|
|
|
3971
4287
|
capabilities: mergedCapabilities,
|
|
3972
4288
|
isAttached: shouldAutoDetach,
|
|
3973
4289
|
provider: args.provider ?? "local",
|
|
4290
|
+
region: args.region,
|
|
4291
|
+
tunnelName,
|
|
3974
4292
|
tunnelHandle,
|
|
3975
4293
|
trace: args.trace ?? false
|
|
3976
4294
|
};
|
|
@@ -4154,51 +4472,112 @@ var switchFrameTool = async ({
|
|
|
4154
4472
|
}
|
|
4155
4473
|
};
|
|
4156
4474
|
|
|
4157
|
-
// src/tools/
|
|
4158
|
-
import { existsSync as existsSync2,
|
|
4475
|
+
// src/tools/cloud-provider.tool.ts
|
|
4476
|
+
import { existsSync as existsSync2, readFileSync } from "fs";
|
|
4159
4477
|
import { z as z17 } from "zod";
|
|
4160
|
-
var BS_API = "https://api-cloud.browserstack.com";
|
|
4161
|
-
function getAuth() {
|
|
4162
|
-
const user = process.env.BROWSERSTACK_USERNAME;
|
|
4163
|
-
const key = process.env.BROWSERSTACK_ACCESS_KEY;
|
|
4164
|
-
if (!user || !key) return null;
|
|
4165
|
-
return Buffer.from(`${user}:${key}`).toString("base64");
|
|
4166
|
-
}
|
|
4167
4478
|
function formatAppList(apps) {
|
|
4168
4479
|
if (apps.length === 0) return "No apps found.";
|
|
4169
4480
|
return apps.map((a) => {
|
|
4170
|
-
const id = a.
|
|
4171
|
-
|
|
4481
|
+
const id = a.customId ? ` [${a.customId}]` : "";
|
|
4482
|
+
const ts = a.uploadedAt ?? "unknown";
|
|
4483
|
+
return `${a.name}${id} \u2014 ${a.ref} (${ts})`;
|
|
4172
4484
|
}).join("\n");
|
|
4173
4485
|
}
|
|
4486
|
+
var PROVIDER_CONFIGS = {
|
|
4487
|
+
browserstack: {
|
|
4488
|
+
name: "BrowserStack",
|
|
4489
|
+
apiBase: "https://api-cloud.browserstack.com",
|
|
4490
|
+
credsEnvNames: ["BROWSERSTACK_USERNAME", "BROWSERSTACK_ACCESS_KEY"],
|
|
4491
|
+
listPath: "/app-automate/recent_apps",
|
|
4492
|
+
supportsOrgWide: true,
|
|
4493
|
+
parseListResponse: (raw) => {
|
|
4494
|
+
const apps = Array.isArray(raw) ? raw : [];
|
|
4495
|
+
return apps.map((a) => ({
|
|
4496
|
+
name: `${a.app_name}`,
|
|
4497
|
+
ref: `${a.app_url}`,
|
|
4498
|
+
uploadedAt: `${a.uploaded_at}`,
|
|
4499
|
+
customId: a.custom_id
|
|
4500
|
+
}));
|
|
4501
|
+
},
|
|
4502
|
+
uploadPath: "/app-automate/upload",
|
|
4503
|
+
uploadField: "file",
|
|
4504
|
+
parseUploadResponse: (raw, fileName) => {
|
|
4505
|
+
const data = raw;
|
|
4506
|
+
return { appRef: data.app_url, appName: fileName };
|
|
4507
|
+
}
|
|
4508
|
+
},
|
|
4509
|
+
saucelabs: {
|
|
4510
|
+
name: "Sauce Labs",
|
|
4511
|
+
apiBase: "https://api.eu-central-1.saucelabs.com",
|
|
4512
|
+
// region overridden via param
|
|
4513
|
+
credsEnvNames: ["SAUCE_USERNAME", "SAUCE_ACCESS_KEY"],
|
|
4514
|
+
listPath: "/v1/storage/files",
|
|
4515
|
+
supportsOrgWide: false,
|
|
4516
|
+
parseListResponse: (raw) => {
|
|
4517
|
+
const data = raw;
|
|
4518
|
+
return (data.items ?? []).map((a) => ({
|
|
4519
|
+
name: a.name,
|
|
4520
|
+
ref: `storage:filename=${a.name}`,
|
|
4521
|
+
uploadedAt: a.uploadTimestamp ? new Date(a.uploadTimestamp).toISOString() : void 0,
|
|
4522
|
+
customId: a.customId
|
|
4523
|
+
}));
|
|
4524
|
+
},
|
|
4525
|
+
uploadPath: "/v1/storage/upload",
|
|
4526
|
+
uploadField: "payload",
|
|
4527
|
+
parseUploadResponse: (raw, fileName) => {
|
|
4528
|
+
const data = raw;
|
|
4529
|
+
const name = data.item?.name ?? fileName;
|
|
4530
|
+
return { appRef: `storage:filename=${name}`, appName: name };
|
|
4531
|
+
}
|
|
4532
|
+
}
|
|
4533
|
+
};
|
|
4534
|
+
function getProviderConfig(provider, region) {
|
|
4535
|
+
const base = PROVIDER_CONFIGS[provider];
|
|
4536
|
+
if (!base) return { error: `Unknown provider: ${provider}` };
|
|
4537
|
+
const [userEnv, keyEnv] = base.credsEnvNames;
|
|
4538
|
+
const user = process.env[userEnv];
|
|
4539
|
+
const key = process.env[keyEnv];
|
|
4540
|
+
if (!user || !key) {
|
|
4541
|
+
const vars = base.credsEnvNames.join(" and ");
|
|
4542
|
+
return { error: `Missing credentials: set ${vars} environment variables.` };
|
|
4543
|
+
}
|
|
4544
|
+
const config = provider === "saucelabs" ? { ...base, apiBase: `https://api.${region ?? "eu-central-1"}.saucelabs.com` } : base;
|
|
4545
|
+
return { config, auth: basicAuth(user, key) };
|
|
4546
|
+
}
|
|
4174
4547
|
var listAppsToolDefinition = {
|
|
4175
4548
|
name: "list_apps",
|
|
4176
|
-
description: "List apps uploaded to BrowserStack App Automate. Reads
|
|
4177
|
-
annotations: { title: "List
|
|
4549
|
+
description: "List apps uploaded to a cloud provider (BrowserStack App Automate or Sauce Labs App Storage). Reads provider-specific credentials from environment.",
|
|
4550
|
+
annotations: { title: "List Cloud Provider Apps", readOnlyHint: true, idempotentHint: true },
|
|
4178
4551
|
inputSchema: {
|
|
4552
|
+
provider: z17.enum(["browserstack", "saucelabs"]).describe("Cloud provider"),
|
|
4179
4553
|
sortBy: z17.enum(["app_name", "uploaded_at"]).optional().default("uploaded_at").describe("Sort order for results"),
|
|
4180
|
-
organizationWide: coerceBoolean.optional().default(false).describe("List apps uploaded by all users in the organization
|
|
4181
|
-
limit: z17.number().int().min(1).optional().default(20).describe("Maximum number of apps to return (only applies when organizationWide is true, default 20)")
|
|
4554
|
+
organizationWide: coerceBoolean.optional().default(false).describe("(BrowserStack only) List apps uploaded by all users in the organization. Defaults to false (own uploads only)."),
|
|
4555
|
+
limit: z17.number().int().min(1).optional().default(20).describe("Maximum number of apps to return (only applies when organizationWide is true, default 20)"),
|
|
4556
|
+
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)")
|
|
4182
4557
|
}
|
|
4183
4558
|
};
|
|
4184
|
-
var listAppsTool = async (
|
|
4185
|
-
const
|
|
4186
|
-
|
|
4187
|
-
|
|
4559
|
+
var listAppsTool = async (args) => {
|
|
4560
|
+
const { provider, sortBy = "uploaded_at", organizationWide = false, limit = 20, region = "eu-central-1" } = args;
|
|
4561
|
+
const resolved = getProviderConfig(provider, region);
|
|
4562
|
+
if ("error" in resolved) {
|
|
4563
|
+
return { isError: true, content: [{ type: "text", text: resolved.error }] };
|
|
4188
4564
|
}
|
|
4565
|
+
const { config, auth } = resolved;
|
|
4189
4566
|
try {
|
|
4190
|
-
let url = `${
|
|
4191
|
-
if (
|
|
4567
|
+
let url = `${config.apiBase}${config.listPath}`;
|
|
4568
|
+
if (config.supportsOrgWide && organizationWide) {
|
|
4569
|
+
url = `${config.apiBase}/app-automate/recent_group_apps?limit=${limit}`;
|
|
4570
|
+
}
|
|
4192
4571
|
const res = await fetch(url, {
|
|
4193
4572
|
headers: { Authorization: `Basic ${auth}` }
|
|
4194
4573
|
});
|
|
4195
4574
|
if (!res.ok) {
|
|
4196
4575
|
const body = await res.text();
|
|
4197
|
-
return { isError: true, content: [{ type: "text", text:
|
|
4576
|
+
return { isError: true, content: [{ type: "text", text: `${config.name} API error ${res.status}: ${body}` }] };
|
|
4198
4577
|
}
|
|
4199
4578
|
const raw = await res.json();
|
|
4200
|
-
let apps =
|
|
4201
|
-
apps = sortBy === "app_name" ? apps.sort((a, b) => a.
|
|
4579
|
+
let apps = config.parseListResponse(raw);
|
|
4580
|
+
apps = sortBy === "app_name" ? apps.sort((a, b) => a.name.localeCompare(b.name)) : apps.sort((a, b) => (b.uploadedAt ?? "").localeCompare(a.uploadedAt ?? ""));
|
|
4202
4581
|
return { content: [{ type: "text", text: formatAppList(apps) }] };
|
|
4203
4582
|
} catch (e) {
|
|
4204
4583
|
return { isError: true, content: [{ type: "text", text: `Error listing apps: ${e}` }] };
|
|
@@ -4206,28 +4585,33 @@ var listAppsTool = async ({ sortBy = "uploaded_at", organizationWide = false, li
|
|
|
4206
4585
|
};
|
|
4207
4586
|
var uploadAppToolDefinition = {
|
|
4208
4587
|
name: "upload_app",
|
|
4209
|
-
description: "Upload a local .apk or .ipa to BrowserStack App Automate. Returns
|
|
4210
|
-
annotations: { title: "Upload App to
|
|
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.",
|
|
4589
|
+
annotations: { title: "Upload App to Cloud Provider", destructiveHint: false },
|
|
4211
4590
|
inputSchema: {
|
|
4591
|
+
provider: z17.enum(["browserstack", "saucelabs"]).describe("Cloud provider"),
|
|
4212
4592
|
path: z17.string().describe("Absolute path to the .apk or .ipa file"),
|
|
4213
|
-
customId: z17.string().optional().describe("Optional custom ID for the app (used to reference it later)")
|
|
4593
|
+
customId: z17.string().optional().describe("Optional custom ID for the app (used to reference it later)"),
|
|
4594
|
+
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)")
|
|
4214
4595
|
}
|
|
4215
4596
|
};
|
|
4216
|
-
var uploadAppTool = async (
|
|
4217
|
-
const
|
|
4218
|
-
|
|
4219
|
-
|
|
4597
|
+
var uploadAppTool = async (args) => {
|
|
4598
|
+
const { provider, path, customId, region = "eu-central-1" } = args;
|
|
4599
|
+
const resolved = getProviderConfig(provider, region);
|
|
4600
|
+
if ("error" in resolved) {
|
|
4601
|
+
return { isError: true, content: [{ type: "text", text: resolved.error }] };
|
|
4220
4602
|
}
|
|
4603
|
+
const { config, auth } = resolved;
|
|
4221
4604
|
if (!existsSync2(path)) {
|
|
4222
4605
|
return { isError: true, content: [{ type: "text", text: `File not found: ${path}` }] };
|
|
4223
4606
|
}
|
|
4607
|
+
const fileName = path.split("/").pop() ?? "app";
|
|
4224
4608
|
try {
|
|
4225
4609
|
const form = new FormData();
|
|
4226
|
-
const
|
|
4227
|
-
const
|
|
4228
|
-
form.append(
|
|
4610
|
+
const fileBuffer = readFileSync(path);
|
|
4611
|
+
const fileBlob = new Blob([fileBuffer], { type: "application/octet-stream" });
|
|
4612
|
+
form.append(config.uploadField, fileBlob, fileName);
|
|
4229
4613
|
if (customId) form.append("custom_id", customId);
|
|
4230
|
-
const res = await fetch(`${
|
|
4614
|
+
const res = await fetch(`${config.apiBase}${config.uploadPath}`, {
|
|
4231
4615
|
method: "POST",
|
|
4232
4616
|
headers: { Authorization: `Basic ${auth}` },
|
|
4233
4617
|
body: form
|
|
@@ -4236,13 +4620,14 @@ var uploadAppTool = async ({ path, customId }) => {
|
|
|
4236
4620
|
const body = await res.text();
|
|
4237
4621
|
return { isError: true, content: [{ type: "text", text: `Upload failed ${res.status}: ${body}` }] };
|
|
4238
4622
|
}
|
|
4239
|
-
const
|
|
4240
|
-
const
|
|
4241
|
-
|
|
4623
|
+
const raw = await res.json();
|
|
4624
|
+
const { appRef } = config.parseUploadResponse(raw, fileName);
|
|
4625
|
+
const customIdNote = customId ? `
|
|
4626
|
+
Custom ID: ${customId}` : "";
|
|
4242
4627
|
return { content: [{ type: "text", text: `Upload successful.
|
|
4243
|
-
App
|
|
4628
|
+
App: ${appRef}${customIdNote}
|
|
4244
4629
|
|
|
4245
|
-
Use
|
|
4630
|
+
Use "${appRef}" as the "app" parameter in start_session with provider: "${provider}".` }] };
|
|
4246
4631
|
} catch (e) {
|
|
4247
4632
|
return { isError: true, content: [{ type: "text", text: `Error uploading app: ${e}` }] };
|
|
4248
4633
|
}
|
|
@@ -4433,6 +4818,7 @@ function createServer() {
|
|
|
4433
4818
|
registerResource(sessionStepsResource);
|
|
4434
4819
|
registerResource(sessionCodeResource);
|
|
4435
4820
|
registerResource(browserstackLocalBinaryResource);
|
|
4821
|
+
registerResource(saucelabsLocalBinaryResource);
|
|
4436
4822
|
registerResource(capabilitiesResource);
|
|
4437
4823
|
registerResource(elementsResource);
|
|
4438
4824
|
registerResource(accessibilityResource);
|