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