@wdio/mcp 2.3.1 → 2.4.0

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 CHANGED
@@ -83,6 +83,8 @@ appium
83
83
  - **Page Analysis**: Get visible elements, accessibility trees, take screenshots
84
84
  - **Cookie Management**: Get, set, and delete cookies
85
85
  - **Scrolling**: Smooth scrolling with configurable distances
86
+ - **Attach to running Chrome**: Connect to an existing Chrome window via `--remote-debugging-port` — ideal for testing authenticated or pre-configured sessions
87
+ - **Device emulation**: Apply mobile/tablet presets (iPhone 15, Pixel 7, etc.) to simulate responsive layouts without a physical device
86
88
 
87
89
  ### Mobile App Automation (iOS/Android)
88
90
 
@@ -102,6 +104,8 @@ appium
102
104
  | `start_browser` | Start a browser session (Chrome, Firefox, Edge, Safari; headless/headed, custom dimensions) |
103
105
  | `start_app_session` | Start an iOS or Android app session via Appium (supports state preservation via noReset) |
104
106
  | `close_session` | Close or detach from the current browser or app session (supports detach mode) |
107
+ | `attach_browser` | Attach to a running Chrome instance via `--remote-debugging-port` (CDP) |
108
+ | `emulate_device` | Emulate a mobile/tablet device preset (viewport, DPR, UA, touch); requires BiDi session |
105
109
 
106
110
  ### Navigation & Page Interaction (Web & Mobile)
107
111
 
@@ -231,6 +235,37 @@ start_browser({
231
235
  })
