@wdio/mcp 2.5.3 → 3.1.0

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