@zenrows/mcp 1.1.6 → 2.0.1
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/dist/http.js +17 -0
- package/dist/server.js +3 -0
- package/dist/tools/browser-fetch.d.ts +7 -0
- package/dist/tools/browser-fetch.js +23 -0
- package/dist/tools/browser.d.ts +2 -0
- package/dist/tools/browser.js +961 -0
- package/package.json +1 -1
package/dist/http.js
CHANGED
|
@@ -59,6 +59,23 @@ app.post("/register", async (c) => {
|
|
|
59
59
|
...body,
|
|
60
60
|
}, 201);
|
|
61
61
|
});
|
|
62
|
+
// Proxy browser tool calls to internal service.
|
|
63
|
+
// npx clients default ZENROWS_BROWSER_URL to https://mcp.zenrows.com, so their
|
|
64
|
+
// /browser/* calls are forwarded here and proxied to the internal service.
|
|
65
|
+
const BROWSER_URL = process.env.ZENROWS_BROWSER_URL;
|
|
66
|
+
app.all("/browser/*", async (c) => {
|
|
67
|
+
if (!BROWSER_URL) {
|
|
68
|
+
return c.json({ error: "Browser service not available." }, 503);
|
|
69
|
+
}
|
|
70
|
+
const target = `${BROWSER_URL.replace(/\/$/, "")}${c.req.path}`;
|
|
71
|
+
const req = new Request(target, {
|
|
72
|
+
method: c.req.method,
|
|
73
|
+
headers: c.req.raw.headers,
|
|
74
|
+
body: c.req.raw.body,
|
|
75
|
+
});
|
|
76
|
+
const res = await fetch(req);
|
|
77
|
+
return new Response(res.body, { status: res.status, headers: res.headers });
|
|
78
|
+
});
|
|
62
79
|
app.all("/mcp", async (c) => {
|
|
63
80
|
const apiKey = extractApiKey(c.req.raw);
|
|
64
81
|
if (!apiKey) {
|
package/dist/server.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createRequire } from "module";
|
|
2
2
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
3
|
import { z } from "zod";
|
|
4
|
+
import { registerBrowserTools } from "./tools/browser.js";
|
|
4
5
|
const require = createRequire(import.meta.url);
|
|
5
6
|
const pkg = require("../package.json");
|
|
6
7
|
const ZENROWS_API_URL = "https://api.zenrows.com/v1/";
|
|
@@ -259,5 +260,7 @@ Examples:
|
|
|
259
260
|
},
|
|
260
261
|
],
|
|
261
262
|
}));
|
|
263
|
+
const BROWSER_URL = process.env.ZENROWS_BROWSER_URL ?? "https://mcp.zenrows.com";
|
|
264
|
+
registerBrowserTools(server, apiKey, BROWSER_URL);
|
|
262
265
|
return server;
|
|
263
266
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export interface BrowserFetchResult {
|
|
2
|
+
ok: boolean;
|
|
3
|
+
status: number;
|
|
4
|
+
data: unknown;
|
|
5
|
+
}
|
|
6
|
+
export declare function browserFetch(method: string, path: string, apiKey: string, browserUrl: string, body?: unknown): Promise<BrowserFetchResult>;
|
|
7
|
+
export declare function browserError(result: BrowserFetchResult): string;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const REQUEST_TIMEOUT_MS = 35_000;
|
|
2
|
+
export async function browserFetch(method, path, apiKey, browserUrl, body) {
|
|
3
|
+
const url = `${browserUrl.replace(/\/$/, "")}${path}`;
|
|
4
|
+
const init = {
|
|
5
|
+
method,
|
|
6
|
+
headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
|
|
7
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
8
|
+
};
|
|
9
|
+
if (body !== undefined) {
|
|
10
|
+
init.body = JSON.stringify(body);
|
|
11
|
+
}
|
|
12
|
+
const response = await fetch(url, init);
|
|
13
|
+
let data;
|
|
14
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
15
|
+
data = contentType.includes("application/json") ? await response.json() : await response.text();
|
|
16
|
+
return { ok: response.ok, status: response.status, data };
|
|
17
|
+
}
|
|
18
|
+
export function browserError(result) {
|
|
19
|
+
if (typeof result.data === "object" && result.data !== null && "error" in result.data) {
|
|
20
|
+
return String(result.data.error);
|
|
21
|
+
}
|
|
22
|
+
return `HTTP ${result.status}`;
|
|
23
|
+
}
|
|
@@ -0,0 +1,961 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { browserFetch, browserError } from "./browser-fetch.js";
|
|
3
|
+
function ok() {
|
|
4
|
+
return { content: [{ type: "text", text: JSON.stringify({ ok: true }) }] };
|
|
5
|
+
}
|
|
6
|
+
function err(msg) {
|
|
7
|
+
return { content: [{ type: "text", text: msg }], isError: true };
|
|
8
|
+
}
|
|
9
|
+
function json(data) {
|
|
10
|
+
return { content: [{ type: "text", text: JSON.stringify(data) }] };
|
|
11
|
+
}
|
|
12
|
+
const sessionId = z.string().describe("Session ID returned by browser_navigate");
|
|
13
|
+
export function registerBrowserTools(server, apiKey, browserUrl) {
|
|
14
|
+
const fetch = (method, path, body) => browserFetch(method, path, apiKey, browserUrl, body);
|
|
15
|
+
// ─── Session ─────────────────────────────────────────────────────────────
|
|
16
|
+
server.registerTool("browser_navigate", {
|
|
17
|
+
annotations: { title: "Open Browser & Navigate", readOnlyHint: false, destructiveHint: false },
|
|
18
|
+
description: `Open a ZenRows Scraping Browser session and navigate to a URL.
|
|
19
|
+
|
|
20
|
+
This is the entry point for all browser automation. It creates a new session backed by
|
|
21
|
+
ZenRows' anti-bot infrastructure and navigates to the given URL in one step.
|
|
22
|
+
|
|
23
|
+
Returns a session_id that must be passed to every subsequent browser_* tool call.
|
|
24
|
+
Always call browser_close when done to close the session and free resources.
|
|
25
|
+
|
|
26
|
+
When to use options:
|
|
27
|
+
- proxy_country: geo-restricted content (ISO 3166-1 alpha-2, e.g. "US", "DE")
|
|
28
|
+
- proxy_region: world region for geo-targeted proxy (eu, na, ap, sa, af, me)`,
|
|
29
|
+
inputSchema: {
|
|
30
|
+
url: z.string().url().describe("The URL to navigate to"),
|
|
31
|
+
proxy_country: z
|
|
32
|
+
.string()
|
|
33
|
+
.optional()
|
|
34
|
+
.describe("ISO 3166-1 alpha-2 country code for geo-targeted proxy (e.g. 'US', 'GB', 'DE')"),
|
|
35
|
+
proxy_region: z.string().optional().describe("World region code for geo-targeted proxy (eu=Europe, na=North America, ap=Asia Pacific, sa=South America, af=Africa, me=Middle East)"),
|
|
36
|
+
},
|
|
37
|
+
}, async (params) => {
|
|
38
|
+
let result;
|
|
39
|
+
try {
|
|
40
|
+
result = await fetch("POST", "/browser/sessions", {
|
|
41
|
+
proxy_country: params.proxy_country,
|
|
42
|
+
proxy_region: params.proxy_region,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
catch (e) {
|
|
46
|
+
return err(`Network error: ${e instanceof Error ? e.message : String(e)}`);
|
|
47
|
+
}
|
|
48
|
+
if (!result.ok)
|
|
49
|
+
return err(`Failed to create session: ${browserError(result)}`);
|
|
50
|
+
const session = result.data;
|
|
51
|
+
let navResult;
|
|
52
|
+
try {
|
|
53
|
+
navResult = await fetch("POST", `/browser/sessions/${session.session_id}/navigate`, { url: params.url });
|
|
54
|
+
}
|
|
55
|
+
catch (e) {
|
|
56
|
+
// Best-effort cleanup — session was created but navigation failed, free the slot.
|
|
57
|
+
void fetch("DELETE", `/browser/sessions/${session.session_id}`).catch(() => undefined);
|
|
58
|
+
return err(`Network error: ${e instanceof Error ? e.message : String(e)}`);
|
|
59
|
+
}
|
|
60
|
+
if (!navResult.ok) {
|
|
61
|
+
void fetch("DELETE", `/browser/sessions/${session.session_id}`).catch(() => undefined);
|
|
62
|
+
return err(`Navigation failed: ${browserError(navResult)}`);
|
|
63
|
+
}
|
|
64
|
+
const nav = navResult.data;
|
|
65
|
+
return json({ session_id: session.session_id, url: nav.url, title: nav.title, expires_at: session.expires_at });
|
|
66
|
+
});
|
|
67
|
+
server.registerTool("browser_close", {
|
|
68
|
+
annotations: { title: "Close Browser Session", readOnlyHint: false, destructiveHint: false },
|
|
69
|
+
description: "Close a browser session and free all associated resources. Always call this when done to release the browser slot.",
|
|
70
|
+
inputSchema: { session_id: sessionId },
|
|
71
|
+
}, async ({ session_id }) => {
|
|
72
|
+
try {
|
|
73
|
+
const result = await fetch("DELETE", `/browser/sessions/${session_id}`);
|
|
74
|
+
if (!result.ok)
|
|
75
|
+
return err(`Failed to close session: ${browserError(result)}`);
|
|
76
|
+
}
|
|
77
|
+
catch (e) {
|
|
78
|
+
return err(`Network error: ${e instanceof Error ? e.message : String(e)}`);
|
|
79
|
+
}
|
|
80
|
+
return ok();
|
|
81
|
+
});
|
|
82
|
+
// ─── Navigation ───────────────────────────────────────────────────────────
|
|
83
|
+
server.registerTool("browser_go_back", {
|
|
84
|
+
annotations: { title: "Go Back", readOnlyHint: false, destructiveHint: false },
|
|
85
|
+
description: "Navigate back to the previous page in the browser history.",
|
|
86
|
+
inputSchema: { session_id: sessionId },
|
|
87
|
+
}, async ({ session_id }) => {
|
|
88
|
+
try {
|
|
89
|
+
const result = await fetch("POST", `/browser/sessions/${session_id}/go_back`);
|
|
90
|
+
if (!result.ok)
|
|
91
|
+
return err(`Go back failed: ${browserError(result)}`);
|
|
92
|
+
return json(result.data);
|
|
93
|
+
}
|
|
94
|
+
catch (e) {
|
|
95
|
+
return err(`Network error: ${e instanceof Error ? e.message : String(e)}`);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
server.registerTool("browser_go_forward", {
|
|
99
|
+
annotations: { title: "Go Forward", readOnlyHint: false, destructiveHint: false },
|
|
100
|
+
description: "Navigate forward to the next page in the browser history.",
|
|
101
|
+
inputSchema: { session_id: sessionId },
|
|
102
|
+
}, async ({ session_id }) => {
|
|
103
|
+
try {
|
|
104
|
+
const result = await fetch("POST", `/browser/sessions/${session_id}/go_forward`);
|
|
105
|
+
if (!result.ok)
|
|
106
|
+
return err(`Go forward failed: ${browserError(result)}`);
|
|
107
|
+
return json(result.data);
|
|
108
|
+
}
|
|
109
|
+
catch (e) {
|
|
110
|
+
return err(`Network error: ${e instanceof Error ? e.message : String(e)}`);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
server.registerTool("browser_reload", {
|
|
114
|
+
annotations: { title: "Reload Page", readOnlyHint: false, destructiveHint: false },
|
|
115
|
+
description: "Reload the current page.",
|
|
116
|
+
inputSchema: { session_id: sessionId },
|
|
117
|
+
}, async ({ session_id }) => {
|
|
118
|
+
try {
|
|
119
|
+
const result = await fetch("POST", `/browser/sessions/${session_id}/reload`);
|
|
120
|
+
if (!result.ok)
|
|
121
|
+
return err(`Reload failed: ${browserError(result)}`);
|
|
122
|
+
return json(result.data);
|
|
123
|
+
}
|
|
124
|
+
catch (e) {
|
|
125
|
+
return err(`Network error: ${e instanceof Error ? e.message : String(e)}`);
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
// ─── Interaction ──────────────────────────────────────────────────────────
|
|
129
|
+
server.registerTool("browser_click", {
|
|
130
|
+
annotations: { title: "Click Element", readOnlyHint: false, destructiveHint: false },
|
|
131
|
+
description: "Click an element on the page using a CSS selector. Use browser_get_accessibility_tree first to find the right selector.",
|
|
132
|
+
inputSchema: {
|
|
133
|
+
session_id: sessionId,
|
|
134
|
+
selector: z.string().describe("CSS selector of the element to click"),
|
|
135
|
+
},
|
|
136
|
+
}, async ({ session_id, selector }) => {
|
|
137
|
+
try {
|
|
138
|
+
const result = await fetch("POST", `/browser/sessions/${session_id}/click`, { selector });
|
|
139
|
+
if (!result.ok)
|
|
140
|
+
return err(`Click failed: ${browserError(result)}`);
|
|
141
|
+
return ok();
|
|
142
|
+
}
|
|
143
|
+
catch (e) {
|
|
144
|
+
return err(`Network error: ${e instanceof Error ? e.message : String(e)}`);
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
server.registerTool("browser_hover", {
|
|
148
|
+
annotations: { title: "Hover Element", readOnlyHint: false, destructiveHint: false },
|
|
149
|
+
description: "Move the mouse cursor over an element to trigger hover effects.",
|
|
150
|
+
inputSchema: {
|
|
151
|
+
session_id: sessionId,
|
|
152
|
+
selector: z.string().describe("CSS selector of the element to hover over"),
|
|
153
|
+
},
|
|
154
|
+
}, async ({ session_id, selector }) => {
|
|
155
|
+
try {
|
|
156
|
+
const result = await fetch("POST", `/browser/sessions/${session_id}/hover`, { selector });
|
|
157
|
+
if (!result.ok)
|
|
158
|
+
return err(`Hover failed: ${browserError(result)}`);
|
|
159
|
+
return ok();
|
|
160
|
+
}
|
|
161
|
+
catch (e) {
|
|
162
|
+
return err(`Network error: ${e instanceof Error ? e.message : String(e)}`);
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
server.registerTool("browser_type", {
|
|
166
|
+
annotations: { title: "Type Text", readOnlyHint: false, destructiveHint: false },
|
|
167
|
+
description: "Type text into an input element. Appends to existing content unless clear_first is set.",
|
|
168
|
+
inputSchema: {
|
|
169
|
+
session_id: sessionId,
|
|
170
|
+
selector: z.string().describe("CSS selector of the input element"),
|
|
171
|
+
text: z.string().describe("Text to type"),
|
|
172
|
+
clear_first: z.boolean().optional().describe("Clear existing content before typing (default false)"),
|
|
173
|
+
},
|
|
174
|
+
}, async ({ session_id, selector, text, clear_first }) => {
|
|
175
|
+
try {
|
|
176
|
+
const result = await fetch("POST", `/browser/sessions/${session_id}/type`, { selector, text, clear_first });
|
|
177
|
+
if (!result.ok)
|
|
178
|
+
return err(`Type failed: ${browserError(result)}`);
|
|
179
|
+
return ok();
|
|
180
|
+
}
|
|
181
|
+
catch (e) {
|
|
182
|
+
return err(`Network error: ${e instanceof Error ? e.message : String(e)}`);
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
server.registerTool("browser_fill", {
|
|
186
|
+
annotations: { title: "Fill Input", readOnlyHint: false, destructiveHint: false },
|
|
187
|
+
description: "Clear an input field and set its value. Preferred over browser_type when replacing the full value.",
|
|
188
|
+
inputSchema: {
|
|
189
|
+
session_id: sessionId,
|
|
190
|
+
selector: z.string().describe("CSS selector of the input element"),
|
|
191
|
+
value: z.string().describe("Value to fill in"),
|
|
192
|
+
},
|
|
193
|
+
}, async ({ session_id, selector, value }) => {
|
|
194
|
+
try {
|
|
195
|
+
const result = await fetch("POST", `/browser/sessions/${session_id}/fill`, { selector, value });
|
|
196
|
+
if (!result.ok)
|
|
197
|
+
return err(`Fill failed: ${browserError(result)}`);
|
|
198
|
+
return ok();
|
|
199
|
+
}
|
|
200
|
+
catch (e) {
|
|
201
|
+
return err(`Network error: ${e instanceof Error ? e.message : String(e)}`);
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
server.registerTool("browser_select_option", {
|
|
205
|
+
annotations: { title: "Select Option", readOnlyHint: false, destructiveHint: false },
|
|
206
|
+
description: "Select an option from a <select> dropdown element.",
|
|
207
|
+
inputSchema: {
|
|
208
|
+
session_id: sessionId,
|
|
209
|
+
selector: z.string().describe("CSS selector of the <select> element"),
|
|
210
|
+
value: z.string().describe("Value of the option to select"),
|
|
211
|
+
},
|
|
212
|
+
}, async ({ session_id, selector, value }) => {
|
|
213
|
+
try {
|
|
214
|
+
const result = await fetch("POST", `/browser/sessions/${session_id}/select`, { selector, value });
|
|
215
|
+
if (!result.ok)
|
|
216
|
+
return err(`Select failed: ${browserError(result)}`);
|
|
217
|
+
return ok();
|
|
218
|
+
}
|
|
219
|
+
catch (e) {
|
|
220
|
+
return err(`Network error: ${e instanceof Error ? e.message : String(e)}`);
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
server.registerTool("browser_check", {
|
|
224
|
+
annotations: { title: "Check Checkbox", readOnlyHint: false, destructiveHint: false },
|
|
225
|
+
description: "Check a checkbox or radio button.",
|
|
226
|
+
inputSchema: {
|
|
227
|
+
session_id: sessionId,
|
|
228
|
+
selector: z.string().describe("CSS selector of the checkbox or radio element"),
|
|
229
|
+
},
|
|
230
|
+
}, async ({ session_id, selector }) => {
|
|
231
|
+
try {
|
|
232
|
+
const result = await fetch("POST", `/browser/sessions/${session_id}/check`, { selector });
|
|
233
|
+
if (!result.ok)
|
|
234
|
+
return err(`Check failed: ${browserError(result)}`);
|
|
235
|
+
return ok();
|
|
236
|
+
}
|
|
237
|
+
catch (e) {
|
|
238
|
+
return err(`Network error: ${e instanceof Error ? e.message : String(e)}`);
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
server.registerTool("browser_uncheck", {
|
|
242
|
+
annotations: { title: "Uncheck Checkbox", readOnlyHint: false, destructiveHint: false },
|
|
243
|
+
description: "Uncheck a checkbox.",
|
|
244
|
+
inputSchema: {
|
|
245
|
+
session_id: sessionId,
|
|
246
|
+
selector: z.string().describe("CSS selector of the checkbox element"),
|
|
247
|
+
},
|
|
248
|
+
}, async ({ session_id, selector }) => {
|
|
249
|
+
try {
|
|
250
|
+
const result = await fetch("POST", `/browser/sessions/${session_id}/uncheck`, { selector });
|
|
251
|
+
if (!result.ok)
|
|
252
|
+
return err(`Uncheck failed: ${browserError(result)}`);
|
|
253
|
+
return ok();
|
|
254
|
+
}
|
|
255
|
+
catch (e) {
|
|
256
|
+
return err(`Network error: ${e instanceof Error ? e.message : String(e)}`);
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
server.registerTool("browser_focus", {
|
|
260
|
+
annotations: { title: "Focus Element", readOnlyHint: false, destructiveHint: false },
|
|
261
|
+
description: "Move focus to an element, triggering focus-dependent UI changes.",
|
|
262
|
+
inputSchema: {
|
|
263
|
+
session_id: sessionId,
|
|
264
|
+
selector: z.string().describe("CSS selector of the element to focus"),
|
|
265
|
+
},
|
|
266
|
+
}, async ({ session_id, selector }) => {
|
|
267
|
+
try {
|
|
268
|
+
const result = await fetch("POST", `/browser/sessions/${session_id}/focus`, { selector });
|
|
269
|
+
if (!result.ok)
|
|
270
|
+
return err(`Focus failed: ${browserError(result)}`);
|
|
271
|
+
return ok();
|
|
272
|
+
}
|
|
273
|
+
catch (e) {
|
|
274
|
+
return err(`Network error: ${e instanceof Error ? e.message : String(e)}`);
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
server.registerTool("browser_press_key", {
|
|
278
|
+
annotations: { title: "Press Key", readOnlyHint: false, destructiveHint: false },
|
|
279
|
+
description: 'Press a keyboard key. Examples: "Enter", "Tab", "Escape", "ArrowDown", "Control+a".',
|
|
280
|
+
inputSchema: {
|
|
281
|
+
session_id: sessionId,
|
|
282
|
+
key: z.string().describe('Key to press (e.g. "Enter", "Tab", "Escape", "ArrowDown", "Control+a")'),
|
|
283
|
+
},
|
|
284
|
+
}, async ({ session_id, key }) => {
|
|
285
|
+
try {
|
|
286
|
+
const result = await fetch("POST", `/browser/sessions/${session_id}/press_key`, { key });
|
|
287
|
+
if (!result.ok)
|
|
288
|
+
return err(`Key press failed: ${browserError(result)}`);
|
|
289
|
+
return ok();
|
|
290
|
+
}
|
|
291
|
+
catch (e) {
|
|
292
|
+
return err(`Network error: ${e instanceof Error ? e.message : String(e)}`);
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
server.registerTool("browser_scroll", {
|
|
296
|
+
annotations: { title: "Scroll Page", readOnlyHint: false, destructiveHint: false },
|
|
297
|
+
description: "Scroll the page in a given direction.",
|
|
298
|
+
inputSchema: {
|
|
299
|
+
session_id: sessionId,
|
|
300
|
+
direction: z.enum(["up", "down", "left", "right"]).describe("Scroll direction"),
|
|
301
|
+
distance: z.number().int().positive().optional().describe("Pixels to scroll (default 500)"),
|
|
302
|
+
},
|
|
303
|
+
}, async ({ session_id, direction, distance }) => {
|
|
304
|
+
try {
|
|
305
|
+
const result = await fetch("POST", `/browser/sessions/${session_id}/scroll`, { direction, distance });
|
|
306
|
+
if (!result.ok)
|
|
307
|
+
return err(`Scroll failed: ${browserError(result)}`);
|
|
308
|
+
return ok();
|
|
309
|
+
}
|
|
310
|
+
catch (e) {
|
|
311
|
+
return err(`Network error: ${e instanceof Error ? e.message : String(e)}`);
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
server.registerTool("browser_drag", {
|
|
315
|
+
annotations: { title: "Drag Element", readOnlyHint: false, destructiveHint: false },
|
|
316
|
+
description: "Drag an element from its current position to a target element.",
|
|
317
|
+
inputSchema: {
|
|
318
|
+
session_id: sessionId,
|
|
319
|
+
source_selector: z.string().describe("CSS selector of the element to drag"),
|
|
320
|
+
target_selector: z.string().describe("CSS selector of the drop target"),
|
|
321
|
+
},
|
|
322
|
+
}, async ({ session_id, source_selector, target_selector }) => {
|
|
323
|
+
try {
|
|
324
|
+
const result = await fetch("POST", `/browser/sessions/${session_id}/drag`, {
|
|
325
|
+
source_selector,
|
|
326
|
+
target_selector,
|
|
327
|
+
});
|
|
328
|
+
if (!result.ok)
|
|
329
|
+
return err(`Drag failed: ${browserError(result)}`);
|
|
330
|
+
return ok();
|
|
331
|
+
}
|
|
332
|
+
catch (e) {
|
|
333
|
+
return err(`Network error: ${e instanceof Error ? e.message : String(e)}`);
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
// ─── Extraction ───────────────────────────────────────────────────────────
|
|
337
|
+
server.registerTool("browser_get_accessibility_tree", {
|
|
338
|
+
annotations: { title: "Get Accessibility Tree", readOnlyHint: true, destructiveHint: false },
|
|
339
|
+
description: `Get the ARIA accessibility tree of the current page as readable text.
|
|
340
|
+
|
|
341
|
+
Call this first after browser_navigate to understand the page structure before
|
|
342
|
+
interacting with elements. More token-efficient than a screenshot for finding
|
|
343
|
+
selectors and understanding layout.
|
|
344
|
+
|
|
345
|
+
The tree shows all interactive elements (buttons, inputs, links) with their roles,
|
|
346
|
+
labels, and states — everything needed to drive browser interactions.`,
|
|
347
|
+
inputSchema: { session_id: sessionId },
|
|
348
|
+
}, async ({ session_id }) => {
|
|
349
|
+
try {
|
|
350
|
+
const result = await fetch("GET", `/browser/sessions/${session_id}/accessibility_tree`);
|
|
351
|
+
if (!result.ok)
|
|
352
|
+
return err(`Failed to get accessibility tree: ${browserError(result)}`);
|
|
353
|
+
const data = result.data;
|
|
354
|
+
return { content: [{ type: "text", text: data.tree }] };
|
|
355
|
+
}
|
|
356
|
+
catch (e) {
|
|
357
|
+
return err(`Network error: ${e instanceof Error ? e.message : String(e)}`);
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
server.registerTool("browser_get_url", {
|
|
361
|
+
annotations: { title: "Get Current URL", readOnlyHint: true, destructiveHint: false },
|
|
362
|
+
description: "Get the current URL of the browser session.",
|
|
363
|
+
inputSchema: { session_id: sessionId },
|
|
364
|
+
}, async ({ session_id }) => {
|
|
365
|
+
try {
|
|
366
|
+
const result = await fetch("GET", `/browser/sessions/${session_id}/url`);
|
|
367
|
+
if (!result.ok)
|
|
368
|
+
return err(`Failed to get URL: ${browserError(result)}`);
|
|
369
|
+
return json(result.data);
|
|
370
|
+
}
|
|
371
|
+
catch (e) {
|
|
372
|
+
return err(`Network error: ${e instanceof Error ? e.message : String(e)}`);
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
server.registerTool("browser_get_title", {
|
|
376
|
+
annotations: { title: "Get Page Title", readOnlyHint: true, destructiveHint: false },
|
|
377
|
+
description: "Get the title of the current page.",
|
|
378
|
+
inputSchema: { session_id: sessionId },
|
|
379
|
+
}, async ({ session_id }) => {
|
|
380
|
+
try {
|
|
381
|
+
const result = await fetch("GET", `/browser/sessions/${session_id}/title`);
|
|
382
|
+
if (!result.ok)
|
|
383
|
+
return err(`Failed to get title: ${browserError(result)}`);
|
|
384
|
+
return json(result.data);
|
|
385
|
+
}
|
|
386
|
+
catch (e) {
|
|
387
|
+
return err(`Network error: ${e instanceof Error ? e.message : String(e)}`);
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
server.registerTool("browser_get_text", {
|
|
391
|
+
annotations: { title: "Get Text Content", readOnlyHint: true, destructiveHint: false },
|
|
392
|
+
description: "Get the visible text content of an element or the entire page body.",
|
|
393
|
+
inputSchema: {
|
|
394
|
+
session_id: sessionId,
|
|
395
|
+
selector: z.string().optional().describe("CSS selector of the element (omit for full page body text)"),
|
|
396
|
+
},
|
|
397
|
+
}, async ({ session_id, selector }) => {
|
|
398
|
+
try {
|
|
399
|
+
const result = await fetch("POST", `/browser/sessions/${session_id}/get_text`, { selector });
|
|
400
|
+
if (!result.ok)
|
|
401
|
+
return err(`Failed to get text: ${browserError(result)}`);
|
|
402
|
+
const data = result.data;
|
|
403
|
+
return { content: [{ type: "text", text: data.text }] };
|
|
404
|
+
}
|
|
405
|
+
catch (e) {
|
|
406
|
+
return err(`Network error: ${e instanceof Error ? e.message : String(e)}`);
|
|
407
|
+
}
|
|
408
|
+
});
|
|
409
|
+
server.registerTool("browser_get_attribute", {
|
|
410
|
+
annotations: { title: "Get Element Attribute", readOnlyHint: true, destructiveHint: false },
|
|
411
|
+
description: "Get the value of a specific attribute on an element.",
|
|
412
|
+
inputSchema: {
|
|
413
|
+
session_id: sessionId,
|
|
414
|
+
selector: z.string().describe("CSS selector of the element"),
|
|
415
|
+
attribute: z.string().describe("Attribute name to retrieve (e.g. 'href', 'src', 'data-id')"),
|
|
416
|
+
},
|
|
417
|
+
}, async ({ session_id, selector, attribute }) => {
|
|
418
|
+
try {
|
|
419
|
+
const result = await fetch("POST", `/browser/sessions/${session_id}/get_attribute`, { selector, attribute });
|
|
420
|
+
if (!result.ok)
|
|
421
|
+
return err(`Failed to get attribute: ${browserError(result)}`);
|
|
422
|
+
return json(result.data);
|
|
423
|
+
}
|
|
424
|
+
catch (e) {
|
|
425
|
+
return err(`Network error: ${e instanceof Error ? e.message : String(e)}`);
|
|
426
|
+
}
|
|
427
|
+
});
|
|
428
|
+
server.registerTool("browser_get_html", {
|
|
429
|
+
annotations: { title: "Get HTML", readOnlyHint: true, destructiveHint: false },
|
|
430
|
+
description: "Get the HTML source of an element or the full page.",
|
|
431
|
+
inputSchema: {
|
|
432
|
+
session_id: sessionId,
|
|
433
|
+
selector: z.string().optional().describe("CSS selector of the element (omit for full page HTML)"),
|
|
434
|
+
},
|
|
435
|
+
}, async ({ session_id, selector }) => {
|
|
436
|
+
try {
|
|
437
|
+
const result = await fetch("POST", `/browser/sessions/${session_id}/get_html`, { selector });
|
|
438
|
+
if (!result.ok)
|
|
439
|
+
return err(`Failed to get HTML: ${browserError(result)}`);
|
|
440
|
+
const data = result.data;
|
|
441
|
+
return { content: [{ type: "text", text: data.html }] };
|
|
442
|
+
}
|
|
443
|
+
catch (e) {
|
|
444
|
+
return err(`Network error: ${e instanceof Error ? e.message : String(e)}`);
|
|
445
|
+
}
|
|
446
|
+
});
|
|
447
|
+
server.registerTool("browser_query_selector_all", {
|
|
448
|
+
annotations: { title: "Query All Matching Elements", readOnlyHint: true, destructiveHint: false },
|
|
449
|
+
description: "Find all elements matching a CSS selector and return their text, HTML, and attributes.",
|
|
450
|
+
inputSchema: {
|
|
451
|
+
session_id: sessionId,
|
|
452
|
+
selector: z.string().describe("CSS selector to query"),
|
|
453
|
+
},
|
|
454
|
+
}, async ({ session_id, selector }) => {
|
|
455
|
+
try {
|
|
456
|
+
const result = await fetch("POST", `/browser/sessions/${session_id}/query_selector_all`, { selector });
|
|
457
|
+
if (!result.ok)
|
|
458
|
+
return err(`Query failed: ${browserError(result)}`);
|
|
459
|
+
return json(result.data);
|
|
460
|
+
}
|
|
461
|
+
catch (e) {
|
|
462
|
+
return err(`Network error: ${e instanceof Error ? e.message : String(e)}`);
|
|
463
|
+
}
|
|
464
|
+
});
|
|
465
|
+
// ─── Visual ───────────────────────────────────────────────────────────────
|
|
466
|
+
server.registerTool("browser_screenshot", {
|
|
467
|
+
annotations: { title: "Take Screenshot", readOnlyHint: true, destructiveHint: false },
|
|
468
|
+
description: "Take a screenshot of the current page or a specific element. Use for visual verification or when the accessibility tree is not sufficient.",
|
|
469
|
+
inputSchema: {
|
|
470
|
+
session_id: sessionId,
|
|
471
|
+
full_page: z.boolean().optional().describe("Capture full page including content below the fold (default false)"),
|
|
472
|
+
selector: z
|
|
473
|
+
.string()
|
|
474
|
+
.optional()
|
|
475
|
+
.describe("CSS selector to capture only a specific element (overrides full_page)"),
|
|
476
|
+
},
|
|
477
|
+
}, async ({ session_id, full_page, selector }) => {
|
|
478
|
+
try {
|
|
479
|
+
const result = await fetch("POST", `/browser/sessions/${session_id}/screenshot`, { full_page, selector });
|
|
480
|
+
if (!result.ok)
|
|
481
|
+
return err(`Screenshot failed: ${browserError(result)}`);
|
|
482
|
+
const data = result.data;
|
|
483
|
+
return {
|
|
484
|
+
content: [
|
|
485
|
+
{
|
|
486
|
+
type: "image",
|
|
487
|
+
data: data.data,
|
|
488
|
+
mimeType: data.mime_type,
|
|
489
|
+
},
|
|
490
|
+
],
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
catch (e) {
|
|
494
|
+
return err(`Network error: ${e instanceof Error ? e.message : String(e)}`);
|
|
495
|
+
}
|
|
496
|
+
});
|
|
497
|
+
server.registerTool("browser_generate_pdf", {
|
|
498
|
+
annotations: { title: "Generate PDF", readOnlyHint: true, destructiveHint: false },
|
|
499
|
+
description: "Render the current page as a PDF document.",
|
|
500
|
+
inputSchema: {
|
|
501
|
+
session_id: sessionId,
|
|
502
|
+
print_background: z.boolean().optional().describe("Print background graphics (default false)"),
|
|
503
|
+
landscape: z.boolean().optional().describe("Landscape orientation (default false)"),
|
|
504
|
+
scale: z.number().min(0.1).max(2).optional().describe("Page scale factor (default 1)"),
|
|
505
|
+
},
|
|
506
|
+
}, async ({ session_id, print_background, landscape, scale }) => {
|
|
507
|
+
try {
|
|
508
|
+
const result = await fetch("POST", `/browser/sessions/${session_id}/generate_pdf`, { print_background, landscape, scale });
|
|
509
|
+
if (!result.ok)
|
|
510
|
+
return err(`PDF generation failed: ${browserError(result)}`);
|
|
511
|
+
const data = result.data;
|
|
512
|
+
return {
|
|
513
|
+
content: [
|
|
514
|
+
{
|
|
515
|
+
type: "resource",
|
|
516
|
+
resource: {
|
|
517
|
+
uri: `data:${data.mime_type};base64,${data.data}`,
|
|
518
|
+
mimeType: data.mime_type,
|
|
519
|
+
blob: data.data,
|
|
520
|
+
},
|
|
521
|
+
},
|
|
522
|
+
],
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
catch (e) {
|
|
526
|
+
return err(`Network error: ${e instanceof Error ? e.message : String(e)}`);
|
|
527
|
+
}
|
|
528
|
+
});
|
|
529
|
+
// ─── Wait ─────────────────────────────────────────────────────────────────
|
|
530
|
+
server.registerTool("browser_wait_for_selector", {
|
|
531
|
+
annotations: { title: "Wait for Element", readOnlyHint: true, destructiveHint: false },
|
|
532
|
+
description: "Wait until an element matching the CSS selector is stable in the DOM. Set visible=true to also require the element to be visible (not hidden).",
|
|
533
|
+
inputSchema: {
|
|
534
|
+
session_id: sessionId,
|
|
535
|
+
selector: z.string().describe("CSS selector to wait for"),
|
|
536
|
+
visible: z.boolean().optional().describe("Also require the element to be visible, not just present in the DOM (default false)"),
|
|
537
|
+
},
|
|
538
|
+
}, async ({ session_id, selector, visible }) => {
|
|
539
|
+
try {
|
|
540
|
+
const result = await fetch("POST", `/browser/sessions/${session_id}/wait_for_selector`, { selector, visible });
|
|
541
|
+
if (!result.ok)
|
|
542
|
+
return err(`Wait for selector failed: ${browserError(result)}`);
|
|
543
|
+
return ok();
|
|
544
|
+
}
|
|
545
|
+
catch (e) {
|
|
546
|
+
return err(`Network error: ${e instanceof Error ? e.message : String(e)}`);
|
|
547
|
+
}
|
|
548
|
+
});
|
|
549
|
+
server.registerTool("browser_wait_for_navigation", {
|
|
550
|
+
annotations: { title: "Wait for Navigation", readOnlyHint: true, destructiveHint: false },
|
|
551
|
+
description: "Wait for the page to navigate to a new URL. IMPORTANT: call this BEFORE the action that triggers navigation (e.g. before browser_click on a submit button), not after — the navigation event may already have fired and this will hang until timeout. If the page stays on the same URL (AJAX/SPA), skip this tool entirely. Optional timeout_ms (default 30000ms).",
|
|
552
|
+
inputSchema: {
|
|
553
|
+
session_id: sessionId,
|
|
554
|
+
timeout_ms: z.number().int().min(1000).max(60000).optional().describe("How long to wait in milliseconds (default 30000, max 60000)"),
|
|
555
|
+
},
|
|
556
|
+
}, async ({ session_id, timeout_ms }) => {
|
|
557
|
+
try {
|
|
558
|
+
const result = await fetch("POST", `/browser/sessions/${session_id}/wait_for_navigation`, timeout_ms ? { timeout_ms } : undefined);
|
|
559
|
+
if (!result.ok)
|
|
560
|
+
return err(`Wait for navigation failed: ${browserError(result)}`);
|
|
561
|
+
return json(result.data);
|
|
562
|
+
}
|
|
563
|
+
catch (e) {
|
|
564
|
+
return err(`Network error: ${e instanceof Error ? e.message : String(e)}`);
|
|
565
|
+
}
|
|
566
|
+
});
|
|
567
|
+
server.registerTool("browser_wait", {
|
|
568
|
+
annotations: { title: "Wait", readOnlyHint: true, destructiveHint: false },
|
|
569
|
+
description: "Wait for a fixed duration. Use sparingly — prefer browser_wait_for_selector when possible.",
|
|
570
|
+
inputSchema: {
|
|
571
|
+
session_id: sessionId,
|
|
572
|
+
ms: z.number().int().min(0).max(30000).describe("Milliseconds to wait (max 30000)"),
|
|
573
|
+
},
|
|
574
|
+
}, async ({ session_id, ms }) => {
|
|
575
|
+
try {
|
|
576
|
+
const result = await fetch("POST", `/browser/sessions/${session_id}/wait`, { ms });
|
|
577
|
+
if (!result.ok)
|
|
578
|
+
return err(`Wait failed: ${browserError(result)}`);
|
|
579
|
+
return ok();
|
|
580
|
+
}
|
|
581
|
+
catch (e) {
|
|
582
|
+
return err(`Network error: ${e instanceof Error ? e.message : String(e)}`);
|
|
583
|
+
}
|
|
584
|
+
});
|
|
585
|
+
// ─── JavaScript ───────────────────────────────────────────────────────────
|
|
586
|
+
server.registerTool("browser_evaluate", {
|
|
587
|
+
annotations: { title: "Evaluate JavaScript", readOnlyHint: false, destructiveHint: false },
|
|
588
|
+
description: "Execute JavaScript in the browser context and return the result.",
|
|
589
|
+
inputSchema: {
|
|
590
|
+
session_id: sessionId,
|
|
591
|
+
script: z.string().describe("JavaScript expression or function body to evaluate in the page context"),
|
|
592
|
+
},
|
|
593
|
+
}, async ({ session_id, script }) => {
|
|
594
|
+
try {
|
|
595
|
+
const result = await fetch("POST", `/browser/sessions/${session_id}/evaluate`, { script });
|
|
596
|
+
if (!result.ok)
|
|
597
|
+
return err(`Evaluate failed: ${browserError(result)}`);
|
|
598
|
+
return json(result.data);
|
|
599
|
+
}
|
|
600
|
+
catch (e) {
|
|
601
|
+
return err(`Network error: ${e instanceof Error ? e.message : String(e)}`);
|
|
602
|
+
}
|
|
603
|
+
});
|
|
604
|
+
// ─── Cookies ──────────────────────────────────────────────────────────────
|
|
605
|
+
server.registerTool("browser_get_cookies", {
|
|
606
|
+
annotations: { title: "Get Cookies", readOnlyHint: true, destructiveHint: false },
|
|
607
|
+
description: "Get all cookies for the current page.",
|
|
608
|
+
inputSchema: { session_id: sessionId },
|
|
609
|
+
}, async ({ session_id }) => {
|
|
610
|
+
try {
|
|
611
|
+
const result = await fetch("GET", `/browser/sessions/${session_id}/cookies`);
|
|
612
|
+
if (!result.ok)
|
|
613
|
+
return err(`Failed to get cookies: ${browserError(result)}`);
|
|
614
|
+
return json(result.data);
|
|
615
|
+
}
|
|
616
|
+
catch (e) {
|
|
617
|
+
return err(`Network error: ${e instanceof Error ? e.message : String(e)}`);
|
|
618
|
+
}
|
|
619
|
+
});
|
|
620
|
+
server.registerTool("browser_set_cookies", {
|
|
621
|
+
annotations: { title: "Set Cookies", readOnlyHint: false, destructiveHint: false },
|
|
622
|
+
description: "Set one or more cookies in the browser session.",
|
|
623
|
+
inputSchema: {
|
|
624
|
+
session_id: sessionId,
|
|
625
|
+
cookies: z
|
|
626
|
+
.array(z.object({
|
|
627
|
+
name: z.string(),
|
|
628
|
+
value: z.string(),
|
|
629
|
+
domain: z.string().optional(),
|
|
630
|
+
path: z.string().optional(),
|
|
631
|
+
expires: z.number().optional().describe("Unix timestamp"),
|
|
632
|
+
http_only: z.boolean().optional(),
|
|
633
|
+
secure: z.boolean().optional(),
|
|
634
|
+
}))
|
|
635
|
+
.describe("Array of cookie objects to set"),
|
|
636
|
+
},
|
|
637
|
+
}, async ({ session_id, cookies }) => {
|
|
638
|
+
try {
|
|
639
|
+
// Remap http_only → httpOnly to match Chrome DevTools Protocol / Rod's JSON field names.
|
|
640
|
+
const mapped = cookies.map(({ http_only, ...rest }) => ({ ...rest, httpOnly: http_only }));
|
|
641
|
+
const result = await fetch("POST", `/browser/sessions/${session_id}/cookies`, { cookies: mapped });
|
|
642
|
+
if (!result.ok)
|
|
643
|
+
return err(`Failed to set cookies: ${browserError(result)}`);
|
|
644
|
+
return ok();
|
|
645
|
+
}
|
|
646
|
+
catch (e) {
|
|
647
|
+
return err(`Network error: ${e instanceof Error ? e.message : String(e)}`);
|
|
648
|
+
}
|
|
649
|
+
});
|
|
650
|
+
server.registerTool("browser_clear_cookies", {
|
|
651
|
+
annotations: { title: "Clear Cookies", readOnlyHint: false, destructiveHint: false },
|
|
652
|
+
description: "Clear all cookies for the current browser session.",
|
|
653
|
+
inputSchema: { session_id: sessionId },
|
|
654
|
+
}, async ({ session_id }) => {
|
|
655
|
+
try {
|
|
656
|
+
const result = await fetch("DELETE", `/browser/sessions/${session_id}/cookies`);
|
|
657
|
+
if (!result.ok)
|
|
658
|
+
return err(`Failed to clear cookies: ${browserError(result)}`);
|
|
659
|
+
return ok();
|
|
660
|
+
}
|
|
661
|
+
catch (e) {
|
|
662
|
+
return err(`Network error: ${e instanceof Error ? e.message : String(e)}`);
|
|
663
|
+
}
|
|
664
|
+
});
|
|
665
|
+
// ─── Local Storage ────────────────────────────────────────────────────────
|
|
666
|
+
server.registerTool("browser_local_storage", {
|
|
667
|
+
annotations: { title: "Local Storage", readOnlyHint: false, destructiveHint: false },
|
|
668
|
+
description: "Read, write, or clear localStorage in the current page context.",
|
|
669
|
+
inputSchema: {
|
|
670
|
+
session_id: sessionId,
|
|
671
|
+
action: z.enum(["get", "set", "clear"]).describe("Operation: get a value, set a value, or clear all"),
|
|
672
|
+
key: z.string().optional().describe("Storage key (required for get and set)"),
|
|
673
|
+
value: z.string().optional().describe("Value to store (required for set)"),
|
|
674
|
+
},
|
|
675
|
+
}, async ({ session_id, action, key, value }) => {
|
|
676
|
+
try {
|
|
677
|
+
const result = await fetch("POST", `/browser/sessions/${session_id}/local_storage`, { action, key, value });
|
|
678
|
+
if (!result.ok)
|
|
679
|
+
return err(`Local storage operation failed: ${browserError(result)}`);
|
|
680
|
+
return json(result.data);
|
|
681
|
+
}
|
|
682
|
+
catch (e) {
|
|
683
|
+
return err(`Network error: ${e instanceof Error ? e.message : String(e)}`);
|
|
684
|
+
}
|
|
685
|
+
});
|
|
686
|
+
// ─── Tabs ─────────────────────────────────────────────────────────────────
|
|
687
|
+
server.registerTool("browser_new_tab", {
|
|
688
|
+
annotations: { title: "Open New Tab", readOnlyHint: false, destructiveHint: false },
|
|
689
|
+
description: "Open a new browser tab. Returns a tab_id to use with browser_switch_tab.",
|
|
690
|
+
inputSchema: {
|
|
691
|
+
session_id: sessionId,
|
|
692
|
+
url: z.string().url().optional().describe("URL to open in the new tab (opens blank tab if omitted)"),
|
|
693
|
+
},
|
|
694
|
+
}, async ({ session_id, url }) => {
|
|
695
|
+
try {
|
|
696
|
+
const result = await fetch("POST", `/browser/sessions/${session_id}/new_tab`, { url });
|
|
697
|
+
if (!result.ok)
|
|
698
|
+
return err(`Failed to open new tab: ${browserError(result)}`);
|
|
699
|
+
return json(result.data);
|
|
700
|
+
}
|
|
701
|
+
catch (e) {
|
|
702
|
+
return err(`Network error: ${e instanceof Error ? e.message : String(e)}`);
|
|
703
|
+
}
|
|
704
|
+
});
|
|
705
|
+
server.registerTool("browser_switch_tab", {
|
|
706
|
+
annotations: { title: "Switch Tab", readOnlyHint: false, destructiveHint: false },
|
|
707
|
+
description: "Switch focus to a different browser tab.",
|
|
708
|
+
inputSchema: {
|
|
709
|
+
session_id: sessionId,
|
|
710
|
+
tab_id: z.string().describe("Tab ID returned by browser_new_tab"),
|
|
711
|
+
},
|
|
712
|
+
}, async ({ session_id, tab_id }) => {
|
|
713
|
+
try {
|
|
714
|
+
const result = await fetch("POST", `/browser/sessions/${session_id}/switch_tab`, { tab_id });
|
|
715
|
+
if (!result.ok)
|
|
716
|
+
return err(`Failed to switch tab: ${browserError(result)}`);
|
|
717
|
+
return ok();
|
|
718
|
+
}
|
|
719
|
+
catch (e) {
|
|
720
|
+
return err(`Network error: ${e instanceof Error ? e.message : String(e)}`);
|
|
721
|
+
}
|
|
722
|
+
});
|
|
723
|
+
// ─── Batch ────────────────────────────────────────────────────────────────
|
|
724
|
+
const batchAction = z.discriminatedUnion("type", [
|
|
725
|
+
z.object({ type: z.literal("navigate"), url: z.string().url() }),
|
|
726
|
+
z.object({ type: z.literal("go_back") }),
|
|
727
|
+
z.object({ type: z.literal("go_forward") }),
|
|
728
|
+
z.object({ type: z.literal("reload") }),
|
|
729
|
+
z.object({ type: z.literal("click"), selector: z.string() }),
|
|
730
|
+
z.object({ type: z.literal("hover"), selector: z.string() }),
|
|
731
|
+
z.object({ type: z.literal("type"), selector: z.string(), text: z.string(), clear_first: z.boolean().optional() }),
|
|
732
|
+
z.object({ type: z.literal("fill"), selector: z.string(), value: z.string() }),
|
|
733
|
+
z.object({ type: z.literal("select"), selector: z.string(), value: z.string() }),
|
|
734
|
+
z.object({ type: z.literal("check"), selector: z.string() }),
|
|
735
|
+
z.object({ type: z.literal("uncheck"), selector: z.string() }),
|
|
736
|
+
z.object({ type: z.literal("focus"), selector: z.string() }),
|
|
737
|
+
z.object({ type: z.literal("press_key"), key: z.string() }),
|
|
738
|
+
z.object({ type: z.literal("scroll"), direction: z.enum(["up", "down", "left", "right"]), distance: z.number().optional() }),
|
|
739
|
+
z.object({ type: z.literal("drag"), source_selector: z.string(), target_selector: z.string() }),
|
|
740
|
+
z.object({ type: z.literal("get_accessibility_tree") }),
|
|
741
|
+
z.object({ type: z.literal("get_url") }),
|
|
742
|
+
z.object({ type: z.literal("get_title") }),
|
|
743
|
+
z.object({ type: z.literal("get_text"), selector: z.string().optional() }),
|
|
744
|
+
z.object({ type: z.literal("get_attribute"), selector: z.string(), attribute: z.string() }),
|
|
745
|
+
z.object({ type: z.literal("get_html"), selector: z.string().optional() }),
|
|
746
|
+
z.object({ type: z.literal("query_selector_all"), selector: z.string() }),
|
|
747
|
+
z.object({ type: z.literal("screenshot"), full_page: z.boolean().optional(), selector: z.string().optional() }),
|
|
748
|
+
z.object({ type: z.literal("wait_for_selector"), selector: z.string(), visible: z.boolean().optional() }),
|
|
749
|
+
z.object({ type: z.literal("wait_for_navigation") }),
|
|
750
|
+
z.object({ type: z.literal("wait"), ms: z.number().int().min(0).max(30000) }),
|
|
751
|
+
z.object({ type: z.literal("evaluate"), script: z.string() }),
|
|
752
|
+
]);
|
|
753
|
+
async function runAction(action, session_id) {
|
|
754
|
+
const sid = session_id;
|
|
755
|
+
switch (action.type) {
|
|
756
|
+
case "navigate": {
|
|
757
|
+
const r = await fetch("POST", `/browser/sessions/${sid}/navigate`, { url: action.url });
|
|
758
|
+
if (!r.ok)
|
|
759
|
+
throw new Error(browserError(r));
|
|
760
|
+
return r.data;
|
|
761
|
+
}
|
|
762
|
+
case "go_back": {
|
|
763
|
+
const r = await fetch("POST", `/browser/sessions/${sid}/go_back`);
|
|
764
|
+
if (!r.ok)
|
|
765
|
+
throw new Error(browserError(r));
|
|
766
|
+
return r.data;
|
|
767
|
+
}
|
|
768
|
+
case "go_forward": {
|
|
769
|
+
const r = await fetch("POST", `/browser/sessions/${sid}/go_forward`);
|
|
770
|
+
if (!r.ok)
|
|
771
|
+
throw new Error(browserError(r));
|
|
772
|
+
return r.data;
|
|
773
|
+
}
|
|
774
|
+
case "reload": {
|
|
775
|
+
const r = await fetch("POST", `/browser/sessions/${sid}/reload`);
|
|
776
|
+
if (!r.ok)
|
|
777
|
+
throw new Error(browserError(r));
|
|
778
|
+
return r.data;
|
|
779
|
+
}
|
|
780
|
+
case "click": {
|
|
781
|
+
const r = await fetch("POST", `/browser/sessions/${sid}/click`, { selector: action.selector });
|
|
782
|
+
if (!r.ok)
|
|
783
|
+
throw new Error(browserError(r));
|
|
784
|
+
return { ok: true };
|
|
785
|
+
}
|
|
786
|
+
case "hover": {
|
|
787
|
+
const r = await fetch("POST", `/browser/sessions/${sid}/hover`, { selector: action.selector });
|
|
788
|
+
if (!r.ok)
|
|
789
|
+
throw new Error(browserError(r));
|
|
790
|
+
return { ok: true };
|
|
791
|
+
}
|
|
792
|
+
case "type": {
|
|
793
|
+
const r = await fetch("POST", `/browser/sessions/${sid}/type`, { selector: action.selector, text: action.text, clear_first: action.clear_first });
|
|
794
|
+
if (!r.ok)
|
|
795
|
+
throw new Error(browserError(r));
|
|
796
|
+
return { ok: true };
|
|
797
|
+
}
|
|
798
|
+
case "fill": {
|
|
799
|
+
const r = await fetch("POST", `/browser/sessions/${sid}/fill`, { selector: action.selector, value: action.value });
|
|
800
|
+
if (!r.ok)
|
|
801
|
+
throw new Error(browserError(r));
|
|
802
|
+
return { ok: true };
|
|
803
|
+
}
|
|
804
|
+
case "select": {
|
|
805
|
+
const r = await fetch("POST", `/browser/sessions/${sid}/select`, { selector: action.selector, value: action.value });
|
|
806
|
+
if (!r.ok)
|
|
807
|
+
throw new Error(browserError(r));
|
|
808
|
+
return { ok: true };
|
|
809
|
+
}
|
|
810
|
+
case "check": {
|
|
811
|
+
const r = await fetch("POST", `/browser/sessions/${sid}/check`, { selector: action.selector });
|
|
812
|
+
if (!r.ok)
|
|
813
|
+
throw new Error(browserError(r));
|
|
814
|
+
return { ok: true };
|
|
815
|
+
}
|
|
816
|
+
case "uncheck": {
|
|
817
|
+
const r = await fetch("POST", `/browser/sessions/${sid}/uncheck`, { selector: action.selector });
|
|
818
|
+
if (!r.ok)
|
|
819
|
+
throw new Error(browserError(r));
|
|
820
|
+
return { ok: true };
|
|
821
|
+
}
|
|
822
|
+
case "focus": {
|
|
823
|
+
const r = await fetch("POST", `/browser/sessions/${sid}/focus`, { selector: action.selector });
|
|
824
|
+
if (!r.ok)
|
|
825
|
+
throw new Error(browserError(r));
|
|
826
|
+
return { ok: true };
|
|
827
|
+
}
|
|
828
|
+
case "press_key": {
|
|
829
|
+
const r = await fetch("POST", `/browser/sessions/${sid}/press_key`, { key: action.key });
|
|
830
|
+
if (!r.ok)
|
|
831
|
+
throw new Error(browserError(r));
|
|
832
|
+
return { ok: true };
|
|
833
|
+
}
|
|
834
|
+
case "scroll": {
|
|
835
|
+
const r = await fetch("POST", `/browser/sessions/${sid}/scroll`, { direction: action.direction, distance: action.distance });
|
|
836
|
+
if (!r.ok)
|
|
837
|
+
throw new Error(browserError(r));
|
|
838
|
+
return { ok: true };
|
|
839
|
+
}
|
|
840
|
+
case "drag": {
|
|
841
|
+
const r = await fetch("POST", `/browser/sessions/${sid}/drag`, { source_selector: action.source_selector, target_selector: action.target_selector });
|
|
842
|
+
if (!r.ok)
|
|
843
|
+
throw new Error(browserError(r));
|
|
844
|
+
return { ok: true };
|
|
845
|
+
}
|
|
846
|
+
case "get_accessibility_tree": {
|
|
847
|
+
const r = await fetch("GET", `/browser/sessions/${sid}/accessibility_tree`);
|
|
848
|
+
if (!r.ok)
|
|
849
|
+
throw new Error(browserError(r));
|
|
850
|
+
return r.data.tree;
|
|
851
|
+
}
|
|
852
|
+
case "get_url": {
|
|
853
|
+
const r = await fetch("GET", `/browser/sessions/${sid}/url`);
|
|
854
|
+
if (!r.ok)
|
|
855
|
+
throw new Error(browserError(r));
|
|
856
|
+
return r.data;
|
|
857
|
+
}
|
|
858
|
+
case "get_title": {
|
|
859
|
+
const r = await fetch("GET", `/browser/sessions/${sid}/title`);
|
|
860
|
+
if (!r.ok)
|
|
861
|
+
throw new Error(browserError(r));
|
|
862
|
+
return r.data;
|
|
863
|
+
}
|
|
864
|
+
case "get_text": {
|
|
865
|
+
const r = await fetch("POST", `/browser/sessions/${sid}/get_text`, { selector: action.selector });
|
|
866
|
+
if (!r.ok)
|
|
867
|
+
throw new Error(browserError(r));
|
|
868
|
+
return r.data.text;
|
|
869
|
+
}
|
|
870
|
+
case "get_attribute": {
|
|
871
|
+
const r = await fetch("POST", `/browser/sessions/${sid}/get_attribute`, { selector: action.selector, attribute: action.attribute });
|
|
872
|
+
if (!r.ok)
|
|
873
|
+
throw new Error(browserError(r));
|
|
874
|
+
return r.data;
|
|
875
|
+
}
|
|
876
|
+
case "get_html": {
|
|
877
|
+
const r = await fetch("POST", `/browser/sessions/${sid}/get_html`, { selector: action.selector });
|
|
878
|
+
if (!r.ok)
|
|
879
|
+
throw new Error(browserError(r));
|
|
880
|
+
return r.data.html;
|
|
881
|
+
}
|
|
882
|
+
case "query_selector_all": {
|
|
883
|
+
const r = await fetch("POST", `/browser/sessions/${sid}/query_selector_all`, { selector: action.selector });
|
|
884
|
+
if (!r.ok)
|
|
885
|
+
throw new Error(browserError(r));
|
|
886
|
+
return r.data;
|
|
887
|
+
}
|
|
888
|
+
case "screenshot": {
|
|
889
|
+
const r = await fetch("POST", `/browser/sessions/${sid}/screenshot`, { full_page: action.full_page, selector: action.selector });
|
|
890
|
+
if (!r.ok)
|
|
891
|
+
throw new Error(browserError(r));
|
|
892
|
+
// Return as data URI string so it fits in the JSON result array.
|
|
893
|
+
// For proper image rendering, use browser_screenshot directly.
|
|
894
|
+
const d = r.data;
|
|
895
|
+
return { mime_type: d.mime_type, data: d.data };
|
|
896
|
+
}
|
|
897
|
+
case "wait_for_selector": {
|
|
898
|
+
const r = await fetch("POST", `/browser/sessions/${sid}/wait_for_selector`, { selector: action.selector, visible: action.visible });
|
|
899
|
+
if (!r.ok)
|
|
900
|
+
throw new Error(browserError(r));
|
|
901
|
+
return { ok: true };
|
|
902
|
+
}
|
|
903
|
+
case "wait_for_navigation": {
|
|
904
|
+
const r = await fetch("POST", `/browser/sessions/${sid}/wait_for_navigation`);
|
|
905
|
+
if (!r.ok)
|
|
906
|
+
throw new Error(browserError(r));
|
|
907
|
+
return r.data;
|
|
908
|
+
}
|
|
909
|
+
case "wait": {
|
|
910
|
+
const r = await fetch("POST", `/browser/sessions/${sid}/wait`, { ms: action.ms });
|
|
911
|
+
if (!r.ok)
|
|
912
|
+
throw new Error(browserError(r));
|
|
913
|
+
return { ok: true };
|
|
914
|
+
}
|
|
915
|
+
case "evaluate": {
|
|
916
|
+
const r = await fetch("POST", `/browser/sessions/${sid}/evaluate`, { script: action.script });
|
|
917
|
+
if (!r.ok)
|
|
918
|
+
throw new Error(browserError(r));
|
|
919
|
+
return r.data;
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
server.registerTool("browser_batch", {
|
|
924
|
+
annotations: { title: "Run Batch of Browser Actions", readOnlyHint: false, destructiveHint: false },
|
|
925
|
+
description: `Execute a sequence of browser actions in a single call against an existing session.
|
|
926
|
+
|
|
927
|
+
Use this when you already know the full sequence of steps — it reduces round trips and
|
|
928
|
+
is faster than calling each tool individually.
|
|
929
|
+
|
|
930
|
+
Actions run sequentially and stop at the first failure unless stop_on_error is false.
|
|
931
|
+
Each step result is returned in the results array at the same index.
|
|
932
|
+
|
|
933
|
+
Note: screenshots in batch results are returned as base64 strings inside the JSON.
|
|
934
|
+
For proper image rendering, use browser_screenshot directly.`,
|
|
935
|
+
inputSchema: {
|
|
936
|
+
session_id: sessionId,
|
|
937
|
+
actions: z.array(batchAction).min(1).max(50).describe("Ordered list of actions to perform. Each action has a 'type' field plus type-specific parameters."),
|
|
938
|
+
stop_on_error: z.boolean().optional().describe("Stop executing on the first failed action (default true)"),
|
|
939
|
+
},
|
|
940
|
+
}, async ({ session_id, actions, stop_on_error = true }) => {
|
|
941
|
+
const results = [];
|
|
942
|
+
for (let i = 0; i < actions.length; i++) {
|
|
943
|
+
const action = actions[i];
|
|
944
|
+
try {
|
|
945
|
+
const result = await runAction(action, session_id);
|
|
946
|
+
results.push({ step: i, type: action.type, result });
|
|
947
|
+
}
|
|
948
|
+
catch (e) {
|
|
949
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
950
|
+
results.push({ step: i, type: action.type, error: msg });
|
|
951
|
+
if (stop_on_error) {
|
|
952
|
+
return {
|
|
953
|
+
content: [{ type: "text", text: JSON.stringify({ completed: i, total: actions.length, results }) }],
|
|
954
|
+
isError: true,
|
|
955
|
+
};
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
return json({ completed: actions.length, total: actions.length, results });
|
|
960
|
+
});
|
|
961
|
+
}
|