@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/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
- if (!args.detach) {
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 = args.detach ? "detached from" : "closed";
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: 60. Set to 300 for 5 minutes, etc."),
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 getState2(el) {
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) ?? "", ...getState2(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.3.0",
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(navigateToolDefinition, navigateTool);
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);