@wdio/mcp 2.4.0 → 2.5.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/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,33 @@ 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
+ });
162
+ state.currentSession = sessionId;
141
163
  let sizeNote = "";
142
164
  try {
143
165
  await wdioBrowser.setWindowSize(windowWidth, windowHeight);
@@ -164,6 +186,10 @@ var closeSessionTool = async (args = {}) => {
164
186
  const browser = getBrowser();
165
187
  const sessionId = state.currentSession;
166
188
  const metadata = state.sessionMetadata.get(sessionId);
189
+ const history = state.sessionHistory.get(sessionId);
190
+ if (history) {
191
+ history.endedAt = (/* @__PURE__ */ new Date()).toISOString();
192
+ }
167
193
  const effectiveDetach = args.detach || !!metadata?.isAttached;
168
194
  if (!effectiveDetach) {
169
195
  await browser.deleteSession();
@@ -178,6 +204,7 @@ var closeSessionTool = async (args = {}) => {
178
204
  };
179
205
  } catch (e) {
180
206
  return {
207
+ isError: true,
181
208
  content: [{ type: "text", text: `Error closing session: ${e}` }]
182
209
  };
183
210
  }
@@ -201,6 +228,7 @@ var navigateTool = async ({ url }) => {
201
228
  };
202
229
  } catch (e) {
203
230
  return {
231
+ isError: true,
204
232
  content: [{ type: "text", text: `Error navigating: ${e}` }]
205
233
  };
206
234
  }
@@ -231,6 +259,7 @@ var clickAction = async (selector, timeout, scrollToView = true) => {
231
259
  };
232
260
  } catch (e) {
233
261
  return {
262
+ isError: true,
234
263
  content: [{ type: "text", text: `Error clicking element: ${e}` }]
235
264
  };
236
265
  }
@@ -264,6 +293,7 @@ var setValueTool = async ({ selector, value, scrollToView = true, timeout = defa
264
293
  };
265
294
  } catch (e) {
266
295
  return {
296
+ isError: true,
267
297
  content: [{ type: "text", text: `Error entering text: ${e}` }]
268
298
  };
269
299
  }
@@ -310,7 +340,7 @@ function buildIOSCapabilities(appPath, options) {
310
340
  capabilities["appium:autoAcceptAlerts"] = void 0;
311
341
  }
