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,9 +2,9 @@
2
2
  "use strict";
3
3
 
4
4
  // bin/mcp-scraper-combined-stdio-server.ts
5
- var import_node_fs4 = require("fs");
6
- var import_node_os4 = require("os");
7
- var import_node_path5 = require("path");
5
+ var import_node_fs6 = require("fs");
6
+ var import_node_os6 = require("os");
7
+ var import_node_path8 = require("path");
8
8
  var import_mcp3 = require("@modelcontextprotocol/sdk/server/mcp.js");
9
9
  var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
10
10
 
@@ -246,9 +246,582 @@ var HttpMcpToolExecutor = class {
246
246
 
247
247
  // src/mcp/browser-agent-mcp-server.ts
248
248
  var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
249
+ var import_node_fs3 = require("fs");
250
+ var import_node_os4 = require("os");
251
+ var import_node_path5 = require("path");
252
+
253
+ // src/lib/chrome-profiles.ts
254
+ var import_promises = require("fs/promises");
255
+ var import_node_os = require("os");
256
+ var import_node_path = require("path");
257
+ function defaultChromeUserDataDir() {
258
+ if (process.platform === "darwin") return (0, import_node_path.join)((0, import_node_os.homedir)(), "Library", "Application Support", "Google", "Chrome");
259
+ if (process.platform === "win32") {
260
+ 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");
261
+ }
262
+ return (0, import_node_path.join)(process.env.XDG_CONFIG_HOME ?? (0, import_node_path.join)((0, import_node_os.homedir)(), ".config"), "google-chrome");
263
+ }
264
+ function recommendedKernelProfileName(value) {
265
+ return value.trim().toLowerCase().replace(/@/g, "-").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80) || "browser-profile";
266
+ }
267
+ async function listLocalChromeProfiles(userDataDir = defaultChromeUserDataDir(), emailFilter) {
268
+ const raw = await (0, import_promises.readFile)((0, import_node_path.join)(userDataDir, "Local State"), "utf8");
269
+ const localState = JSON.parse(raw);
270
+ const wanted = emailFilter?.trim().toLowerCase();
271
+ return Object.entries(localState.profile?.info_cache ?? {}).map(([directory, info]) => {
272
+ const email = (info.user_name ?? "").trim();
273
+ const displayName = (info.name ?? info.shortcut_name ?? "").trim();
274
+ const labelSource = email || displayName || directory;
275
+ return {
276
+ email,
277
+ displayName,
278
+ chromeProfileDirectory: directory,
279
+ chromeProfilePath: (0, import_node_path.join)(userDataDir, directory),
280
+ recommendedKernelProfileName: recommendedKernelProfileName(labelSource)
281
+ };
282
+ }).filter((row) => !wanted || row.email.toLowerCase() === wanted || row.displayName.toLowerCase().includes(wanted));
283
+ }
284
+
285
+ // src/lib/browser-service-env.ts
286
+ function browserServiceProfileName() {
287
+ 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();
288
+ return value || void 0;
289
+ }
290
+ function browserServiceProfileSaveChanges() {
291
+ 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();
292
+ if (!value) return void 0;
293
+ if (["1", "true", "yes", "on"].includes(value)) return true;
294
+ if (["0", "false", "no", "off"].includes(value)) return false;
295
+ return void 0;
296
+ }
297
+
298
+ // src/lib/local-browser-profiles.ts
249
299
  var import_node_fs = require("fs");
300
+ var import_promises2 = require("fs/promises");
250
301
  var import_node_os2 = require("os");
251
302
  var import_node_path2 = require("path");
