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.
Files changed (39) hide show
  1. package/README.md +34 -3
  2. package/dist/bin/api-server.cjs +52 -1
  3. package/dist/bin/api-server.cjs.map +1 -1
  4. package/dist/bin/api-server.js +2 -2
  5. package/dist/bin/browser-agent-stdio-server.cjs +960 -20
  6. package/dist/bin/browser-agent-stdio-server.cjs.map +1 -1
  7. package/dist/bin/browser-agent-stdio-server.js +3 -1
  8. package/dist/bin/browser-agent-stdio-server.js.map +1 -1
  9. package/dist/bin/mcp-scraper-cli.cjs +342 -47
  10. package/dist/bin/mcp-scraper-cli.cjs.map +1 -1
  11. package/dist/bin/mcp-scraper-cli.js +105 -7
  12. package/dist/bin/mcp-scraper-cli.js.map +1 -1
  13. package/dist/bin/mcp-scraper-combined-stdio-server.cjs +976 -36
  14. package/dist/bin/mcp-scraper-combined-stdio-server.cjs.map +1 -1
  15. package/dist/bin/mcp-scraper-combined-stdio-server.js +3 -1
  16. package/dist/bin/mcp-scraper-combined-stdio-server.js.map +1 -1
  17. package/dist/bin/paa-harvest.cjs.map +1 -1
  18. package/dist/bin/paa-harvest.js +4 -2
  19. package/dist/bin/paa-harvest.js.map +1 -1
  20. package/dist/chunk-DUEW4EOO.js +214 -0
  21. package/dist/chunk-DUEW4EOO.js.map +1 -0
  22. package/dist/{chunk-L4OWOUGR.js → chunk-HS6OKELW.js} +737 -3
  23. package/dist/chunk-HS6OKELW.js.map +1 -0
  24. package/dist/{chunk-XGUDTDZ2.js → chunk-IPW4LFOT.js} +5 -13
  25. package/dist/chunk-IPW4LFOT.js.map +1 -0
  26. package/dist/chunk-WN7PBKMV.js +28 -0
  27. package/dist/chunk-WN7PBKMV.js.map +1 -0
  28. package/dist/index.cjs.map +1 -1
  29. package/dist/index.js +2 -1
  30. package/dist/index.js.map +1 -1
  31. package/dist/{server-7NE45K2A.js → server-24XIQ6W6.js} +40 -6
  32. package/dist/server-24XIQ6W6.js.map +1 -0
  33. package/dist/{worker-MIYG2B2I.js → worker-SLQ375UG.js} +5 -3
  34. package/dist/{worker-MIYG2B2I.js.map → worker-SLQ375UG.js.map} +1 -1
  35. package/docs/mcp-tool-manifest.generated.json +132 -4
  36. package/package.json +1 -1
  37. package/dist/chunk-L4OWOUGR.js.map +0 -1
  38. package/dist/chunk-XGUDTDZ2.js.map +0 -1
  39. package/dist/server-7NE45K2A.js.map +0 -1
@@ -2,16 +2,589 @@
2
2
  "use strict";
3
3
 
4
4
  // bin/browser-agent-stdio-server.ts
5
- var import_node_fs2 = require("fs");
6
- var import_node_os3 = require("os");
7
- var import_node_path3 = require("path");
5
+ var import_node_fs4 = require("fs");
6
+ var import_node_os5 = require("os");
7
+ var import_node_path6 = require("path");
8
8
  var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
9
9
 
10
10
  // src/mcp/browser-agent-mcp-server.ts
11
11
  var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
12
+ var import_node_fs3 = require("fs");
13
+ var import_node_os4 = require("os");
14
+ var import_node_path5 = require("path");
15
+
16
+ // src/lib/chrome-profiles.ts
17
+ var import_promises = require("fs/promises");
18
+ var import_node_os = require("os");
19
+ var import_node_path = require("path");
20
+ function defaultChromeUserDataDir() {
21
+ if (process.platform === "darwin") return (0, import_node_path.join)((0, import_node_os.homedir)(), "Library", "Application Support", "Google", "Chrome");
22
+ if (process.platform === "win32") {
23
+ return (0, import_node_path.join)(process.env.LOCALAPPDATA ?? (0, import_node_path.join)((0, import_node_os.homedir)(), "AppData", "Local"), "Google", "Chrome", "User Data");
24
+ }
25
+ return (0, import_node_path.join)(process.env.XDG_CONFIG_HOME ?? (0, import_node_path.join)((0, import_node_os.homedir)(), ".config"), "google-chrome");
26
+ }
27
+ function recommendedKernelProfileName(value) {
28
+ return value.trim().toLowerCase().replace(/@/g, "-").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80) || "browser-profile";
29
+ }
30
+ async function listLocalChromeProfiles(userDataDir = defaultChromeUserDataDir(), emailFilter) {
31
+ const raw = await (0, import_promises.readFile)((0, import_node_path.join)(userDataDir, "Local State"), "utf8");
32
+ const localState = JSON.parse(raw);
33
+ const wanted = emailFilter?.trim().toLowerCase();
34
+ return Object.entries(localState.profile?.info_cache ?? {}).map(([directory, info]) => {
35
+ const email = (info.user_name ?? "").trim();
36
+ const displayName = (info.name ?? info.shortcut_name ?? "").trim();
37
+ const labelSource = email || displayName || directory;
38
+ return {
39
+ email,
40
+ displayName,
41
+ chromeProfileDirectory: directory,
42
+ chromeProfilePath: (0, import_node_path.join)(userDataDir, directory),
43
+ recommendedKernelProfileName: recommendedKernelProfileName(labelSource)
44
+ };
45
+ }).filter((row) => !wanted || row.email.toLowerCase() === wanted || row.displayName.toLowerCase().includes(wanted));
46
+ }
47
+
48
+ // src/lib/browser-service-env.ts
49
+ function browserServiceProfileName() {
50
+ const value = (process.env.BROWSER_AGENT_PROFILE_NAME ?? process.env.BROWSER_SERVICE_PROFILE_NAME ?? process.env.KERNEL_BROWSER_PROFILE_NAME ?? process.env.KERNEL_PROFILE_NAME)?.trim();
51
+ return value || void 0;
52
+ }
53
+ function browserServiceProfileSaveChanges() {
54
+ const value = (process.env.BROWSER_AGENT_PROFILE_SAVE_CHANGES ?? process.env.BROWSER_SERVICE_PROFILE_SAVE_CHANGES ?? process.env.KERNEL_BROWSER_PROFILE_SAVE_CHANGES ?? process.env.KERNEL_PROFILE_SAVE_CHANGES)?.trim().toLowerCase();
55
+ if (!value) return void 0;
56
+ if (["1", "true", "yes", "on"].includes(value)) return true;
57
+ if (["0", "false", "no", "off"].includes(value)) return false;
58
+ return void 0;
59
+ }
60
+
61
+ // src/lib/local-browser-profiles.ts
12
62
  var import_node_fs = require("fs");
63
+ var import_promises2 = require("fs/promises");
13
64
  var import_node_os2 = require("os");
14
65
  var import_node_path2 = require("path");
