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