@wdio/mcp 2.5.3 → 3.0.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
@@ -1,4 +1,41 @@
1
1
  #!/usr/bin/env node
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __esm = (fn, res) => function __init() {
5
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
6
+ };
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+
12
+ // src/session/state.ts
13
+ var state_exports = {};
14
+ __export(state_exports, {
15
+ getBrowser: () => getBrowser,
16
+ getState: () => getState
17
+ });
18
+ function getBrowser() {
19
+ const browser = state.browsers.get(state.currentSession);
20
+ if (!browser) {
21
+ throw new Error("No active browser session");
22
+ }
23
+ return browser;
24
+ }
25
+ function getState() {
26
+ return state;
27
+ }
28
+ var state;
29
+ var init_state = __esm({
30
+ "src/session/state.ts"() {
31
+ state = {
32
+ browsers: /* @__PURE__ */ new Map(),
33
+ currentSession: null,
34
+ sessionMetadata: /* @__PURE__ */ new Map(),
35
+ sessionHistory: /* @__PURE__ */ new Map()
36
+ };
37
+ }
38
+ });
2
39
 
3
40
  // package.json
4
41
  var package_default = {
@@ -9,7 +46,7 @@ var package_default = {
9
46
  type: "git",
10
47
  url: "git://github.com/webdriverio/mcp.git"
11
48
  },
12
- version: "2.5.2",
49
+ version: "2.5.3",
13
50
  description: "MCP server with WebdriverIO for browser and mobile app automation (iOS/Android via Appium)",
14
51
  main: "./lib/server.js",
15
52
  module: "./lib/server.js",
@@ -40,7 +77,8 @@ var package_default = {
40
77
  prebundle: "rimraf lib --glob ./*.tgz",
41
78
  bundle: "tsup && shx chmod +x lib/server.js",
42
79
  postbundle: "npm pack",
43
- lint: "eslint src/ --fix && tsc --noEmit",
80
+ lint: "npm run lint:src && npm run lint:tests",
81
+ "lint:src": "eslint src/ --fix && tsc --noEmit",
44
82
  "lint:tests": "eslint tests/ --fix && tsc -p tsconfig.test.json --noEmit",
45
83
  start: "node lib/server.js",
46
84
  dev: "tsx --watch src/server.ts",
@@ -78,225 +116,20 @@ var package_default = {
78
116
  };
79
117
 
80
118
  // src/server.ts
81
- import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
119
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp";
82
120
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
83
121
 
84
- // src/tools/browser.tool.ts
85
- import { remote } from "webdriverio";
86
- import { z } from "zod";
87
- var supportedBrowsers = ["chrome", "firefox", "edge", "safari"];
88
- var browserSchema = z.enum(supportedBrowsers).default("chrome");
89
- var startBrowserToolDefinition = {
90
- name: "start_browser",
91
- 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.",
92
- inputSchema: {
93
- browser: browserSchema.describe("Browser to launch: chrome, firefox, edge, safari (default: chrome)"),
94
- headless: z.boolean().optional().default(true),
95
- windowWidth: z.number().min(400).max(3840).optional().default(1920),
96
- windowHeight: z.number().min(400).max(2160).optional().default(1080),
97
- navigationUrl: z.string().optional().describe("URL to navigate to after starting the browser"),
98
- capabilities: z.record(z.string(), z.unknown()).optional().describe("Additional W3C capabilities to merge with defaults (e.g. goog:chromeOptions args/extensions/prefs)")
99
- }
100
- };
101
- var closeSessionToolDefinition = {
102
- name: "close_session",
103
- description: "closes or detaches from the current browser or app session",
104
- inputSchema: {
105
- detach: z.boolean().optional().describe("If true, disconnect from session without terminating it (preserves app state). Default: false")
106
- }
107
- };
108
- var state = {
109
- browsers: /* @__PURE__ */ new Map(),
110
- currentSession: null,
111
- sessionMetadata: /* @__PURE__ */ new Map(),
112
- sessionHistory: /* @__PURE__ */ new Map()
113
- };
114
- var getBrowser = () => {
115
- const browser = state.browsers.get(state.currentSession);
116
- if (!browser) {
117
- throw new Error("No active browser session");
118
- }
119
- return browser;
120
- };
121
- getBrowser.__state = state;
122
- var startBrowserTool = async ({
123
- browser = "chrome",
124
- headless = true,
125
- windowWidth = 1920,
126
- windowHeight = 1080,
127
- navigationUrl,
128
- capabilities: userCapabilities = {}
129
- }) => {
130
- const browserDisplayNames = {
131
- chrome: "Chrome",
132
- firefox: "Firefox",
133
- edge: "Edge",
134
- safari: "Safari"
135
- };
136
- const selectedBrowser = browser;
137
- const headlessSupported = selectedBrowser !== "safari";
138
- const effectiveHeadless = headless && headlessSupported;
139
- const chromiumArgs = [
140
- `--window-size=${windowWidth},${windowHeight}`,
141
- "--no-sandbox",
142
- "--disable-search-engine-choice-screen",
143
- "--disable-infobars",
144
- "--log-level=3",
145
- "--use-fake-device-for-media-stream",
146
- "--use-fake-ui-for-media-stream",
147
- "--disable-web-security",
148
- "--allow-running-insecure-content"
149
- ];
150
- if (effectiveHeadless) {
151
- chromiumArgs.push("--headless=new");
152
- chromiumArgs.push("--disable-gpu");
153
- chromiumArgs.push("--disable-dev-shm-usage");
154
- }
155
- const firefoxArgs = [];
156
- if (effectiveHeadless && selectedBrowser === "firefox") {
157
- firefoxArgs.push("-headless");
158
- }
159
- const capabilities = {
160
- acceptInsecureCerts: true
161
- };
162
- switch (selectedBrowser) {
163
- case "chrome":
164
- capabilities.browserName = "chrome";
165
- capabilities["goog:chromeOptions"] = { args: chromiumArgs };
166
- break;
167
- case "edge":
168
- capabilities.browserName = "msedge";
169
- capabilities["ms:edgeOptions"] = { args: chromiumArgs };
170
- break;
171
- case "firefox":
172
- capabilities.browserName = "firefox";
173
- if (firefoxArgs.length > 0) {
174
- capabilities["moz:firefoxOptions"] = { args: firefoxArgs };
175
- }
176
- break;
177
- case "safari":
178
- capabilities.browserName = "safari";
179
- break;
180
- }
181
- const mergeCapabilityOptions = (defaultOptions, customOptions) => {
182
- if (!defaultOptions || typeof defaultOptions !== "object" || !customOptions || typeof customOptions !== "object") {
183
- return customOptions ?? defaultOptions;
184
- }
185
- const defaultRecord = defaultOptions;
186
- const customRecord = customOptions;
187
- const merged = { ...defaultRecord, ...customRecord };
188
- if (Array.isArray(defaultRecord.args) || Array.isArray(customRecord.args)) {
189
- merged.args = [
190
- ...Array.isArray(defaultRecord.args) ? defaultRecord.args : [],
191
- ...Array.isArray(customRecord.args) ? customRecord.args : []
192
- ];
193
- }
194
- return merged;
195
- };
196
- const mergedCapabilities = {
197
- ...capabilities,
198
- ...userCapabilities,
199
- "goog:chromeOptions": mergeCapabilityOptions(capabilities["goog:chromeOptions"], userCapabilities["goog:chromeOptions"]),
200
- "ms:edgeOptions": mergeCapabilityOptions(capabilities["ms:edgeOptions"], userCapabilities["ms:edgeOptions"]),
201
- "moz:firefoxOptions": mergeCapabilityOptions(capabilities["moz:firefoxOptions"], userCapabilities["moz:firefoxOptions"])
202
- };
203
- for (const [key, value] of Object.entries(mergedCapabilities)) {
204
- if (value === void 0) {
205
- delete mergedCapabilities[key];
206
- }
207
- }
208
- const wdioBrowser = await remote({
209
- capabilities: mergedCapabilities
210
- });
211
- const { sessionId } = wdioBrowser;
212
- state.browsers.set(sessionId, wdioBrowser);
213
- state.sessionMetadata.set(sessionId, {
214
- type: "browser",
215
- capabilities: wdioBrowser.capabilities,
216
- isAttached: false
217
- });
218
- if (state.currentSession && state.currentSession !== sessionId) {
219
- const outgoing = state.sessionHistory.get(state.currentSession);
220
- if (outgoing) {
221
- outgoing.steps.push({
222
- index: outgoing.steps.length + 1,
223
- tool: "__session_transition__",
224
- params: { newSessionId: sessionId },
225
- status: "ok",
226
- durationMs: 0,
227
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
228
- });
229
- outgoing.endedAt = (/* @__PURE__ */ new Date()).toISOString();
230
- }
231
- }
232
- state.sessionHistory.set(sessionId, {
233
- sessionId,
234
- type: "browser",
235
- startedAt: (/* @__PURE__ */ new Date()).toISOString(),
236
- capabilities: wdioBrowser.capabilities,
237
- steps: []
238
- });
239
- state.currentSession = sessionId;
240
- let sizeNote = "";
241
- try {
242
- await wdioBrowser.setWindowSize(windowWidth, windowHeight);
243
- } catch (e) {
244
- sizeNote = `
245
- Note: Unable to set window size (${windowWidth}x${windowHeight}). ${e}`;
246
- }
247
- if (navigationUrl) {
248
- await wdioBrowser.url(navigationUrl);
249
- }
250
- const modeText = effectiveHeadless ? "headless" : "headed";
251
- const browserText = browserDisplayNames[selectedBrowser];
252
- const urlText = navigationUrl ? ` and navigated to ${navigationUrl}` : "";
253
- const headlessNote = headless && !headlessSupported ? "\nNote: Safari does not support headless mode. Started in headed mode." : "";
254
- return {
255
- content: [{
256
- type: "text",
257
- text: `${browserText} browser started in ${modeText} mode with sessionId: ${sessionId} (${windowWidth}x${windowHeight})${urlText}${headlessNote}${sizeNote}`
258
- }]
259
- };
260
- };
261
- var closeSessionTool = async (args = {}) => {
262
- try {
263
- const browser = getBrowser();
264
- const sessionId = state.currentSession;
265
- const metadata = state.sessionMetadata.get(sessionId);
266
- const history = state.sessionHistory.get(sessionId);
267
- if (history) {
268
- history.endedAt = (/* @__PURE__ */ new Date()).toISOString();
269
- }
270
- const effectiveDetach = args.detach || !!metadata?.isAttached;
271
- if (!effectiveDetach) {
272
- await browser.deleteSession();
273
- }
274
- state.browsers.delete(sessionId);
275
- state.sessionMetadata.delete(sessionId);
276
- state.currentSession = null;
277
- const action = effectiveDetach ? "detached from" : "closed";
278
- const note = args.detach && !metadata?.isAttached ? "\nNote: Session will remain active on Appium server." : "";
279
- return {
280
- content: [{ type: "text", text: `Session ${sessionId} ${action}${note}` }]
281
- };
282
- } catch (e) {
283
- return {
284
- isError: true,
285
- content: [{ type: "text", text: `Error closing session: ${e}` }]
286
- };
287
- }
288
- };
289
-
290
122
  // src/tools/navigate.tool.ts
291
- import { z as z2 } from "zod";
123
+ init_state();
124
+ import { z } from "zod";
292
125
  var navigateToolDefinition = {
293
126
  name: "navigate",
294
127
  description: "navigates to a URL",
295
128
  inputSchema: {
296
- url: z2.string().min(1).describe("The URL to navigate to")
129
+ url: z.string().min(1).describe("The URL to navigate to")
297
130
  }
298
131
  };
299
- var navigateTool = async ({ url }) => {
132
+ var navigateAction = async (url) => {
300
133
  try {
301
134
  const browser = getBrowser();
302
135
  await browser.url(url);
@@ -310,16 +143,32 @@ var navigateTool = async ({ url }) => {
310
143
  };
311
144
  }
312
145
  };
146
+ var navigateTool = async ({ url }) => navigateAction(url);
313
147
 
314
148
  // src/tools/click.tool.ts
149
+ init_state();
315
150
  import { z as z3 } from "zod";
151
+
152
+ // src/utils/zod-helpers.ts
153
+ import { z as z2 } from "zod";
154
+ var coerceBoolean = z2.preprocess((val) => {
155
+ if (typeof val === "boolean") return val;
156
+ if (typeof val === "string") {
157
+ if (val === "false" || val === "0") return false;
158
+ if (val === "true" || val === "1") return true;
159
+ return Boolean(val);
160
+ }
161
+ return val;
162
+ }, z2.boolean());
163
+
164
+ // src/tools/click.tool.ts
316
165
  var defaultTimeout = 3e3;
317
166
  var clickToolDefinition = {
318
167
  name: "click_element",
319
168
  description: "clicks an element",
320
169
  inputSchema: {
321
170
  selector: z3.string().describe(`Value for the selector, in the form of css selector or xpath ("button.my-class" or "//button[@class='my-class']" or "button=Exact text with spaces" or "a*=Link containing text")`),
322
- scrollToView: z3.boolean().optional().describe("Whether to scroll the element into view before clicking").default(true),
171
+ scrollToView: coerceBoolean.optional().describe("Whether to scroll the element into view before clicking").default(true),
323
172
  timeout: z3.number().optional().describe("Maximum time to wait for element in milliseconds")
324
173
  }
325
174
  };
@@ -344,6 +193,7 @@ var clickAction = async (selector, timeout, scrollToView = true) => {
344
193
  var clickTool = async ({ selector, scrollToView, timeout = defaultTimeout }) => clickAction(selector, timeout, scrollToView);
345
194
 
346
195
  // src/tools/set-value.tool.ts
196
+ init_state();
347
197
  import { z as z4 } from "zod";
348
198
  var defaultTimeout2 = 3e3;
349
199
  var setValueToolDefinition = {
@@ -352,11 +202,11 @@ var setValueToolDefinition = {
352
202
  inputSchema: {
353
203
  selector: z4.string().describe(`Value for the selector, in the form of css selector or xpath ("button.my-class" or "//button[@class='my-class']")`),
354
204
  value: z4.string().describe("Text to enter into the element"),
355
- scrollToView: z4.boolean().optional().describe("Whether to scroll the element into view before typing").default(true),
205
+ scrollToView: coerceBoolean.optional().describe("Whether to scroll the element into view before typing").default(true),
356
206
  timeout: z4.number().optional().describe("Maximum time to wait for element in milliseconds")
357
207
  }
358
208
  };
359
- var setValueTool = async ({ selector, value, scrollToView = true, timeout = defaultTimeout2 }) => {
209
+ var setValueAction = async (selector, value, scrollToView = true, timeout = defaultTimeout2) => {
360
210
  try {
361
211
  const browser = getBrowser();
362
212
  await browser.waitUntil(browser.$(selector).isExisting, { timeout });
@@ -375,356 +225,488 @@ var setValueTool = async ({ selector, value, scrollToView = true, timeout = defa
375
225
  };
376
226
  }
377
227
  };
228
+ var setValueTool = async ({ selector, value, scrollToView = true, timeout = defaultTimeout2 }) => setValueAction(selector, value, scrollToView, timeout);
378
229
 
379
- // src/tools/app-session.tool.ts
380
- import { remote as remote2 } from "webdriverio";
230
+ // src/tools/scroll.tool.ts
231
+ init_state();
381
232
  import { z as z5 } from "zod";
382
-
383
- // src/config/appium.config.ts
384
- function getAppiumServerConfig(overrides) {
385
- return {
386
- hostname: overrides?.hostname || process.env.APPIUM_URL || "127.0.0.1",
387
- port: overrides?.port || Number(process.env.APPIUM_URL_PORT) || 4723,
388
- path: overrides?.path || process.env.APPIUM_PATH || "/"
389
- };
390
- }
391
- function buildIOSCapabilities(appPath, options) {
392
- const capabilities = {
393
- platformName: "iOS",
394
- "appium:platformVersion": options.platformVersion,
395
- "appium:deviceName": options.deviceName,
396
- "appium:automationName": options.automationName || "XCUITest"
397
- };
398
- if (appPath) {
399
- capabilities["appium:app"] = appPath;
400
- }
401
- if (options.udid) {
402
- capabilities["appium:udid"] = options.udid;
403
- }
404
- if (options.noReset !== void 0) {
405
- capabilities["appium:noReset"] = options.noReset;
406
- }
407
- if (options.fullReset !== void 0) {
408
- capabilities["appium:fullReset"] = options.fullReset;
409
- }
410
- if (options.newCommandTimeout !== void 0) {
411
- capabilities["appium:newCommandTimeout"] = options.newCommandTimeout;
412
- }
413
- capabilities["appium:autoGrantPermissions"] = options.autoGrantPermissions ?? true;
414
- capabilities["appium:autoAcceptAlerts"] = options.autoAcceptAlerts ?? true;
415
- if (options.autoDismissAlerts !== void 0) {
416
- capabilities["appium:autoDismissAlerts"] = options.autoDismissAlerts;
417
- capabilities["appium:autoAcceptAlerts"] = void 0;
418
- }
419
- for (const [key, value] of Object.entries(options)) {
420
- if (!["deviceName", "platformVersion", "automationName", "autoGrantPermissions", "autoAcceptAlerts", "autoDismissAlerts", "udid", "noReset", "fullReset", "newCommandTimeout"].includes(
421
- key
422
- )) {
423
- capabilities[`appium:${key}`] = value;
424
- }
425
- }
426
- return capabilities;
427
- }
428
- function buildAndroidCapabilities(appPath, options) {
429
- const capabilities = {
430
- platformName: "Android",
431
- "appium:platformVersion": options.platformVersion,
432
- "appium:deviceName": options.deviceName,
433
- "appium:automationName": options.automationName || "UiAutomator2"
434
- };
435
- if (appPath) {
436
- capabilities["appium:app"] = appPath;
437
- }
438
- if (options.noReset !== void 0) {
439
- capabilities["appium:noReset"] = options.noReset;
440
- }
441
- if (options.fullReset !== void 0) {
442
- capabilities["appium:fullReset"] = options.fullReset;
443
- }
444
- if (options.newCommandTimeout !== void 0) {
445
- capabilities["appium:newCommandTimeout"] = options.newCommandTimeout;
446
- }
447
- capabilities["appium:autoGrantPermissions"] = options.autoGrantPermissions ?? true;
448
- capabilities["appium:autoAcceptAlerts"] = options.autoAcceptAlerts ?? true;
449
- if (options.autoDismissAlerts !== void 0) {
450
- capabilities["appium:autoDismissAlerts"] = options.autoDismissAlerts;
451
- capabilities["appium:autoAcceptAlerts"] = void 0;
452
- }
453
- if (options.appWaitActivity) {
454
- capabilities["appium:appWaitActivity"] = options.appWaitActivity;
233
+ var scrollToolDefinition = {
234
+ name: "scroll",
235
+ description: "scrolls the page by specified pixels (browser only). For mobile, use the swipe tool.",
236
+ inputSchema: {
237
+ direction: z5.enum(["up", "down"]).describe("Scroll direction"),
238
+ pixels: z5.number().optional().default(500).describe("Number of pixels to scroll")
455
239
  }
456
- for (const [key, value] of Object.entries(options)) {
457
- if (!["deviceName", "platformVersion", "automationName", "autoGrantPermissions", "autoAcceptAlerts", "autoDismissAlerts", "appWaitActivity", "noReset", "fullReset", "newCommandTimeout"].includes(
458
- key
459
- )) {
460
- capabilities[`appium:${key}`] = value;
240
+ };
241
+ var scrollAction = async (direction, pixels = 500) => {
242
+ try {
243
+ const browser = getBrowser();
244
+ const state2 = getState();
245
+ const metadata = state2.sessionMetadata.get(state2.currentSession);
246
+ const sessionType = metadata?.type;
247
+ if (sessionType !== "browser") {
248
+ throw new Error("scroll only works in browser sessions. For mobile, use the swipe tool.");
461
249
  }
250
+ const scrollAmount = direction === "down" ? pixels : -pixels;
251
+ await browser.execute((amount) => {
252
+ window.scrollBy(0, amount);
253
+ }, scrollAmount);
254
+ return {
255
+ content: [{ type: "text", text: `Scrolled ${direction} ${pixels} pixels` }]
256
+ };
257
+ } catch (e) {
258
+ return {
259
+ isError: true,
260
+ content: [{ type: "text", text: `Error scrolling: ${e}` }]
261
+ };
462
262
  }
463
- return capabilities;
464
- }
263
+ };
264
+ var scrollTool = async ({ direction, pixels = 500 }) => scrollAction(direction, pixels);
465
265
 
466
- // src/tools/app-session.tool.ts
467
- var startAppToolDefinition = {
468
- name: "start_app_session",
469
- description: "starts a mobile app session (iOS/Android) via Appium",
266
+ // src/tools/cookies.tool.ts
267
+ import { z as z6 } from "zod";
268
+ var setCookieToolDefinition = {
269
+ name: "set_cookie",
270
+ description: "sets a cookie with specified name, value, and optional attributes",
470
271
  inputSchema: {
471
- platform: z5.enum(["iOS", "Android"]).describe("Mobile platform"),
472
- appPath: z5.string().optional().describe("Path to the app file (.app/.apk/.ipa). Required unless noReset=true (connecting to already-running app)"),
473
- deviceName: z5.string().describe("Device/emulator/simulator name"),
474
- platformVersion: z5.string().optional().describe('OS version (e.g., "17.0", "14")'),
475
- automationName: z5.enum(["XCUITest", "UiAutomator2", "Espresso"]).optional().describe("Automation driver name"),
476
- appiumHost: z5.string().optional().describe("Appium server hostname (overrides APPIUM_URL env var)"),
477
- appiumPort: z5.number().optional().describe("Appium server port (overrides APPIUM_URL_PORT env var)"),
478
- appiumPath: z5.string().optional().describe("Appium server path (overrides APPIUM_PATH env var)"),
479
- autoGrantPermissions: z5.boolean().optional().describe("Auto-grant app permissions (default: true)"),
480
- autoAcceptAlerts: z5.boolean().optional().describe("Auto-accept alerts (default: true)"),
481
- autoDismissAlerts: z5.boolean().optional().describe('Auto-dismiss alerts (default: false, will override "autoAcceptAlerts" to undefined if set)'),
482
- appWaitActivity: z5.string().optional().describe("Activity to wait for on launch (Android only)"),
483
- udid: z5.string().optional().describe('Unique Device Identifier for iOS real device testing (e.g., "00008030-001234567890002E")'),
484
- noReset: z5.boolean().optional().describe("Do not reset app state before session (preserves app data). Default: false"),
485
- fullReset: z5.boolean().optional().describe("Uninstall app before/after session. Default: true. Set to false with noReset=true to preserve app state completely"),
486
- 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."),
487
- 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)")
272
+ name: z6.string().describe("Cookie name"),
273
+ value: z6.string().describe("Cookie value"),
274
+ domain: z6.string().optional().describe("Cookie domain (defaults to current domain)"),
275
+ path: z6.string().optional().describe('Cookie path (defaults to "/")'),
276
+ expiry: z6.number().optional().describe("Expiry date as Unix timestamp in seconds"),
277
+ httpOnly: coerceBoolean.optional().describe("HttpOnly flag"),
278
+ secure: coerceBoolean.optional().describe("Secure flag"),
279
+ sameSite: z6.enum(["strict", "lax", "none"]).optional().describe("SameSite attribute")
488
280
  }
489
281
  };
490
- var getState = () => {
491
- const sharedState = getBrowser.__state;
492
- if (!sharedState) {
493
- throw new Error("Browser state not initialized");
282
+ var setCookieTool = async ({
283
+ name,
284
+ value,
285
+ domain,
286
+ path = "/",
287
+ expiry,
288
+ httpOnly,
289
+ secure,
290
+ sameSite
291
+ }) => {
292
+ try {
293
+ const cookie = { name, value, path, domain, expiry, httpOnly, secure, sameSite };
294
+ const { getBrowser: getBrowser2 } = await Promise.resolve().then(() => (init_state(), state_exports));
295
+ const browser = getBrowser2();
296
+ await browser.setCookies(cookie);
297
+ return {
298
+ content: [{ type: "text", text: `Cookie "${name}" set successfully` }]
299
+ };
300
+ } catch (e) {
301
+ return {
302
+ isError: true,
303
+ content: [{ type: "text", text: `Error setting cookie: ${e}` }]
304
+ };
494
305
  }
495
- return sharedState;
496
306
  };
497
- var startAppTool = async (args) => {
307
+ var deleteCookiesToolDefinition = {
308
+ name: "delete_cookies",
309
+ description: "deletes all cookies or a specific cookie by name",
310
+ inputSchema: {
311
+ name: z6.string().optional().describe("Optional cookie name to delete a specific cookie. If not provided, deletes all cookies")
312
+ }
313
+ };
314
+ var deleteCookiesTool = async ({ name }) => {
498
315
  try {
499
- const {
500
- platform: platform2,
501
- appPath,
502
- deviceName,
503
- platformVersion,
504
- automationName,
505
- appiumHost,
506
- appiumPort,
507
- appiumPath,
508
- autoGrantPermissions = true,
509
- autoAcceptAlerts,
510
- autoDismissAlerts,
511
- appWaitActivity,
512
- udid,
513
- noReset,
514
- fullReset,
515
- newCommandTimeout = 300,
516
- capabilities: userCapabilities = {}
517
- } = args;
518
- if (!appPath && noReset !== true) {
316
+ const { getBrowser: getBrowser2 } = await Promise.resolve().then(() => (init_state(), state_exports));
317
+ const browser = getBrowser2();
318
+ if (name) {
319
+ await browser.deleteCookies([name]);
519
320
  return {
520
- content: [{
521
- type: "text",
522
- text: 'Error: Either "appPath" must be provided to install an app, or "noReset: true" must be set to connect to an already-running app.'
523
- }]
321
+ content: [{ type: "text", text: `Cookie "${name}" deleted successfully` }]
524
322
  };
525
323
  }
526
- const serverConfig = getAppiumServerConfig({
527
- hostname: appiumHost,
528
- port: appiumPort,
529
- path: appiumPath
530
- });
531
- const capabilities = platform2 === "iOS" ? buildIOSCapabilities(appPath, {
532
- deviceName,
533
- platformVersion,
534
- automationName: automationName || "XCUITest",
535
- autoGrantPermissions,
536
- autoAcceptAlerts,
537
- autoDismissAlerts,
538
- udid,
539
- noReset,
540
- fullReset,
541
- newCommandTimeout
542
- }) : buildAndroidCapabilities(appPath, {
543
- deviceName,
544
- platformVersion,
545
- automationName: automationName || "UiAutomator2",
546
- autoGrantPermissions,
547
- autoAcceptAlerts,
548
- autoDismissAlerts,
549
- appWaitActivity,
550
- noReset,
551
- fullReset,
552
- newCommandTimeout
553
- });
554
- const mergedCapabilities = {
555
- ...capabilities,
556
- ...userCapabilities
324
+ await browser.deleteCookies();
325
+ return {
326
+ content: [{ type: "text", text: "All cookies deleted successfully" }]
557
327
  };
558
- for (const [key, value] of Object.entries(mergedCapabilities)) {
559
- if (value === void 0) {
560
- delete mergedCapabilities[key];
561
- }
562
- }
563
- const browser = await remote2({
564
- protocol: "http",
565
- hostname: serverConfig.hostname,
566
- port: serverConfig.port,
567
- path: serverConfig.path,
568
- capabilities: mergedCapabilities
569
- });
570
- const { sessionId } = browser;
571
- const shouldAutoDetach = noReset === true || !appPath;
572
- const state2 = getState();
573
- state2.browsers.set(sessionId, browser);
574
- state2.sessionMetadata.set(sessionId, {
575
- type: platform2.toLowerCase(),
576
- capabilities: mergedCapabilities,
577
- isAttached: shouldAutoDetach
578
- });
579
- if (state2.currentSession && state2.currentSession !== sessionId) {
580
- const outgoing = state2.sessionHistory.get(state2.currentSession);
581
- if (outgoing) {
582
- outgoing.steps.push({
583
- index: outgoing.steps.length + 1,
584
- tool: "__session_transition__",
585
- params: { newSessionId: sessionId },
586
- status: "ok",
587
- durationMs: 0,
588
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
589
- });
590
- outgoing.endedAt = (/* @__PURE__ */ new Date()).toISOString();
591
- }
328
+ } catch (e) {
329
+ return {
330
+ isError: true,
331
+ content: [{ type: "text", text: `Error deleting cookies: ${e}` }]
332
+ };
333
+ }
334
+ };
335
+
336
+ // src/tools/gestures.tool.ts
337
+ init_state();
338
+ import { z as z7 } from "zod";
339
+ var tapElementToolDefinition = {
340
+ name: "tap_element",
341
+ description: "taps an element by selector or screen coordinates (mobile)",
342
+ inputSchema: {
343
+ selector: z7.string().optional().describe("Element selector (CSS, XPath, accessibility ID, or UiAutomator)"),
344
+ x: z7.number().optional().describe("X coordinate for screen tap (if no selector provided)"),
345
+ y: z7.number().optional().describe("Y coordinate for screen tap (if no selector provided)")
346
+ }
347
+ };
348
+ var tapAction = async (args) => {
349
+ try {
350
+ const browser = getBrowser();
351
+ const { selector, x, y } = args;
352
+ if (selector) {
353
+ const element = await browser.$(selector);
354
+ await element.tap();
355
+ return {
356
+ content: [{ type: "text", text: `Tapped element: ${selector}` }]
357
+ };
358
+ } else if (x !== void 0 && y !== void 0) {
359
+ await browser.tap({ x, y });
360
+ return {
361
+ content: [{ type: "text", text: `Tapped at coordinates: (${x}, ${y})` }]
362
+ };
592
363
  }
593
- state2.sessionHistory.set(sessionId, {
594
- sessionId,
595
- type: platform2.toLowerCase(),
596
- startedAt: (/* @__PURE__ */ new Date()).toISOString(),
597
- capabilities: mergedCapabilities,
598
- appiumConfig: { hostname: serverConfig.hostname, port: serverConfig.port, path: serverConfig.path },
599
- steps: []
600
- });
601
- state2.currentSession = sessionId;
602
- const appInfo = appPath ? `
603
- App: ${appPath}` : "\nApp: (connected to running app)";
604
- const detachNote = shouldAutoDetach ? "\n\n(Auto-detach enabled: session will be preserved on close. Use close_session({ detach: false }) to force terminate.)" : "";
605
364
  return {
606
- content: [
607
- {
608
- type: "text",
609
- text: `${platform2} app session started with sessionId: ${sessionId}
610
- Device: ${deviceName}${appInfo}
611
- Appium Server: ${serverConfig.hostname}:${serverConfig.port}${serverConfig.path}${detachNote}`
612
- }
613
- ]
365
+ isError: true,
366
+ content: [{ type: "text", text: "Error: Must provide either selector or x,y coordinates" }]
614
367
  };
615
368
  } catch (e) {
616
369
  return {
617
370
  isError: true,
618
- content: [{ type: "text", text: `Error starting app session: ${e}` }]
371
+ content: [{ type: "text", text: `Error tapping: ${e}` }]
619
372
  };
620
373
  }
621
374
  };
622
-
623
- // src/tools/scroll.tool.ts
624
- import { z as z6 } from "zod";
625
- var scrollToolDefinition = {
626
- name: "scroll",
627
- description: "scrolls the page by specified pixels (browser only). For mobile, use the swipe tool.",
375
+ var tapElementTool = async (args) => tapAction(args);
376
+ var swipeToolDefinition = {
377
+ name: "swipe",
378
+ description: "performs a swipe gesture in specified direction (mobile)",
628
379
  inputSchema: {
629
- direction: z6.enum(["up", "down"]).describe("Scroll direction"),
630
- pixels: z6.number().optional().default(500).describe("Number of pixels to scroll")
380
+ direction: z7.enum(["up", "down", "left", "right"]).describe("Swipe direction"),
381
+ duration: z7.number().min(100).max(5e3).optional().describe("Swipe duration in milliseconds (default: 500)"),
382
+ percent: z7.number().min(0).max(1).optional().describe("Percentage of screen to swipe (0-1, default: 0.5 for up/down, 0.95 for left/right)")
631
383
  }
632
384
  };
633
- var scrollTool = async ({ direction, pixels = 500 }) => {
385
+ var contentToFingerDirection = {
386
+ up: "down",
387
+ down: "up",
388
+ left: "right",
389
+ right: "left"
390
+ };
391
+ var swipeAction = async (args) => {
634
392
  try {
635
393
  const browser = getBrowser();
636
- const state2 = getState();
637
- const metadata = state2.sessionMetadata.get(state2.currentSession);
638
- const sessionType = metadata?.type;
639
- if (sessionType !== "browser") {
640
- throw new Error("scroll only works in browser sessions. For mobile, use the swipe tool.");
394
+ const { direction, duration, percent } = args;
395
+ const isVertical = direction === "up" || direction === "down";
396
+ const defaultPercent = isVertical ? 0.5 : 0.95;
397
+ const effectivePercent = percent ?? defaultPercent;
398
+ const effectiveDuration = duration ?? 500;
399
+ const fingerDirection = contentToFingerDirection[direction];
400
+ await browser.swipe({ direction: fingerDirection, duration: effectiveDuration, percent: effectivePercent });
401
+ return {
402
+ content: [{ type: "text", text: `Swiped ${direction}` }]
403
+ };
404
+ } catch (e) {
405
+ return {
406
+ isError: true,
407
+ content: [{ type: "text", text: `Error swiping: ${e}` }]
408
+ };
409
+ }
410
+ };
411
+ var swipeTool = async (args) => swipeAction(args);
412
+ var dragAndDropToolDefinition = {
413
+ name: "drag_and_drop",
414
+ description: "drags an element to another element or coordinates (mobile)",
415
+ inputSchema: {
416
+ sourceSelector: z7.string().describe("Source element selector to drag"),
417
+ targetSelector: z7.string().optional().describe("Target element selector to drop onto"),
418
+ x: z7.number().optional().describe("Target X offset (if no targetSelector)"),
419
+ y: z7.number().optional().describe("Target Y offset (if no targetSelector)"),
420
+ duration: z7.number().min(100).max(5e3).optional().describe("Drag duration in milliseconds")
421
+ }
422
+ };
423
+ var dragAndDropAction = async (args) => {
424
+ try {
425
+ const browser = getBrowser();
426
+ const { sourceSelector, targetSelector, x, y, duration } = args;
427
+ const sourceElement = await browser.$(sourceSelector);
428
+ if (targetSelector) {
429
+ const targetElement = await browser.$(targetSelector);
430
+ await sourceElement.dragAndDrop(targetElement, { duration });
431
+ return {
432
+ content: [{ type: "text", text: `Dragged ${sourceSelector} to ${targetSelector}` }]
433
+ };
434
+ } else if (x !== void 0 && y !== void 0) {
435
+ await sourceElement.dragAndDrop({ x, y }, { duration });
436
+ return {
437
+ content: [{ type: "text", text: `Dragged ${sourceSelector} by (${x}, ${y})` }]
438
+ };
641
439
  }
642
- const scrollAmount = direction === "down" ? pixels : -pixels;
643
- await browser.execute((amount) => {
644
- window.scrollBy(0, amount);
645
- }, scrollAmount);
646
440
  return {
647
- content: [{ type: "text", text: `Scrolled ${direction} ${pixels} pixels` }]
441
+ isError: true,
442
+ content: [{ type: "text", text: "Error: Must provide either targetSelector or x,y coordinates" }]
648
443
  };
649
444
  } catch (e) {
650
445
  return {
651
446
  isError: true,
652
- content: [{ type: "text", text: `Error scrolling: ${e}` }]
447
+ content: [{ type: "text", text: `Error dragging: ${e}` }]
653
448
  };
654
449
  }
655
450
  };
451
+ var dragAndDropTool = async (args) => dragAndDropAction(args);
656
452
 
657
- // src/scripts/get-interactable-browser-elements.ts
658
- var elementsScript = (includeBounds) => (function() {
659
- const interactableSelectors = [
660
- "a[href]",
661
- "button",
662
- 'input:not([type="hidden"])',
663
- "select",
664
- "textarea",
665
- '[role="button"]',
666
- '[role="link"]',
667
- '[role="checkbox"]',
668
- '[role="radio"]',
669
- '[role="tab"]',
670
- '[role="menuitem"]',
671
- '[role="combobox"]',
672
- '[role="option"]',
673
- '[role="switch"]',
674
- '[role="slider"]',
675
- '[role="textbox"]',
676
- '[role="searchbox"]',
677
- '[role="spinbutton"]',
678
- '[contenteditable="true"]',
679
- '[tabindex]:not([tabindex="-1"])'
680
- ].join(",");
681
- function isVisible(element) {
682
- if (typeof element.checkVisibility === "function") {
683
- return element.checkVisibility({ opacityProperty: true, visibilityProperty: true, contentVisibilityAuto: true });
684
- }
685
- const style = window.getComputedStyle(element);
686
- return style.display !== "none" && style.visibility !== "hidden" && style.opacity !== "0" && element.offsetWidth > 0 && element.offsetHeight > 0;
453
+ // src/tools/context.tool.ts
454
+ init_state();
455
+ import { z as z8 } from "zod";
456
+ var switchContextToolDefinition = {
457
+ name: "switch_context",
458
+ description: "switches between native and webview contexts",
459
+ inputSchema: {
460
+ context: z8.string().describe(
461
+ 'Context name to switch to (e.g., "NATIVE_APP", "WEBVIEW_com.example.app", or use index from wdio://session/current/contexts resource)'
462
+ )
687
463
  }
688
- function getAccessibleName(el) {
689
- const ariaLabel = el.getAttribute("aria-label");
690
- if (ariaLabel) return ariaLabel.trim();
691
- const labelledBy = el.getAttribute("aria-labelledby");
692
- if (labelledBy) {
693
- const texts = labelledBy.split(/\s+/).map((id) => document.getElementById(id)?.textContent?.trim() || "").filter(Boolean);
694
- if (texts.length > 0) return texts.join(" ").slice(0, 100);
695
- }
696
- const tag = el.tagName.toLowerCase();
697
- if (tag === "img" || tag === "input" && el.getAttribute("type") === "image") {
698
- const alt = el.getAttribute("alt");
699
- if (alt !== null) return alt.trim();
700
- }
701
- if (["input", "select", "textarea"].includes(tag)) {
702
- const id = el.getAttribute("id");
703
- if (id) {
704
- const label = document.querySelector(`label[for="${CSS.escape(id)}"]`);
705
- if (label) return label.textContent?.trim() || "";
706
- }
707
- const parentLabel = el.closest("label");
708
- if (parentLabel) {
709
- const clone = parentLabel.cloneNode(true);
710
- clone.querySelectorAll("input,select,textarea").forEach((n) => n.remove());
711
- const lt = clone.textContent?.trim();
712
- if (lt) return lt;
464
+ };
465
+ var switchContextTool = async (args) => {
466
+ try {
467
+ const browser = getBrowser();
468
+ const { context } = args;
469
+ if (/^\d+$/.test(context)) {
470
+ const contexts = await browser.getContexts();
471
+ const index = Number.parseInt(context, 10) - 1;
472
+ if (index >= 0 && index < contexts.length) {
473
+ const targetContext = contexts[index];
474
+ await browser.switchContext(targetContext);
475
+ return { content: [{ type: "text", text: `Switched to context: ${targetContext}` }] };
713
476
  }
477
+ throw new Error(`Error: Invalid context index ${context}. Available contexts: ${contexts.length}`);
714
478
  }
715
- const ph = el.getAttribute("placeholder");
716
- if (ph) return ph.trim();
717
- const title = el.getAttribute("title");
718
- if (title) return title.trim();
719
- return (el.textContent?.trim().replace(/\s+/g, " ") || "").slice(0, 100);
479
+ await browser.switchContext(context);
480
+ return {
481
+ content: [{ type: "text", text: `Switched to context: ${context}` }]
482
+ };
483
+ } catch (e) {
484
+ return {
485
+ isError: true,
486
+ content: [{ type: "text", text: `Error switching context: ${e}` }]
487
+ };
720
488
  }
721
- function getSelector(element) {
722
- const tag = element.tagName.toLowerCase();
723
- const text = element.textContent?.trim().replace(/\s+/g, " ");
724
- if (text && text.length > 0 && text.length <= 50) {
725
- const sameTagElements = document.querySelectorAll(tag);
726
- let matchCount = 0;
727
- sameTagElements.forEach((el) => {
489
+ };
490
+
491
+ // src/tools/device.tool.ts
492
+ init_state();
493
+ import { z as z9 } from "zod";
494
+ var hideKeyboardToolDefinition = {
495
+ name: "hide_keyboard",
496
+ description: "hides the on-screen keyboard",
497
+ inputSchema: {}
498
+ };
499
+ var rotateDeviceToolDefinition = {
500
+ name: "rotate_device",
501
+ description: "rotates device to portrait or landscape orientation",
502
+ inputSchema: {
503
+ orientation: z9.enum(["PORTRAIT", "LANDSCAPE"]).describe("Device orientation")
504
+ }
505
+ };
506
+ var setGeolocationToolDefinition = {
507
+ name: "set_geolocation",
508
+ description: "sets device geolocation (latitude, longitude, altitude)",
509
+ inputSchema: {
510
+ latitude: z9.number().min(-90).max(90).describe("Latitude coordinate"),
511
+ longitude: z9.number().min(-180).max(180).describe("Longitude coordinate"),
512
+ altitude: z9.number().optional().describe("Altitude in meters (optional)")
513
+ }
514
+ };
515
+ var rotateDeviceTool = async (args) => {
516
+ try {
517
+ const browser = getBrowser();
518
+ const { orientation } = args;
519
+ await browser.setOrientation(orientation);
520
+ return {
521
+ content: [{ type: "text", text: `Device rotated to: ${orientation}` }]
522
+ };
523
+ } catch (e) {
524
+ return {
525
+ isError: true,
526
+ content: [{ type: "text", text: `Error rotating device: ${e}` }]
527
+ };
528
+ }
529
+ };
530
+ var hideKeyboardTool = async () => {
531
+ try {
532
+ const browser = getBrowser();
533
+ await browser.hideKeyboard();
534
+ return {
535
+ content: [{ type: "text", text: "Keyboard hidden" }]
536
+ };
537
+ } catch (e) {
538
+ return {
539
+ isError: true,
540
+ content: [{ type: "text", text: `Error hiding keyboard: ${e}` }]
541
+ };
542
+ }
543
+ };
544
+ var setGeolocationTool = async (args) => {
545
+ try {
546
+ const browser = getBrowser();
547
+ const { latitude, longitude, altitude } = args;
548
+ await browser.setGeoLocation({ latitude, longitude, altitude });
549
+ return {
550
+ content: [
551
+ {
552
+ type: "text",
553
+ text: `Geolocation set to:
554
+ Latitude: ${latitude}
555
+ Longitude: ${longitude}${altitude ? `
556
+ Altitude: ${altitude}m` : ""}`
557
+ }
558
+ ]
559
+ };
560
+ } catch (e) {
561
+ return {
562
+ isError: true,
563
+ content: [{ type: "text", text: `Error setting geolocation: ${e}` }]
564
+ };
565
+ }
566
+ };
567
+
568
+ // src/tools/execute-script.tool.ts
569
+ init_state();
570
+ import { z as z10 } from "zod";
571
+ var executeScriptToolDefinition = {
572
+ name: "execute_script",
573
+ description: `Executes JavaScript in browser or mobile commands via Appium.
574
+
575
+ **Option B for browser interaction** \u2014 prefer get_visible_elements or click_element/set_value with a selector instead. Use execute_script only when no dedicated tool covers the action (e.g. reading computed values, triggering custom events, scrolling to a position).
576
+
577
+ **Browser:** Runs JavaScript in page context. Use 'return' to get values back.
578
+ - Example: execute_script({ script: "return document.title" })
579
+ - Example: execute_script({ script: "return window.scrollY" })
580
+ - Example: execute_script({ script: "arguments[0].click()", args: ["#myButton"] })
581
+
582
+ **Mobile (Appium):** Executes mobile-specific commands using 'mobile: <command>' syntax.
583
+ - Press key (Android): execute_script({ script: "mobile: pressKey", args: [{ keycode: 4 }] }) // BACK=4, HOME=3
584
+ - Activate app: execute_script({ script: "mobile: activateApp", args: [{ appId: "com.example" }] })
585
+ - Terminate app: execute_script({ script: "mobile: terminateApp", args: [{ appId: "com.example" }] })
586
+ - Deep link: execute_script({ script: "mobile: deepLink", args: [{ url: "myapp://screen", package: "com.example" }] })
587
+ - Shell command (Android): execute_script({ script: "mobile: shell", args: [{ command: "dumpsys", args: ["battery"] }] })`,
588
+ inputSchema: {
589
+ script: z10.string().describe('JavaScript code (browser) or mobile command string like "mobile: pressKey" (Appium)'),
590
+ args: z10.array(z10.any()).optional().describe("Arguments to pass to the script. For browser: element selectors or values. For mobile commands: command-specific parameters as objects.")
591
+ }
592
+ };
593
+ var executeScriptTool = async (args) => {
594
+ try {
595
+ const browser = getBrowser();
596
+ const { script, args: scriptArgs = [] } = args;
597
+ const resolvedArgs = await Promise.all(
598
+ scriptArgs.map(async (arg) => {
599
+ if (typeof arg === "string" && !script.startsWith("mobile:")) {
600
+ try {
601
+ const element = await browser.$(arg);
602
+ if (await element.isExisting()) {
603
+ return element;
604
+ }
605
+ } catch {
606
+ }
607
+ }
608
+ return arg;
609
+ })
610
+ );
611
+ const result = await browser.execute(script, ...resolvedArgs);
612
+ let resultText;
613
+ if (result === void 0 || result === null) {
614
+ resultText = "Script executed successfully (no return value)";
615
+ } else if (typeof result === "object") {
616
+ try {
617
+ resultText = `Result: ${JSON.stringify(result, null, 2)}`;
618
+ } catch {
619
+ resultText = `Result: ${String(result)}`;
620
+ }
621
+ } else {
622
+ resultText = `Result: ${result}`;
623
+ }
624
+ return {
625
+ content: [{ type: "text", text: resultText }]
626
+ };
627
+ } catch (e) {
628
+ return {
629
+ isError: true,
630
+ content: [{ type: "text", text: `Error executing script: ${e}` }]
631
+ };
632
+ }
633
+ };
634
+
635
+ // src/tools/get-elements.tool.ts
636
+ init_state();
637
+ import { z as z11 } from "zod";
638
+
639
+ // src/scripts/get-interactable-browser-elements.ts
640
+ var elementsScript = (includeBounds) => (function() {
641
+ const interactableSelectors = [
642
+ "a[href]",
643
+ "button",
644
+ 'input:not([type="hidden"])',
645
+ "select",
646
+ "textarea",
647
+ '[role="button"]',
648
+ '[role="link"]',
649
+ '[role="checkbox"]',
650
+ '[role="radio"]',
651
+ '[role="tab"]',
652
+ '[role="menuitem"]',
653
+ '[role="combobox"]',
654
+ '[role="option"]',
655
+ '[role="switch"]',
656
+ '[role="slider"]',
657
+ '[role="textbox"]',
658
+ '[role="searchbox"]',
659
+ '[role="spinbutton"]',
660
+ '[contenteditable="true"]',
661
+ '[tabindex]:not([tabindex="-1"])'
662
+ ].join(",");
663
+ function isVisible(element) {
664
+ if (typeof element.checkVisibility === "function") {
665
+ return element.checkVisibility({ opacityProperty: true, visibilityProperty: true, contentVisibilityAuto: true });
666
+ }
667
+ const style = window.getComputedStyle(element);
668
+ return style.display !== "none" && style.visibility !== "hidden" && style.opacity !== "0" && element.offsetWidth > 0 && element.offsetHeight > 0;
669
+ }
670
+ function getAccessibleName(el) {
671
+ const ariaLabel = el.getAttribute("aria-label");
672
+ if (ariaLabel) return ariaLabel.trim();
673
+ const labelledBy = el.getAttribute("aria-labelledby");
674
+ if (labelledBy) {
675
+ const texts = labelledBy.split(/\s+/).map((id) => document.getElementById(id)?.textContent?.trim() || "").filter(Boolean);
676
+ if (texts.length > 0) return texts.join(" ").slice(0, 100);
677
+ }
678
+ const tag = el.tagName.toLowerCase();
679
+ if (tag === "img" || tag === "input" && el.getAttribute("type") === "image") {
680
+ const alt = el.getAttribute("alt");
681
+ if (alt !== null) return alt.trim();
682
+ }
683
+ if (["input", "select", "textarea"].includes(tag)) {
684
+ const id = el.getAttribute("id");
685
+ if (id) {
686
+ const label = document.querySelector(`label[for="${CSS.escape(id)}"]`);
687
+ if (label) return label.textContent?.trim() || "";
688
+ }
689
+ const parentLabel = el.closest("label");
690
+ if (parentLabel) {
691
+ const clone = parentLabel.cloneNode(true);
692
+ clone.querySelectorAll("input,select,textarea").forEach((n) => n.remove());
693
+ const lt = clone.textContent?.trim();
694
+ if (lt) return lt;
695
+ }
696
+ }
697
+ const ph = el.getAttribute("placeholder");
698
+ if (ph) return ph.trim();
699
+ const title = el.getAttribute("title");
700
+ if (title) return title.trim();
701
+ return (el.textContent?.trim().replace(/\s+/g, " ") || "").slice(0, 100);
702
+ }
703
+ function getSelector(element) {
704
+ const tag = element.tagName.toLowerCase();
705
+ const text = element.textContent?.trim().replace(/\s+/g, " ");
706
+ if (text && text.length > 0 && text.length <= 50) {
707
+ const sameTagElements = document.querySelectorAll(tag);
708
+ let matchCount = 0;
709
+ sameTagElements.forEach((el) => {
728
710
  if (el.textContent?.includes(text)) matchCount++;
729
711
  });
730
712
  if (matchCount === 1) return `${tag}*=${text}`;
@@ -1768,82 +1750,567 @@ async function getMobileVisibleElements(browser, platform2, options = {}) {
1768
1750
  return elements.map((el) => toMobileElementInfo(el, includeBounds));
1769
1751
  }
1770
1752
 
1771
- // src/tools/get-visible-elements.tool.ts
1753
+ // src/scripts/get-elements.ts
1754
+ async function getElements(browser, params) {
1755
+ const {
1756
+ inViewportOnly = true,
1757
+ includeContainers = false,
1758
+ includeBounds = false,
1759
+ limit = 0,
1760
+ offset = 0
1761
+ } = params;
1762
+ let elements;
1763
+ if (browser.isAndroid || browser.isIOS) {
1764
+ const platform2 = browser.isAndroid ? "android" : "ios";
1765
+ elements = await getMobileVisibleElements(browser, platform2, { includeContainers, includeBounds });
1766
+ } else {
1767
+ elements = await getInteractableBrowserElements(browser, { includeBounds });
1768
+ }
1769
+ if (inViewportOnly) {
1770
+ elements = elements.filter((el) => el.isInViewport !== false);
1771
+ }
1772
+ const total = elements.length;
1773
+ if (offset > 0) {
1774
+ elements = elements.slice(offset);
1775
+ }
1776
+ if (limit > 0) {
1777
+ elements = elements.slice(0, limit);
1778
+ }
1779
+ return {
1780
+ total,
1781
+ showing: elements.length,
1782
+ hasMore: offset + elements.length < total,
1783
+ elements
1784
+ };
1785
+ }
1786
+
1787
+ // src/tools/get-elements.tool.ts
1772
1788
  import { encode } from "@toon-format/toon";
1773
- import { z as z7 } from "zod";
1774
- var getVisibleElementsToolDefinition = {
1775
- name: "get_visible_elements",
1776
- description: "Get interactable elements on the page (buttons, links, inputs). Use get_accessibility for page structure and non-interactable elements.",
1789
+ var getElementsToolDefinition = {
1790
+ name: "get_elements",
1791
+ description: "Get interactable elements on the current page. Use when wdio://session/current/elements does not return the desired elements.",
1777
1792
  inputSchema: {
1778
- inViewportOnly: z7.boolean().optional().describe("Only return elements within the visible viewport. Default: true. Set to false to get ALL elements on the page."),
1779
- includeContainers: z7.boolean().optional().describe("Mobile only: include layout containers. Default: false."),
1780
- includeBounds: z7.boolean().optional().describe("Include element bounds/coordinates (x, y, width, height). Default: false."),
1781
- limit: z7.number().optional().describe("Maximum number of elements to return. Default: 0 (unlimited)."),
1782
- offset: z7.number().optional().describe("Number of elements to skip (for pagination). Default: 0.")
1793
+ inViewportOnly: coerceBoolean.optional().default(false).describe("Only return elements visible in the current viewport (default: false)."),
1794
+ includeContainers: coerceBoolean.optional().default(false).describe("Include container elements like divs and sections (default: false)"),
1795
+ includeBounds: coerceBoolean.optional().default(false).describe("Include element bounding box coordinates (default: false)"),
1796
+ limit: z11.number().optional().default(0).describe("Maximum number of elements to return (0 = no limit)"),
1797
+ offset: z11.number().optional().default(0).describe("Number of elements to skip (for pagination)")
1783
1798
  }
1784
1799
  };
1785
- var getVisibleElementsTool = async (args) => {
1800
+ var getElementsTool = async ({
1801
+ inViewportOnly = false,
1802
+ includeContainers = false,
1803
+ includeBounds = false,
1804
+ limit = 0,
1805
+ offset = 0
1806
+ }) => {
1786
1807
  try {
1787
1808
  const browser = getBrowser();
1788
- const {
1789
- inViewportOnly = true,
1790
- includeContainers = false,
1791
- includeBounds = false,
1792
- limit = 0,
1793
- offset = 0
1794
- } = args || {};
1795
- let elements;
1796
- if (browser.isAndroid || browser.isIOS) {
1797
- const platform2 = browser.isAndroid ? "android" : "ios";
1798
- elements = await getMobileVisibleElements(browser, platform2, { includeContainers, includeBounds });
1799
- } else {
1800
- elements = await getInteractableBrowserElements(browser, { includeBounds });
1801
- }
1802
- if (inViewportOnly) {
1803
- elements = elements.filter((el) => el.isInViewport !== false);
1804
- }
1805
- const total = elements.length;
1806
- if (offset > 0) {
1807
- elements = elements.slice(offset);
1808
- }
1809
- if (limit > 0) {
1810
- elements = elements.slice(0, limit);
1811
- }
1812
- const result = {
1813
- total,
1814
- showing: elements.length,
1815
- hasMore: offset + elements.length < total,
1816
- elements
1817
- };
1818
- const toon = encode(result).replace(/,""/g, ",").replace(/"",/g, ",");
1819
- return {
1820
- content: [{ type: "text", text: toon }]
1821
- };
1809
+ const result = await getElements(browser, { inViewportOnly, includeContainers, includeBounds, limit, offset });
1810
+ const text = encode(result).replace(/,""/g, ",").replace(/"",/g, ",");
1811
+ return { content: [{ type: "text", text }] };
1822
1812
  } catch (e) {
1823
- return {
1824
- isError: true,
1825
- content: [{ type: "text", text: `Error getting visible elements: ${e}` }]
1826
- };
1813
+ return { isError: true, content: [{ type: "text", text: `Error getting elements: ${e}` }] };
1827
1814
  }
1828
1815
  };
1829
1816
 
1830
- // src/scripts/get-browser-accessibility-tree.ts
1831
- var accessibilityTreeScript = () => (function() {
1832
- const INPUT_TYPE_ROLES = {
1833
- text: "textbox",
1834
- search: "searchbox",
1835
- email: "textbox",
1836
- url: "textbox",
1837
- tel: "textbox",
1838
- password: "textbox",
1839
- number: "spinbutton",
1840
- checkbox: "checkbox",
1841
- radio: "radio",
1842
- range: "slider",
1843
- submit: "button",
1844
- reset: "button",
1845
- image: "button",
1846
- file: "button",
1817
+ // src/tools/launch-chrome.tool.ts
1818
+ import { spawn } from "child_process";
1819
+ import { copyFileSync, cpSync, existsSync, mkdirSync, rmSync, writeFileSync } from "fs";
1820
+ import { homedir, platform, tmpdir } from "os";
1821
+ import { join } from "path";
1822
+ import { z as z12 } from "zod";
1823
+ var USER_DATA_DIR = join(tmpdir(), "chrome-debug");
1824
+ var launchChromeToolDefinition = {
1825
+ name: "launch_chrome",
1826
+ description: `Prepares and launches Chrome with remote debugging enabled so attach_browser() can connect.
1827
+
1828
+ Two modes:
1829
+
1830
+ newInstance (default): Opens a Chrome window alongside your existing one using a separate
1831
+ profile dir. Your current Chrome session is untouched.
1832
+
1833
+ freshSession: Launches Chrome with an empty profile (no cookies, no logins).
1834
+
1835
+ Use copyProfileFiles: true to carry over your cookies and logins into the debug session.
1836
+ Note: changes made during the session won't sync back to your main profile.
1837
+
1838
+ After this tool succeeds, call attach_browser() to connect.`,
1839
+ inputSchema: {
1840
+ port: z12.number().default(9222).describe("Remote debugging port (default: 9222)"),
1841
+ mode: z12.enum(["newInstance", "freshSession"]).default("newInstance").describe(
1842
+ "newInstance: open alongside existing Chrome | freshSession: clean profile"
1843
+ ),
1844
+ copyProfileFiles: coerceBoolean.default(false).describe(
1845
+ "Copy your Default Chrome profile (cookies, logins) into the debug session."
1846
+ )
1847
+ }
1848
+ };
1849
+ function isMac() {
1850
+ return platform() === "darwin";
1851
+ }
1852
+ function chromeExec() {
1853
+ if (isMac()) return "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome";
1854
+ if (platform() === "win32") {
1855
+ const candidates = [
1856
+ join("C:", "Program Files", "Google", "Chrome", "Application", "chrome.exe"),
1857
+ join("C:", "Program Files (x86)", "Google", "Chrome", "Application", "chrome.exe")
1858
+ ];
1859
+ return candidates.find((p) => existsSync(p)) ?? candidates[0];
1860
+ }
1861
+ return "google-chrome";
1862
+ }
1863
+ function defaultProfileDir() {
1864
+ const home = homedir();
1865
+ if (isMac()) return join(home, "Library", "Application Support", "Google", "Chrome");
1866
+ if (platform() === "win32") return join(home, "AppData", "Local", "Google", "Chrome", "User Data");
1867
+ return join(home, ".config", "google-chrome");
1868
+ }
1869
+ function copyProfile() {
1870
+ const srcDir = defaultProfileDir();
1871
+ rmSync(USER_DATA_DIR, { recursive: true, force: true });
1872
+ mkdirSync(USER_DATA_DIR, { recursive: true });
1873
+ copyFileSync(join(srcDir, "Local State"), join(USER_DATA_DIR, "Local State"));
1874
+ cpSync(join(srcDir, "Default"), join(USER_DATA_DIR, "Default"), { recursive: true });
1875
+ for (const f of ["SingletonLock", "SingletonCookie", "SingletonSocket"]) {
1876
+ rmSync(join(USER_DATA_DIR, f), { force: true });
1877
+ }
1878
+ for (const f of ["Current Session", "Current Tabs", "Last Session", "Last Tabs"]) {
1879
+ rmSync(join(USER_DATA_DIR, "Default", f), { force: true });
1880
+ }
1881
+ writeFileSync(join(USER_DATA_DIR, "First Run"), "");
1882
+ }
1883
+ function launchChrome(port) {
1884
+ spawn(chromeExec(), [
1885
+ `--remote-debugging-port=${port}`,
1886
+ `--user-data-dir=${USER_DATA_DIR}`,
1887
+ "--profile-directory=Default",
1888
+ "--no-first-run",
1889
+ "--disable-session-crashed-bubble"
1890
+ ], { detached: true, stdio: "ignore" }).unref();
1891
+ }
1892
+ async function waitForCDP(port, timeoutMs = 15e3) {
1893
+ const deadline = Date.now() + timeoutMs;
1894
+ while (Date.now() < deadline) {
1895
+ try {
1896
+ const res = await fetch(`http://localhost:${port}/json/version`);
1897
+ if (res.ok) return;
1898
+ } catch {
1899
+ }
1900
+ await new Promise((r) => setTimeout(r, 300));
1901
+ }
1902
+ throw new Error(`Chrome did not expose CDP on port ${port} within ${timeoutMs}ms`);
1903
+ }
1904
+ var launchChromeTool = async ({
1905
+ port = 9222,
1906
+ mode = "newInstance",
1907
+ copyProfileFiles = false
1908
+ }) => {
1909
+ const warnings = [];
1910
+ const notes = [];
1911
+ try {
1912
+ if (copyProfileFiles) {
1913
+ warnings.push("\u26A0\uFE0F Cookies and logins were copied at this moment. Changes during this session won't sync back to your main profile.");
1914
+ copyProfile();
1915
+ } else {
1916
+ notes.push(mode === "newInstance" ? "No profile copied \u2014 this instance starts with no cookies or logins." : "Fresh profile \u2014 no existing cookies or logins.");
1917
+ rmSync(USER_DATA_DIR, { recursive: true, force: true });
1918
+ mkdirSync(USER_DATA_DIR, { recursive: true });
1919
+ }
1920
+ launchChrome(port);
1921
+ await waitForCDP(port);
1922
+ const lines = [
1923
+ `Chrome launched on port ${port} (mode: ${mode}).`,
1924
+ ...warnings,
1925
+ ...notes
1926
+ ];
1927
+ return { content: [{ type: "text", text: lines.join("\n") }] };
1928
+ } catch (e) {
1929
+ return {
1930
+ isError: true,
1931
+ content: [{ type: "text", text: `Error launching Chrome: ${e}` }]
1932
+ };
1933
+ }
1934
+ };
1935
+
1936
+ // src/tools/emulate-device.tool.ts
1937
+ init_state();
1938
+ import { z as z13 } from "zod";
1939
+ var restoreFunctions = /* @__PURE__ */ new Map();
1940
+ var emulateDeviceToolDefinition = {
1941
+ name: "emulate_device",
1942
+ description: `Emulate a mobile or tablet device in the current browser session (sets viewport, DPR, user-agent, touch events).
1943
+
1944
+ Requires a BiDi-enabled session: start_browser({ capabilities: { webSocketUrl: true } })
1945
+
1946
+ Usage:
1947
+ emulate_device() \u2014 list available device presets
1948
+ emulate_device({ device: "iPhone 15" }) \u2014 activate emulation
1949
+ emulate_device({ device: "reset" }) \u2014 restore desktop defaults`,
1950
+ inputSchema: {
1951
+ device: z13.string().optional().describe(
1952
+ 'Device preset name (e.g. "iPhone 15", "Pixel 7"). Omit to list available presets. Pass "reset" to restore desktop defaults.'
1953
+ )
1954
+ }
1955
+ };
1956
+ var emulateDeviceTool = async ({
1957
+ device
1958
+ }) => {
1959
+ try {
1960
+ const browser = getBrowser();
1961
+ const state2 = getState();
1962
+ const sessionId = state2.currentSession;
1963
+ const metadata = state2.sessionMetadata.get(sessionId);
1964
+ if (metadata?.type === "ios" || metadata?.type === "android") {
1965
+ return {
1966
+ isError: true,
1967
+ content: [{
1968
+ type: "text",
1969
+ text: "Error: emulate_device is only supported for web browser sessions, not iOS/Android."
1970
+ }]
1971
+ };
1972
+ }
1973
+ if (!browser.isBidi) {
1974
+ return {
1975
+ isError: true,
1976
+ content: [{
1977
+ type: "text",
1978
+ text: "Error: emulate_device requires a BiDi-enabled session.\nRestart the browser with: start_browser({ capabilities: { webSocketUrl: true } })"
1979
+ }]
1980
+ };
1981
+ }
1982
+ if (!device) {
1983
+ try {
1984
+ await browser.emulate("device", "\0");
1985
+ } catch (e) {
1986
+ const msg = String(e);
1987
+ const match = msg.match(/please use one of the following: (.+)$/);
1988
+ if (match) {
1989
+ const names = match[1].split(", ").sort();
1990
+ return {
1991
+ content: [{ type: "text", text: `Available devices (${names.length}):
1992
+ ${names.join("\n")}` }]
1993
+ };
1994
+ }
1995
+ return { isError: true, content: [{ type: "text", text: `Error listing devices: ${e}` }] };
1996
+ }
1997
+ return { content: [{ type: "text", text: "Could not retrieve device list." }] };
1998
+ }
1999
+ if (device === "reset") {
2000
+ const restoreFn = restoreFunctions.get(sessionId);
2001
+ if (!restoreFn) {
2002
+ return { content: [{ type: "text", text: "No active device emulation to reset." }] };
2003
+ }
2004
+ await restoreFn();
2005
+ restoreFunctions.delete(sessionId);
2006
+ return { content: [{ type: "text", text: "Device emulation reset to desktop defaults." }] };
2007
+ }
2008
+ try {
2009
+ const restoreFn = await browser.emulate("device", device);
2010
+ restoreFunctions.set(sessionId, restoreFn);
2011
+ return {
2012
+ content: [{ type: "text", text: `Emulating "${device}".` }]
2013
+ };
2014
+ } catch (e) {
2015
+ const msg = String(e);
2016
+ if (msg.includes("Unknown device name")) {
2017
+ return {
2018
+ content: [{
2019
+ type: "text",
2020
+ text: `Error: Unknown device "${device}". Call emulate_device() with no arguments to list valid names.`
2021
+ }]
2022
+ };
2023
+ }
2024
+ return { isError: true, content: [{ type: "text", text: `Error: ${e}` }] };
2025
+ }
2026
+ } catch (e) {
2027
+ return {
2028
+ isError: true,
2029
+ content: [{ type: "text", text: `Error: ${e}` }]
2030
+ };
2031
+ }
2032
+ };
2033
+
2034
+ // src/recording/step-recorder.ts
2035
+ init_state();
2036
+ function appendStep(toolName, params, status, durationMs, error) {
2037
+ const state2 = getState();
2038
+ const sessionId = state2.currentSession;
2039
+ if (!sessionId) return;
2040
+ const history = state2.sessionHistory.get(sessionId);
2041
+ if (!history) return;
2042
+ const step = {
2043
+ index: history.steps.length + 1,
2044
+ tool: toolName,
2045
+ params,
2046
+ status,
2047
+ durationMs,
2048
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2049
+ ...error !== void 0 && { error }
2050
+ };
2051
+ history.steps.push(step);
2052
+ }
2053
+ function getSessionHistory() {
2054
+ return getState().sessionHistory;
2055
+ }
2056
+ function extractErrorText(result) {
2057
+ const textContent = result.content.find((c) => c.type === "text");
2058
+ return textContent ? textContent.text : "Unknown error";
2059
+ }
2060
+ function withRecording(toolName, callback) {
2061
+ return async (params, extra) => {
2062
+ const start = Date.now();
2063
+ const result = await callback(params, extra);
2064
+ const isError = result.isError === true;
2065
+ appendStep(
2066
+ toolName,
2067
+ params,
2068
+ isError ? "error" : "ok",
2069
+ Date.now() - start,
2070
+ isError ? extractErrorText(result) : void 0
2071
+ );
2072
+ return result;
2073
+ };
2074
+ }
2075
+
2076
+ // src/resources/sessions.resource.ts
2077
+ import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp";
2078
+
2079
+ // src/recording/code-generator.ts
2080
+ function escapeStr(value) {
2081
+ return String(value).replace(/\\/g, "\\\\").replace(/'/g, "\\'");
2082
+ }
2083
+ function formatParams(params) {
2084
+ return Object.entries(params).map(([k, v]) => `${k}="${v}"`).join(" ");
2085
+ }
2086
+ function indentJson(value) {
2087
+ return JSON.stringify(value, null, 2).split("\n").map((line, i) => i > 0 ? ` ${line}` : line).join("\n");
2088
+ }
2089
+ function generateStep(step, history) {
2090
+ if (step.tool === "__session_transition__") {
2091
+ const newId = step.params.newSessionId ?? "unknown";
2092
+ return `// --- new session: ${newId} started at ${step.timestamp} ---`;
2093
+ }
2094
+ if (step.status === "error") {
2095
+ return `// [error] ${step.tool}: ${formatParams(step.params)} \u2014 ${step.error ?? "unknown error"}`;
2096
+ }
2097
+ const p = step.params;
2098
+ switch (step.tool) {
2099
+ case "start_session": {
2100
+ const platform2 = p.platform;
2101
+ if (platform2 === "browser") {
2102
+ const nav = p.navigationUrl ? `
2103
+ await browser.url('${escapeStr(p.navigationUrl)}');` : "";
2104
+ return `const browser = await remote({
2105
+ capabilities: ${indentJson(history.capabilities)}
2106
+ });${nav}`;
2107
+ }
2108
+ const config = {
2109
+ protocol: "http",
2110
+ hostname: history.appiumConfig?.hostname ?? "localhost",
2111
+ port: history.appiumConfig?.port ?? 4723,
2112
+ path: history.appiumConfig?.path ?? "/",
2113
+ capabilities: history.capabilities
2114
+ };
2115
+ return `const browser = await remote(${indentJson(config)});`;
2116
+ }
2117
+ case "close_session":
2118
+ return "// Session closed";
2119
+ case "navigate":
2120
+ return `await browser.url('${escapeStr(p.url)}');`;
2121
+ case "click_element":
2122
+ return `await browser.$('${escapeStr(p.selector)}').click();`;
2123
+ case "set_value":
2124
+ return `await browser.$('${escapeStr(p.selector)}').setValue('${escapeStr(p.value)}');`;
2125
+ case "scroll": {
2126
+ const scrollAmount = p.direction === "down" ? p.pixels : -p.pixels;
2127
+ return `await browser.execute(() => window.scrollBy(0, ${scrollAmount}));`;
2128
+ }
2129
+ case "tap_element":
2130
+ if (p.selector !== void 0) {
2131
+ return `await browser.$('${escapeStr(p.selector)}').click();`;
2132
+ }
2133
+ return `await browser.tap({ x: ${p.x}, y: ${p.y} });`;
2134
+ case "swipe":
2135
+ return `await browser.execute('mobile: swipe', { direction: '${escapeStr(p.direction)}' });`;
2136
+ case "drag_and_drop":
2137
+ if (p.targetSelector !== void 0) {
2138
+ return `await browser.$('${escapeStr(p.sourceSelector)}').dragAndDrop(browser.$('${escapeStr(p.targetSelector)}'));`;
2139
+ }
2140
+ return `await browser.$('${escapeStr(p.sourceSelector)}').dragAndDrop({ x: ${p.x}, y: ${p.y} });`;
2141
+ case "execute_script": {
2142
+ const scriptCode = `'${escapeStr(p.script)}'`;
2143
+ const scriptArgs = p.args?.length ? `, ${indentJson(p.args)}` : "";
2144
+ return `await browser.execute(${scriptCode}${scriptArgs});`;
2145
+ }
2146
+ default:
2147
+ return `// [unknown tool] ${step.tool}`;
2148
+ }
2149
+ }
2150
+ function generateCode(history) {
2151
+ const steps = history.steps.map((step) => generateStep(step, history)).join("\n").split("\n").map((line) => ` ${line}`).join("\n");
2152
+ return `import { remote } from 'webdriverio';
2153
+
2154
+ try {
2155
+ ${steps}
2156
+ } finally {
2157
+ await browser.deleteSession();
2158
+ }`;
2159
+ }
2160
+
2161
+ // src/resources/sessions.resource.ts
2162
+ init_state();
2163
+ function getCurrentSessionId() {
2164
+ return getState().currentSession;
2165
+ }
2166
+ function buildSessionsIndex() {
2167
+ const histories = getSessionHistory();
2168
+ const currentId = getCurrentSessionId();
2169
+ const sessions = Array.from(histories.values()).map((h) => ({
2170
+ sessionId: h.sessionId,
2171
+ type: h.type,
2172
+ startedAt: h.startedAt,
2173
+ ...h.endedAt ? { endedAt: h.endedAt } : {},
2174
+ stepCount: h.steps.length,
2175
+ isCurrent: h.sessionId === currentId
2176
+ }));
2177
+ return JSON.stringify({ sessions });
2178
+ }
2179
+ function buildCurrentSessionSteps() {
2180
+ const currentId = getCurrentSessionId();
2181
+ if (!currentId) return null;
2182
+ return buildSessionStepsById(currentId);
2183
+ }
2184
+ function buildSessionStepsById(sessionId) {
2185
+ const history = getSessionHistory().get(sessionId);
2186
+ if (!history) return null;
2187
+ return buildSessionPayload(history);
2188
+ }
2189
+ function buildSessionPayload(history) {
2190
+ const stepsJson = JSON.stringify({
2191
+ sessionId: history.sessionId,
2192
+ type: history.type,
2193
+ startedAt: history.startedAt,
2194
+ ...history.endedAt ? { endedAt: history.endedAt } : {},
2195
+ stepCount: history.steps.length,
2196
+ steps: history.steps
2197
+ });
2198
+ return { stepsJson, generatedJs: generateCode(history) };
2199
+ }
2200
+ var sessionsIndexResource = {
2201
+ name: "sessions",
2202
+ uri: "wdio://sessions",
2203
+ description: "JSON index of all browser and app sessions with metadata and step counts",
2204
+ handler: async () => ({
2205
+ contents: [{ uri: "wdio://sessions", mimeType: "application/json", text: buildSessionsIndex() }]
2206
+ })
2207
+ };
2208
+ var sessionCurrentStepsResource = {
2209
+ name: "session-current-steps",
2210
+ uri: "wdio://session/current/steps",
2211
+ description: "JSON step log for the currently active session",
2212
+ handler: async () => {
2213
+ const payload = buildCurrentSessionSteps();
2214
+ return {
2215
+ contents: [{
2216
+ uri: "wdio://session/current/steps",
2217
+ mimeType: "application/json",
2218
+ text: payload?.stepsJson ?? '{"error":"No active session"}'
2219
+ }]
2220
+ };
2221
+ }
2222
+ };
2223
+ var sessionCurrentCodeResource = {
2224
+ name: "session-current-code",
2225
+ uri: "wdio://session/current/code",
2226
+ description: "Generated WebdriverIO JS code for the currently active session",
2227
+ handler: async () => {
2228
+ const payload = buildCurrentSessionSteps();
2229
+ return {
2230
+ contents: [{
2231
+ uri: "wdio://session/current/code",
2232
+ mimeType: "text/plain",
2233
+ text: payload?.generatedJs ?? "// No active session"
2234
+ }]
2235
+ };
2236
+ }
2237
+ };
2238
+ var sessionStepsResource = {
2239
+ name: "session-steps",
2240
+ template: new ResourceTemplate("wdio://session/{sessionId}/steps", { list: void 0 }),
2241
+ description: "JSON step log for a specific session by ID",
2242
+ handler: async (uri, { sessionId }) => {
2243
+ const payload = buildSessionStepsById(sessionId);
2244
+ return {
2245
+ contents: [{
2246
+ uri: uri.href,
2247
+ mimeType: "application/json",
2248
+ text: payload?.stepsJson ?? `{"error":"Session not found: ${sessionId}"}`
2249
+ }]
2250
+ };
2251
+ }
2252
+ };
2253
+ var sessionCodeResource = {
2254
+ name: "session-code",
2255
+ template: new ResourceTemplate("wdio://session/{sessionId}/code", { list: void 0 }),
2256
+ description: "Generated WebdriverIO JS code for a specific session by ID",
2257
+ handler: async (uri, { sessionId }) => {
2258
+ const payload = buildSessionStepsById(sessionId);
2259
+ return {
2260
+ contents: [{
2261
+ uri: uri.href,
2262
+ mimeType: "text/plain",
2263
+ text: payload?.generatedJs ?? `// Session not found: ${sessionId}`
2264
+ }]
2265
+ };
2266
+ }
2267
+ };
2268
+
2269
+ // src/resources/elements.resource.ts
2270
+ init_state();
2271
+ import { encode as encode2 } from "@toon-format/toon";
2272
+ var elementsResource = {
2273
+ name: "session-current-elements",
2274
+ uri: "wdio://session/current/elements",
2275
+ description: "Interactable elements on the current page",
2276
+ handler: async () => {
2277
+ try {
2278
+ const browser = getBrowser();
2279
+ const result = await getElements(browser, {});
2280
+ const text = encode2(result).replace(/,""/g, ",").replace(/"",/g, ",");
2281
+ return { contents: [{ uri: "wdio://session/current/elements", mimeType: "text/plain", text }] };
2282
+ } catch (e) {
2283
+ return {
2284
+ contents: [{
2285
+ uri: "wdio://session/current/elements",
2286
+ mimeType: "text/plain",
2287
+ text: `Error getting visible elements: ${e}`
2288
+ }]
2289
+ };
2290
+ }
2291
+ }
2292
+ };
2293
+
2294
+ // src/resources/accessibility.resource.ts
2295
+ init_state();
2296
+
2297
+ // src/scripts/get-browser-accessibility-tree.ts
2298
+ var accessibilityTreeScript = () => (function() {
2299
+ const INPUT_TYPE_ROLES = {
2300
+ text: "textbox",
2301
+ search: "searchbox",
2302
+ email: "textbox",
2303
+ url: "textbox",
2304
+ tel: "textbox",
2305
+ password: "textbox",
2306
+ number: "spinbutton",
2307
+ checkbox: "checkbox",
2308
+ radio: "radio",
2309
+ range: "slider",
2310
+ submit: "button",
2311
+ reset: "button",
2312
+ image: "button",
2313
+ file: "button",
1847
2314
  color: "button"
1848
2315
  };
1849
2316
  const LANDMARK_ROLES = /* @__PURE__ */ new Set([
@@ -2037,7 +2504,7 @@ var accessibilityTreeScript = () => (function() {
2037
2504
  if (ariaLevel) return parseInt(ariaLevel, 10);
2038
2505
  return void 0;
2039
2506
  }
2040
- function getState3(el) {
2507
+ function getState2(el) {
2041
2508
  const inputEl = el;
2042
2509
  const isCheckable = ["input", "menuitemcheckbox", "menuitemradio"].includes(el.tagName.toLowerCase()) || ["checkbox", "radio", "switch"].includes(el.getAttribute("role") || "");
2043
2510
  return {
@@ -2065,380 +2532,158 @@ var accessibilityTreeScript = () => (function() {
2065
2532
  const isLandmark = LANDMARK_ROLES.has(role);
2066
2533
  const hasIdentity = !!(name || isLandmark);
2067
2534
  const selector = hasIdentity ? getSelector(el) : "";
2068
- const node = { role, name, selector, level: getLevel(el) ?? "", ...getState3(el) };
2535
+ const node = { role, name, selector, level: getLevel(el) ?? "", ...getState2(el) };
2069
2536
  result.push(node);
2070
2537
  for (const child of Array.from(el.children)) {
2071
2538
  walk(child, depth + 1);
2072
- }
2073
- }
2074
- for (const child of Array.from(document.body.children)) {
2075
- walk(child, 0);
2076
- }
2077
- return result;
2078
- })();
2079
- async function getBrowserAccessibilityTree(browser) {
2080
- return browser.execute(accessibilityTreeScript);
2081
- }
2082
-
2083
- // src/tools/get-accessibility-tree.tool.ts
2084
- import { encode as encode2 } from "@toon-format/toon";
2085
- import { z as z8 } from "zod";
2086
- var getAccessibilityToolDefinition = {
2087
- name: "get_accessibility",
2088
- description: "Gets the accessibility tree: page structure with headings, landmarks, and semantic roles. Browser-only. Use to understand page layout and context around interactable elements.",
2089
- inputSchema: {
2090
- limit: z8.number().optional().describe("Maximum number of nodes to return. Default: 100. Use 0 for unlimited."),
2091
- offset: z8.number().optional().describe("Number of nodes to skip (for pagination). Default: 0."),
2092
- roles: z8.array(z8.string()).optional().describe('Filter to specific roles (e.g., ["heading", "navigation", "region"]). Default: all roles.')
2093
- }
2094
- };
2095
- var getAccessibilityTreeTool = async (args) => {
2096
- try {
2097
- const browser = getBrowser();
2098
- if (browser.isAndroid || browser.isIOS) {
2099
- return {
2100
- content: [{
2101
- type: "text",
2102
- text: "Error: get_accessibility is browser-only. For mobile apps, use get_visible_elements instead."
2103
- }]
2104
- };
2105
- }
2106
- const { limit = 100, offset = 0, roles } = args || {};
2107
- let nodes = await getBrowserAccessibilityTree(browser);
2108
- if (nodes.length === 0) {
2109
- return {
2110
- content: [{ type: "text", text: "No accessibility tree available" }]
2111
- };
2112
- }
2113
- nodes = nodes.filter((n) => n.name && n.name.trim() !== "");
2114
- if (roles && roles.length > 0) {
2115
- const roleSet = new Set(roles.map((r) => r.toLowerCase()));
2116
- nodes = nodes.filter((n) => n.role && roleSet.has(n.role.toLowerCase()));
2117
- }
2118
- const total = nodes.length;
2119
- if (offset > 0) {
2120
- nodes = nodes.slice(offset);
2121
- }
2122
- if (limit > 0) {
2123
- nodes = nodes.slice(0, limit);
2124
- }
2125
- const stateKeys = ["level", "disabled", "checked", "expanded", "selected", "pressed", "required", "readonly"];
2126
- const usedKeys = stateKeys.filter((k) => nodes.some((n) => n[k] !== ""));
2127
- const trimmed = nodes.map(({ role, name, selector, ...state2 }) => {
2128
- const node = { role, name, selector };
2129
- for (const k of usedKeys) node[k] = state2[k];
2130
- return node;
2131
- });
2132
- const result = {
2133
- total,
2134
- showing: trimmed.length,
2135
- hasMore: offset + trimmed.length < total,
2136
- nodes: trimmed
2137
- };
2138
- const toon = encode2(result).replace(/,""/g, ",").replace(/"",/g, ",");
2139
- return {
2140
- content: [{ type: "text", text: toon }]
2141
- };
2142
- } catch (e) {
2143
- return {
2144
- isError: true,
2145
- content: [{ type: "text", text: `Error getting accessibility tree: ${e}` }]
2146
- };
2147
- }
2148
- };
2149
-
2150
- // src/tools/take-screenshot.tool.ts
2151
- import { z as z9 } from "zod";
2152
- import sharp from "sharp";
2153
- var MAX_DIMENSION = 2e3;
2154
- var MAX_FILE_SIZE_BYTES = 1024 * 1024;
2155
- var takeScreenshotToolDefinition = {
2156
- name: "take_screenshot",
2157
- description: "captures a screenshot of the current page",
2158
- inputSchema: {
2159
- outputPath: z9.string().optional().describe("Optional path where to save the screenshot. If not provided, returns base64 data.")
2160
- }
2161
- };
2162
- async function processScreenshot(screenshotBase64) {
2163
- const inputBuffer = Buffer.from(screenshotBase64, "base64");
2164
- let image = sharp(inputBuffer);
2165
- const metadata = await image.metadata();
2166
- const width = metadata.width ?? 0;
2167
- const height = metadata.height ?? 0;
2168
- if (width > MAX_DIMENSION || height > MAX_DIMENSION) {
2169
- const resizeOptions = width > height ? { width: MAX_DIMENSION } : { height: MAX_DIMENSION };
2170
- image = image.resize(resizeOptions);
2171
- }
2172
- let outputBuffer = await image.png({ compressionLevel: 9 }).toBuffer();
2173
- if (outputBuffer.length > MAX_FILE_SIZE_BYTES) {
2174
- let quality = 90;
2175
- while (quality >= 10 && outputBuffer.length > MAX_FILE_SIZE_BYTES) {
2176
- outputBuffer = await image.jpeg({ quality, mozjpeg: true }).toBuffer();
2177
- quality -= 10;
2178
- }
2179
- return { data: outputBuffer, mimeType: "image/jpeg" };
2180
- }
2181
- return { data: outputBuffer, mimeType: "image/png" };
2182
- }
2183
- var takeScreenshotTool = async ({ outputPath }) => {
2184
- try {
2185
- const browser = getBrowser();
2186
- const screenshot = await browser.takeScreenshot();
2187
- const { data, mimeType } = await processScreenshot(screenshot);
2188
- if (outputPath) {
2189
- const fs = await import("fs");
2190
- await fs.promises.writeFile(outputPath, data);
2191
- const sizeKB2 = (data.length / 1024).toFixed(1);
2192
- return {
2193
- content: [{ type: "text", text: `Screenshot saved to ${outputPath} (${sizeKB2}KB, ${mimeType})` }]
2194
- };
2195
- }
2196
- const sizeKB = (data.length / 1024).toFixed(1);
2197
- return {
2198
- content: [
2199
- { type: "text", text: `Screenshot captured (${sizeKB}KB, ${mimeType}):` },
2200
- { type: "image", data: data.toString("base64"), mimeType }
2201
- ]
2202
- };
2203
- } catch (e) {
2204
- return {
2205
- isError: true,
2206
- content: [{ type: "text", text: `Error taking screenshot: ${e.message}` }]
2207
- };
2208
- }
2209
- };
2210
-
2211
- // src/tools/cookies.tool.ts
2212
- import { z as z10 } from "zod";
2213
- var getCookiesToolDefinition = {
2214
- name: "get_cookies",
2215
- description: "gets all cookies or a specific cookie by name",
2216
- inputSchema: {
2217
- name: z10.string().optional().describe("Optional cookie name to retrieve a specific cookie. If not provided, returns all cookies")
2218
- }
2219
- };
2220
- var getCookiesTool = async ({ name }) => {
2221
- try {
2222
- const browser = getBrowser();
2223
- if (name) {
2224
- const cookie = await browser.getCookies([name]);
2225
- if (cookie.length === 0) {
2226
- return {
2227
- content: [{ type: "text", text: `Cookie "${name}" not found` }]
2228
- };
2229
- }
2230
- return {
2231
- content: [{ type: "text", text: JSON.stringify(cookie[0], null, 2) }]
2232
- };
2233
- }
2234
- const cookies = await browser.getCookies();
2235
- if (cookies.length === 0) {
2236
- return {
2237
- content: [{ type: "text", text: "No cookies found" }]
2238
- };
2239
- }
2240
- return {
2241
- content: [{ type: "text", text: JSON.stringify(cookies, null, 2) }]
2242
- };
2243
- } catch (e) {
2244
- return {
2245
- isError: true,
2246
- content: [{ type: "text", text: `Error getting cookies: ${e}` }]
2247
- };
2248
- }
2249
- };
2250
- var setCookieToolDefinition = {
2251
- name: "set_cookie",
2252
- description: "sets a cookie with specified name, value, and optional attributes",
2253
- inputSchema: {
2254
- name: z10.string().describe("Cookie name"),
2255
- value: z10.string().describe("Cookie value"),
2256
- domain: z10.string().optional().describe("Cookie domain (defaults to current domain)"),
2257
- path: z10.string().optional().describe('Cookie path (defaults to "/")'),
2258
- expiry: z10.number().optional().describe("Expiry date as Unix timestamp in seconds"),
2259
- httpOnly: z10.boolean().optional().describe("HttpOnly flag"),
2260
- secure: z10.boolean().optional().describe("Secure flag"),
2261
- sameSite: z10.enum(["strict", "lax", "none"]).optional().describe("SameSite attribute")
2262
- }
2263
- };
2264
- var setCookieTool = async ({
2265
- name,
2266
- value,
2267
- domain,
2268
- path = "/",
2269
- expiry,
2270
- httpOnly,
2271
- secure,
2272
- sameSite
2273
- }) => {
2274
- try {
2275
- const browser = getBrowser();
2276
- const cookie = { name, value, path, domain, expiry, httpOnly, secure, sameSite };
2277
- await browser.setCookies(cookie);
2278
- return {
2279
- content: [{ type: "text", text: `Cookie "${name}" set successfully` }]
2280
- };
2281
- } catch (e) {
2282
- return {
2283
- isError: true,
2284
- content: [{ type: "text", text: `Error setting cookie: ${e}` }]
2285
- };
2286
- }
2287
- };
2288
- var deleteCookiesToolDefinition = {
2289
- name: "delete_cookies",
2290
- description: "deletes all cookies or a specific cookie by name",
2291
- inputSchema: {
2292
- name: z10.string().optional().describe("Optional cookie name to delete a specific cookie. If not provided, deletes all cookies")
2293
- }
2294
- };
2295
- var deleteCookiesTool = async ({ name }) => {
2296
- try {
2297
- const browser = getBrowser();
2298
- if (name) {
2299
- await browser.deleteCookies([name]);
2300
- return {
2301
- content: [{ type: "text", text: `Cookie "${name}" deleted successfully` }]
2302
- };
2303
- }
2304
- await browser.deleteCookies();
2305
- return {
2306
- content: [{ type: "text", text: "All cookies deleted successfully" }]
2307
- };
2308
- } catch (e) {
2309
- return {
2310
- isError: true,
2311
- content: [{ type: "text", text: `Error deleting cookies: ${e}` }]
2312
- };
2539
+ }
2313
2540
  }
2314
- };
2315
-
2316
- // src/tools/gestures.tool.ts
2317
- import { z as z11 } from "zod";
2318
- var tapElementToolDefinition = {
2319
- name: "tap_element",
2320
- description: "taps an element by selector or screen coordinates (mobile)",
2321
- inputSchema: {
2322
- selector: z11.string().optional().describe("Element selector (CSS, XPath, accessibility ID, or UiAutomator)"),
2323
- x: z11.number().optional().describe("X coordinate for screen tap (if no selector provided)"),
2324
- y: z11.number().optional().describe("Y coordinate for screen tap (if no selector provided)")
2541
+ for (const child of Array.from(document.body.children)) {
2542
+ walk(child, 0);
2325
2543
  }
2326
- };
2327
- var tapElementTool = async (args) => {
2544
+ return result;
2545
+ })();
2546
+ async function getBrowserAccessibilityTree(browser) {
2547
+ return browser.execute(accessibilityTreeScript);
2548
+ }
2549
+
2550
+ // src/resources/accessibility.resource.ts
2551
+ import { encode as encode3 } from "@toon-format/toon";
2552
+ async function readAccessibilityTree(params) {
2328
2553
  try {
2329
2554
  const browser = getBrowser();
2330
- const { selector, x, y } = args;
2331
- if (selector) {
2332
- const element = await browser.$(selector);
2333
- await element.tap();
2334
- return {
2335
- content: [{ type: "text", text: `Tapped element: ${selector}` }]
2336
- };
2337
- } else if (x !== void 0 && y !== void 0) {
2338
- await browser.tap({ x, y });
2555
+ if (browser.isAndroid || browser.isIOS) {
2339
2556
  return {
2340
- content: [{ type: "text", text: `Tapped at coordinates: (${x}, ${y})` }]
2557
+ mimeType: "text/plain",
2558
+ text: "Error: accessibility is browser-only. For mobile apps, use elements resource instead."
2341
2559
  };
2342
2560
  }
2343
- return {
2344
- isError: true,
2345
- content: [{ type: "text", text: "Error: Must provide either selector or x,y coordinates" }]
2561
+ const { limit = 0, offset = 0, roles } = params;
2562
+ let nodes = await getBrowserAccessibilityTree(browser);
2563
+ if (nodes.length === 0) {
2564
+ return { mimeType: "text/plain", text: "No accessibility tree available" };
2565
+ }
2566
+ nodes = nodes.filter((n) => n.name && n.name.trim() !== "");
2567
+ if (roles && roles.length > 0) {
2568
+ const roleSet = new Set(roles.map((r) => r.toLowerCase()));
2569
+ nodes = nodes.filter((n) => n.role && roleSet.has(n.role.toLowerCase()));
2570
+ }
2571
+ const total = nodes.length;
2572
+ if (offset > 0) {
2573
+ nodes = nodes.slice(offset);
2574
+ }
2575
+ if (limit > 0) {
2576
+ nodes = nodes.slice(0, limit);
2577
+ }
2578
+ const stateKeys = ["level", "disabled", "checked", "expanded", "selected", "pressed", "required", "readonly"];
2579
+ const usedKeys = stateKeys.filter((k) => nodes.some((n) => n[k] !== ""));
2580
+ const trimmed = nodes.map(({ role, name, selector, ...state2 }) => {
2581
+ const node = { role, name, selector };
2582
+ for (const k of usedKeys) node[k] = state2[k];
2583
+ return node;
2584
+ });
2585
+ const result = {
2586
+ total,
2587
+ showing: trimmed.length,
2588
+ hasMore: offset + trimmed.length < total,
2589
+ nodes: trimmed
2346
2590
  };
2591
+ const toon = encode3(result).replace(/,""/g, ",").replace(/"",/g, ",");
2592
+ return { mimeType: "text/plain", text: toon };
2347
2593
  } catch (e) {
2348
- return {
2349
- isError: true,
2350
- content: [{ type: "text", text: `Error tapping: ${e}` }]
2351
- };
2594
+ return { mimeType: "text/plain", text: `Error getting accessibility tree: ${e}` };
2352
2595
  }
2353
- };
2354
- var swipeToolDefinition = {
2355
- name: "swipe",
2356
- description: "performs a swipe gesture in specified direction (mobile)",
2357
- inputSchema: {
2358
- direction: z11.enum(["up", "down", "left", "right"]).describe("Swipe direction"),
2359
- duration: z11.number().min(100).max(5e3).optional().describe("Swipe duration in milliseconds (default: 500)"),
2360
- percent: z11.number().min(0).max(1).optional().describe("Percentage of screen to swipe (0-1, default: 0.5 for up/down, 0.95 for left/right)")
2596
+ }
2597
+ var accessibilityResource = {
2598
+ name: "session-current-accessibility",
2599
+ uri: "wdio://session/current/accessibility",
2600
+ description: "Accessibility tree for the current page. Returns all elements by default.",
2601
+ handler: async () => {
2602
+ const result = await readAccessibilityTree({});
2603
+ return { contents: [{ uri: "wdio://session/current/accessibility", mimeType: result.mimeType, text: result.text }] };
2361
2604
  }
2362
2605
  };
2363
- var contentToFingerDirection = {
2364
- up: "down",
2365
- down: "up",
2366
- left: "right",
2367
- right: "left"
2368
- };
2369
- var swipeTool = async (args) => {
2606
+
2607
+ // src/resources/screenshot.resource.ts
2608
+ init_state();
2609
+ import sharp from "sharp";
2610
+ var MAX_DIMENSION = 2e3;
2611
+ var MAX_FILE_SIZE_BYTES = 1024 * 1024;
2612
+ async function processScreenshot(screenshotBase64) {
2613
+ const inputBuffer = Buffer.from(screenshotBase64, "base64");
2614
+ let image = sharp(inputBuffer);
2615
+ const metadata = await image.metadata();
2616
+ const width = metadata.width ?? 0;
2617
+ const height = metadata.height ?? 0;
2618
+ if (width > MAX_DIMENSION || height > MAX_DIMENSION) {
2619
+ const resizeOptions = width > height ? { width: MAX_DIMENSION } : { height: MAX_DIMENSION };
2620
+ image = image.resize(resizeOptions);
2621
+ }
2622
+ let outputBuffer = await image.png({ compressionLevel: 9 }).toBuffer();
2623
+ if (outputBuffer.length > MAX_FILE_SIZE_BYTES) {
2624
+ let quality = 90;
2625
+ while (quality >= 10 && outputBuffer.length > MAX_FILE_SIZE_BYTES) {
2626
+ outputBuffer = await image.jpeg({ quality, mozjpeg: true }).toBuffer();
2627
+ quality -= 10;
2628
+ }
2629
+ return { data: outputBuffer, mimeType: "image/jpeg" };
2630
+ }
2631
+ return { data: outputBuffer, mimeType: "image/png" };
2632
+ }
2633
+ async function readScreenshot() {
2370
2634
  try {
2371
2635
  const browser = getBrowser();
2372
- const { direction, duration, percent } = args;
2373
- const isVertical = direction === "up" || direction === "down";
2374
- const defaultPercent = isVertical ? 0.5 : 0.95;
2375
- const effectivePercent = percent ?? defaultPercent;
2376
- const effectiveDuration = duration ?? 500;
2377
- const fingerDirection = contentToFingerDirection[direction];
2378
- await browser.swipe({ direction: fingerDirection, duration: effectiveDuration, percent: effectivePercent });
2379
- return {
2380
- content: [{ type: "text", text: `Swiped ${direction}` }]
2381
- };
2636
+ const screenshot = await browser.takeScreenshot();
2637
+ const { data, mimeType } = await processScreenshot(screenshot);
2638
+ return { mimeType, blob: data.toString("base64") };
2382
2639
  } catch (e) {
2383
- return {
2384
- isError: true,
2385
- content: [{ type: "text", text: `Error swiping: ${e}` }]
2386
- };
2640
+ return { mimeType: "text/plain", blob: Buffer.from(`Error: ${e}`).toString("base64") };
2387
2641
  }
2388
- };
2389
- var dragAndDropToolDefinition = {
2390
- name: "drag_and_drop",
2391
- description: "drags an element to another element or coordinates (mobile)",
2392
- inputSchema: {
2393
- sourceSelector: z11.string().describe("Source element selector to drag"),
2394
- targetSelector: z11.string().optional().describe("Target element selector to drop onto"),
2395
- x: z11.number().optional().describe("Target X offset (if no targetSelector)"),
2396
- y: z11.number().optional().describe("Target Y offset (if no targetSelector)"),
2397
- duration: z11.number().min(100).max(5e3).optional().describe("Drag duration in milliseconds")
2642
+ }
2643
+ var screenshotResource = {
2644
+ name: "session-current-screenshot",
2645
+ uri: "wdio://session/current/screenshot",
2646
+ description: "Screenshot of the current page",
2647
+ handler: async () => {
2648
+ const result = await readScreenshot();
2649
+ return { contents: [{ uri: "wdio://session/current/screenshot", mimeType: result.mimeType, blob: result.blob }] };
2398
2650
  }
2399
2651
  };
2400
- var dragAndDropTool = async (args) => {
2652
+
2653
+ // src/resources/cookies.resource.ts
2654
+ init_state();
2655
+ async function readCookies(name) {
2401
2656
  try {
2402
2657
  const browser = getBrowser();
2403
- const { sourceSelector, targetSelector, x, y, duration } = args;
2404
- const sourceElement = await browser.$(sourceSelector);
2405
- if (targetSelector) {
2406
- const targetElement = await browser.$(targetSelector);
2407
- await sourceElement.dragAndDrop(targetElement, { duration });
2408
- return {
2409
- content: [{ type: "text", text: `Dragged ${sourceSelector} to ${targetSelector}` }]
2410
- };
2411
- } else if (x !== void 0 && y !== void 0) {
2412
- await sourceElement.dragAndDrop({ x, y }, { duration });
2413
- return {
2414
- content: [{ type: "text", text: `Dragged ${sourceSelector} by (${x}, ${y})` }]
2415
- };
2658
+ if (name) {
2659
+ const cookie = await browser.getCookies([name]);
2660
+ if (cookie.length === 0) {
2661
+ return { mimeType: "application/json", text: JSON.stringify(null) };
2662
+ }
2663
+ return { mimeType: "application/json", text: JSON.stringify(cookie[0]) };
2416
2664
  }
2417
- return {
2418
- isError: true,
2419
- content: [{ type: "text", text: "Error: Must provide either targetSelector or x,y coordinates" }]
2420
- };
2665
+ const cookies = await browser.getCookies();
2666
+ return { mimeType: "application/json", text: JSON.stringify(cookies) };
2421
2667
  } catch (e) {
2422
- return {
2423
- isError: true,
2424
- content: [{ type: "text", text: `Error dragging: ${e}` }]
2425
- };
2668
+ return { mimeType: "application/json", text: JSON.stringify({ error: String(e) }) };
2426
2669
  }
2427
- };
2428
-
2429
- // src/tools/app-actions.tool.ts
2430
- import { z as z12 } from "zod";
2431
- var getAppStateToolDefinition = {
2432
- name: "get_app_state",
2433
- description: "gets the state of an app (not installed, not running, background, foreground)",
2434
- inputSchema: {
2435
- bundleId: z12.string().describe("App bundle ID (e.g., com.example.app)")
2670
+ }
2671
+ var cookiesResource = {
2672
+ name: "session-current-cookies",
2673
+ uri: "wdio://session/current/cookies",
2674
+ description: "Cookies for the current session",
2675
+ handler: async () => {
2676
+ const result = await readCookies();
2677
+ return { contents: [{ uri: "wdio://session/current/cookies", mimeType: result.mimeType, text: result.text }] };
2436
2678
  }
2437
2679
  };
2438
- var getAppStateTool = async (args) => {
2680
+
2681
+ // src/resources/app-state.resource.ts
2682
+ init_state();
2683
+ import { ResourceTemplate as ResourceTemplate2 } from "@modelcontextprotocol/sdk/server/mcp";
2684
+ async function readAppState(bundleId) {
2439
2685
  try {
2440
2686
  const browser = getBrowser();
2441
- const { bundleId } = args;
2442
2687
  const appIdentifier = browser.isAndroid ? { appId: bundleId } : { bundleId };
2443
2688
  const state2 = await browser.execute("mobile: queryAppState", appIdentifier);
2444
2689
  const stateMap = {
@@ -2449,283 +2694,464 @@ var getAppStateTool = async (args) => {
2449
2694
  4: "running in foreground"
2450
2695
  };
2451
2696
  return {
2452
- content: [
2453
- {
2454
- type: "text",
2455
- text: `App state for ${bundleId}: ${stateMap[state2] || "unknown: " + state2}`
2456
- }
2457
- ]
2458
- };
2459
- } catch (e) {
2460
- return {
2461
- isError: true,
2462
- content: [{ type: "text", text: `Error getting app state: ${e}` }]
2463
- };
2464
- }
2465
- };
2466
-
2467
- // src/tools/context.tool.ts
2468
- import { z as z13 } from "zod";
2469
- var getContextsToolDefinition = {
2470
- name: "get_contexts",
2471
- description: "lists available contexts (NATIVE_APP, WEBVIEW)",
2472
- inputSchema: {}
2473
- };
2474
- var getCurrentContextToolDefinition = {
2475
- name: "get_current_context",
2476
- description: "shows the currently active context",
2477
- inputSchema: {}
2478
- };
2479
- var switchContextToolDefinition = {
2480
- name: "switch_context",
2481
- description: "switches between native and webview contexts",
2482
- inputSchema: {
2483
- context: z13.string().describe(
2484
- 'Context name to switch to (e.g., "NATIVE_APP", "WEBVIEW_com.example.app", or use index from get_contexts)'
2485
- )
2486
- }
2487
- };
2488
- var getContextsTool = async () => {
2489
- try {
2490
- const browser = getBrowser();
2491
- const contexts = await browser.getContexts();
2492
- return {
2493
- content: [
2494
- {
2495
- type: "text",
2496
- text: `Available contexts:
2497
- ${contexts.map((ctx, idx) => `${idx + 1}. ${ctx}`).join("\n")}`
2498
- }
2499
- ]
2500
- };
2501
- } catch (e) {
2502
- return {
2503
- isError: true,
2504
- content: [{ type: "text", text: `Error getting contexts: ${e}` }]
2505
- };
2506
- }
2507
- };
2508
- var getCurrentContextTool = async () => {
2509
- try {
2510
- const browser = getBrowser();
2511
- const currentContext = await browser.getContext();
2512
- return {
2513
- content: [{ type: "text", text: `Current context: ${JSON.stringify(currentContext)}` }]
2697
+ mimeType: "text/plain",
2698
+ text: `App state for ${bundleId}: ${stateMap[state2] || "unknown: " + state2}`
2514
2699
  };
2515
2700
  } catch (e) {
2516
- return {
2517
- isError: true,
2518
- content: [{ type: "text", text: `Error getting current context: ${e}` }]
2519
- };
2701
+ return { mimeType: "text/plain", text: `Error getting app state: ${e}` };
2520
2702
  }
2521
- };
2522
- var switchContextTool = async (args) => {
2523
- try {
2524
- const browser = getBrowser();
2525
- const { context } = args;
2526
- let targetContext = context;
2527
- if (/^\d+$/.test(context)) {
2528
- const contexts = await browser.getContexts();
2529
- const index = parseInt(context, 10) - 1;
2530
- if (index >= 0 && index < contexts.length) {
2531
- targetContext = contexts[index];
2532
- } else {
2533
- throw new Error(`Error: Invalid context index ${context}. Available contexts: ${contexts.length}`);
2534
- }
2535
- }
2536
- await browser.switchContext(targetContext);
2537
- return {
2538
- content: [{ type: "text", text: `Switched to context: ${targetContext}` }]
2539
- };
2540
- } catch (e) {
2541
- return {
2542
- isError: true,
2543
- content: [{ type: "text", text: `Error switching context: ${e}` }]
2544
- };
2703
+ }
2704
+ var appStateResource = {
2705
+ name: "session-current-app-state",
2706
+ template: new ResourceTemplate2("wdio://session/current/app-state/{bundleId}", { list: void 0 }),
2707
+ description: "App state for a given bundle ID",
2708
+ handler: async (uri, variables) => {
2709
+ const result = await readAppState(variables.bundleId);
2710
+ return { contents: [{ uri: uri.href, mimeType: result.mimeType, text: result.text }] };
2545
2711
  }
2546
2712
  };
2547
2713
 
2548
- // src/tools/device.tool.ts
2549
- import { z as z14 } from "zod";
2550
- var hideKeyboardToolDefinition = {
2551
- name: "hide_keyboard",
2552
- description: "hides the on-screen keyboard",
2553
- inputSchema: {}
2554
- };
2555
- var getGeolocationToolDefinition = {
2556
- name: "get_geolocation",
2557
- description: "gets current device geolocation",
2558
- inputSchema: {}
2559
- };
2560
- var rotateDeviceToolDefinition = {
2561
- name: "rotate_device",
2562
- description: "rotates device to portrait or landscape orientation",
2563
- inputSchema: {
2564
- orientation: z14.enum(["PORTRAIT", "LANDSCAPE"]).describe("Device orientation")
2565
- }
2566
- };
2567
- var setGeolocationToolDefinition = {
2568
- name: "set_geolocation",
2569
- description: "sets device geolocation (latitude, longitude, altitude)",
2570
- inputSchema: {
2571
- latitude: z14.number().min(-90).max(90).describe("Latitude coordinate"),
2572
- longitude: z14.number().min(-180).max(180).describe("Longitude coordinate"),
2573
- altitude: z14.number().optional().describe("Altitude in meters (optional)")
2574
- }
2575
- };
2576
- var rotateDeviceTool = async (args) => {
2714
+ // src/resources/contexts.resource.ts
2715
+ init_state();
2716
+ async function readContexts() {
2577
2717
  try {
2578
2718
  const browser = getBrowser();
2579
- const { orientation } = args;
2580
- await browser.setOrientation(orientation);
2581
- return {
2582
- content: [{ type: "text", text: `Device rotated to: ${orientation}` }]
2583
- };
2719
+ const contexts = await browser.getContexts();
2720
+ return { mimeType: "application/json", text: JSON.stringify(contexts) };
2584
2721
  } catch (e) {
2585
- return {
2586
- isError: true,
2587
- content: [{ type: "text", text: `Error rotating device: ${e}` }]
2588
- };
2722
+ return { mimeType: "text/plain", text: `Error: ${e}` };
2589
2723
  }
2590
- };
2591
- var hideKeyboardTool = async () => {
2724
+ }
2725
+ async function readCurrentContext() {
2592
2726
  try {
2593
2727
  const browser = getBrowser();
2594
- await browser.hideKeyboard();
2595
- return {
2596
- content: [{ type: "text", text: "Keyboard hidden" }]
2597
- };
2728
+ const currentContext = await browser.getContext();
2729
+ return { mimeType: "application/json", text: JSON.stringify(currentContext) };
2598
2730
  } catch (e) {
2599
- return {
2600
- isError: true,
2601
- content: [{ type: "text", text: `Error hiding keyboard: ${e}` }]
2602
- };
2731
+ return { mimeType: "text/plain", text: `Error: ${e}` };
2732
+ }
2733
+ }
2734
+ var contextsResource = {
2735
+ name: "session-current-contexts",
2736
+ uri: "wdio://session/current/contexts",
2737
+ description: "Available contexts (NATIVE_APP, WEBVIEW)",
2738
+ handler: async () => {
2739
+ const result = await readContexts();
2740
+ return { contents: [{ uri: "wdio://session/current/contexts", mimeType: result.mimeType, text: result.text }] };
2741
+ }
2742
+ };
2743
+ var contextResource = {
2744
+ name: "session-current-context",
2745
+ uri: "wdio://session/current/context",
2746
+ description: "Currently active context",
2747
+ handler: async () => {
2748
+ const result = await readCurrentContext();
2749
+ return { contents: [{ uri: "wdio://session/current/context", mimeType: result.mimeType, text: result.text }] };
2603
2750
  }
2604
2751
  };
2605
- var getGeolocationTool = async () => {
2752
+
2753
+ // src/resources/geolocation.resource.ts
2754
+ init_state();
2755
+ async function readGeolocation() {
2606
2756
  try {
2607
2757
  const browser = getBrowser();
2608
2758
  const location = await browser.getGeoLocation();
2609
- return {
2610
- content: [
2611
- {
2612
- type: "text",
2613
- text: `Location:
2614
- Latitude: ${location.latitude}
2615
- Longitude: ${location.longitude}
2616
- Altitude: ${location.altitude || "N/A"}`
2617
- }
2618
- ]
2619
- };
2759
+ return { mimeType: "application/json", text: JSON.stringify(location) };
2620
2760
  } catch (e) {
2621
- return {
2622
- isError: true,
2623
- content: [{ type: "text", text: `Error getting geolocation: ${e}` }]
2624
- };
2761
+ return { mimeType: "text/plain", text: `Error: ${e}` };
2762
+ }
2763
+ }
2764
+ var geolocationResource = {
2765
+ name: "session-current-geolocation",
2766
+ uri: "wdio://session/current/geolocation",
2767
+ description: "Current device geolocation",
2768
+ handler: async () => {
2769
+ const result = await readGeolocation();
2770
+ return { contents: [{ uri: "wdio://session/current/geolocation", mimeType: result.mimeType, text: result.text }] };
2625
2771
  }
2626
2772
  };
2627
- var setGeolocationTool = async (args) => {
2773
+
2774
+ // src/resources/tabs.resource.ts
2775
+ init_state();
2776
+ async function readTabs() {
2628
2777
  try {
2629
2778
  const browser = getBrowser();
2630
- const { latitude, longitude, altitude } = args;
2631
- await browser.setGeoLocation({ latitude, longitude, altitude });
2632
- return {
2633
- content: [
2634
- {
2635
- type: "text",
2636
- text: `Geolocation set to:
2637
- Latitude: ${latitude}
2638
- Longitude: ${longitude}${altitude ? `
2639
- Altitude: ${altitude}m` : ""}`
2640
- }
2641
- ]
2642
- };
2779
+ const handles = await browser.getWindowHandles();
2780
+ const currentHandle = await browser.getWindowHandle();
2781
+ const tabs = [];
2782
+ for (const handle of handles) {
2783
+ await browser.switchToWindow(handle);
2784
+ tabs.push({
2785
+ handle,
2786
+ title: await browser.getTitle(),
2787
+ url: await browser.getUrl(),
2788
+ isActive: handle === currentHandle
2789
+ });
2790
+ }
2791
+ await browser.switchToWindow(currentHandle);
2792
+ return { mimeType: "application/json", text: JSON.stringify(tabs) };
2643
2793
  } catch (e) {
2644
- return {
2645
- isError: true,
2646
- content: [{ type: "text", text: `Error setting geolocation: ${e}` }]
2647
- };
2794
+ return { mimeType: "text/plain", text: `Error: ${e}` };
2795
+ }
2796
+ }
2797
+ var tabsResource = {
2798
+ name: "session-current-tabs",
2799
+ uri: "wdio://session/current/tabs",
2800
+ description: "Browser tabs in the current session",
2801
+ handler: async () => {
2802
+ const result = await readTabs();
2803
+ return { contents: [{ uri: "wdio://session/current/tabs", mimeType: result.mimeType, text: result.text }] };
2648
2804
  }
2649
2805
  };
2650
2806
 
2651
- // src/tools/execute-script.tool.ts
2652
- import { z as z15 } from "zod";
2653
- var executeScriptToolDefinition = {
2654
- name: "execute_script",
2655
- description: `Executes JavaScript in browser or mobile commands via Appium.
2656
-
2657
- **Option B for browser interaction** \u2014 prefer get_visible_elements or click_element/set_value with a selector instead. Use execute_script only when no dedicated tool covers the action (e.g. reading computed values, triggering custom events, scrolling to a position).
2807
+ // src/tools/session.tool.ts
2808
+ init_state();
2809
+ import { remote } from "webdriverio";
2810
+ import { z as z14 } from "zod";
2658
2811
 
2659
- **Browser:** Runs JavaScript in page context. Use 'return' to get values back.
2660
- - Example: execute_script({ script: "return document.title" })
2661
- - Example: execute_script({ script: "return window.scrollY" })
2662
- - Example: execute_script({ script: "arguments[0].click()", args: ["#myButton"] })
2812
+ // src/session/lifecycle.ts
2813
+ init_state();
2814
+ function handleSessionTransition(newSessionId) {
2815
+ const state2 = getState();
2816
+ if (state2.currentSession && state2.currentSession !== newSessionId) {
2817
+ const outgoing = state2.sessionHistory.get(state2.currentSession);
2818
+ if (outgoing) {
2819
+ outgoing.steps.push({
2820
+ index: outgoing.steps.length + 1,
2821
+ tool: "__session_transition__",
2822
+ params: { newSessionId },
2823
+ status: "ok",
2824
+ durationMs: 0,
2825
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2826
+ });
2827
+ outgoing.endedAt = (/* @__PURE__ */ new Date()).toISOString();
2828
+ }
2829
+ }
2830
+ }
2831
+ function registerSession(sessionId, browser, metadata, historyEntry) {
2832
+ const state2 = getState();
2833
+ const oldSessionId = state2.currentSession;
2834
+ if (oldSessionId && oldSessionId !== sessionId) {
2835
+ handleSessionTransition(sessionId);
2836
+ }
2837
+ state2.browsers.set(sessionId, browser);
2838
+ state2.sessionMetadata.set(sessionId, metadata);
2839
+ state2.sessionHistory.set(sessionId, historyEntry);
2840
+ state2.currentSession = sessionId;
2841
+ if (oldSessionId && oldSessionId !== sessionId) {
2842
+ const oldBrowser = state2.browsers.get(oldSessionId);
2843
+ if (oldBrowser) {
2844
+ oldBrowser.deleteSession().catch(() => {
2845
+ });
2846
+ state2.browsers.delete(oldSessionId);
2847
+ state2.sessionMetadata.delete(oldSessionId);
2848
+ }
2849
+ }
2850
+ }
2851
+ async function closeSession(sessionId, detach, isAttached, force) {
2852
+ const state2 = getState();
2853
+ const browser = state2.browsers.get(sessionId);
2854
+ if (!browser) return;
2855
+ const history = state2.sessionHistory.get(sessionId);
2856
+ if (history) {
2857
+ history.endedAt = (/* @__PURE__ */ new Date()).toISOString();
2858
+ }
2859
+ if (force || !detach && !isAttached) {
2860
+ await browser.deleteSession();
2861
+ }
2862
+ state2.browsers.delete(sessionId);
2863
+ state2.sessionMetadata.delete(sessionId);
2864
+ if (state2.currentSession === sessionId) {
2865
+ state2.currentSession = null;
2866
+ }
2867
+ }
2663
2868
 
2664
- **Mobile (Appium):** Executes mobile-specific commands using 'mobile: <command>' syntax.
2665
- - Press key (Android): execute_script({ script: "mobile: pressKey", args: [{ keycode: 4 }] }) // BACK=4, HOME=3
2666
- - Activate app: execute_script({ script: "mobile: activateApp", args: [{ appId: "com.example" }] })
2667
- - Terminate app: execute_script({ script: "mobile: terminateApp", args: [{ appId: "com.example" }] })
2668
- - Deep link: execute_script({ script: "mobile: deepLink", args: [{ url: "myapp://screen", package: "com.example" }] })
2669
- - Shell command (Android): execute_script({ script: "mobile: shell", args: [{ command: "dumpsys", args: ["battery"] }] })`,
2670
- inputSchema: {
2671
- script: z15.string().describe('JavaScript code (browser) or mobile command string like "mobile: pressKey" (Appium)'),
2672
- args: z15.array(z15.any()).optional().describe("Arguments to pass to the script. For browser: element selectors or values. For mobile commands: command-specific parameters as objects.")
2869
+ // src/providers/local-browser.provider.ts
2870
+ var LocalBrowserProvider = class {
2871
+ name = "local-browser";
2872
+ getConnectionConfig(_options) {
2873
+ return {};
2874
+ }
2875
+ buildCapabilities(options) {
2876
+ const selectedBrowser = options.browser ?? "chrome";
2877
+ const headless = options.headless ?? true;
2878
+ const windowWidth = options.windowWidth ?? 1920;
2879
+ const windowHeight = options.windowHeight ?? 1080;
2880
+ const userCapabilities = options.capabilities ?? {};
2881
+ const headlessSupported = selectedBrowser !== "safari";
2882
+ const effectiveHeadless = headless && headlessSupported;
2883
+ const chromiumArgs = [
2884
+ `--window-size=${windowWidth},${windowHeight}`,
2885
+ "--no-sandbox",
2886
+ "--disable-search-engine-choice-screen",
2887
+ "--disable-infobars",
2888
+ "--log-level=3",
2889
+ "--use-fake-device-for-media-stream",
2890
+ "--use-fake-ui-for-media-stream",
2891
+ "--disable-web-security",
2892
+ "--allow-running-insecure-content"
2893
+ ];
2894
+ if (effectiveHeadless) {
2895
+ chromiumArgs.push("--headless=new");
2896
+ chromiumArgs.push("--disable-gpu");
2897
+ chromiumArgs.push("--disable-dev-shm-usage");
2898
+ }
2899
+ const firefoxArgs = [];
2900
+ if (effectiveHeadless && selectedBrowser === "firefox") {
2901
+ firefoxArgs.push("-headless");
2902
+ }
2903
+ const capabilities = {
2904
+ acceptInsecureCerts: true
2905
+ };
2906
+ switch (selectedBrowser) {
2907
+ case "chrome":
2908
+ capabilities.browserName = "chrome";
2909
+ capabilities["goog:chromeOptions"] = { args: chromiumArgs };
2910
+ break;
2911
+ case "edge":
2912
+ capabilities.browserName = "msedge";
2913
+ capabilities["ms:edgeOptions"] = { args: chromiumArgs };
2914
+ break;
2915
+ case "firefox":
2916
+ capabilities.browserName = "firefox";
2917
+ if (firefoxArgs.length > 0) {
2918
+ capabilities["moz:firefoxOptions"] = { args: firefoxArgs };
2919
+ }
2920
+ break;
2921
+ case "safari":
2922
+ capabilities.browserName = "safari";
2923
+ break;
2924
+ }
2925
+ const mergedCapabilities = {
2926
+ ...capabilities,
2927
+ ...userCapabilities,
2928
+ "goog:chromeOptions": this.mergeCapabilityOptions(capabilities["goog:chromeOptions"], userCapabilities["goog:chromeOptions"]),
2929
+ "ms:edgeOptions": this.mergeCapabilityOptions(capabilities["ms:edgeOptions"], userCapabilities["ms:edgeOptions"]),
2930
+ "moz:firefoxOptions": this.mergeCapabilityOptions(capabilities["moz:firefoxOptions"], userCapabilities["moz:firefoxOptions"])
2931
+ };
2932
+ for (const [key, value] of Object.entries(mergedCapabilities)) {
2933
+ if (value === void 0) {
2934
+ delete mergedCapabilities[key];
2935
+ }
2936
+ }
2937
+ return mergedCapabilities;
2938
+ }
2939
+ getSessionType(_options) {
2940
+ return "browser";
2941
+ }
2942
+ shouldAutoDetach(_options) {
2943
+ return false;
2944
+ }
2945
+ mergeCapabilityOptions(defaultOptions, customOptions) {
2946
+ if (!defaultOptions || typeof defaultOptions !== "object" || !customOptions || typeof customOptions !== "object") {
2947
+ return customOptions ?? defaultOptions;
2948
+ }
2949
+ const defaultRecord = defaultOptions;
2950
+ const customRecord = customOptions;
2951
+ const merged = { ...defaultRecord, ...customRecord };
2952
+ if (Array.isArray(defaultRecord.args) || Array.isArray(customRecord.args)) {
2953
+ merged.args = [
2954
+ ...Array.isArray(defaultRecord.args) ? defaultRecord.args : [],
2955
+ ...Array.isArray(customRecord.args) ? customRecord.args : []
2956
+ ];
2957
+ }
2958
+ return merged;
2673
2959
  }
2674
2960
  };
2675
- var executeScriptTool = async (args) => {
2676
- try {
2677
- const browser = getBrowser();
2678
- const { script, args: scriptArgs = [] } = args;
2679
- const resolvedArgs = await Promise.all(
2680
- scriptArgs.map(async (arg) => {
2681
- if (typeof arg === "string" && !script.startsWith("mobile:")) {
2682
- try {
2683
- const element = await browser.$(arg);
2684
- if (await element.isExisting()) {
2685
- return element;
2686
- }
2687
- } catch {
2688
- }
2689
- }
2690
- return arg;
2691
- })
2692
- );
2693
- const result = await browser.execute(script, ...resolvedArgs);
2694
- let resultText;
2695
- if (result === void 0 || result === null) {
2696
- resultText = "Script executed successfully (no return value)";
2697
- } else if (typeof result === "object") {
2698
- try {
2699
- resultText = `Result: ${JSON.stringify(result, null, 2)}`;
2700
- } catch {
2701
- resultText = `Result: ${String(result)}`;
2961
+ var localBrowserProvider = new LocalBrowserProvider();
2962
+
2963
+ // src/config/appium.config.ts
2964
+ function getAppiumServerConfig(overrides) {
2965
+ return {
2966
+ hostname: overrides?.hostname || process.env.APPIUM_URL || "127.0.0.1",
2967
+ port: overrides?.port || Number(process.env.APPIUM_URL_PORT) || 4723,
2968
+ path: overrides?.path || process.env.APPIUM_PATH || "/"
2969
+ };
2970
+ }
2971
+ function buildIOSCapabilities(appPath, options) {
2972
+ const capabilities = {
2973
+ platformName: "iOS",
2974
+ "appium:platformVersion": options.platformVersion,
2975
+ "appium:deviceName": options.deviceName,
2976
+ "appium:automationName": options.automationName || "XCUITest"
2977
+ };
2978
+ if (appPath) {
2979
+ capabilities["appium:app"] = appPath;
2980
+ }
2981
+ if (options.udid) {
2982
+ capabilities["appium:udid"] = options.udid;
2983
+ }
2984
+ if (options.noReset !== void 0) {
2985
+ capabilities["appium:noReset"] = options.noReset;
2986
+ }
2987
+ if (options.fullReset !== void 0) {
2988
+ capabilities["appium:fullReset"] = options.fullReset;
2989
+ }
2990
+ if (options.newCommandTimeout !== void 0) {
2991
+ capabilities["appium:newCommandTimeout"] = options.newCommandTimeout;
2992
+ }
2993
+ capabilities["appium:autoGrantPermissions"] = options.autoGrantPermissions ?? true;
2994
+ capabilities["appium:autoAcceptAlerts"] = options.autoAcceptAlerts ?? true;
2995
+ if (options.autoDismissAlerts !== void 0) {
2996
+ capabilities["appium:autoDismissAlerts"] = options.autoDismissAlerts;
2997
+ capabilities["appium:autoAcceptAlerts"] = void 0;
2998
+ }
2999
+ for (const [key, value] of Object.entries(options)) {
3000
+ if (!["deviceName", "platformVersion", "automationName", "autoGrantPermissions", "autoAcceptAlerts", "autoDismissAlerts", "udid", "noReset", "fullReset", "newCommandTimeout"].includes(
3001
+ key
3002
+ )) {
3003
+ capabilities[`appium:${key}`] = value;
3004
+ }
3005
+ }
3006
+ return capabilities;
3007
+ }
3008
+ function buildAndroidCapabilities(appPath, options) {
3009
+ const capabilities = {
3010
+ platformName: "Android",
3011
+ "appium:platformVersion": options.platformVersion,
3012
+ "appium:deviceName": options.deviceName,
3013
+ "appium:automationName": options.automationName || "UiAutomator2"
3014
+ };
3015
+ if (appPath) {
3016
+ capabilities["appium:app"] = appPath;
3017
+ }
3018
+ if (options.noReset !== void 0) {
3019
+ capabilities["appium:noReset"] = options.noReset;
3020
+ }
3021
+ if (options.fullReset !== void 0) {
3022
+ capabilities["appium:fullReset"] = options.fullReset;
3023
+ }
3024
+ if (options.newCommandTimeout !== void 0) {
3025
+ capabilities["appium:newCommandTimeout"] = options.newCommandTimeout;
3026
+ }
3027
+ capabilities["appium:autoGrantPermissions"] = options.autoGrantPermissions ?? true;
3028
+ capabilities["appium:autoAcceptAlerts"] = options.autoAcceptAlerts ?? true;
3029
+ if (options.autoDismissAlerts !== void 0) {
3030
+ capabilities["appium:autoDismissAlerts"] = options.autoDismissAlerts;
3031
+ capabilities["appium:autoAcceptAlerts"] = void 0;
3032
+ }
3033
+ if (options.appWaitActivity) {
3034
+ capabilities["appium:appWaitActivity"] = options.appWaitActivity;
3035
+ }
3036
+ for (const [key, value] of Object.entries(options)) {
3037
+ if (!["deviceName", "platformVersion", "automationName", "autoGrantPermissions", "autoAcceptAlerts", "autoDismissAlerts", "appWaitActivity", "noReset", "fullReset", "newCommandTimeout"].includes(
3038
+ key
3039
+ )) {
3040
+ capabilities[`appium:${key}`] = value;
3041
+ }
3042
+ }
3043
+ return capabilities;
3044
+ }
3045
+
3046
+ // src/providers/local-appium.provider.ts
3047
+ var LocalAppiumProvider = class {
3048
+ name = "local-appium";
3049
+ getConnectionConfig(options) {
3050
+ const config = getAppiumServerConfig({
3051
+ hostname: options.appiumHost,
3052
+ port: options.appiumPort,
3053
+ path: options.appiumPath
3054
+ });
3055
+ return { protocol: "http", ...config };
3056
+ }
3057
+ buildCapabilities(options) {
3058
+ const platform2 = options.platform;
3059
+ const appPath = options.appPath;
3060
+ const deviceName = options.deviceName;
3061
+ const platformVersion = options.platformVersion;
3062
+ const autoGrantPermissions = options.autoGrantPermissions;
3063
+ const autoAcceptAlerts = options.autoAcceptAlerts;
3064
+ const autoDismissAlerts = options.autoDismissAlerts;
3065
+ const udid = options.udid;
3066
+ const noReset = options.noReset;
3067
+ const fullReset = options.fullReset;
3068
+ const newCommandTimeout = options.newCommandTimeout;
3069
+ const appWaitActivity = options.appWaitActivity;
3070
+ const userCapabilities = options.capabilities ?? {};
3071
+ const capabilities = platform2 === "iOS" ? buildIOSCapabilities(appPath, {
3072
+ deviceName,
3073
+ platformVersion,
3074
+ automationName: options.automationName || "XCUITest",
3075
+ autoGrantPermissions,
3076
+ autoAcceptAlerts,
3077
+ autoDismissAlerts,
3078
+ udid,
3079
+ noReset,
3080
+ fullReset,
3081
+ newCommandTimeout
3082
+ }) : buildAndroidCapabilities(appPath, {
3083
+ deviceName,
3084
+ platformVersion,
3085
+ automationName: options.automationName || "UiAutomator2",
3086
+ autoGrantPermissions,
3087
+ autoAcceptAlerts,
3088
+ autoDismissAlerts,
3089
+ appWaitActivity,
3090
+ noReset,
3091
+ fullReset,
3092
+ newCommandTimeout
3093
+ });
3094
+ const mergedCapabilities = {
3095
+ ...capabilities,
3096
+ ...userCapabilities
3097
+ };
3098
+ for (const [key, value] of Object.entries(mergedCapabilities)) {
3099
+ if (value === void 0) {
3100
+ delete mergedCapabilities[key];
2702
3101
  }
2703
- } else {
2704
- resultText = `Result: ${result}`;
2705
3102
  }
2706
- return {
2707
- content: [{ type: "text", text: resultText }]
2708
- };
2709
- } catch (e) {
2710
- return {
2711
- isError: true,
2712
- content: [{ type: "text", text: `Error executing script: ${e}` }]
2713
- };
3103
+ return mergedCapabilities;
3104
+ }
3105
+ getSessionType(options) {
3106
+ const platform2 = options.platform;
3107
+ return platform2.toLowerCase();
3108
+ }
3109
+ shouldAutoDetach(options) {
3110
+ return options.noReset === true || !options.appPath;
2714
3111
  }
2715
3112
  };
3113
+ var localAppiumProvider = new LocalAppiumProvider();
2716
3114
 
2717
- // src/tools/attach-browser.tool.ts
2718
- import { remote as remote3 } from "webdriverio";
2719
- import { z as z16 } from "zod";
2720
- var attachBrowserToolDefinition = {
2721
- name: "attach_browser",
2722
- description: `Attach to a Chrome instance already running with --remote-debugging-port.
2723
-
2724
- Use launch_chrome() first to prepare and launch Chrome with remote debugging enabled.`,
3115
+ // src/tools/session.tool.ts
3116
+ var platformEnum = z14.enum(["browser", "ios", "android"]);
3117
+ var browserEnum = z14.enum(["chrome", "firefox", "edge", "safari"]);
3118
+ var automationEnum = z14.enum(["XCUITest", "UiAutomator2"]);
3119
+ var startSessionToolDefinition = {
3120
+ name: "start_session",
3121
+ description: "Starts a browser or mobile app session. For local browser, use browser platform. For mobile apps, use ios or android platform. Use attach mode to connect to an existing Chrome instance.",
3122
+ inputSchema: {
3123
+ platform: platformEnum.describe("Session platform type"),
3124
+ browser: browserEnum.optional().describe("Browser to launch (required for browser platform)"),
3125
+ headless: coerceBoolean.optional().default(true).describe("Run browser in headless mode (default: true)"),
3126
+ windowWidth: z14.number().min(400).max(3840).optional().default(1920).describe("Browser window width"),
3127
+ windowHeight: z14.number().min(400).max(2160).optional().default(1080).describe("Browser window height"),
3128
+ deviceName: z14.string().optional().describe("Mobile device/emulator/simulator name (required for ios/android)"),
3129
+ platformVersion: z14.string().optional().describe('OS version (e.g., "17.0", "14")'),
3130
+ appPath: z14.string().optional().describe("Path to app file (.app/.apk/.ipa)"),
3131
+ automationName: automationEnum.optional().describe("Automation driver"),
3132
+ autoGrantPermissions: coerceBoolean.optional().describe("Auto-grant app permissions (default: true)"),
3133
+ autoAcceptAlerts: coerceBoolean.optional().describe("Auto-accept alerts (default: true)"),
3134
+ autoDismissAlerts: coerceBoolean.optional().describe("Auto-dismiss alerts (default: false)"),
3135
+ appWaitActivity: z14.string().optional().describe("Activity to wait for on Android launch"),
3136
+ udid: z14.string().optional().describe("Unique Device Identifier for iOS real device"),
3137
+ noReset: coerceBoolean.optional().describe("Preserve app data between sessions"),
3138
+ fullReset: coerceBoolean.optional().describe("Uninstall app before/after session"),
3139
+ newCommandTimeout: z14.number().min(0).optional().default(300).describe("Appium command timeout in seconds"),
3140
+ attach: coerceBoolean.optional().default(false).describe("Attach to existing Chrome instead of launching"),
3141
+ port: z14.number().optional().default(9222).describe("Chrome remote debugging port (for attach mode)"),
3142
+ host: z14.string().optional().default("localhost").describe("Chrome host (for attach mode)"),
3143
+ appiumHost: z14.string().optional().describe("Appium server hostname"),
3144
+ appiumPort: z14.number().optional().describe("Appium server port"),
3145
+ appiumPath: z14.string().optional().describe("Appium server path"),
3146
+ navigationUrl: z14.string().optional().describe("URL to navigate to after starting"),
3147
+ capabilities: z14.record(z14.string(), z14.unknown()).optional().describe("Additional capabilities to merge")
3148
+ }
3149
+ };
3150
+ var closeSessionToolDefinition = {
3151
+ name: "close_session",
3152
+ description: "Closes or detaches from the current browser or app session",
2725
3153
  inputSchema: {
2726
- port: z16.number().default(9222).describe("Chrome remote debugging port (default: 9222)"),
2727
- host: z16.string().default("localhost").describe("Host where Chrome is running (default: localhost)"),
2728
- navigationUrl: z16.string().optional().describe("URL to navigate to immediately after attaching")
3154
+ detach: coerceBoolean.optional().describe("If true, disconnect without terminating (preserves app state). Default: false")
2729
3155
  }
2730
3156
  };
2731
3157
  async function closeStaleMappers(host, port) {
@@ -2763,7 +3189,7 @@ async function restoreAndSwitchToActiveTab(browser, activeTabUrl, allTabUrls) {
2763
3189
  }
2764
3190
  }
2765
3191
  }
2766
- async function waitForCDP(host, port, timeoutMs = 1e4) {
3192
+ async function waitForCDP2(host, port, timeoutMs = 1e4) {
2767
3193
  const deadline = Date.now() + timeoutMs;
2768
3194
  while (Date.now() < deadline) {
2769
3195
  try {
@@ -2775,444 +3201,223 @@ async function waitForCDP(host, port, timeoutMs = 1e4) {
2775
3201
  }
2776
3202
  throw new Error(`Chrome did not expose CDP on ${host}:${port} within ${timeoutMs}ms`);
2777
3203
  }
2778
- var attachBrowserTool = async ({
2779
- port = 9222,
2780
- host = "localhost",
2781
- navigationUrl
2782
- }) => {
3204
+ async function startBrowserSession(args) {
3205
+ const browser = args.browser ?? "chrome";
3206
+ const headless = args.headless ?? true;
3207
+ const windowWidth = args.windowWidth ?? 1920;
3208
+ const windowHeight = args.windowHeight ?? 1080;
3209
+ const navigationUrl = args.navigationUrl;
3210
+ const userCapabilities = args.capabilities ?? {};
3211
+ const browserDisplayNames = {
3212
+ chrome: "Chrome",
3213
+ firefox: "Firefox",
3214
+ edge: "Edge",
3215
+ safari: "Safari"
3216
+ };
3217
+ const headlessSupported = browser !== "safari";
3218
+ const effectiveHeadless = headless && headlessSupported;
3219
+ const mergedCapabilities = localBrowserProvider.buildCapabilities({
3220
+ browser,
3221
+ headless,
3222
+ windowWidth,
3223
+ windowHeight,
3224
+ capabilities: userCapabilities
3225
+ });
3226
+ const wdioBrowser = await remote({ capabilities: mergedCapabilities });
3227
+ const { sessionId } = wdioBrowser;
3228
+ const sessionMetadata = {
3229
+ type: "browser",
3230
+ capabilities: mergedCapabilities,
3231
+ isAttached: false
3232
+ };
3233
+ registerSession(sessionId, wdioBrowser, sessionMetadata, {
3234
+ sessionId,
3235
+ type: "browser",
3236
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
3237
+ capabilities: mergedCapabilities,
3238
+ steps: []
3239
+ });
3240
+ let sizeNote = "";
2783
3241
  try {
2784
- const state2 = getBrowser.__state;
2785
- await waitForCDP(host, port);
2786
- const { activeTabUrl, allTabUrls } = await closeStaleMappers(host, port);
2787
- const browser = await remote3({
2788
- connectionRetryTimeout: 3e4,
2789
- connectionRetryCount: 3,
2790
- capabilities: {
2791
- browserName: "chrome",
2792
- unhandledPromptBehavior: "dismiss",
2793
- webSocketUrl: false,
2794
- "goog:chromeOptions": {
2795
- debuggerAddress: `${host}:${port}`
2796
- }
2797
- }
2798
- });
2799
- const { sessionId } = browser;
2800
- state2.browsers.set(sessionId, browser);
2801
- state2.currentSession = sessionId;
2802
- state2.sessionMetadata.set(sessionId, {
2803
- type: "browser",
2804
- capabilities: browser.capabilities,
2805
- isAttached: true
2806
- });
2807
- state2.sessionHistory.set(sessionId, {
2808
- sessionId,
2809
- type: "browser",
2810
- startedAt: (/* @__PURE__ */ new Date()).toISOString(),
2811
- capabilities: {
2812
- browserName: "chrome",
2813
- "goog:chromeOptions": {
2814
- debuggerAddress: `${host}:${port}`
2815
- }
2816
- },
2817
- steps: []
2818
- });
2819
- if (navigationUrl) {
2820
- await browser.url(navigationUrl);
2821
- } else if (activeTabUrl) {
2822
- await restoreAndSwitchToActiveTab(browser, activeTabUrl, allTabUrls);
2823
- }
2824
- const title = await browser.getTitle();
2825
- const url = await browser.getUrl();
2826
- return {
2827
- content: [{
2828
- type: "text",
2829
- text: `Attached to Chrome on ${host}:${port}
2830
- Session ID: ${sessionId}
2831
- Current page: "${title}" (${url})`
2832
- }]
2833
- };
3242
+ await wdioBrowser.setWindowSize(windowWidth, windowHeight);
2834
3243
  } catch (e) {
2835
- return {
2836
- isError: true,
2837
- content: [{ type: "text", text: `Error attaching to browser: ${e}` }]
2838
- };
2839
- }
2840
- };
2841
-
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";
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 });
3244
+ sizeNote = `
3245
+ Note: Unable to set window size (${windowWidth}x${windowHeight}). ${e}`;
2905
3246
  }
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));
3247
+ if (navigationUrl) {
3248
+ await wdioBrowser.url(navigationUrl);
2926
3249
  }
2927
- throw new Error(`Chrome did not expose CDP on port ${port} within ${timeoutMs}ms`);
3250
+ const modeText = effectiveHeadless ? "headless" : "headed";
3251
+ const urlText = navigationUrl ? ` and navigated to ${navigationUrl}` : "";
3252
+ const headlessNote = headless && !headlessSupported ? "\nNote: Safari does not support headless mode. Started in headed mode." : "";
3253
+ return {
3254
+ content: [{
3255
+ type: "text",
3256
+ text: `${browserDisplayNames[browser]} browser started in ${modeText} mode with sessionId: ${sessionId} (${windowWidth}x${windowHeight})${urlText}${headlessNote}${sizeNote}`
3257
+ }]
3258
+ };
2928
3259
  }
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) {
3260
+ async function startMobileSession(args) {
3261
+ const { platform: platform2, appPath, deviceName, noReset } = args;
3262
+ if (!appPath && noReset !== true) {
2954
3263
  return {
2955
- isError: true,
2956
- content: [{ type: "text", text: `Error launching Chrome: ${e}` }]
3264
+ content: [{
3265
+ type: "text",
3266
+ text: 'Error: Either "appPath" must be provided to install an app, or "noReset: true" must be set to connect to an already-running app.'
3267
+ }]
2957
3268
  };
2958
3269
  }
2959
- };
2960
-
2961
- // src/tools/emulate-device.tool.ts
2962
- import { z as z18 } from "zod";
2963
- var restoreFunctions = /* @__PURE__ */ new Map();
2964
- var emulateDeviceToolDefinition = {
2965
- name: "emulate_device",
2966
- description: `Emulate a mobile or tablet device in the current browser session (sets viewport, DPR, user-agent, touch events).
2967
-
2968
- Requires a BiDi-enabled session: start_browser({ capabilities: { webSocketUrl: true } })
2969
-
2970
- Usage:
2971
- emulate_device() \u2014 list available device presets
2972
- emulate_device({ device: "iPhone 15" }) \u2014 activate emulation
2973
- emulate_device({ device: "reset" }) \u2014 restore desktop defaults`,
2974
- inputSchema: {
2975
- device: z18.string().optional().describe(
2976
- 'Device preset name (e.g. "iPhone 15", "Pixel 7"). Omit to list available presets. Pass "reset" to restore desktop defaults.'
2977
- )
2978
- }
2979
- };
2980
- var emulateDeviceTool = async ({
2981
- device
2982
- }) => {
2983
- try {
2984
- const browser = getBrowser();
2985
- const state2 = getBrowser.__state;
2986
- const sessionId = state2.currentSession;
2987
- const metadata = state2.sessionMetadata.get(sessionId);
2988
- if (metadata?.type === "ios" || metadata?.type === "android") {
2989
- return {
2990
- isError: true,
2991
- content: [{ type: "text", text: "Error: emulate_device is only supported for web browser sessions, not iOS/Android." }]
2992
- };
2993
- }
2994
- if (!browser.isBidi) {
2995
- return {
2996
- isError: true,
2997
- content: [{
2998
- type: "text",
2999
- text: "Error: emulate_device requires a BiDi-enabled session.\nRestart the browser with: start_browser({ capabilities: { webSocketUrl: true } })"
3000
- }]
3001
- };
3002
- }
3003
- if (!device) {
3004
- try {
3005
- await browser.emulate("device", "\0");
3006
- } catch (e) {
3007
- const msg = String(e);
3008
- const match = msg.match(/please use one of the following: (.+)$/);
3009
- if (match) {
3010
- const names = match[1].split(", ").sort();
3011
- return {
3012
- content: [{ type: "text", text: `Available devices (${names.length}):
3013
- ${names.join("\n")}` }]
3014
- };
3015
- }
3016
- return { isError: true, content: [{ type: "text", text: `Error listing devices: ${e}` }] };
3017
- }
3018
- return { content: [{ type: "text", text: "Could not retrieve device list." }] };
3019
- }
3020
- if (device === "reset") {
3021
- const restoreFn = restoreFunctions.get(sessionId);
3022
- if (!restoreFn) {
3023
- return { content: [{ type: "text", text: "No active device emulation to reset." }] };
3270
+ const serverConfig = localAppiumProvider.getConnectionConfig(args);
3271
+ const mergedCapabilities = localAppiumProvider.buildCapabilities(args);
3272
+ const browser = await remote({
3273
+ protocol: serverConfig.protocol,
3274
+ hostname: serverConfig.hostname,
3275
+ port: serverConfig.port,
3276
+ path: serverConfig.path,
3277
+ capabilities: mergedCapabilities
3278
+ });
3279
+ const { sessionId } = browser;
3280
+ const shouldAutoDetach = localAppiumProvider.shouldAutoDetach(args);
3281
+ const sessionType = localAppiumProvider.getSessionType(args);
3282
+ const metadata = {
3283
+ type: sessionType,
3284
+ capabilities: mergedCapabilities,
3285
+ isAttached: shouldAutoDetach
3286
+ };
3287
+ registerSession(sessionId, browser, metadata, {
3288
+ sessionId,
3289
+ type: sessionType,
3290
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
3291
+ capabilities: mergedCapabilities,
3292
+ appiumConfig: { hostname: serverConfig.hostname, port: serverConfig.port, path: serverConfig.path },
3293
+ steps: []
3294
+ });
3295
+ const appInfo = appPath ? `
3296
+ App: ${appPath}` : "\nApp: (connected to running app)";
3297
+ const detachNote = shouldAutoDetach ? "\n\n(Auto-detach enabled: session will be preserved on close. Use close_session({ detach: false }) to force terminate.)" : "";
3298
+ return {
3299
+ content: [
3300
+ {
3301
+ type: "text",
3302
+ text: `${platform2} app session started with sessionId: ${sessionId}
3303
+ Device: ${deviceName}${appInfo}
3304
+ Appium Server: ${serverConfig.hostname}:${serverConfig.port}${serverConfig.path}${detachNote}`
3024
3305
  }
3025
- await restoreFn();
3026
- restoreFunctions.delete(sessionId);
3027
- return { content: [{ type: "text", text: "Device emulation reset to desktop defaults." }] };
3306
+ ]
3307
+ };
3308
+ }
3309
+ async function attachBrowserSession(args) {
3310
+ const port = args.port ?? 9222;
3311
+ const host = args.host ?? "localhost";
3312
+ const navigationUrl = args.navigationUrl;
3313
+ await waitForCDP2(host, port);
3314
+ const { activeTabUrl, allTabUrls } = await closeStaleMappers(host, port);
3315
+ const capabilities = {
3316
+ browserName: "chrome",
3317
+ unhandledPromptBehavior: "dismiss",
3318
+ webSocketUrl: false,
3319
+ "goog:chromeOptions": {
3320
+ debuggerAddress: `${host}:${port}`
3028
3321
  }
3029
- try {
3030
- const restoreFn = await browser.emulate("device", device);
3031
- restoreFunctions.set(sessionId, restoreFn);
3032
- return {
3033
- content: [{ type: "text", text: `Emulating "${device}".` }]
3034
- };
3035
- } catch (e) {
3036
- const msg = String(e);
3037
- if (msg.includes("Unknown device name")) {
3038
- return {
3039
- content: [{
3040
- type: "text",
3041
- text: `Error: Unknown device "${device}". Call emulate_device() with no arguments to list valid names.`
3042
- }]
3043
- };
3322
+ };
3323
+ const browser = await remote({
3324
+ connectionRetryTimeout: 3e4,
3325
+ connectionRetryCount: 3,
3326
+ capabilities
3327
+ });
3328
+ const { sessionId } = browser;
3329
+ const sessionMetadata = {
3330
+ type: "browser",
3331
+ capabilities,
3332
+ isAttached: true
3333
+ };
3334
+ registerSession(sessionId, browser, sessionMetadata, {
3335
+ sessionId,
3336
+ type: "browser",
3337
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
3338
+ capabilities,
3339
+ steps: []
3340
+ });
3341
+ if (navigationUrl) {
3342
+ await browser.url(navigationUrl);
3343
+ } else if (activeTabUrl) {
3344
+ await restoreAndSwitchToActiveTab(browser, activeTabUrl, allTabUrls);
3345
+ }
3346
+ const title = await browser.getTitle();
3347
+ const url = await browser.getUrl();
3348
+ return {
3349
+ content: [{
3350
+ type: "text",
3351
+ text: `Attached to Chrome on ${host}:${port}
3352
+ Session ID: ${sessionId}
3353
+ Current page: "${title}" (${url})`
3354
+ }]
3355
+ };
3356
+ }
3357
+ var startSessionTool = async (args) => {
3358
+ try {
3359
+ if (args.platform === "browser") {
3360
+ if (args.attach) {
3361
+ return await attachBrowserSession(args);
3044
3362
  }
3045
- return { isError: true, content: [{ type: "text", text: `Error: ${e}` }] };
3363
+ return await startBrowserSession(args);
3046
3364
  }
3365
+ return await startMobileSession(args);
3047
3366
  } catch (e) {
3367
+ return { isError: true, content: [{ type: "text", text: `Error starting session: ${e}` }] };
3368
+ }
3369
+ };
3370
+ var closeSessionTool = async (args = {}) => {
3371
+ try {
3372
+ getBrowser();
3373
+ const state2 = getState();
3374
+ const sessionId = state2.currentSession;
3375
+ const metadata = state2.sessionMetadata.get(sessionId);
3376
+ const isAttached = !!metadata?.isAttached;
3377
+ const detach = args.detach ?? false;
3378
+ const force = !detach && isAttached;
3379
+ const effectiveDetach = detach || isAttached;
3380
+ await closeSession(sessionId, detach, isAttached, force);
3381
+ const action = effectiveDetach && !force ? "detached from" : "closed";
3382
+ const note = detach && !isAttached ? "\nNote: Session will remain active on Appium server." : "";
3048
3383
  return {
3049
- isError: true,
3050
- content: [{ type: "text", text: `Error: ${e}` }]
3384
+ content: [{ type: "text", text: `Session ${sessionId} ${action}${note}` }]
3051
3385
  };
3386
+ } catch (e) {
3387
+ return { isError: true, content: [{ type: "text", text: `Error closing session: ${e}` }] };
3052
3388
  }
3053
3389
  };
3054
3390
 
3055
- // src/recording/step-recorder.ts
3056
- function getState2() {
3057
- return getBrowser.__state;
3058
- }
3059
- function appendStep(toolName, params, status, durationMs, error) {
3060
- const state2 = getState2();
3061
- const sessionId = state2.currentSession;
3062
- if (!sessionId) return;
3063
- const history = state2.sessionHistory.get(sessionId);
3064
- if (!history) return;
3065
- const step = {
3066
- index: history.steps.length + 1,
3067
- tool: toolName,
3068
- params,
3069
- status,
3070
- durationMs,
3071
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
3072
- ...error !== void 0 && { error }
3073
- };
3074
- history.steps.push(step);
3075
- }
3076
- function getSessionHistory() {
3077
- return getState2().sessionHistory;
3078
- }
3079
- function extractErrorText(result) {
3080
- const textContent = result.content.find((c) => c.type === "text");
3081
- return textContent ? textContent.text : "Unknown error";
3082
- }
3083
- function withRecording(toolName, callback) {
3084
- return async (params, extra) => {
3085
- const start = Date.now();
3086
- const result = await callback(params, extra);
3087
- const isError = result.isError === true;
3088
- appendStep(
3089
- toolName,
3090
- params,
3091
- isError ? "error" : "ok",
3092
- Date.now() - start,
3093
- isError ? extractErrorText(result) : void 0
3094
- );
3095
- return result;
3096
- };
3097
- }
3098
-
3099
- // src/recording/code-generator.ts
3100
- function escapeStr(value) {
3101
- return String(value).replace(/\\/g, "\\\\").replace(/'/g, "\\'");
3102
- }
3103
- function formatParams(params) {
3104
- return Object.entries(params).map(([k, v]) => `${k}="${v}"`).join(" ");
3105
- }
3106
- function indentJson(value) {
3107
- return JSON.stringify(value, null, 2).split("\n").map((line, i) => i > 0 ? ` ${line}` : line).join("\n");
3108
- }
3109
- function generateStep(step, history) {
3110
- if (step.tool === "__session_transition__") {
3111
- const newId = step.params.newSessionId ?? "unknown";
3112
- return `// --- new session: ${newId} started at ${step.timestamp} ---`;
3113
- }
3114
- if (step.status === "error") {
3115
- return `// [error] ${step.tool}: ${formatParams(step.params)} \u2014 ${step.error ?? "unknown error"}`;
3391
+ // src/tools/tabs.tool.ts
3392
+ init_state();
3393
+ import { z as z15 } from "zod";
3394
+ var switchTabToolDefinition = {
3395
+ name: "switch_tab",
3396
+ description: "switches to a browser tab by handle or index",
3397
+ inputSchema: {
3398
+ handle: z15.string().optional().describe("Window handle to switch to"),
3399
+ index: z15.number().int().min(0).optional().describe("0-based tab index to switch to")
3116
3400
  }
3117
- const p = step.params;
3118
- switch (step.tool) {
3119
- case "start_browser": {
3120
- const nav = p.navigationUrl ? `
3121
- await browser.url('${escapeStr(p.navigationUrl)}');` : "";
3122
- return `const browser = await remote({
3123
- capabilities: ${indentJson(history.capabilities)}
3124
- });${nav}`;
3125
- }
3126
- case "start_app_session": {
3127
- const config = {
3128
- protocol: "http",
3129
- hostname: history.appiumConfig?.hostname ?? "localhost",
3130
- port: history.appiumConfig?.port ?? 4723,
3131
- path: history.appiumConfig?.path ?? "/",
3132
- capabilities: history.capabilities
3133
- };
3134
- return `const browser = await remote(${indentJson(config)});`;
3135
- }
3136
- case "attach_browser": {
3137
- const nav = p.navigationUrl ? `
3138
- await browser.url('${escapeStr(p.navigationUrl)}');` : "";
3139
- return `const browser = await remote({
3140
- capabilities: ${indentJson(history.capabilities)}
3141
- });${nav}`;
3142
- }
3143
- case "navigate":
3144
- return `await browser.url('${escapeStr(p.url)}');`;
3145
- case "click_element":
3146
- return `await browser.$('${escapeStr(p.selector)}').click();`;
3147
- case "set_value":
3148
- return `await browser.$('${escapeStr(p.selector)}').setValue('${escapeStr(p.value)}');`;
3149
- case "scroll": {
3150
- const scrollAmount = p.direction === "down" ? p.pixels : -p.pixels;
3151
- return `await browser.execute(() => window.scrollBy(0, ${scrollAmount}));`;
3152
- }
3153
- case "tap_element":
3154
- if (p.selector !== void 0) {
3155
- return `await browser.$('${escapeStr(p.selector)}').click();`;
3156
- }
3157
- return `await browser.tap({ x: ${p.x}, y: ${p.y} });`;
3158
- case "swipe":
3159
- return `await browser.execute('mobile: swipe', { direction: '${escapeStr(p.direction)}' });`;
3160
- case "drag_and_drop":
3161
- if (p.targetSelector !== void 0) {
3162
- return `await browser.$('${escapeStr(p.sourceSelector)}').dragAndDrop(browser.$('${escapeStr(p.targetSelector)}'));`;
3401
+ };
3402
+ var switchTabTool = async ({ handle, index }) => {
3403
+ try {
3404
+ const browser = getBrowser();
3405
+ if (handle) {
3406
+ await browser.switchToWindow(handle);
3407
+ return { content: [{ type: "text", text: `Switched to tab: ${handle}` }] };
3408
+ } else if (index !== void 0) {
3409
+ const handles = await browser.getWindowHandles();
3410
+ if (index >= handles.length) {
3411
+ return { isError: true, content: [{ type: "text", text: `Error: index ${index} out of range (${handles.length} tabs)` }] };
3163
3412
  }
3164
- return `await browser.$('${escapeStr(p.sourceSelector)}').dragAndDrop({ x: ${p.x}, y: ${p.y} });`;
3165
- default:
3166
- return `// [unknown tool] ${step.tool}`;
3413
+ await browser.switchToWindow(handles[index]);
3414
+ return { content: [{ type: "text", text: `Switched to tab ${index}: ${handles[index]}` }] };
3415
+ }
3416
+ return { isError: true, content: [{ type: "text", text: "Error: Must provide either handle or index" }] };
3417
+ } catch (e) {
3418
+ return { isError: true, content: [{ type: "text", text: `Error switching tab: ${e}` }] };
3167
3419
  }
3168
- }
3169
- function generateCode(history) {
3170
- const steps = history.steps.map((step) => generateStep(step, history)).join("\n");
3171
- return `import { remote } from 'webdriverio';
3172
-
3173
- ${steps}
3174
-
3175
- await browser.deleteSession();`;
3176
- }
3177
-
3178
- // src/recording/resources.ts
3179
- function getCurrentSessionId() {
3180
- return getBrowser.__state?.currentSession ?? null;
3181
- }
3182
- function buildSessionsIndex() {
3183
- const histories = getSessionHistory();
3184
- const currentId = getCurrentSessionId();
3185
- const sessions = Array.from(histories.values()).map((h) => ({
3186
- sessionId: h.sessionId,
3187
- type: h.type,
3188
- startedAt: h.startedAt,
3189
- ...h.endedAt ? { endedAt: h.endedAt } : {},
3190
- stepCount: h.steps.length,
3191
- isCurrent: h.sessionId === currentId
3192
- }));
3193
- return JSON.stringify({ sessions });
3194
- }
3195
- function buildCurrentSessionSteps() {
3196
- const currentId = getCurrentSessionId();
3197
- if (!currentId) return null;
3198
- return buildSessionStepsById(currentId);
3199
- }
3200
- function buildSessionStepsById(sessionId) {
3201
- const history = getSessionHistory().get(sessionId);
3202
- if (!history) return null;
3203
- return buildSessionPayload(history);
3204
- }
3205
- function buildSessionPayload(history) {
3206
- const stepsJson = JSON.stringify({
3207
- sessionId: history.sessionId,
3208
- type: history.type,
3209
- startedAt: history.startedAt,
3210
- ...history.endedAt ? { endedAt: history.endedAt } : {},
3211
- stepCount: history.steps.length,
3212
- steps: history.steps
3213
- });
3214
- return { stepsJson, generatedJs: generateCode(history) };
3215
- }
3420
+ };
3216
3421
 
3217
3422
  // src/server.ts
3218
3423
  console.log = (...args) => console.error("[LOG]", ...args);
@@ -3236,102 +3441,57 @@ var registerTool = (definition, callback) => server.registerTool(definition.name
3236
3441
  description: definition.description,
3237
3442
  inputSchema: definition.inputSchema
3238
3443
  }, callback);
3239
- registerTool(startBrowserToolDefinition, withRecording("start_browser", startBrowserTool));
3240
- registerTool(startAppToolDefinition, withRecording("start_app_session", startAppTool));
3444
+ var registerResource = (definition) => {
3445
+ if ("uri" in definition) {
3446
+ server.registerResource(
3447
+ definition.name,
3448
+ definition.uri,
3449
+ { description: definition.description },
3450
+ definition.handler
3451
+ );
3452
+ } else {
3453
+ server.registerResource(
3454
+ definition.name,
3455
+ definition.template,
3456
+ { description: definition.description },
3457
+ definition.handler
3458
+ );
3459
+ }
3460
+ };
3461
+ registerTool(startSessionToolDefinition, withRecording("start_session", startSessionTool));
3241
3462
  registerTool(closeSessionToolDefinition, closeSessionTool);
3242
3463
  registerTool(launchChromeToolDefinition, withRecording("launch_chrome", launchChromeTool));
3243
- registerTool(attachBrowserToolDefinition, withRecording("attach_browser", attachBrowserTool));
3244
3464
  registerTool(emulateDeviceToolDefinition, emulateDeviceTool);
3245
3465
  registerTool(navigateToolDefinition, withRecording("navigate", navigateTool));
3246
- registerTool(getVisibleElementsToolDefinition, getVisibleElementsTool);
3247
- registerTool(getAccessibilityToolDefinition, getAccessibilityTreeTool);
3466
+ registerTool(switchTabToolDefinition, switchTabTool);
3248
3467
  registerTool(scrollToolDefinition, withRecording("scroll", scrollTool));
3249
3468
  registerTool(clickToolDefinition, withRecording("click_element", clickTool));
3250
3469
  registerTool(setValueToolDefinition, withRecording("set_value", setValueTool));
3251
- registerTool(takeScreenshotToolDefinition, takeScreenshotTool);
3252
- registerTool(getCookiesToolDefinition, getCookiesTool);
3253
3470
  registerTool(setCookieToolDefinition, setCookieTool);
3254
3471
  registerTool(deleteCookiesToolDefinition, deleteCookiesTool);
3255
3472
  registerTool(tapElementToolDefinition, withRecording("tap_element", tapElementTool));
3256
3473
  registerTool(swipeToolDefinition, withRecording("swipe", swipeTool));
3257
3474
  registerTool(dragAndDropToolDefinition, withRecording("drag_and_drop", dragAndDropTool));
3258
- registerTool(getAppStateToolDefinition, getAppStateTool);
3259
- registerTool(getContextsToolDefinition, getContextsTool);
3260
- registerTool(getCurrentContextToolDefinition, getCurrentContextTool);
3261
3475
  registerTool(switchContextToolDefinition, switchContextTool);
3262
3476
  registerTool(rotateDeviceToolDefinition, rotateDeviceTool);
3263
3477
  registerTool(hideKeyboardToolDefinition, hideKeyboardTool);
3264
- registerTool(getGeolocationToolDefinition, getGeolocationTool);
3265
3478
  registerTool(setGeolocationToolDefinition, setGeolocationTool);
3266
- registerTool(executeScriptToolDefinition, executeScriptTool);
3267
- server.registerResource(
3268
- "sessions",
3269
- "wdio://sessions",
3270
- { description: "JSON index of all browser and app sessions with metadata and step counts" },
3271
- async () => ({
3272
- contents: [{ uri: "wdio://sessions", mimeType: "application/json", text: buildSessionsIndex() }]
3273
- })
3274
- );
3275
- server.registerResource(
3276
- "session-current-steps",
3277
- "wdio://session/current/steps",
3278
- { description: "JSON step log for the currently active session" },
3279
- async () => {
3280
- const payload = buildCurrentSessionSteps();
3281
- return {
3282
- contents: [{
3283
- uri: "wdio://session/current/steps",
3284
- mimeType: "application/json",
3285
- text: payload?.stepsJson ?? '{"error":"No active session"}'
3286
- }]
3287
- };
3288
- }
3289
- );
3290
- server.registerResource(
3291
- "session-current-code",
3292
- "wdio://session/current/code",
3293
- { description: "Generated WebdriverIO JS code for the currently active session" },
3294
- async () => {
3295
- const payload = buildCurrentSessionSteps();
3296
- return {
3297
- contents: [{
3298
- uri: "wdio://session/current/code",
3299
- mimeType: "text/plain",
3300
- text: payload?.generatedJs ?? "// No active session"
3301
- }]
3302
- };
3303
- }
3304
- );
3305
- server.registerResource(
3306
- "session-steps",
3307
- new ResourceTemplate("wdio://session/{sessionId}/steps", { list: void 0 }),
3308
- { description: "JSON step log for a specific session by ID" },
3309
- async (uri, { sessionId }) => {
3310
- const payload = buildSessionStepsById(sessionId);
3311
- return {
3312
- contents: [{
3313
- uri: uri.href,
3314
- mimeType: "application/json",
3315
- text: payload?.stepsJson ?? `{"error":"Session not found: ${sessionId}"}`
3316
- }]
3317
- };
3318
- }
3319
- );
3320
- server.registerResource(
3321
- "session-code",
3322
- new ResourceTemplate("wdio://session/{sessionId}/code", { list: void 0 }),
3323
- { description: "Generated WebdriverIO JS code for a specific session by ID" },
3324
- async (uri, { sessionId }) => {
3325
- const payload = buildSessionStepsById(sessionId);
3326
- return {
3327
- contents: [{
3328
- uri: uri.href,
3329
- mimeType: "text/plain",
3330
- text: payload?.generatedJs ?? `// Session not found: ${sessionId}`
3331
- }]
3332
- };
3333
- }
3334
- );
3479
+ registerTool(executeScriptToolDefinition, withRecording("execute_script", executeScriptTool));
3480
+ registerTool(getElementsToolDefinition, getElementsTool);
3481
+ registerResource(sessionsIndexResource);
3482
+ registerResource(sessionCurrentStepsResource);
3483
+ registerResource(sessionCurrentCodeResource);
3484
+ registerResource(sessionStepsResource);
3485
+ registerResource(sessionCodeResource);
3486
+ registerResource(elementsResource);
3487
+ registerResource(accessibilityResource);
3488
+ registerResource(screenshotResource);
3489
+ registerResource(cookiesResource);
3490
+ registerResource(appStateResource);
3491
+ registerResource(contextsResource);
3492
+ registerResource(contextResource);
3493
+ registerResource(geolocationResource);
3494
+ registerResource(tabsResource);
3335
3495
  async function main() {
3336
3496
  const transport = new StdioServerTransport();
3337
3497
  await server.connect(transport);