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