312
342
  for (const [key, value] of Object.entries(options)) {
313
- if (!["deviceName", "platformVersion", "automationName", "autoAcceptAlerts", "autoDismissAlerts", "udid", "noReset", "fullReset", "newCommandTimeout"].includes(
343
+ if (!["deviceName", "platformVersion", "automationName", "autoGrantPermissions", "autoAcceptAlerts", "autoDismissAlerts", "udid", "noReset", "fullReset", "newCommandTimeout"].includes(
314
344
  key
315
345
  )) {
316
346
  capabilities[`appium:${key}`] = value;
@@ -347,7 +377,7 @@ function buildAndroidCapabilities(appPath, options) {
347
377
  capabilities["appium:appWaitActivity"] = options.appWaitActivity;
348
378
  }
349
379
  for (const [key, value] of Object.entries(options)) {
350
- if (!["deviceName", "platformVersion", "automationName", "autoGrantPermissions", "appWaitActivity", "noReset", "fullReset", "newCommandTimeout"].includes(
380
+ if (!["deviceName", "platformVersion", "automationName", "autoGrantPermissions", "autoAcceptAlerts", "autoDismissAlerts", "appWaitActivity", "noReset", "fullReset", "newCommandTimeout"].includes(
351
381
  key
352
382
  )) {
353
383
  capabilities[`appium:${key}`] = value;
@@ -464,12 +494,34 @@ var startAppTool = async (args) => {
464
494
  const shouldAutoDetach = noReset === true || !appPath;
465
495
  const state2 = getState();
466
496
  state2.browsers.set(sessionId, browser);
467
- state2.currentSession = sessionId;
468
497
  state2.sessionMetadata.set(sessionId, {
469
498
  type: platform.toLowerCase(),
470
499
  capabilities: mergedCapabilities,
471
500
  isAttached: shouldAutoDetach
472
501
  });
502
+ if (state2.currentSession && state2.currentSession !== sessionId) {
503
+ const outgoing = state2.sessionHistory.get(state2.currentSession);
504
+ if (outgoing) {
505
+ outgoing.steps.push({
506
+ index: outgoing.steps.length + 1,
507
+ tool: "__session_transition__",
508
+ params: { newSessionId: sessionId },
509
+ status: "ok",
510
+ durationMs: 0,
511
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
512
+ });
513
+ outgoing.endedAt = (/* @__PURE__ */ new Date()).toISOString();
514
+ }
515
+ }
516
+ state2.sessionHistory.set(sessionId, {
517
+ sessionId,
518
+ type: platform.toLowerCase(),
519
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
520
+ capabilities: mergedCapabilities,
521
+ appiumConfig: { hostname: serverConfig.hostname, port: serverConfig.port, path: serverConfig.path },
522
+ steps: []
523
+ });
524
+ state2.currentSession = sessionId;
473
525
  const appInfo = appPath ? `
474
526
  App: ${appPath}` : "\nApp: (connected to running app)";
475
527
  const detachNote = shouldAutoDetach ? "\n\n(Auto-detach enabled: session will be preserved on close. Use close_session({ detach: false }) to force terminate.)" : "";
@@ -485,6 +537,7 @@ Appium Server: ${serverConfig.hostname}:${serverConfig.port}${serverConfig.path}
485
537
  };
486
538
  } catch (e) {
487
539
  return {
540
+ isError: true,
488
541
  content: [{ type: "text", text: `Error starting app session: ${e}` }]
489
542
  };
490
543
  }
@@ -518,6 +571,7 @@ var scrollTool = async ({ direction, pixels = 500 }) => {
518
571
  };
519
572
  } catch (e) {
520
573
  return {
574
+ isError: true,
521
575
  content: [{ type: "text", text: `Error scrolling: ${e}` }]
522
576
  };
523
577
  }
@@ -1690,6 +1744,7 @@ var getVisibleElementsTool = async (args) => {
1690
1744
  };
1691
1745
  } catch (e) {
1692
1746
  return {
1747
+ isError: true,
1693
1748
  content: [{ type: "text", text: `Error getting visible elements: ${e}` }]
1694
1749
  };
1695
1750
  }
@@ -1905,7 +1960,7 @@ var accessibilityTreeScript = () => (function() {
1905
1960
  if (ariaLevel) return parseInt(ariaLevel, 10);
1906
1961
  return void 0;
1907
1962
  }
1908
- function getState2(el) {
1963
+ function getState3(el) {
1909
1964
  const inputEl = el;
1910
1965
  const isCheckable = ["input", "menuitemcheckbox", "menuitemradio"].includes(el.tagName.toLowerCase()) || ["checkbox", "radio", "switch"].includes(el.getAttribute("role") || "");
1911
1966
  return {
@@ -1933,7 +1988,7 @@ var accessibilityTreeScript = () => (function() {
1933
1988
  const isLandmark = LANDMARK_ROLES.has(role);
1934
1989
  const hasIdentity = !!(name || isLandmark);
1935
1990
  const selector = hasIdentity ? getSelector(el) : "";
1936
- const node = { role, name, selector, level: getLevel(el) ?? "", ...getState2(el) };
1991
+ const node = { role, name, selector, level: getLevel(el) ?? "", ...getState3(el) };
1937
1992
  result.push(node);
1938
1993
  for (const child of Array.from(el.children)) {
1939
1994
  walk(child, depth + 1);
@@ -2009,6 +2064,7 @@ var getAccessibilityTreeTool = async (args) => {
2009
2064
  };
2010
2065
  } catch (e) {
2011
2066
  return {
2067
+ isError: true,
2012
2068
  content: [{ type: "text", text: `Error getting accessibility tree: ${e}` }]
2013
2069
  };
2014
2070
  }
@@ -2069,6 +2125,7 @@ var takeScreenshotTool = async ({ outputPath }) => {
2069
2125
  };
2070
2126
  } catch (e) {
2071
2127
  return {
2128
+ isError: true,
2072
2129
  content: [{ type: "text", text: `Error taking screenshot: ${e.message}` }]
2073
2130
  };
2074
2131
  }
@@ -2108,6 +2165,7 @@ var getCookiesTool = async ({ name }) => {
2108
2165
  };
2109
2166
  } catch (e) {
2110
2167
  return {
2168
+ isError: true,
2111
2169
  content: [{ type: "text", text: `Error getting cookies: ${e}` }]
2112
2170
  };
2113
2171
  }
@@ -2145,6 +2203,7 @@ var setCookieTool = async ({
2145
2203
  };
2146
2204
  } catch (e) {
2147
2205
  return {
2206
+ isError: true,
2148
2207
  content: [{ type: "text", text: `Error setting cookie: ${e}` }]
2149
2208
  };
2150
2209
  }
@@ -2171,6 +2230,7 @@ var deleteCookiesTool = async ({ name }) => {
2171
2230
  };
2172
2231
  } catch (e) {
2173
2232
  return {
2233
+ isError: true,
2174
2234
  content: [{ type: "text", text: `Error deleting cookies: ${e}` }]
2175
2235
  };
2176
2236
  }
@@ -2204,10 +2264,12 @@ var tapElementTool = async (args) => {
2204
2264
  };
2205
2265
  }
2206
2266
  return {
2267
+ isError: true,
2207
2268
  content: [{ type: "text", text: "Error: Must provide either selector or x,y coordinates" }]
2208
2269
  };
2209
2270
  } catch (e) {
2210
2271
  return {
2272
+ isError: true,
2211
2273
  content: [{ type: "text", text: `Error tapping: ${e}` }]
2212
2274
  };
2213
2275
  }
@@ -2242,6 +2304,7 @@ var swipeTool = async (args) => {
2242
2304
  };
2243
2305
  } catch (e) {
2244
2306
  return {
2307
+ isError: true,
2245
2308
  content: [{ type: "text", text: `Error swiping: ${e}` }]
2246
2309
  };
2247
2310
  }
@@ -2275,10 +2338,12 @@ var dragAndDropTool = async (args) => {
2275
2338
  };
2276
2339
  }
2277
2340
  return {
2341
+ isError: true,
2278
2342
  content: [{ type: "text", text: "Error: Must provide either targetSelector or x,y coordinates" }]
2279
2343
  };
2280
2344
  } catch (e) {
2281
2345
  return {
2346
+ isError: true,
2282
2347
  content: [{ type: "text", text: `Error dragging: ${e}` }]
2283
2348
  };
2284
2349
  }
@@ -2316,6 +2381,7 @@ var getAppStateTool = async (args) => {
2316
2381
  };
2317
2382
  } catch (e) {
2318
2383
  return {
2384
+ isError: true,
2319
2385
  content: [{ type: "text", text: `Error getting app state: ${e}` }]
2320
2386
  };
2321
2387
  }
@@ -2357,6 +2423,7 @@ ${contexts.map((ctx, idx) => `${idx + 1}. ${ctx}`).join("\n")}`
2357
2423
  };
2358
2424
  } catch (e) {
2359
2425
  return {
2426
+ isError: true,
2360
2427
  content: [{ type: "text", text: `Error getting contexts: ${e}` }]
2361
2428
  };
2362
2429
  }
@@ -2370,6 +2437,7 @@ var getCurrentContextTool = async () => {
2370
2437
  };
2371
2438
  } catch (e) {
2372
2439
  return {
2440
+ isError: true,
2373
2441
  content: [{ type: "text", text: `Error getting current context: ${e}` }]
2374
2442
  };
2375
2443
  }
@@ -2385,14 +2453,7 @@ var switchContextTool = async (args) => {
2385
2453
  if (index >= 0 && index < contexts.length) {
2386
2454
  targetContext = contexts[index];
2387
2455
  } else {
2388
- return {
2389
- content: [
2390
- {
2391
- type: "text",
2392
- text: `Error: Invalid context index ${context}. Available contexts: ${contexts.length}`
2393
- }
2394
- ]
2395
- };
2456
+ throw new Error(`Error: Invalid context index ${context}. Available contexts: ${contexts.length}`);
2396
2457
  }
2397
2458
  }
2398
2459
  await browser.switchContext(targetContext);
@@ -2401,6 +2462,7 @@ var switchContextTool = async (args) => {
2401
2462
  };
2402
2463
  } catch (e) {
2403
2464
  return {
2465
+ isError: true,
2404
2466
  content: [{ type: "text", text: `Error switching context: ${e}` }]
2405
2467
  };
2406
2468
  }
@@ -2444,6 +2506,7 @@ var rotateDeviceTool = async (args) => {
2444
2506
  };
2445
2507
  } catch (e) {
2446
2508
  return {
2509
+ isError: true,
2447
2510
  content: [{ type: "text", text: `Error rotating device: ${e}` }]
2448
2511
  };
2449
2512
  }
@@ -2457,6 +2520,7 @@ var hideKeyboardTool = async () => {
2457
2520
  };
2458
2521
  } catch (e) {
2459
2522
  return {
2523
+ isError: true,
2460
2524
  content: [{ type: "text", text: `Error hiding keyboard: ${e}` }]
2461
2525
  };
2462
2526
  }
@@ -2478,6 +2542,7 @@ var getGeolocationTool = async () => {
2478
2542
  };
2479
2543
  } catch (e) {
2480
2544
  return {
2545
+ isError: true,
2481
2546
  content: [{ type: "text", text: `Error getting geolocation: ${e}` }]
2482
2547
  };
2483
2548
  }
@@ -2500,6 +2565,7 @@ var setGeolocationTool = async (args) => {
2500
2565
  };
2501
2566
  } catch (e) {
2502
2567
  return {
2568
+ isError: true,
2503
2569
  content: [{ type: "text", text: `Error setting geolocation: ${e}` }]
2504
2570
  };
2505
2571
  }
@@ -2565,6 +2631,7 @@ var executeScriptTool = async (args) => {
2565
2631
  };
2566
2632
  } catch (e) {
2567
2633
  return {
2634
+ isError: true,
2568
2635
  content: [{ type: "text", text: `Error executing script: ${e}` }]
2569
2636
  };
2570
2637
  }
@@ -2639,6 +2706,19 @@ var attachBrowserTool = async ({
2639
2706
  capabilities: browser.capabilities,
2640
2707
  isAttached: true
2641
2708
  });
2709
+ state2.sessionHistory.set(sessionId, {
2710
+ sessionId,
2711
+ type: "browser",
2712
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
2713
+ capabilities: {
2714
+ browserName: "chrome",
2715
+ "goog:chromeOptions": {
2716
+ debuggerAddress: `${host}:${port}`,
2717
+ args: [`--user-data-dir=${userDataDir}`]
2718
+ }
2719
+ },
2720
+ steps: []
2721
+ });
2642
2722
  if (activeUrl) {
2643
2723
  await browser.url(activeUrl);
2644
2724
  }
@@ -2654,6 +2734,7 @@ Current page: "${title}" (${url})`
2654
2734
  };
2655
2735
  } catch (e) {
2656
2736
  return {
2737
+ isError: true,
2657
2738
  content: [{ type: "text", text: `Error attaching to browser: ${e}` }]
2658
2739
  };
2659
2740
  }