303
+ var EXCLUDED_PROFILE_NAMES = [
304
+ "Cache",
305
+ "Code Cache",
306
+ "Crashpad",
307
+ "DawnCache",
308
+ "DawnGraphiteCache",
309
+ "DawnWebGPUCache",
310
+ "GPUCache",
311
+ "GrShaderCache",
312
+ "ShaderCache",
313
+ "Shared Dictionary"
314
+ ];
315
+ function defaultImportedBrowserProfilesDir() {
316
+ return process.env.MCP_SCRAPER_BROWSER_PROFILE_BASE_DIR?.trim() || (0, import_node_path2.join)((0, import_node_os2.homedir)(), ".mcp-scraper", "browser-profiles");
317
+ }
318
+ function defaultChromeExecutablePath() {
319
+ const envPath = process.env.MCP_SCRAPER_BROWSER_EXECUTABLE?.trim() || process.env.CHROME_PATH?.trim();
320
+ if (envPath) return envPath;
321
+ const candidates = process.platform === "darwin" ? ["/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"] : process.platform === "win32" ? [
322
+ (0, import_node_path2.join)(process.env.PROGRAMFILES ?? "C:\\Program Files", "Google", "Chrome", "Application", "chrome.exe"),
323
+ (0, import_node_path2.join)(process.env["PROGRAMFILES(X86)"] ?? "C:\\Program Files (x86)", "Google", "Chrome", "Application", "chrome.exe")
324
+ ] : ["/usr/bin/google-chrome", "/usr/bin/google-chrome-stable", "/usr/bin/chromium-browser", "/usr/bin/chromium"];
325
+ return candidates.find((candidate) => (0, import_node_fs.existsSync)(candidate));
326
+ }
327
+ function localBrowserModeEnabled() {
328
+ return process.env.MCP_SCRAPER_BROWSER_MODE?.trim().toLowerCase() === "local";
329
+ }
330
+ function importedProfileName(value) {
331
+ return recommendedKernelProfileName(value);
332
+ }
333
+ function importedProfileRoot(name, outputDir = defaultImportedBrowserProfilesDir()) {
334
+ return (0, import_node_path2.join)(outputDir, importedProfileName(name));
335
+ }
336
+ function importedProfileManifestPath(name, outputDir = defaultImportedBrowserProfilesDir()) {
337
+ return (0, import_node_path2.join)(importedProfileRoot(name, outputDir), "manifest.json");
338
+ }
339
+ function importedProfileUserDataDir(name, outputDir = defaultImportedBrowserProfilesDir()) {
340
+ return (0, import_node_path2.join)(importedProfileRoot(name, outputDir), "user-data");
341
+ }
342
+ async function readExistingManifest(path) {
343
+ try {
344
+ return JSON.parse(await (0, import_promises2.readFile)(path, "utf8"));
345
+ } catch {
346
+ return null;
347
+ }
348
+ }
349
+ function isExcludedProfilePath(sourceRoot, sourcePath) {
350
+ const rel = (0, import_node_path2.relative)(sourceRoot, sourcePath);
351
+ if (!rel) return false;
352
+ const parts = rel.split(import_node_path2.sep).filter(Boolean);
353
+ const last = parts[parts.length - 1] ?? "";
354
+ if (last.startsWith("Singleton")) return true;
355
+ if (last === "LOCK" || last.endsWith(".lock")) return true;
356
+ if (parts.some((part) => EXCLUDED_PROFILE_NAMES.includes(part))) return true;
357
+ if (parts.includes("Service Worker") && parts.includes("CacheStorage")) return true;
358
+ return false;
359
+ }
360
+ async function copyProfileTree(sourceProfilePath, destinationProfilePath) {
361
+ await (0, import_promises2.cp)(sourceProfilePath, destinationProfilePath, {
362
+ recursive: true,
363
+ force: true,
364
+ errorOnExist: false,
365
+ dereference: false,
366
+ filter: (source) => !isExcludedProfilePath(sourceProfilePath, source)
367
+ });
368
+ }
369
+ function selectProfile(profiles, options) {
370
+ const requestedDirectory = options.chromeProfileDirectory?.trim();
371
+ const requestedEmail = options.email?.trim().toLowerCase();
372
+ const exactDirectory = requestedDirectory ? profiles.find((profile) => profile.chromeProfileDirectory === requestedDirectory) : void 0;
373
+ if (exactDirectory) return exactDirectory;
374
+ const exactEmail = requestedEmail ? profiles.find((profile) => profile.email.toLowerCase() === requestedEmail) : void 0;
375
+ if (exactEmail) return exactEmail;
376
+ const first = profiles[0];
377
+ if (first) return first;
378
+ const hint = requestedDirectory ? `Chrome profile directory "${requestedDirectory}"` : requestedEmail ? `Chrome account "${requestedEmail}"` : "Chrome profile";
379
+ throw new Error(`${hint} was not found in ${options.sourceUserDataDir ?? defaultChromeUserDataDir()}`);
380
+ }
381
+ async function ensureProfilePathExists(profile) {
382
+ const details = await (0, import_promises2.stat)(profile.chromeProfilePath).catch(() => null);
383
+ if (!details?.isDirectory()) {
384
+ throw new Error(`Chrome profile path does not exist or is not a directory: ${profile.chromeProfilePath}`);
385
+ }
386
+ }
387
+ async function writeNormalizedLocalState(sourceLocalStatePath, destinationLocalStatePath, sourceProfileDirectory) {
388
+ const raw = await (0, import_promises2.readFile)(sourceLocalStatePath, "utf8");
389
+ const localState = JSON.parse(raw);
390
+ const selectedInfo = localState.profile?.info_cache?.[sourceProfileDirectory] ?? localState.profile?.info_cache?.Default ?? {};
391
+ localState.profile = {
392
+ ...localState.profile ?? {},
393
+ info_cache: { Default: selectedInfo },
394
+ last_used: "Default",
395
+ last_active_profiles: ["Default"]
396
+ };
397
+ await (0, import_promises2.writeFile)(destinationLocalStatePath, `${JSON.stringify(localState, null, 2)}
398
+ `);
399
+ }
400
+ async function importChromeProfile(options = {}) {
401
+ const sourceUserDataDir = options.sourceUserDataDir?.trim() || defaultChromeUserDataDir();
402
+ const profiles = await listLocalChromeProfiles(sourceUserDataDir, options.email);
403
+ const sourceProfile = selectProfile(profiles, { ...options, sourceUserDataDir });
404
+ await ensureProfilePathExists(sourceProfile);
405
+ const name = importedProfileName(options.name?.trim() || sourceProfile.recommendedKernelProfileName);
406
+ const outputDir = options.outputDir?.trim() || defaultImportedBrowserProfilesDir();
407
+ const profileRoot = importedProfileRoot(name, outputDir);
408
+ const manifestPath = importedProfileManifestPath(name, outputDir);
409
+ const userDataDir = importedProfileUserDataDir(name, outputDir);
410
+ const existingManifest = await readExistingManifest(manifestPath);
411
+ const existing = (0, import_node_fs.existsSync)(profileRoot);
412
+ if (existing && !options.overwrite) {
413
+ throw new Error(`Imported browser profile "${name}" already exists at ${profileRoot}. Re-run with --overwrite to refresh it.`);
414
+ }
415
+ if (existing) await (0, import_promises2.rm)(profileRoot, { recursive: true, force: true });
416
+ await (0, import_promises2.mkdir)((0, import_node_path2.join)(userDataDir, "Default"), { recursive: true });
417
+ await writeNormalizedLocalState(
418
+ (0, import_node_path2.join)(sourceUserDataDir, "Local State"),
419
+ (0, import_node_path2.join)(userDataDir, "Local State"),
420
+ sourceProfile.chromeProfileDirectory
421
+ );
422
+ await copyProfileTree(sourceProfile.chromeProfilePath, (0, import_node_path2.join)(userDataDir, "Default"));
423
+ const now = (/* @__PURE__ */ new Date()).toISOString();
424
+ const manifest = {
425
+ layoutVersion: 1,
426
+ name,
427
+ profileRoot,
428
+ userDataDir,
429
+ browserExecutablePath: options.browserExecutablePath?.trim() || defaultChromeExecutablePath() || null,
430
+ sourceUserDataDir,
431
+ sourceProfileDirectory: sourceProfile.chromeProfileDirectory,
432
+ sourceProfilePath: sourceProfile.chromeProfilePath,
433
+ email: sourceProfile.email,
434
+ displayName: sourceProfile.displayName,
435
+ createdAt: existingManifest?.createdAt ?? now,
436
+ updatedAt: now,
437
+ copyMode: "chrome-profile-clone"
438
+ };
439
+ await (0, import_promises2.writeFile)(manifestPath, `${JSON.stringify(manifest, null, 2)}
440
+ `);
441
+ return {
442
+ profile: manifest,
443
+ sourceProfile,
444
+ excludedNames: EXCLUDED_PROFILE_NAMES
445
+ };
446
+ }
447
+ async function loadImportedBrowserProfile(name = process.env.MCP_SCRAPER_BROWSER_PROFILE?.trim() || "default", outputDir = defaultImportedBrowserProfilesDir()) {
448
+ const profileName = importedProfileName(name);
449
+ const manifestPath = importedProfileManifestPath(profileName, outputDir);
450
+ const manifest = await readExistingManifest(manifestPath);
451
+ if (!manifest) {
452
+ throw new Error(
453
+ `Imported browser profile "${profileName}" was not found. Run: mcp-scraper-cli browser import-chrome --name ${profileName}`
454
+ );
455
+ }
456
+ return manifest;
457
+ }
458
+ async function syncImportedChromeProfile(name, options = {}) {
459
+ const outputDir = options.outputDir?.trim() || defaultImportedBrowserProfilesDir();
460
+ const manifest = await loadImportedBrowserProfile(name, outputDir);
461
+ return importChromeProfile({
462
+ name: manifest.name,
463
+ sourceUserDataDir: manifest.sourceUserDataDir,
464
+ chromeProfileDirectory: manifest.sourceProfileDirectory,
465
+ outputDir,
466
+ overwrite: true,
467
+ browserExecutablePath: options.browserExecutablePath ?? manifest.browserExecutablePath ?? void 0
468
+ });
469
+ }
470
+
471
+ // src/services/browser-agent/local-browser-agent-service.ts
472
+ var import_node_crypto = require("crypto");
473
+ var import_node_fs2 = require("fs");
474
+ var import_node_path3 = require("path");
475
+ var import_playwright = require("playwright");
476
+ function directProfileDir() {
477
+ return process.env.MCP_SCRAPER_BROWSER_PROFILE_DIR?.trim() || void 0;
478
+ }
479
+ function configuredProfileName(profile) {
480
+ return profile?.trim() || process.env.MCP_SCRAPER_BROWSER_PROFILE?.trim() || (directProfileDir() ? (0, import_node_path3.basename)(directProfileDir()) : "default");
481
+ }
482
+ function launchExecutablePath(profile) {
483
+ const configured = process.env.MCP_SCRAPER_BROWSER_EXECUTABLE?.trim() || profile.browserExecutablePath || defaultChromeExecutablePath();
484
+ return configured && (0, import_node_fs2.existsSync)(configured) ? configured : void 0;
485
+ }
486
+ function ignoreDefaultArgsFor(executablePath) {
487
+ if (!executablePath) return void 0;
488
+ if (process.platform !== "darwin") return void 0;
489
+ if (!/Google Chrome\.app\/Contents\/MacOS\/Google Chrome$/.test(executablePath)) return void 0;
490
+ return ["--password-store=basic", "--use-mock-keychain"];
491
+ }
492
+ function normalizeKeyCombo(value) {
493
+ return value.split("+").map((part) => {
494
+ const trimmed = part.trim();
495
+ const lower = trimmed.toLowerCase();
496
+ if (lower === "ctrl" || lower === "control") return "Control";
497
+ if (lower === "cmd" || lower === "command" || lower === "meta") return "Meta";
498
+ if (lower === "option" || lower === "alt") return "Alt";
499
+ if (lower === "return") return "Enter";
500
+ if (lower === "esc") return "Escape";
501
+ if (trimmed.length === 1) return trimmed.toUpperCase();
502
+ return trimmed;
503
+ }).join("+");
504
+ }
505
+ function sessionSummary(session) {
506
+ return {
507
+ session_id: session.sessionId,
508
+ status: session.status,
509
+ label: session.label,
510
+ profile: session.profileName,
511
+ user_data_dir: session.userDataDir,
512
+ created_at: session.createdAt,
513
+ last_action_at: session.lastActionAt,
514
+ closed_at: session.closedAt,
515
+ watch_url: `local://browser-session/${session.sessionId}`,
516
+ url: session.status === "open" ? session.page.url() : null
517
+ };
518
+ }
519
+ var LocalBrowserAgentService = class {
520
+ contexts = /* @__PURE__ */ new Map();
521
+ sessions = /* @__PURE__ */ new Map();
522
+ async open(input) {
523
+ const profile = await this.resolveProfile(input.profile);
524
+ const contextState = await this.ensureContext(profile);
525
+ const pages = contextState.context.pages();
526
+ const page = pages.length === 1 && !this.hasOpenSessionForContext(contextState.key) ? pages[0] : await contextState.context.newPage();
527
+ const now = (/* @__PURE__ */ new Date()).toISOString();
528
+ const sessionId = `lbs_${(0, import_node_crypto.randomUUID)().replace(/-/g, "").slice(0, 20)}`;
529
+ const session = {
530
+ sessionId,
531
+ label: input.label ?? null,
532
+ page,
533
+ contextKey: contextState.key,
534
+ profileName: profile.name,
535
+ userDataDir: profile.userDataDir,
536
+ createdAt: now,
537
+ lastActionAt: now,
538
+ closedAt: null,
539
+ status: "open"
540
+ };
541
+ const timeoutSeconds = input.timeout_seconds ?? 600;
542
+ if (timeoutSeconds > 0) {
543
+ session.timeout = setTimeout(() => {
544
+ void this.close(sessionId);
545
+ }, timeoutSeconds * 1e3);
546
+ session.timeout.unref();
547
+ }
548
+ this.sessions.set(sessionId, session);
549
+ if (input.url) await this.goto(sessionId, input.url);
550
+ return {
551
+ session_id: sessionId,
552
+ watch_url: `local://browser-session/${sessionId}`,
553
+ live_view_url: null,
554
+ profile: profile.name,
555
+ user_data_dir: profile.userDataDir,
556
+ url: input.url ?? null,
557
+ mode: "local"
558
+ };
559
+ }
560
+ async screenshot(sessionId) {
561
+ const session = this.requireOpenSession(sessionId);
562
+ await this.waitForPage(session.page);
563
+ const image = await session.page.screenshot({ type: "png", fullPage: false });
564
+ const page = await this.read(sessionId);
565
+ return {
566
+ ...page,
567
+ image_base64: image.toString("base64"),
568
+ mime_type: "image/png"
569
+ };
570
+ }
571
+ async read(sessionId) {
572
+ const session = this.requireOpenSession(sessionId);
573
+ await this.waitForPage(session.page);
574
+ this.touch(session);
575
+ const title = await session.page.title().catch(() => "");
576
+ const snapshot = await session.page.evaluate(() => {
577
+ const cssEscape = (value) => {
578
+ const escapeFn = globalThis.CSS?.escape;
579
+ return escapeFn ? escapeFn(value) : value.replace(/["\\#.:>+~[\]()]/g, "\\$&");
580
+ };
581
+ const textFor = (element) => {
582
+ const html = element;
583
+ return (element.getAttribute("aria-label") || element.getAttribute("title") || html.placeholder || html.value || html.innerText || element.textContent || "").replace(/\s+/g, " ").trim();
584
+ };
585
+ const selectorFor = (element) => {
586
+ const id = element.id;
587
+ if (id) return `#${cssEscape(id)}`;
588
+ const name = element.getAttribute("name");
589
+ const tag = element.tagName.toLowerCase();
590
+ if (name) return `${tag}[name="${name.replace(/"/g, '\\"')}"]`;
591
+ const parts = [];
592
+ let current = element;
593
+ while (current && current !== document.body && parts.length < 4) {
594
+ const currentTag = current.tagName.toLowerCase();
595
+ const parent = current.parentElement;
596
+ if (!parent) {
597
+ parts.unshift(currentTag);
598
+ break;
599
+ }
600
+ const children = Array.from(parent.children);
601
+ const siblings = children.filter((child) => child.tagName === current.tagName);
602
+ const index = siblings.indexOf(current) + 1;
603
+ parts.unshift(siblings.length > 1 ? `${currentTag}:nth-of-type(${index})` : currentTag);
604
+ current = parent;
605
+ }
606
+ return parts.join(" > ") || tag;
607
+ };
608
+ const candidates = Array.from(document.querySelectorAll(
609
+ 'a, button, input, textarea, select, summary, [role], [contenteditable="true"], [tabindex]:not([tabindex="-1"])'
610
+ ));
611
+ const elements = candidates.map((element, index) => {
612
+ const rect = element.getBoundingClientRect();
613
+ const style = window.getComputedStyle(element);
614
+ const visible = rect.width > 0 && rect.height > 0 && style.display !== "none" && style.visibility !== "hidden" && Number(style.opacity || "1") > 0;
615
+ if (!visible) return null;
616
+ const label = textFor(element);
617
+ return {
618
+ index,
619
+ tag: element.tagName.toLowerCase(),
620
+ role: element.getAttribute("role") || null,
621
+ text: label.slice(0, 180),
622
+ selector: selectorFor(element),
623
+ x: Math.round(rect.left + rect.width / 2),
624
+ y: Math.round(rect.top + rect.height / 2),
625
+ left: Math.round(rect.left),
626
+ top: Math.round(rect.top),
627
+ width: Math.round(rect.width),
628
+ height: Math.round(rect.height)
629
+ };
630
+ }).filter(Boolean).slice(0, 120);
631
+ return {
632
+ text: (document.body?.innerText || "").replace(/\n{3,}/g, "\n\n").slice(0, 3e4),
633
+ elements,
634
+ viewport: {
635
+ width: window.innerWidth,
636
+ height: window.innerHeight,
637
+ devicePixelRatio: window.devicePixelRatio,
638
+ scrollX: window.scrollX,
639
+ scrollY: window.scrollY
640
+ }
641
+ };
642
+ });
643
+ return {
644
+ url: session.page.url(),
645
+ title,
646
+ text: snapshot.text,
647
+ elements: snapshot.elements,
648
+ viewport: snapshot.viewport
649
+ };
650
+ }
651
+ async locate(sessionId, targets) {
652
+ const session = this.requireOpenSession(sessionId);
653
+ this.touch(session);
654
+ const located = [];
655
+ for (const target of targets) {
656
+ try {
657
+ const locator = target.selector ? session.page.locator(target.selector) : session.page.getByText(target.text ?? "", { exact: target.match === "exact" });
658
+ const selected = locator.nth(target.index ?? 0);
659
+ await selected.waitFor({ state: "visible", timeout: 3e3 }).catch(() => void 0);
660
+ const box = await selected.boundingBox();
661
+ const text = await selected.textContent({ timeout: 1e3 }).catch(() => null);
662
+ located.push({
663
+ input: target,
664
+ found: Boolean(box),
665
+ element: box ? {
666
+ left: Math.round(box.x),
667
+ top: Math.round(box.y),
668
+ width: Math.round(box.width),
669
+ height: Math.round(box.height),
670
+ x: Math.round(box.x + box.width / 2),
671
+ y: Math.round(box.y + box.height / 2),
672
+ text: (text ?? "").replace(/\s+/g, " ").trim().slice(0, 220)
673
+ } : null
674
+ });
675
+ } catch (err) {
676
+ located.push({
677
+ input: target,
678
+ found: false,
679
+ element: null,
680
+ error: err instanceof Error ? err.message : String(err)
681
+ });
682
+ }
683
+ }
684
+ const read = await this.read(sessionId);
685
+ return {
686
+ url: read.url,
687
+ title: read.title,
688
+ viewport: read.viewport,
689
+ replay: null,
690
+ targets: located
691
+ };
692
+ }
693
+ async goto(sessionId, url) {
694
+ const session = this.requireOpenSession(sessionId);
695
+ this.touch(session);
696
+ const response = await session.page.goto(url, { waitUntil: "domcontentloaded", timeout: 45e3 }).catch((err) => {
697
+ throw new Error(err instanceof Error ? err.message : String(err));
698
+ });
699
+ await this.waitForPage(session.page);
700
+ return {
701
+ ok: true,
702
+ url: session.page.url(),
703
+ status: response?.status() ?? null,
704
+ mode: "local"
705
+ };
706
+ }
707
+ async click(sessionId, input) {
708
+ const session = this.requireOpenSession(sessionId);
709
+ this.touch(session);
710
+ await session.page.mouse.click(input.x, input.y, {
711
+ button: input.button ?? "left",
712
+ clickCount: input.num_clicks ?? 1
713
+ });
714
+ await this.waitForPage(session.page);
715
+ return { ok: true, mode: "local", url: session.page.url() };
716
+ }
717
+ async type(sessionId, input) {
718
+ const session = this.requireOpenSession(sessionId);
719
+ this.touch(session);
720
+ await session.page.keyboard.type(input.text, { delay: input.delay });
721
+ return { ok: true, mode: "local", url: session.page.url() };
722
+ }
723
+ async scroll(sessionId, input) {
724
+ const session = this.requireOpenSession(sessionId);
725
+ this.touch(session);
726
+ if (typeof input.x === "number" && typeof input.y === "number") {
727
+ await session.page.mouse.move(input.x, input.y);
728
+ }
729
+ await session.page.mouse.wheel((input.delta_x ?? 0) * 120, (input.delta_y ?? 5) * 120);
730
+ await session.page.waitForTimeout(150).catch(() => void 0);
731
+ return { ok: true, mode: "local", url: session.page.url() };
732
+ }
733
+ async press(sessionId, input) {
734
+ const session = this.requireOpenSession(sessionId);
735
+ this.touch(session);
736
+ for (const key of input.keys) {
737
+ await session.page.keyboard.press(normalizeKeyCombo(key));
738
+ }
739
+ await this.waitForPage(session.page);
740
+ return { ok: true, mode: "local", url: session.page.url() };
741
+ }
742
+ async close(sessionId) {
743
+ const session = this.sessions.get(sessionId);
744
+ if (!session || session.status === "closed") {
745
+ return { ok: true, closed: true, already_closed: true, mode: "local" };
746
+ }
747
+ if (session.timeout) clearTimeout(session.timeout);
748
+ session.status = "closed";
749
+ session.closedAt = (/* @__PURE__ */ new Date()).toISOString();
750
+ await session.page.close().catch(() => void 0);
751
+ await this.closeContextIfUnused(session.contextKey);
752
+ return { ok: true, closed: true, mode: "local" };
753
+ }
754
+ list(includeClosed = false) {
755
+ return Array.from(this.sessions.values()).filter((session) => includeClosed || session.status === "open").map(sessionSummary);
756
+ }
757
+ async resolveProfile(profile) {
758
+ const directDir = directProfileDir();
759
+ const requestedName = configuredProfileName(profile);
760
+ if (directDir && (!profile || requestedName === process.env.MCP_SCRAPER_BROWSER_PROFILE?.trim())) {
761
+ return {
762
+ name: requestedName,
763
+ userDataDir: directDir,
764
+ browserExecutablePath: process.env.MCP_SCRAPER_BROWSER_EXECUTABLE?.trim() || defaultChromeExecutablePath() || null,
765
+ manifest: null
766
+ };
767
+ }
768
+ const manifest = await loadImportedBrowserProfile(requestedName);
769
+ return {
770
+ name: manifest.name,
771
+ userDataDir: manifest.userDataDir,
772
+ browserExecutablePath: manifest.browserExecutablePath,
773
+ manifest
774
+ };
775
+ }
776
+ async ensureContext(profile) {
777
+ const existing = this.contexts.get(profile.userDataDir);
778
+ if (existing) return existing;
779
+ const executablePath = launchExecutablePath(profile);
780
+ const context = await import_playwright.chromium.launchPersistentContext(profile.userDataDir, {
781
+ headless: false,
782
+ viewport: { width: 1440, height: 900 },
783
+ acceptDownloads: true,
784
+ ...executablePath ? { executablePath } : {},
785
+ ...ignoreDefaultArgsFor(executablePath) ? { ignoreDefaultArgs: ignoreDefaultArgsFor(executablePath) } : {}
786
+ });
787
+ const state = { key: profile.userDataDir, context, profile };
788
+ context.on("close", () => {
789
+ this.contexts.delete(profile.userDataDir);
790
+ const closedAt = (/* @__PURE__ */ new Date()).toISOString();
791
+ for (const session of this.sessions.values()) {
792
+ if (session.contextKey === profile.userDataDir && session.status === "open") {
793
+ session.status = "closed";
794
+ session.closedAt = closedAt;
795
+ if (session.timeout) clearTimeout(session.timeout);
796
+ }
797
+ }
798
+ });
799
+ this.contexts.set(profile.userDataDir, state);
800
+ return state;
801
+ }
802
+ requireOpenSession(sessionId) {
803
+ const session = this.sessions.get(sessionId);
804
+ if (!session || session.status !== "open") throw new Error(`Local browser session not found or closed: ${sessionId}`);
805
+ return session;
806
+ }
807
+ touch(session) {
808
+ session.lastActionAt = (/* @__PURE__ */ new Date()).toISOString();
809
+ }
810
+ hasOpenSessionForContext(contextKey) {
811
+ return Array.from(this.sessions.values()).some((session) => session.contextKey === contextKey && session.status === "open");
812
+ }
813
+ async closeContextIfUnused(contextKey) {
814
+ if (this.hasOpenSessionForContext(contextKey)) return;
815
+ const state = this.contexts.get(contextKey);
816
+ if (!state) return;
817
+ this.contexts.delete(contextKey);
818
+ await state.context.close().catch(() => void 0);
819
+ }
820
+ async waitForPage(page) {
821
+ await page.waitForLoadState("domcontentloaded", { timeout: 5e3 }).catch(() => void 0);
822
+ await page.waitForTimeout(200).catch(() => void 0);
823
+ }
824
+ };
252
825
 
253
826
  // src/version.ts
254
827
  var PACKAGE_VERSION = "0.2.20";
@@ -271,8 +844,35 @@ var BrowserOpenInputSchema = {
271
844
  label: import_zod.z.string().optional().describe("Optional human label for this session, shown in the watch console."),
272
845
  url: import_zod.z.string().url().optional().describe("Optional URL to navigate to immediately after opening."),
273
846
  profile: import_zod.z.string().optional().describe("Optional saved profile name to load a logged-in session for a site."),
847
+ 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."),
274
848
  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.")
275
849
  };
850
+ var BrowserProfileListInputSchema = {
851
+ email: import_zod.z.string().optional().describe("Optional Chrome account email or displayed profile name to filter for, for example seo@example.com."),
852
+ user_data_dir: import_zod.z.string().optional().describe("Optional Chrome user data directory. Defaults to the current OS Chrome profile root.")
853
+ };
854
+ var BrowserProfileOnboardInputSchema = {
855
+ email: import_zod.z.string().optional().describe("Optional local Chrome account email to find and convert into a suggested Kernel profile name."),
856
+ profile: import_zod.z.string().optional().describe("Optional Kernel browser profile name to create/load. If omitted, email is used to derive one."),
857
+ 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."),
858
+ url: import_zod.z.string().url().optional().describe("Setup URL to open after creating the browser. Defaults to https://accounts.google.com/."),
859
+ label: import_zod.z.string().optional().describe("Optional human label for this setup session."),
860
+ 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.")
861
+ };
862
+ var BrowserProfileImportInputSchema = {
863
+ email: import_zod.z.string().optional().describe("Optional Chrome account email or displayed profile name to select from Chrome Local State."),
864
+ 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."),
865
+ chrome_profile_directory: import_zod.z.string().optional().describe("Optional Chrome profile directory to import, for example Default or Profile 1."),
866
+ user_data_dir: import_zod.z.string().optional().describe("Optional source Chrome user data directory. Defaults to the current OS Chrome profile root."),
867
+ output_dir: import_zod.z.string().optional().describe("Optional managed profile base directory. Defaults to ~/.mcp-scraper/browser-profiles."),
868
+ 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."),
869
+ overwrite: import_zod.z.boolean().default(false).describe("Overwrite an existing managed profile clone with the same name.")
870
+ };
871
+ var BrowserProfileSyncInputSchema = {
872
+ name: import_zod.z.string().describe("Managed MCP Scraper browser profile name to refresh from its recorded source Chrome profile."),
873
+ output_dir: import_zod.z.string().optional().describe("Optional managed profile base directory. Defaults to ~/.mcp-scraper/browser-profiles."),
874
+ browser_executable_path: import_zod.z.string().optional().describe("Optional Chrome executable path to store in the refreshed profile manifest.")
875
+ };
276
876
  var BrowserSessionInputSchema = {
277
877
  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.")
278
878
  };
@@ -377,6 +977,43 @@ var BrowserOpenOutputSchema = {
377
977
  hint: import_zod.z.string(),
378
978
  raw: BrowserRawObject.optional()
379
979
  };
980
+ var BrowserProfileListOutputSchema = {
981
+ ok: import_zod.z.boolean(),
982
+ tool: import_zod.z.literal("browser_profile_list"),
983
+ session_id: import_zod.z.null(),
984
+ profiles: import_zod.z.array(BrowserRawObject),
985
+ count: import_zod.z.number().int().min(0),
986
+ hint: import_zod.z.string()
987
+ };
988
+ var BrowserProfileOnboardOutputSchema = {
989
+ ok: import_zod.z.boolean(),
990
+ tool: import_zod.z.literal("browser_profile_onboard"),
991
+ session_id: import_zod.z.string(),
992
+ watch_url: import_zod.z.string(),
993
+ live_view_url: NullableString,
994
+ profile: import_zod.z.string(),
995
+ setup_url: import_zod.z.string(),
996
+ chrome_profile: BrowserRawObject.nullable(),
997
+ next_steps: import_zod.z.array(import_zod.z.string()),
998
+ raw: BrowserRawObject.optional()
999
+ };
1000
+ var BrowserProfileImportOutputSchema = {
1001
+ ok: import_zod.z.boolean(),
1002
+ tool: import_zod.z.literal("browser_profile_import"),
1003
+ session_id: import_zod.z.null(),
1004
+ profile: BrowserRawObject,
1005
+ source_profile: BrowserRawObject,
1006
+ next_steps: import_zod.z.array(import_zod.z.string()),
1007
+ hint: import_zod.z.string()
1008
+ };
1009
+ var BrowserProfileSyncOutputSchema = {
1010
+ ok: import_zod.z.boolean(),
1011
+ tool: import_zod.z.literal("browser_profile_sync"),
1012
+ session_id: import_zod.z.null(),
1013
+ profile: BrowserRawObject,
1014
+ source_profile: BrowserRawObject,
1015
+ hint: import_zod.z.string()
1016
+ };
380
1017
  var BrowserScreenshotOutputSchema = {
381
1018
  ...BrowserBaseOutput,
382
1019
  tool: import_zod.z.literal("browser_screenshot"),
@@ -477,9 +1114,9 @@ var BrowserListSessionsOutputSchema = {
477
1114
 
478
1115
  // src/mcp/replay-annotator.ts
479
1116
  var import_node_child_process = require("child_process");
480
- var import_promises = require("fs/promises");
481
- var import_node_os = require("os");
482
- var import_node_path = require("path");
1117
+ var import_promises3 = require("fs/promises");
1118
+ var import_node_os3 = require("os");
1119
+ var import_node_path4 = require("path");
483
1120
  var import_node_util = require("util");
484
1121
  var execFileAsync = (0, import_node_util.promisify)(import_node_child_process.execFile);
485
1122
  function finiteNumber(value) {
@@ -702,10 +1339,10 @@ function ffmpegFilterPath(path) {
702
1339
  async function annotateReplayVideo(inputFilePath, outputFilePath, options) {
703
1340
  if (!options.annotations.length) throw new Error("annotations must include at least one item");
704
1341
  const size = await videoSize(inputFilePath);
705
- const tmp = await (0, import_promises.mkdtemp)((0, import_node_path.join)((0, import_node_os.tmpdir)(), "mcp-scraper-ass-"));
706
- const assPath = (0, import_node_path.join)(tmp, "annotations.ass");
1342
+ const tmp = await (0, import_promises3.mkdtemp)((0, import_node_path4.join)((0, import_node_os3.tmpdir)(), "mcp-scraper-ass-"));
1343
+ const assPath = (0, import_node_path4.join)(tmp, "annotations.ass");
707
1344
  try {
708
- await (0, import_promises.writeFile)(assPath, buildAssSubtitle(options, size), "utf8");
1345
+ await (0, import_promises3.writeFile)(assPath, buildAssSubtitle(options, size), "utf8");
709
1346
  await execFileAsync("ffmpeg", [
710
1347
  "-y",
711
1348
  "-i",
@@ -722,7 +1359,7 @@ async function annotateReplayVideo(inputFilePath, outputFilePath, options) {
722
1359
  "copy",
723
1360
  outputFilePath
724
1361
  ], { maxBuffer: 1024 * 1024 * 20 });
725
- const out = await (0, import_promises.stat)(outputFilePath);
1362
+ const out = await (0, import_promises3.stat)(outputFilePath);
726
1363
  return {
727
1364
  filePath: outputFilePath,
728
1365
  bytes: out.size,
@@ -731,7 +1368,7 @@ async function annotateReplayVideo(inputFilePath, outputFilePath, options) {
731
1368
  annotationCount: options.annotations.length
732
1369
  };
733
1370
  } finally {
734
- await (0, import_promises.rm)(tmp, { recursive: true, force: true });
1371
+ await (0, import_promises3.rm)(tmp, { recursive: true, force: true });
735
1372
  }
736
1373
  }
737
1374
 
@@ -772,7 +1409,7 @@ function actionResult(tool, sessionId, ok, data, nextRecommendedTool = "browser_
772
1409
  });
773
1410
  }
774
1411
  function outputBaseDir() {
775
- return process.env.MCP_SCRAPER_OUTPUT_DIR?.trim() || (0, import_node_path2.join)((0, import_node_os2.homedir)(), "Downloads", "mcp-scraper");
1412
+ return process.env.MCP_SCRAPER_OUTPUT_DIR?.trim() || (0, import_node_path5.join)((0, import_node_os4.homedir)(), "Downloads", "mcp-scraper");
776
1413
  }
777
1414
  function safeFilePart(value) {
778
1415
  return value.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 120) || "replay";
@@ -781,7 +1418,7 @@ function replayFilePath(sessionId, replayId, filename) {
781
1418
  const requested = filename?.trim();
782
1419
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
783
1420
  const name = requested ? safeFilePart(requested).replace(/\.mp4$/i, "") : `${stamp}-${safeFilePart(sessionId)}-${safeFilePart(replayId)}`;
784
- return (0, import_node_path2.join)(outputBaseDir(), "browser-replays", `${name}.mp4`);
1421
+ return (0, import_node_path5.join)(outputBaseDir(), "browser-replays", `${name}.mp4`);
785
1422
  }
786
1423
  function annotatedReplayFilePath(sessionId, replayId, filename) {
787
1424
  const requested = filename?.trim();
@@ -812,6 +1449,7 @@ function registerBrowserAgentMcpTools(server2, opts) {
812
1449
  const baseUrl2 = opts.baseUrl.replace(/\/$/, "");
813
1450
  const consoleBase = (opts.consoleBaseUrl ?? opts.baseUrl).replace(/\/$/, "");
814
1451
  const timeoutMs = opts.timeoutMs ?? 9e4;
1452
+ const localBrowser = new LocalBrowserAgentService();
815
1453
  async function req(method, path, body) {
816
1454
  try {
817
1455
  const res = await fetch(`${baseUrl2}${path}`, {
@@ -840,8 +1478,8 @@ function registerBrowserAgentMcpTools(server2, opts) {
840
1478
  }
841
1479
  const bytes = Buffer.from(await res.arrayBuffer());
842
1480
  const filePath = replayFilePath(sessionId, replayId, filename);
843
- (0, import_node_fs.mkdirSync)((0, import_node_path2.join)(outputBaseDir(), "browser-replays"), { recursive: true });
844
- (0, import_node_fs.writeFileSync)(filePath, bytes);
1481
+ (0, import_node_fs3.mkdirSync)((0, import_node_path5.join)(outputBaseDir(), "browser-replays"), { recursive: true });
1482
+ (0, import_node_fs3.writeFileSync)(filePath, bytes);
845
1483
  return {
846
1484
  ok: true,
847
1485
  data: {
@@ -863,19 +1501,195 @@ function registerBrowserAgentMcpTools(server2, opts) {
863
1501
  idempotentHint: false,
864
1502
  openWorldHint: true
865
1503
  });
1504
+ function selectChromeProfile(matches, email) {
1505
+ const wanted = email?.trim().toLowerCase();
1506
+ if (!wanted) return matches[0] ?? null;
1507
+ return matches.find((profile) => profile.email.toLowerCase() === wanted) ?? matches[0] ?? null;
1508
+ }
1509
+ function localReplayUnsupported(tool, sessionId = null, replayId = null) {
1510
+ return errorResult(tool, {
1511
+ error: "Local browser mode does not support replay recording yet. Use hosted mode for browser_replay_* tools.",
1512
+ mode: "local"
1513
+ }, sessionId, replayId);
1514
+ }
1515
+ server2.registerTool(
1516
+ "browser_profile_list",
1517
+ {
1518
+ title: "List Local Chrome Profiles",
1519
+ 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.",
1520
+ inputSchema: BrowserProfileListInputSchema,
1521
+ outputSchema: BrowserProfileListOutputSchema,
1522
+ annotations: annotations("List Local Chrome Profiles", true)
1523
+ },
1524
+ async (input) => {
1525
+ try {
1526
+ const profiles = await listLocalChromeProfiles(input.user_data_dir, input.email);
1527
+ return structuredResult({
1528
+ ok: true,
1529
+ tool: "browser_profile_list",
1530
+ session_id: null,
1531
+ profiles,
1532
+ count: profiles.length,
1533
+ hint: "Use browser_profile_import to clone one profile for local mode. Use browser_profile_onboard only for hosted Kernel profile setup."
1534
+ });
1535
+ } catch (err) {
1536
+ return errorResult("browser_profile_list", { error: err instanceof Error ? err.message : String(err) });
1537
+ }
1538
+ }
1539
+ );
1540
+ server2.registerTool(
1541
+ "browser_profile_import",
1542
+ {
1543
+ title: "Import Local Chrome Profile",
1544
+ 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.",
1545
+ inputSchema: BrowserProfileImportInputSchema,
1546
+ outputSchema: BrowserProfileImportOutputSchema,
1547
+ annotations: { title: "Import Local Chrome Profile", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }
1548
+ },
1549
+ async (input) => {
1550
+ try {
1551
+ const result = await importChromeProfile({
1552
+ email: input.email,
1553
+ name: input.name,
1554
+ chromeProfileDirectory: input.chrome_profile_directory,
1555
+ sourceUserDataDir: input.user_data_dir,
1556
+ outputDir: input.output_dir,
1557
+ overwrite: input.overwrite,
1558
+ browserExecutablePath: input.browser_executable_path
1559
+ });
1560
+ return structuredResult({
1561
+ ok: true,
1562
+ tool: "browser_profile_import",
1563
+ session_id: null,
1564
+ profile: result.profile,
1565
+ source_profile: result.sourceProfile,
1566
+ next_steps: [
1567
+ `Set MCP_SCRAPER_BROWSER_MODE=local in the MCP server config.`,
1568
+ `Set MCP_SCRAPER_BROWSER_PROFILE=${result.profile.name}.`,
1569
+ "Restart the MCP client so it starts the local browser mode process.",
1570
+ "Call browser_open with the same MCP server; it will open a local Chrome window using the cloned profile."
1571
+ ],
1572
+ hint: "The clone is local to this machine. Re-run browser_profile_sync after logging into new sites in normal Chrome."
1573
+ });
1574
+ } catch (err) {
1575
+ return errorResult("browser_profile_import", { error: err instanceof Error ? err.message : String(err) });
1576
+ }
1577
+ }
1578
+ );
1579
+ server2.registerTool(
1580
+ "browser_profile_sync",
1581
+ {
1582
+ title: "Sync Local Chrome Profile",
1583
+ 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.",
1584
+ inputSchema: BrowserProfileSyncInputSchema,
1585
+ outputSchema: BrowserProfileSyncOutputSchema,
1586
+ annotations: { title: "Sync Local Chrome Profile", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }
1587
+ },
1588
+ async (input) => {
1589
+ try {
1590
+ const result = await syncImportedChromeProfile(input.name, {
1591
+ outputDir: input.output_dir,
1592
+ browserExecutablePath: input.browser_executable_path
1593
+ });
1594
+ return structuredResult({
1595
+ ok: true,
1596
+ tool: "browser_profile_sync",
1597
+ session_id: null,
1598
+ profile: result.profile,
1599
+ source_profile: result.sourceProfile,
1600
+ hint: `Refreshed ${result.profile.name}. Restart local browser sessions that were using this profile.`
1601
+ });
1602
+ } catch (err) {
1603
+ return errorResult("browser_profile_sync", { error: err instanceof Error ? err.message : String(err) });
1604
+ }
1605
+ }
1606
+ );
1607
+ server2.registerTool(
1608
+ "browser_profile_onboard",
1609
+ {
1610
+ title: "Onboard Hosted Browser Profile",
1611
+ 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.",
1612
+ inputSchema: BrowserProfileOnboardInputSchema,
1613
+ outputSchema: BrowserProfileOnboardOutputSchema,
1614
+ annotations: annotations("Onboard Hosted Browser Profile")
1615
+ },
1616
+ async (input) => {
1617
+ let chromeProfile = null;
1618
+ if (input.email) {
1619
+ try {
1620
+ chromeProfile = selectChromeProfile(await listLocalChromeProfiles(input.user_data_dir, input.email), input.email);
1621
+ } catch (err) {
1622
+ return errorResult("browser_profile_onboard", { error: err instanceof Error ? err.message : String(err) });
1623
+ }
1624
+ }
1625
+ const profile = input.profile?.trim() || chromeProfile?.recommendedKernelProfileName || browserServiceProfileName();
1626
+ if (!profile) {
1627
+ return errorResult("browser_profile_onboard", {
1628
+ error: "profile is required when no email match or BROWSER_AGENT_PROFILE_NAME is available"
1629
+ });
1630
+ }
1631
+ const setupUrl = input.url ?? "https://accounts.google.com/";
1632
+ const open = await req("POST", "/agent/sessions", {
1633
+ label: input.label ?? `Profile setup: ${profile}`,
1634
+ profile,
1635
+ save_profile_changes: true,
1636
+ timeout_seconds: input.timeout_seconds
1637
+ });
1638
+ if (!open.ok) return errorResult("browser_profile_onboard", open.data);
1639
+ const session = open.data;
1640
+ const goto = await req("POST", `/agent/sessions/${session.session_id}/goto`, { url: setupUrl });
1641
+ if (!goto.ok) return errorResult("browser_profile_onboard", goto.data, session.session_id);
1642
+ return structuredResult({
1643
+ ok: true,
1644
+ tool: "browser_profile_onboard",
1645
+ session_id: session.session_id,
1646
+ watch_url: `${consoleBase}/console/${session.session_id}`,
1647
+ live_view_url: session.live_view_url ?? null,
1648
+ profile,
1649
+ setup_url: setupUrl,
1650
+ chrome_profile: chromeProfile,
1651
+ next_steps: [
1652
+ "Open the watch_url and complete the login manually.",
1653
+ "After login is complete, call browser_close with this session_id to persist cookies and local storage to the Kernel profile.",
1654
+ "Use BROWSER_AGENT_PROFILE_NAME or browser_open.profile with this profile name for future authenticated sessions."
1655
+ ],
1656
+ raw: session
1657
+ });
1658
+ }
1659
+ );
866
1660
  server2.registerTool(
867
1661
  "browser_open",
868
1662
  {
869
1663
  title: "Open Browser Session",
870
- 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.",
1664
+ 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.",
871
1665
  inputSchema: BrowserOpenInputSchema,
872
1666
  outputSchema: BrowserOpenOutputSchema,
873
1667
  annotations: annotations("Open Browser Session")
874
1668
  },
875
1669
  async (input) => {
1670
+ if (localBrowserModeEnabled()) {
1671
+ try {
1672
+ const session2 = await localBrowser.open(input);
1673
+ return structuredResult({
1674
+ ok: true,
1675
+ tool: "browser_open",
1676
+ session_id: session2.session_id,
1677
+ watch_url: session2.watch_url,
1678
+ live_view_url: null,
1679
+ url: input.url ?? null,
1680
+ hint: "Local Chrome is open on this machine. Call browser_screenshot to see the page and click by x,y coordinates.",
1681
+ raw: session2
1682
+ });
1683
+ } catch (err) {
1684
+ return errorResult("browser_open", { error: err instanceof Error ? err.message : String(err), mode: "local" });
1685
+ }
1686
+ }
1687
+ const profile = input.profile ?? browserServiceProfileName();
1688
+ const saveProfileChanges = input.save_profile_changes ?? browserServiceProfileSaveChanges();
876
1689
  const open = await req("POST", "/agent/sessions", {
877
1690
  label: input.label,
878
- profile: input.profile,
1691
+ ...profile ? { profile } : {},
1692
+ ...profile && typeof saveProfileChanges === "boolean" ? { save_profile_changes: saveProfileChanges } : {},
879
1693
  timeout_seconds: input.timeout_seconds
880
1694
  });
881
1695
  if (!open.ok) return errorResult("browser_open", open.data);
@@ -905,6 +1719,27 @@ function registerBrowserAgentMcpTools(server2, opts) {
905
1719
  annotations: annotations("See Page", true)
906
1720
  },
907
1721
  async (input) => {
1722
+ if (localBrowserModeEnabled()) {
1723
+ try {
1724
+ const res2 = await localBrowser.screenshot(input.session_id);
1725
+ const content2 = [];
1726
+ if (res2.image_base64) content2.push({ type: "image", data: res2.image_base64, mimeType: res2.mime_type });
1727
+ const structured2 = {
1728
+ ok: true,
1729
+ tool: "browser_screenshot",
1730
+ session_id: input.session_id,
1731
+ url: res2.url ?? null,
1732
+ title: res2.title ?? null,
1733
+ text: res2.text,
1734
+ elements: res2.elements,
1735
+ screenshot: { mime_type: res2.mime_type, inline: true }
1736
+ };
1737
+ content2.push({ type: "text", text: JSON.stringify(structured2) });
1738
+ return { content: content2, structuredContent: structured2 };
1739
+ } catch (err) {
1740
+ return errorResult("browser_screenshot", { error: err instanceof Error ? err.message : String(err), mode: "local" }, input.session_id);
1741
+ }
1742
+ }
908
1743
  const res = await req("POST", `/agent/sessions/${input.session_id}/screenshot`);
909
1744
  if (!res.ok) return errorResult("browser_screenshot", res.data, input.session_id);
910
1745
  const { image_base64, mime_type, url, title, elements, text } = res.data;
@@ -937,6 +1772,23 @@ function registerBrowserAgentMcpTools(server2, opts) {
937
1772
  annotations: annotations("Read Page", true)
938
1773
  },
939
1774
  async (input) => {
1775
+ if (localBrowserModeEnabled()) {
1776
+ try {
1777
+ const res2 = await localBrowser.read(input.session_id);
1778
+ return structuredResult({
1779
+ ok: true,
1780
+ tool: "browser_read",
1781
+ session_id: input.session_id,
1782
+ url: res2.url ?? null,
1783
+ title: res2.title ?? null,
1784
+ text: res2.text,
1785
+ elements: res2.elements,
1786
+ raw: res2
1787
+ });
1788
+ } catch (err) {
1789
+ return errorResult("browser_read", { error: err instanceof Error ? err.message : String(err), mode: "local" }, input.session_id);
1790
+ }
1791
+ }
940
1792
  const res = await req("POST", `/agent/sessions/${input.session_id}/read`);
941
1793
  if (!res.ok) return errorResult("browser_read", res.data, input.session_id);
942
1794
  return structuredResult({
@@ -961,6 +1813,24 @@ function registerBrowserAgentMcpTools(server2, opts) {
961
1813
  annotations: annotations("Locate DOM Targets", true)
962
1814
  },
963
1815
  async (input) => {
1816
+ if (localBrowserModeEnabled()) {
1817
+ try {
1818
+ const res2 = await localBrowser.locate(input.session_id, input.targets);
1819
+ return structuredResult({
1820
+ ok: true,
1821
+ tool: "browser_locate",
1822
+ session_id: input.session_id,
1823
+ url: res2.url ?? null,
1824
+ title: res2.title ?? null,
1825
+ viewport: res2.viewport ?? null,
1826
+ replay: null,
1827
+ targets: Array.isArray(res2.targets) ? res2.targets : [],
1828
+ raw: res2
1829
+ });
1830
+ } catch (err) {
1831
+ return errorResult("browser_locate", { error: err instanceof Error ? err.message : String(err), mode: "local" }, input.session_id);
1832
+ }
1833
+ }
964
1834
  const res = await req("POST", `/agent/sessions/${input.session_id}/locate`, { targets: input.targets });
965
1835
  if (!res.ok) return errorResult("browser_locate", res.data, input.session_id);
966
1836
  return structuredResult({
@@ -986,6 +1856,14 @@ function registerBrowserAgentMcpTools(server2, opts) {
986
1856
  annotations: annotations("Navigate To URL")
987
1857
  },
988
1858
  async (input) => {
1859
+ if (localBrowserModeEnabled()) {
1860
+ try {
1861
+ const res2 = await localBrowser.goto(input.session_id, input.url);
1862
+ return actionResult("browser_goto", input.session_id, true, res2, "browser_screenshot");
1863
+ } catch (err) {
1864
+ return errorResult("browser_goto", { error: err instanceof Error ? err.message : String(err), mode: "local" }, input.session_id);
1865
+ }
1866
+ }
989
1867
  const res = await req("POST", `/agent/sessions/${input.session_id}/goto`, { url: input.url });
990
1868
  return actionResult("browser_goto", input.session_id, res.ok, res.data, "browser_screenshot");
991
1869
  }
@@ -1000,6 +1878,14 @@ function registerBrowserAgentMcpTools(server2, opts) {
1000
1878
  annotations: annotations("Click")
1001
1879
  },
1002
1880
  async (input) => {
1881
+ if (localBrowserModeEnabled()) {
1882
+ try {
1883
+ const res2 = await localBrowser.click(input.session_id, input);
1884
+ return actionResult("browser_click", input.session_id, true, res2, "browser_screenshot");
1885
+ } catch (err) {
1886
+ return errorResult("browser_click", { error: err instanceof Error ? err.message : String(err), mode: "local" }, input.session_id);
1887
+ }
1888
+ }
1003
1889
  const res = await req("POST", `/agent/sessions/${input.session_id}/click`, {
1004
1890
  x: input.x,
1005
1891
  y: input.y,
@@ -1019,6 +1905,14 @@ function registerBrowserAgentMcpTools(server2, opts) {
1019
1905
  annotations: annotations("Type Text")
1020
1906
  },
1021
1907
  async (input) => {
1908
+ if (localBrowserModeEnabled()) {
1909
+ try {
1910
+ const res2 = await localBrowser.type(input.session_id, input);
1911
+ return actionResult("browser_type", input.session_id, true, res2, "browser_screenshot");
1912
+ } catch (err) {
1913
+ return errorResult("browser_type", { error: err instanceof Error ? err.message : String(err), mode: "local" }, input.session_id);
1914
+ }
1915
+ }
1022
1916
  const res = await req("POST", `/agent/sessions/${input.session_id}/type`, { text: input.text, delay: input.delay });
1023
1917
  return actionResult("browser_type", input.session_id, res.ok, res.data, "browser_screenshot");
1024
1918
  }
@@ -1033,6 +1927,14 @@ function registerBrowserAgentMcpTools(server2, opts) {
1033
1927
  annotations: annotations("Scroll")
1034
1928
  },
1035
1929
  async (input) => {
1930
+ if (localBrowserModeEnabled()) {
1931
+ try {
1932
+ const res2 = await localBrowser.scroll(input.session_id, input);
1933
+ return actionResult("browser_scroll", input.session_id, true, res2, "browser_screenshot");
1934
+ } catch (err) {
1935
+ return errorResult("browser_scroll", { error: err instanceof Error ? err.message : String(err), mode: "local" }, input.session_id);
1936
+ }
1937
+ }
1036
1938
  const res = await req("POST", `/agent/sessions/${input.session_id}/scroll`, {
1037
1939
  delta_y: input.delta_y,
1038
1940
  delta_x: input.delta_x,
@@ -1052,6 +1954,14 @@ function registerBrowserAgentMcpTools(server2, opts) {
1052
1954
  annotations: annotations("Press Keys")
1053
1955
  },
1054
1956
  async (input) => {
1957
+ if (localBrowserModeEnabled()) {
1958
+ try {
1959
+ const res2 = await localBrowser.press(input.session_id, input);
1960
+ return actionResult("browser_press", input.session_id, true, res2, "browser_screenshot");
1961
+ } catch (err) {
1962
+ return errorResult("browser_press", { error: err instanceof Error ? err.message : String(err), mode: "local" }, input.session_id);
1963
+ }
1964
+ }
1055
1965
  const res = await req("POST", `/agent/sessions/${input.session_id}/press`, { keys: input.keys });
1056
1966
  return actionResult("browser_press", input.session_id, res.ok, res.data, "browser_screenshot");
1057
1967
  }
@@ -1066,6 +1976,7 @@ function registerBrowserAgentMcpTools(server2, opts) {
1066
1976
  annotations: annotations("Start Recording")
1067
1977
  },
1068
1978
  async (input) => {
1979
+ if (localBrowserModeEnabled()) return localReplayUnsupported("browser_replay_start", input.session_id);
1069
1980
  const res = await req("POST", `/agent/sessions/${input.session_id}/replay/start`);
1070
1981
  if (!res.ok) return errorResult("browser_replay_start", res.data, input.session_id);
1071
1982
  return structuredResult({
@@ -1089,6 +2000,7 @@ function registerBrowserAgentMcpTools(server2, opts) {
1089
2000
  annotations: annotations("Stop Recording")
1090
2001
  },
1091
2002
  async (input) => {
2003
+ if (localBrowserModeEnabled()) return localReplayUnsupported("browser_replay_stop", input.session_id, input.replay_id);
1092
2004
  const res = await req("POST", `/agent/sessions/${input.session_id}/replay/stop`, { replay_id: input.replay_id });
1093
2005
  if (!res.ok) return errorResult("browser_replay_stop", res.data, input.session_id, input.replay_id);
1094
2006
  return structuredResult({
@@ -1112,6 +2024,7 @@ function registerBrowserAgentMcpTools(server2, opts) {
1112
2024
  annotations: annotations("List Replay Videos", true)
1113
2025
  },
1114
2026
  async (input) => {
2027
+ if (localBrowserModeEnabled()) return localReplayUnsupported("browser_list_replays", input.session_id);
1115
2028
  const res = await req("GET", `/agent/sessions/${input.session_id}/replays`);
1116
2029
  if (!res.ok) return errorResult("browser_list_replays", res.data, input.session_id);
1117
2030
  const replays = Array.isArray(res.data?.replays) ? res.data.replays : [];
@@ -1134,6 +2047,7 @@ function registerBrowserAgentMcpTools(server2, opts) {
1134
2047
  annotations: annotations("Download Replay MP4")
1135
2048
  },
1136
2049
  async (input) => {
2050
+ if (localBrowserModeEnabled()) return localReplayUnsupported("browser_replay_download", input.session_id, input.replay_id);
1137
2051
  const res = await downloadReplay(input.session_id, input.replay_id, input.filename);
1138
2052
  if (!res.ok) return errorResult("browser_replay_download", res.data, input.session_id, input.replay_id);
1139
2053
  return structuredResult({
@@ -1158,6 +2072,7 @@ function registerBrowserAgentMcpTools(server2, opts) {
1158
2072
  annotations: annotations("Mark Replay Annotation", true)
1159
2073
  },
1160
2074
  async (input) => {
2075
+ if (localBrowserModeEnabled()) return localReplayUnsupported("browser_replay_mark", input.session_id);
1161
2076
  const res = await req("POST", `/agent/sessions/${input.session_id}/locate`, { targets: [input.target] });
1162
2077
  if (!res.ok) return errorResult("browser_replay_mark", res.data, input.session_id);
1163
2078
  const target = res.data?.targets?.[0];
@@ -1208,13 +2123,14 @@ function registerBrowserAgentMcpTools(server2, opts) {
1208
2123
  annotations: annotations("Annotate Replay MP4")
1209
2124
  },
1210
2125
  async (input) => {
2126
+ if (localBrowserModeEnabled()) return localReplayUnsupported("browser_replay_annotate", input.session_id, input.replay_id);
1211
2127
  const sourceName = input.filename ? `${input.filename}-source` : void 0;
1212
2128
  const downloaded = await downloadReplay(input.session_id, input.replay_id, sourceName);
1213
2129
  if (!downloaded.ok) return errorResult("browser_replay_annotate", downloaded.data, input.session_id, input.replay_id);
1214
2130
  try {
1215
2131
  const sourcePath = String(downloaded.data.file_path);
1216
2132
  const outputPath = annotatedReplayFilePath(input.session_id, input.replay_id, input.filename);
1217
- (0, import_node_fs.mkdirSync)((0, import_node_path2.join)(outputBaseDir(), "browser-replays"), { recursive: true });
2133
+ (0, import_node_fs3.mkdirSync)((0, import_node_path5.join)(outputBaseDir(), "browser-replays"), { recursive: true });
1218
2134
  const result = await annotateReplayVideo(sourcePath, outputPath, {
1219
2135
  annotations: input.annotations,
1220
2136
  sourceWidth: input.source_width,
@@ -1250,6 +2166,20 @@ function registerBrowserAgentMcpTools(server2, opts) {
1250
2166
  annotations: { title: "Close Browser Session", readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false }
1251
2167
  },
1252
2168
  async (input) => {
2169
+ if (localBrowserModeEnabled()) {
2170
+ try {
2171
+ const res2 = await localBrowser.close(input.session_id);
2172
+ return structuredResult({
2173
+ ok: true,
2174
+ tool: "browser_close",
2175
+ session_id: input.session_id,
2176
+ closed: true,
2177
+ raw: res2
2178
+ });
2179
+ } catch (err) {
2180
+ return errorResult("browser_close", { error: err instanceof Error ? err.message : String(err), mode: "local" }, input.session_id);
2181
+ }
2182
+ }
1253
2183
  const res = await req("DELETE", `/agent/sessions/${input.session_id}`);
1254
2184
  if (!res.ok) return errorResult("browser_close", res.data, input.session_id);
1255
2185
  return structuredResult({
@@ -1271,6 +2201,16 @@ function registerBrowserAgentMcpTools(server2, opts) {
1271
2201
  annotations: annotations("List Browser Sessions", true)
1272
2202
  },
1273
2203
  async (input) => {
2204
+ if (localBrowserModeEnabled()) {
2205
+ const sessions2 = localBrowser.list(input.include_closed);
2206
+ return structuredResult({
2207
+ ok: true,
2208
+ tool: "browser_list_sessions",
2209
+ session_id: null,
2210
+ sessions: sessions2,
2211
+ count: sessions2.length
2212
+ });
2213
+ }
1274
2214
  const res = await req("GET", `/agent/sessions${input.include_closed ? "?all=1" : ""}`);
1275
2215
  if (!res.ok) return errorResult("browser_list_sessions", res.data);
1276
2216
  const sessions = (res.data.sessions ?? []).map((s) => ({ ...s, watch_url: `${consoleBase}/console/${s.session_id}` }));
@@ -1287,13 +2227,13 @@ function registerBrowserAgentMcpTools(server2, opts) {
1287
2227
 
1288
2228
  // src/mcp/paa-mcp-server.ts
1289
2229
  var import_mcp2 = require("@modelcontextprotocol/sdk/server/mcp.js");
1290
- var import_node_fs3 = require("fs");
1291
- var import_node_path4 = require("path");
2230
+ var import_node_fs5 = require("fs");
2231
+ var import_node_path7 = require("path");
1292
2232
 
1293
2233
  // src/mcp/mcp-response-formatter.ts
1294
- var import_node_fs2 = require("fs");
1295
- var import_node_os3 = require("os");
1296
- var import_node_path3 = require("path");
2234
+ var import_node_fs4 = require("fs");
2235
+ var import_node_os5 = require("os");
2236
+ var import_node_path6 = require("path");
1297
2237
 
1298
2238
  // src/errors.ts
1299
2239
  function sanitizeVendorName(message) {
@@ -1430,16 +2370,16 @@ function reportTitle(full) {
1430
2370
  return title?.replace(/^#\s+/, "").trim() || "MCP Scraper Report";
1431
2371
  }
1432
2372
  function outputBaseDir2() {
1433
- return process.env.MCP_SCRAPER_OUTPUT_DIR?.trim() || (0, import_node_path3.join)((0, import_node_os3.homedir)(), "Downloads", "mcp-scraper");
2373
+ return process.env.MCP_SCRAPER_OUTPUT_DIR?.trim() || (0, import_node_path6.join)((0, import_node_os5.homedir)(), "Downloads", "mcp-scraper");
1434
2374
  }
1435
2375
  function saveFullReport(full) {
1436
2376
  if (!reportSavingEnabled || process.env.MCP_SCRAPER_SAVE_REPORTS === "false") return null;
1437
2377
  const outDir = outputBaseDir2();
1438
2378
  try {
1439
- (0, import_node_fs2.mkdirSync)(outDir, { recursive: true });
2379
+ (0, import_node_fs4.mkdirSync)(outDir, { recursive: true });
1440
2380
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
1441
- const file = (0, import_node_path3.join)(outDir, `${stamp}-${slugifyReportName(reportTitle(full))}.md`);
1442
- (0, import_node_fs2.writeFileSync)(file, full, "utf8");
2381
+ const file = (0, import_node_path6.join)(outDir, `${stamp}-${slugifyReportName(reportTitle(full))}.md`);
2382
+ (0, import_node_fs4.writeFileSync)(file, full, "utf8");
1443
2383
  return file;
1444
2384
  } catch {
1445
2385
  return null;
@@ -1448,12 +2388,12 @@ function saveFullReport(full) {
1448
2388
  function persistScreenshotLocally(base64, url) {
1449
2389
  if (!reportSavingEnabled || process.env.MCP_SCRAPER_SAVE_REPORTS === "false") return null;
1450
2390
  try {
1451
- const dir = (0, import_node_path3.join)(outputBaseDir2(), "screenshots");
1452
- (0, import_node_fs2.mkdirSync)(dir, { recursive: true });
2391
+ const dir = (0, import_node_path6.join)(outputBaseDir2(), "screenshots");
2392
+ (0, import_node_fs4.mkdirSync)(dir, { recursive: true });
1453
2393
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
1454
2394
  const slug = url.replace(/^https?:\/\//, "").replace(/[^a-z0-9]+/gi, "-").replace(/^-+|-+$/g, "").slice(0, 60);
1455
- const filePath = (0, import_node_path3.join)(dir, `${stamp}-${slug}.png`);
1456
- (0, import_node_fs2.writeFileSync)(filePath, Buffer.from(base64, "base64"));
2395
+ const filePath = (0, import_node_path6.join)(dir, `${stamp}-${slug}.png`);
2396
+ (0, import_node_fs4.writeFileSync)(filePath, Buffer.from(base64, "base64"));
1457
2397
  return filePath;
1458
2398
  } catch {
1459
2399
  return null;
@@ -3829,7 +4769,7 @@ function localPlanningToolAnnotations(title) {
3829
4769
  function listSavedReports() {
3830
4770
  try {
3831
4771
  const dir = outputBaseDir2();
3832
- return (0, import_node_fs3.readdirSync)(dir).filter((f) => f.endsWith(".md")).map((f) => ({ filename: f, mtimeMs: (0, import_node_fs3.statSync)((0, import_node_path4.join)(dir, f)).mtimeMs })).sort((a, b) => b.mtimeMs - a.mtimeMs).slice(0, 100);
4772
+ return (0, import_node_fs5.readdirSync)(dir).filter((f) => f.endsWith(".md")).map((f) => ({ filename: f, mtimeMs: (0, import_node_fs5.statSync)((0, import_node_path7.join)(dir, f)).mtimeMs })).sort((a, b) => b.mtimeMs - a.mtimeMs).slice(0, 100);
3833
4773
  } catch {
3834
4774
  return [];
3835
4775
  }
@@ -3853,9 +4793,9 @@ function registerSavedReportResources(server2) {
3853
4793
  },
3854
4794
  async (uri, variables) => {
3855
4795
  const requested = Array.isArray(variables.filename) ? variables.filename[0] : variables.filename;
3856
- const filename = (0, import_node_path4.basename)(decodeURIComponent(String(requested ?? "")));
4796
+ const filename = (0, import_node_path7.basename)(decodeURIComponent(String(requested ?? "")));
3857
4797
  if (!filename.endsWith(".md")) throw new Error("Only saved .md reports can be read");
3858
- const text = (0, import_node_fs3.readFileSync)((0, import_node_path4.join)(outputBaseDir2(), filename), "utf8");
4798
+ const text = (0, import_node_fs5.readFileSync)((0, import_node_path7.join)(outputBaseDir2(), filename), "utf8");
3859
4799
  return { contents: [{ uri: uri.href, mimeType: "text/markdown", text }] };
3860
4800
  }
3861
4801
  );
@@ -4134,10 +5074,10 @@ if (!forceStdio && (interactiveTerminal || wantsHelp)) {
4134
5074
  }
4135
5075
  function readApiKeyFile() {
4136
5076
  const explicitPath = process.env.MCP_SCRAPER_KEY_PATH?.trim();
4137
- const paths = [explicitPath, (0, import_node_path5.join)((0, import_node_os4.homedir)(), ".mcp-scraper-key")].filter(Boolean);
5077
+ const paths = [explicitPath, (0, import_node_path8.join)((0, import_node_os6.homedir)(), ".mcp-scraper-key")].filter(Boolean);
4138
5078
  for (const path of paths) {
4139
5079
  try {
4140
- const value = (0, import_node_fs4.readFileSync)(path, "utf8").trim();
5080
+ const value = (0, import_node_fs6.readFileSync)(path, "utf8").trim();
4141
5081
  if (value) return value;
4142
5082
  } catch {
4143
5083
  }