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