232
236
  ```
233
237
 
238
+ **Attach to a running Chrome instance:**
239
+
240
+ ```
241
+ // First, launch Chrome with remote debugging enabled:
242
+ //
243
+ // macOS (must quit Chrome first — open -a ignores args if Chrome is already running):
244
+ // pkill -x "Google Chrome" && sleep 1
245
+ // /Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome \
246
+ // --remote-debugging-port=9222 \
247
+ // --user-data-dir=/tmp/chrome-debug &
248
+ //
249
+ // Linux:
250
+ // google-chrome --remote-debugging-port=9222 --user-data-dir=/tmp/chrome-debug &
251
+ //
252
+ // Verify it's ready: curl http://localhost:9222/json/version
253
+ attach_browser()
254
+ attach_browser({port: 9333})
255
+ attach_browser({port: 9222, navigationUrl: 'https://app.example.com'})
256
+ ```
257
+
258
+ **Device emulation (requires BiDi session):**
259
+
260
+ ```
261
+ // Device emulation (requires BiDi session)
262
+ start_browser({capabilities: {webSocketUrl: true}})
263
+ emulate_device() // list available presets
264
+ emulate_device({device: 'iPhone 15'}) // activate emulation
265
+ emulate_device({device: 'Pixel 7'}) // switch device
266
+ emulate_device({device: 'reset'}) // restore desktop defaults
267
+ ```
268
+
234
269
  ### Mobile App Automation
235
270
 
236
271
  **Testing an iOS app on simulator:**
package/lib/server.js CHANGED
@@ -164,13 +164,14 @@ var closeSessionTool = async (args = {}) => {
164
164
  const browser = getBrowser();
165
165
  const sessionId = state.currentSession;
166
166
  const metadata = state.sessionMetadata.get(sessionId);
167
- if (!args.detach) {
167
+ const effectiveDetach = args.detach || !!metadata?.isAttached;
168
+ if (!effectiveDetach) {
168
169
  await browser.deleteSession();
169
170
  }
170
171
  state.browsers.delete(sessionId);
171
172
  state.sessionMetadata.delete(sessionId);
172
173
  state.currentSession = null;
173
- const action = args.detach ? "detached from" : "closed";
174
+ const action = effectiveDetach ? "detached from" : "closed";
174
175
  const note = args.detach && !metadata?.isAttached ? "\nNote: Session will remain active on Appium server." : "";
175
176
  return {
176
177
  content: [{ type: "text", text: `Session ${sessionId} ${action}${note}` }]
@@ -375,7 +376,7 @@ var startAppToolDefinition = {
375
376
  udid: z5.string().optional().describe('Unique Device Identifier for iOS real device testing (e.g., "00008030-001234567890002E")'),
376
377
  noReset: z5.boolean().optional().describe("Do not reset app state before session (preserves app data). Default: false"),
377
378
  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."),
379
+ 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
380
  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
381
  }
381
382
  };
@@ -404,7 +405,7 @@ var startAppTool = async (args) => {
404
405
  udid,
405
406
  noReset,
406
407
  fullReset,
407
- newCommandTimeout,
408
+ newCommandTimeout = 300,
408
409
  capabilities: userCapabilities = {}
409
410
  } = args;
410
411
  if (!appPath && noReset !== true) {
@@ -2569,6 +2570,186 @@ var executeScriptTool = async (args) => {
2569
2570
  }
2570
2571
  };
2571
2572
 
2573
+ // src/tools/attach-browser.tool.ts
2574
+ import { remote as remote3 } from "webdriverio";
2575
+ import { z as z16 } from "zod";
2576
+ var attachBrowserToolDefinition = {
2577
+ name: "attach_browser",
2578
+ description: `Attach to a Chrome instance already running with --remote-debugging-port.
2579
+
2580
+ Start Chrome first (quit any running Chrome instance before launching):
2581
+
2582
+ macOS \u2014 with real profile (preserves extensions, cookies, logins):
2583
+ pkill -x "Google Chrome" && sleep 1
2584
+ /Applications/Google Chrome.app/Contents/MacOS/Google Chrome --remote-debugging-port=9222 --user-data-dir="$HOME/Library/Application Support/Google/Chrome" --profile-directory=Default &
2585
+
2586
+ macOS \u2014 with fresh profile (lightweight, no extensions):
2587
+ pkill -x "Google Chrome" && sleep 1
2588
+ /Applications/Google Chrome.app/Contents/MacOS/Google Chrome --remote-debugging-port=9222 --user-data-dir=/tmp/chrome-debug &
2589
+
2590
+ Linux \u2014 with real profile:
2591
+ google-chrome --remote-debugging-port=9222 --user-data-dir="$HOME/.config/google-chrome" --profile-directory=Default &
2592
+
2593
+ Linux \u2014 with fresh profile:
2594
+ google-chrome --remote-debugging-port=9222 --user-data-dir=/tmp/chrome-debug &
2595
+
2596
+ Verify Chrome is ready: curl http://localhost:9222/json/version
2597
+
2598
+ 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.`,
2599
+ inputSchema: {
2600
+ port: z16.number().default(9222).describe("Chrome remote debugging port (default: 9222)"),
2601
+ host: z16.string().default("localhost").describe("Host where Chrome is running (default: localhost)"),
2602
+ 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)'),
2603
+ navigationUrl: z16.string().optional().describe("URL to navigate to immediately after attaching")
2604
+ }
2605
+ };
2606
+ async function getActiveTabUrl(host, port) {
2607
+ try {
2608
+ const res = await fetch(`http://${host}:${port}/json`);
2609
+ const tabs = await res.json();
2610
+ const page = tabs.find((t) => t.type === "page" && t.url && !t.url.startsWith("devtools://"));
2611
+ return page?.url ?? null;
2612
+ } catch {
2613
+ return null;
2614
+ }
2615
+ }
2616
+ var attachBrowserTool = async ({
2617
+ port = 9222,
2618
+ host = "localhost",
2619
+ userDataDir = "/tmp/chrome-debug",
2620
+ navigationUrl
2621
+ }) => {
2622
+ try {
2623
+ const state2 = getBrowser.__state;
2624
+ const activeUrl = navigationUrl ?? await getActiveTabUrl(host, port);
2625
+ const browser = await remote3({
2626
+ capabilities: {
2627
+ browserName: "chrome",
2628
+ "goog:chromeOptions": {
2629
+ debuggerAddress: `${host}:${port}`,
2630
+ args: [`--user-data-dir=${userDataDir}`]
2631
+ }
2632
+ }
2633
+ });
2634
+ const { sessionId } = browser;
2635
+ state2.browsers.set(sessionId, browser);
2636
+ state2.currentSession = sessionId;
2637
+ state2.sessionMetadata.set(sessionId, {
2638
+ type: "browser",
2639
+ capabilities: browser.capabilities,
2640
+ isAttached: true
2641
+ });
2642
+ if (activeUrl) {
2643
+ await browser.url(activeUrl);
2644
+ }
2645
+ const title = await browser.getTitle();
2646
+ const url = await browser.getUrl();
2647
+ return {
2648
+ content: [{
2649
+ type: "text",
2650
+ text: `Attached to Chrome on ${host}:${port}
2651
+ Session ID: ${sessionId}
2652
+ Current page: "${title}" (${url})`
2653
+ }]
2654
+ };
2655
+ } catch (e) {
2656
+ return {
2657
+ content: [{ type: "text", text: `Error attaching to browser: ${e}` }]
2658
+ };
2659
+ }
2660
+ };
2661
+
2662
+ // src/tools/emulate-device.tool.ts
2663
+ import { z as z17 } from "zod";
2664
+ var restoreFunctions = /* @__PURE__ */ new Map();
2665
+ var emulateDeviceToolDefinition = {
2666
+ name: "emulate_device",
2667
+ description: `Emulate a mobile or tablet device in the current browser session (sets viewport, DPR, user-agent, touch events).
2668
+
2669
+ Requires a BiDi-enabled session: start_browser({ capabilities: { webSocketUrl: true } })
2670
+
2671
+ Usage:
2672
+ emulate_device() \u2014 list available device presets
2673
+ emulate_device({ device: "iPhone 15" }) \u2014 activate emulation
2674
+ emulate_device({ device: "reset" }) \u2014 restore desktop defaults`,
2675
+ inputSchema: {
2676
+ device: z17.string().optional().describe(
2677
+ 'Device preset name (e.g. "iPhone 15", "Pixel 7"). Omit to list available presets. Pass "reset" to restore desktop defaults.'
2678
+ )
2679
+ }
2680
+ };
2681
+ var emulateDeviceTool = async ({
2682
+ device
2683
+ }) => {
2684
+ try {
2685
+ const browser = getBrowser();
2686
+ const state2 = getBrowser.__state;
2687
+ const sessionId = state2.currentSession;
2688
+ const metadata = state2.sessionMetadata.get(sessionId);
2689
+ if (metadata?.type === "ios" || metadata?.type === "android") {
2690
+ return {
2691
+ content: [{ type: "text", text: "Error: emulate_device is only supported for web browser sessions, not iOS/Android." }]
2692
+ };
2693
+ }
2694
+ if (!browser.isBidi) {
2695
+ return {
2696
+ content: [{
2697
+ type: "text",
2698
+ text: "Error: emulate_device requires a BiDi-enabled session.\nRestart the browser with: start_browser({ capabilities: { webSocketUrl: true } })"
2699
+ }]
2700
+ };
2701
+ }
2702
+ if (!device) {
2703
+ try {
2704
+ await browser.emulate("device", "\0");
2705
+ } catch (e) {
2706
+ const msg = String(e);
2707
+ const match = msg.match(/please use one of the following: (.+)$/);
2708
+ if (match) {
2709
+ const names = match[1].split(", ").sort();
2710
+ return {
2711
+ content: [{ type: "text", text: `Available devices (${names.length}):
2712
+ ${names.join("\n")}` }]
2713
+ };
2714
+ }
2715
+ return { content: [{ type: "text", text: `Error listing devices: ${e}` }] };
2716
+ }
2717
+ return { content: [{ type: "text", text: "Could not retrieve device list." }] };
2718
+ }
2719
+ if (device === "reset") {
2720
+ const restoreFn = restoreFunctions.get(sessionId);
2721
+ if (!restoreFn) {
2722
+ return { content: [{ type: "text", text: "No active device emulation to reset." }] };
2723
+ }
2724
+ await restoreFn();
2725
+ restoreFunctions.delete(sessionId);
2726
+ return { content: [{ type: "text", text: "Device emulation reset to desktop defaults." }] };
2727
+ }
2728
+ try {
2729
+ const restoreFn = await browser.emulate("device", device);
2730
+ restoreFunctions.set(sessionId, restoreFn);
2731
+ return {
2732
+ content: [{ type: "text", text: `Emulating "${device}".` }]
2733
+ };
2734
+ } catch (e) {
2735
+ const msg = String(e);
2736
+ if (msg.includes("Unknown device name")) {
2737
+ return {
2738
+ content: [{
2739
+ type: "text",
2740
+ text: `Error: Unknown device "${device}". Call emulate_device() with no arguments to list valid names.`
2741
+ }]
2742
+ };
2743
+ }
2744
+ return { content: [{ type: "text", text: `Error: ${e}` }] };
2745
+ }
2746
+ } catch (e) {
2747
+ return {
2748
+ content: [{ type: "text", text: `Error: ${e}` }]
2749
+ };
2750
+ }
2751
+ };
2752
+
2572
2753
  // package.json
