@wdio/mcp 2.3.1 → 2.4.1
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/README.md +35 -0
- package/lib/server.js +510 -19
- package/lib/server.js.map +1 -1
- package/package.json +1 -1
package/lib/server.js
CHANGED
|
@@ -11,7 +11,7 @@ var supportedBrowsers = ["chrome", "firefox", "edge", "safari"];
|
|
|
11
11
|
var browserSchema = z.enum(supportedBrowsers).default("chrome");
|
|
12
12
|
var startBrowserToolDefinition = {
|
|
13
13
|
name: "start_browser",
|
|
14
|
-
description: "starts a browser session (Chrome, Firefox, Edge, Safari) and sets it to the current state",
|
|
14
|
+
description: "starts a browser session (Chrome, Firefox, Edge, Safari) and sets it to the current state. Prefer headless: true unless the user explicitly asks to see the browser.",
|
|
15
15
|
inputSchema: {
|
|
16
16
|
browser: browserSchema.describe("Browser to launch: chrome, firefox, edge, safari (default: chrome)"),
|
|
17
17
|
headless: z.boolean().optional().default(true),
|
|
@@ -31,7 +31,8 @@ var closeSessionToolDefinition = {
|
|
|
31
31
|
var state = {
|
|
32
32
|
browsers: /* @__PURE__ */ new Map(),
|
|
33
33
|
currentSession: null,
|
|
34
|
-
sessionMetadata: /* @__PURE__ */ new Map()
|
|
34
|
+
sessionMetadata: /* @__PURE__ */ new Map(),
|
|
35
|
+
sessionHistory: /* @__PURE__ */ new Map()
|
|
35
36
|
};
|
|
36
37
|
var getBrowser = () => {
|
|
37
38
|
const browser = state.browsers.get(state.currentSession);
|
|
@@ -132,12 +133,40 @@ var startBrowserTool = async ({
|
|
|
132
133
|
});
|
|
133
134
|
const { sessionId } = wdioBrowser;
|
|
134
135
|
state.browsers.set(sessionId, wdioBrowser);
|
|
135
|
-
state.currentSession = sessionId;
|
|
136
136
|
state.sessionMetadata.set(sessionId, {
|
|
137
137
|
type: "browser",
|
|
138
138
|
capabilities: wdioBrowser.capabilities,
|
|
139
139
|
isAttached: false
|
|
140
140
|
});
|
|
141
|
+
if (state.currentSession && state.currentSession !== sessionId) {
|
|
142
|
+
const outgoing = state.sessionHistory.get(state.currentSession);
|
|
143
|
+
if (outgoing) {
|
|
144
|
+
outgoing.steps.push({
|
|
145
|
+
index: outgoing.steps.length + 1,
|
|
146
|
+
tool: "__session_transition__",
|
|
147
|
+
params: { newSessionId: sessionId },
|
|
148
|
+
status: "ok",
|
|
149
|
+
durationMs: 0,
|
|
150
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
151
|
+
});
|
|
152
|
+
outgoing.endedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
state.sessionHistory.set(sessionId, {
|
|
156
|
+
sessionId,
|
|
157
|
+
type: "browser",
|
|
158
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
159
|
+
capabilities: wdioBrowser.capabilities,
|
|
160
|
+
steps: [{
|
|
161
|
+
index: 1,
|
|
162
|
+
tool: "start_browser",
|
|
163
|
+
params: { browser, headless, windowWidth, windowHeight, ...navigationUrl && { navigationUrl }, ...Object.keys(userCapabilities).length > 0 && { capabilities: userCapabilities } },
|
|
164
|
+
status: "ok",
|
|
165
|
+
durationMs: 0,
|
|
166
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
167
|
+
}]
|
|
168
|
+
});
|
|
169
|
+
state.currentSession = sessionId;
|
|
141
170
|
let sizeNote = "";
|
|
142
171
|
try {
|
|
143
172
|
await wdioBrowser.setWindowSize(windowWidth, windowHeight);
|
|
@@ -164,13 +193,18 @@ var closeSessionTool = async (args = {}) => {
|
|
|
164
193
|
const browser = getBrowser();
|
|
165
194
|
const sessionId = state.currentSession;
|
|
166
195
|
const metadata = state.sessionMetadata.get(sessionId);
|
|
167
|
-
|
|
196
|
+
const history = state.sessionHistory.get(sessionId);
|
|
197
|
+
if (history) {
|
|
198
|
+
history.endedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
199
|
+
}
|
|
200
|
+
const effectiveDetach = args.detach || !!metadata?.isAttached;
|
|
201
|
+
if (!effectiveDetach) {
|
|
168
202
|
await browser.deleteSession();
|
|
169
203
|
}
|
|
170
204
|
state.browsers.delete(sessionId);
|
|
171
205
|
state.sessionMetadata.delete(sessionId);
|
|
172
206
|
state.currentSession = null;
|
|
173
|
-
const action =
|
|
207
|
+
const action = effectiveDetach ? "detached from" : "closed";
|
|
174
208
|
const note = args.detach && !metadata?.isAttached ? "\nNote: Session will remain active on Appium server." : "";
|
|
175
209
|
return {
|
|
176
210
|
content: [{ type: "text", text: `Session ${sessionId} ${action}${note}` }]
|
|
@@ -375,7 +409,7 @@ var startAppToolDefinition = {
|
|
|
375
409
|
udid: z5.string().optional().describe('Unique Device Identifier for iOS real device testing (e.g., "00008030-001234567890002E")'),
|
|
376
410
|
noReset: z5.boolean().optional().describe("Do not reset app state before session (preserves app data). Default: false"),
|
|
377
411
|
fullReset: z5.boolean().optional().describe("Uninstall app before/after session. Default: true. Set to false with noReset=true to preserve app state completely"),
|
|
378
|
-
newCommandTimeout: z5.number().min(0).optional().describe("How long (in seconds) Appium will wait for a new command before assuming the client has quit and ending the session. Default:
|
|
412
|
+
newCommandTimeout: z5.number().min(0).optional().default(300).describe("How long (in seconds) Appium will wait for a new command before assuming the client has quit and ending the session. Default: 300."),
|
|
379
413
|
capabilities: z5.record(z5.string(), z5.unknown()).optional().describe("Additional Appium/WebDriver capabilities to merge with defaults (e.g. appium:udid, appium:chromedriverExecutable, appium:autoWebview)")
|
|
380
414
|
}
|
|
381
415
|
};
|
|
@@ -404,7 +438,7 @@ var startAppTool = async (args) => {
|
|
|
404
438
|
udid,
|
|
405
439
|
noReset,
|
|
406
440
|
fullReset,
|
|
407
|
-
newCommandTimeout,
|
|
441
|
+
newCommandTimeout = 300,
|
|
408
442
|
capabilities: userCapabilities = {}
|
|
409
443
|
} = args;
|
|
410
444
|
if (!appPath && noReset !== true) {
|
|
@@ -463,12 +497,58 @@ var startAppTool = async (args) => {
|
|
|
463
497
|
const shouldAutoDetach = noReset === true || !appPath;
|
|
464
498
|
const state2 = getState();
|
|
465
499
|
state2.browsers.set(sessionId, browser);
|
|
466
|
-
state2.currentSession = sessionId;
|
|
467
500
|
state2.sessionMetadata.set(sessionId, {
|
|
468
501
|
type: platform.toLowerCase(),
|
|
469
502
|
capabilities: mergedCapabilities,
|
|
470
503
|
isAttached: shouldAutoDetach
|
|
471
504
|
});
|
|
505
|
+
if (state2.currentSession && state2.currentSession !== sessionId) {
|
|
506
|
+
const outgoing = state2.sessionHistory.get(state2.currentSession);
|
|
507
|
+
if (outgoing) {
|
|
508
|
+
outgoing.steps.push({
|
|
509
|
+
index: outgoing.steps.length + 1,
|
|
510
|
+
tool: "__session_transition__",
|
|
511
|
+
params: { newSessionId: sessionId },
|
|
512
|
+
status: "ok",
|
|
513
|
+
durationMs: 0,
|
|
514
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
515
|
+
});
|
|
516
|
+
outgoing.endedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
state2.sessionHistory.set(sessionId, {
|
|
520
|
+
sessionId,
|
|
521
|
+
type: platform.toLowerCase(),
|
|
522
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
523
|
+
capabilities: mergedCapabilities,
|
|
524
|
+
steps: [{
|
|
525
|
+
index: 1,
|
|
526
|
+
tool: "start_app_session",
|
|
527
|
+
params: {
|
|
528
|
+
platform,
|
|
529
|
+
deviceName,
|
|
530
|
+
...platformVersion !== void 0 && { platformVersion },
|
|
531
|
+
...automationName !== void 0 && { automationName },
|
|
532
|
+
...appPath !== void 0 && { appPath },
|
|
533
|
+
...udid !== void 0 && { udid },
|
|
534
|
+
...noReset !== void 0 && { noReset },
|
|
535
|
+
...fullReset !== void 0 && { fullReset },
|
|
536
|
+
...autoGrantPermissions !== void 0 && { autoGrantPermissions },
|
|
537
|
+
...autoAcceptAlerts !== void 0 && { autoAcceptAlerts },
|
|
538
|
+
...autoDismissAlerts !== void 0 && { autoDismissAlerts },
|
|
539
|
+
...appWaitActivity !== void 0 && { appWaitActivity },
|
|
540
|
+
...newCommandTimeout !== void 0 && { newCommandTimeout },
|
|
541
|
+
...appiumHost !== void 0 && { appiumHost },
|
|
542
|
+
...appiumPort !== void 0 && { appiumPort },
|
|
543
|
+
...appiumPath !== void 0 && { appiumPath },
|
|
544
|
+
...Object.keys(userCapabilities).length > 0 && { capabilities: userCapabilities }
|
|
545
|
+
},
|
|
546
|
+
status: "ok",
|
|
547
|
+
durationMs: 0,
|
|
548
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
549
|
+
}]
|
|
550
|
+
});
|
|
551
|
+
state2.currentSession = sessionId;
|
|
472
552
|
const appInfo = appPath ? `
|
|
473
553
|
App: ${appPath}` : "\nApp: (connected to running app)";
|
|
474
554
|
const detachNote = shouldAutoDetach ? "\n\n(Auto-detach enabled: session will be preserved on close. Use close_session({ detach: false }) to force terminate.)" : "";
|
|
@@ -1904,7 +1984,7 @@ var accessibilityTreeScript = () => (function() {
|
|
|
1904
1984
|
if (ariaLevel) return parseInt(ariaLevel, 10);
|
|
1905
1985
|
return void 0;
|
|
1906
1986
|
}
|
|
1907
|
-
function
|
|
1987
|
+
function getState3(el) {
|
|
1908
1988
|
const inputEl = el;
|
|
1909
1989
|
const isCheckable = ["input", "menuitemcheckbox", "menuitemradio"].includes(el.tagName.toLowerCase()) || ["checkbox", "radio", "switch"].includes(el.getAttribute("role") || "");
|
|
1910
1990
|
return {
|
|
@@ -1932,7 +2012,7 @@ var accessibilityTreeScript = () => (function() {
|
|
|
1932
2012
|
const isLandmark = LANDMARK_ROLES.has(role);
|
|
1933
2013
|
const hasIdentity = !!(name || isLandmark);
|
|
1934
2014
|
const selector = hasIdentity ? getSelector(el) : "";
|
|
1935
|
-
const node = { role, name, selector, level: getLevel(el) ?? "", ...
|
|
2015
|
+
const node = { role, name, selector, level: getLevel(el) ?? "", ...getState3(el) };
|
|
1936
2016
|
result.push(node);
|
|
1937
2017
|
for (const child of Array.from(el.children)) {
|
|
1938
2018
|
walk(child, depth + 1);
|
|
@@ -2569,6 +2649,186 @@ var executeScriptTool = async (args) => {
|
|
|
2569
2649
|
}
|
|
2570
2650
|
};
|
|
2571
2651
|
|
|
2652
|
+
// src/tools/attach-browser.tool.ts
|
|
2653
|
+
import { remote as remote3 } from "webdriverio";
|
|
2654
|
+
import { z as z16 } from "zod";
|
|
2655
|
+
var attachBrowserToolDefinition = {
|
|
2656
|
+
name: "attach_browser",
|
|
2657
|
+
description: `Attach to a Chrome instance already running with --remote-debugging-port.
|
|
2658
|
+
|
|
2659
|
+
Start Chrome first (quit any running Chrome instance before launching):
|
|
2660
|
+
|
|
2661
|
+
macOS \u2014 with real profile (preserves extensions, cookies, logins):
|
|
2662
|
+
pkill -x "Google Chrome" && sleep 1
|
|
2663
|
+
/Applications/Google Chrome.app/Contents/MacOS/Google Chrome --remote-debugging-port=9222 --user-data-dir="$HOME/Library/Application Support/Google/Chrome" --profile-directory=Default &
|
|
2664
|
+
|
|
2665
|
+
macOS \u2014 with fresh profile (lightweight, no extensions):
|
|
2666
|
+
pkill -x "Google Chrome" && sleep 1
|
|
2667
|
+
/Applications/Google Chrome.app/Contents/MacOS/Google Chrome --remote-debugging-port=9222 --user-data-dir=/tmp/chrome-debug &
|
|
2668
|
+
|
|
2669
|
+
Linux \u2014 with real profile:
|
|
2670
|
+
google-chrome --remote-debugging-port=9222 --user-data-dir="$HOME/.config/google-chrome" --profile-directory=Default &
|
|
2671
|
+
|
|
2672
|
+
Linux \u2014 with fresh profile:
|
|
2673
|
+
google-chrome --remote-debugging-port=9222 --user-data-dir=/tmp/chrome-debug &
|
|
2674
|
+
|
|
2675
|
+
Verify Chrome is ready: curl http://localhost:9222/json/version
|
|
2676
|
+
|
|
2677
|
+
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.`,
|
|
2678
|
+
inputSchema: {
|
|
2679
|
+
port: z16.number().default(9222).describe("Chrome remote debugging port (default: 9222)"),
|
|
2680
|
+
host: z16.string().default("localhost").describe("Host where Chrome is running (default: localhost)"),
|
|
2681
|
+
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)'),
|
|
2682
|
+
navigationUrl: z16.string().optional().describe("URL to navigate to immediately after attaching")
|
|
2683
|
+
}
|
|
2684
|
+
};
|
|
2685
|
+
async function getActiveTabUrl(host, port) {
|
|
2686
|
+
try {
|
|
2687
|
+
const res = await fetch(`http://${host}:${port}/json`);
|
|
2688
|
+
const tabs = await res.json();
|
|
2689
|
+
const page = tabs.find((t) => t.type === "page" && t.url && !t.url.startsWith("devtools://"));
|
|
2690
|
+
return page?.url ?? null;
|
|
2691
|
+
} catch {
|
|
2692
|
+
return null;
|
|
2693
|
+
}
|
|
2694
|
+
}
|
|
2695
|
+
var attachBrowserTool = async ({
|
|
2696
|
+
port = 9222,
|
|
2697
|
+
host = "localhost",
|
|
2698
|
+
userDataDir = "/tmp/chrome-debug",
|
|
2699
|
+
navigationUrl
|
|
2700
|
+
}) => {
|
|
2701
|
+
try {
|
|
2702
|
+
const state2 = getBrowser.__state;
|
|
2703
|
+
const activeUrl = navigationUrl ?? await getActiveTabUrl(host, port);
|
|
2704
|
+
const browser = await remote3({
|
|
2705
|
+
capabilities: {
|
|
2706
|
+
browserName: "chrome",
|
|
2707
|
+
"goog:chromeOptions": {
|
|
2708
|
+
debuggerAddress: `${host}:${port}`,
|
|
2709
|
+
args: [`--user-data-dir=${userDataDir}`]
|
|
2710
|
+
}
|
|
2711
|
+
}
|
|
2712
|
+
});
|
|
2713
|
+
const { sessionId } = browser;
|
|
2714
|
+
state2.browsers.set(sessionId, browser);
|
|
2715
|
+
state2.currentSession = sessionId;
|
|
2716
|
+
state2.sessionMetadata.set(sessionId, {
|
|
2717
|
+
type: "browser",
|
|
2718
|
+
capabilities: browser.capabilities,
|
|
2719
|
+
isAttached: true
|
|
2720
|
+
});
|
|
2721
|
+
if (activeUrl) {
|
|
2722
|
+
await browser.url(activeUrl);
|
|
2723
|
+
}
|
|
2724
|
+
const title = await browser.getTitle();
|
|
2725
|
+
const url = await browser.getUrl();
|
|
2726
|
+
return {
|
|
2727
|
+
content: [{
|
|
2728
|
+
type: "text",
|
|
2729
|
+
text: `Attached to Chrome on ${host}:${port}
|
|
2730
|
+
Session ID: ${sessionId}
|
|
2731
|
+
Current page: "${title}" (${url})`
|
|
2732
|
+
}]
|
|
2733
|
+
};
|
|
2734
|
+
} catch (e) {
|
|
2735
|
+
return {
|
|
2736
|
+
content: [{ type: "text", text: `Error attaching to browser: ${e}` }]
|
|
2737
|
+
};
|
|
2738
|
+
}
|
|
2739
|
+
};
|
|
2740
|
+
|
|
2741
|
+
// src/tools/emulate-device.tool.ts
|
|
2742
|
+
import { z as z17 } from "zod";
|
|
2743
|
+
var restoreFunctions = /* @__PURE__ */ new Map();
|
|
2744
|
+
var emulateDeviceToolDefinition = {
|
|
2745
|
+
name: "emulate_device",
|
|
2746
|
+
description: `Emulate a mobile or tablet device in the current browser session (sets viewport, DPR, user-agent, touch events).
|
|
2747
|
+
|
|
2748
|
+
Requires a BiDi-enabled session: start_browser({ capabilities: { webSocketUrl: true } })
|
|
2749
|
+
|
|
2750
|
+
Usage:
|
|
2751
|
+
emulate_device() \u2014 list available device presets
|
|
2752
|
+
emulate_device({ device: "iPhone 15" }) \u2014 activate emulation
|
|
2753
|
+
emulate_device({ device: "reset" }) \u2014 restore desktop defaults`,
|
|
2754
|
+
inputSchema: {
|
|
2755
|
+
device: z17.string().optional().describe(
|
|
2756
|
+
'Device preset name (e.g. "iPhone 15", "Pixel 7"). Omit to list available presets. Pass "reset" to restore desktop defaults.'
|
|
2757
|
+
)
|
|
2758
|
+
}
|
|
2759
|
+
};
|
|
2760
|
+
var emulateDeviceTool = async ({
|
|
2761
|
+
device
|
|
2762
|
+
}) => {
|
|
2763
|
+
try {
|
|
2764
|
+
const browser = getBrowser();
|
|
2765
|
+
const state2 = getBrowser.__state;
|
|
2766
|
+
const sessionId = state2.currentSession;
|
|
2767
|
+
const metadata = state2.sessionMetadata.get(sessionId);
|
|
2768
|
+
if (metadata?.type === "ios" || metadata?.type === "android") {
|
|
2769
|
+
return {
|
|
2770
|
+
content: [{ type: "text", text: "Error: emulate_device is only supported for web browser sessions, not iOS/Android." }]
|
|
2771
|
+
};
|
|
2772
|
+
}
|
|
2773
|
+
if (!browser.isBidi) {
|
|
2774
|
+
return {
|
|
2775
|
+
content: [{
|
|
2776
|
+
type: "text",
|
|
2777
|
+
text: "Error: emulate_device requires a BiDi-enabled session.\nRestart the browser with: start_browser({ capabilities: { webSocketUrl: true } })"
|
|
2778
|
+
}]
|
|
2779
|
+
};
|
|
2780
|
+
}
|
|
2781
|
+
if (!device) {
|
|
2782
|
+
try {
|
|
2783
|
+
await browser.emulate("device", "\0");
|
|
2784
|
+
} catch (e) {
|
|
2785
|
+
const msg = String(e);
|
|
2786
|
+
const match = msg.match(/please use one of the following: (.+)$/);
|
|
2787
|
+
if (match) {
|
|
2788
|
+
const names = match[1].split(", ").sort();
|
|
2789
|
+
return {
|
|
2790
|
+
content: [{ type: "text", text: `Available devices (${names.length}):
|
|
2791
|
+
${names.join("\n")}` }]
|
|
2792
|
+
};
|
|
2793
|
+
}
|
|
2794
|
+
return { content: [{ type: "text", text: `Error listing devices: ${e}` }] };
|
|
2795
|
+
}
|
|
2796
|
+
return { content: [{ type: "text", text: "Could not retrieve device list." }] };
|
|
2797
|
+
}
|
|
2798
|
+
if (device === "reset") {
|
|
2799
|
+
const restoreFn = restoreFunctions.get(sessionId);
|
|
2800
|
+
if (!restoreFn) {
|
|
2801
|
+
return { content: [{ type: "text", text: "No active device emulation to reset." }] };
|
|
2802
|
+
}
|
|
2803
|
+
await restoreFn();
|
|
2804
|
+
restoreFunctions.delete(sessionId);
|
|
2805
|
+
return { content: [{ type: "text", text: "Device emulation reset to desktop defaults." }] };
|
|
2806
|
+
}
|
|
2807
|
+
try {
|
|
2808
|
+
const restoreFn = await browser.emulate("device", device);
|
|
2809
|
+
restoreFunctions.set(sessionId, restoreFn);
|
|
2810
|
+
return {
|
|
2811
|
+
content: [{ type: "text", text: `Emulating "${device}".` }]
|
|
2812
|
+
};
|
|
2813
|
+
} catch (e) {
|
|
2814
|
+
const msg = String(e);
|
|
2815
|
+
if (msg.includes("Unknown device name")) {
|
|
2816
|
+
return {
|
|
2817
|
+
content: [{
|
|
2818
|
+
type: "text",
|
|
2819
|
+
text: `Error: Unknown device "${device}". Call emulate_device() with no arguments to list valid names.`
|
|
2820
|
+
}]
|
|
2821
|
+
};
|
|
2822
|
+
}
|
|
2823
|
+
return { content: [{ type: "text", text: `Error: ${e}` }] };
|
|
2824
|
+
}
|
|
2825
|
+
} catch (e) {
|
|
2826
|
+
return {
|
|
2827
|
+
content: [{ type: "text", text: `Error: ${e}` }]
|
|
2828
|
+
};
|
|
2829
|
+
}
|
|
2830
|
+
};
|
|
2831
|
+
|
|
2572
2832
|
// package.json
|
|
2573
2833
|
var package_default = {
|
|
2574
2834
|
name: "@wdio/mcp",
|
|
@@ -2577,7 +2837,7 @@ var package_default = {
|
|
|
2577
2837
|
type: "git",
|
|
2578
2838
|
url: "git://github.com/webdriverio/mcp.git"
|
|
2579
2839
|
},
|
|
2580
|
-
version: "2.
|
|
2840
|
+
version: "2.4.0",
|
|
2581
2841
|
description: "MCP server with WebdriverIO for browser and mobile app automation (iOS/Android via Appium)",
|
|
2582
2842
|
main: "./lib/server.js",
|
|
2583
2843
|
module: "./lib/server.js",
|
|
@@ -2645,6 +2905,206 @@ var package_default = {
|
|
|
2645
2905
|
packageManager: "pnpm@10.12.4"
|
|
2646
2906
|
};
|
|
2647
2907
|
|
|
2908
|
+
// src/server.ts
|
|
2909
|
+
import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2910
|
+
|
|
2911
|
+
// src/recording/step-recorder.ts
|
|
2912
|
+
function getState2() {
|
|
2913
|
+
return getBrowser.__state;
|
|
2914
|
+
}
|
|
2915
|
+
function appendStep(toolName, params, status, durationMs, error) {
|
|
2916
|
+
const state2 = getState2();
|
|
2917
|
+
const sessionId = state2.currentSession;
|
|
2918
|
+
if (!sessionId) return;
|
|
2919
|
+
const history = state2.sessionHistory.get(sessionId);
|
|
2920
|
+
if (!history) return;
|
|
2921
|
+
const step = {
|
|
2922
|
+
index: history.steps.length + 1,
|
|
2923
|
+
tool: toolName,
|
|
2924
|
+
params,
|
|
2925
|
+
status,
|
|
2926
|
+
durationMs,
|
|
2927
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2928
|
+
...error !== void 0 && { error }
|
|
2929
|
+
};
|
|
2930
|
+
history.steps.push(step);
|
|
2931
|
+
}
|
|
2932
|
+
function getSessionHistory() {
|
|
2933
|
+
return getState2().sessionHistory;
|
|
2934
|
+
}
|
|
2935
|
+
function extractErrorText(result) {
|
|
2936
|
+
const textContent = result.content.find((c) => c.type === "text");
|
|
2937
|
+
return textContent ? textContent.text : "Unknown error";
|
|
2938
|
+
}
|
|
2939
|
+
function withRecording(toolName, callback) {
|
|
2940
|
+
return async (params, extra) => {
|
|
2941
|
+
const start = Date.now();
|
|
2942
|
+
const result = await callback(params, extra);
|
|
2943
|
+
const isError = result.content.some(
|
|
2944
|
+
(c) => c.type === "text" && typeof c.text === "string" && c.text.startsWith("Error")
|
|
2945
|
+
);
|
|
2946
|
+
appendStep(
|
|
2947
|
+
toolName,
|
|
2948
|
+
params,
|
|
2949
|
+
isError ? "error" : "ok",
|
|
2950
|
+
Date.now() - start,
|
|
2951
|
+
isError ? extractErrorText(result) : void 0
|
|
2952
|
+
);
|
|
2953
|
+
return result;
|
|
2954
|
+
};
|
|
2955
|
+
}
|
|
2956
|
+
|
|
2957
|
+
// src/recording/code-generator.ts
|
|
2958
|
+
function escapeStr(value) {
|
|
2959
|
+
return String(value).replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
2960
|
+
}
|
|
2961
|
+
function formatParams(params) {
|
|
2962
|
+
return Object.entries(params).map(([k, v]) => `${k}="${v}"`).join(" ");
|
|
2963
|
+
}
|
|
2964
|
+
function indentJson(value) {
|
|
2965
|
+
return JSON.stringify(value, null, 2).split("\n").map((line, i) => i > 0 ? ` ${line}` : line).join("\n");
|
|
2966
|
+
}
|
|
2967
|
+
function generateStep(step) {
|
|
2968
|
+
if (step.tool === "__session_transition__") {
|
|
2969
|
+
const newId = step.params.newSessionId ?? "unknown";
|
|
2970
|
+
return `// --- new session: ${newId} started at ${step.timestamp} ---`;
|
|
2971
|
+
}
|
|
2972
|
+
if (step.status === "error") {
|
|
2973
|
+
return `// [error] ${step.tool}: ${formatParams(step.params)} \u2014 ${step.error ?? "unknown error"}`;
|
|
2974
|
+
}
|
|
2975
|
+
const p = step.params;
|
|
2976
|
+
switch (step.tool) {
|
|
2977
|
+
case "start_browser": {
|
|
2978
|
+
const browserName = p.browser === "edge" ? "msedge" : String(p.browser ?? "chrome");
|
|
2979
|
+
const headless = p.headless !== false;
|
|
2980
|
+
const width = p.windowWidth ?? 1920;
|
|
2981
|
+
const height = p.windowHeight ?? 1080;
|
|
2982
|
+
const args = [`--window-size=${width},${height}`];
|
|
2983
|
+
if (headless && browserName !== "safari") {
|
|
2984
|
+
args.push("--headless=new", "--disable-gpu", "--disable-dev-shm-usage");
|
|
2985
|
+
}
|
|
2986
|
+
const caps = { browserName };
|
|
2987
|
+
if (browserName === "chrome") caps["goog:chromeOptions"] = { args };
|
|
2988
|
+
else if (browserName === "msedge") caps["ms:edgeOptions"] = { args };
|
|
2989
|
+
else if (browserName === "firefox" && headless) caps["moz:firefoxOptions"] = { args: ["-headless"] };
|
|
2990
|
+
const extra = p.capabilities;
|
|
2991
|
+
const merged = extra ? { ...caps, ...extra } : caps;
|
|
2992
|
+
const nav = p.navigationUrl ? `
|
|
2993
|
+
await browser.url('${escapeStr(p.navigationUrl)}');` : "";
|
|
2994
|
+
return `const browser = await remote({
|
|
2995
|
+
capabilities: ${indentJson(merged)}
|
|
2996
|
+
});${nav}`;
|
|
2997
|
+
}
|
|
2998
|
+
case "start_app_session": {
|
|
2999
|
+
const caps = {
|
|
3000
|
+
platformName: p.platform,
|
|
3001
|
+
"appium:deviceName": p.deviceName,
|
|
3002
|
+
...p.platformVersion !== void 0 && { "appium:platformVersion": p.platformVersion },
|
|
3003
|
+
...p.automationName !== void 0 && { "appium:automationName": p.automationName },
|
|
3004
|
+
...p.appPath !== void 0 && { "appium:app": p.appPath },
|
|
3005
|
+
...p.udid !== void 0 && { "appium:udid": p.udid },
|
|
3006
|
+
...p.noReset !== void 0 && { "appium:noReset": p.noReset },
|
|
3007
|
+
...p.fullReset !== void 0 && { "appium:fullReset": p.fullReset },
|
|
3008
|
+
...p.autoGrantPermissions !== void 0 && { "appium:autoGrantPermissions": p.autoGrantPermissions },
|
|
3009
|
+
...p.autoAcceptAlerts !== void 0 && { "appium:autoAcceptAlerts": p.autoAcceptAlerts },
|
|
3010
|
+
...p.autoDismissAlerts !== void 0 && { "appium:autoDismissAlerts": p.autoDismissAlerts },
|
|
3011
|
+
...p.appWaitActivity !== void 0 && { "appium:appWaitActivity": p.appWaitActivity },
|
|
3012
|
+
...p.newCommandTimeout !== void 0 && { "appium:newCommandTimeout": p.newCommandTimeout },
|
|
3013
|
+
...p.capabilities ?? {}
|
|
3014
|
+
};
|
|
3015
|
+
const config = {
|
|
3016
|
+
protocol: "http",
|
|
3017
|
+
hostname: p.appiumHost ?? "localhost",
|
|
3018
|
+
port: p.appiumPort ?? 4723,
|
|
3019
|
+
path: p.appiumPath ?? "/",
|
|
3020
|
+
capabilities: caps
|
|
3021
|
+
};
|
|
3022
|
+
return `const browser = await remote(${indentJson(config)});`;
|
|
3023
|
+
}
|
|
3024
|
+
case "navigate":
|
|
3025
|
+
return `await browser.url('${escapeStr(p.url)}');`;
|
|
3026
|
+
case "click_element":
|
|
3027
|
+
return `await browser.$('${escapeStr(p.selector)}').click();`;
|
|
3028
|
+
case "set_value":
|
|
3029
|
+
return `await browser.$('${escapeStr(p.selector)}').setValue('${escapeStr(p.value)}');`;
|
|
3030
|
+
case "scroll": {
|
|
3031
|
+
const scrollAmount = p.direction === "down" ? p.pixels : -p.pixels;
|
|
3032
|
+
return `await browser.execute(() => window.scrollBy(0, ${scrollAmount}));`;
|
|
3033
|
+
}
|
|
3034
|
+
case "tap_element":
|
|
3035
|
+
if (p.selector !== void 0) {
|
|
3036
|
+
return `await browser.$('${escapeStr(p.selector)}').click();`;
|
|
3037
|
+
}
|
|
3038
|
+
return `await browser.tap({ x: ${p.x}, y: ${p.y} });`;
|
|
3039
|
+
case "swipe":
|
|
3040
|
+
return `await browser.execute('mobile: swipe', { direction: '${escapeStr(p.direction)}' });`;
|
|
3041
|
+
case "drag_and_drop":
|
|
3042
|
+
if (p.targetSelector !== void 0) {
|
|
3043
|
+
return `await browser.$('${escapeStr(p.sourceSelector)}').dragAndDrop(browser.$('${escapeStr(p.targetSelector)}'));`;
|
|
3044
|
+
}
|
|
3045
|
+
return `await browser.$('${escapeStr(p.sourceSelector)}').dragAndDrop({ x: ${p.x}, y: ${p.y} });`;
|
|
3046
|
+
default:
|
|
3047
|
+
return `// [unknown tool] ${step.tool}`;
|
|
3048
|
+
}
|
|
3049
|
+
}
|
|
3050
|
+
function generateCode(history) {
|
|
3051
|
+
const steps = history.steps.map(generateStep).join("\n");
|
|
3052
|
+
return `import { remote } from 'webdriverio';
|
|
3053
|
+
|
|
3054
|
+
${steps}
|
|
3055
|
+
|
|
3056
|
+
await browser.deleteSession();`;
|
|
3057
|
+
}
|
|
3058
|
+
|
|
3059
|
+
// src/recording/resources.ts
|
|
3060
|
+
function getCurrentSessionId() {
|
|
3061
|
+
return getBrowser.__state?.currentSession ?? null;
|
|
3062
|
+
}
|
|
3063
|
+
function buildSessionsIndex() {
|
|
3064
|
+
const histories = getSessionHistory();
|
|
3065
|
+
if (histories.size === 0) return "No sessions recorded.";
|
|
3066
|
+
const currentId = getCurrentSessionId();
|
|
3067
|
+
const lines = [`Sessions (${histories.size} total):
|
|
3068
|
+
`];
|
|
3069
|
+
for (const [id, h] of histories) {
|
|
3070
|
+
const ended = h.endedAt ?? "-";
|
|
3071
|
+
const current = id === currentId ? " [current]" : "";
|
|
3072
|
+
lines.push(`- ${id} ${h.type} started: ${h.startedAt} ended: ${ended} ${h.steps.length} steps${current}`);
|
|
3073
|
+
}
|
|
3074
|
+
return lines.join("\n");
|
|
3075
|
+
}
|
|
3076
|
+
function buildCurrentSessionSteps() {
|
|
3077
|
+
const currentId = getCurrentSessionId();
|
|
3078
|
+
if (!currentId) return "No active session.";
|
|
3079
|
+
return buildSessionStepsById(currentId);
|
|
3080
|
+
}
|
|
3081
|
+
function buildSessionStepsById(sessionId) {
|
|
3082
|
+
const history = getSessionHistory().get(sessionId);
|
|
3083
|
+
if (!history) return `Session not found: ${sessionId}`;
|
|
3084
|
+
return formatSessionSteps(history);
|
|
3085
|
+
}
|
|
3086
|
+
function formatSessionSteps(history) {
|
|
3087
|
+
const header = `Session: ${history.sessionId} (${history.type}) \u2014 ${history.steps.length} steps
|
|
3088
|
+
`;
|
|
3089
|
+
const stepLines = history.steps.map((step) => {
|
|
3090
|
+
if (step.tool === "__session_transition__") {
|
|
3091
|
+
return `--- session transitioned to ${step.params.newSessionId ?? "unknown"} at ${step.timestamp} ---`;
|
|
3092
|
+
}
|
|
3093
|
+
const statusLabel = step.status === "ok" ? "[ok] " : "[error]";
|
|
3094
|
+
const params = Object.entries(step.params).map(([k, v]) => `${k}="${v}"`).join(" ");
|
|
3095
|
+
const errorSuffix = step.error ? ` \u2014 ${step.error}` : "";
|
|
3096
|
+
return `${step.index}. ${statusLabel} ${step.tool.padEnd(24)} ${params}${errorSuffix} ${step.durationMs}ms`;
|
|
3097
|
+
});
|
|
3098
|
+
const stepsText = stepLines.length > 0 ? stepLines.join("\n") : "(no steps yet)";
|
|
3099
|
+
const jsCode = generateCode(history);
|
|
3100
|
+
return `${header}
|
|
3101
|
+
Steps:
|
|
3102
|
+
${stepsText}
|
|
3103
|
+
|
|
3104
|
+
--- Generated WebdriverIO JS ---
|
|
3105
|
+
${jsCode}`;
|
|
3106
|
+
}
|
|
3107
|
+
|
|
2648
3108
|
// src/server.ts
|
|
2649
3109
|
console.log = (...args) => console.error("[LOG]", ...args);
|
|
2650
3110
|
console.info = (...args) => console.error("[INFO]", ...args);
|
|
@@ -2659,7 +3119,8 @@ var server = new McpServer({
|
|
|
2659
3119
|
}, {
|
|
2660
3120
|
instructions: "MCP server for browser and mobile app automation using WebDriverIO. Supports Chrome, Firefox, Edge, and Safari browser control plus iOS/Android native app testing via Appium.",
|
|
2661
3121
|
capabilities: {
|
|
2662
|
-
tools: {}
|
|
3122
|
+
tools: {},
|
|
3123
|
+
resources: {}
|
|
2663
3124
|
}
|
|
2664
3125
|
});
|
|
2665
3126
|
var registerTool = (definition, callback) => server.registerTool(definition.name, {
|
|
@@ -2669,19 +3130,21 @@ var registerTool = (definition, callback) => server.registerTool(definition.name
|
|
|
2669
3130
|
registerTool(startBrowserToolDefinition, startBrowserTool);
|
|
2670
3131
|
registerTool(startAppToolDefinition, startAppTool);
|
|
2671
3132
|
registerTool(closeSessionToolDefinition, closeSessionTool);
|
|
2672
|
-
registerTool(
|
|
3133
|
+
registerTool(attachBrowserToolDefinition, attachBrowserTool);
|
|
3134
|
+
registerTool(emulateDeviceToolDefinition, emulateDeviceTool);
|
|
3135
|
+
registerTool(navigateToolDefinition, withRecording("navigate", navigateTool));
|
|
2673
3136
|
registerTool(getVisibleElementsToolDefinition, getVisibleElementsTool);
|
|
2674
3137
|
registerTool(getAccessibilityToolDefinition, getAccessibilityTreeTool);
|
|
2675
|
-
registerTool(scrollToolDefinition, scrollTool);
|
|
2676
|
-
registerTool(clickToolDefinition, clickTool);
|
|
2677
|
-
registerTool(setValueToolDefinition, setValueTool);
|
|
3138
|
+
registerTool(scrollToolDefinition, withRecording("scroll", scrollTool));
|
|
3139
|
+
registerTool(clickToolDefinition, withRecording("click_element", clickTool));
|
|
3140
|
+
registerTool(setValueToolDefinition, withRecording("set_value", setValueTool));
|
|
2678
3141
|
registerTool(takeScreenshotToolDefinition, takeScreenshotTool);
|
|
2679
3142
|
registerTool(getCookiesToolDefinition, getCookiesTool);
|
|
2680
3143
|
registerTool(setCookieToolDefinition, setCookieTool);
|
|
2681
3144
|
registerTool(deleteCookiesToolDefinition, deleteCookiesTool);
|
|
2682
|
-
registerTool(tapElementToolDefinition, tapElementTool);
|
|
2683
|
-
registerTool(swipeToolDefinition, swipeTool);
|
|
2684
|
-
registerTool(dragAndDropToolDefinition, dragAndDropTool);
|
|
3145
|
+
registerTool(tapElementToolDefinition, withRecording("tap_element", tapElementTool));
|
|
3146
|
+
registerTool(swipeToolDefinition, withRecording("swipe", swipeTool));
|
|
3147
|
+
registerTool(dragAndDropToolDefinition, withRecording("drag_and_drop", dragAndDropTool));
|
|
2685
3148
|
registerTool(getAppStateToolDefinition, getAppStateTool);
|
|
2686
3149
|
registerTool(getContextsToolDefinition, getContextsTool);
|
|
2687
3150
|
registerTool(getCurrentContextToolDefinition, getCurrentContextTool);
|
|
@@ -2691,6 +3154,34 @@ registerTool(hideKeyboardToolDefinition, hideKeyboardTool);
|
|
|
2691
3154
|
registerTool(getGeolocationToolDefinition, getGeolocationTool);
|
|
2692
3155
|
registerTool(setGeolocationToolDefinition, setGeolocationTool);
|
|
2693
3156
|
registerTool(executeScriptToolDefinition, executeScriptTool);
|
|
3157
|
+
server.registerResource(
|
|
3158
|
+
"sessions",
|
|
3159
|
+
"wdio://sessions",
|
|
3160
|
+
{ description: "Index of all browser and app sessions with step counts" },
|
|
3161
|
+
async () => ({
|
|
3162
|
+
contents: [{ uri: "wdio://sessions", mimeType: "text/plain", text: buildSessionsIndex() }]
|
|
3163
|
+
})
|
|
3164
|
+
);
|
|
3165
|
+
server.registerResource(
|
|
3166
|
+
"session-current-steps",
|
|
3167
|
+
"wdio://session/current/steps",
|
|
3168
|
+
{ description: "Steps for the currently active session with generated WebdriverIO JS" },
|
|
3169
|
+
async () => ({
|
|
3170
|
+
contents: [{ uri: "wdio://session/current/steps", mimeType: "text/plain", text: buildCurrentSessionSteps() }]
|
|
3171
|
+
})
|
|
3172
|
+
);
|
|
3173
|
+
server.registerResource(
|
|
3174
|
+
"session-steps",
|
|
3175
|
+
new ResourceTemplate("wdio://session/{sessionId}/steps", { list: void 0 }),
|
|
3176
|
+
{ description: "Steps for a specific session by ID with generated WebdriverIO JS" },
|
|
3177
|
+
async (uri, { sessionId }) => ({
|
|
3178
|
+
contents: [{
|
|
3179
|
+
uri: uri.href,
|
|
3180
|
+
mimeType: "text/plain",
|
|
3181
|
+
text: buildSessionStepsById(sessionId)
|
|
3182
|
+
}]
|
|
3183
|
+
})
|
|
3184
|
+
);
|
|
2694
3185
|
async function main() {
|
|
2695
3186
|
const transport = new StdioServerTransport();
|
|
2696
3187
|
await server.connect(transport);
|