@wdio/mcp 2.4.0 → 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,6 +193,10 @@ var closeSessionTool = async (args = {}) => {
164
193
  const browser = getBrowser();
165
194
  const sessionId = state.currentSession;
166
195
  const metadata = state.sessionMetadata.get(sessionId);
196
+ const history = state.sessionHistory.get(sessionId);
197
+ if (history) {
198
+ history.endedAt = (/* @__PURE__ */ new Date()).toISOString();
199
+ }
167
200
  const effectiveDetach = args.detach || !!metadata?.isAttached;
168
201
  if (!effectiveDetach) {
169
202
  await browser.deleteSession();
@@ -464,12 +497,58 @@ var startAppTool = async (args) => {
464
497
  const shouldAutoDetach = noReset === true || !appPath;
465
498
  const state2 = getState();
466
499
  state2.browsers.set(sessionId, browser);
467
- state2.currentSession = sessionId;
468
500
  state2.sessionMetadata.set(sessionId, {
469
501
  type: platform.toLowerCase(),
470
502
  capabilities: mergedCapabilities,
471
503
  isAttached: shouldAutoDetach
472
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;
473
552
  const appInfo = appPath ? `
474
553
  App: ${appPath}` : "\nApp: (connected to running app)";
475
554
  const detachNote = shouldAutoDetach ? "\n\n(Auto-detach enabled: session will be preserved on close. Use close_session({ detach: false }) to force terminate.)" : "";
@@ -1905,7 +1984,7 @@ var accessibilityTreeScript = () => (function() {
1905
1984
  if (ariaLevel) return parseInt(ariaLevel, 10);
1906
1985
  return void 0;
1907
1986
  }
1908
- function getState2(el) {
1987
+ function getState3(el) {
1909
1988
  const inputEl = el;
1910
1989
  const isCheckable = ["input", "menuitemcheckbox", "menuitemradio"].includes(el.tagName.toLowerCase()) || ["checkbox", "radio", "switch"].includes(el.getAttribute("role") || "");
1911
1990
  return {
@@ -1933,7 +2012,7 @@ var accessibilityTreeScript = () => (function() {
1933
2012
  const isLandmark = LANDMARK_ROLES.has(role);
1934
2013
  const hasIdentity = !!(name || isLandmark);
1935
2014
  const selector = hasIdentity ? getSelector(el) : "";
1936
- const node = { role, name, selector, level: getLevel(el) ?? "", ...getState2(el) };
2015
+ const node = { role, name, selector, level: getLevel(el) ?? "", ...getState3(el) };
1937
2016
  result.push(node);
1938
2017
  for (const child of Array.from(el.children)) {
1939
2018
  walk(child, depth + 1);
@@ -2758,7 +2837,7 @@ var package_default = {
2758
2837
  type: "git",
2759
2838
  url: "git://github.com/webdriverio/mcp.git"
2760
2839
  },
2761
- version: "2.3.1",
2840
+ version: "2.4.0",
2762
2841
  description: "MCP server with WebdriverIO for browser and mobile app automation (iOS/Android via Appium)",
2763
2842
  main: "./lib/server.js",
2764
2843
  module: "./lib/server.js",
@@ -2826,6 +2905,206 @@ var package_default = {
2826
2905
  packageManager: "pnpm@10.12.4"
2827
2906
  };
2828
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
+
2829
3108
  // src/server.ts
2830
3109
  console.log = (...args) => console.error("[LOG]", ...args);
2831
3110
  console.info = (...args) => console.error("[INFO]", ...args);
@@ -2840,7 +3119,8 @@ var server = new McpServer({
2840
3119
  }, {
2841
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.",
2842
3121
  capabilities: {
2843
- tools: {}
3122
+ tools: {},
3123
+ resources: {}
2844
3124
  }
2845
3125
  });
2846
3126
  var registerTool = (definition, callback) => server.registerTool(definition.name, {
@@ -2852,19 +3132,19 @@ registerTool(startAppToolDefinition, startAppTool);
2852
3132
  registerTool(closeSessionToolDefinition, closeSessionTool);
2853
3133
  registerTool(attachBrowserToolDefinition, attachBrowserTool);
2854
3134
  registerTool(emulateDeviceToolDefinition, emulateDeviceTool);
2855
- registerTool(navigateToolDefinition, navigateTool);
3135
+ registerTool(navigateToolDefinition, withRecording("navigate", navigateTool));
2856
3136
  registerTool(getVisibleElementsToolDefinition, getVisibleElementsTool);
2857
3137
  registerTool(getAccessibilityToolDefinition, getAccessibilityTreeTool);
2858
- registerTool(scrollToolDefinition, scrollTool);
2859
- registerTool(clickToolDefinition, clickTool);
2860
- registerTool(setValueToolDefinition, setValueTool);
3138
+ registerTool(scrollToolDefinition, withRecording("scroll", scrollTool));
3139
+ registerTool(clickToolDefinition, withRecording("click_element", clickTool));
3140
+ registerTool(setValueToolDefinition, withRecording("set_value", setValueTool));
2861
3141
  registerTool(takeScreenshotToolDefinition, takeScreenshotTool);
2862
3142
  registerTool(getCookiesToolDefinition, getCookiesTool);
2863
3143
  registerTool(setCookieToolDefinition, setCookieTool);
2864
3144
  registerTool(deleteCookiesToolDefinition, deleteCookiesTool);
2865
- registerTool(tapElementToolDefinition, tapElementTool);
2866
- registerTool(swipeToolDefinition, swipeTool);
2867
- registerTool(dragAndDropToolDefinition, dragAndDropTool);
3145
+ registerTool(tapElementToolDefinition, withRecording("tap_element", tapElementTool));
3146
+ registerTool(swipeToolDefinition, withRecording("swipe", swipeTool));
3147
+ registerTool(dragAndDropToolDefinition, withRecording("drag_and_drop", dragAndDropTool));
2868
3148
  registerTool(getAppStateToolDefinition, getAppStateTool);
2869
3149
  registerTool(getContextsToolDefinition, getContextsTool);
2870
3150
  registerTool(getCurrentContextToolDefinition, getCurrentContextTool);
@@ -2874,6 +3154,34 @@ registerTool(hideKeyboardToolDefinition, hideKeyboardTool);
2874
3154
  registerTool(getGeolocationToolDefinition, getGeolocationTool);
2875
3155
  registerTool(setGeolocationToolDefinition, setGeolocationTool);
2876
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
+ );
2877
3185
  async function main() {
2878
3186
  const transport = new StdioServerTransport();
2879
3187
  await server.connect(transport);