@@ -2688,11 +2769,13 @@ var emulateDeviceTool = async ({
2688
2769
  const metadata = state2.sessionMetadata.get(sessionId);
2689
2770
  if (metadata?.type === "ios" || metadata?.type === "android") {
2690
2771
  return {
2772
+ isError: true,
2691
2773
  content: [{ type: "text", text: "Error: emulate_device is only supported for web browser sessions, not iOS/Android." }]
2692
2774
  };
2693
2775
  }
2694
2776
  if (!browser.isBidi) {
2695
2777
  return {
2778
+ isError: true,
2696
2779
  content: [{
2697
2780
  type: "text",
2698
2781
  text: "Error: emulate_device requires a BiDi-enabled session.\nRestart the browser with: start_browser({ capabilities: { webSocketUrl: true } })"
@@ -2712,7 +2795,7 @@ var emulateDeviceTool = async ({
2712
2795
  ${names.join("\n")}` }]
2713
2796
  };
2714
2797
  }
2715
- return { content: [{ type: "text", text: `Error listing devices: ${e}` }] };
2798
+ return { isError: true, content: [{ type: "text", text: `Error listing devices: ${e}` }] };
2716
2799
  }
2717
2800
  return { content: [{ type: "text", text: "Could not retrieve device list." }] };
2718
2801
  }
@@ -2741,10 +2824,11 @@ ${names.join("\n")}` }]
2741
2824
  }]
2742
2825
  };
2743
2826
  }
2744
- return { content: [{ type: "text", text: `Error: ${e}` }] };
2827
+ return { isError: true, content: [{ type: "text", text: `Error: ${e}` }] };
2745
2828
  }
2746
2829
  } catch (e) {
2747
2830
  return {
2831
+ isError: true,
2748
2832
  content: [{ type: "text", text: `Error: ${e}` }]
2749
2833
  };
2750
2834
  }
