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