@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 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 = platform === "iOS" ? buildIOSCapabilities(appPath, {
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: platform.toLowerCase(),
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: platform.toLowerCase(),
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: `${platform} app session started with sessionId: ${sessionId}
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, platform) {
1112
- const containerList = platform === "android" ? ANDROID_LAYOUT_CONTAINERS : IOS_LAYOUT_CONTAINERS;
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, platform) {
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 (platform === "android") {
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(platform, includeContainers = false) {
1168
- const layoutContainers = platform === "android" ? ANDROID_LAYOUT_CONTAINERS : IOS_LAYOUT_CONTAINERS;
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, platform) {
1539
- return platform === "android" ? parseAndroidBounds(element.attributes.bounds || "") : parseIOSBounds(element.attributes);
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, platform, options = {}) {
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(platform, includeContainers),
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 platform = browser.isAndroid ? "android" : "ios";
1721
- elements = await getMobileVisibleElements(browser, platform, { includeContainers, includeBounds });
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
- Start Chrome first (quit any running Chrome instance before launching):
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 getActiveTabUrl(host, port) {
2731
+ async function closeStaleMappers(host, port) {
2674
2732
  try {
2675
2733
  const res = await fetch(`http://${host}:${port}/json`);
2676
- const tabs = await res.json();
2677
- const page = tabs.find((t) => t.type === "page" && t.url && !t.url.startsWith("devtools://"));
2678
- return page?.url ?? null;
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 null;
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
- const activeUrl = navigationUrl ?? await getActiveTabUrl(host, port);
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 (activeUrl) {
2723
- await browser.url(activeUrl);
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/emulate-device.tool.ts
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: z17.string().optional().describe(
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: [{ uri: "wdio://session/current/steps", mimeType: "application/json", text: payload?.stepsJson ?? '{"error":"No active session"}' }]
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: [{ uri: "wdio://session/current/code", mimeType: "text/plain", text: payload?.generatedJs ?? "// No active session" }]
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: [{ uri: uri.href, mimeType: "application/json", text: payload?.stepsJson ?? `{"error":"Session not found: ${sessionId}"}` }]
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: [{ uri: uri.href, mimeType: "text/plain", text: payload?.generatedJs ?? `// Session not found: ${sessionId}` }]
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
  );