mcp-scraper 0.2.20 → 0.2.21
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 +34 -3
- package/dist/bin/api-server.cjs +52 -1
- package/dist/bin/api-server.cjs.map +1 -1
- package/dist/bin/api-server.js +2 -2
- package/dist/bin/browser-agent-stdio-server.cjs +960 -20
- package/dist/bin/browser-agent-stdio-server.cjs.map +1 -1
- package/dist/bin/browser-agent-stdio-server.js +3 -1
- package/dist/bin/browser-agent-stdio-server.js.map +1 -1
- package/dist/bin/mcp-scraper-cli.cjs +342 -47
- package/dist/bin/mcp-scraper-cli.cjs.map +1 -1
- package/dist/bin/mcp-scraper-cli.js +105 -7
- package/dist/bin/mcp-scraper-cli.js.map +1 -1
- package/dist/bin/mcp-scraper-combined-stdio-server.cjs +976 -36
- package/dist/bin/mcp-scraper-combined-stdio-server.cjs.map +1 -1
- package/dist/bin/mcp-scraper-combined-stdio-server.js +3 -1
- package/dist/bin/mcp-scraper-combined-stdio-server.js.map +1 -1
- package/dist/bin/paa-harvest.cjs.map +1 -1
- package/dist/bin/paa-harvest.js +4 -2
- package/dist/bin/paa-harvest.js.map +1 -1
- package/dist/chunk-DUEW4EOO.js +214 -0
- package/dist/chunk-DUEW4EOO.js.map +1 -0
- package/dist/{chunk-L4OWOUGR.js → chunk-HS6OKELW.js} +737 -3
- package/dist/chunk-HS6OKELW.js.map +1 -0
- package/dist/{chunk-XGUDTDZ2.js → chunk-IPW4LFOT.js} +5 -13
- package/dist/chunk-IPW4LFOT.js.map +1 -0
- package/dist/chunk-WN7PBKMV.js +28 -0
- package/dist/chunk-WN7PBKMV.js.map +1 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/{server-7NE45K2A.js → server-24XIQ6W6.js} +40 -6
- package/dist/server-24XIQ6W6.js.map +1 -0
- package/dist/{worker-MIYG2B2I.js → worker-SLQ375UG.js} +5 -3
- package/dist/{worker-MIYG2B2I.js.map → worker-SLQ375UG.js.map} +1 -1
- package/docs/mcp-tool-manifest.generated.json +132 -4
- package/package.json +1 -1
- package/dist/chunk-L4OWOUGR.js.map +0 -1
- package/dist/chunk-XGUDTDZ2.js.map +0 -1
- package/dist/server-7NE45K2A.js.map +0 -1
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
import {
|
|
2
|
+
browserServiceProfileName,
|
|
3
|
+
browserServiceProfileSaveChanges
|
|
4
|
+
} from "./chunk-WN7PBKMV.js";
|
|
5
|
+
import {
|
|
6
|
+
defaultChromeExecutablePath,
|
|
7
|
+
importChromeProfile,
|
|
8
|
+
listLocalChromeProfiles,
|
|
9
|
+
loadImportedBrowserProfile,
|
|
10
|
+
localBrowserModeEnabled,
|
|
11
|
+
syncImportedChromeProfile
|
|
12
|
+
} from "./chunk-DUEW4EOO.js";
|
|
1
13
|
import {
|
|
2
14
|
PACKAGE_VERSION
|
|
3
15
|
} from "./chunk-XKUDVN2E.js";
|
|
@@ -8,6 +20,361 @@ import { mkdirSync, writeFileSync } from "fs";
|
|
|
8
20
|
import { homedir } from "os";
|
|
9
21
|
import { join as join2 } from "path";
|
|
10
22
|
|
|
23
|
+
// src/services/browser-agent/local-browser-agent-service.ts
|
|
24
|
+
import { randomUUID } from "crypto";
|
|
25
|
+
import { existsSync } from "fs";
|
|
26
|
+
import { basename } from "path";
|
|
27
|
+
import { chromium } from "playwright";
|
|
28
|
+
function directProfileDir() {
|
|
29
|
+
return process.env.MCP_SCRAPER_BROWSER_PROFILE_DIR?.trim() || void 0;
|
|
30
|
+
}
|
|
31
|
+
function configuredProfileName(profile) {
|
|
32
|
+
return profile?.trim() || process.env.MCP_SCRAPER_BROWSER_PROFILE?.trim() || (directProfileDir() ? basename(directProfileDir()) : "default");
|
|
33
|
+
}
|
|
34
|
+
function launchExecutablePath(profile) {
|
|
35
|
+
const configured = process.env.MCP_SCRAPER_BROWSER_EXECUTABLE?.trim() || profile.browserExecutablePath || defaultChromeExecutablePath();
|
|
36
|
+
return configured && existsSync(configured) ? configured : void 0;
|
|
37
|
+
}
|
|
38
|
+
function ignoreDefaultArgsFor(executablePath) {
|
|
39
|
+
if (!executablePath) return void 0;
|
|
40
|
+
if (process.platform !== "darwin") return void 0;
|
|
41
|
+
if (!/Google Chrome\.app\/Contents\/MacOS\/Google Chrome$/.test(executablePath)) return void 0;
|
|
42
|
+
return ["--password-store=basic", "--use-mock-keychain"];
|
|
43
|
+
}
|
|
44
|
+
function normalizeKeyCombo(value) {
|
|
45
|
+
return value.split("+").map((part) => {
|
|
46
|
+
const trimmed = part.trim();
|
|
47
|
+
const lower = trimmed.toLowerCase();
|
|
48
|
+
if (lower === "ctrl" || lower === "control") return "Control";
|
|
49
|
+
if (lower === "cmd" || lower === "command" || lower === "meta") return "Meta";
|
|
50
|
+
if (lower === "option" || lower === "alt") return "Alt";
|
|
51
|
+
if (lower === "return") return "Enter";
|
|
52
|
+
if (lower === "esc") return "Escape";
|
|
53
|
+
if (trimmed.length === 1) return trimmed.toUpperCase();
|
|
54
|
+
return trimmed;
|
|
55
|
+
}).join("+");
|
|
56
|
+
}
|
|
57
|
+
function sessionSummary(session) {
|
|
58
|
+
return {
|
|
59
|
+
session_id: session.sessionId,
|
|
60
|
+
status: session.status,
|
|
61
|
+
label: session.label,
|
|
62
|
+
profile: session.profileName,
|
|
63
|
+
user_data_dir: session.userDataDir,
|
|
64
|
+
created_at: session.createdAt,
|
|
65
|
+
last_action_at: session.lastActionAt,
|
|
66
|
+
closed_at: session.closedAt,
|
|
67
|
+
watch_url: `local://browser-session/${session.sessionId}`,
|
|
68
|
+
url: session.status === "open" ? session.page.url() : null
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
var LocalBrowserAgentService = class {
|
|
72
|
+
contexts = /* @__PURE__ */ new Map();
|
|
73
|
+
sessions = /* @__PURE__ */ new Map();
|
|
74
|
+
async open(input) {
|
|
75
|
+
const profile = await this.resolveProfile(input.profile);
|
|
76
|
+
const contextState = await this.ensureContext(profile);
|
|
77
|
+
const pages = contextState.context.pages();
|
|
78
|
+
const page = pages.length === 1 && !this.hasOpenSessionForContext(contextState.key) ? pages[0] : await contextState.context.newPage();
|
|
79
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
80
|
+
const sessionId = `lbs_${randomUUID().replace(/-/g, "").slice(0, 20)}`;
|
|
81
|
+
const session = {
|
|
82
|
+
sessionId,
|
|
83
|
+
label: input.label ?? null,
|
|
84
|
+
page,
|
|
85
|
+
contextKey: contextState.key,
|
|
86
|
+
profileName: profile.name,
|
|
87
|
+
userDataDir: profile.userDataDir,
|
|
88
|
+
createdAt: now,
|
|
89
|
+
lastActionAt: now,
|
|
90
|
+
closedAt: null,
|
|
91
|
+
status: "open"
|
|
92
|
+
};
|
|
93
|
+
const timeoutSeconds = input.timeout_seconds ?? 600;
|
|
94
|
+
if (timeoutSeconds > 0) {
|
|
95
|
+
session.timeout = setTimeout(() => {
|
|
96
|
+
void this.close(sessionId);
|
|
97
|
+
}, timeoutSeconds * 1e3);
|
|
98
|
+
session.timeout.unref();
|
|
99
|
+
}
|
|
100
|
+
this.sessions.set(sessionId, session);
|
|
101
|
+
if (input.url) await this.goto(sessionId, input.url);
|
|
102
|
+
return {
|
|
103
|
+
session_id: sessionId,
|
|
104
|
+
watch_url: `local://browser-session/${sessionId}`,
|
|
105
|
+
live_view_url: null,
|
|
106
|
+
profile: profile.name,
|
|
107
|
+
user_data_dir: profile.userDataDir,
|
|
108
|
+
url: input.url ?? null,
|
|
109
|
+
mode: "local"
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
async screenshot(sessionId) {
|
|
113
|
+
const session = this.requireOpenSession(sessionId);
|
|
114
|
+
await this.waitForPage(session.page);
|
|
115
|
+
const image = await session.page.screenshot({ type: "png", fullPage: false });
|
|
116
|
+
const page = await this.read(sessionId);
|
|
117
|
+
return {
|
|
118
|
+
...page,
|
|
119
|
+
image_base64: image.toString("base64"),
|
|
120
|
+
mime_type: "image/png"
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
async read(sessionId) {
|
|
124
|
+
const session = this.requireOpenSession(sessionId);
|
|
125
|
+
await this.waitForPage(session.page);
|
|
126
|
+
this.touch(session);
|
|
127
|
+
const title = await session.page.title().catch(() => "");
|
|
128
|
+
const snapshot = await session.page.evaluate(() => {
|
|
129
|
+
const cssEscape = (value) => {
|
|
130
|
+
const escapeFn = globalThis.CSS?.escape;
|
|
131
|
+
return escapeFn ? escapeFn(value) : value.replace(/["\\#.:>+~[\]()]/g, "\\$&");
|
|
132
|
+
};
|
|
133
|
+
const textFor = (element) => {
|
|
134
|
+
const html = element;
|
|
135
|
+
return (element.getAttribute("aria-label") || element.getAttribute("title") || html.placeholder || html.value || html.innerText || element.textContent || "").replace(/\s+/g, " ").trim();
|
|
136
|
+
};
|
|
137
|
+
const selectorFor = (element) => {
|
|
138
|
+
const id = element.id;
|
|
139
|
+
if (id) return `#${cssEscape(id)}`;
|
|
140
|
+
const name = element.getAttribute("name");
|
|
141
|
+
const tag = element.tagName.toLowerCase();
|
|
142
|
+
if (name) return `${tag}[name="${name.replace(/"/g, '\\"')}"]`;
|
|
143
|
+
const parts = [];
|
|
144
|
+
let current = element;
|
|
145
|
+
while (current && current !== document.body && parts.length < 4) {
|
|
146
|
+
const currentTag = current.tagName.toLowerCase();
|
|
147
|
+
const parent = current.parentElement;
|
|
148
|
+
if (!parent) {
|
|
149
|
+
parts.unshift(currentTag);
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
const children = Array.from(parent.children);
|
|
153
|
+
const siblings = children.filter((child) => child.tagName === current.tagName);
|
|
154
|
+
const index = siblings.indexOf(current) + 1;
|
|
155
|
+
parts.unshift(siblings.length > 1 ? `${currentTag}:nth-of-type(${index})` : currentTag);
|
|
156
|
+
current = parent;
|
|
157
|
+
}
|
|
158
|
+
return parts.join(" > ") || tag;
|
|
159
|
+
};
|
|
160
|
+
const candidates = Array.from(document.querySelectorAll(
|
|
161
|
+
'a, button, input, textarea, select, summary, [role], [contenteditable="true"], [tabindex]:not([tabindex="-1"])'
|
|
162
|
+
));
|
|
163
|
+
const elements = candidates.map((element, index) => {
|
|
164
|
+
const rect = element.getBoundingClientRect();
|
|
165
|
+
const style = window.getComputedStyle(element);
|
|
166
|
+
const visible = rect.width > 0 && rect.height > 0 && style.display !== "none" && style.visibility !== "hidden" && Number(style.opacity || "1") > 0;
|
|
167
|
+
if (!visible) return null;
|
|
168
|
+
const label = textFor(element);
|
|
169
|
+
return {
|
|
170
|
+
index,
|
|
171
|
+
tag: element.tagName.toLowerCase(),
|
|
172
|
+
role: element.getAttribute("role") || null,
|
|
173
|
+
text: label.slice(0, 180),
|
|
174
|
+
selector: selectorFor(element),
|
|
175
|
+
x: Math.round(rect.left + rect.width / 2),
|
|
176
|
+
y: Math.round(rect.top + rect.height / 2),
|
|
177
|
+
left: Math.round(rect.left),
|
|
178
|
+
top: Math.round(rect.top),
|
|
179
|
+
width: Math.round(rect.width),
|
|
180
|
+
height: Math.round(rect.height)
|
|
181
|
+
};
|
|
182
|
+
}).filter(Boolean).slice(0, 120);
|
|
183
|
+
return {
|
|
184
|
+
text: (document.body?.innerText || "").replace(/\n{3,}/g, "\n\n").slice(0, 3e4),
|
|
185
|
+
elements,
|
|
186
|
+
viewport: {
|
|
187
|
+
width: window.innerWidth,
|
|
188
|
+
height: window.innerHeight,
|
|
189
|
+
devicePixelRatio: window.devicePixelRatio,
|
|
190
|
+
scrollX: window.scrollX,
|
|
191
|
+
scrollY: window.scrollY
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
});
|
|
195
|
+
return {
|
|
196
|
+
url: session.page.url(),
|
|
197
|
+
title,
|
|
198
|
+
text: snapshot.text,
|
|
199
|
+
elements: snapshot.elements,
|
|
200
|
+
viewport: snapshot.viewport
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
async locate(sessionId, targets) {
|
|
204
|
+
const session = this.requireOpenSession(sessionId);
|
|
205
|
+
this.touch(session);
|
|
206
|
+
const located = [];
|
|
207
|
+
for (const target of targets) {
|
|
208
|
+
try {
|
|
209
|
+
const locator = target.selector ? session.page.locator(target.selector) : session.page.getByText(target.text ?? "", { exact: target.match === "exact" });
|
|
210
|
+
const selected = locator.nth(target.index ?? 0);
|
|
211
|
+
await selected.waitFor({ state: "visible", timeout: 3e3 }).catch(() => void 0);
|
|
212
|
+
const box = await selected.boundingBox();
|
|
213
|
+
const text = await selected.textContent({ timeout: 1e3 }).catch(() => null);
|
|
214
|
+
located.push({
|
|
215
|
+
input: target,
|
|
216
|
+
found: Boolean(box),
|
|
217
|
+
element: box ? {
|
|
218
|
+
left: Math.round(box.x),
|
|
219
|
+
top: Math.round(box.y),
|
|
220
|
+
width: Math.round(box.width),
|
|
221
|
+
height: Math.round(box.height),
|
|
222
|
+
x: Math.round(box.x + box.width / 2),
|
|
223
|
+
y: Math.round(box.y + box.height / 2),
|
|
224
|
+
text: (text ?? "").replace(/\s+/g, " ").trim().slice(0, 220)
|
|
225
|
+
} : null
|
|
226
|
+
});
|
|
227
|
+
} catch (err) {
|
|
228
|
+
located.push({
|
|
229
|
+
input: target,
|
|
230
|
+
found: false,
|
|
231
|
+
element: null,
|
|
232
|
+
error: err instanceof Error ? err.message : String(err)
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
const read = await this.read(sessionId);
|
|
237
|
+
return {
|
|
238
|
+
url: read.url,
|
|
239
|
+
title: read.title,
|
|
240
|
+
viewport: read.viewport,
|
|
241
|
+
replay: null,
|
|
242
|
+
targets: located
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
async goto(sessionId, url) {
|
|
246
|
+
const session = this.requireOpenSession(sessionId);
|
|
247
|
+
this.touch(session);
|
|
248
|
+
const response = await session.page.goto(url, { waitUntil: "domcontentloaded", timeout: 45e3 }).catch((err) => {
|
|
249
|
+
throw new Error(err instanceof Error ? err.message : String(err));
|
|
250
|
+
});
|
|
251
|
+
await this.waitForPage(session.page);
|
|
252
|
+
return {
|
|
253
|
+
ok: true,
|
|
254
|
+
url: session.page.url(),
|
|
255
|
+
status: response?.status() ?? null,
|
|
256
|
+
mode: "local"
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
async click(sessionId, input) {
|
|
260
|
+
const session = this.requireOpenSession(sessionId);
|
|
261
|
+
this.touch(session);
|
|
262
|
+
await session.page.mouse.click(input.x, input.y, {
|
|
263
|
+
button: input.button ?? "left",
|
|
264
|
+
clickCount: input.num_clicks ?? 1
|
|
265
|
+
});
|
|
266
|
+
await this.waitForPage(session.page);
|
|
267
|
+
return { ok: true, mode: "local", url: session.page.url() };
|
|
268
|
+
}
|
|
269
|
+
async type(sessionId, input) {
|
|
270
|
+
const session = this.requireOpenSession(sessionId);
|
|
271
|
+
this.touch(session);
|
|
272
|
+
await session.page.keyboard.type(input.text, { delay: input.delay });
|
|
273
|
+
return { ok: true, mode: "local", url: session.page.url() };
|
|
274
|
+
}
|
|
275
|
+
async scroll(sessionId, input) {
|
|
276
|
+
const session = this.requireOpenSession(sessionId);
|
|
277
|
+
this.touch(session);
|
|
278
|
+
if (typeof input.x === "number" && typeof input.y === "number") {
|
|
279
|
+
await session.page.mouse.move(input.x, input.y);
|
|
280
|
+
}
|
|
281
|
+
await session.page.mouse.wheel((input.delta_x ?? 0) * 120, (input.delta_y ?? 5) * 120);
|
|
282
|
+
await session.page.waitForTimeout(150).catch(() => void 0);
|
|
283
|
+
return { ok: true, mode: "local", url: session.page.url() };
|
|
284
|
+
}
|
|
285
|
+
async press(sessionId, input) {
|
|
286
|
+
const session = this.requireOpenSession(sessionId);
|
|
287
|
+
this.touch(session);
|
|
288
|
+
for (const key of input.keys) {
|
|
289
|
+
await session.page.keyboard.press(normalizeKeyCombo(key));
|
|
290
|
+
}
|
|
291
|
+
await this.waitForPage(session.page);
|
|
292
|
+
return { ok: true, mode: "local", url: session.page.url() };
|
|
293
|
+
}
|
|
294
|
+
async close(sessionId) {
|
|
295
|
+
const session = this.sessions.get(sessionId);
|
|
296
|
+
if (!session || session.status === "closed") {
|
|
297
|
+
return { ok: true, closed: true, already_closed: true, mode: "local" };
|
|
298
|
+
}
|
|
299
|
+
if (session.timeout) clearTimeout(session.timeout);
|
|
300
|
+
session.status = "closed";
|
|
301
|
+
session.closedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
302
|
+
await session.page.close().catch(() => void 0);
|
|
303
|
+
await this.closeContextIfUnused(session.contextKey);
|
|
304
|
+
return { ok: true, closed: true, mode: "local" };
|
|
305
|
+
}
|
|
306
|
+
list(includeClosed = false) {
|
|
307
|
+
return Array.from(this.sessions.values()).filter((session) => includeClosed || session.status === "open").map(sessionSummary);
|
|
308
|
+
}
|
|
309
|
+
async resolveProfile(profile) {
|
|
310
|
+
const directDir = directProfileDir();
|
|
311
|
+
const requestedName = configuredProfileName(profile);
|
|
312
|
+
if (directDir && (!profile || requestedName === process.env.MCP_SCRAPER_BROWSER_PROFILE?.trim())) {
|
|
313
|
+
return {
|
|
314
|
+
name: requestedName,
|
|
315
|
+
userDataDir: directDir,
|
|
316
|
+
browserExecutablePath: process.env.MCP_SCRAPER_BROWSER_EXECUTABLE?.trim() || defaultChromeExecutablePath() || null,
|
|
317
|
+
manifest: null
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
const manifest = await loadImportedBrowserProfile(requestedName);
|
|
321
|
+
return {
|
|
322
|
+
name: manifest.name,
|
|
323
|
+
userDataDir: manifest.userDataDir,
|
|
324
|
+
browserExecutablePath: manifest.browserExecutablePath,
|
|
325
|
+
manifest
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
async ensureContext(profile) {
|
|
329
|
+
const existing = this.contexts.get(profile.userDataDir);
|
|
330
|
+
if (existing) return existing;
|
|
331
|
+
const executablePath = launchExecutablePath(profile);
|
|
332
|
+
const context = await chromium.launchPersistentContext(profile.userDataDir, {
|
|
333
|
+
headless: false,
|
|
334
|
+
viewport: { width: 1440, height: 900 },
|
|
335
|
+
acceptDownloads: true,
|
|
336
|
+
...executablePath ? { executablePath } : {},
|
|
337
|
+
...ignoreDefaultArgsFor(executablePath) ? { ignoreDefaultArgs: ignoreDefaultArgsFor(executablePath) } : {}
|
|
338
|
+
});
|
|
339
|
+
const state = { key: profile.userDataDir, context, profile };
|
|
340
|
+
context.on("close", () => {
|
|
341
|
+
this.contexts.delete(profile.userDataDir);
|
|
342
|
+
const closedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
343
|
+
for (const session of this.sessions.values()) {
|
|
344
|
+
if (session.contextKey === profile.userDataDir && session.status === "open") {
|
|
345
|
+
session.status = "closed";
|
|
346
|
+
session.closedAt = closedAt;
|
|
347
|
+
if (session.timeout) clearTimeout(session.timeout);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
});
|
|
351
|
+
this.contexts.set(profile.userDataDir, state);
|
|
352
|
+
return state;
|
|
353
|
+
}
|
|
354
|
+
requireOpenSession(sessionId) {
|
|
355
|
+
const session = this.sessions.get(sessionId);
|
|
356
|
+
if (!session || session.status !== "open") throw new Error(`Local browser session not found or closed: ${sessionId}`);
|
|
357
|
+
return session;
|
|
358
|
+
}
|
|
359
|
+
touch(session) {
|
|
360
|
+
session.lastActionAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
361
|
+
}
|
|
362
|
+
hasOpenSessionForContext(contextKey) {
|
|
363
|
+
return Array.from(this.sessions.values()).some((session) => session.contextKey === contextKey && session.status === "open");
|
|
364
|
+
}
|
|
365
|
+
async closeContextIfUnused(contextKey) {
|
|
366
|
+
if (this.hasOpenSessionForContext(contextKey)) return;
|
|
367
|
+
const state = this.contexts.get(contextKey);
|
|
368
|
+
if (!state) return;
|
|
369
|
+
this.contexts.delete(contextKey);
|
|
370
|
+
await state.context.close().catch(() => void 0);
|
|
371
|
+
}
|
|
372
|
+
async waitForPage(page) {
|
|
373
|
+
await page.waitForLoadState("domcontentloaded", { timeout: 5e3 }).catch(() => void 0);
|
|
374
|
+
await page.waitForTimeout(200).catch(() => void 0);
|
|
375
|
+
}
|
|
376
|
+
};
|
|
377
|
+
|
|
11
378
|
// src/mcp/browser-agent-tool-schemas.ts
|
|
12
379
|
import { z } from "zod";
|
|
13
380
|
var NullableString = z.string().nullable();
|
|
@@ -26,8 +393,35 @@ var BrowserOpenInputSchema = {
|
|
|
26
393
|
label: z.string().optional().describe("Optional human label for this session, shown in the watch console."),
|
|
27
394
|
url: z.string().url().optional().describe("Optional URL to navigate to immediately after opening."),
|
|
28
395
|
profile: z.string().optional().describe("Optional saved profile name to load a logged-in session for a site."),
|
|
396
|
+
save_profile_changes: z.boolean().optional().describe("Persist cookies and local storage back to the named profile when the session is closed. Use this for profile setup or intentional auth refreshes; avoid parallel sessions writing to the same profile."),
|
|
29
397
|
timeout_seconds: z.number().int().min(60).max(259200).optional().describe("How long the session may live before auto-termination. Defaults to 600. The browser idles into a zero-cost standby between actions, so a longer timeout is cheap.")
|
|
30
398
|
};
|
|
399
|
+
var BrowserProfileListInputSchema = {
|
|
400
|
+
email: z.string().optional().describe("Optional Chrome account email or displayed profile name to filter for, for example seo@example.com."),
|
|
401
|
+
user_data_dir: z.string().optional().describe("Optional Chrome user data directory. Defaults to the current OS Chrome profile root.")
|
|
402
|
+
};
|
|
403
|
+
var BrowserProfileOnboardInputSchema = {
|
|
404
|
+
email: z.string().optional().describe("Optional local Chrome account email to find and convert into a suggested Kernel profile name."),
|
|
405
|
+
profile: z.string().optional().describe("Optional Kernel browser profile name to create/load. If omitted, email is used to derive one."),
|
|
406
|
+
user_data_dir: z.string().optional().describe("Optional Chrome user data directory for email lookup. Defaults to the current OS Chrome profile root."),
|
|
407
|
+
url: z.string().url().optional().describe("Setup URL to open after creating the browser. Defaults to https://accounts.google.com/."),
|
|
408
|
+
label: z.string().optional().describe("Optional human label for this setup session."),
|
|
409
|
+
timeout_seconds: z.number().int().min(60).max(259200).optional().describe("How long the setup session may live before auto-termination. Defaults to 600.")
|
|
410
|
+
};
|
|
411
|
+
var BrowserProfileImportInputSchema = {
|
|
412
|
+
email: z.string().optional().describe("Optional Chrome account email or displayed profile name to select from Chrome Local State."),
|
|
413
|
+
name: z.string().optional().describe("Name for the managed MCP Scraper browser profile. Defaults to a slug derived from the selected Chrome account."),
|
|
414
|
+
chrome_profile_directory: z.string().optional().describe("Optional Chrome profile directory to import, for example Default or Profile 1."),
|
|
415
|
+
user_data_dir: z.string().optional().describe("Optional source Chrome user data directory. Defaults to the current OS Chrome profile root."),
|
|
416
|
+
output_dir: z.string().optional().describe("Optional managed profile base directory. Defaults to ~/.mcp-scraper/browser-profiles."),
|
|
417
|
+
browser_executable_path: z.string().optional().describe("Optional Chrome executable path for local browser launches. Defaults to the installed Google Chrome path when found."),
|
|
418
|
+
overwrite: z.boolean().default(false).describe("Overwrite an existing managed profile clone with the same name.")
|
|
419
|
+
};
|
|
420
|
+
var BrowserProfileSyncInputSchema = {
|
|
421
|
+
name: z.string().describe("Managed MCP Scraper browser profile name to refresh from its recorded source Chrome profile."),
|
|
422
|
+
output_dir: z.string().optional().describe("Optional managed profile base directory. Defaults to ~/.mcp-scraper/browser-profiles."),
|
|
423
|
+
browser_executable_path: z.string().optional().describe("Optional Chrome executable path to store in the refreshed profile manifest.")
|
|
424
|
+
};
|
|
31
425
|
var BrowserSessionInputSchema = {
|
|
32
426
|
session_id: z.string().describe("The session id returned by browser_open or browser_list_sessions. Use only a returned session_id; do not construct one yourself.")
|
|
33
427
|
};
|
|
@@ -132,6 +526,43 @@ var BrowserOpenOutputSchema = {
|
|
|
132
526
|
hint: z.string(),
|
|
133
527
|
raw: BrowserRawObject.optional()
|
|
134
528
|
};
|
|
529
|
+
var BrowserProfileListOutputSchema = {
|
|
530
|
+
ok: z.boolean(),
|
|
531
|
+
tool: z.literal("browser_profile_list"),
|
|
532
|
+
session_id: z.null(),
|
|
533
|
+
profiles: z.array(BrowserRawObject),
|
|
534
|
+
count: z.number().int().min(0),
|
|
535
|
+
hint: z.string()
|
|
536
|
+
};
|
|
537
|
+
var BrowserProfileOnboardOutputSchema = {
|
|
538
|
+
ok: z.boolean(),
|
|
539
|
+
tool: z.literal("browser_profile_onboard"),
|
|
540
|
+
session_id: z.string(),
|
|
541
|
+
watch_url: z.string(),
|
|
542
|
+
live_view_url: NullableString,
|
|
543
|
+
profile: z.string(),
|
|
544
|
+
setup_url: z.string(),
|
|
545
|
+
chrome_profile: BrowserRawObject.nullable(),
|
|
546
|
+
next_steps: z.array(z.string()),
|
|
547
|
+
raw: BrowserRawObject.optional()
|
|
548
|
+
};
|
|
549
|
+
var BrowserProfileImportOutputSchema = {
|
|
550
|
+
ok: z.boolean(),
|
|
551
|
+
tool: z.literal("browser_profile_import"),
|
|
552
|
+
session_id: z.null(),
|
|
553
|
+
profile: BrowserRawObject,
|
|
554
|
+
source_profile: BrowserRawObject,
|
|
555
|
+
next_steps: z.array(z.string()),
|
|
556
|
+
hint: z.string()
|
|
557
|
+
};
|
|
558
|
+
var BrowserProfileSyncOutputSchema = {
|
|
559
|
+
ok: z.boolean(),
|
|
560
|
+
tool: z.literal("browser_profile_sync"),
|
|
561
|
+
session_id: z.null(),
|
|
562
|
+
profile: BrowserRawObject,
|
|
563
|
+
source_profile: BrowserRawObject,
|
|
564
|
+
hint: z.string()
|
|
565
|
+
};
|
|
135
566
|
var BrowserScreenshotOutputSchema = {
|
|
136
567
|
...BrowserBaseOutput,
|
|
137
568
|
tool: z.literal("browser_screenshot"),
|
|
@@ -572,6 +1003,7 @@ function registerBrowserAgentMcpTools(server, opts) {
|
|
|
572
1003
|
const baseUrl = opts.baseUrl.replace(/\/$/, "");
|
|
573
1004
|
const consoleBase = (opts.consoleBaseUrl ?? opts.baseUrl).replace(/\/$/, "");
|
|
574
1005
|
const timeoutMs = opts.timeoutMs ?? 9e4;
|
|
1006
|
+
const localBrowser = new LocalBrowserAgentService();
|
|
575
1007
|
async function req(method, path, body) {
|
|
576
1008
|
try {
|
|
577
1009
|
const res = await fetch(`${baseUrl}${path}`, {
|
|
@@ -623,19 +1055,195 @@ function registerBrowserAgentMcpTools(server, opts) {
|
|
|
623
1055
|
idempotentHint: false,
|
|
624
1056
|
openWorldHint: true
|
|
625
1057
|
});
|
|
1058
|
+
function selectChromeProfile(matches, email) {
|
|
1059
|
+
const wanted = email?.trim().toLowerCase();
|
|
1060
|
+
if (!wanted) return matches[0] ?? null;
|
|
1061
|
+
return matches.find((profile) => profile.email.toLowerCase() === wanted) ?? matches[0] ?? null;
|
|
1062
|
+
}
|
|
1063
|
+
function localReplayUnsupported(tool, sessionId = null, replayId = null) {
|
|
1064
|
+
return errorResult(tool, {
|
|
1065
|
+
error: "Local browser mode does not support replay recording yet. Use hosted mode for browser_replay_* tools.",
|
|
1066
|
+
mode: "local"
|
|
1067
|
+
}, sessionId, replayId);
|
|
1068
|
+
}
|
|
1069
|
+
server.registerTool(
|
|
1070
|
+
"browser_profile_list",
|
|
1071
|
+
{
|
|
1072
|
+
title: "List Local Chrome Profiles",
|
|
1073
|
+
description: "List local Chrome profile metadata visible to this MCP server: account email, Chrome profile directory, local path, and a suggested managed browser profile name. This reads Chrome Local State only; it does not read cookies, passwords, browsing history, or copy local browser state. Use browser_profile_import when the user wants MCP Scraper to clone a local Chrome profile for local browser mode.",
|
|
1074
|
+
inputSchema: BrowserProfileListInputSchema,
|
|
1075
|
+
outputSchema: BrowserProfileListOutputSchema,
|
|
1076
|
+
annotations: annotations("List Local Chrome Profiles", true)
|
|
1077
|
+
},
|
|
1078
|
+
async (input) => {
|
|
1079
|
+
try {
|
|
1080
|
+
const profiles = await listLocalChromeProfiles(input.user_data_dir, input.email);
|
|
1081
|
+
return structuredResult({
|
|
1082
|
+
ok: true,
|
|
1083
|
+
tool: "browser_profile_list",
|
|
1084
|
+
session_id: null,
|
|
1085
|
+
profiles,
|
|
1086
|
+
count: profiles.length,
|
|
1087
|
+
hint: "Use browser_profile_import to clone one profile for local mode. Use browser_profile_onboard only for hosted Kernel profile setup."
|
|
1088
|
+
});
|
|
1089
|
+
} catch (err) {
|
|
1090
|
+
return errorResult("browser_profile_list", { error: err instanceof Error ? err.message : String(err) });
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
);
|
|
1094
|
+
server.registerTool(
|
|
1095
|
+
"browser_profile_import",
|
|
1096
|
+
{
|
|
1097
|
+
title: "Import Local Chrome Profile",
|
|
1098
|
+
description: "Clone one local Chrome profile into MCP Scraper managed storage for local browser mode. This copies browser state files such as cookies, history, local storage, session storage, and password database metadata into ~/.mcp-scraper/browser-profiles by default; it skips cache and lock files and never prints cookie or password values. Use this when the user wants browser_open to run locally with their existing logged-in Chrome state.",
|
|
1099
|
+
inputSchema: BrowserProfileImportInputSchema,
|
|
1100
|
+
outputSchema: BrowserProfileImportOutputSchema,
|
|
1101
|
+
annotations: { title: "Import Local Chrome Profile", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }
|
|
1102
|
+
},
|
|
1103
|
+
async (input) => {
|
|
1104
|
+
try {
|
|
1105
|
+
const result = await importChromeProfile({
|
|
1106
|
+
email: input.email,
|
|
1107
|
+
name: input.name,
|
|
1108
|
+
chromeProfileDirectory: input.chrome_profile_directory,
|
|
1109
|
+
sourceUserDataDir: input.user_data_dir,
|
|
1110
|
+
outputDir: input.output_dir,
|
|
1111
|
+
overwrite: input.overwrite,
|
|
1112
|
+
browserExecutablePath: input.browser_executable_path
|
|
1113
|
+
});
|
|
1114
|
+
return structuredResult({
|
|
1115
|
+
ok: true,
|
|
1116
|
+
tool: "browser_profile_import",
|
|
1117
|
+
session_id: null,
|
|
1118
|
+
profile: result.profile,
|
|
1119
|
+
source_profile: result.sourceProfile,
|
|
1120
|
+
next_steps: [
|
|
1121
|
+
`Set MCP_SCRAPER_BROWSER_MODE=local in the MCP server config.`,
|
|
1122
|
+
`Set MCP_SCRAPER_BROWSER_PROFILE=${result.profile.name}.`,
|
|
1123
|
+
"Restart the MCP client so it starts the local browser mode process.",
|
|
1124
|
+
"Call browser_open with the same MCP server; it will open a local Chrome window using the cloned profile."
|
|
1125
|
+
],
|
|
1126
|
+
hint: "The clone is local to this machine. Re-run browser_profile_sync after logging into new sites in normal Chrome."
|
|
1127
|
+
});
|
|
1128
|
+
} catch (err) {
|
|
1129
|
+
return errorResult("browser_profile_import", { error: err instanceof Error ? err.message : String(err) });
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
);
|
|
1133
|
+
server.registerTool(
|
|
1134
|
+
"browser_profile_sync",
|
|
1135
|
+
{
|
|
1136
|
+
title: "Sync Local Chrome Profile",
|
|
1137
|
+
description: "Refresh an existing MCP Scraper managed local browser profile from the source Chrome profile recorded in its manifest. This overwrites the managed clone, copies browser state files again, skips cache and lock files, and never prints cookie or password values.",
|
|
1138
|
+
inputSchema: BrowserProfileSyncInputSchema,
|
|
1139
|
+
outputSchema: BrowserProfileSyncOutputSchema,
|
|
1140
|
+
annotations: { title: "Sync Local Chrome Profile", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }
|
|
1141
|
+
},
|
|
1142
|
+
async (input) => {
|
|
1143
|
+
try {
|
|
1144
|
+
const result = await syncImportedChromeProfile(input.name, {
|
|
1145
|
+
outputDir: input.output_dir,
|
|
1146
|
+
browserExecutablePath: input.browser_executable_path
|
|
1147
|
+
});
|
|
1148
|
+
return structuredResult({
|
|
1149
|
+
ok: true,
|
|
1150
|
+
tool: "browser_profile_sync",
|
|
1151
|
+
session_id: null,
|
|
1152
|
+
profile: result.profile,
|
|
1153
|
+
source_profile: result.sourceProfile,
|
|
1154
|
+
hint: `Refreshed ${result.profile.name}. Restart local browser sessions that were using this profile.`
|
|
1155
|
+
});
|
|
1156
|
+
} catch (err) {
|
|
1157
|
+
return errorResult("browser_profile_sync", { error: err instanceof Error ? err.message : String(err) });
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
);
|
|
1161
|
+
server.registerTool(
|
|
1162
|
+
"browser_profile_onboard",
|
|
1163
|
+
{
|
|
1164
|
+
title: "Onboard Hosted Browser Profile",
|
|
1165
|
+
description: "Create or load a saved hosted Kernel browser profile and open a live setup browser with profile saving enabled. Use this only for hosted browser mode. If email is provided, the tool reads local Chrome profile metadata only to suggest the Kernel profile name; it does not import local cookies. For local browser mode with real Chrome state, use browser_profile_import instead.",
|
|
1166
|
+
inputSchema: BrowserProfileOnboardInputSchema,
|
|
1167
|
+
outputSchema: BrowserProfileOnboardOutputSchema,
|
|
1168
|
+
annotations: annotations("Onboard Hosted Browser Profile")
|
|
1169
|
+
},
|
|
1170
|
+
async (input) => {
|
|
1171
|
+
let chromeProfile = null;
|
|
1172
|
+
if (input.email) {
|
|
1173
|
+
try {
|
|
1174
|
+
chromeProfile = selectChromeProfile(await listLocalChromeProfiles(input.user_data_dir, input.email), input.email);
|
|
1175
|
+
} catch (err) {
|
|
1176
|
+
return errorResult("browser_profile_onboard", { error: err instanceof Error ? err.message : String(err) });
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
const profile = input.profile?.trim() || chromeProfile?.recommendedKernelProfileName || browserServiceProfileName();
|
|
1180
|
+
if (!profile) {
|
|
1181
|
+
return errorResult("browser_profile_onboard", {
|
|
1182
|
+
error: "profile is required when no email match or BROWSER_AGENT_PROFILE_NAME is available"
|
|
1183
|
+
});
|
|
1184
|
+
}
|
|
1185
|
+
const setupUrl = input.url ?? "https://accounts.google.com/";
|
|
1186
|
+
const open = await req("POST", "/agent/sessions", {
|
|
1187
|
+
label: input.label ?? `Profile setup: ${profile}`,
|
|
1188
|
+
profile,
|
|
1189
|
+
save_profile_changes: true,
|
|
1190
|
+
timeout_seconds: input.timeout_seconds
|
|
1191
|
+
});
|
|
1192
|
+
if (!open.ok) return errorResult("browser_profile_onboard", open.data);
|
|
1193
|
+
const session = open.data;
|
|
1194
|
+
const goto = await req("POST", `/agent/sessions/${session.session_id}/goto`, { url: setupUrl });
|
|
1195
|
+
if (!goto.ok) return errorResult("browser_profile_onboard", goto.data, session.session_id);
|
|
1196
|
+
return structuredResult({
|
|
1197
|
+
ok: true,
|
|
1198
|
+
tool: "browser_profile_onboard",
|
|
1199
|
+
session_id: session.session_id,
|
|
1200
|
+
watch_url: `${consoleBase}/console/${session.session_id}`,
|
|
1201
|
+
live_view_url: session.live_view_url ?? null,
|
|
1202
|
+
profile,
|
|
1203
|
+
setup_url: setupUrl,
|
|
1204
|
+
chrome_profile: chromeProfile,
|
|
1205
|
+
next_steps: [
|
|
1206
|
+
"Open the watch_url and complete the login manually.",
|
|
1207
|
+
"After login is complete, call browser_close with this session_id to persist cookies and local storage to the Kernel profile.",
|
|
1208
|
+
"Use BROWSER_AGENT_PROFILE_NAME or browser_open.profile with this profile name for future authenticated sessions."
|
|
1209
|
+
],
|
|
1210
|
+
raw: session
|
|
1211
|
+
});
|
|
1212
|
+
}
|
|
1213
|
+
);
|
|
626
1214
|
server.registerTool(
|
|
627
1215
|
"browser_open",
|
|
628
1216
|
{
|
|
629
1217
|
title: "Open Browser Session",
|
|
630
|
-
description: "Open a
|
|
1218
|
+
description: "Open a browser you can drive. By default this creates a hosted cloud browser with a watch_url; when MCP_SCRAPER_BROWSER_MODE=local it opens a local Google Chrome window against an imported MCP Scraper Chrome profile. Returns a session_id used by all other browser_* tools. In hosted mode, anti-bot stealth and automatic CAPTCHA/Cloudflare solving are on by default. Call browser_close when done.",
|
|
631
1219
|
inputSchema: BrowserOpenInputSchema,
|
|
632
1220
|
outputSchema: BrowserOpenOutputSchema,
|
|
633
1221
|
annotations: annotations("Open Browser Session")
|
|
634
1222
|
},
|
|
635
1223
|
async (input) => {
|
|
1224
|
+
if (localBrowserModeEnabled()) {
|
|
1225
|
+
try {
|
|
1226
|
+
const session2 = await localBrowser.open(input);
|
|
1227
|
+
return structuredResult({
|
|
1228
|
+
ok: true,
|
|
1229
|
+
tool: "browser_open",
|
|
1230
|
+
session_id: session2.session_id,
|
|
1231
|
+
watch_url: session2.watch_url,
|
|
1232
|
+
live_view_url: null,
|
|
1233
|
+
url: input.url ?? null,
|
|
1234
|
+
hint: "Local Chrome is open on this machine. Call browser_screenshot to see the page and click by x,y coordinates.",
|
|
1235
|
+
raw: session2
|
|
1236
|
+
});
|
|
1237
|
+
} catch (err) {
|
|
1238
|
+
return errorResult("browser_open", { error: err instanceof Error ? err.message : String(err), mode: "local" });
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
const profile = input.profile ?? browserServiceProfileName();
|
|
1242
|
+
const saveProfileChanges = input.save_profile_changes ?? browserServiceProfileSaveChanges();
|
|
636
1243
|
const open = await req("POST", "/agent/sessions", {
|
|
637
1244
|
label: input.label,
|
|
638
|
-
profile:
|
|
1245
|
+
...profile ? { profile } : {},
|
|
1246
|
+
...profile && typeof saveProfileChanges === "boolean" ? { save_profile_changes: saveProfileChanges } : {},
|
|
639
1247
|
timeout_seconds: input.timeout_seconds
|
|
640
1248
|
});
|
|
641
1249
|
if (!open.ok) return errorResult("browser_open", open.data);
|
|
@@ -665,6 +1273,27 @@ function registerBrowserAgentMcpTools(server, opts) {
|
|
|
665
1273
|
annotations: annotations("See Page", true)
|
|
666
1274
|
},
|
|
667
1275
|
async (input) => {
|
|
1276
|
+
if (localBrowserModeEnabled()) {
|
|
1277
|
+
try {
|
|
1278
|
+
const res2 = await localBrowser.screenshot(input.session_id);
|
|
1279
|
+
const content2 = [];
|
|
1280
|
+
if (res2.image_base64) content2.push({ type: "image", data: res2.image_base64, mimeType: res2.mime_type });
|
|
1281
|
+
const structured2 = {
|
|
1282
|
+
ok: true,
|
|
1283
|
+
tool: "browser_screenshot",
|
|
1284
|
+
session_id: input.session_id,
|
|
1285
|
+
url: res2.url ?? null,
|
|
1286
|
+
title: res2.title ?? null,
|
|
1287
|
+
text: res2.text,
|
|
1288
|
+
elements: res2.elements,
|
|
1289
|
+
screenshot: { mime_type: res2.mime_type, inline: true }
|
|
1290
|
+
};
|
|
1291
|
+
content2.push({ type: "text", text: JSON.stringify(structured2) });
|
|
1292
|
+
return { content: content2, structuredContent: structured2 };
|
|
1293
|
+
} catch (err) {
|
|
1294
|
+
return errorResult("browser_screenshot", { error: err instanceof Error ? err.message : String(err), mode: "local" }, input.session_id);
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
668
1297
|
const res = await req("POST", `/agent/sessions/${input.session_id}/screenshot`);
|
|
669
1298
|
if (!res.ok) return errorResult("browser_screenshot", res.data, input.session_id);
|
|
670
1299
|
const { image_base64, mime_type, url, title, elements, text } = res.data;
|
|
@@ -697,6 +1326,23 @@ function registerBrowserAgentMcpTools(server, opts) {
|
|
|
697
1326
|
annotations: annotations("Read Page", true)
|
|
698
1327
|
},
|
|
699
1328
|
async (input) => {
|
|
1329
|
+
if (localBrowserModeEnabled()) {
|
|
1330
|
+
try {
|
|
1331
|
+
const res2 = await localBrowser.read(input.session_id);
|
|
1332
|
+
return structuredResult({
|
|
1333
|
+
ok: true,
|
|
1334
|
+
tool: "browser_read",
|
|
1335
|
+
session_id: input.session_id,
|
|
1336
|
+
url: res2.url ?? null,
|
|
1337
|
+
title: res2.title ?? null,
|
|
1338
|
+
text: res2.text,
|
|
1339
|
+
elements: res2.elements,
|
|
1340
|
+
raw: res2
|
|
1341
|
+
});
|
|
1342
|
+
} catch (err) {
|
|
1343
|
+
return errorResult("browser_read", { error: err instanceof Error ? err.message : String(err), mode: "local" }, input.session_id);
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
700
1346
|
const res = await req("POST", `/agent/sessions/${input.session_id}/read`);
|
|
701
1347
|
if (!res.ok) return errorResult("browser_read", res.data, input.session_id);
|
|
702
1348
|
return structuredResult({
|
|
@@ -721,6 +1367,24 @@ function registerBrowserAgentMcpTools(server, opts) {
|
|
|
721
1367
|
annotations: annotations("Locate DOM Targets", true)
|
|
722
1368
|
},
|
|
723
1369
|
async (input) => {
|
|
1370
|
+
if (localBrowserModeEnabled()) {
|
|
1371
|
+
try {
|
|
1372
|
+
const res2 = await localBrowser.locate(input.session_id, input.targets);
|
|
1373
|
+
return structuredResult({
|
|
1374
|
+
ok: true,
|
|
1375
|
+
tool: "browser_locate",
|
|
1376
|
+
session_id: input.session_id,
|
|
1377
|
+
url: res2.url ?? null,
|
|
1378
|
+
title: res2.title ?? null,
|
|
1379
|
+
viewport: res2.viewport ?? null,
|
|
1380
|
+
replay: null,
|
|
1381
|
+
targets: Array.isArray(res2.targets) ? res2.targets : [],
|
|
1382
|
+
raw: res2
|
|
1383
|
+
});
|
|
1384
|
+
} catch (err) {
|
|
1385
|
+
return errorResult("browser_locate", { error: err instanceof Error ? err.message : String(err), mode: "local" }, input.session_id);
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
724
1388
|
const res = await req("POST", `/agent/sessions/${input.session_id}/locate`, { targets: input.targets });
|
|
725
1389
|
if (!res.ok) return errorResult("browser_locate", res.data, input.session_id);
|
|
726
1390
|
return structuredResult({
|
|
@@ -746,6 +1410,14 @@ function registerBrowserAgentMcpTools(server, opts) {
|
|
|
746
1410
|
annotations: annotations("Navigate To URL")
|
|
747
1411
|
},
|
|
748
1412
|
async (input) => {
|
|
1413
|
+
if (localBrowserModeEnabled()) {
|
|
1414
|
+
try {
|
|
1415
|
+
const res2 = await localBrowser.goto(input.session_id, input.url);
|
|
1416
|
+
return actionResult("browser_goto", input.session_id, true, res2, "browser_screenshot");
|
|
1417
|
+
} catch (err) {
|
|
1418
|
+
return errorResult("browser_goto", { error: err instanceof Error ? err.message : String(err), mode: "local" }, input.session_id);
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
749
1421
|
const res = await req("POST", `/agent/sessions/${input.session_id}/goto`, { url: input.url });
|
|
750
1422
|
return actionResult("browser_goto", input.session_id, res.ok, res.data, "browser_screenshot");
|
|
751
1423
|
}
|
|
@@ -760,6 +1432,14 @@ function registerBrowserAgentMcpTools(server, opts) {
|
|
|
760
1432
|
annotations: annotations("Click")
|
|
761
1433
|
},
|
|
762
1434
|
async (input) => {
|
|
1435
|
+
if (localBrowserModeEnabled()) {
|
|
1436
|
+
try {
|
|
1437
|
+
const res2 = await localBrowser.click(input.session_id, input);
|
|
1438
|
+
return actionResult("browser_click", input.session_id, true, res2, "browser_screenshot");
|
|
1439
|
+
} catch (err) {
|
|
1440
|
+
return errorResult("browser_click", { error: err instanceof Error ? err.message : String(err), mode: "local" }, input.session_id);
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
763
1443
|
const res = await req("POST", `/agent/sessions/${input.session_id}/click`, {
|
|
764
1444
|
x: input.x,
|
|
765
1445
|
y: input.y,
|
|
@@ -779,6 +1459,14 @@ function registerBrowserAgentMcpTools(server, opts) {
|
|
|
779
1459
|
annotations: annotations("Type Text")
|
|
780
1460
|
},
|
|
781
1461
|
async (input) => {
|
|
1462
|
+
if (localBrowserModeEnabled()) {
|
|
1463
|
+
try {
|
|
1464
|
+
const res2 = await localBrowser.type(input.session_id, input);
|
|
1465
|
+
return actionResult("browser_type", input.session_id, true, res2, "browser_screenshot");
|
|
1466
|
+
} catch (err) {
|
|
1467
|
+
return errorResult("browser_type", { error: err instanceof Error ? err.message : String(err), mode: "local" }, input.session_id);
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
782
1470
|
const res = await req("POST", `/agent/sessions/${input.session_id}/type`, { text: input.text, delay: input.delay });
|
|
783
1471
|
return actionResult("browser_type", input.session_id, res.ok, res.data, "browser_screenshot");
|
|
784
1472
|
}
|
|
@@ -793,6 +1481,14 @@ function registerBrowserAgentMcpTools(server, opts) {
|
|
|
793
1481
|
annotations: annotations("Scroll")
|
|
794
1482
|
},
|
|
795
1483
|
async (input) => {
|
|
1484
|
+
if (localBrowserModeEnabled()) {
|
|
1485
|
+
try {
|
|
1486
|
+
const res2 = await localBrowser.scroll(input.session_id, input);
|
|
1487
|
+
return actionResult("browser_scroll", input.session_id, true, res2, "browser_screenshot");
|
|
1488
|
+
} catch (err) {
|
|
1489
|
+
return errorResult("browser_scroll", { error: err instanceof Error ? err.message : String(err), mode: "local" }, input.session_id);
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
796
1492
|
const res = await req("POST", `/agent/sessions/${input.session_id}/scroll`, {
|
|
797
1493
|
delta_y: input.delta_y,
|
|
798
1494
|
delta_x: input.delta_x,
|
|
@@ -812,6 +1508,14 @@ function registerBrowserAgentMcpTools(server, opts) {
|
|
|
812
1508
|
annotations: annotations("Press Keys")
|
|
813
1509
|
},
|
|
814
1510
|
async (input) => {
|
|
1511
|
+
if (localBrowserModeEnabled()) {
|
|
1512
|
+
try {
|
|
1513
|
+
const res2 = await localBrowser.press(input.session_id, input);
|
|
1514
|
+
return actionResult("browser_press", input.session_id, true, res2, "browser_screenshot");
|
|
1515
|
+
} catch (err) {
|
|
1516
|
+
return errorResult("browser_press", { error: err instanceof Error ? err.message : String(err), mode: "local" }, input.session_id);
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
815
1519
|
const res = await req("POST", `/agent/sessions/${input.session_id}/press`, { keys: input.keys });
|
|
816
1520
|
return actionResult("browser_press", input.session_id, res.ok, res.data, "browser_screenshot");
|
|
817
1521
|
}
|
|
@@ -826,6 +1530,7 @@ function registerBrowserAgentMcpTools(server, opts) {
|
|
|
826
1530
|
annotations: annotations("Start Recording")
|
|
827
1531
|
},
|
|
828
1532
|
async (input) => {
|
|
1533
|
+
if (localBrowserModeEnabled()) return localReplayUnsupported("browser_replay_start", input.session_id);
|
|
829
1534
|
const res = await req("POST", `/agent/sessions/${input.session_id}/replay/start`);
|
|
830
1535
|
if (!res.ok) return errorResult("browser_replay_start", res.data, input.session_id);
|
|
831
1536
|
return structuredResult({
|
|
@@ -849,6 +1554,7 @@ function registerBrowserAgentMcpTools(server, opts) {
|
|
|
849
1554
|
annotations: annotations("Stop Recording")
|
|
850
1555
|
},
|
|
851
1556
|
async (input) => {
|
|
1557
|
+
if (localBrowserModeEnabled()) return localReplayUnsupported("browser_replay_stop", input.session_id, input.replay_id);
|
|
852
1558
|
const res = await req("POST", `/agent/sessions/${input.session_id}/replay/stop`, { replay_id: input.replay_id });
|
|
853
1559
|
if (!res.ok) return errorResult("browser_replay_stop", res.data, input.session_id, input.replay_id);
|
|
854
1560
|
return structuredResult({
|
|
@@ -872,6 +1578,7 @@ function registerBrowserAgentMcpTools(server, opts) {
|
|
|
872
1578
|
annotations: annotations("List Replay Videos", true)
|
|
873
1579
|
},
|
|
874
1580
|
async (input) => {
|
|
1581
|
+
if (localBrowserModeEnabled()) return localReplayUnsupported("browser_list_replays", input.session_id);
|
|
875
1582
|
const res = await req("GET", `/agent/sessions/${input.session_id}/replays`);
|
|
876
1583
|
if (!res.ok) return errorResult("browser_list_replays", res.data, input.session_id);
|
|
877
1584
|
const replays = Array.isArray(res.data?.replays) ? res.data.replays : [];
|
|
@@ -894,6 +1601,7 @@ function registerBrowserAgentMcpTools(server, opts) {
|
|
|
894
1601
|
annotations: annotations("Download Replay MP4")
|
|
895
1602
|
},
|
|
896
1603
|
async (input) => {
|
|
1604
|
+
if (localBrowserModeEnabled()) return localReplayUnsupported("browser_replay_download", input.session_id, input.replay_id);
|
|
897
1605
|
const res = await downloadReplay(input.session_id, input.replay_id, input.filename);
|
|
898
1606
|
if (!res.ok) return errorResult("browser_replay_download", res.data, input.session_id, input.replay_id);
|
|
899
1607
|
return structuredResult({
|
|
@@ -918,6 +1626,7 @@ function registerBrowserAgentMcpTools(server, opts) {
|
|
|
918
1626
|
annotations: annotations("Mark Replay Annotation", true)
|
|
919
1627
|
},
|
|
920
1628
|
async (input) => {
|
|
1629
|
+
if (localBrowserModeEnabled()) return localReplayUnsupported("browser_replay_mark", input.session_id);
|
|
921
1630
|
const res = await req("POST", `/agent/sessions/${input.session_id}/locate`, { targets: [input.target] });
|
|
922
1631
|
if (!res.ok) return errorResult("browser_replay_mark", res.data, input.session_id);
|
|
923
1632
|
const target = res.data?.targets?.[0];
|
|
@@ -968,6 +1677,7 @@ function registerBrowserAgentMcpTools(server, opts) {
|
|
|
968
1677
|
annotations: annotations("Annotate Replay MP4")
|
|
969
1678
|
},
|
|
970
1679
|
async (input) => {
|
|
1680
|
+
if (localBrowserModeEnabled()) return localReplayUnsupported("browser_replay_annotate", input.session_id, input.replay_id);
|
|
971
1681
|
const sourceName = input.filename ? `${input.filename}-source` : void 0;
|
|
972
1682
|
const downloaded = await downloadReplay(input.session_id, input.replay_id, sourceName);
|
|
973
1683
|
if (!downloaded.ok) return errorResult("browser_replay_annotate", downloaded.data, input.session_id, input.replay_id);
|
|
@@ -1010,6 +1720,20 @@ function registerBrowserAgentMcpTools(server, opts) {
|
|
|
1010
1720
|
annotations: { title: "Close Browser Session", readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false }
|
|
1011
1721
|
},
|
|
1012
1722
|
async (input) => {
|
|
1723
|
+
if (localBrowserModeEnabled()) {
|
|
1724
|
+
try {
|
|
1725
|
+
const res2 = await localBrowser.close(input.session_id);
|
|
1726
|
+
return structuredResult({
|
|
1727
|
+
ok: true,
|
|
1728
|
+
tool: "browser_close",
|
|
1729
|
+
session_id: input.session_id,
|
|
1730
|
+
closed: true,
|
|
1731
|
+
raw: res2
|
|
1732
|
+
});
|
|
1733
|
+
} catch (err) {
|
|
1734
|
+
return errorResult("browser_close", { error: err instanceof Error ? err.message : String(err), mode: "local" }, input.session_id);
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1013
1737
|
const res = await req("DELETE", `/agent/sessions/${input.session_id}`);
|
|
1014
1738
|
if (!res.ok) return errorResult("browser_close", res.data, input.session_id);
|
|
1015
1739
|
return structuredResult({
|
|
@@ -1031,6 +1755,16 @@ function registerBrowserAgentMcpTools(server, opts) {
|
|
|
1031
1755
|
annotations: annotations("List Browser Sessions", true)
|
|
1032
1756
|
},
|
|
1033
1757
|
async (input) => {
|
|
1758
|
+
if (localBrowserModeEnabled()) {
|
|
1759
|
+
const sessions2 = localBrowser.list(input.include_closed);
|
|
1760
|
+
return structuredResult({
|
|
1761
|
+
ok: true,
|
|
1762
|
+
tool: "browser_list_sessions",
|
|
1763
|
+
session_id: null,
|
|
1764
|
+
sessions: sessions2,
|
|
1765
|
+
count: sessions2.length
|
|
1766
|
+
});
|
|
1767
|
+
}
|
|
1034
1768
|
const res = await req("GET", `/agent/sessions${input.include_closed ? "?all=1" : ""}`);
|
|
1035
1769
|
if (!res.ok) return errorResult("browser_list_sessions", res.data);
|
|
1036
1770
|
const sessions = (res.data.sessions ?? []).map((s) => ({ ...s, watch_url: `${consoleBase}/console/${s.session_id}` }));
|
|
@@ -1049,4 +1783,4 @@ export {
|
|
|
1049
1783
|
buildBrowserAgentMcpServer,
|
|
1050
1784
|
registerBrowserAgentMcpTools
|
|
1051
1785
|
};
|
|
1052
|
-
//# sourceMappingURL=chunk-
|
|
1786
|
+
//# sourceMappingURL=chunk-HS6OKELW.js.map
|