@wdio/mcp 2.5.0 → 2.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 +293 -138
- package/lib/server.js.map +1 -1
- package/lib/snapshot.js +1 -1
- package/lib/snapshot.js.map +1 -1
- package/package.json +2 -2
package/lib/server.js
CHANGED
|
@@ -1,7 +1,83 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
// package.json
|
|
4
|
+
var package_default = {
|
|
5
|
+
name: "@wdio/mcp",
|
|
6
|
+
author: "Vince Graics",
|
|
7
|
+
repository: {
|
|
8
|
+
type: "git",
|
|
9
|
+
url: "git://github.com/webdriverio/mcp.git"
|
|
10
|
+
},
|
|
11
|
+
version: "2.5.1",
|
|
12
|
+
description: "MCP server with WebdriverIO for browser and mobile app automation (iOS/Android via Appium)",
|
|
13
|
+
main: "./lib/server.js",
|
|
14
|
+
module: "./lib/server.js",
|
|
15
|
+
types: "./lib/server.d.ts",
|
|
16
|
+
exports: {
|
|
17
|
+
".": {
|
|
18
|
+
import: "./lib/server.js",
|
|
19
|
+
types: "./lib/server.d.ts"
|
|
20
|
+
},
|
|
21
|
+
"./snapshot": {
|
|
22
|
+
import: "./lib/snapshot.js",
|
|
23
|
+
types: "./lib/snapshot.d.ts"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
bin: {
|
|
27
|
+
"wdio-mcp": "lib/server.js"
|
|
28
|
+
},
|
|
29
|
+
license: "MIT",
|
|
30
|
+
publishConfig: {
|
|
31
|
+
access: "public"
|
|
32
|
+
},
|
|
33
|
+
type: "module",
|
|
34
|
+
files: [
|
|
35
|
+
"lib",
|
|
36
|
+
"README.md"
|
|
37
|
+
],
|
|
38
|
+
scripts: {
|
|
39
|
+
prebundle: "rimraf lib --glob ./*.tgz",
|
|
40
|
+
bundle: "tsup && shx chmod +x lib/server.js",
|
|
41
|
+
postbundle: "npm pack",
|
|
42
|
+
lint: "eslint src/ --fix && tsc --noEmit",
|
|
43
|
+
"lint:tests": "eslint tests/ --fix && tsc -p tsconfig.test.json --noEmit",
|
|
44
|
+
start: "node lib/server.js",
|
|
45
|
+
dev: "tsx --watch src/server.ts",
|
|
46
|
+
prepare: "husky",
|
|
47
|
+
test: "vitest run"
|
|
48
|
+
},
|
|
49
|
+
dependencies: {
|
|
50
|
+
"@modelcontextprotocol/sdk": "1.27",
|
|
51
|
+
"@toon-format/toon": "^2.1.0",
|
|
52
|
+
"@wdio/protocols": "^9.16.2",
|
|
53
|
+
"@xmldom/xmldom": "^0.8.11",
|
|
54
|
+
"puppeteer-core": "^24.35.0",
|
|
55
|
+
sharp: "^0.34.5",
|
|
56
|
+
webdriverio: "9.24",
|
|
57
|
+
xpath: "^0.0.34",
|
|
58
|
+
zod: "^4.3.5"
|
|
59
|
+
},
|
|
60
|
+
devDependencies: {
|
|
61
|
+
"@release-it/conventional-changelog": "^10.0.4",
|
|
62
|
+
"@types/node": "^20.11.0",
|
|
63
|
+
"@wdio/eslint": "^0.1.3",
|
|
64
|
+
"@wdio/types": "^9.20.0",
|
|
65
|
+
eslint: "^9.39.2",
|
|
66
|
+
"happy-dom": "^20.7.0",
|
|
67
|
+
husky: "^9.1.7",
|
|
68
|
+
"release-it": "^19.2.3",
|
|
69
|
+
rimraf: "^6.1.2",
|
|
70
|
+
shx: "^0.4.0",
|
|
71
|
+
tsup: "^8.5.1",
|
|
72
|
+
tsx: "^4.21.0",
|
|
73
|
+
typescript: "5.9",
|
|
74
|
+
vitest: "^4.0.18"
|
|
75
|
+
},
|
|
76
|
+
packageManager: "pnpm@10.32.1"
|
|
77
|
+
};
|
|
78
|
+
|
|
3
79
|
// src/server.ts
|
|
4
|
-
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
80
|
+
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
81
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
82
|
|
|
7
83
|
// src/tools/browser.tool.ts
|
|
@@ -420,7 +496,7 @@ var getState = () => {
|
|
|
420
496
|
var startAppTool = async (args) => {
|
|
421
497
|
try {
|
|
422
498
|
const {
|
|
423
|
-
platform,
|
|
499
|
+
platform: platform2,
|
|
424
500
|
appPath,
|
|
425
501
|
deviceName,
|
|
426
502
|
platformVersion,
|
|
@@ -451,7 +527,7 @@ var startAppTool = async (args) => {
|
|
|
451
527
|
port: appiumPort,
|
|
452
528
|
path: appiumPath
|
|
453
529
|
});
|
|
454
|
-
const capabilities =
|
|
530
|
+
const capabilities = platform2 === "iOS" ? buildIOSCapabilities(appPath, {
|
|
455
531
|
deviceName,
|
|
456
532
|
platformVersion,
|
|
457
533
|
automationName: automationName || "XCUITest",
|
|
@@ -495,7 +571,7 @@ var startAppTool = async (args) => {
|
|
|
495
571
|
const state2 = getState();
|
|
496
572
|
state2.browsers.set(sessionId, browser);
|
|
497
573
|
state2.sessionMetadata.set(sessionId, {
|
|
498
|
-
type:
|
|
574
|
+
type: platform2.toLowerCase(),
|
|
499
575
|
capabilities: mergedCapabilities,
|
|
500
576
|
isAttached: shouldAutoDetach
|
|
501
577
|
});
|
|
@@ -515,7 +591,7 @@ var startAppTool = async (args) => {
|
|
|
515
591
|
}
|
|
516
592
|
state2.sessionHistory.set(sessionId, {
|
|
517
593
|
sessionId,
|
|
518
|
-
type:
|
|
594
|
+
type: platform2.toLowerCase(),
|
|
519
595
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
520
596
|
capabilities: mergedCapabilities,
|
|
521
597
|
appiumConfig: { hostname: serverConfig.hostname, port: serverConfig.port, path: serverConfig.path },
|
|
@@ -529,7 +605,7 @@ App: ${appPath}` : "\nApp: (connected to running app)";
|
|
|
529
605
|
content: [
|
|
530
606
|
{
|
|
531
607
|
type: "text",
|
|
532
|
-
text: `${
|
|
608
|
+
text: `${platform2} app session started with sessionId: ${sessionId}
|
|
533
609
|
Device: ${deviceName}${appInfo}
|
|
534
610
|
Appium Server: ${serverConfig.hostname}:${serverConfig.port}${serverConfig.path}${detachNote}`
|
|
535
611
|
}
|
|
@@ -1108,16 +1184,16 @@ function isInteractableElement(element, isNative, automationName) {
|
|
|
1108
1184
|
}
|
|
1109
1185
|
return false;
|
|
1110
1186
|
}
|
|
1111
|
-
function isLayoutContainer(element,
|
|
1112
|
-
const containerList =
|
|
1187
|
+
function isLayoutContainer(element, platform2) {
|
|
1188
|
+
const containerList = platform2 === "android" ? ANDROID_LAYOUT_CONTAINERS : IOS_LAYOUT_CONTAINERS;
|
|
1113
1189
|
return matchesTagList(element.tagName, containerList);
|
|
1114
1190
|
}
|
|
1115
|
-
function hasMeaningfulContent(element,
|
|
1191
|
+
function hasMeaningfulContent(element, platform2) {
|
|
1116
1192
|
const attrs = element.attributes;
|
|
1117
1193
|
if (attrs.text && attrs.text.trim() !== "" && attrs.text !== "null") {
|
|
1118
1194
|
return true;
|
|
1119
1195
|
}
|
|
1120
|
-
if (
|
|
1196
|
+
if (platform2 === "android") {
|
|
1121
1197
|
if (attrs["content-desc"] && attrs["content-desc"].trim() !== "" && attrs["content-desc"] !== "null") {
|
|
1122
1198
|
return true;
|
|
1123
1199
|
}
|
|
@@ -1164,8 +1240,8 @@ function shouldIncludeElement(element, filters, isNative, automationName) {
|
|
|
1164
1240
|
}
|
|
1165
1241
|
return true;
|
|
1166
1242
|
}
|
|
1167
|
-
function getDefaultFilters(
|
|
1168
|
-
const layoutContainers =
|
|
1243
|
+
function getDefaultFilters(platform2, includeContainers = false) {
|
|
1244
|
+
const layoutContainers = platform2 === "android" ? ANDROID_LAYOUT_CONTAINERS : IOS_LAYOUT_CONTAINERS;
|
|
1169
1245
|
return {
|
|
1170
1246
|
excludeTagNames: includeContainers ? ["hierarchy"] : ["hierarchy", ...layoutContainers],
|
|
1171
1247
|
fetchableOnly: !includeContainers,
|
|
@@ -1179,7 +1255,7 @@ function isValidValue(value) {
|
|
|
1179
1255
|
return value !== void 0 && value !== null && value !== "null" && value.trim() !== "";
|
|
1180
1256
|
}
|
|
1181
1257
|
function escapeText(text) {
|
|
1182
|
-
return text.replace(/"/g, '\\"').replace(/\n/g, "\\n");
|
|
1258
|
+
return text.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n");
|
|
1183
1259
|
}
|
|
1184
1260
|
function escapeXPathValue(value) {
|
|
1185
1261
|
if (!value.includes("'")) {
|
|
@@ -1535,8 +1611,8 @@ function locatorsToObject(locators) {
|
|
|
1535
1611
|
}
|
|
1536
1612
|
|
|
1537
1613
|
// src/locators/index.ts
|
|
1538
|
-
function parseBounds(element,
|
|
1539
|
-
return
|
|
1614
|
+
function parseBounds(element, platform2) {
|
|
1615
|
+
return platform2 === "android" ? parseAndroidBounds(element.attributes.bounds || "") : parseIOSBounds(element.attributes);
|
|
1540
1616
|
}
|
|
1541
1617
|
function isWithinViewport(bounds, viewport) {
|
|
1542
1618
|
return bounds.x >= 0 && bounds.y >= 0 && bounds.width > 0 && bounds.height > 0 && bounds.x + bounds.width <= viewport.width && bounds.y + bounds.height <= viewport.height;
|
|
@@ -1675,16 +1751,16 @@ async function getViewportSize(browser) {
|
|
|
1675
1751
|
return { width: 9999, height: 9999 };
|
|
1676
1752
|
}
|
|
1677
1753
|
}
|
|
1678
|
-
async function getMobileVisibleElements(browser,
|
|
1754
|
+
async function getMobileVisibleElements(browser, platform2, options = {}) {
|
|
1679
1755
|
const { includeContainers = false, includeBounds = false, filterOptions } = options;
|
|
1680
1756
|
const viewportSize = await getViewportSize(browser);
|
|
1681
1757
|
const pageSource = await browser.getPageSource();
|
|
1682
1758
|
const filters = {
|
|
1683
|
-
...getDefaultFilters(
|
|
1759
|
+
...getDefaultFilters(platform2, includeContainers),
|
|
1684
1760
|
...filterOptions
|
|
1685
1761
|
};
|
|
1686
1762
|
const elements = generateAllElementLocators(pageSource, {
|
|
1687
|
-
platform,
|
|
1763
|
+
platform: platform2,
|
|
1688
1764
|
viewportSize,
|
|
1689
1765
|
filters
|
|
1690
1766
|
});
|
|
@@ -1717,8 +1793,8 @@ var getVisibleElementsTool = async (args) => {
|
|
|
1717
1793
|
} = args || {};
|
|
1718
1794
|
let elements;
|
|
1719
1795
|
if (browser.isAndroid || browser.isIOS) {
|
|
1720
|
-
const
|
|
1721
|
-
elements = await getMobileVisibleElements(browser,
|
|
1796
|
+
const platform2 = browser.isAndroid ? "android" : "ios";
|
|
1797
|
+
elements = await getMobileVisibleElements(browser, platform2, { includeContainers, includeBounds });
|
|
1722
1798
|
} else {
|
|
1723
1799
|
elements = await getInteractableBrowserElements(browser, { includeBounds });
|
|
1724
1800
|
}
|
|
@@ -2644,57 +2720,78 @@ var attachBrowserToolDefinition = {
|
|
|
2644
2720
|
name: "attach_browser",
|
|
2645
2721
|
description: `Attach to a Chrome instance already running with --remote-debugging-port.
|
|
2646
2722
|
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
macOS \u2014 with real profile (preserves extensions, cookies, logins):
|
|
2650
|
-
pkill -x "Google Chrome" && sleep 1
|
|
2651
|
-
/Applications/Google Chrome.app/Contents/MacOS/Google Chrome --remote-debugging-port=9222 --user-data-dir="$HOME/Library/Application Support/Google/Chrome" --profile-directory=Default &
|
|
2652
|
-
|
|
2653
|
-
macOS \u2014 with fresh profile (lightweight, no extensions):
|
|
2654
|
-
pkill -x "Google Chrome" && sleep 1
|
|
2655
|
-
/Applications/Google Chrome.app/Contents/MacOS/Google Chrome --remote-debugging-port=9222 --user-data-dir=/tmp/chrome-debug &
|
|
2656
|
-
|
|
2657
|
-
Linux \u2014 with real profile:
|
|
2658
|
-
google-chrome --remote-debugging-port=9222 --user-data-dir="$HOME/.config/google-chrome" --profile-directory=Default &
|
|
2659
|
-
|
|
2660
|
-
Linux \u2014 with fresh profile:
|
|
2661
|
-
google-chrome --remote-debugging-port=9222 --user-data-dir=/tmp/chrome-debug &
|
|
2662
|
-
|
|
2663
|
-
Verify Chrome is ready: curl http://localhost:9222/json/version
|
|
2664
|
-
|
|
2665
|
-
Then call attach_browser() to hand control to the AI. All other tools (navigate, click, get_visible_elements, etc.) will work on the attached session. Use close_session() to detach without closing Chrome.`,
|
|
2723
|
+
Use launch_chrome() first to prepare and launch Chrome with remote debugging enabled.`,
|
|
2666
2724
|
inputSchema: {
|
|
2667
2725
|
port: z16.number().default(9222).describe("Chrome remote debugging port (default: 9222)"),
|
|
2668
2726
|
host: z16.string().default("localhost").describe("Host where Chrome is running (default: localhost)"),
|
|
2669
|
-
userDataDir: z16.string().default("/tmp/chrome-debug").describe('Chrome user data directory \u2014 must match the --user-data-dir used when launching Chrome. Use your real profile path (e.g. "$HOME/Library/Application Support/Google/Chrome") to preserve extensions and logins, or /tmp/chrome-debug for a fresh profile (default: /tmp/chrome-debug)'),
|
|
2670
2727
|
navigationUrl: z16.string().optional().describe("URL to navigate to immediately after attaching")
|
|
2671
2728
|
}
|
|
2672
2729
|
};
|
|
2673
|
-
async function
|
|
2730
|
+
async function closeStaleMappers(host, port) {
|
|
2674
2731
|
try {
|
|
2675
2732
|
const res = await fetch(`http://${host}:${port}/json`);
|
|
2676
|
-
const
|
|
2677
|
-
const
|
|
2678
|
-
|
|
2733
|
+
const targets = await res.json();
|
|
2734
|
+
const mappers = targets.filter((t) => t.title?.includes("BiDi"));
|
|
2735
|
+
await Promise.all(mappers.map((t) => fetch(`http://${host}:${port}/json/close/${t.id}`)));
|
|
2736
|
+
const pages = targets.filter((t) => t.type === "page" && !t.title?.includes("BiDi"));
|
|
2737
|
+
return { activeTabUrl: pages[0]?.url, allTabUrls: pages.map((t) => t.url) };
|
|
2679
2738
|
} catch {
|
|
2680
|
-
return
|
|
2739
|
+
return { activeTabUrl: void 0, allTabUrls: [] };
|
|
2740
|
+
}
|
|
2741
|
+
}
|
|
2742
|
+
async function restoreAndSwitchToActiveTab(browser, activeTabUrl, allTabUrls) {
|
|
2743
|
+
const handles = await browser.getWindowHandles();
|
|
2744
|
+
const currentUrls = [];
|
|
2745
|
+
for (const handle of handles) {
|
|
2746
|
+
await browser.switchToWindow(handle);
|
|
2747
|
+
currentUrls.push(await browser.getUrl());
|
|
2748
|
+
}
|
|
2749
|
+
const missingUrls = allTabUrls.filter((u) => !currentUrls.includes(u));
|
|
2750
|
+
let missingIdx = 0;
|
|
2751
|
+
for (let i = 0; i < handles.length; i++) {
|
|
2752
|
+
if (currentUrls[i] === "about:blank" && missingIdx < missingUrls.length) {
|
|
2753
|
+
await browser.switchToWindow(handles[i]);
|
|
2754
|
+
await browser.url(missingUrls[missingIdx]);
|
|
2755
|
+
currentUrls[i] = missingUrls[missingIdx++];
|
|
2756
|
+
}
|
|
2757
|
+
}
|
|
2758
|
+
for (let i = 0; i < handles.length; i++) {
|
|
2759
|
+
if (currentUrls[i] === activeTabUrl) {
|
|
2760
|
+
await browser.switchToWindow(handles[i]);
|
|
2761
|
+
break;
|
|
2762
|
+
}
|
|
2681
2763
|
}
|
|
2682
2764
|
}
|
|
2765
|
+
async function waitForCDP(host, port, timeoutMs = 1e4) {
|
|
2766
|
+
const deadline = Date.now() + timeoutMs;
|
|
2767
|
+
while (Date.now() < deadline) {
|
|
2768
|
+
try {
|
|
2769
|
+
const res = await fetch(`http://${host}:${port}/json/version`);
|
|
2770
|
+
if (res.ok) return;
|
|
2771
|
+
} catch {
|
|
2772
|
+
}
|
|
2773
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
2774
|
+
}
|
|
2775
|
+
throw new Error(`Chrome did not expose CDP on ${host}:${port} within ${timeoutMs}ms`);
|
|
2776
|
+
}
|
|
2683
2777
|
var attachBrowserTool = async ({
|
|
2684
2778
|
port = 9222,
|
|
2685
2779
|
host = "localhost",
|
|
2686
|
-
userDataDir = "/tmp/chrome-debug",
|
|
2687
2780
|
navigationUrl
|
|
2688
2781
|
}) => {
|
|
2689
2782
|
try {
|
|
2690
2783
|
const state2 = getBrowser.__state;
|
|
2691
|
-
|
|
2784
|
+
await waitForCDP(host, port);
|
|
2785
|
+
const { activeTabUrl, allTabUrls } = await closeStaleMappers(host, port);
|
|
2692
2786
|
const browser = await remote3({
|
|
2787
|
+
connectionRetryTimeout: 3e4,
|
|
2788
|
+
connectionRetryCount: 3,
|
|
2693
2789
|
capabilities: {
|
|
2694
2790
|
browserName: "chrome",
|
|
2791
|
+
unhandledPromptBehavior: "dismiss",
|
|
2792
|
+
webSocketUrl: false,
|
|
2695
2793
|
"goog:chromeOptions": {
|
|
2696
|
-
debuggerAddress: `${host}:${port}
|
|
2697
|
-
args: [`--user-data-dir=${userDataDir}`]
|
|
2794
|
+
debuggerAddress: `${host}:${port}`
|
|
2698
2795
|
}
|
|
2699
2796
|
}
|
|
2700
2797
|
});
|
|
@@ -2713,14 +2810,15 @@ var attachBrowserTool = async ({
|
|
|
2713
2810
|
capabilities: {
|
|
2714
2811
|
browserName: "chrome",
|
|
2715
2812
|
"goog:chromeOptions": {
|
|
2716
|
-
debuggerAddress: `${host}:${port}
|
|
2717
|
-
args: [`--user-data-dir=${userDataDir}`]
|
|
2813
|
+
debuggerAddress: `${host}:${port}`
|
|
2718
2814
|
}
|
|
2719
2815
|
},
|
|
2720
2816
|
steps: []
|
|
2721
2817
|
});
|
|
2722
|
-
if (
|
|
2723
|
-
await browser.url(
|
|
2818
|
+
if (navigationUrl) {
|
|
2819
|
+
await browser.url(navigationUrl);
|
|
2820
|
+
} else if (activeTabUrl) {
|
|
2821
|
+
await restoreAndSwitchToActiveTab(browser, activeTabUrl, allTabUrls);
|
|
2724
2822
|
}
|
|
2725
2823
|
const title = await browser.getTitle();
|
|
2726
2824
|
const url = await browser.getUrl();
|
|
@@ -2740,8 +2838,127 @@ Current page: "${title}" (${url})`
|
|
|
2740
2838
|
}
|
|
2741
2839
|
};
|
|
2742
2840
|
|
|
2743
|
-
// src/tools/
|
|
2841
|
+
// src/tools/launch-chrome.tool.ts
|
|
2842
|
+
import { spawn } from "child_process";
|
|
2843
|
+
import { copyFileSync, cpSync, existsSync, mkdirSync, rmSync, writeFileSync } from "fs";
|
|
2844
|
+
import { homedir, platform, tmpdir } from "os";
|
|
2845
|
+
import { join } from "path";
|
|
2744
2846
|
import { z as z17 } from "zod";
|
|
2847
|
+
var USER_DATA_DIR = join(tmpdir(), "chrome-debug");
|
|
2848
|
+
var launchChromeToolDefinition = {
|
|
2849
|
+
name: "launch_chrome",
|
|
2850
|
+
description: `Prepares and launches Chrome with remote debugging enabled so attach_browser() can connect.
|
|
2851
|
+
|
|
2852
|
+
Two modes:
|
|
2853
|
+
|
|
2854
|
+
newInstance (default): Opens a Chrome window alongside your existing one using a separate
|
|
2855
|
+
profile dir. Your current Chrome session is untouched.
|
|
2856
|
+
|
|
2857
|
+
freshSession: Launches Chrome with an empty profile (no cookies, no logins).
|
|
2858
|
+
|
|
2859
|
+
Use copyProfileFiles: true to carry over your cookies and logins into the debug session.
|
|
2860
|
+
Note: changes made during the session won't sync back to your main profile.
|
|
2861
|
+
|
|
2862
|
+
After this tool succeeds, call attach_browser() to connect.`,
|
|
2863
|
+
inputSchema: {
|
|
2864
|
+
port: z17.number().default(9222).describe("Remote debugging port (default: 9222)"),
|
|
2865
|
+
mode: z17.enum(["newInstance", "freshSession"]).default("newInstance").describe(
|
|
2866
|
+
"newInstance: open alongside existing Chrome | freshSession: clean profile"
|
|
2867
|
+
),
|
|
2868
|
+
copyProfileFiles: z17.boolean().default(false).describe(
|
|
2869
|
+
"Copy your Default Chrome profile (cookies, logins) into the debug session."
|
|
2870
|
+
)
|
|
2871
|
+
}
|
|
2872
|
+
};
|
|
2873
|
+
function isMac() {
|
|
2874
|
+
return platform() === "darwin";
|
|
2875
|
+
}
|
|
2876
|
+
function chromeExec() {
|
|
2877
|
+
if (isMac()) return "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome";
|
|
2878
|
+
if (platform() === "win32") {
|
|
2879
|
+
const candidates = [
|
|
2880
|
+
join("C:", "Program Files", "Google", "Chrome", "Application", "chrome.exe"),
|
|
2881
|
+
join("C:", "Program Files (x86)", "Google", "Chrome", "Application", "chrome.exe")
|
|
2882
|
+
];
|
|
2883
|
+
return candidates.find((p) => existsSync(p)) ?? candidates[0];
|
|
2884
|
+
}
|
|
2885
|
+
return "google-chrome";
|
|
2886
|
+
}
|
|
2887
|
+
function defaultProfileDir() {
|
|
2888
|
+
const home = homedir();
|
|
2889
|
+
if (isMac()) return join(home, "Library", "Application Support", "Google", "Chrome");
|
|
2890
|
+
if (platform() === "win32") return join(home, "AppData", "Local", "Google", "Chrome", "User Data");
|
|
2891
|
+
return join(home, ".config", "google-chrome");
|
|
2892
|
+
}
|
|
2893
|
+
function copyProfile() {
|
|
2894
|
+
const srcDir = defaultProfileDir();
|
|
2895
|
+
rmSync(USER_DATA_DIR, { recursive: true, force: true });
|
|
2896
|
+
mkdirSync(USER_DATA_DIR, { recursive: true });
|
|
2897
|
+
copyFileSync(join(srcDir, "Local State"), join(USER_DATA_DIR, "Local State"));
|
|
2898
|
+
cpSync(join(srcDir, "Default"), join(USER_DATA_DIR, "Default"), { recursive: true });
|
|
2899
|
+
for (const f of ["SingletonLock", "SingletonCookie", "SingletonSocket"]) {
|
|
2900
|
+
rmSync(join(USER_DATA_DIR, f), { force: true });
|
|
2901
|
+
}
|
|
2902
|
+
for (const f of ["Current Session", "Current Tabs", "Last Session", "Last Tabs"]) {
|
|
2903
|
+
rmSync(join(USER_DATA_DIR, "Default", f), { force: true });
|
|
2904
|
+
}
|
|
2905
|
+
writeFileSync(join(USER_DATA_DIR, "First Run"), "");
|
|
2906
|
+
}
|
|
2907
|
+
function launchChrome(port) {
|
|
2908
|
+
spawn(chromeExec(), [
|
|
2909
|
+
`--remote-debugging-port=${port}`,
|
|
2910
|
+
`--user-data-dir=${USER_DATA_DIR}`,
|
|
2911
|
+
"--profile-directory=Default",
|
|
2912
|
+
"--no-first-run",
|
|
2913
|
+
"--disable-session-crashed-bubble"
|
|
2914
|
+
], { detached: true, stdio: "ignore" }).unref();
|
|
2915
|
+
}
|
|
2916
|
+
async function waitForCDP2(port, timeoutMs = 15e3) {
|
|
2917
|
+
const deadline = Date.now() + timeoutMs;
|
|
2918
|
+
while (Date.now() < deadline) {
|
|
2919
|
+
try {
|
|
2920
|
+
const res = await fetch(`http://localhost:${port}/json/version`);
|
|
2921
|
+
if (res.ok) return;
|
|
2922
|
+
} catch {
|
|
2923
|
+
}
|
|
2924
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
2925
|
+
}
|
|
2926
|
+
throw new Error(`Chrome did not expose CDP on port ${port} within ${timeoutMs}ms`);
|
|
2927
|
+
}
|
|
2928
|
+
var launchChromeTool = async ({
|
|
2929
|
+
port = 9222,
|
|
2930
|
+
mode = "newInstance",
|
|
2931
|
+
copyProfileFiles = false
|
|
2932
|
+
}) => {
|
|
2933
|
+
const warnings = [];
|
|
2934
|
+
const notes = [];
|
|
2935
|
+
try {
|
|
2936
|
+
if (copyProfileFiles) {
|
|
2937
|
+
warnings.push("\u26A0\uFE0F Cookies and logins were copied at this moment. Changes during this session won't sync back to your main profile.");
|
|
2938
|
+
copyProfile();
|
|
2939
|
+
} else {
|
|
2940
|
+
notes.push(mode === "newInstance" ? "No profile copied \u2014 this instance starts with no cookies or logins." : "Fresh profile \u2014 no existing cookies or logins.");
|
|
2941
|
+
rmSync(USER_DATA_DIR, { recursive: true, force: true });
|
|
2942
|
+
mkdirSync(USER_DATA_DIR, { recursive: true });
|
|
2943
|
+
}
|
|
2944
|
+
launchChrome(port);
|
|
2945
|
+
await waitForCDP2(port);
|
|
2946
|
+
const lines = [
|
|
2947
|
+
`Chrome launched on port ${port} (mode: ${mode}).`,
|
|
2948
|
+
...warnings,
|
|
2949
|
+
...notes
|
|
2950
|
+
];
|
|
2951
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
2952
|
+
} catch (e) {
|
|
2953
|
+
return {
|
|
2954
|
+
isError: true,
|
|
2955
|
+
content: [{ type: "text", text: `Error launching Chrome: ${e}` }]
|
|
2956
|
+
};
|
|
2957
|
+
}
|
|
2958
|
+
};
|
|
2959
|
+
|
|
2960
|
+
// src/tools/emulate-device.tool.ts
|
|
2961
|
+
import { z as z18 } from "zod";
|
|
2745
2962
|
var restoreFunctions = /* @__PURE__ */ new Map();
|
|
2746
2963
|
var emulateDeviceToolDefinition = {
|
|
2747
2964
|
name: "emulate_device",
|
|
@@ -2754,7 +2971,7 @@ Usage:
|
|
|
2754
2971
|
emulate_device({ device: "iPhone 15" }) \u2014 activate emulation
|
|
2755
2972
|
emulate_device({ device: "reset" }) \u2014 restore desktop defaults`,
|
|
2756
2973
|
inputSchema: {
|
|
2757
|
-
device:
|
|
2974
|
+
device: z18.string().optional().describe(
|
|
2758
2975
|
'Device preset name (e.g. "iPhone 15", "Pixel 7"). Omit to list available presets. Pass "reset" to restore desktop defaults.'
|
|
2759
2976
|
)
|
|
2760
2977
|
}
|
|
@@ -2834,85 +3051,6 @@ ${names.join("\n")}` }]
|
|
|
2834
3051
|
}
|
|
2835
3052
|
};
|
|
2836
3053
|
|
|
2837
|
-
// package.json
|
|
2838
|
-
var package_default = {
|
|
2839
|
-
name: "@wdio/mcp",
|
|
2840
|
-
author: "Vince Graics",
|
|
2841
|
-
repository: {
|
|
2842
|
-
type: "git",
|
|
2843
|
-
url: "git://github.com/webdriverio/mcp.git"
|
|
2844
|
-
},
|
|
2845
|
-
version: "2.4.1",
|
|
2846
|
-
description: "MCP server with WebdriverIO for browser and mobile app automation (iOS/Android via Appium)",
|
|
2847
|
-
main: "./lib/server.js",
|
|
2848
|
-
module: "./lib/server.js",
|
|
2849
|
-
types: "./lib/server.d.ts",
|
|
2850
|
-
exports: {
|
|
2851
|
-
".": {
|
|
2852
|
-
import: "./lib/server.js",
|
|
2853
|
-
types: "./lib/server.d.ts"
|
|
2854
|
-
},
|
|
2855
|
-
"./snapshot": {
|
|
2856
|
-
import: "./lib/snapshot.js",
|
|
2857
|
-
types: "./lib/snapshot.d.ts"
|
|
2858
|
-
}
|
|
2859
|
-
},
|
|
2860
|
-
bin: {
|
|
2861
|
-
"wdio-mcp": "lib/server.js"
|
|
2862
|
-
},
|
|
2863
|
-
license: "MIT",
|
|
2864
|
-
publishConfig: {
|
|
2865
|
-
access: "public"
|
|
2866
|
-
},
|
|
2867
|
-
type: "module",
|
|
2868
|
-
files: [
|
|
2869
|
-
"lib",
|
|
2870
|
-
"README.md"
|
|
2871
|
-
],
|
|
2872
|
-
scripts: {
|
|
2873
|
-
prebundle: "rimraf lib --glob ./*.tgz",
|
|
2874
|
-
bundle: "tsup && shx chmod +x lib/server.js",
|
|
2875
|
-
postbundle: "npm pack",
|
|
2876
|
-
lint: "eslint src/ --fix && tsc --noEmit",
|
|
2877
|
-
"lint:tests": "eslint tests/ --fix && tsc -p tsconfig.test.json --noEmit",
|
|
2878
|
-
start: "node lib/server.js",
|
|
2879
|
-
dev: "tsx --watch src/server.ts",
|
|
2880
|
-
prepare: "husky",
|
|
2881
|
-
test: "vitest run"
|
|
2882
|
-
},
|
|
2883
|
-
dependencies: {
|
|
2884
|
-
"@modelcontextprotocol/sdk": "1.27",
|
|
2885
|
-
"@toon-format/toon": "^2.1.0",
|
|
2886
|
-
"@wdio/protocols": "^9.16.2",
|
|
2887
|
-
"@xmldom/xmldom": "^0.8.11",
|
|
2888
|
-
"puppeteer-core": "^24.35.0",
|
|
2889
|
-
sharp: "^0.34.5",
|
|
2890
|
-
webdriverio: "9.24",
|
|
2891
|
-
xpath: "^0.0.34",
|
|
2892
|
-
zod: "^4.3.5"
|
|
2893
|
-
},
|
|
2894
|
-
devDependencies: {
|
|
2895
|
-
"@release-it/conventional-changelog": "^10.0.4",
|
|
2896
|
-
"@types/node": "^20.11.0",
|
|
2897
|
-
"@wdio/eslint": "^0.1.3",
|
|
2898
|
-
"@wdio/types": "^9.20.0",
|
|
2899
|
-
eslint: "^9.39.2",
|
|
2900
|
-
"happy-dom": "^20.7.0",
|
|
2901
|
-
husky: "^9.1.7",
|
|
2902
|
-
"release-it": "^19.2.3",
|
|
2903
|
-
rimraf: "^6.1.2",
|
|
2904
|
-
shx: "^0.4.0",
|
|
2905
|
-
tsup: "^8.5.1",
|
|
2906
|
-
tsx: "^4.21.0",
|
|
2907
|
-
typescript: "5.9",
|
|
2908
|
-
vitest: "^4.0.18"
|
|
2909
|
-
},
|
|
2910
|
-
packageManager: "pnpm@10.12.4"
|
|
2911
|
-
};
|
|
2912
|
-
|
|
2913
|
-
// src/server.ts
|
|
2914
|
-
import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2915
|
-
|
|
2916
3054
|
// src/recording/step-recorder.ts
|
|
2917
3055
|
function getState2() {
|
|
2918
3056
|
return getBrowser.__state;
|
|
@@ -3100,6 +3238,7 @@ var registerTool = (definition, callback) => server.registerTool(definition.name
|
|
|
3100
3238
|
registerTool(startBrowserToolDefinition, withRecording("start_browser", startBrowserTool));
|
|
3101
3239
|
registerTool(startAppToolDefinition, withRecording("start_app_session", startAppTool));
|
|
3102
3240
|
registerTool(closeSessionToolDefinition, closeSessionTool);
|
|
3241
|
+
registerTool(launchChromeToolDefinition, withRecording("launch_chrome", launchChromeTool));
|
|
3103
3242
|
registerTool(attachBrowserToolDefinition, withRecording("attach_browser", attachBrowserTool));
|
|
3104
3243
|
registerTool(emulateDeviceToolDefinition, emulateDeviceTool);
|
|
3105
3244
|
registerTool(navigateToolDefinition, withRecording("navigate", navigateTool));
|
|
@@ -3139,7 +3278,11 @@ server.registerResource(
|
|
|
3139
3278
|
async () => {
|
|
3140
3279
|
const payload = buildCurrentSessionSteps();
|
|
3141
3280
|
return {
|
|
3142
|
-
contents: [{
|
|
3281
|
+
contents: [{
|
|
3282
|
+
uri: "wdio://session/current/steps",
|
|
3283
|
+
mimeType: "application/json",
|
|
3284
|
+
text: payload?.stepsJson ?? '{"error":"No active session"}'
|
|
3285
|
+
}]
|
|
3143
3286
|
};
|
|
3144
3287
|
}
|
|
3145
3288
|
);
|
|
@@ -3150,7 +3293,11 @@ server.registerResource(
|
|
|
3150
3293
|
async () => {
|
|
3151
3294
|
const payload = buildCurrentSessionSteps();
|
|
3152
3295
|
return {
|
|
3153
|
-
contents: [{
|
|
3296
|
+
contents: [{
|
|
3297
|
+
uri: "wdio://session/current/code",
|
|
3298
|
+
mimeType: "text/plain",
|
|
3299
|
+
text: payload?.generatedJs ?? "// No active session"
|
|
3300
|
+
}]
|
|
3154
3301
|
};
|
|
3155
3302
|
}
|
|
3156
3303
|
);
|
|
@@ -3161,7 +3308,11 @@ server.registerResource(
|
|
|
3161
3308
|
async (uri, { sessionId }) => {
|
|
3162
3309
|
const payload = buildSessionStepsById(sessionId);
|
|
3163
3310
|
return {
|
|
3164
|
-
contents: [{
|
|
3311
|
+
contents: [{
|
|
3312
|
+
uri: uri.href,
|
|
3313
|
+
mimeType: "application/json",
|
|
3314
|
+
text: payload?.stepsJson ?? `{"error":"Session not found: ${sessionId}"}`
|
|
3315
|
+
}]
|
|
3165
3316
|
};
|
|
3166
3317
|
}
|
|
3167
3318
|
);
|
|
@@ -3172,7 +3323,11 @@ server.registerResource(
|
|
|
3172
3323
|
async (uri, { sessionId }) => {
|
|
3173
3324
|
const payload = buildSessionStepsById(sessionId);
|
|
3174
3325
|
return {
|
|
3175
|
-
contents: [{
|
|
3326
|
+
contents: [{
|
|
3327
|
+
uri: uri.href,
|
|
3328
|
+
mimeType: "text/plain",
|
|
3329
|
+
text: payload?.generatedJs ?? `// Session not found: ${sessionId}`
|
|
3330
|
+
}]
|
|
3176
3331
|
};
|
|
3177
3332
|
}
|
|
3178
3333
|
);
|