mcp-scraper 0.3.6 → 0.3.8
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 +21 -29
- package/dist/bin/api-server.cjs +159 -322
- package/dist/bin/api-server.cjs.map +1 -1
- package/dist/bin/api-server.js +1 -1
- package/dist/bin/browser-agent-stdio-server.cjs +43 -1754
- package/dist/bin/browser-agent-stdio-server.cjs.map +1 -1
- package/dist/bin/browser-agent-stdio-server.js +2 -4
- package/dist/bin/browser-agent-stdio-server.js.map +1 -1
- package/dist/bin/mcp-scraper-cli.cjs +48 -329
- package/dist/bin/mcp-scraper-cli.cjs.map +1 -1
- package/dist/bin/mcp-scraper-cli.js +8 -92
- package/dist/bin/mcp-scraper-cli.js.map +1 -1
- package/dist/bin/mcp-scraper-combined-stdio-server.cjs +83 -1801
- package/dist/bin/mcp-scraper-combined-stdio-server.cjs.map +1 -1
- package/dist/bin/mcp-scraper-combined-stdio-server.js +4 -6
- package/dist/bin/mcp-scraper-combined-stdio-server.js.map +1 -1
- package/dist/bin/mcp-scraper-install.cjs +4 -4
- package/dist/bin/mcp-scraper-install.cjs.map +1 -1
- package/dist/bin/mcp-scraper-install.js +2 -2
- package/dist/bin/mcp-stdio-server.cjs +19 -26
- package/dist/bin/mcp-stdio-server.cjs.map +1 -1
- package/dist/bin/mcp-stdio-server.js +2 -2
- package/dist/chunk-7I6PA2UQ.js +7 -0
- package/dist/chunk-7I6PA2UQ.js.map +1 -0
- package/dist/{chunk-RVYHP7QN.js → chunk-ELI6NDEG.js} +4 -4
- package/dist/{chunk-RVYHP7QN.js.map → chunk-ELI6NDEG.js.map} +1 -1
- package/dist/{chunk-ROZEDHAA.js → chunk-FU3WERZ7.js} +20 -27
- package/dist/chunk-FU3WERZ7.js.map +1 -0
- package/dist/{chunk-TK4D2BZ5.js → chunk-VQFVA6RH.js} +17 -698
- package/dist/chunk-VQFVA6RH.js.map +1 -0
- package/dist/{server-YSKBD2VX.js → server-7CGBJWJY.js} +836 -114
- package/dist/server-7CGBJWJY.js.map +1 -0
- package/docs/mcp-tool-craft-lint.generated.md +2 -5
- package/docs/mcp-tool-manifest.generated.json +18 -110
- package/package.json +1 -1
- package/dist/chunk-DUEW4EOO.js +0 -214
- package/dist/chunk-DUEW4EOO.js.map +0 -1
- package/dist/chunk-IHXHQL53.js +0 -7
- package/dist/chunk-IHXHQL53.js.map +0 -1
- package/dist/chunk-NXRWFOEZ.js +0 -816
- package/dist/chunk-NXRWFOEZ.js.map +0 -1
- package/dist/chunk-ROZEDHAA.js.map +0 -1
- package/dist/chunk-TK4D2BZ5.js.map +0 -1
- package/dist/server-YSKBD2VX.js.map +0 -1
|
@@ -1,21 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
runFanoutCapture
|
|
3
|
-
} from "./chunk-NXRWFOEZ.js";
|
|
4
1
|
import {
|
|
5
2
|
browserServiceProfileName,
|
|
6
3
|
browserServiceProfileSaveChanges
|
|
7
4
|
} from "./chunk-WN7PBKMV.js";
|
|
8
|
-
import {
|
|
9
|
-
defaultChromeExecutablePath,
|
|
10
|
-
importChromeProfile,
|
|
11
|
-
listLocalChromeProfiles,
|
|
12
|
-
loadImportedBrowserProfile,
|
|
13
|
-
localBrowserModeEnabled,
|
|
14
|
-
syncImportedChromeProfile
|
|
15
|
-
} from "./chunk-DUEW4EOO.js";
|
|
16
5
|
import {
|
|
17
6
|
PACKAGE_VERSION
|
|
18
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-7I6PA2UQ.js";
|
|
19
8
|
|
|
20
9
|
// src/mcp/browser-agent-mcp-server.ts
|
|
21
10
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
@@ -23,374 +12,6 @@ import { mkdirSync, writeFileSync } from "fs";
|
|
|
23
12
|
import { homedir } from "os";
|
|
24
13
|
import { join as join2 } from "path";
|
|
25
14
|
|
|
26
|
-
// src/services/browser-agent/local-browser-agent-service.ts
|
|
27
|
-
import { randomUUID } from "crypto";
|
|
28
|
-
import { existsSync } from "fs";
|
|
29
|
-
import { basename } from "path";
|
|
30
|
-
import { chromium } from "playwright-extra";
|
|
31
|
-
import StealthPlugin from "puppeteer-extra-plugin-stealth";
|
|
32
|
-
chromium.use(StealthPlugin());
|
|
33
|
-
function directProfileDir() {
|
|
34
|
-
return process.env.MCP_SCRAPER_BROWSER_PROFILE_DIR?.trim() || void 0;
|
|
35
|
-
}
|
|
36
|
-
function configuredProfileName(profile) {
|
|
37
|
-
return profile?.trim() || process.env.MCP_SCRAPER_BROWSER_PROFILE?.trim() || (directProfileDir() ? basename(directProfileDir()) : "default");
|
|
38
|
-
}
|
|
39
|
-
function launchExecutablePath(profile) {
|
|
40
|
-
const configured = process.env.MCP_SCRAPER_BROWSER_EXECUTABLE?.trim() || profile.browserExecutablePath || defaultChromeExecutablePath();
|
|
41
|
-
return configured && existsSync(configured) ? configured : void 0;
|
|
42
|
-
}
|
|
43
|
-
function ignoreDefaultArgsFor(executablePath) {
|
|
44
|
-
if (!executablePath) return void 0;
|
|
45
|
-
if (process.platform !== "darwin") return void 0;
|
|
46
|
-
if (!/Google Chrome\.app\/Contents\/MacOS\/Google Chrome$/.test(executablePath)) return void 0;
|
|
47
|
-
return ["--password-store=basic", "--use-mock-keychain"];
|
|
48
|
-
}
|
|
49
|
-
function normalizeKeyCombo(value) {
|
|
50
|
-
return value.split("+").map((part) => {
|
|
51
|
-
const trimmed = part.trim();
|
|
52
|
-
const lower = trimmed.toLowerCase();
|
|
53
|
-
if (lower === "ctrl" || lower === "control") return "Control";
|
|
54
|
-
if (lower === "cmd" || lower === "command" || lower === "meta") return "Meta";
|
|
55
|
-
if (lower === "option" || lower === "alt") return "Alt";
|
|
56
|
-
if (lower === "return") return "Enter";
|
|
57
|
-
if (lower === "esc") return "Escape";
|
|
58
|
-
if (trimmed.length === 1) return trimmed.toUpperCase();
|
|
59
|
-
return trimmed;
|
|
60
|
-
}).join("+");
|
|
61
|
-
}
|
|
62
|
-
function sessionSummary(session) {
|
|
63
|
-
return {
|
|
64
|
-
session_id: session.sessionId,
|
|
65
|
-
status: session.status,
|
|
66
|
-
label: session.label,
|
|
67
|
-
profile: session.profileName,
|
|
68
|
-
user_data_dir: session.userDataDir,
|
|
69
|
-
created_at: session.createdAt,
|
|
70
|
-
last_action_at: session.lastActionAt,
|
|
71
|
-
closed_at: session.closedAt,
|
|
72
|
-
watch_url: `local://browser-session/${session.sessionId}`,
|
|
73
|
-
url: session.status === "open" ? session.page.url() : null
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
var LocalBrowserAgentService = class {
|
|
77
|
-
contexts = /* @__PURE__ */ new Map();
|
|
78
|
-
sessions = /* @__PURE__ */ new Map();
|
|
79
|
-
async open(input) {
|
|
80
|
-
const profile = await this.resolveProfile(input.profile);
|
|
81
|
-
const contextState = await this.ensureContext(profile);
|
|
82
|
-
const pages = contextState.context.pages();
|
|
83
|
-
const page = pages.length === 1 && !this.hasOpenSessionForContext(contextState.key) ? pages[0] : await contextState.context.newPage();
|
|
84
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
85
|
-
const sessionId = `lbs_${randomUUID().replace(/-/g, "").slice(0, 20)}`;
|
|
86
|
-
const session = {
|
|
87
|
-
sessionId,
|
|
88
|
-
label: input.label ?? null,
|
|
89
|
-
page,
|
|
90
|
-
contextKey: contextState.key,
|
|
91
|
-
profileName: profile.name,
|
|
92
|
-
userDataDir: profile.userDataDir,
|
|
93
|
-
createdAt: now,
|
|
94
|
-
lastActionAt: now,
|
|
95
|
-
closedAt: null,
|
|
96
|
-
status: "open"
|
|
97
|
-
};
|
|
98
|
-
const timeoutSeconds = input.timeout_seconds ?? 600;
|
|
99
|
-
if (timeoutSeconds > 0) {
|
|
100
|
-
session.timeout = setTimeout(() => {
|
|
101
|
-
void this.close(sessionId);
|
|
102
|
-
}, timeoutSeconds * 1e3);
|
|
103
|
-
session.timeout.unref();
|
|
104
|
-
}
|
|
105
|
-
this.sessions.set(sessionId, session);
|
|
106
|
-
if (input.url) await this.goto(sessionId, input.url);
|
|
107
|
-
return {
|
|
108
|
-
session_id: sessionId,
|
|
109
|
-
watch_url: `local://browser-session/${sessionId}`,
|
|
110
|
-
live_view_url: null,
|
|
111
|
-
profile: profile.name,
|
|
112
|
-
user_data_dir: profile.userDataDir,
|
|
113
|
-
url: input.url ?? null,
|
|
114
|
-
mode: "local"
|
|
115
|
-
};
|
|
116
|
-
}
|
|
117
|
-
async screenshot(sessionId) {
|
|
118
|
-
const session = this.requireOpenSession(sessionId);
|
|
119
|
-
await this.waitForPage(session.page);
|
|
120
|
-
const image = await session.page.screenshot({ type: "png", fullPage: false });
|
|
121
|
-
const page = await this.read(sessionId);
|
|
122
|
-
return {
|
|
123
|
-
...page,
|
|
124
|
-
image_base64: image.toString("base64"),
|
|
125
|
-
mime_type: "image/png"
|
|
126
|
-
};
|
|
127
|
-
}
|
|
128
|
-
async read(sessionId) {
|
|
129
|
-
const session = this.requireOpenSession(sessionId);
|
|
130
|
-
await this.waitForPage(session.page);
|
|
131
|
-
this.touch(session);
|
|
132
|
-
const title = await session.page.title().catch(() => "");
|
|
133
|
-
const snapshot = await session.page.evaluate(() => {
|
|
134
|
-
const cssEscape = (value) => {
|
|
135
|
-
const escapeFn = globalThis.CSS?.escape;
|
|
136
|
-
return escapeFn ? escapeFn(value) : value.replace(/["\\#.:>+~[\]()]/g, "\\$&");
|
|
137
|
-
};
|
|
138
|
-
const textFor = (element) => {
|
|
139
|
-
const html = element;
|
|
140
|
-
return (element.getAttribute("aria-label") || element.getAttribute("title") || html.placeholder || html.value || html.innerText || element.textContent || "").replace(/\s+/g, " ").trim();
|
|
141
|
-
};
|
|
142
|
-
const selectorFor = (element) => {
|
|
143
|
-
const id = element.id;
|
|
144
|
-
if (id) return `#${cssEscape(id)}`;
|
|
145
|
-
const name = element.getAttribute("name");
|
|
146
|
-
const tag = element.tagName.toLowerCase();
|
|
147
|
-
if (name) return `${tag}[name="${name.replace(/"/g, '\\"')}"]`;
|
|
148
|
-
const parts = [];
|
|
149
|
-
let current = element;
|
|
150
|
-
while (current && current !== document.body && parts.length < 4) {
|
|
151
|
-
const currentTag = current.tagName.toLowerCase();
|
|
152
|
-
const parent = current.parentElement;
|
|
153
|
-
if (!parent) {
|
|
154
|
-
parts.unshift(currentTag);
|
|
155
|
-
break;
|
|
156
|
-
}
|
|
157
|
-
const children = Array.from(parent.children);
|
|
158
|
-
const siblings = children.filter((child) => child.tagName === current.tagName);
|
|
159
|
-
const index = siblings.indexOf(current) + 1;
|
|
160
|
-
parts.unshift(siblings.length > 1 ? `${currentTag}:nth-of-type(${index})` : currentTag);
|
|
161
|
-
current = parent;
|
|
162
|
-
}
|
|
163
|
-
return parts.join(" > ") || tag;
|
|
164
|
-
};
|
|
165
|
-
const candidates = Array.from(document.querySelectorAll(
|
|
166
|
-
'a, button, input, textarea, select, summary, [role], [contenteditable="true"], [tabindex]:not([tabindex="-1"])'
|
|
167
|
-
));
|
|
168
|
-
const elements = candidates.map((element, index) => {
|
|
169
|
-
const rect = element.getBoundingClientRect();
|
|
170
|
-
const style = window.getComputedStyle(element);
|
|
171
|
-
const visible = rect.width > 0 && rect.height > 0 && style.display !== "none" && style.visibility !== "hidden" && Number(style.opacity || "1") > 0;
|
|
172
|
-
if (!visible) return null;
|
|
173
|
-
const label = textFor(element);
|
|
174
|
-
return {
|
|
175
|
-
index,
|
|
176
|
-
tag: element.tagName.toLowerCase(),
|
|
177
|
-
role: element.getAttribute("role") || null,
|
|
178
|
-
text: label.slice(0, 180),
|
|
179
|
-
selector: selectorFor(element),
|
|
180
|
-
x: Math.round(rect.left + rect.width / 2),
|
|
181
|
-
y: Math.round(rect.top + rect.height / 2),
|
|
182
|
-
left: Math.round(rect.left),
|
|
183
|
-
top: Math.round(rect.top),
|
|
184
|
-
width: Math.round(rect.width),
|
|
185
|
-
height: Math.round(rect.height)
|
|
186
|
-
};
|
|
187
|
-
}).filter(Boolean).slice(0, 120);
|
|
188
|
-
return {
|
|
189
|
-
text: (document.body?.innerText || "").replace(/\n{3,}/g, "\n\n").slice(0, 3e4),
|
|
190
|
-
elements,
|
|
191
|
-
viewport: {
|
|
192
|
-
width: window.innerWidth,
|
|
193
|
-
height: window.innerHeight,
|
|
194
|
-
devicePixelRatio: window.devicePixelRatio,
|
|
195
|
-
scrollX: window.scrollX,
|
|
196
|
-
scrollY: window.scrollY
|
|
197
|
-
}
|
|
198
|
-
};
|
|
199
|
-
});
|
|
200
|
-
return {
|
|
201
|
-
url: session.page.url(),
|
|
202
|
-
title,
|
|
203
|
-
text: snapshot.text,
|
|
204
|
-
elements: snapshot.elements,
|
|
205
|
-
viewport: snapshot.viewport
|
|
206
|
-
};
|
|
207
|
-
}
|
|
208
|
-
async locate(sessionId, targets) {
|
|
209
|
-
const session = this.requireOpenSession(sessionId);
|
|
210
|
-
this.touch(session);
|
|
211
|
-
const located = [];
|
|
212
|
-
for (const target of targets) {
|
|
213
|
-
try {
|
|
214
|
-
const locator = target.selector ? session.page.locator(target.selector) : session.page.getByText(target.text ?? "", { exact: target.match === "exact" });
|
|
215
|
-
const selected = locator.nth(target.index ?? 0);
|
|
216
|
-
await selected.waitFor({ state: "visible", timeout: 3e3 }).catch(() => void 0);
|
|
217
|
-
const box = await selected.boundingBox();
|
|
218
|
-
const text = await selected.textContent({ timeout: 1e3 }).catch(() => null);
|
|
219
|
-
located.push({
|
|
220
|
-
input: target,
|
|
221
|
-
found: Boolean(box),
|
|
222
|
-
element: box ? {
|
|
223
|
-
left: Math.round(box.x),
|
|
224
|
-
top: Math.round(box.y),
|
|
225
|
-
width: Math.round(box.width),
|
|
226
|
-
height: Math.round(box.height),
|
|
227
|
-
x: Math.round(box.x + box.width / 2),
|
|
228
|
-
y: Math.round(box.y + box.height / 2),
|
|
229
|
-
text: (text ?? "").replace(/\s+/g, " ").trim().slice(0, 220)
|
|
230
|
-
} : null
|
|
231
|
-
});
|
|
232
|
-
} catch (err) {
|
|
233
|
-
located.push({
|
|
234
|
-
input: target,
|
|
235
|
-
found: false,
|
|
236
|
-
element: null,
|
|
237
|
-
error: err instanceof Error ? err.message : String(err)
|
|
238
|
-
});
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
const read = await this.read(sessionId);
|
|
242
|
-
return {
|
|
243
|
-
url: read.url,
|
|
244
|
-
title: read.title,
|
|
245
|
-
viewport: read.viewport,
|
|
246
|
-
replay: null,
|
|
247
|
-
targets: located
|
|
248
|
-
};
|
|
249
|
-
}
|
|
250
|
-
async goto(sessionId, url) {
|
|
251
|
-
const session = this.requireOpenSession(sessionId);
|
|
252
|
-
this.touch(session);
|
|
253
|
-
const response = await session.page.goto(url, { waitUntil: "domcontentloaded", timeout: 45e3 }).catch((err) => {
|
|
254
|
-
throw new Error(err instanceof Error ? err.message : String(err));
|
|
255
|
-
});
|
|
256
|
-
await this.waitForPage(session.page);
|
|
257
|
-
return {
|
|
258
|
-
ok: true,
|
|
259
|
-
url: session.page.url(),
|
|
260
|
-
status: response?.status() ?? null,
|
|
261
|
-
mode: "local"
|
|
262
|
-
};
|
|
263
|
-
}
|
|
264
|
-
async click(sessionId, input) {
|
|
265
|
-
const session = this.requireOpenSession(sessionId);
|
|
266
|
-
this.touch(session);
|
|
267
|
-
await session.page.mouse.click(input.x, input.y, {
|
|
268
|
-
button: input.button ?? "left",
|
|
269
|
-
clickCount: input.num_clicks ?? 1
|
|
270
|
-
});
|
|
271
|
-
await this.waitForPage(session.page);
|
|
272
|
-
return { ok: true, mode: "local", url: session.page.url() };
|
|
273
|
-
}
|
|
274
|
-
async type(sessionId, input) {
|
|
275
|
-
const session = this.requireOpenSession(sessionId);
|
|
276
|
-
this.touch(session);
|
|
277
|
-
await session.page.keyboard.type(input.text, { delay: input.delay });
|
|
278
|
-
return { ok: true, mode: "local", url: session.page.url() };
|
|
279
|
-
}
|
|
280
|
-
async scroll(sessionId, input) {
|
|
281
|
-
const session = this.requireOpenSession(sessionId);
|
|
282
|
-
this.touch(session);
|
|
283
|
-
if (typeof input.x === "number" && typeof input.y === "number") {
|
|
284
|
-
await session.page.mouse.move(input.x, input.y);
|
|
285
|
-
}
|
|
286
|
-
await session.page.mouse.wheel((input.delta_x ?? 0) * 120, (input.delta_y ?? 5) * 120);
|
|
287
|
-
await session.page.waitForTimeout(150).catch(() => void 0);
|
|
288
|
-
return { ok: true, mode: "local", url: session.page.url() };
|
|
289
|
-
}
|
|
290
|
-
async press(sessionId, input) {
|
|
291
|
-
const session = this.requireOpenSession(sessionId);
|
|
292
|
-
this.touch(session);
|
|
293
|
-
for (const key of input.keys) {
|
|
294
|
-
await session.page.keyboard.press(normalizeKeyCombo(key));
|
|
295
|
-
}
|
|
296
|
-
await this.waitForPage(session.page);
|
|
297
|
-
return { ok: true, mode: "local", url: session.page.url() };
|
|
298
|
-
}
|
|
299
|
-
async close(sessionId) {
|
|
300
|
-
const session = this.sessions.get(sessionId);
|
|
301
|
-
if (!session || session.status === "closed") {
|
|
302
|
-
return { ok: true, closed: true, already_closed: true, mode: "local" };
|
|
303
|
-
}
|
|
304
|
-
if (session.timeout) clearTimeout(session.timeout);
|
|
305
|
-
session.status = "closed";
|
|
306
|
-
session.closedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
307
|
-
await session.page.close().catch(() => void 0);
|
|
308
|
-
await this.closeContextIfUnused(session.contextKey);
|
|
309
|
-
return { ok: true, closed: true, mode: "local" };
|
|
310
|
-
}
|
|
311
|
-
list(includeClosed = false) {
|
|
312
|
-
return Array.from(this.sessions.values()).filter((session) => includeClosed || session.status === "open").map(sessionSummary);
|
|
313
|
-
}
|
|
314
|
-
async captureFanout(sessionId, input) {
|
|
315
|
-
const session = this.requireOpenSession(sessionId);
|
|
316
|
-
this.touch(session);
|
|
317
|
-
return runFanoutCapture(session.page, input);
|
|
318
|
-
}
|
|
319
|
-
async screenshotOnly(sessionId) {
|
|
320
|
-
const session = this.requireOpenSession(sessionId);
|
|
321
|
-
await this.waitForPage(session.page);
|
|
322
|
-
const img = await session.page.screenshot({ type: "png", fullPage: false });
|
|
323
|
-
return img.toString("base64");
|
|
324
|
-
}
|
|
325
|
-
async resolveProfile(profile) {
|
|
326
|
-
const directDir = directProfileDir();
|
|
327
|
-
const requestedName = configuredProfileName(profile);
|
|
328
|
-
if (directDir && (!profile || requestedName === process.env.MCP_SCRAPER_BROWSER_PROFILE?.trim())) {
|
|
329
|
-
return {
|
|
330
|
-
name: requestedName,
|
|
331
|
-
userDataDir: directDir,
|
|
332
|
-
browserExecutablePath: process.env.MCP_SCRAPER_BROWSER_EXECUTABLE?.trim() || defaultChromeExecutablePath() || null,
|
|
333
|
-
manifest: null
|
|
334
|
-
};
|
|
335
|
-
}
|
|
336
|
-
const manifest = await loadImportedBrowserProfile(requestedName);
|
|
337
|
-
return {
|
|
338
|
-
name: manifest.name,
|
|
339
|
-
userDataDir: manifest.userDataDir,
|
|
340
|
-
browserExecutablePath: manifest.browserExecutablePath,
|
|
341
|
-
manifest
|
|
342
|
-
};
|
|
343
|
-
}
|
|
344
|
-
async ensureContext(profile) {
|
|
345
|
-
const existing = this.contexts.get(profile.userDataDir);
|
|
346
|
-
if (existing) return existing;
|
|
347
|
-
const executablePath = launchExecutablePath(profile);
|
|
348
|
-
const context = await chromium.launchPersistentContext(profile.userDataDir, {
|
|
349
|
-
headless: false,
|
|
350
|
-
viewport: { width: 1440, height: 900 },
|
|
351
|
-
acceptDownloads: true,
|
|
352
|
-
...executablePath ? { executablePath } : {},
|
|
353
|
-
...ignoreDefaultArgsFor(executablePath) ? { ignoreDefaultArgs: ignoreDefaultArgsFor(executablePath) } : {}
|
|
354
|
-
});
|
|
355
|
-
const state = { key: profile.userDataDir, context, profile };
|
|
356
|
-
context.on("close", () => {
|
|
357
|
-
this.contexts.delete(profile.userDataDir);
|
|
358
|
-
const closedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
359
|
-
for (const session of this.sessions.values()) {
|
|
360
|
-
if (session.contextKey === profile.userDataDir && session.status === "open") {
|
|
361
|
-
session.status = "closed";
|
|
362
|
-
session.closedAt = closedAt;
|
|
363
|
-
if (session.timeout) clearTimeout(session.timeout);
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
});
|
|
367
|
-
this.contexts.set(profile.userDataDir, state);
|
|
368
|
-
return state;
|
|
369
|
-
}
|
|
370
|
-
requireOpenSession(sessionId) {
|
|
371
|
-
const session = this.sessions.get(sessionId);
|
|
372
|
-
if (!session || session.status !== "open") throw new Error(`Local browser session not found or closed: ${sessionId}`);
|
|
373
|
-
return session;
|
|
374
|
-
}
|
|
375
|
-
touch(session) {
|
|
376
|
-
session.lastActionAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
377
|
-
}
|
|
378
|
-
hasOpenSessionForContext(contextKey) {
|
|
379
|
-
return Array.from(this.sessions.values()).some((session) => session.contextKey === contextKey && session.status === "open");
|
|
380
|
-
}
|
|
381
|
-
async closeContextIfUnused(contextKey) {
|
|
382
|
-
if (this.hasOpenSessionForContext(contextKey)) return;
|
|
383
|
-
const state = this.contexts.get(contextKey);
|
|
384
|
-
if (!state) return;
|
|
385
|
-
this.contexts.delete(contextKey);
|
|
386
|
-
await state.context.close().catch(() => void 0);
|
|
387
|
-
}
|
|
388
|
-
async waitForPage(page) {
|
|
389
|
-
await page.waitForLoadState("domcontentloaded", { timeout: 5e3 }).catch(() => void 0);
|
|
390
|
-
await page.waitForTimeout(200).catch(() => void 0);
|
|
391
|
-
}
|
|
392
|
-
};
|
|
393
|
-
|
|
394
15
|
// src/mcp/browser-agent-tool-schemas.ts
|
|
395
16
|
import { z } from "zod";
|
|
396
17
|
var NullableString = z.string().nullable();
|
|
@@ -409,35 +30,16 @@ var BrowserOpenInputSchema = {
|
|
|
409
30
|
label: z.string().optional().describe("Optional human label for this session, shown in the watch console."),
|
|
410
31
|
url: z.string().url().optional().describe("Optional URL to navigate to immediately after opening."),
|
|
411
32
|
profile: z.string().optional().describe("Optional saved hosted profile name to load a logged-in session for a site."),
|
|
412
|
-
save_profile_changes: z.boolean().optional().describe("Persist cookies and
|
|
33
|
+
save_profile_changes: z.boolean().optional().describe("Persist cookies and browser 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."),
|
|
413
34
|
timeout_seconds: z.number().int().min(60).max(259200).optional().describe("How long the direct no-proxy browser 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.")
|
|
414
35
|
};
|
|
415
|
-
var BrowserProfileListInputSchema = {
|
|
416
|
-
email: z.string().optional().describe("Optional Chrome account email or displayed profile name to filter for, for example seo@example.com."),
|
|
417
|
-
user_data_dir: z.string().optional().describe("Optional Chrome user data directory. Defaults to the current OS Chrome profile root.")
|
|
418
|
-
};
|
|
419
36
|
var BrowserProfileOnboardInputSchema = {
|
|
420
37
|
email: z.string().optional().describe("Optional account email used to derive a stable MCP Scraper profile name. This does not import cookies or browser state."),
|
|
421
38
|
profile: z.string().optional().describe("Optional saved MCP Scraper hosted browser profile name to create or load. If omitted, email is used to derive one."),
|
|
422
|
-
user_data_dir: z.string().optional().describe("Optional Chrome metadata directory for deriving a profile name only. This is not a local browser setup path."),
|
|
423
39
|
url: z.string().url().optional().describe("Setup URL to open after creating the direct no-proxy browser. Defaults to https://google.com/ so the user can click Sign in."),
|
|
424
40
|
label: z.string().optional().describe("Optional human label for this setup session."),
|
|
425
41
|
timeout_seconds: z.number().int().min(60).max(259200).optional().describe("How long the direct no-proxy setup session may live before auto-termination. Defaults to 600.")
|
|
426
42
|
};
|
|
427
|
-
var BrowserProfileImportInputSchema = {
|
|
428
|
-
email: z.string().optional().describe("Optional Chrome account email or displayed profile name to select from Chrome Local State."),
|
|
429
|
-
name: z.string().optional().describe("Name for the managed MCP Scraper browser profile. Defaults to a slug derived from the selected Chrome account."),
|
|
430
|
-
chrome_profile_directory: z.string().optional().describe("Optional Chrome profile directory to import, for example Default or Profile 1."),
|
|
431
|
-
user_data_dir: z.string().optional().describe("Optional source Chrome user data directory. Defaults to the current OS Chrome profile root."),
|
|
432
|
-
output_dir: z.string().optional().describe("Optional managed profile base directory. Defaults to ~/.mcp-scraper/browser-profiles."),
|
|
433
|
-
browser_executable_path: z.string().optional().describe("Optional Chrome executable path for local browser launches. Defaults to the installed Google Chrome path when found."),
|
|
434
|
-
overwrite: z.boolean().default(false).describe("Overwrite an existing managed profile clone with the same name.")
|
|
435
|
-
};
|
|
436
|
-
var BrowserProfileSyncInputSchema = {
|
|
437
|
-
name: z.string().describe("Managed MCP Scraper browser profile name to refresh from its recorded source Chrome profile."),
|
|
438
|
-
output_dir: z.string().optional().describe("Optional managed profile base directory. Defaults to ~/.mcp-scraper/browser-profiles."),
|
|
439
|
-
browser_executable_path: z.string().optional().describe("Optional Chrome executable path to store in the refreshed profile manifest.")
|
|
440
|
-
};
|
|
441
43
|
var BrowserSessionInputSchema = {
|
|
442
44
|
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.")
|
|
443
45
|
};
|
|
@@ -606,14 +208,6 @@ var BrowserOpenOutputSchema = {
|
|
|
606
208
|
hint: z.string(),
|
|
607
209
|
raw: BrowserRawObject.optional()
|
|
608
210
|
};
|
|
609
|
-
var BrowserProfileListOutputSchema = {
|
|
610
|
-
ok: z.boolean(),
|
|
611
|
-
tool: z.literal("browser_profile_list"),
|
|
612
|
-
session_id: z.null(),
|
|
613
|
-
profiles: z.array(BrowserRawObject),
|
|
614
|
-
count: z.number().int().min(0),
|
|
615
|
-
hint: z.string()
|
|
616
|
-
};
|
|
617
211
|
var BrowserProfileOnboardOutputSchema = {
|
|
618
212
|
ok: z.boolean(),
|
|
619
213
|
tool: z.literal("browser_profile_onboard"),
|
|
@@ -622,27 +216,10 @@ var BrowserProfileOnboardOutputSchema = {
|
|
|
622
216
|
live_view_url: NullableString,
|
|
623
217
|
profile: z.string(),
|
|
624
218
|
setup_url: z.string(),
|
|
625
|
-
|
|
219
|
+
account_email: NullableString,
|
|
626
220
|
next_steps: z.array(z.string()),
|
|
627
221
|
raw: BrowserRawObject.optional()
|
|
628
222
|
};
|
|
629
|
-
var BrowserProfileImportOutputSchema = {
|
|
630
|
-
ok: z.boolean(),
|
|
631
|
-
tool: z.literal("browser_profile_import"),
|
|
632
|
-
session_id: z.null(),
|
|
633
|
-
profile: BrowserRawObject,
|
|
634
|
-
source_profile: BrowserRawObject,
|
|
635
|
-
next_steps: z.array(z.string()),
|
|
636
|
-
hint: z.string()
|
|
637
|
-
};
|
|
638
|
-
var BrowserProfileSyncOutputSchema = {
|
|
639
|
-
ok: z.boolean(),
|
|
640
|
-
tool: z.literal("browser_profile_sync"),
|
|
641
|
-
session_id: z.null(),
|
|
642
|
-
profile: BrowserRawObject,
|
|
643
|
-
source_profile: BrowserRawObject,
|
|
644
|
-
hint: z.string()
|
|
645
|
-
};
|
|
646
223
|
var BrowserScreenshotOutputSchema = {
|
|
647
224
|
...BrowserBaseOutput,
|
|
648
225
|
tool: z.literal("browser_screenshot"),
|
|
@@ -1049,6 +626,11 @@ function replayFilePath(sessionId, replayId, filename) {
|
|
|
1049
626
|
const name = requested ? safeFilePart(requested).replace(/\.mp4$/i, "") : `${stamp}-${safeFilePart(sessionId)}-${safeFilePart(replayId)}`;
|
|
1050
627
|
return join2(outputBaseDir(), "browser-replays", `${name}.mp4`);
|
|
1051
628
|
}
|
|
629
|
+
function savedProfileNameFromEmail(email) {
|
|
630
|
+
const value = email?.trim().toLowerCase();
|
|
631
|
+
if (!value) return null;
|
|
632
|
+
return value.replace(/[^a-z0-9._-]+/g, "-").replace(/[._-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80) || null;
|
|
633
|
+
}
|
|
1052
634
|
function annotatedReplayFilePath(sessionId, replayId, filename) {
|
|
1053
635
|
const requested = filename?.trim();
|
|
1054
636
|
if (requested) return replayFilePath(sessionId, replayId, requested);
|
|
@@ -1083,7 +665,6 @@ function registerBrowserAgentMcpTools(server, opts) {
|
|
|
1083
665
|
const baseUrl = opts.baseUrl.replace(/\/$/, "");
|
|
1084
666
|
const consoleBase = (opts.consoleBaseUrl ?? opts.baseUrl).replace(/\/$/, "");
|
|
1085
667
|
const timeoutMs = opts.timeoutMs ?? 9e4;
|
|
1086
|
-
const localBrowser = new LocalBrowserAgentService();
|
|
1087
668
|
async function req(method, path, body) {
|
|
1088
669
|
try {
|
|
1089
670
|
const res = await fetch(`${baseUrl}${path}`, {
|
|
@@ -1135,131 +716,20 @@ function registerBrowserAgentMcpTools(server, opts) {
|
|
|
1135
716
|
idempotentHint: false,
|
|
1136
717
|
openWorldHint: true
|
|
1137
718
|
});
|
|
1138
|
-
function selectChromeProfile(matches, email) {
|
|
1139
|
-
const wanted = email?.trim().toLowerCase();
|
|
1140
|
-
if (!wanted) return matches[0] ?? null;
|
|
1141
|
-
return matches.find((profile) => profile.email.toLowerCase() === wanted) ?? matches[0] ?? null;
|
|
1142
|
-
}
|
|
1143
|
-
function localReplayUnsupported(tool, sessionId = null, replayId = null) {
|
|
1144
|
-
return errorResult(tool, {
|
|
1145
|
-
error: "Local browser mode does not support replay recording yet. Use hosted mode for browser_replay_* tools.",
|
|
1146
|
-
mode: "local"
|
|
1147
|
-
}, sessionId, replayId);
|
|
1148
|
-
}
|
|
1149
|
-
server.registerTool(
|
|
1150
|
-
"browser_profile_list",
|
|
1151
|
-
{
|
|
1152
|
-
title: "Developer: List Local Chrome Profiles",
|
|
1153
|
-
description: "Developer/local-machine helper only. 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. Do not use this for customer AI visibility setup or reconnects; use browser_profile_onboard at Google and provide the hosted watch_url instead.",
|
|
1154
|
-
inputSchema: BrowserProfileListInputSchema,
|
|
1155
|
-
outputSchema: BrowserProfileListOutputSchema,
|
|
1156
|
-
annotations: annotations("Developer: List Local Chrome Profiles", true)
|
|
1157
|
-
},
|
|
1158
|
-
async (input) => {
|
|
1159
|
-
try {
|
|
1160
|
-
const profiles = await listLocalChromeProfiles(input.user_data_dir, input.email);
|
|
1161
|
-
return structuredResult({
|
|
1162
|
-
ok: true,
|
|
1163
|
-
tool: "browser_profile_list",
|
|
1164
|
-
session_id: null,
|
|
1165
|
-
profiles,
|
|
1166
|
-
count: profiles.length,
|
|
1167
|
-
hint: "For customer auth setup, use browser_profile_onboard so the user signs in through the hosted watch_url. Local import is only for developer/local-machine mode."
|
|
1168
|
-
});
|
|
1169
|
-
} catch (err) {
|
|
1170
|
-
return errorResult("browser_profile_list", { error: err instanceof Error ? err.message : String(err) });
|
|
1171
|
-
}
|
|
1172
|
-
}
|
|
1173
|
-
);
|
|
1174
|
-
server.registerTool(
|
|
1175
|
-
"browser_profile_import",
|
|
1176
|
-
{
|
|
1177
|
-
title: "Developer: Import Local Chrome Profile",
|
|
1178
|
-
description: "Developer/local-machine helper only. 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. Do not use this for customer AI visibility setup or reconnects; use browser_profile_onboard at Google and provide the hosted watch_url instead.",
|
|
1179
|
-
inputSchema: BrowserProfileImportInputSchema,
|
|
1180
|
-
outputSchema: BrowserProfileImportOutputSchema,
|
|
1181
|
-
annotations: { title: "Developer: Import Local Chrome Profile", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }
|
|
1182
|
-
},
|
|
1183
|
-
async (input) => {
|
|
1184
|
-
try {
|
|
1185
|
-
const result = await importChromeProfile({
|
|
1186
|
-
email: input.email,
|
|
1187
|
-
name: input.name,
|
|
1188
|
-
chromeProfileDirectory: input.chrome_profile_directory,
|
|
1189
|
-
sourceUserDataDir: input.user_data_dir,
|
|
1190
|
-
outputDir: input.output_dir,
|
|
1191
|
-
overwrite: input.overwrite,
|
|
1192
|
-
browserExecutablePath: input.browser_executable_path
|
|
1193
|
-
});
|
|
1194
|
-
return structuredResult({
|
|
1195
|
-
ok: true,
|
|
1196
|
-
tool: "browser_profile_import",
|
|
1197
|
-
session_id: null,
|
|
1198
|
-
profile: result.profile,
|
|
1199
|
-
source_profile: result.sourceProfile,
|
|
1200
|
-
next_steps: [
|
|
1201
|
-
`Set MCP_SCRAPER_BROWSER_MODE=local in the MCP server config.`,
|
|
1202
|
-
`Set MCP_SCRAPER_BROWSER_PROFILE=${result.profile.name}.`,
|
|
1203
|
-
"Restart the MCP client so it starts the local browser mode process.",
|
|
1204
|
-
"Call browser_open with the same MCP server; it will open a local Chrome window using the cloned profile."
|
|
1205
|
-
],
|
|
1206
|
-
hint: "Developer-only local clone. For customer auth setup, use browser_profile_onboard at Google, provide the hosted watch_url, then browser_close to save the hosted profile."
|
|
1207
|
-
});
|
|
1208
|
-
} catch (err) {
|
|
1209
|
-
return errorResult("browser_profile_import", { error: err instanceof Error ? err.message : String(err) });
|
|
1210
|
-
}
|
|
1211
|
-
}
|
|
1212
|
-
);
|
|
1213
|
-
server.registerTool(
|
|
1214
|
-
"browser_profile_sync",
|
|
1215
|
-
{
|
|
1216
|
-
title: "Developer: Sync Local Chrome Profile",
|
|
1217
|
-
description: "Developer/local-machine helper only. 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. Do not use this for customer AI visibility setup or reconnects; use browser_profile_onboard at Google and provide the hosted watch_url instead.",
|
|
1218
|
-
inputSchema: BrowserProfileSyncInputSchema,
|
|
1219
|
-
outputSchema: BrowserProfileSyncOutputSchema,
|
|
1220
|
-
annotations: { title: "Developer: Sync Local Chrome Profile", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }
|
|
1221
|
-
},
|
|
1222
|
-
async (input) => {
|
|
1223
|
-
try {
|
|
1224
|
-
const result = await syncImportedChromeProfile(input.name, {
|
|
1225
|
-
outputDir: input.output_dir,
|
|
1226
|
-
browserExecutablePath: input.browser_executable_path
|
|
1227
|
-
});
|
|
1228
|
-
return structuredResult({
|
|
1229
|
-
ok: true,
|
|
1230
|
-
tool: "browser_profile_sync",
|
|
1231
|
-
session_id: null,
|
|
1232
|
-
profile: result.profile,
|
|
1233
|
-
source_profile: result.sourceProfile,
|
|
1234
|
-
hint: `Developer-only local clone refreshed for ${result.profile.name}. Customer reconnects should use browser_profile_onboard with the same hosted profile name.`
|
|
1235
|
-
});
|
|
1236
|
-
} catch (err) {
|
|
1237
|
-
return errorResult("browser_profile_sync", { error: err instanceof Error ? err.message : String(err) });
|
|
1238
|
-
}
|
|
1239
|
-
}
|
|
1240
|
-
);
|
|
1241
719
|
server.registerTool(
|
|
1242
720
|
"browser_profile_onboard",
|
|
1243
721
|
{
|
|
1244
722
|
title: "Connect Hosted Browser Profile",
|
|
1245
|
-
description: "Create or load a saved MCP Scraper hosted browser profile and open a direct no-proxy setup browser with profile saving enabled. This is the customer auth setup and reconnect flow: open Google, give the user the watch_url, have them click Sign in and complete login, then call browser_close to save cookies and
|
|
723
|
+
description: "Create or load a saved MCP Scraper hosted browser profile and open a direct no-proxy setup browser with profile saving enabled. This is the customer auth setup and reconnect flow: open Google, give the user the watch_url, have them click Sign in and complete login, then call browser_close to save cookies and storage. If a saved profile later shows login controls, run this same setup flow again.",
|
|
1246
724
|
inputSchema: BrowserProfileOnboardInputSchema,
|
|
1247
725
|
outputSchema: BrowserProfileOnboardOutputSchema,
|
|
1248
726
|
annotations: annotations("Connect Hosted Browser Profile")
|
|
1249
727
|
},
|
|
1250
728
|
async (input) => {
|
|
1251
|
-
|
|
1252
|
-
if (input.email) {
|
|
1253
|
-
try {
|
|
1254
|
-
chromeProfile = selectChromeProfile(await listLocalChromeProfiles(input.user_data_dir, input.email), input.email);
|
|
1255
|
-
} catch (err) {
|
|
1256
|
-
return errorResult("browser_profile_onboard", { error: err instanceof Error ? err.message : String(err) });
|
|
1257
|
-
}
|
|
1258
|
-
}
|
|
1259
|
-
const profile = input.profile?.trim() || chromeProfile?.recommendedKernelProfileName || browserServiceProfileName();
|
|
729
|
+
const profile = input.profile?.trim() || savedProfileNameFromEmail(input.email) || browserServiceProfileName();
|
|
1260
730
|
if (!profile) {
|
|
1261
731
|
return errorResult("browser_profile_onboard", {
|
|
1262
|
-
error: "profile is required when
|
|
732
|
+
error: "profile or email is required when BROWSER_AGENT_PROFILE_NAME is not available"
|
|
1263
733
|
});
|
|
1264
734
|
}
|
|
1265
735
|
const setupUrl = input.url ?? "https://google.com/";
|
|
@@ -1282,10 +752,10 @@ function registerBrowserAgentMcpTools(server, opts) {
|
|
|
1282
752
|
live_view_url: session.live_view_url ?? null,
|
|
1283
753
|
profile,
|
|
1284
754
|
setup_url: setupUrl,
|
|
1285
|
-
|
|
755
|
+
account_email: input.email?.trim() || null,
|
|
1286
756
|
next_steps: [
|
|
1287
757
|
"Open the watch_url and let the user click Sign in on Google.",
|
|
1288
|
-
"After the user confirms login is complete, call browser_close with this session_id to persist cookies and
|
|
758
|
+
"After the user confirms login is complete, call browser_close with this session_id to persist cookies and storage to the saved profile.",
|
|
1289
759
|
"If the saved profile later shows login controls or disconnects, repeat browser_profile_onboard and close the setup session again.",
|
|
1290
760
|
"Use BROWSER_AGENT_PROFILE_NAME or browser_open.profile with this profile name for future authenticated sessions."
|
|
1291
761
|
],
|
|
@@ -1303,23 +773,6 @@ function registerBrowserAgentMcpTools(server, opts) {
|
|
|
1303
773
|
annotations: annotations("Open Browser Session")
|
|
1304
774
|
},
|
|
1305
775
|
async (input) => {
|
|
1306
|
-
if (localBrowserModeEnabled()) {
|
|
1307
|
-
try {
|
|
1308
|
-
const session2 = await localBrowser.open(input);
|
|
1309
|
-
return structuredResult({
|
|
1310
|
-
ok: true,
|
|
1311
|
-
tool: "browser_open",
|
|
1312
|
-
session_id: session2.session_id,
|
|
1313
|
-
watch_url: session2.watch_url,
|
|
1314
|
-
live_view_url: null,
|
|
1315
|
-
url: input.url ?? null,
|
|
1316
|
-
hint: "Local Chrome is open on this machine. Call browser_screenshot to see the page and click by x,y coordinates.",
|
|
1317
|
-
raw: session2
|
|
1318
|
-
});
|
|
1319
|
-
} catch (err) {
|
|
1320
|
-
return errorResult("browser_open", { error: err instanceof Error ? err.message : String(err), mode: "local" });
|
|
1321
|
-
}
|
|
1322
|
-
}
|
|
1323
776
|
const profile = input.profile ?? browserServiceProfileName();
|
|
1324
777
|
const saveProfileChanges = input.save_profile_changes ?? browserServiceProfileSaveChanges();
|
|
1325
778
|
const open = await req("POST", "/agent/sessions", {
|
|
@@ -1356,27 +809,6 @@ function registerBrowserAgentMcpTools(server, opts) {
|
|
|
1356
809
|
annotations: annotations("See Page", true)
|
|
1357
810
|
},
|
|
1358
811
|
async (input) => {
|
|
1359
|
-
if (localBrowserModeEnabled()) {
|
|
1360
|
-
try {
|
|
1361
|
-
const res2 = await localBrowser.screenshot(input.session_id);
|
|
1362
|
-
const content2 = [];
|
|
1363
|
-
if (res2.image_base64) content2.push({ type: "image", data: res2.image_base64, mimeType: res2.mime_type });
|
|
1364
|
-
const structured2 = {
|
|
1365
|
-
ok: true,
|
|
1366
|
-
tool: "browser_screenshot",
|
|
1367
|
-
session_id: input.session_id,
|
|
1368
|
-
url: res2.url ?? null,
|
|
1369
|
-
title: res2.title ?? null,
|
|
1370
|
-
text: res2.text,
|
|
1371
|
-
elements: res2.elements,
|
|
1372
|
-
screenshot: { mime_type: res2.mime_type, inline: true }
|
|
1373
|
-
};
|
|
1374
|
-
content2.push({ type: "text", text: JSON.stringify(structured2) });
|
|
1375
|
-
return { content: content2, structuredContent: structured2 };
|
|
1376
|
-
} catch (err) {
|
|
1377
|
-
return errorResult("browser_screenshot", { error: err instanceof Error ? err.message : String(err), mode: "local" }, input.session_id);
|
|
1378
|
-
}
|
|
1379
|
-
}
|
|
1380
812
|
const res = await req("POST", `/agent/sessions/${input.session_id}/screenshot`);
|
|
1381
813
|
if (!res.ok) return errorResult("browser_screenshot", res.data, input.session_id);
|
|
1382
814
|
const { image_base64, mime_type, url, title, elements, text } = res.data;
|
|
@@ -1409,23 +841,6 @@ function registerBrowserAgentMcpTools(server, opts) {
|
|
|
1409
841
|
annotations: annotations("Read Page", true)
|
|
1410
842
|
},
|
|
1411
843
|
async (input) => {
|
|
1412
|
-
if (localBrowserModeEnabled()) {
|
|
1413
|
-
try {
|
|
1414
|
-
const res2 = await localBrowser.read(input.session_id);
|
|
1415
|
-
return structuredResult({
|
|
1416
|
-
ok: true,
|
|
1417
|
-
tool: "browser_read",
|
|
1418
|
-
session_id: input.session_id,
|
|
1419
|
-
url: res2.url ?? null,
|
|
1420
|
-
title: res2.title ?? null,
|
|
1421
|
-
text: res2.text,
|
|
1422
|
-
elements: res2.elements,
|
|
1423
|
-
raw: res2
|
|
1424
|
-
});
|
|
1425
|
-
} catch (err) {
|
|
1426
|
-
return errorResult("browser_read", { error: err instanceof Error ? err.message : String(err), mode: "local" }, input.session_id);
|
|
1427
|
-
}
|
|
1428
|
-
}
|
|
1429
844
|
const res = await req("POST", `/agent/sessions/${input.session_id}/read`);
|
|
1430
845
|
if (!res.ok) return errorResult("browser_read", res.data, input.session_id);
|
|
1431
846
|
return structuredResult({
|
|
@@ -1450,24 +865,6 @@ function registerBrowserAgentMcpTools(server, opts) {
|
|
|
1450
865
|
annotations: annotations("Locate DOM Targets", true)
|
|
1451
866
|
},
|
|
1452
867
|
async (input) => {
|
|
1453
|
-
if (localBrowserModeEnabled()) {
|
|
1454
|
-
try {
|
|
1455
|
-
const res2 = await localBrowser.locate(input.session_id, input.targets);
|
|
1456
|
-
return structuredResult({
|
|
1457
|
-
ok: true,
|
|
1458
|
-
tool: "browser_locate",
|
|
1459
|
-
session_id: input.session_id,
|
|
1460
|
-
url: res2.url ?? null,
|
|
1461
|
-
title: res2.title ?? null,
|
|
1462
|
-
viewport: res2.viewport ?? null,
|
|
1463
|
-
replay: null,
|
|
1464
|
-
targets: Array.isArray(res2.targets) ? res2.targets : [],
|
|
1465
|
-
raw: res2
|
|
1466
|
-
});
|
|
1467
|
-
} catch (err) {
|
|
1468
|
-
return errorResult("browser_locate", { error: err instanceof Error ? err.message : String(err), mode: "local" }, input.session_id);
|
|
1469
|
-
}
|
|
1470
|
-
}
|
|
1471
868
|
const res = await req("POST", `/agent/sessions/${input.session_id}/locate`, { targets: input.targets });
|
|
1472
869
|
if (!res.ok) return errorResult("browser_locate", res.data, input.session_id);
|
|
1473
870
|
return structuredResult({
|
|
@@ -1493,14 +890,6 @@ function registerBrowserAgentMcpTools(server, opts) {
|
|
|
1493
890
|
annotations: annotations("Navigate To URL")
|
|
1494
891
|
},
|
|
1495
892
|
async (input) => {
|
|
1496
|
-
if (localBrowserModeEnabled()) {
|
|
1497
|
-
try {
|
|
1498
|
-
const res2 = await localBrowser.goto(input.session_id, input.url);
|
|
1499
|
-
return actionResult("browser_goto", input.session_id, true, res2, "browser_screenshot");
|
|
1500
|
-
} catch (err) {
|
|
1501
|
-
return errorResult("browser_goto", { error: err instanceof Error ? err.message : String(err), mode: "local" }, input.session_id);
|
|
1502
|
-
}
|
|
1503
|
-
}
|
|
1504
893
|
const res = await req("POST", `/agent/sessions/${input.session_id}/goto`, { url: input.url });
|
|
1505
894
|
return actionResult("browser_goto", input.session_id, res.ok, res.data, "browser_screenshot");
|
|
1506
895
|
}
|
|
@@ -1515,14 +904,6 @@ function registerBrowserAgentMcpTools(server, opts) {
|
|
|
1515
904
|
annotations: annotations("Click")
|
|
1516
905
|
},
|
|
1517
906
|
async (input) => {
|
|
1518
|
-
if (localBrowserModeEnabled()) {
|
|
1519
|
-
try {
|
|
1520
|
-
const res2 = await localBrowser.click(input.session_id, input);
|
|
1521
|
-
return actionResult("browser_click", input.session_id, true, res2, "browser_screenshot");
|
|
1522
|
-
} catch (err) {
|
|
1523
|
-
return errorResult("browser_click", { error: err instanceof Error ? err.message : String(err), mode: "local" }, input.session_id);
|
|
1524
|
-
}
|
|
1525
|
-
}
|
|
1526
907
|
const res = await req("POST", `/agent/sessions/${input.session_id}/click`, {
|
|
1527
908
|
x: input.x,
|
|
1528
909
|
y: input.y,
|
|
@@ -1542,14 +923,6 @@ function registerBrowserAgentMcpTools(server, opts) {
|
|
|
1542
923
|
annotations: annotations("Type Text")
|
|
1543
924
|
},
|
|
1544
925
|
async (input) => {
|
|
1545
|
-
if (localBrowserModeEnabled()) {
|
|
1546
|
-
try {
|
|
1547
|
-
const res2 = await localBrowser.type(input.session_id, input);
|
|
1548
|
-
return actionResult("browser_type", input.session_id, true, res2, "browser_screenshot");
|
|
1549
|
-
} catch (err) {
|
|
1550
|
-
return errorResult("browser_type", { error: err instanceof Error ? err.message : String(err), mode: "local" }, input.session_id);
|
|
1551
|
-
}
|
|
1552
|
-
}
|
|
1553
926
|
const res = await req("POST", `/agent/sessions/${input.session_id}/type`, { text: input.text, delay: input.delay });
|
|
1554
927
|
return actionResult("browser_type", input.session_id, res.ok, res.data, "browser_screenshot");
|
|
1555
928
|
}
|
|
@@ -1564,14 +937,6 @@ function registerBrowserAgentMcpTools(server, opts) {
|
|
|
1564
937
|
annotations: annotations("Scroll")
|
|
1565
938
|
},
|
|
1566
939
|
async (input) => {
|
|
1567
|
-
if (localBrowserModeEnabled()) {
|
|
1568
|
-
try {
|
|
1569
|
-
const res2 = await localBrowser.scroll(input.session_id, input);
|
|
1570
|
-
return actionResult("browser_scroll", input.session_id, true, res2, "browser_screenshot");
|
|
1571
|
-
} catch (err) {
|
|
1572
|
-
return errorResult("browser_scroll", { error: err instanceof Error ? err.message : String(err), mode: "local" }, input.session_id);
|
|
1573
|
-
}
|
|
1574
|
-
}
|
|
1575
940
|
const res = await req("POST", `/agent/sessions/${input.session_id}/scroll`, {
|
|
1576
941
|
delta_y: input.delta_y,
|
|
1577
942
|
delta_x: input.delta_x,
|
|
@@ -1591,14 +956,6 @@ function registerBrowserAgentMcpTools(server, opts) {
|
|
|
1591
956
|
annotations: annotations("Press Keys")
|
|
1592
957
|
},
|
|
1593
958
|
async (input) => {
|
|
1594
|
-
if (localBrowserModeEnabled()) {
|
|
1595
|
-
try {
|
|
1596
|
-
const res2 = await localBrowser.press(input.session_id, input);
|
|
1597
|
-
return actionResult("browser_press", input.session_id, true, res2, "browser_screenshot");
|
|
1598
|
-
} catch (err) {
|
|
1599
|
-
return errorResult("browser_press", { error: err instanceof Error ? err.message : String(err), mode: "local" }, input.session_id);
|
|
1600
|
-
}
|
|
1601
|
-
}
|
|
1602
959
|
const res = await req("POST", `/agent/sessions/${input.session_id}/press`, { keys: input.keys });
|
|
1603
960
|
return actionResult("browser_press", input.session_id, res.ok, res.data, "browser_screenshot");
|
|
1604
961
|
}
|
|
@@ -1613,7 +970,6 @@ function registerBrowserAgentMcpTools(server, opts) {
|
|
|
1613
970
|
annotations: annotations("Start Recording")
|
|
1614
971
|
},
|
|
1615
972
|
async (input) => {
|
|
1616
|
-
if (localBrowserModeEnabled()) return localReplayUnsupported("browser_replay_start", input.session_id);
|
|
1617
973
|
const res = await req("POST", `/agent/sessions/${input.session_id}/replay/start`);
|
|
1618
974
|
if (!res.ok) return errorResult("browser_replay_start", res.data, input.session_id);
|
|
1619
975
|
return structuredResult({
|
|
@@ -1631,13 +987,12 @@ function registerBrowserAgentMcpTools(server, opts) {
|
|
|
1631
987
|
"browser_replay_stop",
|
|
1632
988
|
{
|
|
1633
989
|
title: "Stop Recording",
|
|
1634
|
-
description: "Stop a replay recording and expose the final view_url and download_url. Use browser_replay_download to save the MP4
|
|
990
|
+
description: "Stop a replay recording and expose the final view_url and download_url. Use browser_replay_download to save the MP4 to the configured output directory.",
|
|
1635
991
|
inputSchema: BrowserReplayStopInputSchema,
|
|
1636
992
|
outputSchema: BrowserReplayStopOutputSchema,
|
|
1637
993
|
annotations: annotations("Stop Recording")
|
|
1638
994
|
},
|
|
1639
995
|
async (input) => {
|
|
1640
|
-
if (localBrowserModeEnabled()) return localReplayUnsupported("browser_replay_stop", input.session_id, input.replay_id);
|
|
1641
996
|
const res = await req("POST", `/agent/sessions/${input.session_id}/replay/stop`, { replay_id: input.replay_id });
|
|
1642
997
|
if (!res.ok) return errorResult("browser_replay_stop", res.data, input.session_id, input.replay_id);
|
|
1643
998
|
return structuredResult({
|
|
@@ -1661,7 +1016,6 @@ function registerBrowserAgentMcpTools(server, opts) {
|
|
|
1661
1016
|
annotations: annotations("List Replay Videos", true)
|
|
1662
1017
|
},
|
|
1663
1018
|
async (input) => {
|
|
1664
|
-
if (localBrowserModeEnabled()) return localReplayUnsupported("browser_list_replays", input.session_id);
|
|
1665
1019
|
const res = await req("GET", `/agent/sessions/${input.session_id}/replays`);
|
|
1666
1020
|
if (!res.ok) return errorResult("browser_list_replays", res.data, input.session_id);
|
|
1667
1021
|
const replays = Array.isArray(res.data?.replays) ? res.data.replays : [];
|
|
@@ -1678,13 +1032,12 @@ function registerBrowserAgentMcpTools(server, opts) {
|
|
|
1678
1032
|
"browser_replay_download",
|
|
1679
1033
|
{
|
|
1680
1034
|
title: "Download Replay MP4",
|
|
1681
|
-
description: "Download a replay recording through MCP Scraper and save the MP4
|
|
1035
|
+
description: "Download a replay recording through MCP Scraper and save the MP4 under MCP_SCRAPER_OUTPUT_DIR/browser-replays. Use after browser_replay_stop or browser_list_replays.",
|
|
1682
1036
|
inputSchema: BrowserReplayDownloadInputSchema,
|
|
1683
1037
|
outputSchema: BrowserReplayDownloadOutputSchema,
|
|
1684
1038
|
annotations: annotations("Download Replay MP4")
|
|
1685
1039
|
},
|
|
1686
1040
|
async (input) => {
|
|
1687
|
-
if (localBrowserModeEnabled()) return localReplayUnsupported("browser_replay_download", input.session_id, input.replay_id);
|
|
1688
1041
|
const res = await downloadReplay(input.session_id, input.replay_id, input.filename);
|
|
1689
1042
|
if (!res.ok) return errorResult("browser_replay_download", res.data, input.session_id, input.replay_id);
|
|
1690
1043
|
return structuredResult({
|
|
@@ -1709,7 +1062,6 @@ function registerBrowserAgentMcpTools(server, opts) {
|
|
|
1709
1062
|
annotations: annotations("Mark Replay Annotation", true)
|
|
1710
1063
|
},
|
|
1711
1064
|
async (input) => {
|
|
1712
|
-
if (localBrowserModeEnabled()) return localReplayUnsupported("browser_replay_mark", input.session_id);
|
|
1713
1065
|
const res = await req("POST", `/agent/sessions/${input.session_id}/locate`, { targets: [input.target] });
|
|
1714
1066
|
if (!res.ok) return errorResult("browser_replay_mark", res.data, input.session_id);
|
|
1715
1067
|
const target = res.data?.targets?.[0];
|
|
@@ -1754,13 +1106,12 @@ function registerBrowserAgentMcpTools(server, opts) {
|
|
|
1754
1106
|
"browser_replay_annotate",
|
|
1755
1107
|
{
|
|
1756
1108
|
title: "Annotate Replay MP4",
|
|
1757
|
-
description: "Download a browser replay MP4, render visual annotations over it, and save a new annotated MP4
|
|
1109
|
+
description: "Download a browser replay MP4, render visual annotations over it, and save a new annotated MP4 under MCP_SCRAPER_OUTPUT_DIR/browser-replays. Use this after browser_replay_stop when the user wants proof videos with circles, boxes, arrows, underlines, or labels. For accurate timing and placement, prefer annotations returned by browser_replay_mark while the replay is recording; otherwise use exact left/top/width/height bounds from browser_locate. If the replay video size differs from the screenshot coordinate space, pass source_width and source_height.",
|
|
1758
1110
|
inputSchema: BrowserReplayAnnotateInputSchema,
|
|
1759
1111
|
outputSchema: BrowserReplayAnnotateOutputSchema,
|
|
1760
1112
|
annotations: annotations("Annotate Replay MP4")
|
|
1761
1113
|
},
|
|
1762
1114
|
async (input) => {
|
|
1763
|
-
if (localBrowserModeEnabled()) return localReplayUnsupported("browser_replay_annotate", input.session_id, input.replay_id);
|
|
1764
1115
|
const sourceName = input.filename ? `${input.filename}-source` : void 0;
|
|
1765
1116
|
const downloaded = await downloadReplay(input.session_id, input.replay_id, sourceName);
|
|
1766
1117
|
if (!downloaded.ok) return errorResult("browser_replay_annotate", downloaded.data, input.session_id, input.replay_id);
|
|
@@ -1803,20 +1154,6 @@ function registerBrowserAgentMcpTools(server, opts) {
|
|
|
1803
1154
|
annotations: { title: "Close Browser Session", readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false }
|
|
1804
1155
|
},
|
|
1805
1156
|
async (input) => {
|
|
1806
|
-
if (localBrowserModeEnabled()) {
|
|
1807
|
-
try {
|
|
1808
|
-
const res2 = await localBrowser.close(input.session_id);
|
|
1809
|
-
return structuredResult({
|
|
1810
|
-
ok: true,
|
|
1811
|
-
tool: "browser_close",
|
|
1812
|
-
session_id: input.session_id,
|
|
1813
|
-
closed: true,
|
|
1814
|
-
raw: res2
|
|
1815
|
-
});
|
|
1816
|
-
} catch (err) {
|
|
1817
|
-
return errorResult("browser_close", { error: err instanceof Error ? err.message : String(err), mode: "local" }, input.session_id);
|
|
1818
|
-
}
|
|
1819
|
-
}
|
|
1820
1157
|
const res = await req("DELETE", `/agent/sessions/${input.session_id}`);
|
|
1821
1158
|
if (!res.ok) return errorResult("browser_close", res.data, input.session_id);
|
|
1822
1159
|
return structuredResult({
|
|
@@ -1838,16 +1175,6 @@ function registerBrowserAgentMcpTools(server, opts) {
|
|
|
1838
1175
|
annotations: annotations("List Browser Sessions", true)
|
|
1839
1176
|
},
|
|
1840
1177
|
async (input) => {
|
|
1841
|
-
if (localBrowserModeEnabled()) {
|
|
1842
|
-
const sessions2 = localBrowser.list(input.include_closed);
|
|
1843
|
-
return structuredResult({
|
|
1844
|
-
ok: true,
|
|
1845
|
-
tool: "browser_list_sessions",
|
|
1846
|
-
session_id: null,
|
|
1847
|
-
sessions: sessions2,
|
|
1848
|
-
count: sessions2.length
|
|
1849
|
-
});
|
|
1850
|
-
}
|
|
1851
1178
|
const res = await req("GET", `/agent/sessions${input.include_closed ? "?all=1" : ""}`);
|
|
1852
1179
|
if (!res.ok) return errorResult("browser_list_sessions", res.data);
|
|
1853
1180
|
const sessions = (res.data.sessions ?? []).map((s) => ({ ...s, watch_url: `${consoleBase}/console/${s.session_id}` }));
|
|
@@ -1889,14 +1216,6 @@ function registerBrowserAgentMcpTools(server, opts) {
|
|
|
1889
1216
|
exports: exports ?? null,
|
|
1890
1217
|
...result.debug ? { debug: result.debug } : {}
|
|
1891
1218
|
});
|
|
1892
|
-
if (localBrowserModeEnabled()) {
|
|
1893
|
-
try {
|
|
1894
|
-
const { result, exports } = await localBrowser.captureFanout(input.session_id, input);
|
|
1895
|
-
return emit(result, exports);
|
|
1896
|
-
} catch (err) {
|
|
1897
|
-
return errorResult("browser_capture_fanout", { error: err instanceof Error ? err.message : String(err), mode: "local" }, input.session_id);
|
|
1898
|
-
}
|
|
1899
|
-
}
|
|
1900
1219
|
const res = await req("POST", `/agent/sessions/${input.session_id}/capture-fanout`, {
|
|
1901
1220
|
prompt: input.prompt,
|
|
1902
1221
|
wait_ms: input.wait_ms,
|
|
@@ -1914,4 +1233,4 @@ export {
|
|
|
1914
1233
|
buildBrowserAgentMcpServer,
|
|
1915
1234
|
registerBrowserAgentMcpTools
|
|
1916
1235
|
};
|
|
1917
|
-
//# sourceMappingURL=chunk-
|
|
1236
|
+
//# sourceMappingURL=chunk-VQFVA6RH.js.map
|