2573
2754
  var package_default = {
2574
2755
  name: "@wdio/mcp",
@@ -2577,7 +2758,7 @@ var package_default = {
2577
2758
  type: "git",
2578
2759
  url: "git://github.com/webdriverio/mcp.git"
2579
2760
  },
2580
- version: "2.3.0",
2761
+ version: "2.3.1",
2581
2762
  description: "MCP server with WebdriverIO for browser and mobile app automation (iOS/Android via Appium)",
2582
2763
  main: "./lib/server.js",
2583
2764
  module: "./lib/server.js",
@@ -2669,6 +2850,8 @@ var registerTool = (definition, callback) => server.registerTool(definition.name
2669
2850
  registerTool(startBrowserToolDefinition, startBrowserTool);
2670
2851
  registerTool(startAppToolDefinition, startAppTool);
2671
2852
  registerTool(closeSessionToolDefinition, closeSessionTool);
2853
+ registerTool(attachBrowserToolDefinition, attachBrowserTool);
2854
+ registerTool(emulateDeviceToolDefinition, emulateDeviceTool);
2672
2855
  registerTool(navigateToolDefinition, navigateTool);
2673
2856
  registerTool(getVisibleElementsToolDefinition, getVisibleElementsTool);
2674
2857
  registerTool(getAccessibilityToolDefinition, getAccessibilityTreeTool);