66
+ var EXCLUDED_PROFILE_NAMES = [
67
+ "Cache",
68
+ "Code Cache",
69
+ "Crashpad",
70
+ "DawnCache",
71
+ "DawnGraphiteCache",
72
+ "DawnWebGPUCache",
73
+ "GPUCache",
74
+ "GrShaderCache",
75
+ "ShaderCache",
76
+ "Shared Dictionary"
77
+ ];
78
+ function defaultImportedBrowserProfilesDir() {
79
+ return process.env.MCP_SCRAPER_BROWSER_PROFILE_BASE_DIR?.trim() || (0, import_node_path2.join)((0, import_node_os2.homedir)(), ".mcp-scraper", "browser-profiles");
80
+ }
81
+ function defaultChromeExecutablePath() {
82
+ const envPath = process.env.MCP_SCRAPER_BROWSER_EXECUTABLE?.trim() || process.env.CHROME_PATH?.trim();
83
+ if (envPath) return envPath;
84
+ const candidates = process.platform === "darwin" ? ["/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"] : process.platform === "win32" ? [
85
+ (0, import_node_path2.join)(process.env.PROGRAMFILES ?? "C:\\Program Files", "Google", "Chrome", "Application", "chrome.exe"),
86
+ (0, import_node_path2.join)(process.env["PROGRAMFILES(X86)"] ?? "C:\\Program Files (x86)", "Google", "Chrome", "Application", "chrome.exe")
87
+ ] : ["/usr/bin/google-chrome", "/usr/bin/google-chrome-stable", "/usr/bin/chromium-browser", "/usr/bin/chromium"];
88
+ return candidates.find((candidate) => (0, import_node_fs.existsSync)(candidate));
89
+ }
90
+ function localBrowserModeEnabled() {
91
+ return process.env.MCP_SCRAPER_BROWSER_MODE?.trim().toLowerCase() === "local";
92
+ }
93
+ function importedProfileName(value) {
94
+ return recommendedKernelProfileName(value);
95
+ }
96
+ function importedProfileRoot(name, outputDir = defaultImportedBrowserProfilesDir()) {
97
+ return (0, import_node_path2.join)(outputDir, importedProfileName(name));
98
+ }
99
+ function importedProfileManifestPath(name, outputDir = defaultImportedBrowserProfilesDir()) {
100
+ return (0, import_node_path2.join)(importedProfileRoot(name, outputDir), "manifest.json");
101
+ }
102
+ function importedProfileUserDataDir(name, outputDir = defaultImportedBrowserProfilesDir()) {
103
+ return (0, import_node_path2.join)(importedProfileRoot(name, outputDir), "user-data");
104
+ }
105
+ async function readExistingManifest(path) {
106
+ try {
107
+ return JSON.parse(await (0, import_promises2.readFile)(path, "utf8"));
108
+ } catch {
109
+ return null;
110
+ }
111
+ }
112
+ function isExcludedProfilePath(sourceRoot, sourcePath) {
113
+ const rel = (0, import_node_path2.relative)(sourceRoot, sourcePath);
114
+ if (!rel) return false;
115
+ const parts = rel.split(import_node_path2.sep).filter(Boolean);
116
+ const last = parts[parts.length - 1] ?? "";
117
+ if (last.startsWith("Singleton")) return true;
118
+ if (last === "LOCK" || last.endsWith(".lock")) return true;
119
+ if (parts.some((part) => EXCLUDED_PROFILE_NAMES.includes(part))) return true;
120
+ if (parts.includes("Service Worker") && parts.includes("CacheStorage")) return true;
121
+ return false;
122
+ }
123
+ async function copyProfileTree(sourceProfilePath, destinationProfilePath) {
124
+ await (0, import_promises2.cp)(sourceProfilePath, destinationProfilePath, {
125
+ recursive: true,
126
+ force: true,
127
+ errorOnExist: false,
128
+ dereference: false,
129
+ filter: (source) => !isExcludedProfilePath(sourceProfilePath, source)
130
+ });
131
+ }
132
+ function selectProfile(profiles, options) {
133
+ const requestedDirectory = options.chromeProfileDirectory?.trim();
134
+ const requestedEmail = options.email?.trim().toLowerCase();
135
+ const exactDirectory = requestedDirectory ? profiles.find((profile) => profile.chromeProfileDirectory === requestedDirectory) : void 0;
136
+ if (exactDirectory) return exactDirectory;
137
+ const exactEmail = requestedEmail ? profiles.find((profile) => profile.email.toLowerCase() === requestedEmail) : void 0;
138
+ if (exactEmail) return exactEmail;
139
+ const first = profiles[0];
140
+ if (first) return first;
141
+ const hint = requestedDirectory ? `Chrome profile directory "${requestedDirectory}"` : requestedEmail ? `Chrome account "${requestedEmail}"` : "Chrome profile";
142
+ throw new Error(`${hint} was not found in ${options.sourceUserDataDir ?? defaultChromeUserDataDir()}`);
143
+ }
144
+ async function ensureProfilePathExists(profile) {
145
+ const details = await (0, import_promises2.stat)(profile.chromeProfilePath).catch(() => null);
146
+ if (!details?.isDirectory()) {
147
+ throw new Error(`Chrome profile path does not exist or is not a directory: ${profile.chromeProfilePath}`);
148
+ }
149
+ }
150
+ async function writeNormalizedLocalState(sourceLocalStatePath, destinationLocalStatePath, sourceProfileDirectory) {
151
+ const raw = await (0, import_promises2.readFile)(sourceLocalStatePath, "utf8");
152
+ const localState = JSON.parse(raw);
153
+ const selectedInfo = localState.profile?.info_cache?.[sourceProfileDirectory] ?? localState.profile?.info_cache?.Default ?? {};
154
+ localState.profile = {
155
+ ...localState.profile ?? {},
156
+ info_cache: { Default: selectedInfo },
157
+ last_used: "Default",
158
+ last_active_profiles: ["Default"]
159
+ };
160
+ await (0, import_promises2.writeFile)(destinationLocalStatePath, `${JSON.stringify(localState, null, 2)}
161
+ `);
162
+ }
163
+ async function importChromeProfile(options = {}) {
164
+ const sourceUserDataDir = options.sourceUserDataDir?.trim() || defaultChromeUserDataDir();
165
+ const profiles = await listLocalChromeProfiles(sourceUserDataDir, options.email);
166
+ const sourceProfile = selectProfile(profiles, { ...options, sourceUserDataDir });
167
+ await ensureProfilePathExists(sourceProfile);
168
+ const name = importedProfileName(options.name?.trim() || sourceProfile.recommendedKernelProfileName);
169
+ const outputDir = options.outputDir?.trim() || defaultImportedBrowserProfilesDir();
170
+ const profileRoot = importedProfileRoot(name, outputDir);
171
+ const manifestPath = importedProfileManifestPath(name, outputDir);
172
+ const userDataDir = importedProfileUserDataDir(name, outputDir);
173
+ const existingManifest = await readExistingManifest(manifestPath);
174
+ const existing = (0, import_node_fs.existsSync)(profileRoot);
175
+ if (existing && !options.overwrite) {
176
+ throw new Error(`Imported browser profile "${name}" already exists at ${profileRoot}. Re-run with --overwrite to refresh it.`);
177
+ }
178
+ if (existing) await (0, import_promises2.rm)(profileRoot, { recursive: true, force: true });
179
+ await (0, import_promises2.mkdir)((0, import_node_path2.join)(userDataDir, "Default"), { recursive: true });
180
+ await writeNormalizedLocalState(
181
+ (0, import_node_path2.join)(sourceUserDataDir, "Local State"),
182
+ (0, import_node_path2.join)(userDataDir, "Local State"),
183
+ sourceProfile.chromeProfileDirectory
184
+ );
185
+ await copyProfileTree(sourceProfile.chromeProfilePath, (0, import_node_path2.join)(userDataDir, "Default"));
186
+ const now = (/* @__PURE__ */ new Date()).toISOString();
187
+ const manifest = {
188
+ layoutVersion: 1,
189
+ name,
190
+ profileRoot,
191
+ userDataDir,
192
+ browserExecutablePath: options.browserExecutablePath?.trim() || defaultChromeExecutablePath() || null,
193
+ sourceUserDataDir,
194
+ sourceProfileDirectory: sourceProfile.chromeProfileDirectory,
195
+ sourceProfilePath: sourceProfile.chromeProfilePath,
196
+ email: sourceProfile.email,
197
+ displayName: sourceProfile.displayName,
198
+ createdAt: existingManifest?.createdAt ?? now,
199
+ updatedAt: now,
200
+ copyMode: "chrome-profile-clone"
201
+ };
202
+ await (0, import_promises2.writeFile)(manifestPath, `${JSON.stringify(manifest, null, 2)}
203
+ `);
204
+ return {
205
+ profile: manifest,
206
+ sourceProfile,
207
+ excludedNames: EXCLUDED_PROFILE_NAMES
208
+ };
209
+ }
210
+ async function loadImportedBrowserProfile(name = process.env.MCP_SCRAPER_BROWSER_PROFILE?.trim() || "default", outputDir = defaultImportedBrowserProfilesDir()) {
211
+ const profileName = importedProfileName(name);
212
+ const manifestPath = importedProfileManifestPath(profileName, outputDir);
213
+ const manifest = await readExistingManifest(manifestPath);
214
+ if (!manifest) {
215
+ throw new Error(
216
+ `Imported browser profile "${profileName}" was not found. Run: mcp-scraper-cli browser import-chrome --name ${profileName}`
217
+ );
218
+ }
219
+ return manifest;
220
+ }
221
+ async function syncImportedChromeProfile(name, options = {}) {
222
+ const outputDir = options.outputDir?.trim() || defaultImportedBrowserProfilesDir();
223
+ const manifest = await loadImportedBrowserProfile(name, outputDir);
224
+ return importChromeProfile({
225
+ name: manifest.name,
226
+ sourceUserDataDir: manifest.sourceUserDataDir,
227
+ chromeProfileDirectory: manifest.sourceProfileDirectory,
228
+ outputDir,
229
+ overwrite: true,
230
+ browserExecutablePath: options.browserExecutablePath ?? manifest.browserExecutablePath ?? void 0
231
+ });
232
+ }
233
+
234
+ // src/services/browser-agent/local-browser-agent-service.ts
235
+ var import_node_crypto = require("crypto");
236
+ var import_node_fs2 = require("fs");
237
+ var import_node_path3 = require("path");
238
+ var import_playwright = require("playwright");
239
+ function directProfileDir() {
240
+ return process.env.MCP_SCRAPER_BROWSER_PROFILE_DIR?.trim() || void 0;
241
+ }
242
+ function configuredProfileName(profile) {
243
+ return profile?.trim() || process.env.MCP_SCRAPER_BROWSER_PROFILE?.trim() || (directProfileDir() ? (0, import_node_path3.basename)(directProfileDir()) : "default");
244
+ }
245
+ function launchExecutablePath(profile) {
246
+ const configured = process.env.MCP_SCRAPER_BROWSER_EXECUTABLE?.trim() || profile.browserExecutablePath || defaultChromeExecutablePath();
247
+ return configured && (0, import_node_fs2.existsSync)(configured) ? configured : void 0;
248
+ }
249
+ function ignoreDefaultArgsFor(executablePath) {
250
+ if (!executablePath) return void 0;
251
+ if (process.platform !== "darwin") return void 0;
252
+ if (!/Google Chrome\.app\/Contents\/MacOS\/Google Chrome$/.test(executablePath)) return void 0;
253
+ return ["--password-store=basic", "--use-mock-keychain"];
254
+ }
255
+ function normalizeKeyCombo(value) {
256
+ return value.split("+").map((part) => {
257
+ const trimmed = part.trim();
258
+ const lower = trimmed.toLowerCase();
259
+ if (lower === "ctrl" || lower === "control") return "Control";
260
+ if (lower === "cmd" || lower === "command" || lower === "meta") return "Meta";
261
+ if (lower === "option" || lower === "alt") return "Alt";
262
+ if (lower === "return") return "Enter";
263
+ if (lower === "esc") return "Escape";
264
+ if (trimmed.length === 1) return trimmed.toUpperCase();
265
+ return trimmed;
266
+ }).join("+");
267
+ }
268
+ function sessionSummary(session) {
269
+ return {
270
+ session_id: session.sessionId,
271
+ status: session.status,
272
+ label: session.label,
273
+ profile: session.profileName,
274
+ user_data_dir: session.userDataDir,
275
+ created_at: session.createdAt,
276
+ last_action_at: session.lastActionAt,
277
+ closed_at: session.closedAt,
278
+ watch_url: `local://browser-session/${session.sessionId}`,
279
+ url: session.status === "open" ? session.page.url() : null
280
+ };
281
+ }
282
+ var LocalBrowserAgentService = class {
283
+ contexts = /* @__PURE__ */ new Map();
284
+ sessions = /* @__PURE__ */ new Map();
285
+ async open(input) {
286
+ const profile = await this.resolveProfile(input.profile);
287
+ const contextState = await this.ensureContext(profile);
288
+ const pages = contextState.context.pages();
289
+ const page = pages.length === 1 && !this.hasOpenSessionForContext(contextState.key) ? pages[0] : await contextState.context.newPage();
290
+ const now = (/* @__PURE__ */ new Date()).toISOString();
291
+ const sessionId = `lbs_${(0, import_node_crypto.randomUUID)().replace(/-/g, "").slice(0, 20)}`;
292
+ const session = {
293
+ sessionId,
294
+ label: input.label ?? null,
295
+ page,
296
+ contextKey: contextState.key,
297
+ profileName: profile.name,
298
+ userDataDir: profile.userDataDir,
299
+ createdAt: now,
300
+ lastActionAt: now,
301
+ closedAt: null,
302
+ status: "open"
303
+ };
304
+ const timeoutSeconds = input.timeout_seconds ?? 600;
305
+ if (timeoutSeconds > 0) {
306
+ session.timeout = setTimeout(() => {
307
+ void this.close(sessionId);
308
+ }, timeoutSeconds * 1e3);
309
+ session.timeout.unref();
310
+ }
311
+ this.sessions.set(sessionId, session);
312
+ if (input.url) await this.goto(sessionId, input.url);
313
+ return {
314
+ session_id: sessionId,
315
+ watch_url: `local://browser-session/${sessionId}`,
316
+ live_view_url: null,
317
+ profile: profile.name,
318
+ user_data_dir: profile.userDataDir,
319
+ url: input.url ?? null,
320
+ mode: "local"
321
+ };
322
+ }
323
+ async screenshot(sessionId) {
324
+ const session = this.requireOpenSession(sessionId);
325
+ await this.waitForPage(session.page);
326
+ const image = await session.page.screenshot({ type: "png", fullPage: false });
327
+ const page = await this.read(sessionId);
328
+ return {
329
+ ...page,
330
+ image_base64: image.toString("base64"),
331
+ mime_type: "image/png"
332
+ };
333
+ }
334
+ async read(sessionId) {
335
+ const session = this.requireOpenSession(sessionId);
336
+ await this.waitForPage(session.page);
337
+ this.touch(session);
338
+ const title = await session.page.title().catch(() => "");
339
+ const snapshot = await session.page.evaluate(() => {
340
+ const cssEscape = (value) => {
341
+ const escapeFn = globalThis.CSS?.escape;
342
+ return escapeFn ? escapeFn(value) : value.replace(/["\\#.:>+~[\]()]/g, "\\$&");
343
+ };
344
+ const textFor = (element) => {
345
+ const html = element;
346
+ return (element.getAttribute("aria-label") || element.getAttribute("title") || html.placeholder || html.value || html.innerText || element.textContent || "").replace(/\s+/g, " ").trim();
347
+ };
348
+ const selectorFor = (element) => {
349
+ const id = element.id;
350
+ if (id) return `#${cssEscape(id)}`;
351
+ const name = element.getAttribute("name");
352
+ const tag = element.tagName.toLowerCase();
353
+ if (name) return `${tag}[name="${name.replace(/"/g, '\\"')}"]`;
354
+ const parts = [];
355
+ let current = element;
356
+ while (current && current !== document.body && parts.length < 4) {
357
+ const currentTag = current.tagName.toLowerCase();
358
+ const parent = current.parentElement;
359
+ if (!parent) {
360
+ parts.unshift(currentTag);
361
+ break;
362
+ }
363
+ const children = Array.from(parent.children);
364
+ const siblings = children.filter((child) => child.tagName === current.tagName);
365
+ const index = siblings.indexOf(current) + 1;
366
+ parts.unshift(siblings.length > 1 ? `${currentTag}:nth-of-type(${index})` : currentTag);
367
+ current = parent;
368
+ }
369
+ return parts.join(" > ") || tag;
370
+ };
371
+ const candidates = Array.from(document.querySelectorAll(
372
+ 'a, button, input, textarea, select, summary, [role], [contenteditable="true"], [tabindex]:not([tabindex="-1"])'
373
+ ));
374
+ const elements = candidates.map((element, index) => {
375
+ const rect = element.getBoundingClientRect();
376
+ const style = window.getComputedStyle(element);
377
+ const visible = rect.width > 0 && rect.height > 0 && style.display !== "none" && style.visibility !== "hidden" && Number(style.opacity || "1") > 0;
378
+ if (!visible) return null;
379
+ const label = textFor(element);
380
+ return {
381
+ index,
382
+ tag: element.tagName.toLowerCase(),
383
+ role: element.getAttribute("role") || null,
384
+ text: label.slice(0, 180),
385
+ selector: selectorFor(element),
386
+ x: Math.round(rect.left + rect.width / 2),
387
+ y: Math.round(rect.top + rect.height / 2),
388
+ left: Math.round(rect.left),
389
+ top: Math.round(rect.top),
390
+ width: Math.round(rect.width),
391
+ height: Math.round(rect.height)
392
+ };
393
+ }).filter(Boolean).slice(0, 120);
394
+ return {
395
+ text: (document.body?.innerText || "").replace(/\n{3,}/g, "\n\n").slice(0, 3e4),
396
+ elements,
397
+ viewport: {
398
+ width: window.innerWidth,
399
+ height: window.innerHeight,
400
+ devicePixelRatio: window.devicePixelRatio,
401
+ scrollX: window.scrollX,
402
+ scrollY: window.scrollY
403
+ }
404
+ };
405
+ });
406
+ return {
407
+ url: session.page.url(),
408
+ title,
409
+ text: snapshot.text,
410
+ elements: snapshot.elements,
411
+ viewport: snapshot.viewport
412
+ };
413
+ }
414
+ async locate(sessionId, targets) {
415
+ const session = this.requireOpenSession(sessionId);
416
+ this.touch(session);
417
+ const located = [];
418
+ for (const target of targets) {
419
+ try {
420
+ const locator = target.selector ? session.page.locator(target.selector) : session.page.getByText(target.text ?? "", { exact: target.match === "exact" });
421
+ const selected = locator.nth(target.index ?? 0);
422
+ await selected.waitFor({ state: "visible", timeout: 3e3 }).catch(() => void 0);
423
+ const box = await selected.boundingBox();
424
+ const text = await selected.textContent({ timeout: 1e3 }).catch(() => null);
425
+ located.push({
426
+ input: target,
427
+ found: Boolean(box),
428
+ element: box ? {
429
+ left: Math.round(box.x),
430
+ top: Math.round(box.y),
431
+ width: Math.round(box.width),
432
+ height: Math.round(box.height),
433
+ x: Math.round(box.x + box.width / 2),
434
+ y: Math.round(box.y + box.height / 2),
435
+ text: (text ?? "").replace(/\s+/g, " ").trim().slice(0, 220)
436
+ } : null
437
+ });
438
+ } catch (err) {
439
+ located.push({
440
+ input: target,
441
+ found: false,
442
+ element: null,
443
+ error: err instanceof Error ? err.message : String(err)
444
+ });
445
+ }
446
+ }
447
+ const read = await this.read(sessionId);
448
+ return {
449
+ url: read.url,
450
+ title: read.title,
451
+ viewport: read.viewport,
452
+ replay: null,
453
+ targets: located
454
+ };
455
+ }
456
+ async goto(sessionId, url) {
457
+ const session = this.requireOpenSession(sessionId);
458
+ this.touch(session);
459
+ const response = await session.page.goto(url, { waitUntil: "domcontentloaded", timeout: 45e3 }).catch((err) => {
460
+ throw new Error(err instanceof Error ? err.message : String(err));
461
+ });
462
+ await this.waitForPage(session.page);
463
+ return {
464
+ ok: true,
465
+ url: session.page.url(),
466
+ status: response?.status() ?? null,
467
+ mode: "local"
468
+ };
469
+ }
470
+ async click(sessionId, input) {
471
+ const session = this.requireOpenSession(sessionId);
472
+ this.touch(session);
473
+ await session.page.mouse.click(input.x, input.y, {
474
+ button: input.button ?? "left",
475
+ clickCount: input.num_clicks ?? 1
476
+ });
477
+ await this.waitForPage(session.page);
478
+ return { ok: true, mode: "local", url: session.page.url() };
479
+ }
480
+ async type(sessionId, input) {
481
+ const session = this.requireOpenSession(sessionId);
482
+ this.touch(session);
483
+ await session.page.keyboard.type(input.text, { delay: input.delay });
484
+ return { ok: true, mode: "local", url: session.page.url() };
485
+ }
486
+ async scroll(sessionId, input) {
487
+ const session = this.requireOpenSession(sessionId);
488
+ this.touch(session);
489
+ if (typeof input.x === "number" && typeof input.y === "number") {
490
+ await session.page.mouse.move(input.x, input.y);
491
+ }
492
+ await session.page.mouse.wheel((input.delta_x ?? 0) * 120, (input.delta_y ?? 5) * 120);
493
+ await session.page.waitForTimeout(150).catch(() => void 0);
494
+ return { ok: true, mode: "local", url: session.page.url() };
495
+ }
496
+ async press(sessionId, input) {
497
+ const session = this.requireOpenSession(sessionId);
498
+ this.touch(session);
499
+ for (const key of input.keys) {
500
+ await session.page.keyboard.press(normalizeKeyCombo(key));
501
+ }
502
+ await this.waitForPage(session.page);
503
+ return { ok: true, mode: "local", url: session.page.url() };
504
+ }
505
+ async close(sessionId) {
506
+ const session = this.sessions.get(sessionId);
507
+ if (!session || session.status === "closed") {
508
+ return { ok: true, closed: true, already_closed: true, mode: "local" };
509
+ }
510
+ if (session.timeout) clearTimeout(session.timeout);
511
+ session.status = "closed";
512
+ session.closedAt = (/* @__PURE__ */ new Date()).toISOString();
513
+ await session.page.close().catch(() => void 0);
514
+ await this.closeContextIfUnused(session.contextKey);
515
+ return { ok: true, closed: true, mode: "local" };
516
+ }
517
+ list(includeClosed = false) {
518
+ return Array.from(this.sessions.values()).filter((session) => includeClosed || session.status === "open").map(sessionSummary);
519
+ }
520
+ async resolveProfile(profile) {
521
+ const directDir = directProfileDir();
522
+ const requestedName = configuredProfileName(profile);
523
+ if (directDir && (!profile || requestedName === process.env.MCP_SCRAPER_BROWSER_PROFILE?.trim())) {
524
+ return {
525
+ name: requestedName,
526
+ userDataDir: directDir,
527
+ browserExecutablePath: process.env.MCP_SCRAPER_BROWSER_EXECUTABLE?.trim() || defaultChromeExecutablePath() || null,
528
+ manifest: null
529
+ };
530
+ }
531
+ const manifest = await loadImportedBrowserProfile(requestedName);
532
+ return {
533
+ name: manifest.name,
534
+ userDataDir: manifest.userDataDir,
535
+ browserExecutablePath: manifest.browserExecutablePath,
536
+ manifest
537
+ };
538
+ }
539
+ async ensureContext(profile) {
540
+ const existing = this.contexts.get(profile.userDataDir);
541
+ if (existing) return existing;
542
+ const executablePath = launchExecutablePath(profile);
543
+ const context = await import_playwright.chromium.launchPersistentContext(profile.userDataDir, {
544
+ headless: false,
545
+ viewport: { width: 1440, height: 900 },
546
+ acceptDownloads: true,
547
+ ...executablePath ? { executablePath } : {},
548
+ ...ignoreDefaultArgsFor(executablePath) ? { ignoreDefaultArgs: ignoreDefaultArgsFor(executablePath) } : {}
549
+ });
550
+ const state = { key: profile.userDataDir, context, profile };
551
+ context.on("close", () => {
552
+ this.contexts.delete(profile.userDataDir);
553
+ const closedAt = (/* @__PURE__ */ new Date()).toISOString();
554
+ for (const session of this.sessions.values()) {
555
+ if (session.contextKey === profile.userDataDir && session.status === "open") {
556
+ session.status = "closed";
557
+ session.closedAt = closedAt;
558
+ if (session.timeout) clearTimeout(session.timeout);
559
+ }
560
+ }
561
+ });
562
+ this.contexts.set(profile.userDataDir, state);
563
+ return state;
564
+ }
565
+ requireOpenSession(sessionId) {
566
+ const session = this.sessions.get(sessionId);
567
+ if (!session || session.status !== "open") throw new Error(`Local browser session not found or closed: ${sessionId}`);
568
+ return session;
569
+ }
570
+ touch(session) {
571
+ session.lastActionAt = (/* @__PURE__ */ new Date()).toISOString();
572
+ }
573
+ hasOpenSessionForContext(contextKey) {
574
+ return Array.from(this.sessions.values()).some((session) => session.contextKey === contextKey && session.status === "open");
575
+ }
576
+ async closeContextIfUnused(contextKey) {
577
+ if (this.hasOpenSessionForContext(contextKey)) return;
578
+ const state = this.contexts.get(contextKey);
579
+ if (!state) return;
580
+ this.contexts.delete(contextKey);
581
+ await state.context.close().catch(() => void 0);
582
+ }
583
+ async waitForPage(page) {
584
+ await page.waitForLoadState("domcontentloaded", { timeout: 5e3 }).catch(() => void 0);
585
+ await page.waitForTimeout(200).catch(() => void 0);
586
+ }
587
+ };
15
588
 
16
589
  // src/version.ts
17
590
  var PACKAGE_VERSION = "0.2.20";
@@ -34,8 +607,35 @@ var BrowserOpenInputSchema = {
34
607
  label: import_zod.z.string().optional().describe("Optional human label for this session, shown in the watch console."),
35
608
  url: import_zod.z.string().url().optional().describe("Optional URL to navigate to immediately after opening."),
36
609
  profile: import_zod.z.string().optional().describe("Optional saved profile name to load a logged-in session for a site."),
610
+ save_profile_changes: import_zod.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."),
37
611
  timeout_seconds: import_zod.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.")
38
612
  };
613
+ var BrowserProfileListInputSchema = {
614
+ email: import_zod.z.string().optional().describe("Optional Chrome account email or displayed profile name to filter for, for example seo@example.com."),
615
+ user_data_dir: import_zod.z.string().optional().describe("Optional Chrome user data directory. Defaults to the current OS Chrome profile root.")
616
+ };
617
+ var BrowserProfileOnboardInputSchema = {
618
+ email: import_zod.z.string().optional().describe("Optional local Chrome account email to find and convert into a suggested Kernel profile name."),
619
+ profile: import_zod.z.string().optional().describe("Optional Kernel browser profile name to create/load. If omitted, email is used to derive one."),
620
+ user_data_dir: import_zod.z.string().optional().describe("Optional Chrome user data directory for email lookup. Defaults to the current OS Chrome profile root."),
621
+ url: import_zod.z.string().url().optional().describe("Setup URL to open after creating the browser. Defaults to https://accounts.google.com/."),
622
+ label: import_zod.z.string().optional().describe("Optional human label for this setup session."),
623
+ timeout_seconds: import_zod.z.number().int().min(60).max(259200).optional().describe("How long the setup session may live before auto-termination. Defaults to 600.")
624
+ };
625
+ var BrowserProfileImportInputSchema = {
626
+ email: import_zod.z.string().optional().describe("Optional Chrome account email or displayed profile name to select from Chrome Local State."),
627
+ name: import_zod.z.string().optional().describe("Name for the managed MCP Scraper browser profile. Defaults to a slug derived from the selected Chrome account."),
628
+ chrome_profile_directory: import_zod.z.string().optional().describe("Optional Chrome profile directory to import, for example Default or Profile 1."),
629
+ user_data_dir: import_zod.z.string().optional().describe("Optional source Chrome user data directory. Defaults to the current OS Chrome profile root."),
630
+ output_dir: import_zod.z.string().optional().describe("Optional managed profile base directory. Defaults to ~/.mcp-scraper/browser-profiles."),
631
+ browser_executable_path: import_zod.z.string().optional().describe("Optional Chrome executable path for local browser launches. Defaults to the installed Google Chrome path when found."),
632
+ overwrite: import_zod.z.boolean().default(false).describe("Overwrite an existing managed profile clone with the same name.")
633
+ };
634
+ var BrowserProfileSyncInputSchema = {
635
+ name: import_zod.z.string().describe("Managed MCP Scraper browser profile name to refresh from its recorded source Chrome profile."),
636
+ output_dir: import_zod.z.string().optional().describe("Optional managed profile base directory. Defaults to ~/.mcp-scraper/browser-profiles."),
637
+ browser_executable_path: import_zod.z.string().optional().describe("Optional Chrome executable path to store in the refreshed profile manifest.")
638
+ };
39
639
  var BrowserSessionInputSchema = {
40
640
  session_id: import_zod.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.")
41
641
  };
@@ -140,6 +740,43 @@ var BrowserOpenOutputSchema = {
140
740
  hint: import_zod.z.string(),
141
741
  raw: BrowserRawObject.optional()
142
742
  };
743
+ var BrowserProfileListOutputSchema = {
744
+ ok: import_zod.z.boolean(),
745
+ tool: import_zod.z.literal("browser_profile_list"),
746
+ session_id: import_zod.z.null(),
747
+ profiles: import_zod.z.array(BrowserRawObject),
748
+ count: import_zod.z.number().int().min(0),
749
+ hint: import_zod.z.string()
750
+ };
751
+ var BrowserProfileOnboardOutputSchema = {
752
+ ok: import_zod.z.boolean(),
753
+ tool: import_zod.z.literal("browser_profile_onboard"),
754
+ session_id: import_zod.z.string(),
755
+ watch_url: import_zod.z.string(),
756
+ live_view_url: NullableString,
757
+ profile: import_zod.z.string(),
758
+ setup_url: import_zod.z.string(),
759
+ chrome_profile: BrowserRawObject.nullable(),
760
+ next_steps: import_zod.z.array(import_zod.z.string()),
761
+ raw: BrowserRawObject.optional()
762
+ };
763
+ var BrowserProfileImportOutputSchema = {
764
+ ok: import_zod.z.boolean(),
765
+ tool: import_zod.z.literal("browser_profile_import"),
766
+ session_id: import_zod.z.null(),
767
+ profile: BrowserRawObject,
768
+ source_profile: BrowserRawObject,
769
+ next_steps: import_zod.z.array(import_zod.z.string()),
770
+ hint: import_zod.z.string()
771
+ };
772
+ var BrowserProfileSyncOutputSchema = {
773
+ ok: import_zod.z.boolean(),
774
+ tool: import_zod.z.literal("browser_profile_sync"),
775
+ session_id: import_zod.z.null(),
776
+ profile: BrowserRawObject,
777
+ source_profile: BrowserRawObject,
778
+ hint: import_zod.z.string()
779
+ };
143
780
  var BrowserScreenshotOutputSchema = {
144
781
  ...BrowserBaseOutput,
145
782
  tool: import_zod.z.literal("browser_screenshot"),
@@ -240,9 +877,9 @@ var BrowserListSessionsOutputSchema = {
240
877
 
241
878
  // src/mcp/replay-annotator.ts
242
879
  var import_node_child_process = require("child_process");
243
- var import_promises = require("fs/promises");
244
- var import_node_os = require("os");
245
- var import_node_path = require("path");
880
+ var import_promises3 = require("fs/promises");
881
+ var import_node_os3 = require("os");
882
+ var import_node_path4 = require("path");
246
883
  var import_node_util = require("util");
247
884
  var execFileAsync = (0, import_node_util.promisify)(import_node_child_process.execFile);
248
885
  function finiteNumber(value) {
@@ -465,10 +1102,10 @@ function ffmpegFilterPath(path) {
465
1102
  async function annotateReplayVideo(inputFilePath, outputFilePath, options) {
466
1103
  if (!options.annotations.length) throw new Error("annotations must include at least one item");
467
1104
  const size = await videoSize(inputFilePath);
468
- const tmp = await (0, import_promises.mkdtemp)((0, import_node_path.join)((0, import_node_os.tmpdir)(), "mcp-scraper-ass-"));
469
- const assPath = (0, import_node_path.join)(tmp, "annotations.ass");
1105
+ const tmp = await (0, import_promises3.mkdtemp)((0, import_node_path4.join)((0, import_node_os3.tmpdir)(), "mcp-scraper-ass-"));
1106
+ const assPath = (0, import_node_path4.join)(tmp, "annotations.ass");
470
1107
  try {
471
- await (0, import_promises.writeFile)(assPath, buildAssSubtitle(options, size), "utf8");
1108
+ await (0, import_promises3.writeFile)(assPath, buildAssSubtitle(options, size), "utf8");
472
1109
  await execFileAsync("ffmpeg", [
473
1110
  "-y",
474
1111
  "-i",
@@ -485,7 +1122,7 @@ async function annotateReplayVideo(inputFilePath, outputFilePath, options) {
485
1122
  "copy",
486
1123
  outputFilePath
487
1124
  ], { maxBuffer: 1024 * 1024 * 20 });
488
- const out = await (0, import_promises.stat)(outputFilePath);
1125
+ const out = await (0, import_promises3.stat)(outputFilePath);
489
1126
  return {
490
1127
  filePath: outputFilePath,
491
1128
  bytes: out.size,
@@ -494,7 +1131,7 @@ async function annotateReplayVideo(inputFilePath, outputFilePath, options) {
494
1131
  annotationCount: options.annotations.length
495
1132
  };
496
1133
  } finally {
497
- await (0, import_promises.rm)(tmp, { recursive: true, force: true });
1134
+ await (0, import_promises3.rm)(tmp, { recursive: true, force: true });
498
1135
  }
499
1136
  }
500
1137
 
@@ -535,7 +1172,7 @@ function actionResult(tool, sessionId, ok, data, nextRecommendedTool = "browser_
535
1172
  });
536
1173
  }
537
1174
  function outputBaseDir() {
538
- return process.env.MCP_SCRAPER_OUTPUT_DIR?.trim() || (0, import_node_path2.join)((0, import_node_os2.homedir)(), "Downloads", "mcp-scraper");
1175
+ return process.env.MCP_SCRAPER_OUTPUT_DIR?.trim() || (0, import_node_path5.join)((0, import_node_os4.homedir)(), "Downloads", "mcp-scraper");
539
1176
  }
540
1177
  function safeFilePart(value) {
541
1178
  return value.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 120) || "replay";
@@ -544,7 +1181,7 @@ function replayFilePath(sessionId, replayId, filename) {
544
1181
  const requested = filename?.trim();
545
1182
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
546
1183
  const name = requested ? safeFilePart(requested).replace(/\.mp4$/i, "") : `${stamp}-${safeFilePart(sessionId)}-${safeFilePart(replayId)}`;
547
- return (0, import_node_path2.join)(outputBaseDir(), "browser-replays", `${name}.mp4`);
1184
+ return (0, import_node_path5.join)(outputBaseDir(), "browser-replays", `${name}.mp4`);
548
1185
  }
549
1186
  function annotatedReplayFilePath(sessionId, replayId, filename) {
550
1187
  const requested = filename?.trim();
@@ -580,6 +1217,7 @@ function registerBrowserAgentMcpTools(server2, opts) {
580
1217
  const baseUrl2 = opts.baseUrl.replace(/\/$/, "");
581
1218
  const consoleBase = (opts.consoleBaseUrl ?? opts.baseUrl).replace(/\/$/, "");
582
1219
  const timeoutMs = opts.timeoutMs ?? 9e4;
1220
+ const localBrowser = new LocalBrowserAgentService();
583
1221
  async function req(method, path, body) {
584
1222
  try {
585
1223
  const res = await fetch(`${baseUrl2}${path}`, {
@@ -608,8 +1246,8 @@ function registerBrowserAgentMcpTools(server2, opts) {
608
1246
  }
609
1247
  const bytes = Buffer.from(await res.arrayBuffer());
610
1248
  const filePath = replayFilePath(sessionId, replayId, filename);
611
- (0, import_node_fs.mkdirSync)((0, import_node_path2.join)(outputBaseDir(), "browser-replays"), { recursive: true });
612
- (0, import_node_fs.writeFileSync)(filePath, bytes);
1249
+ (0, import_node_fs3.mkdirSync)((0, import_node_path5.join)(outputBaseDir(), "browser-replays"), { recursive: true });
1250
+ (0, import_node_fs3.writeFileSync)(filePath, bytes);
613
1251
  return {
614
1252
  ok: true,
615
1253
  data: {
@@ -631,19 +1269,195 @@ function registerBrowserAgentMcpTools(server2, opts) {
631
1269
  idempotentHint: false,
632
1270
  openWorldHint: true
633
1271
  });
1272
+ function selectChromeProfile(matches, email) {
1273
+ const wanted = email?.trim().toLowerCase();
1274
+ if (!wanted) return matches[0] ?? null;
1275
+ return matches.find((profile) => profile.email.toLowerCase() === wanted) ?? matches[0] ?? null;
1276
+ }
1277
+ function localReplayUnsupported(tool, sessionId = null, replayId = null) {
1278
+ return errorResult(tool, {
1279
+ error: "Local browser mode does not support replay recording yet. Use hosted mode for browser_replay_* tools.",
1280
+ mode: "local"
1281
+ }, sessionId, replayId);
1282
+ }
1283
+ server2.registerTool(
1284
+ "browser_profile_list",
1285
+ {
1286
+ title: "List Local Chrome Profiles",
1287
+ 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.",
1288
+ inputSchema: BrowserProfileListInputSchema,
1289
+ outputSchema: BrowserProfileListOutputSchema,
1290
+ annotations: annotations("List Local Chrome Profiles", true)
1291
+ },
1292
+ async (input) => {
1293
+ try {
1294
+ const profiles = await listLocalChromeProfiles(input.user_data_dir, input.email);
1295
+ return structuredResult({
1296
+ ok: true,
1297
+ tool: "browser_profile_list",
1298
+ session_id: null,
1299
+ profiles,
1300
+ count: profiles.length,
1301
+ hint: "Use browser_profile_import to clone one profile for local mode. Use browser_profile_onboard only for hosted Kernel profile setup."
1302
+ });
1303
+ } catch (err) {
1304
+ return errorResult("browser_profile_list", { error: err instanceof Error ? err.message : String(err) });
1305
+ }
1306
+ }
1307
+ );
1308
+ server2.registerTool(
1309
+ "browser_profile_import",
1310
+ {
1311
+ title: "Import Local Chrome Profile",
1312
+ 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.",
1313
+ inputSchema: BrowserProfileImportInputSchema,
1314
+ outputSchema: BrowserProfileImportOutputSchema,
1315
+ annotations: { title: "Import Local Chrome Profile", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }
1316
+ },
1317
+ async (input) => {
1318
+ try {
1319
+ const result = await importChromeProfile({
1320
+ email: input.email,
1321
+ name: input.name,
1322
+ chromeProfileDirectory: input.chrome_profile_directory,
1323
+ sourceUserDataDir: input.user_data_dir,
1324
+ outputDir: input.output_dir,
1325
+ overwrite: input.overwrite,
1326
+ browserExecutablePath: input.browser_executable_path
1327
+ });
1328
+ return structuredResult({
1329
+ ok: true,
1330
+ tool: "browser_profile_import",
1331
+ session_id: null,
1332
+ profile: result.profile,
1333
+ source_profile: result.sourceProfile,
1334
+ next_steps: [
1335
+ `Set MCP_SCRAPER_BROWSER_MODE=local in the MCP server config.`,
1336
+ `Set MCP_SCRAPER_BROWSER_PROFILE=${result.profile.name}.`,
1337
+ "Restart the MCP client so it starts the local browser mode process.",
1338
+ "Call browser_open with the same MCP server; it will open a local Chrome window using the cloned profile."
1339
+ ],
1340
+ hint: "The clone is local to this machine. Re-run browser_profile_sync after logging into new sites in normal Chrome."
1341
+ });
1342
+ } catch (err) {
1343
+ return errorResult("browser_profile_import", { error: err instanceof Error ? err.message : String(err) });
1344
+ }
1345
+ }
1346
+ );
1347
+ server2.registerTool(
1348
+ "browser_profile_sync",
1349
+ {
1350
+ title: "Sync Local Chrome Profile",
1351
+ 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.",
1352
+ inputSchema: BrowserProfileSyncInputSchema,
1353
+ outputSchema: BrowserProfileSyncOutputSchema,
1354
+ annotations: { title: "Sync Local Chrome Profile", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }
1355
+ },
1356
+ async (input) => {
1357
+ try {
1358
+ const result = await syncImportedChromeProfile(input.name, {
1359
+ outputDir: input.output_dir,
1360
+ browserExecutablePath: input.browser_executable_path
1361
+ });
1362
+ return structuredResult({
1363
+ ok: true,
1364
+ tool: "browser_profile_sync",
1365
+ session_id: null,
1366
+ profile: result.profile,
1367
+ source_profile: result.sourceProfile,
1368
+ hint: `Refreshed ${result.profile.name}. Restart local browser sessions that were using this profile.`
1369
+ });
1370
+ } catch (err) {
1371
+ return errorResult("browser_profile_sync", { error: err instanceof Error ? err.message : String(err) });
1372
+ }
1373
+ }
1374
+ );
1375
+ server2.registerTool(
1376
+ "browser_profile_onboard",
1377
+ {
1378
+ title: "Onboard Hosted Browser Profile",
1379
+ 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.",
1380
+ inputSchema: BrowserProfileOnboardInputSchema,
1381
+ outputSchema: BrowserProfileOnboardOutputSchema,
1382
+ annotations: annotations("Onboard Hosted Browser Profile")
1383
+ },
1384
+ async (input) => {
1385
+ let chromeProfile = null;
1386
+ if (input.email) {
1387
+ try {
1388
+ chromeProfile = selectChromeProfile(await listLocalChromeProfiles(input.user_data_dir, input.email), input.email);
1389
+ } catch (err) {
1390
+ return errorResult("browser_profile_onboard", { error: err instanceof Error ? err.message : String(err) });
1391
+ }
1392
+ }
1393
+ const profile = input.profile?.trim() || chromeProfile?.recommendedKernelProfileName || browserServiceProfileName();
1394
+ if (!profile) {
1395
+ return errorResult("browser_profile_onboard", {
1396
+ error: "profile is required when no email match or BROWSER_AGENT_PROFILE_NAME is available"
1397
+ });
1398
+ }
1399
+ const setupUrl = input.url ?? "https://accounts.google.com/";
1400
+ const open = await req("POST", "/agent/sessions", {
1401
+ label: input.label ?? `Profile setup: ${profile}`,
1402
+ profile,
1403
+ save_profile_changes: true,
1404
+ timeout_seconds: input.timeout_seconds
1405
+ });
1406
+ if (!open.ok) return errorResult("browser_profile_onboard", open.data);
1407
+ const session = open.data;
1408
+ const goto = await req("POST", `/agent/sessions/${session.session_id}/goto`, { url: setupUrl });
1409
+ if (!goto.ok) return errorResult("browser_profile_onboard", goto.data, session.session_id);
1410
+ return structuredResult({
1411
+ ok: true,
1412
+ tool: "browser_profile_onboard",
1413
+ session_id: session.session_id,
1414
+ watch_url: `${consoleBase}/console/${session.session_id}`,
1415
+ live_view_url: session.live_view_url ?? null,
1416
+ profile,
1417
+ setup_url: setupUrl,
1418
+ chrome_profile: chromeProfile,
1419
+ next_steps: [
1420
+ "Open the watch_url and complete the login manually.",
1421
+ "After login is complete, call browser_close with this session_id to persist cookies and local storage to the Kernel profile.",
1422
+ "Use BROWSER_AGENT_PROFILE_NAME or browser_open.profile with this profile name for future authenticated sessions."
1423
+ ],
1424
+ raw: session
1425
+ });
1426
+ }
1427
+ );
634
1428
  server2.registerTool(
635
1429
  "browser_open",
636
1430
  {
637
1431
  title: "Open Browser Session",
638
- description: "Open a fresh cloud browser you can drive. Returns a session_id used by all other browser_* tools, and a watch_url where a human can watch live or take over. Anti-bot stealth and automatic CAPTCHA/Cloudflare solving are on by default: if a Cloudflare or CAPTCHA challenge appears, do NOT click it \u2014 wait a few seconds and call browser_screenshot again; it is solved automatically. Billing: metered per second of active browser work at ~4 credits per minute; idle and standby time are free. Call browser_close when done to stop the meter. After opening, call browser_screenshot to see the page.",
1432
+ 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.",
639
1433
  inputSchema: BrowserOpenInputSchema,
640
1434
  outputSchema: BrowserOpenOutputSchema,
641
1435
  annotations: annotations("Open Browser Session")
642
1436
  },
643
1437
  async (input) => {
1438
+ if (localBrowserModeEnabled()) {
1439
+ try {
1440
+ const session2 = await localBrowser.open(input);
1441
+ return structuredResult({
1442
+ ok: true,
1443
+ tool: "browser_open",
1444
+ session_id: session2.session_id,
1445
+ watch_url: session2.watch_url,
1446
+ live_view_url: null,
1447
+ url: input.url ?? null,
1448
+ hint: "Local Chrome is open on this machine. Call browser_screenshot to see the page and click by x,y coordinates.",
1449
+ raw: session2
1450
+ });
1451
+ } catch (err) {
1452
+ return errorResult("browser_open", { error: err instanceof Error ? err.message : String(err), mode: "local" });
1453
+ }
1454
+ }
1455
+ const profile = input.profile ?? browserServiceProfileName();
1456
+ const saveProfileChanges = input.save_profile_changes ?? browserServiceProfileSaveChanges();
644
1457
  const open = await req("POST", "/agent/sessions", {
645
1458
  label: input.label,
646
- profile: input.profile,
1459
+ ...profile ? { profile } : {},
1460
+ ...profile && typeof saveProfileChanges === "boolean" ? { save_profile_changes: saveProfileChanges } : {},
647
1461
  timeout_seconds: input.timeout_seconds
648
1462
  });
649
1463
  if (!open.ok) return errorResult("browser_open", open.data);
@@ -673,6 +1487,27 @@ function registerBrowserAgentMcpTools(server2, opts) {
673
1487
  annotations: annotations("See Page", true)
674
1488
  },
675
1489
  async (input) => {
1490
+ if (localBrowserModeEnabled()) {
1491
+ try {
1492
+ const res2 = await localBrowser.screenshot(input.session_id);
1493
+ const content2 = [];
1494
+ if (res2.image_base64) content2.push({ type: "image", data: res2.image_base64, mimeType: res2.mime_type });
1495
+ const structured2 = {
1496
+ ok: true,
1497
+ tool: "browser_screenshot",
1498
+ session_id: input.session_id,
1499
+ url: res2.url ?? null,
1500
+ title: res2.title ?? null,
1501
+ text: res2.text,
1502
+ elements: res2.elements,
1503
+ screenshot: { mime_type: res2.mime_type, inline: true }
1504
+ };
1505
+ content2.push({ type: "text", text: JSON.stringify(structured2) });
1506
+ return { content: content2, structuredContent: structured2 };
1507
+ } catch (err) {
1508
+ return errorResult("browser_screenshot", { error: err instanceof Error ? err.message : String(err), mode: "local" }, input.session_id);
1509
+ }
1510
+ }
676
1511
  const res = await req("POST", `/agent/sessions/${input.session_id}/screenshot`);
677
1512
  if (!res.ok) return errorResult("browser_screenshot", res.data, input.session_id);
678
1513
  const { image_base64, mime_type, url, title, elements, text } = res.data;
@@ -705,6 +1540,23 @@ function registerBrowserAgentMcpTools(server2, opts) {
705
1540
  annotations: annotations("Read Page", true)
706
1541
  },
707
1542
  async (input) => {
1543
+ if (localBrowserModeEnabled()) {
1544
+ try {
1545
+ const res2 = await localBrowser.read(input.session_id);
1546
+ return structuredResult({
1547
+ ok: true,
1548
+ tool: "browser_read",
1549
+ session_id: input.session_id,
1550
+ url: res2.url ?? null,
1551
+ title: res2.title ?? null,
1552
+ text: res2.text,
1553
+ elements: res2.elements,
1554
+ raw: res2
1555
+ });
1556
+ } catch (err) {
1557
+ return errorResult("browser_read", { error: err instanceof Error ? err.message : String(err), mode: "local" }, input.session_id);
1558
+ }
1559
+ }
708
1560
  const res = await req("POST", `/agent/sessions/${input.session_id}/read`);
709
1561
  if (!res.ok) return errorResult("browser_read", res.data, input.session_id);
710
1562
  return structuredResult({
@@ -729,6 +1581,24 @@ function registerBrowserAgentMcpTools(server2, opts) {
729
1581
  annotations: annotations("Locate DOM Targets", true)
730
1582
  },
731
1583
  async (input) => {
1584
+ if (localBrowserModeEnabled()) {
1585
+ try {
1586
+ const res2 = await localBrowser.locate(input.session_id, input.targets);
1587
+ return structuredResult({
1588
+ ok: true,
1589
+ tool: "browser_locate",
1590
+ session_id: input.session_id,
1591
+ url: res2.url ?? null,
1592
+ title: res2.title ?? null,
1593
+ viewport: res2.viewport ?? null,
1594
+ replay: null,
1595
+ targets: Array.isArray(res2.targets) ? res2.targets : [],
1596
+ raw: res2
1597
+ });
1598
+ } catch (err) {
1599
+ return errorResult("browser_locate", { error: err instanceof Error ? err.message : String(err), mode: "local" }, input.session_id);
1600
+ }
1601
+ }
732
1602
  const res = await req("POST", `/agent/sessions/${input.session_id}/locate`, { targets: input.targets });
733
1603
  if (!res.ok) return errorResult("browser_locate", res.data, input.session_id);
734
1604
  return structuredResult({
@@ -754,6 +1624,14 @@ function registerBrowserAgentMcpTools(server2, opts) {
754
1624
  annotations: annotations("Navigate To URL")
755
1625
  },
756
1626
  async (input) => {
1627
+ if (localBrowserModeEnabled()) {
1628
+ try {
1629
+ const res2 = await localBrowser.goto(input.session_id, input.url);
1630
+ return actionResult("browser_goto", input.session_id, true, res2, "browser_screenshot");
1631
+ } catch (err) {
1632
+ return errorResult("browser_goto", { error: err instanceof Error ? err.message : String(err), mode: "local" }, input.session_id);
1633
+ }
1634
+ }
757
1635
  const res = await req("POST", `/agent/sessions/${input.session_id}/goto`, { url: input.url });
758
1636
  return actionResult("browser_goto", input.session_id, res.ok, res.data, "browser_screenshot");
759
1637
  }
@@ -768,6 +1646,14 @@ function registerBrowserAgentMcpTools(server2, opts) {
768
1646
  annotations: annotations("Click")
769
1647
  },
770
1648
  async (input) => {
1649
+ if (localBrowserModeEnabled()) {
1650
+ try {
1651
+ const res2 = await localBrowser.click(input.session_id, input);
1652
+ return actionResult("browser_click", input.session_id, true, res2, "browser_screenshot");
1653
+ } catch (err) {
1654
+ return errorResult("browser_click", { error: err instanceof Error ? err.message : String(err), mode: "local" }, input.session_id);
1655
+ }
1656
+ }
771
1657
  const res = await req("POST", `/agent/sessions/${input.session_id}/click`, {
772
1658
  x: input.x,
773
1659
  y: input.y,
@@ -787,6 +1673,14 @@ function registerBrowserAgentMcpTools(server2, opts) {
787
1673
  annotations: annotations("Type Text")
788
1674
  },
789
1675
  async (input) => {
1676
+ if (localBrowserModeEnabled()) {
1677
+ try {
1678
+ const res2 = await localBrowser.type(input.session_id, input);
1679
+ return actionResult("browser_type", input.session_id, true, res2, "browser_screenshot");
1680
+ } catch (err) {
1681
+ return errorResult("browser_type", { error: err instanceof Error ? err.message : String(err), mode: "local" }, input.session_id);
1682
+ }
1683
+ }
790
1684
  const res = await req("POST", `/agent/sessions/${input.session_id}/type`, { text: input.text, delay: input.delay });
791
1685
  return actionResult("browser_type", input.session_id, res.ok, res.data, "browser_screenshot");
792
1686
  }
@@ -801,6 +1695,14 @@ function registerBrowserAgentMcpTools(server2, opts) {
801
1695
  annotations: annotations("Scroll")
802
1696
  },
803
1697
  async (input) => {
1698
+ if (localBrowserModeEnabled()) {
1699
+ try {
1700
+ const res2 = await localBrowser.scroll(input.session_id, input);
1701
+ return actionResult("browser_scroll", input.session_id, true, res2, "browser_screenshot");
1702
+ } catch (err) {
1703
+ return errorResult("browser_scroll", { error: err instanceof Error ? err.message : String(err), mode: "local" }, input.session_id);
1704
+ }
1705
+ }
804
1706
  const res = await req("POST", `/agent/sessions/${input.session_id}/scroll`, {
805
1707
  delta_y: input.delta_y,
806
1708
  delta_x: input.delta_x,
@@ -820,6 +1722,14 @@ function registerBrowserAgentMcpTools(server2, opts) {
820
1722
  annotations: annotations("Press Keys")
821
1723
  },
822
1724
  async (input) => {
1725
+ if (localBrowserModeEnabled()) {
1726
+ try {
1727
+ const res2 = await localBrowser.press(input.session_id, input);
1728
+ return actionResult("browser_press", input.session_id, true, res2, "browser_screenshot");
1729
+ } catch (err) {
1730
+ return errorResult("browser_press", { error: err instanceof Error ? err.message : String(err), mode: "local" }, input.session_id);
1731
+ }
1732
+ }
823
1733
  const res = await req("POST", `/agent/sessions/${input.session_id}/press`, { keys: input.keys });
824
1734
  return actionResult("browser_press", input.session_id, res.ok, res.data, "browser_screenshot");
825
1735
  }
@@ -834,6 +1744,7 @@ function registerBrowserAgentMcpTools(server2, opts) {
834
1744
  annotations: annotations("Start Recording")
835
1745
  },
836
1746
  async (input) => {
1747
+ if (localBrowserModeEnabled()) return localReplayUnsupported("browser_replay_start", input.session_id);
837
1748
  const res = await req("POST", `/agent/sessions/${input.session_id}/replay/start`);
838
1749
  if (!res.ok) return errorResult("browser_replay_start", res.data, input.session_id);
839
1750
  return structuredResult({
@@ -857,6 +1768,7 @@ function registerBrowserAgentMcpTools(server2, opts) {
857
1768
  annotations: annotations("Stop Recording")
858
1769
  },
859
1770
  async (input) => {
1771
+ if (localBrowserModeEnabled()) return localReplayUnsupported("browser_replay_stop", input.session_id, input.replay_id);
860
1772
  const res = await req("POST", `/agent/sessions/${input.session_id}/replay/stop`, { replay_id: input.replay_id });
861
1773
  if (!res.ok) return errorResult("browser_replay_stop", res.data, input.session_id, input.replay_id);
862
1774
  return structuredResult({
@@ -880,6 +1792,7 @@ function registerBrowserAgentMcpTools(server2, opts) {
880
1792
  annotations: annotations("List Replay Videos", true)
881
1793
  },
882
1794
  async (input) => {
1795
+ if (localBrowserModeEnabled()) return localReplayUnsupported("browser_list_replays", input.session_id);
883
1796
  const res = await req("GET", `/agent/sessions/${input.session_id}/replays`);
884
1797
  if (!res.ok) return errorResult("browser_list_replays", res.data, input.session_id);
885
1798
  const replays = Array.isArray(res.data?.replays) ? res.data.replays : [];
@@ -902,6 +1815,7 @@ function registerBrowserAgentMcpTools(server2, opts) {
902
1815
  annotations: annotations("Download Replay MP4")
903
1816
  },
904
1817
  async (input) => {
1818
+ if (localBrowserModeEnabled()) return localReplayUnsupported("browser_replay_download", input.session_id, input.replay_id);
905
1819
  const res = await downloadReplay(input.session_id, input.replay_id, input.filename);
906
1820
  if (!res.ok) return errorResult("browser_replay_download", res.data, input.session_id, input.replay_id);
907
1821
  return structuredResult({
@@ -926,6 +1840,7 @@ function registerBrowserAgentMcpTools(server2, opts) {
926
1840
  annotations: annotations("Mark Replay Annotation", true)
927
1841
  },
928
1842
  async (input) => {
1843
+ if (localBrowserModeEnabled()) return localReplayUnsupported("browser_replay_mark", input.session_id);
929
1844
  const res = await req("POST", `/agent/sessions/${input.session_id}/locate`, { targets: [input.target] });
930
1845
  if (!res.ok) return errorResult("browser_replay_mark", res.data, input.session_id);
931
1846
  const target = res.data?.targets?.[0];
@@ -976,13 +1891,14 @@ function registerBrowserAgentMcpTools(server2, opts) {
976
1891
  annotations: annotations("Annotate Replay MP4")
977
1892
  },
978
1893
  async (input) => {
1894
+ if (localBrowserModeEnabled()) return localReplayUnsupported("browser_replay_annotate", input.session_id, input.replay_id);
979
1895
  const sourceName = input.filename ? `${input.filename}-source` : void 0;
980
1896
  const downloaded = await downloadReplay(input.session_id, input.replay_id, sourceName);
981
1897
  if (!downloaded.ok) return errorResult("browser_replay_annotate", downloaded.data, input.session_id, input.replay_id);
982
1898
  try {
983
1899
  const sourcePath = String(downloaded.data.file_path);
984
1900
  const outputPath = annotatedReplayFilePath(input.session_id, input.replay_id, input.filename);
985
- (0, import_node_fs.mkdirSync)((0, import_node_path2.join)(outputBaseDir(), "browser-replays"), { recursive: true });
1901
+ (0, import_node_fs3.mkdirSync)((0, import_node_path5.join)(outputBaseDir(), "browser-replays"), { recursive: true });
986
1902
  const result = await annotateReplayVideo(sourcePath, outputPath, {
987
1903
  annotations: input.annotations,
988
1904
  sourceWidth: input.source_width,
@@ -1018,6 +1934,20 @@ function registerBrowserAgentMcpTools(server2, opts) {
1018
1934
  annotations: { title: "Close Browser Session", readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false }
1019
1935
  },
1020
1936
  async (input) => {
1937
+ if (localBrowserModeEnabled()) {
1938
+ try {
1939
+ const res2 = await localBrowser.close(input.session_id);
1940
+ return structuredResult({
1941
+ ok: true,
1942
+ tool: "browser_close",
1943
+ session_id: input.session_id,
1944
+ closed: true,
1945
+ raw: res2
1946
+ });
1947
+ } catch (err) {
1948
+ return errorResult("browser_close", { error: err instanceof Error ? err.message : String(err), mode: "local" }, input.session_id);
1949
+ }
1950
+ }
1021
1951
  const res = await req("DELETE", `/agent/sessions/${input.session_id}`);
1022
1952
  if (!res.ok) return errorResult("browser_close", res.data, input.session_id);
1023
1953
  return structuredResult({
@@ -1039,6 +1969,16 @@ function registerBrowserAgentMcpTools(server2, opts) {
1039
1969
  annotations: annotations("List Browser Sessions", true)
1040
1970
  },
1041
1971
  async (input) => {
1972
+ if (localBrowserModeEnabled()) {
1973
+ const sessions2 = localBrowser.list(input.include_closed);
1974
+ return structuredResult({
1975
+ ok: true,
1976
+ tool: "browser_list_sessions",
1977
+ session_id: null,
1978
+ sessions: sessions2,
1979
+ count: sessions2.length
1980
+ });
1981
+ }
1042
1982
  const res = await req("GET", `/agent/sessions${input.include_closed ? "?all=1" : ""}`);
1043
1983
  if (!res.ok) return errorResult("browser_list_sessions", res.data);
1044
1984
  const sessions = (res.data.sessions ?? []).map((s) => ({ ...s, watch_url: `${consoleBase}/console/${s.session_id}` }));
@@ -1056,10 +1996,10 @@ function registerBrowserAgentMcpTools(server2, opts) {
1056
1996
  // bin/browser-agent-stdio-server.ts
1057
1997
  function readApiKeyFile() {
1058
1998
  const explicitPath = process.env.MCP_SCRAPER_KEY_PATH?.trim();
1059
- const paths = [explicitPath, (0, import_node_path3.join)((0, import_node_os3.homedir)(), ".mcp-scraper-key")].filter(Boolean);
1999
+ const paths = [explicitPath, (0, import_node_path6.join)((0, import_node_os5.homedir)(), ".mcp-scraper-key")].filter(Boolean);
1060
2000
  for (const path of paths) {
1061
2001
  try {
1062
- const value = (0, import_node_fs2.readFileSync)(path, "utf8").trim();
2002
+ const value = (0, import_node_fs4.readFileSync)(path, "utf8").trim();
1063
2003
  if (value) return value;
1064
2004
  } catch {
1065
2005
  }