@@ -2758,7 +2842,7 @@ var package_default = {
2758
2842
  type: "git",
2759
2843
  url: "git://github.com/webdriverio/mcp.git"
2760
2844
  },
2761
- version: "2.3.1",
2845
+ version: "2.4.1",
2762
2846
  description: "MCP server with WebdriverIO for browser and mobile app automation (iOS/Android via Appium)",
2763
2847
  main: "./lib/server.js",
2764
2848
  module: "./lib/server.js",
@@ -2826,6 +2910,171 @@ var package_default = {
2826
2910
  packageManager: "pnpm@10.12.4"
2827
2911
  };
2828
2912
 
2913
+ // src/server.ts
2914
+ import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
2915
+
2916
+ // src/recording/step-recorder.ts
2917
+ function getState2() {
2918
+ return getBrowser.__state;
2919
+ }
2920
+ function appendStep(toolName, params, status, durationMs, error) {
2921
+ const state2 = getState2();
2922
+ const sessionId = state2.currentSession;
2923
+ if (!sessionId) return;
2924
+ const history = state2.sessionHistory.get(sessionId);
2925
+ if (!history) return;
2926
+ const step = {
2927
+ index: history.steps.length + 1,
2928
+ tool: toolName,
2929
+ params,
2930
+ status,
2931
+ durationMs,
2932
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2933
+ ...error !== void 0 && { error }
2934
+ };
2935
+ history.steps.push(step);
2936
+ }
2937
+ function getSessionHistory() {
2938
+ return getState2().sessionHistory;
2939
+ }
2940
+ function extractErrorText(result) {
2941
+ const textContent = result.content.find((c) => c.type === "text");
2942
+ return textContent ? textContent.text : "Unknown error";
2943
+ }
2944
+ function withRecording(toolName, callback) {
2945
+ return async (params, extra) => {
2946
+ const start = Date.now();
2947
+ const result = await callback(params, extra);
2948
+ const isError = result.isError === true;
2949
+ appendStep(
2950
+ toolName,
2951
+ params,
2952
+ isError ? "error" : "ok",
2953
+ Date.now() - start,
2954
+ isError ? extractErrorText(result) : void 0
2955
+ );
2956
+ return result;
2957
+ };
2958
+ }
2959
+
2960
+ // src/recording/code-generator.ts
2961
+ function escapeStr(value) {
2962
+ return String(value).replace(/\\/g, "\\\\").replace(/'/g, "\\'");
2963
+ }
2964
+ function formatParams(params) {
2965
+ return Object.entries(params).map(([k, v]) => `${k}="${v}"`).join(" ");
2966
+ }
2967
+ function indentJson(value) {
2968
+ return JSON.stringify(value, null, 2).split("\n").map((line, i) => i > 0 ? ` ${line}` : line).join("\n");
2969
+ }
2970
+ function generateStep(step, history) {
2971
+ if (step.tool === "__session_transition__") {
2972
+ const newId = step.params.newSessionId ?? "unknown";
2973
+ return `// --- new session: ${newId} started at ${step.timestamp} ---`;
2974
+ }
2975
+ if (step.status === "error") {
2976
+ return `// [error] ${step.tool}: ${formatParams(step.params)} \u2014 ${step.error ?? "unknown error"}`;
2977
+ }
2978
+ const p = step.params;
2979
+ switch (step.tool) {
2980
+ case "start_browser": {
2981
+ const nav = p.navigationUrl ? `
2982
+ await browser.url('${escapeStr(p.navigationUrl)}');` : "";
2983
+ return `const browser = await remote({
2984
+ capabilities: ${indentJson(history.capabilities)}
2985
+ });${nav}`;
2986
+ }
2987
+ case "start_app_session": {
2988
+ const config = {
2989
+ protocol: "http",
2990
+ hostname: history.appiumConfig?.hostname ?? "localhost",
2991
+ port: history.appiumConfig?.port ?? 4723,
2992
+ path: history.appiumConfig?.path ?? "/",
2993
+ capabilities: history.capabilities
2994
+ };
2995
+ return `const browser = await remote(${indentJson(config)});`;
2996
+ }
2997
+ case "attach_browser": {
2998
+ const nav = p.navigationUrl ? `
2999
+ await browser.url('${escapeStr(p.navigationUrl)}');` : "";
3000
+ return `const browser = await remote({
3001
+ capabilities: ${indentJson(history.capabilities)}
3002
+ });${nav}`;
3003
+ }
3004
+ case "navigate":
3005
+ return `await browser.url('${escapeStr(p.url)}');`;
3006
+ case "click_element":
3007
+ return `await browser.$('${escapeStr(p.selector)}').click();`;
3008
+ case "set_value":
3009
+ return `await browser.$('${escapeStr(p.selector)}').setValue('${escapeStr(p.value)}');`;
3010
+ case "scroll": {
3011
+ const scrollAmount = p.direction === "down" ? p.pixels : -p.pixels;
3012
+ return `await browser.execute(() => window.scrollBy(0, ${scrollAmount}));`;
3013
+ }
3014
+ case "tap_element":
3015
+ if (p.selector !== void 0) {
3016
+ return `await browser.$('${escapeStr(p.selector)}').click();`;
3017
+ }
3018
+ return `await browser.tap({ x: ${p.x}, y: ${p.y} });`;
3019
+ case "swipe":
3020
+ return `await browser.execute('mobile: swipe', { direction: '${escapeStr(p.direction)}' });`;
3021
+ case "drag_and_drop":
3022
+ if (p.targetSelector !== void 0) {
3023
+ return `await browser.$('${escapeStr(p.sourceSelector)}').dragAndDrop(browser.$('${escapeStr(p.targetSelector)}'));`;
3024
+ }
3025
+ return `await browser.$('${escapeStr(p.sourceSelector)}').dragAndDrop({ x: ${p.x}, y: ${p.y} });`;
3026
+ default:
3027
+ return `// [unknown tool] ${step.tool}`;
3028
+ }
3029
+ }
3030
+ function generateCode(history) {
3031
+ const steps = history.steps.map((step) => generateStep(step, history)).join("\n");
3032
+ return `import { remote } from 'webdriverio';
3033
+
3034
+ ${steps}
3035
+
3036
+ await browser.deleteSession();`;
3037
+ }
3038
+
3039
+ // src/recording/resources.ts
3040
+ function getCurrentSessionId() {
3041
+ return getBrowser.__state?.currentSession ?? null;
3042
+ }
3043
+ function buildSessionsIndex() {
3044
+ const histories = getSessionHistory();
3045
+ const currentId = getCurrentSessionId();
3046
+ const sessions = Array.from(histories.values()).map((h) => ({
3047
+ sessionId: h.sessionId,
3048
+ type: h.type,
3049
+ startedAt: h.startedAt,
3050
+ ...h.endedAt ? { endedAt: h.endedAt } : {},
3051
+ stepCount: h.steps.length,
3052
+ isCurrent: h.sessionId === currentId
3053
+ }));
3054
+ return JSON.stringify({ sessions });
3055
+ }
3056
+ function buildCurrentSessionSteps() {
3057
+ const currentId = getCurrentSessionId();
3058
+ if (!currentId) return null;
3059
+ return buildSessionStepsById(currentId);
3060
+ }
3061
+ function buildSessionStepsById(sessionId) {
3062
+ const history = getSessionHistory().get(sessionId);
3063
+ if (!history) return null;
3064
+ return buildSessionPayload(history);
3065
+ }
3066
+ function buildSessionPayload(history) {
3067
+ const stepsJson = JSON.stringify({
3068
+ sessionId: history.sessionId,
3069
+ type: history.type,
3070
+ startedAt: history.startedAt,
3071
+ ...history.endedAt ? { endedAt: history.endedAt } : {},
3072
+ stepCount: history.steps.length,
3073
+ steps: history.steps
3074
+ });
3075
+ return { stepsJson, generatedJs: generateCode(history) };
3076
+ }
3077
+
2829
3078
  // src/server.ts
2830
3079
  console.log = (...args) => console.error("[LOG]", ...args);
2831
3080
  console.info = (...args) => console.error("[INFO]", ...args);
@@ -2840,31 +3089,32 @@ var server = new McpServer({
2840
3089
  }, {
2841
3090
  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
3091
  capabilities: {
2843
- tools: {}
3092
+ tools: {},
3093
+ resources: {}
2844
3094
  }
2845
3095
  });
2846
3096
  var registerTool = (definition, callback) => server.registerTool(definition.name, {
2847
3097
  description: definition.description,
2848
3098
  inputSchema: definition.inputSchema
2849
3099
  }, callback);
2850
- registerTool(startBrowserToolDefinition, startBrowserTool);
2851
- registerTool(startAppToolDefinition, startAppTool);
3100
+ registerTool(startBrowserToolDefinition, withRecording("start_browser", startBrowserTool));
3101
+ registerTool(startAppToolDefinition, withRecording("start_app_session", startAppTool));
2852
3102
  registerTool(closeSessionToolDefinition, closeSessionTool);
2853
- registerTool(attachBrowserToolDefinition, attachBrowserTool);
3103
+ registerTool(attachBrowserToolDefinition, withRecording("attach_browser", attachBrowserTool));
2854
3104
  registerTool(emulateDeviceToolDefinition, emulateDeviceTool);
2855
- registerTool(navigateToolDefinition, navigateTool);
3105
+ registerTool(navigateToolDefinition, withRecording("navigate", navigateTool));
2856
3106
  registerTool(getVisibleElementsToolDefinition, getVisibleElementsTool);
2857
3107
  registerTool(getAccessibilityToolDefinition, getAccessibilityTreeTool);
2858
- registerTool(scrollToolDefinition, scrollTool);
2859
- registerTool(clickToolDefinition, clickTool);
2860
- registerTool(setValueToolDefinition, setValueTool);
3108
+ registerTool(scrollToolDefinition, withRecording("scroll", scrollTool));
3109
+ registerTool(clickToolDefinition, withRecording("click_element", clickTool));
3110
+ registerTool(setValueToolDefinition, withRecording("set_value", setValueTool));
2861
3111
  registerTool(takeScreenshotToolDefinition, takeScreenshotTool);
2862
3112
  registerTool(getCookiesToolDefinition, getCookiesTool);
2863
3113
  registerTool(setCookieToolDefinition, setCookieTool);
2864
3114
  registerTool(deleteCookiesToolDefinition, deleteCookiesTool);
2865
- registerTool(tapElementToolDefinition, tapElementTool);
2866
- registerTool(swipeToolDefinition, swipeTool);
2867
- registerTool(dragAndDropToolDefinition, dragAndDropTool);
3115
+ registerTool(tapElementToolDefinition, withRecording("tap_element", tapElementTool));
3116
+ registerTool(swipeToolDefinition, withRecording("swipe", swipeTool));
3117
+ registerTool(dragAndDropToolDefinition, withRecording("drag_and_drop", dragAndDropTool));
2868
3118
  registerTool(getAppStateToolDefinition, getAppStateTool);
2869
3119
  registerTool(getContextsToolDefinition, getContextsTool);
2870
3120
  registerTool(getCurrentContextToolDefinition, getCurrentContextTool);
@@ -2874,6 +3124,58 @@ registerTool(hideKeyboardToolDefinition, hideKeyboardTool);
2874
3124
  registerTool(getGeolocationToolDefinition, getGeolocationTool);
2875
3125
  registerTool(setGeolocationToolDefinition, setGeolocationTool);
2876
3126
  registerTool(executeScriptToolDefinition, executeScriptTool);
3127
+ server.registerResource(
3128
+ "sessions",
3129
+ "wdio://sessions",
3130
+ { description: "JSON index of all browser and app sessions with metadata and step counts" },
3131
+ async () => ({
3132
+ contents: [{ uri: "wdio://sessions", mimeType: "application/json", text: buildSessionsIndex() }]
3133
+ })
3134
+ );
3135
+ server.registerResource(
3136
+ "session-current-steps",
3137
+ "wdio://session/current/steps",
3138
+ { description: "JSON step log for the currently active session" },
3139
+ async () => {
3140
+ const payload = buildCurrentSessionSteps();
3141
+ return {
3142
+ contents: [{ uri: "wdio://session/current/steps", mimeType: "application/json", text: payload?.stepsJson ?? '{"error":"No active session"}' }]
3143
+ };
3144
+ }
3145
+ );
3146
+ server.registerResource(
3147
+ "session-current-code",
3148
+ "wdio://session/current/code",
3149
+ { description: "Generated WebdriverIO JS code for the currently active session" },
3150
+ async () => {
3151
+ const payload = buildCurrentSessionSteps();
3152
+ return {
3153
+ contents: [{ uri: "wdio://session/current/code", mimeType: "text/plain", text: payload?.generatedJs ?? "// No active session" }]
3154
+ };
3155
+ }
3156
+ );
3157
+ server.registerResource(
3158
+ "session-steps",
3159
+ new ResourceTemplate("wdio://session/{sessionId}/steps", { list: void 0 }),
3160
+ { description: "JSON step log for a specific session by ID" },
3161
+ async (uri, { sessionId }) => {
3162
+ const payload = buildSessionStepsById(sessionId);
3163
+ return {
3164
+ contents: [{ uri: uri.href, mimeType: "application/json", text: payload?.stepsJson ?? `{"error":"Session not found: ${sessionId}"}` }]
3165
+ };
3166
+ }
3167
+ );
3168
+ server.registerResource(
3169
+ "session-code",
3170
+ new ResourceTemplate("wdio://session/{sessionId}/code", { list: void 0 }),
3171
+ { description: "Generated WebdriverIO JS code for a specific session by ID" },
3172
+ async (uri, { sessionId }) => {
3173
+ const payload = buildSessionStepsById(sessionId);
3174
+ return {
3175
+ contents: [{ uri: uri.href, mimeType: "text/plain", text: payload?.generatedJs ?? `// Session not found: ${sessionId}` }]
3176
+ };
3177
+ }
3178
+ );
2877
3179
  async function main() {
2878
3180
  const transport = new StdioServerTransport();
2879
3181
  await server.connect(transport);