@wdio/mcp 3.5.1 → 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 +451 -63
- package/lib/server.js.map +1 -1
- package/lib/trace.js +2 -1
- 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.5.
|
|
49
|
+
version: "3.5.1",
|
|
50
50
|
description: "MCP server with WebdriverIO for browser and mobile app automation (iOS/Android via Appium)",
|
|
51
51
|
main: "./lib/server.js",
|
|
52
52
|
module: "./lib/server.js",
|
|
@@ -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",
|
|
@@ -2427,6 +2428,71 @@ var browserstackLocalBinaryResource = {
|
|
|
2427
2428
|
}
|
|
2428
2429
|
};
|
|
2429
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
|
+
|
|
2430
2496
|
// src/resources/sessions.resource.ts
|
|
2431
2497
|
import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2432
2498
|
|
|
@@ -2453,6 +2519,7 @@ function generateStep(step, history) {
|
|
|
2453
2519
|
case "start_session": {
|
|
2454
2520
|
const platform2 = p.platform;
|
|
2455
2521
|
const isBrowserStack = "bstack:options" in history.capabilities;
|
|
2522
|
+
const isSauceLabs = "sauce:options" in history.capabilities;
|
|
2456
2523
|
const capJson = indentJson(history.capabilities).split("\n").map((line, i) => i > 0 ? ` ${line}` : line).join("\n");
|
|
2457
2524
|
if (isBrowserStack) {
|
|
2458
2525
|
const nav = platform2 === "browser" && p.navigationUrl ? `
|
|
@@ -2469,6 +2536,22 @@ await browser.url('${escapeStr(p.navigationUrl)}');` : "";
|
|
|
2469
2536
|
`});${nav}`
|
|
2470
2537
|
].join("\n");
|
|
2471
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
|
+
}
|
|
2472
2555
|
if (platform2 === "browser") {
|
|
2473
2556
|
const nav = p.navigationUrl ? `
|
|
2474
2557
|
await browser.url('${escapeStr(p.navigationUrl)}');` : "";
|
|
@@ -2531,8 +2614,10 @@ function bsStatusUpdateLines(sessionType) {
|
|
|
2531
2614
|
}
|
|
2532
2615
|
function generateCode(history) {
|
|
2533
2616
|
const bstackOptions = history.capabilities["bstack:options"];
|
|
2617
|
+
const sauceOptions = history.capabilities["sauce:options"];
|
|
2534
2618
|
const isBrowserStack = bstackOptions !== void 0;
|
|
2535
|
-
const
|
|
2619
|
+
const isSauceLabs = sauceOptions !== void 0;
|
|
2620
|
+
const usesLocalTunnel = isBrowserStack ? bstackOptions?.local === true : sauceOptions?.tunnelName !== void 0;
|
|
2536
2621
|
const steps = history.steps.map((step) => generateStep(step, history)).join("\n").split("\n").map((line) => ` ${line}`).join("\n");
|
|
2537
2622
|
if (isBrowserStack) {
|
|
2538
2623
|
const bsSteps = steps.replace(/const browser = await remote\(/g, "browser = await remote(");
|
|
@@ -2543,7 +2628,7 @@ function generateCode(history) {
|
|
|
2543
2628
|
${statusUpdate}
|
|
2544
2629
|
await browser.deleteSession();
|
|
2545
2630
|
}`;
|
|
2546
|
-
if (
|
|
2631
|
+
if (usesLocalTunnel) {
|
|
2547
2632
|
const tunnelSetup = [
|
|
2548
2633
|
"",
|
|
2549
2634
|
"const tunnel = new BrowserstackTunnel();",
|
|
@@ -2579,6 +2664,64 @@ ${statusUpdate}
|
|
|
2579
2664
|
"}"
|
|
2580
2665
|
].join("\n");
|
|
2581
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
|
+
}
|
|
2582
2725
|
return `import { remote } from 'webdriverio';
|
|
2583
2726
|
|
|
2584
2727
|
try {
|
|
@@ -3276,7 +3419,7 @@ import { z as z14 } from "zod";
|
|
|
3276
3419
|
// src/session/lifecycle.ts
|
|
3277
3420
|
init_state();
|
|
3278
3421
|
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
3279
|
-
import { join as
|
|
3422
|
+
import { join as join4 } from "path";
|
|
3280
3423
|
|
|
3281
3424
|
// src/providers/local-browser.provider.ts
|
|
3282
3425
|
var LocalBrowserProvider = class {
|
|
@@ -3531,6 +3674,13 @@ import { promisify } from "util";
|
|
|
3531
3674
|
import { tmpdir as tmpdir2 } from "os";
|
|
3532
3675
|
import { join as join2 } from "path";
|
|
3533
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
|
|
3534
3684
|
var BrowserStackProvider = class {
|
|
3535
3685
|
name = "browserstack";
|
|
3536
3686
|
getConnectionConfig(options) {
|
|
@@ -3548,7 +3698,7 @@ var BrowserStackProvider = class {
|
|
|
3548
3698
|
buildCapabilities(options) {
|
|
3549
3699
|
const platform2 = options.platform;
|
|
3550
3700
|
const userCapabilities = options.capabilities ?? {};
|
|
3551
|
-
const browserstackLocal = options.browserstackLocal;
|
|
3701
|
+
const browserstackLocal = options.tunnel ?? options.browserstackLocal;
|
|
3552
3702
|
if (platform2 === "browser") {
|
|
3553
3703
|
const bstackOptions2 = {
|
|
3554
3704
|
browserVersion: options.browserVersion ?? "latest"
|
|
@@ -3626,7 +3776,7 @@ var BrowserStackProvider = class {
|
|
|
3626
3776
|
const key = process.env.BROWSERSTACK_ACCESS_KEY;
|
|
3627
3777
|
if (!user || !key) return;
|
|
3628
3778
|
const baseUrl = sessionType === "browser" ? "https://api.browserstack.com/automate/sessions" : "https://api-cloud.browserstack.com/app-automate/sessions";
|
|
3629
|
-
const auth =
|
|
3779
|
+
const auth = basicAuth(user, key);
|
|
3630
3780
|
const body = { status: result.status, ...result.reason ? { reason: result.reason } : {} };
|
|
3631
3781
|
await fetch(`${baseUrl}/${sessionId}.json`, {
|
|
3632
3782
|
method: "PUT",
|
|
@@ -3637,11 +3787,153 @@ var BrowserStackProvider = class {
|
|
|
3637
3787
|
};
|
|
3638
3788
|
var browserStackProvider = new BrowserStackProvider();
|
|
3639
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
|
+
|
|
3640
3926
|
// src/providers/registry.ts
|
|
3641
|
-
|
|
3642
|
-
|
|
3927
|
+
var providers = /* @__PURE__ */ new Map([
|
|
3928
|
+
["browserstack", browserStackProvider],
|
|
3929
|
+
["saucelabs", sauceLabsProvider]
|
|
3930
|
+
]);
|
|
3931
|
+
function getDefaultProvider(platform2) {
|
|
3643
3932
|
return platform2 === "browser" ? localBrowserProvider : localAppiumProvider;
|
|
3644
3933
|
}
|
|
3934
|
+
function getProvider(providerName, platform2) {
|
|
3935
|
+
return providers.get(providerName) ?? getDefaultProvider(platform2);
|
|
3936
|
+
}
|
|
3645
3937
|
|
|
3646
3938
|
// src/trace/zip-writer.ts
|
|
3647
3939
|
import yazl from "yazl";
|
|
@@ -3671,10 +3963,10 @@ async function finalizeTrace(sessionId, browser) {
|
|
|
3671
3963
|
if (!traceSession) return;
|
|
3672
3964
|
try {
|
|
3673
3965
|
await traceSession.screenshotChain;
|
|
3674
|
-
const traceDir =
|
|
3966
|
+
const traceDir = join4(process.cwd(), ".trace");
|
|
3675
3967
|
mkdirSync2(traceDir, { recursive: true });
|
|
3676
3968
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
3677
|
-
const outPath =
|
|
3969
|
+
const outPath = join4(traceDir, `${timestamp}-${sessionId.slice(0, 8)}.zip`);
|
|
3678
3970
|
const zipBuffer = await buildTraceZip(traceSession);
|
|
3679
3971
|
writeFileSync2(outPath, zipBuffer);
|
|
3680
3972
|
console.error(`[TRACE] Saved to ${outPath}`);
|
|
@@ -3726,11 +4018,16 @@ function registerSession(sessionId, browser, metadata, historyEntry) {
|
|
|
3726
4018
|
if (oldMetadata?.provider) {
|
|
3727
4019
|
const oldHistory = state2.sessionHistory.get(oldSessionId);
|
|
3728
4020
|
const provider = getProvider(oldMetadata.provider, oldMetadata.type);
|
|
3729
|
-
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(() => {
|
|
3730
4022
|
});
|
|
3731
4023
|
}
|
|
3732
4024
|
await oldBrowser.deleteSession().catch(() => {
|
|
3733
4025
|
});
|
|
4026
|
+
if (oldMetadata?.provider && oldMetadata?.tunnelHandle) {
|
|
4027
|
+
const provider = getProvider(oldMetadata.provider, oldMetadata.type);
|
|
4028
|
+
await provider.stopTunnel?.(oldMetadata.tunnelHandle).catch(() => {
|
|
4029
|
+
});
|
|
4030
|
+
}
|
|
3734
4031
|
})();
|
|
3735
4032
|
state2.browsers.delete(oldSessionId);
|
|
3736
4033
|
state2.sessionMetadata.delete(oldSessionId);
|
|
@@ -3753,12 +4050,20 @@ async function closeSession(sessionId, detach, isAttached, force) {
|
|
|
3753
4050
|
if (metadata?.provider) {
|
|
3754
4051
|
try {
|
|
3755
4052
|
const provider = getProvider(metadata.provider, metadata.type);
|
|
3756
|
-
await provider.onSessionClose?.(sessionId, metadata.type, getSessionResult(history), metadata.tunnelHandle);
|
|
4053
|
+
await provider.onSessionClose?.(sessionId, metadata.type, getSessionResult(history), metadata.tunnelHandle, browser, metadata.region);
|
|
3757
4054
|
} catch (e) {
|
|
3758
4055
|
console.error("[WARN] Failed to run provider onSessionClose:", e);
|
|
3759
4056
|
}
|
|
3760
4057
|
}
|
|
3761
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
|
+
}
|
|
3762
4067
|
}
|
|
3763
4068
|
state2.browsers.delete(sessionId);
|
|
3764
4069
|
state2.sessionMetadata.delete(sessionId);
|
|
@@ -3776,18 +4081,18 @@ var startSessionToolDefinition = {
|
|
|
3776
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.',
|
|
3777
4082
|
annotations: { title: "Start Session", destructiveHint: false },
|
|
3778
4083
|
inputSchema: {
|
|
3779
|
-
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)"),
|
|
3780
4085
|
platform: platformEnum.describe("Session platform type"),
|
|
3781
4086
|
browser: browserEnum.optional().describe("Browser to launch (required for browser platform)"),
|
|
3782
|
-
browserVersion: z14.string().optional().describe("Browser version (
|
|
3783
|
-
os: z14.string().optional().describe('Operating system (
|
|
3784
|
-
osVersion: z14.string().optional().describe('OS version (
|
|
3785
|
-
app: z14.string().optional().describe("
|
|
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"),
|
|
3786
4091
|
reporting: z14.object({
|
|
3787
4092
|
project: z14.string().optional(),
|
|
3788
4093
|
build: z14.string().optional(),
|
|
3789
4094
|
session: z14.string().optional()
|
|
3790
|
-
}).optional().describe("
|
|
4095
|
+
}).optional().describe("Cloud provider reporting labels (project, build, session)"),
|
|
3791
4096
|
headless: coerceBoolean.optional().default(true).describe("Run browser in headless mode (default: true)"),
|
|
3792
4097
|
windowWidth: z14.number().min(400).max(3840).optional().default(1920).describe("Browser window width"),
|
|
3793
4098
|
windowHeight: z14.number().min(400).max(2160).optional().default(1080).describe("Browser window height"),
|
|
@@ -3815,7 +4120,11 @@ var startSessionToolDefinition = {
|
|
|
3815
4120
|
path: z14.string().optional(),
|
|
3816
4121
|
protocol: z14.string().optional()
|
|
3817
4122
|
}).optional().describe("Appium server connection (local provider only)"),
|
|
3818
|
-
|
|
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.'),
|
|
3819
4128
|
navigationUrl: z14.string().optional().describe("URL to navigate to after starting"),
|
|
3820
4129
|
capabilities: z14.record(z14.string(), z14.unknown()).optional().describe("Additional capabilities to merge")
|
|
3821
4130
|
}
|
|
@@ -3894,15 +4203,19 @@ async function startBrowserSession(args) {
|
|
|
3894
4203
|
const effectiveHeadless = headless && headlessSupported;
|
|
3895
4204
|
const provider = getProvider(args.provider ?? "local", "browser");
|
|
3896
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;
|
|
3897
4209
|
const mergedCapabilities = provider.buildCapabilities({
|
|
3898
4210
|
...args,
|
|
3899
4211
|
browser,
|
|
3900
4212
|
headless,
|
|
3901
4213
|
windowWidth,
|
|
3902
4214
|
windowHeight,
|
|
3903
|
-
capabilities: userCapabilities
|
|
4215
|
+
capabilities: userCapabilities,
|
|
4216
|
+
tunnelName
|
|
3904
4217
|
});
|
|
3905
|
-
const tunnelHandle =
|
|
4218
|
+
const tunnelHandle = tunnelEnabled ? await provider.startTunnel?.({ ...args, tunnelName }) : void 0;
|
|
3906
4219
|
const wdioBrowser = await remote({ ...connectionConfig, capabilities: mergedCapabilities });
|
|
3907
4220
|
const { sessionId } = wdioBrowser;
|
|
3908
4221
|
const sessionMetadata = {
|
|
@@ -3910,6 +4223,8 @@ async function startBrowserSession(args) {
|
|
|
3910
4223
|
capabilities: mergedCapabilities,
|
|
3911
4224
|
isAttached: false,
|
|
3912
4225
|
provider: args.provider ?? "local",
|
|
4226
|
+
region: args.region,
|
|
4227
|
+
tunnelName,
|
|
3913
4228
|
tunnelHandle,
|
|
3914
4229
|
trace: args.trace ?? false
|
|
3915
4230
|
};
|
|
@@ -3958,8 +4273,11 @@ async function startMobileSession(args) {
|
|
|
3958
4273
|
}
|
|
3959
4274
|
const provider = getProvider(args.provider ?? "local", args.platform);
|
|
3960
4275
|
const serverConfig = provider.getConnectionConfig(args);
|
|
3961
|
-
const
|
|
3962
|
-
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;
|
|
3963
4281
|
const browser = await remote({ ...serverConfig, capabilities: mergedCapabilities });
|
|
3964
4282
|
const { sessionId } = browser;
|
|
3965
4283
|
const shouldAutoDetach = provider.shouldAutoDetach(args);
|
|
@@ -3969,6 +4287,8 @@ async function startMobileSession(args) {
|
|
|
3969
4287
|
capabilities: mergedCapabilities,
|
|
3970
4288
|
isAttached: shouldAutoDetach,
|
|
3971
4289
|
provider: args.provider ?? "local",
|
|
4290
|
+
region: args.region,
|
|
4291
|
+
tunnelName,
|
|
3972
4292
|
tunnelHandle,
|
|
3973
4293
|
trace: args.trace ?? false
|
|
3974
4294
|
};
|
|
@@ -4152,51 +4472,112 @@ var switchFrameTool = async ({
|
|
|
4152
4472
|
}
|
|
4153
4473
|
};
|
|
4154
4474
|
|
|
4155
|
-
// src/tools/
|
|
4156
|
-
import { existsSync as existsSync2,
|
|
4475
|
+
// src/tools/cloud-provider.tool.ts
|
|
4476
|
+
import { existsSync as existsSync2, readFileSync } from "fs";
|
|
4157
4477
|
import { z as z17 } from "zod";
|
|
4158
|
-
var BS_API = "https://api-cloud.browserstack.com";
|
|
4159
|
-
function getAuth() {
|
|
4160
|
-
const user = process.env.BROWSERSTACK_USERNAME;
|
|
4161
|
-
const key = process.env.BROWSERSTACK_ACCESS_KEY;
|
|
4162
|
-
if (!user || !key) return null;
|
|
4163
|
-
return Buffer.from(`${user}:${key}`).toString("base64");
|
|
4164
|
-
}
|
|
4165
4478
|
function formatAppList(apps) {
|
|
4166
4479
|
if (apps.length === 0) return "No apps found.";
|
|
4167
4480
|
return apps.map((a) => {
|
|
4168
|
-
const id = a.
|
|
4169
|
-
|
|
4481
|
+
const id = a.customId ? ` [${a.customId}]` : "";
|
|
4482
|
+
const ts = a.uploadedAt ?? "unknown";
|
|
4483
|
+
return `${a.name}${id} \u2014 ${a.ref} (${ts})`;
|
|
4170
4484
|
}).join("\n");
|
|
4171
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
|
+
}
|
|
4172
4547
|
var listAppsToolDefinition = {
|
|
4173
4548
|
name: "list_apps",
|
|
4174
|
-
description: "List apps uploaded to BrowserStack App Automate. Reads
|
|
4175
|
-
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 },
|
|
4176
4551
|
inputSchema: {
|
|
4552
|
+
provider: z17.enum(["browserstack", "saucelabs"]).describe("Cloud provider"),
|
|
4177
4553
|
sortBy: z17.enum(["app_name", "uploaded_at"]).optional().default("uploaded_at").describe("Sort order for results"),
|
|
4178
|
-
organizationWide: coerceBoolean.optional().default(false).describe("List apps uploaded by all users in the organization
|
|
4179
|
-
limit: z17.number().int().min(1).optional().default(20).describe("Maximum number of apps to return (only applies when organizationWide is true, default 20)")
|
|
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)")
|
|
4180
4557
|
}
|
|
4181
4558
|
};
|
|
4182
|
-
var listAppsTool = async (
|
|
4183
|
-
const
|
|
4184
|
-
|
|
4185
|
-
|
|
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 }] };
|
|
4186
4564
|
}
|
|
4565
|
+
const { config, auth } = resolved;
|
|
4187
4566
|
try {
|
|
4188
|
-
let url = `${
|
|
4189
|
-
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
|
+
}
|
|
4190
4571
|
const res = await fetch(url, {
|
|
4191
4572
|
headers: { Authorization: `Basic ${auth}` }
|
|
4192
4573
|
});
|
|
4193
4574
|
if (!res.ok) {
|
|
4194
4575
|
const body = await res.text();
|
|
4195
|
-
return { isError: true, content: [{ type: "text", text:
|
|
4576
|
+
return { isError: true, content: [{ type: "text", text: `${config.name} API error ${res.status}: ${body}` }] };
|
|
4196
4577
|
}
|
|
4197
4578
|
const raw = await res.json();
|
|
4198
|
-
let apps =
|
|
4199
|
-
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 ?? ""));
|
|
4200
4581
|
return { content: [{ type: "text", text: formatAppList(apps) }] };
|
|
4201
4582
|
} catch (e) {
|
|
4202
4583
|
return { isError: true, content: [{ type: "text", text: `Error listing apps: ${e}` }] };
|
|
@@ -4204,28 +4585,33 @@ var listAppsTool = async ({ sortBy = "uploaded_at", organizationWide = false, li
|
|
|
4204
4585
|
};
|
|
4205
4586
|
var uploadAppToolDefinition = {
|
|
4206
4587
|
name: "upload_app",
|
|
4207
|
-
description: "Upload a local .apk or .ipa to BrowserStack App Automate. Returns
|
|
4208
|
-
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 },
|
|
4209
4590
|
inputSchema: {
|
|
4591
|
+
provider: z17.enum(["browserstack", "saucelabs"]).describe("Cloud provider"),
|
|
4210
4592
|
path: z17.string().describe("Absolute path to the .apk or .ipa file"),
|
|
4211
|
-
customId: z17.string().optional().describe("Optional custom ID for the app (used to reference it later)")
|
|
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)")
|
|
4212
4595
|
}
|
|
4213
4596
|
};
|
|
4214
|
-
var uploadAppTool = async (
|
|
4215
|
-
const
|
|
4216
|
-
|
|
4217
|
-
|
|
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 }] };
|
|
4218
4602
|
}
|
|
4603
|
+
const { config, auth } = resolved;
|
|
4219
4604
|
if (!existsSync2(path)) {
|
|
4220
4605
|
return { isError: true, content: [{ type: "text", text: `File not found: ${path}` }] };
|
|
4221
4606
|
}
|
|
4607
|
+
const fileName = path.split("/").pop() ?? "app";
|
|
4222
4608
|
try {
|
|
4223
4609
|
const form = new FormData();
|
|
4224
|
-
const
|
|
4225
|
-
const
|
|
4226
|
-
form.append(
|
|
4610
|
+
const fileBuffer = readFileSync(path);
|
|
4611
|
+
const fileBlob = new Blob([fileBuffer], { type: "application/octet-stream" });
|
|
4612
|
+
form.append(config.uploadField, fileBlob, fileName);
|
|
4227
4613
|
if (customId) form.append("custom_id", customId);
|
|
4228
|
-
const res = await fetch(`${
|
|
4614
|
+
const res = await fetch(`${config.apiBase}${config.uploadPath}`, {
|
|
4229
4615
|
method: "POST",
|
|
4230
4616
|
headers: { Authorization: `Basic ${auth}` },
|
|
4231
4617
|
body: form
|
|
@@ -4234,13 +4620,14 @@ var uploadAppTool = async ({ path, customId }) => {
|
|
|
4234
4620
|
const body = await res.text();
|
|
4235
4621
|
return { isError: true, content: [{ type: "text", text: `Upload failed ${res.status}: ${body}` }] };
|
|
4236
4622
|
}
|
|
4237
|
-
const
|
|
4238
|
-
const
|
|
4239
|
-
|
|
4623
|
+
const raw = await res.json();
|
|
4624
|
+
const { appRef } = config.parseUploadResponse(raw, fileName);
|
|
4625
|
+
const customIdNote = customId ? `
|
|
4626
|
+
Custom ID: ${customId}` : "";
|
|
4240
4627
|
return { content: [{ type: "text", text: `Upload successful.
|
|
4241
|
-
App
|
|
4628
|
+
App: ${appRef}${customIdNote}
|
|
4242
4629
|
|
|
4243
|
-
Use
|
|
4630
|
+
Use "${appRef}" as the "app" parameter in start_session with provider: "${provider}".` }] };
|
|
4244
4631
|
} catch (e) {
|
|
4245
4632
|
return { isError: true, content: [{ type: "text", text: `Error uploading app: ${e}` }] };
|
|
4246
4633
|
}
|
|
@@ -4431,6 +4818,7 @@ function createServer() {
|
|
|
4431
4818
|
registerResource(sessionStepsResource);
|
|
4432
4819
|
registerResource(sessionCodeResource);
|
|
4433
4820
|
registerResource(browserstackLocalBinaryResource);
|
|
4821
|
+
registerResource(saucelabsLocalBinaryResource);
|
|
4434
4822
|
registerResource(capabilitiesResource);
|
|
4435
4823
|
registerResource(elementsResource);
|
|
4436
4824
|
registerResource(accessibilityResource);
|