mcp-scraper 0.3.42 → 0.3.45

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/dist/bin/api-server.cjs +1558 -1312
  2. package/dist/bin/api-server.cjs.map +1 -1
  3. package/dist/bin/api-server.js +3 -3
  4. package/dist/bin/mcp-scraper-cli.cjs +1 -1
  5. package/dist/bin/mcp-scraper-cli.cjs.map +1 -1
  6. package/dist/bin/mcp-scraper-cli.js +1 -1
  7. package/dist/bin/mcp-scraper-install.cjs +1 -1
  8. package/dist/bin/mcp-scraper-install.cjs.map +1 -1
  9. package/dist/bin/mcp-scraper-install.js +1 -1
  10. package/dist/bin/mcp-stdio-server.cjs +283 -224
  11. package/dist/bin/mcp-stdio-server.cjs.map +1 -1
  12. package/dist/bin/mcp-stdio-server.js +2 -2
  13. package/dist/bin/paa-harvest.cjs.map +1 -1
  14. package/dist/bin/paa-harvest.js +2 -2
  15. package/dist/{chunk-RXJHXBGG.js → chunk-BRVX6RDN.js} +21 -4
  16. package/dist/chunk-BRVX6RDN.js.map +1 -0
  17. package/dist/{chunk-KPF64WST.js → chunk-E7BZFMWB.js} +284 -225
  18. package/dist/chunk-E7BZFMWB.js.map +1 -0
  19. package/dist/{chunk-DVRPXPYH.js → chunk-FUVQR6GK.js} +14 -3
  20. package/dist/chunk-FUVQR6GK.js.map +1 -0
  21. package/dist/chunk-NVTFLLEG.js +7 -0
  22. package/dist/chunk-NVTFLLEG.js.map +1 -0
  23. package/dist/{chunk-GMTS35L6.js → chunk-RXNHY3TF.js} +2 -2
  24. package/dist/{db-LYQENFPW.js → db-H3S3M6KK.js} +2 -2
  25. package/dist/index.cjs.map +1 -1
  26. package/dist/index.js +2 -2
  27. package/dist/{server-CZT7EULY.js → server-MKUN6NQZ.js} +513 -370
  28. package/dist/server-MKUN6NQZ.js.map +1 -0
  29. package/dist/{worker-FNSBPP7Y.js → worker-4CVMSUZR.js} +4 -4
  30. package/package.json +4 -2
  31. package/dist/chunk-DVRPXPYH.js.map +0 -1
  32. package/dist/chunk-KPF64WST.js.map +0 -1
  33. package/dist/chunk-OUZZNW3I.js +0 -7
  34. package/dist/chunk-OUZZNW3I.js.map +0 -1
  35. package/dist/chunk-RXJHXBGG.js.map +0 -1
  36. package/dist/server-CZT7EULY.js.map +0 -1
  37. /package/dist/{chunk-GMTS35L6.js.map → chunk-RXNHY3TF.js.map} +0 -0
  38. /package/dist/{db-LYQENFPW.js.map → db-H3S3M6KK.js.map} +0 -0
  39. /package/dist/{worker-FNSBPP7Y.js.map → worker-4CVMSUZR.js.map} +0 -0
@@ -264,6 +264,12 @@ var HttpMcpToolExecutor = class {
264
264
  redditThread(input) {
265
265
  return this.call("/reddit/thread", input, this.httpTimeoutOverrideMs ?? 24e4);
266
266
  }
267
+ videoFrameAnalysis(input) {
268
+ return this.call("/video/analyze", input);
269
+ }
270
+ videoFrameAnalysisStatus(input) {
271
+ return this.call("/video/status", input);
272
+ }
267
273
  facebookAdTranscribe(input) {
268
274
  return this.call("/facebook/transcribe", input);
269
275
  }
@@ -468,7 +474,7 @@ render();
468
474
  }
469
475
 
470
476
  // src/version.ts
471
- var PACKAGE_VERSION = "0.3.42";
477
+ var PACKAGE_VERSION = "0.3.45";
472
478
 
473
479
  // src/mcp/browser-agent-tool-schemas.ts
474
480
  var import_zod = require("zod");
@@ -488,18 +494,18 @@ var BrowserOpenInputSchema = {
488
494
  label: import_zod.z.string().optional().describe("Optional human label for this session, shown in the watch console."),
489
495
  url: import_zod.z.string().url().optional().describe("Optional URL to navigate to immediately after opening."),
490
496
  profile: import_zod.z.string().optional().describe("Optional saved hosted profile name to load a logged-in session for a site."),
491
- save_profile_changes: import_zod.z.boolean().optional().describe("Persist cookies and browser storage back to the named profile when the session is closed. Use this for profile setup or intentional auth refreshes; avoid parallel sessions writing to the same profile."),
492
- timeout_seconds: import_zod.z.number().int().min(60).max(259200).optional().describe("How long the direct no-proxy browser session may live before auto-termination. Defaults to 600. The browser idles into a zero-cost standby between actions, so a longer timeout is cheap.")
497
+ save_profile_changes: import_zod.z.boolean().optional().describe("Persist cookies/storage back to the named profile on close. Avoid parallel sessions writing to the same profile."),
498
+ timeout_seconds: import_zod.z.number().int().min(60).max(259200).optional().describe("Session lifetime before auto-termination. Defaults to 600.")
493
499
  };
494
500
  var BrowserProfileConnectInputSchema = {
495
- email: import_zod.z.string().optional().describe("Account email for the login being saved. Used to derive a stable, reusable profile name AND recorded as a note of who is connected. Does NOT import existing cookies \u2014 the user signs in fresh via the returned watch_url."),
496
- profile: import_zod.z.string().optional().describe("Profile to add this login to. Omit to derive one from email. A single profile holds MANY site logins \u2014 pass the same profile name again with a different domain to stack another account (e.g. add claude.ai to a profile that already has chatgpt.com)."),
497
- domain: import_zod.z.string().optional().describe("Site to log into, e.g. chatgpt.com, claude.ai, reddit.com. Defaults to chatgpt.com. Each domain is a separate login inside the profile."),
498
- login_url: import_zod.z.string().url().optional().describe("Login page for the domain. Defaults to https://<domain>/ (https://chatgpt.com/ when domain is omitted)."),
499
- url: import_zod.z.string().url().optional().describe("Deprecated alias for login_url. Use login_url."),
500
- note: import_zod.z.string().optional().describe('Free-text note describing this login, e.g. "ChatGPT Plus, work account". Surfaced by browser_profile_list so you can see what the profile holds.'),
501
+ email: import_zod.z.string().optional().describe("Account email for the login. Derives a stable profile name and is recorded as a note. Does NOT import existing cookies \u2014 the user signs in fresh."),
502
+ profile: import_zod.z.string().optional().describe("Profile to add this login to. Omit to derive from email. A single profile holds MANY logins \u2014 pass the same name with a different domain to stack accounts."),
503
+ domain: import_zod.z.string().optional().describe("Site to log into, e.g. chatgpt.com, claude.ai, reddit.com. Defaults to chatgpt.com."),
504
+ login_url: import_zod.z.string().url().optional().describe("Login page for the domain. Defaults to https://<domain>/."),
505
+ url: import_zod.z.string().url().optional().describe("Deprecated alias for login_url."),
506
+ note: import_zod.z.string().optional().describe("Free-text note describing this login. Surfaced by browser_profile_list."),
501
507
  label: import_zod.z.string().optional().describe("Optional human label for this sign-in setup session."),
502
- timeout_seconds: import_zod.z.number().int().min(60).max(259200).optional().describe("How long the sign-in setup session may live before auto-termination. Defaults to 600.")
508
+ timeout_seconds: import_zod.z.number().int().min(60).max(259200).optional().describe("Sign-in session lifetime before auto-termination. Defaults to 600.")
503
509
  };
504
510
  var BrowserProfileListInputSchema = {
505
511
  profile: import_zod.z.string().optional().describe("Profile whose saved logins to list. Omit to derive from email."),
@@ -508,7 +514,7 @@ var BrowserProfileListInputSchema = {
508
514
  connection_id: import_zod.z.string().optional().describe("A specific login connection id returned by browser_profile_connect, to poll just that one.")
509
515
  };
510
516
  var BrowserSessionInputSchema = {
511
- 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.")
517
+ session_id: import_zod.z.string().describe("The session id returned by browser_open or browser_list_sessions.")
512
518
  };
513
519
  var BrowserLocateTargetSchema = import_zod.z.object({
514
520
  name: import_zod.z.string().optional().describe("Optional label for this target, echoed in the result."),
@@ -520,94 +526,94 @@ var BrowserLocateTargetSchema = import_zod.z.object({
520
526
  message: "target requires selector or text"
521
527
  });
522
528
  var BrowserLocateInputSchema = {
523
- 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."),
529
+ session_id: import_zod.z.string().describe("The session id returned by browser_open or browser_list_sessions."),
524
530
  targets: import_zod.z.array(BrowserLocateTargetSchema).min(1).max(20).describe("DOM targets to locate in the current viewport. Use selectors for exact elements, or text for visible text ranges.")
525
531
  };
526
532
  var BrowserGotoInputSchema = {
527
- 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."),
533
+ session_id: import_zod.z.string().describe("The session id returned by browser_open or browser_list_sessions."),
528
534
  url: import_zod.z.string().url().describe("URL to navigate the browser to.")
529
535
  };
530
536
  var BrowserClickInputSchema = {
531
- 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."),
537
+ session_id: import_zod.z.string().describe("The session id returned by browser_open or browser_list_sessions."),
532
538
  x: import_zod.z.number().describe("X coordinate to click, in screenshot pixels. Use only coordinates from the latest browser_screenshot, browser_read, or browser_locate result; do not guess."),
533
539
  y: import_zod.z.number().describe("Y coordinate to click, in screenshot pixels. Use only coordinates from the latest browser_screenshot, browser_read, or browser_locate result; do not guess."),
534
540
  button: import_zod.z.enum(["left", "right", "middle"]).default("left").describe("Mouse button."),
535
541
  num_clicks: import_zod.z.number().int().min(1).max(3).optional().describe("Number of clicks, e.g. 2 for double-click.")
536
542
  };
537
543
  var BrowserTypeInputSchema = {
538
- 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."),
544
+ session_id: import_zod.z.string().describe("The session id returned by browser_open or browser_list_sessions."),
539
545
  text: import_zod.z.string().describe("Text to type at the current focus. Click a field first to focus it."),
540
546
  delay: import_zod.z.number().int().min(0).max(500).optional().describe("Optional per-keystroke delay in ms for human-like typing.")
541
547
  };
542
548
  var BrowserScrollInputSchema = {
543
- 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."),
549
+ session_id: import_zod.z.string().describe("The session id returned by browser_open or browser_list_sessions."),
544
550
  delta_y: import_zod.z.number().default(5).describe("Vertical scroll in wheel units. Positive scrolls down, negative up."),
545
551
  delta_x: import_zod.z.number().default(0).describe("Horizontal scroll in wheel units."),
546
552
  x: import_zod.z.number().optional().describe("X position to scroll at. Defaults to screen center."),
547
553
  y: import_zod.z.number().optional().describe("Y position to scroll at. Defaults to screen center.")
548
554
  };
549
555
  var BrowserPressInputSchema = {
550
- 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."),
556
+ session_id: import_zod.z.string().describe("The session id returned by browser_open or browser_list_sessions."),
551
557
  keys: import_zod.z.array(import_zod.z.string()).min(1).describe('Keys or combinations to press, e.g. ["Return"], ["Ctrl+a"], ["Ctrl+Shift+Tab"].')
552
558
  };
553
559
  var BrowserReplayStopInputSchema = {
554
- 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."),
555
- replay_id: import_zod.z.string().describe("The replay id returned by browser_replay_start or browser_list_replays. Use only a returned replay_id; do not construct one yourself.")
560
+ session_id: import_zod.z.string().describe("The session id returned by browser_open or browser_list_sessions."),
561
+ replay_id: import_zod.z.string().describe("The replay id returned by browser_replay_start or browser_list_replays.")
556
562
  };
557
563
  var BrowserReplayDownloadInputSchema = {
558
- 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."),
559
- replay_id: import_zod.z.string().describe("The replay id returned by browser_replay_start or browser_list_replays. Use only a returned replay_id; do not construct one yourself."),
564
+ session_id: import_zod.z.string().describe("The session id returned by browser_open or browser_list_sessions."),
565
+ replay_id: import_zod.z.string().describe("The replay id returned by browser_replay_start or browser_list_replays."),
560
566
  filename: import_zod.z.string().optional().describe("Optional local MP4 filename. Defaults to a timestamped replay filename.")
561
567
  };
562
568
  var BrowserReplayAnnotationSchema = import_zod.z.object({
563
- type: import_zod.z.enum(["box", "circle", "underline", "arrow", "label"]).default("box").describe("Annotation style to draw over the replay."),
564
- start_seconds: import_zod.z.number().min(0).default(0).describe("When the annotation should appear in the replay."),
565
- end_seconds: import_zod.z.number().min(0).optional().describe("When the annotation should disappear. Defaults to two seconds after start_seconds."),
566
- left: import_zod.z.number().optional().describe("Target left edge in browser screenshot pixels. Use element.left from browser_screenshot/browser_read."),
567
- top: import_zod.z.number().optional().describe("Target top edge in browser screenshot pixels. Use element.top from browser_screenshot/browser_read."),
568
- width: import_zod.z.number().positive().optional().describe("Target width in browser screenshot pixels. Use element.width from browser_screenshot/browser_read."),
569
- height: import_zod.z.number().positive().optional().describe("Target height in browser screenshot pixels. Use element.height from browser_screenshot/browser_read."),
570
- x: import_zod.z.number().optional().describe("Point target x coordinate in browser screenshot pixels when no box is available."),
571
- y: import_zod.z.number().optional().describe("Point target y coordinate in browser screenshot pixels when no box is available."),
572
- from_x: import_zod.z.number().optional().describe("Arrow start x coordinate in browser screenshot pixels. Defaults near the target."),
573
- from_y: import_zod.z.number().optional().describe("Arrow start y coordinate in browser screenshot pixels. Defaults near the target."),
574
- to_x: import_zod.z.number().optional().describe("Arrow target x coordinate in browser screenshot pixels. Defaults to the target box center."),
575
- to_y: import_zod.z.number().optional().describe("Arrow target y coordinate in browser screenshot pixels. Defaults to the target box center."),
576
- label: import_zod.z.string().max(120).optional().describe("Optional text callout to render near the annotation."),
577
- color: import_zod.z.string().regex(/^#?[0-9a-fA-F]{6}$/).optional().describe("Annotation color as hex, for example #ff3b30."),
569
+ type: import_zod.z.enum(["box", "circle", "underline", "arrow", "label"]).default("box").describe("Annotation style."),
570
+ start_seconds: import_zod.z.number().min(0).default(0).describe("When the annotation should appear."),
571
+ end_seconds: import_zod.z.number().min(0).optional().describe("When it disappears. Defaults to 2s after start_seconds."),
572
+ left: import_zod.z.number().optional().describe("Target left edge in screenshot pixels (element.left)."),
573
+ top: import_zod.z.number().optional().describe("Target top edge in screenshot pixels (element.top)."),
574
+ width: import_zod.z.number().positive().optional().describe("Target width in screenshot pixels (element.width)."),
575
+ height: import_zod.z.number().positive().optional().describe("Target height in screenshot pixels (element.height)."),
576
+ x: import_zod.z.number().optional().describe("Point target x coordinate when no box is available."),
577
+ y: import_zod.z.number().optional().describe("Point target y coordinate when no box is available."),
578
+ from_x: import_zod.z.number().optional().describe("Arrow start x coordinate. Defaults near the target."),
579
+ from_y: import_zod.z.number().optional().describe("Arrow start y coordinate. Defaults near the target."),
580
+ to_x: import_zod.z.number().optional().describe("Arrow end x coordinate. Defaults to the target box center."),
581
+ to_y: import_zod.z.number().optional().describe("Arrow end y coordinate. Defaults to the target box center."),
582
+ label: import_zod.z.string().max(120).optional().describe("Optional text callout."),
583
+ color: import_zod.z.string().regex(/^#?[0-9a-fA-F]{6}$/).optional().describe("Annotation color as hex, e.g. #ff3b30."),
578
584
  thickness: import_zod.z.number().min(1).max(24).optional().describe("Stroke thickness in pixels. Defaults to 5.")
579
585
  });
580
586
  var BrowserReplayMarkInputSchema = {
581
- 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. A replay must already be recording."),
587
+ session_id: import_zod.z.string().describe("The session id returned by browser_open or browser_list_sessions. A replay must already be recording."),
582
588
  target: BrowserLocateTargetSchema.describe("The exact DOM element or text range to mark in the current viewport."),
583
- type: import_zod.z.enum(["box", "circle", "underline", "arrow"]).default("box").describe("Annotation style to generate. Labels are included on the returned annotation when label is provided."),
589
+ type: import_zod.z.enum(["box", "circle", "underline", "arrow"]).default("box").describe("Annotation style to generate."),
584
590
  label: import_zod.z.string().max(120).optional().describe("Optional callout text to render near the target."),
585
- color: import_zod.z.string().regex(/^#?[0-9a-fA-F]{6}$/).optional().describe("Annotation color as hex, for example #ff3b30."),
591
+ color: import_zod.z.string().regex(/^#?[0-9a-fA-F]{6}$/).optional().describe("Annotation color as hex, e.g. #ff3b30."),
586
592
  thickness: import_zod.z.number().min(1).max(24).optional().describe("Stroke thickness in pixels. Defaults to 5."),
587
593
  padding: import_zod.z.number().min(0).max(80).default(8).describe("Pixels to expand the DOM bounds so the highlight does not touch the text edge."),
588
- start_offset_seconds: import_zod.z.number().min(-5).max(10).default(-0.25).describe("Offset from the current replay time. Negative values make the callout appear just before the mark action is captured."),
589
- duration_seconds: import_zod.z.number().min(0.5).max(30).default(4).describe("How long the generated annotation should remain visible.")
594
+ start_offset_seconds: import_zod.z.number().min(-5).max(10).default(-0.25).describe("Offset from the current replay time; negative appears just before the mark action."),
595
+ duration_seconds: import_zod.z.number().min(0.5).max(30).default(4).describe("How long the annotation should remain visible.")
590
596
  };
591
597
  var BrowserReplayAnnotateInputSchema = {
592
- 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."),
593
- replay_id: import_zod.z.string().describe("The replay id returned by browser_replay_start or browser_list_replays. Use only a returned replay_id; do not construct one yourself."),
594
- annotations: import_zod.z.array(BrowserReplayAnnotationSchema).min(1).max(50).describe("Timed overlay annotations to render on the replay. Prefer annotations returned by browser_replay_mark; otherwise use exact DOM bounds from browser_locate/browser_screenshot/browser_read."),
595
- filename: import_zod.z.string().optional().describe("Optional output MP4 filename. Defaults to a timestamped annotated replay filename."),
598
+ session_id: import_zod.z.string().describe("The session id returned by browser_open or browser_list_sessions."),
599
+ replay_id: import_zod.z.string().describe("The replay id returned by browser_replay_start or browser_list_replays."),
600
+ annotations: import_zod.z.array(BrowserReplayAnnotationSchema).min(1).max(50).describe("Timed overlay annotations. Prefer ones from browser_replay_mark; otherwise use exact DOM bounds from browser_locate."),
601
+ filename: import_zod.z.string().optional().describe("Optional output MP4 filename. Defaults to a timestamped filename."),
596
602
  source_width: import_zod.z.number().positive().optional().describe("Width of the screenshot coordinate space used for annotations. Defaults to the replay video width."),
597
- source_height: import_zod.z.number().positive().optional().describe("Height of the coordinate space used for annotations. When this is a page viewport height smaller than the replay video height, the annotator infers the browser chrome top offset."),
598
- source_left_offset: import_zod.z.number().min(0).optional().describe("Optional explicit X offset from annotation coordinates to replay video coordinates. Usually omitted."),
599
- source_top_offset: import_zod.z.number().min(0).optional().describe("Optional explicit Y offset from annotation coordinates to replay video coordinates. Usually omitted because browser chrome offset is inferred when source_height is a page viewport height.")
603
+ source_height: import_zod.z.number().positive().optional().describe("Height of the annotation coordinate space; if smaller than the replay video height, the browser chrome offset is inferred."),
604
+ source_left_offset: import_zod.z.number().min(0).optional().describe("Explicit X offset from annotation to replay video coordinates. Usually omitted."),
605
+ source_top_offset: import_zod.z.number().min(0).optional().describe("Explicit Y offset from annotation to replay video coordinates. Usually omitted.")
600
606
  };
601
607
  var BrowserListInputSchema = {
602
608
  include_closed: import_zod.z.boolean().default(false).describe("Include closed sessions in the list.")
603
609
  };
604
610
  var BrowserCaptureFanoutInputSchema = {
605
- session_id: import_zod.z.string().describe("The session id returned by browser_open or browser_list_sessions. The session must be on chatgpt.com or claude.ai and logged in through a saved hosted profile. Use only a returned session_id; do not construct one yourself."),
606
- prompt: import_zod.z.string().optional().describe("Optional prompt to type into the chat composer and submit before capturing. Omit to passively capture the fan-out of a prompt the user just ran in the visible browser. The prompt must trigger web search for there to be a fan-out."),
607
- wait_ms: import_zod.z.number().int().min(0).max(18e4).optional().describe("How long to wait for the answer stream to finish before parsing. Defaults to 90000 when a prompt is sent, 8000 for passive capture. Capture ends as soon as the answer stream finishes."),
608
- first_party_domain: import_zod.z.string().optional().describe("The brand/site being researched, for example example.com. Sources on this domain are tagged First-party/vendor. Set this when the prompt is about a specific business."),
609
- reset: import_zod.z.boolean().default(false).describe("Clear any previously buffered stream for this page before capturing. Use when re-running to avoid mixing turns."),
610
- export: import_zod.z.boolean().default(false).describe("When true, write JSON, CSV, TSV exports (queries, citations, all sources, browsed-only sources, snippets, domains) and a self-contained HTML report to MCP_SCRAPER_OUTPUT_DIR/fanout, returning paths relative to MCP_SCRAPER_OUTPUT_DIR.")
611
+ session_id: import_zod.z.string().describe("Session id from browser_open. Must be on chatgpt.com or claude.ai, logged in via a saved hosted profile."),
612
+ prompt: import_zod.z.string().optional().describe("Optional prompt to type and submit before capturing. Omit to passively capture a prompt the user just ran. Must trigger web search to produce a fan-out."),
613
+ wait_ms: import_zod.z.number().int().min(0).max(18e4).optional().describe("How long to wait for the answer stream to finish. Defaults to 90000 when a prompt is sent, 8000 for passive capture."),
614
+ first_party_domain: import_zod.z.string().optional().describe("The brand/site being researched, e.g. example.com \u2014 sources on this domain are tagged First-party/vendor."),
615
+ reset: import_zod.z.boolean().default(false).describe("Clear any previously buffered stream for this page before capturing."),
616
+ export: import_zod.z.boolean().default(false).describe("Write JSON/CSV/TSV/HTML exports to MCP_SCRAPER_OUTPUT_DIR/fanout, returning relative paths.")
611
617
  };
612
618
  var FanoutSourceOutput = import_zod.z.object({
613
619
  url: import_zod.z.string(),
@@ -631,10 +637,10 @@ var BrowserCaptureFanoutOutputSchema = {
631
637
  title: import_zod.z.string(),
632
638
  rounds: import_zod.z.number().int().min(0)
633
639
  }),
634
- queries: import_zod.z.array(import_zod.z.string()).describe("Every web-search sub-query the model issued, in capture order. Classify these by funnel stage and type yourself from the text."),
635
- browsed_urls: import_zod.z.array(FanoutSourceOutput).describe("Every researched URL (cited and browsed-only), cited first."),
636
- cited_urls: import_zod.z.array(FanoutSourceOutput).describe("The subset of researched URLs actually cited in the final answer."),
637
- browsed_only: import_zod.z.array(FanoutSourceOutput).describe("Researched URLs the model pulled but did not cite."),
640
+ queries: import_zod.z.array(import_zod.z.string()).describe("Every web-search sub-query issued, in capture order."),
641
+ browsed_urls: import_zod.z.array(FanoutSourceOutput).describe("Every researched URL, cited first."),
642
+ cited_urls: import_zod.z.array(FanoutSourceOutput).describe("Researched URLs cited in the final answer."),
643
+ browsed_only: import_zod.z.array(FanoutSourceOutput).describe("Researched URLs pulled but not cited."),
638
644
  snippets: import_zod.z.array(import_zod.z.object({ url: import_zod.z.string(), domain: import_zod.z.string(), title: import_zod.z.string(), text: import_zod.z.string() })),
639
645
  counts: import_zod.z.object({
640
646
  subQueries: import_zod.z.number().int().min(0),
@@ -1293,7 +1299,7 @@ function registerBrowserAgentMcpTools(server2, opts) {
1293
1299
  "browser_profile_connect",
1294
1300
  {
1295
1301
  title: "Save a Site Login to a Profile",
1296
- description: `Save a logged-in browser session so this server can reuse a site the user is signed into (ChatGPT, Claude, Reddit, or any account-gated site). Returns an mcpscraper.dev watch_url \u2014 give it to the user, they sign in once, and the cookies are saved to a named profile for later browser_open calls. ONE profile holds MANY logins: call this again with the SAME profile and a DIFFERENT domain to stack another account (e.g. add claude.ai to a profile that already has chatgpt.com). Pass note to record what each login is. Side effects: opens a short-lived sign-in session and persists cookies to the profile; it does NOT import the user's existing browser cookies (they log in fresh). NOT for one-off scraping of public pages (use extract_url) and NOT a way to drive the browser (use browser_open). Sample asks: "save my ChatGPT login", "connect my Reddit account", "add my Claude login to the same profile". After the user signs in, poll browser_profile_list until the login reads AUTHENTICATED, then browser_open with the profile.`,
1302
+ description: "Save a logged-in browser session so this server can reuse a site the user is signed into (ChatGPT, Claude, Reddit, any account-gated site). Returns a watch_url; the user signs in fresh (existing browser cookies are NOT imported) and cookies save to a named profile. ONE profile holds MANY logins \u2014 call again with the same profile and a different domain to stack another account. NOT for one-off scraping (use extract_url) or driving the browser (use browser_open). After sign-in, poll browser_profile_list until AUTHENTICATED, then browser_open with the profile.",
1297
1303
  inputSchema: BrowserProfileConnectInputSchema,
1298
1304
  outputSchema: BrowserProfileConnectOutputSchema,
1299
1305
  annotations: annotations("Save a Site Login to a Profile")
@@ -1351,7 +1357,7 @@ function registerBrowserAgentMcpTools(server2, opts) {
1351
1357
  "browser_profile_list",
1352
1358
  {
1353
1359
  title: "List Saved Logins in a Profile",
1354
- description: `List every site login saved in a profile, each with its current auth status (NEEDS_AUTH / AUTHENTICATED), account email, and note. Use it to (1) see what a profile is connected to before opening a session, and (2) poll a just-saved login until it flips to AUTHENTICATED. Read-only, no cost. Pass profile (or email to derive it); narrow with domain or connection_id to poll a single login. Sample asks: "what's connected in my profile", "is my ChatGPT login ready yet", "which accounts are saved". Pairs with browser_profile_connect (which adds logins) and browser_open (which uses them).`,
1360
+ description: "List every site login saved in a profile with its auth status (NEEDS_AUTH/AUTHENTICATED), email, and note. Use to check what's connected, or to poll a just-saved login until AUTHENTICATED. Read-only, no cost. Pass profile (or email to derive it); narrow with domain or connection_id.",
1355
1361
  inputSchema: BrowserProfileListInputSchema,
1356
1362
  outputSchema: BrowserProfileListOutputSchema,
1357
1363
  annotations: annotations("List Saved Logins in a Profile", true)
@@ -1385,7 +1391,7 @@ function registerBrowserAgentMcpTools(server2, opts) {
1385
1391
  "browser_open",
1386
1392
  {
1387
1393
  title: "Open Browser Session",
1388
- description: "Open a direct no-proxy hosted browser session you can drive. Pass a saved profile name to load a session that is already logged into the sites saved in that profile (one profile can hold several logins). To set up or refresh a login first, use browser_profile_connect, then poll browser_profile_list until it reads AUTHENTICATED. Returns a session_id used by all other browser_* tools.",
1394
+ description: "Open a direct no-proxy hosted browser session you can drive. Pass a saved profile name to load a session already logged into that profile's sites (set one up first with browser_profile_connect). Returns a session_id used by all other browser_* tools.",
1389
1395
  inputSchema: BrowserOpenInputSchema,
1390
1396
  outputSchema: BrowserOpenOutputSchema,
1391
1397
  annotations: annotations("Open Browser Session")
@@ -1419,7 +1425,7 @@ function registerBrowserAgentMcpTools(server2, opts) {
1419
1425
  "browser_screenshot",
1420
1426
  {
1421
1427
  title: "See Page (Screenshot + Elements)",
1422
- description: "Capture what the browser currently shows. Returns a screenshot image PLUS a text snapshot listing interactive elements with their center x,y coordinates, the page url and title, and visible text. This is your primary way to perceive the page. Click elements by their listed x,y. If a Cloudflare/CAPTCHA challenge is visible, wait and screenshot again rather than clicking it.",
1428
+ description: "Capture what the browser currently shows: a screenshot plus a text snapshot of interactive elements with x,y coordinates, page url/title, and visible text. Primary way to perceive the page; click elements by their listed x,y. If a Cloudflare/CAPTCHA challenge is visible, wait and screenshot again rather than clicking it.",
1423
1429
  inputSchema: BrowserSessionInputSchema,
1424
1430
  outputSchema: BrowserScreenshotOutputSchema,
1425
1431
  annotations: annotations("See Page", true)
@@ -1451,7 +1457,7 @@ function registerBrowserAgentMcpTools(server2, opts) {
1451
1457
  "browser_read",
1452
1458
  {
1453
1459
  title: "Read Page Text + Elements",
1454
- description: "Return the page url, title, visible text, and the list of interactive elements (with x,y) without an image. Cheaper than browser_screenshot when you only need to read content or find a target element to click.",
1460
+ description: "Return the page url, title, visible text, and interactive elements (with x,y) without an image. Cheaper than browser_screenshot when you only need to read content or find a click target.",
1455
1461
  inputSchema: BrowserSessionInputSchema,
1456
1462
  outputSchema: BrowserReadOutputSchema,
1457
1463
  annotations: annotations("Read Page", true)
@@ -1475,7 +1481,7 @@ function registerBrowserAgentMcpTools(server2, opts) {
1475
1481
  "browser_locate",
1476
1482
  {
1477
1483
  title: "Locate DOM Targets",
1478
- description: "Locate exact visible DOM elements or text ranges in the current browser viewport and return left/top/width/height bounds in screenshot pixels. Use this before drawing annotations or when a callout must literally circle, box, underline, or point to a real element. Prefer CSS selectors for exact UI elements; use text when selector is unknown. When a replay is actively recording, the result includes replay_elapsed_seconds for timing.",
1484
+ description: "Locate exact visible DOM elements or text ranges and return left/top/width/height bounds in screenshot pixels. Use before drawing annotations that must circle, box, underline, or point to a real element. Prefer CSS selectors; use text when selector is unknown.",
1479
1485
  inputSchema: BrowserLocateInputSchema,
1480
1486
  outputSchema: BrowserLocateOutputSchema,
1481
1487
  annotations: annotations("Locate DOM Targets", true)
@@ -1500,7 +1506,7 @@ function registerBrowserAgentMcpTools(server2, opts) {
1500
1506
  "browser_goto",
1501
1507
  {
1502
1508
  title: "Navigate To URL",
1503
- description: "Navigate an existing browser session to a URL when the user asks you to go to another page or continue browsing in the same session. Use browser_open first if no session exists. Follow with browser_screenshot to see redirects, login walls, CAPTCHA/Cloudflare state, or the loaded page.",
1509
+ description: "Navigate an existing browser session to a URL. Use browser_open first if no session exists; follow with browser_screenshot to see the loaded page.",
1504
1510
  inputSchema: BrowserGotoInputSchema,
1505
1511
  outputSchema: BrowserActionOutputSchema,
1506
1512
  annotations: annotations("Navigate To URL")
@@ -1514,7 +1520,7 @@ function registerBrowserAgentMcpTools(server2, opts) {
1514
1520
  "browser_click",
1515
1521
  {
1516
1522
  title: "Click",
1517
- description: "Click a visible page target using screenshot pixel coordinates. Use this when the user asks you to press a button, open a menu, choose a result, or activate a visible UI element. Use x/y only from the latest browser_screenshot, browser_read, or browser_locate result; do not guess coordinates. Follow with browser_screenshot when the click may change the page.",
1523
+ description: "Click a visible page target using screenshot pixel coordinates. Use x/y only from the latest browser_screenshot, browser_read, or browser_locate result; do not guess coordinates.",
1518
1524
  inputSchema: BrowserClickInputSchema,
1519
1525
  outputSchema: BrowserActionOutputSchema,
1520
1526
  annotations: annotations("Click")
@@ -1533,7 +1539,7 @@ function registerBrowserAgentMcpTools(server2, opts) {
1533
1539
  "browser_type",
1534
1540
  {
1535
1541
  title: "Type Text",
1536
- description: 'Type text into the currently focused browser field. Use this when the user asks you to fill a search box, form field, login field, or editable text area. Click or Tab to the field first if focus is uncertain. Use browser_press with ["Return"] to submit, and browser_screenshot afterward when page state matters.',
1542
+ description: 'Type text into the currently focused browser field. Click or Tab to the field first if focus is uncertain. Use browser_press with ["Return"] to submit.',
1537
1543
  inputSchema: BrowserTypeInputSchema,
1538
1544
  outputSchema: BrowserActionOutputSchema,
1539
1545
  annotations: annotations("Type Text")
@@ -1547,7 +1553,7 @@ function registerBrowserAgentMcpTools(server2, opts) {
1547
1553
  "browser_scroll",
1548
1554
  {
1549
1555
  title: "Scroll",
1550
- description: "Scroll the page to reveal more content before reading, clicking, or locating elements. Positive delta_y scrolls down; negative delta_y scrolls up. Follow with browser_screenshot or browser_read to inspect newly revealed content.",
1556
+ description: "Scroll the page to reveal more content. Positive delta_y scrolls down; negative scrolls up.",
1551
1557
  inputSchema: BrowserScrollInputSchema,
1552
1558
  outputSchema: BrowserActionOutputSchema,
1553
1559
  annotations: annotations("Scroll")
@@ -1566,7 +1572,7 @@ function registerBrowserAgentMcpTools(server2, opts) {
1566
1572
  "browser_press",
1567
1573
  {
1568
1574
  title: "Press Keys",
1569
- description: 'Press keyboard keys or combinations in the active browser session. Use this for submit, Escape, Tab navigation, select-all, or keyboard shortcuts. Examples: ["Return"], ["Escape"], ["Tab"], ["Ctrl+a"], ["Ctrl+Shift+Tab"]. Use browser_type for text entry and browser_screenshot after keypresses that may change the page.',
1575
+ description: "Press keyboard keys or combinations in the active browser session \u2014 submit, Escape, Tab navigation, select-all, or shortcuts. Use browser_type for text entry.",
1570
1576
  inputSchema: BrowserPressInputSchema,
1571
1577
  outputSchema: BrowserActionOutputSchema,
1572
1578
  annotations: annotations("Press Keys")
@@ -1580,7 +1586,7 @@ function registerBrowserAgentMcpTools(server2, opts) {
1580
1586
  "browser_replay_start",
1581
1587
  {
1582
1588
  title: "Start Recording",
1583
- description: "Start recording an MP4 replay of the session. Returns replay_id, view_url when available, and a download_url. Use to capture a task for later review; stop with browser_replay_stop.",
1589
+ description: "Start recording an MP4 replay of the session. Returns replay_id and a download_url. Stop with browser_replay_stop.",
1584
1590
  inputSchema: BrowserSessionInputSchema,
1585
1591
  outputSchema: BrowserReplayStartOutputSchema,
1586
1592
  annotations: annotations("Start Recording")
@@ -1603,7 +1609,7 @@ function registerBrowserAgentMcpTools(server2, opts) {
1603
1609
  "browser_replay_stop",
1604
1610
  {
1605
1611
  title: "Stop Recording",
1606
- description: "Stop a replay recording and expose the final view_url and download_url. Use browser_replay_download to save the MP4 to the configured output directory.",
1612
+ description: "Stop a replay recording and expose its final view_url/download_url. Use browser_replay_download to save the MP4.",
1607
1613
  inputSchema: BrowserReplayStopInputSchema,
1608
1614
  outputSchema: BrowserReplayStopOutputSchema,
1609
1615
  annotations: annotations("Stop Recording")
@@ -1626,7 +1632,7 @@ function registerBrowserAgentMcpTools(server2, opts) {
1626
1632
  "browser_list_replays",
1627
1633
  {
1628
1634
  title: "List Replay Videos",
1629
- description: "List replay recordings for a browser session, including final view_url and authenticated download_url values when available.",
1635
+ description: "List replay recordings for a browser session, including view_url and download_url when available.",
1630
1636
  inputSchema: BrowserSessionInputSchema,
1631
1637
  outputSchema: BrowserListReplaysOutputSchema,
1632
1638
  annotations: annotations("List Replay Videos", true)
@@ -1648,7 +1654,7 @@ function registerBrowserAgentMcpTools(server2, opts) {
1648
1654
  "browser_replay_download",
1649
1655
  {
1650
1656
  title: "Download Replay MP4",
1651
- description: "Download a replay recording through MCP Scraper and save the MP4 under MCP_SCRAPER_OUTPUT_DIR/browser-replays. Use after browser_replay_stop or browser_list_replays.",
1657
+ description: "Download a replay recording and save the MP4 under MCP_SCRAPER_OUTPUT_DIR/browser-replays. Use after browser_replay_stop or browser_list_replays.",
1652
1658
  inputSchema: BrowserReplayDownloadInputSchema,
1653
1659
  outputSchema: BrowserReplayDownloadOutputSchema,
1654
1660
  annotations: annotations("Download Replay MP4")
@@ -1672,7 +1678,7 @@ function registerBrowserAgentMcpTools(server2, opts) {
1672
1678
  "browser_replay_mark",
1673
1679
  {
1674
1680
  title: "Mark Replay Annotation",
1675
- description: "While a replay is actively recording, locate one exact DOM target and return a ready-to-use annotation object with DOM bounds and replay-relative timing. Use this instead of guessing start_seconds or drawing rough rectangles. Workflow: start browser_replay_start, navigate until the target is visible and stable, call browser_replay_mark for each callout, then stop the replay and pass the returned annotations to browser_replay_annotate.",
1681
+ description: "While a replay is actively recording, locate one exact DOM target and return a ready-to-use annotation with DOM bounds and replay-relative timing, instead of guessing start_seconds or rectangles. Pass the returned annotations to browser_replay_annotate after stopping the replay.",
1676
1682
  inputSchema: BrowserReplayMarkInputSchema,
1677
1683
  outputSchema: BrowserReplayMarkOutputSchema,
1678
1684
  annotations: annotations("Mark Replay Annotation", true)
@@ -1722,7 +1728,7 @@ function registerBrowserAgentMcpTools(server2, opts) {
1722
1728
  "browser_replay_annotate",
1723
1729
  {
1724
1730
  title: "Annotate Replay MP4",
1725
- description: "Download a browser replay MP4, render visual annotations over it, and save a new annotated MP4 under MCP_SCRAPER_OUTPUT_DIR/browser-replays. Use this after browser_replay_stop when the user wants proof videos with circles, boxes, arrows, underlines, or labels. For accurate timing and placement, prefer annotations returned by browser_replay_mark while the replay is recording; otherwise use exact left/top/width/height bounds from browser_locate. If the replay video size differs from the screenshot coordinate space, pass source_width and source_height.",
1731
+ description: "Download a browser replay MP4, render visual annotations (circles/boxes/arrows/labels) over it, and save a new annotated MP4. Prefer annotations from browser_replay_mark for accurate timing; otherwise use exact bounds from browser_locate. Pass source_width/source_height if the replay video size differs from the screenshot coordinate space.",
1726
1732
  inputSchema: BrowserReplayAnnotateInputSchema,
1727
1733
  outputSchema: BrowserReplayAnnotateOutputSchema,
1728
1734
  annotations: annotations("Annotate Replay MP4")
@@ -1764,7 +1770,7 @@ function registerBrowserAgentMcpTools(server2, opts) {
1764
1770
  "browser_close",
1765
1771
  {
1766
1772
  title: "Close Browser Session",
1767
- description: "Close and release a browser session when the task is done, when the user asks to stop the browser, or when active browser billing should end. Use browser_list_sessions first if you need to recover the session_id.",
1773
+ description: "Close and release a browser session when the task is done, to end active browser billing. Use browser_list_sessions first to recover a session_id.",
1768
1774
  inputSchema: BrowserSessionInputSchema,
1769
1775
  outputSchema: BrowserCloseOutputSchema,
1770
1776
  annotations: { title: "Close Browser Session", readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false }
@@ -1785,7 +1791,7 @@ function registerBrowserAgentMcpTools(server2, opts) {
1785
1791
  "browser_list_sessions",
1786
1792
  {
1787
1793
  title: "List Browser Sessions",
1788
- description: "List browser sessions and their status, with a watch_url for each. Use this to recover a session_id, find an active browser, or decide which session to close. Use browser_open to create a new session.",
1794
+ description: "List browser sessions and their status, with a watch_url for each. Use to recover a session_id or decide which session to close.",
1789
1795
  inputSchema: BrowserListInputSchema,
1790
1796
  outputSchema: BrowserListSessionsOutputSchema,
1791
1797
  annotations: annotations("List Browser Sessions", true)
@@ -1807,7 +1813,7 @@ function registerBrowserAgentMcpTools(server2, opts) {
1807
1813
  "query_fanout_workflow",
1808
1814
  {
1809
1815
  title: "Capture AI Search Fan-Out",
1810
- description: `Capture the query fan-out behind a ChatGPT or Claude web-search answer for Answer Engine Optimization (AEO): the exact sub-queries the model issued, every researched URL split into cited vs browsed-only (with citation frequency and snippet), each source tagged by category (First-party/vendor, News/media, Reddit, Social/video, Encyclopedia, Review site, Docs, Blog), plus top sourced sites and citation order. Set export=true for durable JSON, CSV, TSV, and HTML artifacts; export paths are returned relative to MCP_SCRAPER_OUTPUT_DIR. This returns the raw structured data; YOU analyze it \u2014 classify each sub-query by funnel stage (Problem-aware, Solution-aware, Decision-aware, Retention) and type (BoFu, Branded, Comparison, How-to, Search operator), name the brands the model researched, and give AEO insights. Sample asks: "capture the fan-out for this ChatGPT answer", "what did Claude search and cite for best CRM", "show the sub-queries and sources behind this AI answer". WRITE NOTE: passing prompt submits a real message in the user's logged-in account (a new conversation turn) \u2014 only send when the user wants that; omit prompt to capture a prompt the user just ran. Setup, if not already connected or if the profile disconnected: call browser_profile_connect, give the user the watch_url, let them complete login, then poll browser_profile_list until status is AUTHENTICATED. After that, browser_open the saved direct no-proxy profile and go to chatgpt.com or claude.ai. Fan-out is captured only as it streams, so the session must be open when the prompt runs. NOT for Google AI Overview citations \u2014 use harvest_paa for those; this tool is ChatGPT and Claude only.`,
1816
+ description: "Capture the query fan-out behind a ChatGPT or Claude web-search answer for AEO: sub-queries issued, every researched URL split into cited vs browsed-only, and top sourced sites. Returns raw structured data for you to classify and analyze. Set export=true for JSON/CSV/TSV/HTML artifacts. WRITE NOTE: passing prompt submits a real message in the user's logged-in account \u2014 only send when the user wants that; omit it to capture a prompt the user just ran. The session must already be open on chatgpt.com or claude.ai (see browser_profile_connect) while the prompt streams. NOT for Google AI Overview \u2014 use harvest_paa for that.",
1811
1817
  inputSchema: BrowserCaptureFanoutInputSchema,
1812
1818
  outputSchema: BrowserCaptureFanoutOutputSchema,
1813
1819
  annotations: annotations("Capture AI Search Fan-Out")
@@ -4544,6 +4550,19 @@ Multi-step orchestrations \u2014 prefer these over hand-chaining primitives when
4544
4550
  - Stand up a rank-tracking blueprint (database + cron + ingestion plan) -> **rank_tracker_workflow**.
4545
4551
  - Find which sources an AI answer cites for a query (AEO) -> **query_fanout_workflow** (open a browser
4546
4552
  session on chatgpt.com or claude.ai FIRST, then run it against that session).
4553
+ - Not sure which workflow fits a goal -> **workflow_suggest**; to just see what's available -> **workflow_list**.
4554
+ - Hosted/stepwise chain: **workflow_run** starts and returns \`runId\` (+ \`nextStep\` when multi-leg) ->
4555
+ **workflow_step** with that \`runId\` advances one leg at a time until \`done\` -> **workflow_status**
4556
+ re-opens a run to check state or recover artifact ids -> **workflow_artifact_read** pulls one artifact's
4557
+ full content by \`runId\` + \`artifactId\`.
4558
+ - Browser replay recording: **browser_replay_start** begins it -> **browser_replay_mark** (while
4559
+ recording) locates a DOM target and returns a ready-to-use timed annotation -> **browser_replay_stop**
4560
+ ends it -> feed collected annotations to **browser_replay_annotate**, or just
4561
+ **browser_replay_download** the plain MP4. **browser_list_replays** recovers replay ids.
4562
+ - Video breakdown is async: **video_frame_analysis** takes a DIRECT media file URL (resolve
4563
+ YouTube/Facebook/Instagram page URLs to one first, e.g. via \`facebook_page_intel\`'s \`videoUrl\`) and
4564
+ returns a \`runId\` immediately -> poll **video_frame_analysis_status** with that \`runId\` until \`status\`
4565
+ is \`done\`.
4547
4566
 
4548
4567
  ## Notes
4549
4568
  - Bulk / full-site crawls: call \`extract_site\` with \`rotateProxies:true\` for blocked or rate-limited
@@ -4658,25 +4677,25 @@ var RawMapsAboutAttributeSchema = import_zod2.z.object({
4658
4677
 
4659
4678
  // src/mcp/mcp-tool-schemas.ts
4660
4679
  var HarvestPaaInputSchema = {
4661
- query: import_zod3.z.string().min(1).describe('Core search topic only. If the user says "best hvac company in Denver CO", use query="best hvac company" and location="Denver, CO". Do not include the location in query when it can be separated.'),
4662
- location: import_zod3.z.string().optional().describe('City, region, or country for geo-targeted results, inferred from the user request when present, e.g. "Denver, CO", "Tokyo, Japan", "London, UK".'),
4663
- maxQuestions: import_zod3.z.number().int().min(1).max(200).default(30).describe("Number of PAA questions to extract. Default 30. Maximum 200. Use 10 for quick probes, 30 for normal research, 100-200 when the user asks for everything/full/deep research. Larger harvests get a longer server time budget (151-200 questions \u2192 up to 280s). Credits are charged by extracted question; unused request hold is refunded."),
4664
- gl: import_zod3.z.string().length(2).default("us").describe("Google country code inferred from location or user language. Examples: United States us, United Kingdom gb, Japan jp, Canada ca, Australia au."),
4665
- hl: import_zod3.z.string().default("en").describe("Google interface/content language inferred from the user request. Use en unless the user asks for another language or locale."),
4666
- device: import_zod3.z.enum(["desktop", "mobile"]).default("desktop").describe("SERP device context. Use desktop by default; use mobile only when the user asks for mobile rankings."),
4667
- proxyMode: import_zod3.z.enum(["location", "configured", "none"]).default(DEFAULT_PROXY_MODE).describe('Leave unset \u2014 routing is automatic (clean egress by default; residential only when a location is given, with fallback). Set "location" only to force local-pack targeting.'),
4668
- proxyZip: import_zod3.z.string().regex(/^\d{5}$/).optional().describe("Optional US ZIP override for residential location proxy targeting. Use only with proxyMode location when the user gives a specific ZIP or city-center targeting needs to be forced."),
4669
- debug: import_zod3.z.boolean().default(false).describe("Include sanitized browser/session/location diagnostics in the response. Use true when debugging localization, CAPTCHA, or proxy behavior.")
4680
+ query: import_zod3.z.string().min(1).describe('The search query. KEEP the place in the query text for localized results (e.g. "best hvac company Denver CO") and also set location \u2014 city-in-query is what localizes reliably.'),
4681
+ location: import_zod3.z.string().optional().describe('City, region, or country for geo signals, e.g. "Denver, CO". Set alongside city-in-query wording; alone it does NOT reliably localize.'),
4682
+ maxQuestions: import_zod3.z.number().int().min(1).max(200).default(30).describe("PAA questions to extract. Default 30, maximum 200. Use 10 for quick probes, 100-200 for deep research. Billed per extracted question; unused hold refunded."),
4683
+ gl: import_zod3.z.string().length(2).default("us").describe("Google country code inferred from location or user language."),
4684
+ hl: import_zod3.z.string().default("en").describe("Google interface/content language inferred from the user request."),
4685
+ device: import_zod3.z.enum(["desktop", "mobile"]).default("desktop").describe("SERP device context. Use mobile only for mobile rankings."),
4686
+ proxyMode: import_zod3.z.enum(["location", "configured", "none"]).default(DEFAULT_PROXY_MODE).describe('Leave unset (clean egress). Do NOT set "location" just because a city was named \u2014 that comes from city-in-query wording. "location" forces residential geo-IP for rank-tracking fidelity, is frequently CAPTCHA-blocked, and accepts failures.'),
4687
+ proxyZip: import_zod3.z.string().regex(/^\d{5}$/).optional().describe('US ZIP for residential geo-IP targeting. Only meaningful with proxyMode "location".'),
4688
+ debug: import_zod3.z.boolean().default(false).describe("Include sanitized diagnostics for debugging localization, CAPTCHA, or proxy behavior.")
4670
4689
  };
4671
4690
  var ExtractUrlInputSchema = {
4672
- url: import_zod3.z.string().url().describe("Public http/https URL to extract. Use this when the user provides one specific page URL."),
4673
- screenshot: import_zod3.z.boolean().default(false).describe("Also capture a full-page screenshot of the URL. Saved to ~/Downloads/mcp-scraper/screenshots/ and returned inline. Use when the user asks to see or capture the page visually."),
4674
- screenshotDevice: import_zod3.z.enum(["desktop", "mobile"]).default("desktop").describe("Viewport for screenshot. desktop = 1440\xD7900. mobile = 390\xD7844. Default desktop."),
4675
- extractBranding: import_zod3.z.boolean().default(false).describe("Extract brand colors, fonts, logo, and favicon using a rendered browser session. Returns colorScheme (light/dark), colors (primary/accent/background/text/heading as hex), fonts (heading/body family names), and assets (logo URL, favicon URL). Use when the user asks about brand colors, site theme, or brand assets."),
4676
- downloadMedia: import_zod3.z.boolean().default(false).describe("Extract and download all page media (images, video, audio) to ~/Downloads/mcp-scraper/media/. Ad networks, tracking pixels, and noise URLs are filtered automatically. Use when the user asks to download or harvest assets from a page."),
4691
+ url: import_zod3.z.string().url().describe("Public http/https URL to extract."),
4692
+ screenshot: import_zod3.z.boolean().default(false).describe("Capture a full-page screenshot, saved to ~/Downloads/mcp-scraper/screenshots/ and returned inline."),
4693
+ screenshotDevice: import_zod3.z.enum(["desktop", "mobile"]).default("desktop").describe("Viewport for screenshot. desktop = 1440\xD7900, mobile = 390\xD7844."),
4694
+ extractBranding: import_zod3.z.boolean().default(false).describe("Extract brand colors, fonts, logo, and favicon via a rendered browser session."),
4695
+ downloadMedia: import_zod3.z.boolean().default(false).describe("Extract and download page media (images/video/audio) to ~/Downloads/mcp-scraper/media/. Ad/tracking noise is filtered automatically."),
4677
4696
  mediaTypes: import_zod3.z.array(import_zod3.z.enum(["image", "video", "audio"])).default(["image", "video", "audio"]).describe("Which media types to download. Default all three."),
4678
- allowLocal: import_zod3.z.boolean().default(false).describe("Allow localhost and private-network URLs. For local development only."),
4679
- depositToVault: import_zod3.z.boolean().default(false).describe("Also save the full page content into the user's MCP Memory Library vault, server-side. The complete body is stored and embedded for semantic recall WITHOUT passing through this conversation \u2014 the tool result only confirms the saved note. Use when the user asks to save/remember/store a page, or to build a knowledge base from scrapes."),
4697
+ allowLocal: import_zod3.z.boolean().default(false).describe("Allow localhost and private-network URLs. Local development only."),
4698
+ depositToVault: import_zod3.z.boolean().default(false).describe("Save the full page content into the user's MCP Memory vault server-side, embedded for semantic recall \u2014 the full body is NOT returned to chat."),
4680
4699
  vaultName: import_zod3.z.string().trim().min(1).max(120).optional().describe("Optional vault to deposit into. Defaults to the user's personal vault.")
4681
4700
  };
4682
4701
  var MapSiteUrlsInputSchema = {
@@ -4684,34 +4703,34 @@ var MapSiteUrlsInputSchema = {
4684
4703
  maxUrls: import_zod3.z.number().int().min(1).max(1e4).optional().describe("Maximum URLs to discover. Use 100 for normal maps, up to 10000 for a full inventory. Large maps (over 500 URLs) write the complete inventory to a local file and return only a summary plus the file path instead of the full list inline.")
4685
4704
  };
4686
4705
  var ExtractSiteInputSchema = {
4687
- url: import_zod3.z.string().url().describe("Public website URL or domain to crawl for page CONTENT across multiple pages (map + scrape). Use when the user wants the content/text/markdown of a site's pages. For a technical SEO audit (issues, link graph, indexability, headings, image weights) use audit_site instead \u2014 extract_site returns content only, not analysis."),
4688
- maxPages: import_zod3.z.number().int().min(1).max(1e4).optional().describe("Maximum pages to extract. Use 50 for a normal crawl, up to 10000 for a full-site bulk scrape. Bulk crawls (over 25 pages) switch to folder mode: every page is saved as its own Markdown file in a local folder and the response returns only a summary plus the folder path, so the full content never floods the context window."),
4689
- rotateProxies: import_zod3.z.boolean().optional().describe("Route page fetches through rotating residential proxies in headful browsers to defeat rate-limiting and bot blocks (403/429). Discovers URLs from the sitemap, fetches in batches with a fresh proxy per batch, retries failures on a new proxy, and automatically parallelizes across the account's concurrency slots. Slower and pricier than the default fetch path, so use only when a site blocks normal crawling."),
4690
- rotateProxyEvery: import_zod3.z.number().int().min(1).max(100).optional().describe("When rotateProxies is on, how many pages to fetch per browser/proxy before discarding it and minting a fresh one. Default 30. Lower values rotate IPs more aggressively against strict rate limits."),
4691
- formats: import_zod3.z.array(import_zod3.z.enum(["markdown", "links", "json", "images", "branding"])).optional().describe("Which data formats to include in the per-page output. markdown=page content as Markdown, links=internal/external link graph, json=structured data/schema, images=per-page image URLs (links only, no downloads), branding=site-level logo/colors/fonts captured once from the homepage (requires a browser, adds time). markdown, links, json, and images are always captured cheaply from the HTML; branding is the only opt-in capture. Defaults to markdown+links when omitted.")
4706
+ url: import_zod3.z.string().url().describe("Public website URL or domain to crawl for page CONTENT (map + scrape). For a technical SEO audit use audit_site instead \u2014 this returns content only, not analysis."),
4707
+ maxPages: import_zod3.z.number().int().min(1).max(1e4).optional().describe("Maximum pages to extract. Bulk crawls (over 25 pages) switch to folder mode: each page saved as its own Markdown file, with a summary plus folder path returned instead of inlining content."),
4708
+ rotateProxies: import_zod3.z.boolean().optional().describe("Route page fetches through rotating residential proxies to defeat rate-limiting and bot blocks (403/429). Slower and pricier \u2014 use only when a site blocks normal crawling."),
4709
+ rotateProxyEvery: import_zod3.z.number().int().min(1).max(100).optional().describe("When rotateProxies is on, pages fetched per proxy before rotating. Default 30."),
4710
+ formats: import_zod3.z.array(import_zod3.z.enum(["markdown", "links", "json", "images", "branding"])).optional().describe("Per-page output formats: markdown, links, json, images are captured cheaply from HTML; branding (site-level logo/colors/fonts) requires a browser and adds time. Defaults to markdown+links.")
4692
4711
  };
4693
4712
  var AuditSiteInputSchema = {
4694
- url: import_zod3.z.string().url().describe("Public website URL or domain to run a full technical SEO audit on. Use when the user asks for a technical audit, SEO audit, site health check, or a Screaming-Frog-style crawl \u2014 i.e. they want ANALYSIS (issues, internal link graph, indexability, heading breakdown, image sizes/formats), not just page content. For plain content scraping use extract_site instead."),
4695
- maxPages: import_zod3.z.number().int().min(1).max(1e4).optional().describe("Maximum pages to crawl and audit. Use 50 for a normal audit, up to 10000 for a full-site audit. The audit always writes a folder of analysis files (report.md, issues.json, pages.jsonl, links.jsonl, link-metrics.jsonl, images.jsonl) plus per-page content, and returns a headline summary plus the folder path."),
4696
- rotateProxies: import_zod3.z.boolean().optional().describe("Route page fetches through rotating residential proxies to defeat rate-limiting and bot blocks (403/429). Slower and pricier than the default fetch path, so use only when a site blocks normal crawling."),
4697
- rotateProxyEvery: import_zod3.z.number().int().min(1).max(100).optional().describe("When rotateProxies is on, how many pages to fetch per browser/proxy before discarding it and minting a fresh one. Default 30.")
4713
+ url: import_zod3.z.string().url().describe("Public website URL or domain for a full technical SEO audit (issues, link graph, indexability, headings, images). For plain content use extract_site instead."),
4714
+ maxPages: import_zod3.z.number().int().min(1).max(1e4).optional().describe("Maximum pages to crawl and audit. Always writes a folder of analysis files plus per-page content, returning a summary plus the folder path."),
4715
+ rotateProxies: import_zod3.z.boolean().optional().describe("Route page fetches through rotating residential proxies to defeat rate-limiting and bot blocks. Slower/pricier \u2014 use only when a site blocks normal crawling."),
4716
+ rotateProxyEvery: import_zod3.z.number().int().min(1).max(100).optional().describe("When rotateProxies is on, pages fetched per proxy before rotating. Default 30.")
4698
4717
  };
4699
4718
  var YoutubeHarvestInputSchema = {
4700
4719
  mode: import_zod3.z.enum(["search", "channel"]).describe("Use search for topic/keyword requests. Use channel when the user provides @handle, channel ID, or channel URL."),
4701
4720
  query: import_zod3.z.string().optional().describe("Required when mode is search. The YouTube search topic in the user\u2019s words."),
4702
4721
  channelHandle: import_zod3.z.string().optional().describe("YouTube channel handle, channel ID, or URL. Examples: @mkbhd, UC..., https://youtube.com/@mkbhd."),
4703
- maxVideos: import_zod3.z.number().int().min(1).max(500).default(50).describe("Number of videos to return. Default 50, maximum 500. Use 10-25 for quick topic discovery, 50 for normal channel/search harvests, and larger values only when the user asks for full channel/history because large responses consume more context.")
4722
+ maxVideos: import_zod3.z.number().int().min(1).max(500).default(50).describe("Number of videos to return. Default 50, maximum 500.")
4704
4723
  };
4705
4724
  var YoutubeTranscribeInputSchema = {
4706
4725
  videoId: import_zod3.z.string().min(1).optional().describe("YouTube video ID, e.g. dQw4w9WgXcQ. Use only an ID returned by youtube_harvest or visible in a YouTube URL; do not invent one."),
4707
- url: import_zod3.z.string().url().optional().describe("Full YouTube URL, e.g. https://www.youtube.com/watch?v=dQw4w9WgXcQ or https://youtu.be/dQw4w9WgXcQ. Use this when the user pasted a URL instead of an ID. Provide videoId or url.")
4726
+ url: import_zod3.z.string().url().optional().describe("Full YouTube URL. Use when the user pasted a URL instead of an ID. Provide videoId or url.")
4708
4727
  };
4709
4728
  var FacebookPageIntelInputSchema = {
4710
- pageId: import_zod3.z.string().optional().describe("Facebook advertiser/page ID. Use only a pageId returned by facebook_ad_search/facebook_page_intel or copied from Facebook Ad Library; do not construct one yourself."),
4711
- libraryId: import_zod3.z.string().optional().describe("Facebook Ad Library archive ID for a known ad or advertiser sample. Use a libraryId returned by facebook_ad_search, or a libraryId/adArchiveId visible in Ad Library."),
4729
+ pageId: import_zod3.z.string().optional().describe("Facebook advertiser/page ID. Use only a value returned by facebook_ad_search or copied from Ad Library."),
4730
+ libraryId: import_zod3.z.string().optional().describe("Facebook Ad Library archive ID. Use a value returned by facebook_ad_search, or a libraryId/adArchiveId visible in Ad Library."),
4712
4731
  query: import_zod3.z.string().optional().describe("Advertiser or brand name when pageId/libraryId is not known. One of pageId, libraryId, or query is required."),
4713
- maxAds: import_zod3.z.number().int().min(1).max(200).default(50).describe("Maximum ads to inspect. Default 50, maximum 200. Prefer 25-50 for focused advertiser scans; use 100-200 only when the user asks for a broad ad archive sweep."),
4714
- country: import_zod3.z.string().length(2).default("US").describe("Two-letter Ad Library country code. Default US. Examples: US, CA, GB, AU. Infer from the user request when they name a country.")
4732
+ maxAds: import_zod3.z.number().int().min(1).max(200).default(50).describe("Maximum ads to inspect. Default 50, maximum 200."),
4733
+ country: import_zod3.z.string().length(2).default("US").describe("Two-letter Ad Library country code. Default US.")
4715
4734
  };
4716
4735
  var FacebookAdSearchInputSchema = {
4717
4736
  query: import_zod3.z.string().min(1).describe("Advertiser, brand, competitor, niche, or keyword to search in Facebook Ad Library."),
@@ -4719,18 +4738,28 @@ var FacebookAdSearchInputSchema = {
4719
4738
  maxResults: import_zod3.z.number().int().min(1).max(20).default(10).describe("Maximum advertisers to return. Default 10, maximum 20. Prefer tighter search terms over maxing this out.")
4720
4739
  };
4721
4740
  var RedditThreadInputSchema = {
4722
- url: import_zod3.z.string().min(1).describe("A reddit.com thread/post URL (www, old, or new Reddit, or a redd.it link). The service fetches it via old.reddit through a residential proxy and returns the post plus its comment tree."),
4741
+ url: import_zod3.z.string().min(1).describe("A reddit.com thread/post URL (www, old, new Reddit, or redd.it)."),
4723
4742
  maxComments: import_zod3.z.number().int().min(1).max(2e3).optional().describe("Optional cap on comments returned. Omit to return all captured comments.")
4724
4743
  };
4744
+ var VideoFrameAnalysisInputSchema = {
4745
+ sourceUrl: import_zod3.z.string().min(1).describe("A DIRECT video file URL (.mp4/.webm/.mov). Not a YouTube/Facebook/Instagram page URL \u2014 resolve those to a direct media URL first. Videos up to 30 minutes are supported."),
4746
+ intervalS: import_zod3.z.number().min(1).max(30).optional().describe("Preferred seconds between sampled frames (1-30, default 2). Automatically widened for long videos so the whole duration is covered within the frame budget."),
4747
+ maxFrames: import_zod3.z.number().int().min(1).max(120).optional().describe("Max frames analyzed (<=120, default 120). Frames are spread evenly across the whole video."),
4748
+ detail: import_zod3.z.enum(["fast", "standard", "deep"]).optional().describe("Analysis depth. Default standard."),
4749
+ vault: import_zod3.z.string().min(1).optional().describe('Memory vault to save the finished breakdown into. Default "Library".')
4750
+ };
4751
+ var VideoFrameAnalysisStatusInputSchema = {
4752
+ runId: import_zod3.z.string().min(1).describe("The runId returned by video_frame_analysis.")
4753
+ };
4725
4754
  var FacebookAdTranscribeInputSchema = {
4726
- videoUrl: import_zod3.z.string().url().describe("Direct Facebook CDN video URL from a facebook_page_intel ad result. Do not pass a public Facebook reel/post/share URL here; use facebook_video_transcribe for organic Facebook URLs.")
4755
+ videoUrl: import_zod3.z.string().url().describe("Direct Facebook CDN video URL from facebook_page_intel. Do not pass a public post/reel/share URL \u2014 use facebook_video_transcribe for those.")
4727
4756
  };
4728
4757
  var FacebookVideoTranscribeInputSchema = {
4729
- url: import_zod3.z.string().url().describe("Organic Facebook reel, video, watch, post, or share URL from facebook.com, m.facebook.com, or fb.watch. The tool renders the page, extracts the best matching public Facebook CDN MP4 URL, then transcribes it. Use this when the user pastes a normal Facebook video page URL and asks for the transcript or downloadable MP4."),
4758
+ url: import_zod3.z.string().url().describe("Organic Facebook reel/video/watch/post/share URL from facebook.com, m.facebook.com, or fb.watch."),
4730
4759
  quality: import_zod3.z.enum(["best", "hd", "sd"]).default("best").describe("Preferred progressive MP4 quality. Use best by default; hd prefers the highest HD progressive URL; sd forces the SD URL.")
4731
4760
  };
4732
4761
  var GoogleAdsSearchInputSchema = {
4733
- query: import_zod3.z.string().min(1).describe("A domain (e.g. getviktor.com) or advertiser/brand name to look up in the Google Ads Transparency Center. Domains resolve to the advertisers running ads that point to that site."),
4762
+ query: import_zod3.z.string().min(1).describe("A domain (e.g. getviktor.com) or advertiser/brand name to look up in Google Ads Transparency Center."),
4734
4763
  region: import_zod3.z.string().length(2).default("US").describe("Two-letter region code for where the ads are shown. Default US. Examples: US, CA, GB, AU."),
4735
4764
  maxResults: import_zod3.z.number().int().min(1).max(20).default(10).describe("Maximum advertisers to return. Default 10, maximum 20.")
4736
4765
  };
@@ -4741,78 +4770,78 @@ var GoogleAdsPageIntelInputSchema = {
4741
4770
  maxAds: import_zod3.z.number().int().min(1).max(200).default(50).describe("Maximum creatives to inspect and hydrate. Default 50, maximum 200. Prefer 25-50 for focused scans.")
4742
4771
  };
4743
4772
  var GoogleAdsTranscribeInputSchema = {
4744
- videoUrl: import_zod3.z.string().url().describe("Direct googlevideo.com playback URL from a google_ads_page_intel video ad result (the videoUrl field). For YouTube-hosted ads use youtube_transcribe with the returned youtubeVideoId instead.")
4773
+ videoUrl: import_zod3.z.string().url().describe("Direct googlevideo.com playback URL from google_ads_page_intel. For YouTube-hosted ads use youtube_transcribe instead.")
4745
4774
  };
4746
4775
  var InstagramProfileContentInputSchema = {
4747
4776
  handle: import_zod3.z.string().min(1).optional().describe("Instagram handle, with or without @. Provide handle or url."),
4748
- url: import_zod3.z.string().url().optional().describe("Instagram profile URL, e.g. https://www.instagram.com/nasaartemis/. Provide handle or url."),
4749
- profile: import_zod3.z.string().min(1).optional().describe("Optional saved hosted browser profile name to load authenticated Instagram access. If omitted, the server uses its configured default profile when present."),
4750
- saveProfileChanges: import_zod3.z.boolean().optional().describe("Whether to save browser changes back to the hosted profile. Leave unset unless intentionally updating the saved login session."),
4751
- maxItems: import_zod3.z.number().int().min(1).max(2e3).default(50).describe("Maximum profile grid post/reel/tv URLs to collect. Default 50, maximum 2000. Use higher values only when the user asks for a fuller archive."),
4752
- maxScrolls: import_zod3.z.number().int().min(0).max(250).default(10).describe("Maximum pagination scroll attempts. Default 10, maximum 250. Increase for long profiles when Instagram continues loading more grid links."),
4753
- scrollDelayMs: import_zod3.z.number().int().min(250).max(5e3).default(1200).describe("Delay after each pagination scroll before collecting newly loaded links. Default 1200ms. Increase to 2000-3000ms when Instagram loads slowly."),
4754
- stableScrollLimit: import_zod3.z.number().int().min(1).max(10).default(4).describe("Stop after this many consecutive scrolls with no new links or scroll progress. Default 4.")
4777
+ url: import_zod3.z.string().url().optional().describe("Instagram profile URL. Provide handle or url."),
4778
+ profile: import_zod3.z.string().min(1).optional().describe("Optional saved hosted browser profile name for authenticated Instagram access."),
4779
+ saveProfileChanges: import_zod3.z.boolean().optional().describe("Save browser changes back to the hosted profile. Leave unset unless intentionally updating the saved login."),
4780
+ maxItems: import_zod3.z.number().int().min(1).max(2e3).default(50).describe("Maximum grid URLs to collect. Default 50, maximum 2000."),
4781
+ maxScrolls: import_zod3.z.number().int().min(0).max(250).default(10).describe("Maximum pagination scroll attempts. Default 10, maximum 250."),
4782
+ scrollDelayMs: import_zod3.z.number().int().min(250).max(5e3).default(1200).describe("Delay after each scroll before collecting new links. Default 1200ms."),
4783
+ stableScrollLimit: import_zod3.z.number().int().min(1).max(10).default(4).describe("Stop after this many consecutive scrolls with no new links.")
4755
4784
  };
4756
4785
  var InstagramMediaDownloadInputSchema = {
4757
- url: import_zod3.z.string().url().describe("Instagram post, reel, or tv URL, e.g. https://www.instagram.com/reel/SHORTCODE/. The tool renders the page, extracts text, image metadata, and Instagram CDN media tracks."),
4758
- profile: import_zod3.z.string().min(1).optional().describe("Optional saved hosted browser profile name to load authenticated Instagram access. If omitted, the server uses its configured default profile when present."),
4759
- saveProfileChanges: import_zod3.z.boolean().optional().describe("Whether to save browser changes back to the hosted profile. Leave unset unless intentionally updating the saved login session."),
4760
- mediaTypes: import_zod3.z.array(import_zod3.z.enum(["image", "video", "audio"])).default(["image", "video", "audio"]).describe("Which media types to download when downloadMedia is true. Reels commonly expose separate video-only and audio-only MP4 tracks."),
4761
- downloadMedia: import_zod3.z.boolean().default(true).describe("Download extracted text/media files to the MCP Scraper output directory when the API server can write files. Always returns extracted media URLs even when false."),
4762
- downloadAllTracks: import_zod3.z.boolean().default(false).describe("Download every captured Instagram MP4 track instead of only the selected best video and audio tracks. Use false by default to avoid duplicate bitrates."),
4763
- includeTranscript: import_zod3.z.boolean().default(false).describe("Transcribe the selected audio track when available. This adds transcription cost and may take longer."),
4764
- mux: import_zod3.z.boolean().default(true).describe("When video and audio tracks are downloaded separately, try to mux them into a single MP4 if ffmpeg is available. Returns separate tracks when muxing is unavailable.")
4786
+ url: import_zod3.z.string().url().describe("Instagram post, reel, or tv URL, e.g. https://www.instagram.com/reel/SHORTCODE/."),
4787
+ profile: import_zod3.z.string().min(1).optional().describe("Optional saved hosted browser profile name for authenticated Instagram access."),
4788
+ saveProfileChanges: import_zod3.z.boolean().optional().describe("Save browser changes back to the hosted profile. Leave unset unless intentionally updating the saved login."),
4789
+ mediaTypes: import_zod3.z.array(import_zod3.z.enum(["image", "video", "audio"])).default(["image", "video", "audio"]).describe("Which media types to download when downloadMedia is true."),
4790
+ downloadMedia: import_zod3.z.boolean().default(true).describe("Download extracted text/media files to the output directory. Media URLs are always returned even when false."),
4791
+ downloadAllTracks: import_zod3.z.boolean().default(false).describe("Download every captured MP4 track instead of only the best video/audio pair."),
4792
+ includeTranscript: import_zod3.z.boolean().default(false).describe("Transcribe the selected audio track. Adds transcription cost and time."),
4793
+ mux: import_zod3.z.boolean().default(true).describe("Mux separately downloaded video/audio tracks into one MP4 if ffmpeg is available.")
4765
4794
  };
4766
4795
  var MapsPlaceIntelInputSchema = {
4767
- businessName: import_zod3.z.string().min(1).describe('Business name only. If user says "Elite Roofing Denver CO", use businessName="Elite Roofing" and location="Denver, CO".'),
4768
- location: import_zod3.z.string().min(1).describe('City/region/country where the business should be searched, e.g. "Denver, CO". Infer from the user request when possible.'),
4796
+ businessName: import_zod3.z.string().min(1).describe('Business name only, e.g. "Elite Roofing" (not "Elite Roofing Denver CO" \u2014 put the city in location).'),
4797
+ location: import_zod3.z.string().min(1).describe('City/region/country where the business should be searched, e.g. "Denver, CO".'),
4769
4798
  gl: import_zod3.z.string().length(2).default("us").describe("Google country code inferred from location."),
4770
4799
  hl: import_zod3.z.string().length(2).default("en").describe("Language inferred from user request."),
4771
- includeReviews: import_zod3.z.boolean().default(false).describe("Whether to fetch individual review cards. Use true when the user asks for reviews, customer pain, complaints, praise, themes, or review evidence."),
4772
- maxReviews: import_zod3.z.number().int().min(1).max(500).default(50).describe("Max review cards to return when includeReviews is true. Default 50, maximum 500. Use 50 for normal review analysis and larger values only for deep review mining.")
4800
+ includeReviews: import_zod3.z.boolean().default(false).describe("Fetch individual review cards \u2014 for reviews, customer pain, complaints, or praise themes."),
4801
+ maxReviews: import_zod3.z.number().int().min(1).max(500).default(50).describe("Max review cards when includeReviews is true. Default 50, maximum 500.")
4773
4802
  };
4774
4803
  var MapsSearchInputSchema = {
4775
- query: import_zod3.z.string().min(1).describe('Business category, niche, keyword, or search term. If the user says "roofers in Denver CO", use query="roofers" and location="Denver, CO". Do not put the location here when it can be separated.'),
4776
- location: import_zod3.z.string().optional().describe('City, region, country, or service area for the Maps search, e.g. "Denver, CO". Infer from the user request when present.'),
4804
+ query: import_zod3.z.string().min(1).describe('Business category, niche, or search term, e.g. "roofers". Do not include location here \u2014 use location instead.'),
4805
+ location: import_zod3.z.string().optional().describe('City, region, country, or service area, e.g. "Denver, CO".'),
4777
4806
  gl: import_zod3.z.string().length(2).default("us").describe("Google country code inferred from location."),
4778
4807
  hl: import_zod3.z.string().length(2).default("en").describe("Language inferred from user request."),
4779
- maxResults: import_zod3.z.number().int().min(1).max(50).default(10).describe("Number of Google Maps business/profile candidates to return. Default 10. Maximum 50. Use 10 unless the user asks for more."),
4780
- proxyMode: import_zod3.z.enum(["location", "configured", "none"]).default(DEFAULT_MAPS_PROXY_MODE).describe("Proxy targeting mode. Maps defaults to location so local market searches get city/state residential proxy targeting and rotation. Use configured to force the service proxy without city/ZIP targeting, and none only for local direct-network debugging."),
4781
- proxyZip: import_zod3.z.string().regex(/^\d{5}$/).optional().describe("Optional US ZIP override for residential location proxy targeting. Use when the user gives a specific ZIP or city-center ZIP."),
4782
- debug: import_zod3.z.boolean().default(false).describe("Include sanitized browser/proxy diagnostics when debugging Maps localization, CAPTCHA, or proxy behavior.")
4808
+ maxResults: import_zod3.z.number().int().min(1).max(50).default(10).describe("Number of candidates to return. Default 10, maximum 50."),
4809
+ proxyMode: import_zod3.z.enum(["location", "configured", "none"]).default(DEFAULT_MAPS_PROXY_MODE).describe("Defaults to location (city/state residential proxy targeting). configured forces the service proxy without city/ZIP targeting; none is local debugging only."),
4810
+ proxyZip: import_zod3.z.string().regex(/^\d{5}$/).optional().describe("Optional US ZIP override for residential proxy targeting."),
4811
+ debug: import_zod3.z.boolean().default(false).describe("Include sanitized browser/proxy diagnostics.")
4783
4812
  };
4784
4813
  var DirectoryWorkflowInputSchema = {
4785
- query: import_zod3.z.string().min(1).describe("Business category, niche, or keyword to search on Google Maps for every selected market, e.g. roofers, dentists, med spas. Do not include the city here."),
4786
- state: import_zod3.z.string().min(2).default("TN").describe("US state abbreviation or state name used to select Census places, e.g. TN or Tennessee."),
4787
- minPopulation: import_zod3.z.number().int().min(0).default(1e5).describe('Minimum Census place population for market selection. Use 100000 for "cities above 100k population".'),
4788
- populationYear: import_zod3.z.number().int().min(2020).max(2025).default(2025).describe("Census population estimate year from the 2020-2025 Population Estimates Program city/place dataset."),
4789
- maxCities: import_zod3.z.number().int().min(1).max(100).default(25).describe("Maximum number of markets to process after sorting by population descending."),
4790
- maxResultsPerCity: import_zod3.z.number().int().min(1).max(50).default(50).describe("Google Maps business/profile candidates to collect for each city. Maximum 50."),
4791
- concurrency: import_zod3.z.number().int().min(1).max(5).default(5).describe("How many city Maps searches to run in parallel. Use 5 for broad directory batches unless debugging."),
4792
- includeZipGroups: import_zod3.z.boolean().default(true).describe("Attach ZIP groups from a configured US ZIPS CSV when available. Set MCP_SCRAPER_USZIPS_CSV_PATH on the API server or pass usZipsCsvPath in local/test mode."),
4793
- usZipsCsvPath: import_zod3.z.string().optional().describe("Local/test-only path to a US ZIPS CSV with state_abbr, zipcode, county, city columns, such as Lead Magician tools/analytics/data/uszips.csv. Deployed APIs should use MCP_SCRAPER_USZIPS_CSV_PATH instead."),
4794
- saveCsv: import_zod3.z.boolean().default(true).describe("Save a directory-ready CSV to the MCP Scraper output directory and return its path. CSV rows include source_location, result_position, business_name, review_stars, review_count, category, address, phone, hours_status, website_url, directions_url, place_url, CID fields, population, and ZIP groups."),
4795
- proxyMode: import_zod3.z.enum(["location", "configured", "none"]).default(DEFAULT_MAPS_PROXY_MODE).describe("Proxy targeting mode for every city Maps search. Maps workflows default to location so each city can use city/state or ZIP-group residential proxy targeting. Use configured to force the service proxy without city/ZIP targeting, and none only for local direct-network debugging."),
4796
- proxyZip: import_zod3.z.string().regex(/^\d{5}$/).optional().describe("Optional ZIP override for proxy targeting. Normally omit it so each city can use its ZIP group or city/state location."),
4797
- debug: import_zod3.z.boolean().default(false).describe("Include sanitized browser/proxy diagnostics in each Maps browser session when supported.")
4814
+ query: import_zod3.z.string().min(1).describe("Business category, niche, or keyword to search on Google Maps for every market. Do not include the city."),
4815
+ state: import_zod3.z.string().min(2).default("TN").describe("US state abbreviation or name used to select Census places, e.g. TN."),
4816
+ minPopulation: import_zod3.z.number().int().min(0).default(1e5).describe("Minimum Census place population for market selection."),
4817
+ populationYear: import_zod3.z.number().int().min(2020).max(2025).default(2025).describe("Census population estimate year (2020-2025 Population Estimates Program)."),
4818
+ maxCities: import_zod3.z.number().int().min(1).max(100).default(25).describe("Maximum markets to process after sorting by population descending."),
4819
+ maxResultsPerCity: import_zod3.z.number().int().min(1).max(50).default(50).describe("Google Maps candidates to collect per city."),
4820
+ concurrency: import_zod3.z.number().int().min(1).max(5).default(5).describe("City Maps searches to run in parallel."),
4821
+ includeZipGroups: import_zod3.z.boolean().default(true).describe("Attach ZIP groups from a configured US ZIPS CSV when available (MCP_SCRAPER_USZIPS_CSV_PATH or usZipsCsvPath)."),
4822
+ usZipsCsvPath: import_zod3.z.string().optional().describe("Local/test-only path to a US ZIPS CSV (state_abbr, zipcode, county, city columns). Deployed APIs should use MCP_SCRAPER_USZIPS_CSV_PATH instead."),
4823
+ saveCsv: import_zod3.z.boolean().default(true).describe("Save a directory-ready CSV of results to the MCP Scraper output directory and return its path."),
4824
+ proxyMode: import_zod3.z.enum(["location", "configured", "none"]).default(DEFAULT_MAPS_PROXY_MODE).describe("Proxy targeting per city search. Defaults to location (city/ZIP-group residential targeting); configured forces the service proxy; none is local debugging only."),
4825
+ proxyZip: import_zod3.z.string().regex(/^\d{5}$/).optional().describe("Optional ZIP override for proxy targeting; normally omitted."),
4826
+ debug: import_zod3.z.boolean().default(false).describe("Include sanitized browser/proxy diagnostics.")
4798
4827
  };
4799
4828
  var RankTrackerModeSchema = import_zod3.z.enum(["maps", "organic", "ai_overview", "paa"]);
4800
4829
  var RankTrackerBlueprintInputSchema = {
4801
4830
  projectName: import_zod3.z.string().min(1).optional().describe("Optional name for the rank tracker project, client, or campaign."),
4802
- targetDomain: import_zod3.z.string().min(1).optional().describe("Primary domain to track in organic results, AI Overview citations, and PAA sources, e.g. example.com."),
4803
- targetBusinessName: import_zod3.z.string().min(1).optional().describe("Primary Google Business Profile or brand/business name to match in Maps results. Required for reliable Maps rank tracking."),
4804
- trackingModes: import_zod3.z.array(RankTrackerModeSchema).min(1).max(4).default(["maps", "organic", "ai_overview", "paa"]).describe("Rank tracker surfaces to build. maps uses directory_workflow/maps_search. organic uses search_serp. ai_overview uses search_serp/harvest_paa. paa uses harvest_paa source presence."),
4805
- keywords: import_zod3.z.array(import_zod3.z.string().min(1)).max(200).default([]).describe("Seed keywords or service queries to track. Leave empty when the downstream AI should create the keyword table from user input."),
4806
- locations: import_zod3.z.array(import_zod3.z.string().min(1)).max(100).default([]).describe("Markets, cities, ZIPs, or service areas to track. Use city/state strings like Denver, CO for localized SERP and Maps checks."),
4831
+ targetDomain: import_zod3.z.string().min(1).optional().describe("Primary domain to track in organic results, AI Overview citations, and PAA sources."),
4832
+ targetBusinessName: import_zod3.z.string().min(1).optional().describe("Primary Google Business Profile/brand name to match in Maps results. Required for Maps tracking."),
4833
+ trackingModes: import_zod3.z.array(RankTrackerModeSchema).min(1).max(4).default(["maps", "organic", "ai_overview", "paa"]).describe("Rank tracker surfaces to build: maps, organic, ai_overview, paa."),
4834
+ keywords: import_zod3.z.array(import_zod3.z.string().min(1)).max(200).default([]).describe("Seed keywords or service queries to track. Leave empty to derive from user input downstream."),
4835
+ locations: import_zod3.z.array(import_zod3.z.string().min(1)).max(100).default([]).describe('Markets, cities, ZIPs, or service areas to track, e.g. "Denver, CO".'),
4807
4836
  competitors: import_zod3.z.array(import_zod3.z.string().min(1)).max(100).default([]).describe("Optional competitor domains or business names to persist as comparison targets."),
4808
- database: import_zod3.z.enum(["postgres", "neon", "supabase", "sqlite", "mysql"]).default("postgres").describe("Database family the downstream AI should target when generating migrations."),
4809
- scheduleCadence: import_zod3.z.enum(["daily", "weekly", "monthly", "custom"]).default("weekly").describe("Default recurring rank check cadence for the generated cron/heartbeat plan."),
4837
+ database: import_zod3.z.enum(["postgres", "neon", "supabase", "sqlite", "mysql"]).default("postgres").describe("Database family to target when generating migrations."),
4838
+ scheduleCadence: import_zod3.z.enum(["daily", "weekly", "monthly", "custom"]).default("weekly").describe("Default recurring rank check cadence."),
4810
4839
  customCron: import_zod3.z.string().min(1).optional().describe("Cron expression to use when scheduleCadence is custom."),
4811
- timezone: import_zod3.z.string().min(1).default("UTC").describe("IANA timezone for scheduled rank checks, e.g. America/Denver."),
4812
- includeCron: import_zod3.z.boolean().default(true).describe("Include a cron or heartbeat worker plan. Keep true for production rank trackers."),
4813
- includeDashboard: import_zod3.z.boolean().default(true).describe("Include dashboard/reporting requirements in the generated prompt."),
4814
- includeAlerts: import_zod3.z.boolean().default(true).describe("Include alert rules for rank movement and SERP feature gains/losses."),
4815
- notes: import_zod3.z.string().max(4e3).optional().describe("Extra product, client, stack, or hosting requirements to include in the implementation prompt.")
4840
+ timezone: import_zod3.z.string().min(1).default("UTC").describe("IANA timezone for scheduled rank checks."),
4841
+ includeCron: import_zod3.z.boolean().default(true).describe("Include a cron/heartbeat worker plan."),
4842
+ includeDashboard: import_zod3.z.boolean().default(true).describe("Include dashboard/reporting requirements."),
4843
+ includeAlerts: import_zod3.z.boolean().default(true).describe("Include alert rules for rank movement and SERP feature changes."),
4844
+ notes: import_zod3.z.string().max(4e3).optional().describe("Extra product, client, stack, or hosting requirements.")
4816
4845
  };
4817
4846
  var NullableString2 = import_zod3.z.string().nullable();
4818
4847
  var MapsSearchAttemptOutput = import_zod3.z.object({
@@ -4962,7 +4991,7 @@ var EntityIdsOutput = import_zod3.z.object({
4962
4991
  kgId: import_zod3.z.string().nullable(),
4963
4992
  cid: import_zod3.z.string().nullable(),
4964
4993
  gcid: import_zod3.z.string().nullable()
4965
- })).describe("Each entity actually named on the page, with whichever of its kgId/cid/gcid were found \u2014 the linked view. kgIds/cids/gcids below are the same IDs as flat deduplicated lists, without the name link, kept for backward compatibility."),
4994
+ })).describe("Entities named on the page with their kgId/cid/gcid. Flat lists below are the same IDs deduplicated, kept for backward compatibility."),
4966
4995
  kgIds: import_zod3.z.array(import_zod3.z.string()),
4967
4996
  cids: import_zod3.z.array(import_zod3.z.string()),
4968
4997
  gcids: import_zod3.z.array(import_zod3.z.string())
@@ -5151,6 +5180,22 @@ var FacebookAdSearchOutputSchema = {
5151
5180
  sampleLibraryId: NullableString2
5152
5181
  }))
5153
5182
  };
5183
+ var VideoFrameAnalysisOutputSchema = {
5184
+ ok: import_zod3.z.boolean(),
5185
+ runId: NullableString2,
5186
+ status: NullableString2,
5187
+ message: NullableString2
5188
+ };
5189
+ var VideoFrameAnalysisStatusOutputSchema = {
5190
+ ok: import_zod3.z.boolean(),
5191
+ runId: NullableString2,
5192
+ status: NullableString2,
5193
+ progress: import_zod3.z.object({ analyzed: import_zod3.z.number().int(), total: import_zod3.z.number().int() }).nullable().optional(),
5194
+ frameCount: import_zod3.z.number().int().nullable().optional(),
5195
+ artifactPath: NullableString2,
5196
+ report: NullableString2,
5197
+ error: NullableString2
5198
+ };
5154
5199
  var RedditThreadOutputSchema = {
5155
5200
  sourceUrl: NullableString2,
5156
5201
  oldRedditUrl: NullableString2,
@@ -5413,7 +5458,7 @@ var WorkflowIdSchema = import_zod3.z.enum([
5413
5458
  "ai-overview-language"
5414
5459
  ]);
5415
5460
  var WorkflowListInputSchema = {
5416
- includeRecipes: import_zod3.z.boolean().default(true).describe("Include high-level AI-facing recipes such as market analysis, ICP research, forum/review research, brand design briefings, CRO audits, positioning briefs, content gaps, and AI search visibility audits.")
5461
+ includeRecipes: import_zod3.z.boolean().default(true).describe("Include high-level AI-facing recipes (market analysis, ICP research, CRO audits, content gaps, etc).")
5417
5462
  };
5418
5463
  var WorkflowSuggestInputSchema = {
5419
5464
  goal: import_zod3.z.string().min(1).describe('The user goal or job to route, e.g. "market analysis for roofers in Tennessee", "ICP research for med spas", "CRO audit for this URL", or "brand design briefing".'),
@@ -5426,12 +5471,12 @@ var WorkflowSuggestInputSchema = {
5426
5471
  maxSuggestions: import_zod3.z.number().int().min(1).max(8).default(3).describe("Number of matching workflow recipes to return.")
5427
5472
  };
5428
5473
  var WorkflowRunInputSchema = {
5429
- workflowId: WorkflowIdSchema.describe("Workflow to run: directory, get-leads, agent-packet, local-competitive-audit, map-comparison, serp-comparison, paa-expansion-brief, ai-overview-language. Use only these values; call workflow_list or workflow_suggest first when unsure."),
5430
- input: import_zod3.z.record(import_zod3.z.unknown()).default({}).describe("Workflow-specific input object. Examples: get-leads uses {query, location, maxResults?, enrichWebsites?, hydrateReviewCounts?}; agent-packet uses {keyword, domain?, location?, maxQuestions?}; local-competitive-audit uses {query, state, minPopulation?, maxCities?, maxResultsPerCity?, hydrateTop?, maxReviews?}; serp-comparison uses {keyword, domain?, url?, location?, extractTop?}."),
5474
+ workflowId: WorkflowIdSchema.describe("Workflow to run. Call workflow_list or workflow_suggest first when unsure."),
5475
+ input: import_zod3.z.record(import_zod3.z.unknown()).default({}).describe("Workflow-specific input object; shape depends on workflowId. Call workflow_list or workflow_suggest to see required fields."),
5431
5476
  webhookUrl: import_zod3.z.string().url().optional().describe("Optional HTTPS webhook to receive the completed hosted workflow run event.")
5432
5477
  };
5433
5478
  var WorkflowStepInputSchema = {
5434
- runId: import_zod3.z.string().min(1).describe("Workflow run id returned by workflow_run, workflow_step, or workflow_status. Use only a returned runId; do not construct one yourself. Advances the run by exactly one step (one logical leg, e.g. one live harvest).")
5479
+ runId: import_zod3.z.string().min(1).describe("Workflow run id returned by workflow_run/workflow_step/workflow_status. Advances the run by exactly one step.")
5435
5480
  };
5436
5481
  var WorkflowStatusInputSchema = {
5437
5482
  runId: import_zod3.z.string().min(1).describe("Workflow run id returned by workflow_run, workflow_step, or workflow_status. Use only a returned runId; do not construct one yourself.")
@@ -5439,7 +5484,7 @@ var WorkflowStatusInputSchema = {
5439
5484
  var WorkflowArtifactReadInputSchema = {
5440
5485
  runId: import_zod3.z.string().min(1).describe("Workflow run id returned by workflow_run, workflow_step, or workflow_status. Use only a returned runId; do not construct one yourself."),
5441
5486
  artifactId: import_zod3.z.string().min(1).describe("Artifact id from the run artifact list returned by workflow_run, workflow_step, or workflow_status. Use only a returned artifactId; do not construct one yourself."),
5442
- maxBytes: import_zod3.z.number().int().min(1e3).max(1e6).default(2e5).describe("Maximum bytes of artifact text to return inline. Use lower values for large CSV/JSON artifacts; call again with the downloadUrl if needed outside MCP.")
5487
+ maxBytes: import_zod3.z.number().int().min(1e3).max(1e6).default(2e5).describe("Maximum bytes of artifact text to return inline.")
5443
5488
  };
5444
5489
  var WorkflowRecipeOutput = import_zod3.z.object({
5445
5490
  id: import_zod3.z.string(),
@@ -5498,28 +5543,28 @@ var WorkflowArtifactReadOutputSchema = {
5498
5543
  text: import_zod3.z.string()
5499
5544
  };
5500
5545
  var SearchSerpInputSchema = {
5501
- query: import_zod3.z.string().min(1).describe('Core search topic only. Separate location when possible. If user says "best dentist in Brooklyn NY serp", use query="best dentist" and location="Brooklyn, NY".'),
5502
- location: import_zod3.z.string().optional().describe("City, region, or country for geo-targeted results, inferred from user request when present."),
5546
+ query: import_zod3.z.string().min(1).describe('The search query. KEEP the place in the query text for localized results (e.g. "best dentist Brooklyn NY") and also set location \u2014 city-in-query is what localizes reliably.'),
5547
+ location: import_zod3.z.string().optional().describe("City, region, or country for geo signals. Set alongside city-in-query wording; alone it does NOT reliably localize."),
5503
5548
  gl: import_zod3.z.string().length(2).default("us").describe("Google country code inferred from location or user language."),
5504
5549
  hl: import_zod3.z.string().default("en").describe("Google interface/content language inferred from user request."),
5505
- device: import_zod3.z.enum(["desktop", "mobile"]).default("desktop").describe("SERP device context. Use desktop by default; use mobile only when the user asks for mobile rankings."),
5506
- proxyMode: import_zod3.z.enum(["location", "configured", "none"]).default(DEFAULT_PROXY_MODE).describe('Leave unset \u2014 routing is automatic. Set "location" only to force local-pack targeting.'),
5507
- proxyZip: import_zod3.z.string().regex(/^\d{5}$/).optional().describe("Optional US ZIP override for residential location proxy targeting. Use only with proxyMode location when the user gives a specific ZIP or city-center targeting needs to be forced."),
5508
- debug: import_zod3.z.boolean().default(false).describe("Include sanitized browser/session/location diagnostics in the response. Use true when debugging localization, CAPTCHA, or proxy behavior."),
5509
- pages: import_zod3.z.number().int().min(1).max(2).default(1).describe("Number of result pages to fetch (1\u20132)")
5550
+ device: import_zod3.z.enum(["desktop", "mobile"]).default("desktop").describe("SERP device context. Use mobile only for mobile rankings."),
5551
+ proxyMode: import_zod3.z.enum(["location", "configured", "none"]).default(DEFAULT_PROXY_MODE).describe('Leave unset (clean egress). Do NOT set "location" just because a city was named \u2014 that comes from city-in-query wording. "location" forces residential geo-IP for rank-tracking fidelity, is frequently CAPTCHA-blocked, and accepts failures.'),
5552
+ proxyZip: import_zod3.z.string().regex(/^\d{5}$/).optional().describe('US ZIP for residential geo-IP targeting. Only meaningful with proxyMode "location".'),
5553
+ debug: import_zod3.z.boolean().default(false).describe("Include sanitized diagnostics for debugging localization, CAPTCHA, or proxy behavior."),
5554
+ pages: import_zod3.z.number().int().min(1).max(2).default(1).describe("Number of result pages to fetch (1\u20132).")
5510
5555
  };
5511
5556
  var CaptureSerpSnapshotInputSchema = {
5512
- query: import_zod3.z.string().min(1).describe("Core search query to capture as a structured SERP Intelligence snapshot. Separate the place into location when the user gives a city, region, country, or ZIP."),
5513
- location: import_zod3.z.string().optional().describe("City, region, country, or service area used for localized Google results. MCP Scraper records location evidence; UULE alone is not proof of localization."),
5514
- gl: import_zod3.z.string().length(2).default("us").describe("Google country code inferred from the requested market, e.g. us, gb, ca, au."),
5557
+ query: import_zod3.z.string().min(1).describe('Search query to capture. KEEP the place in the query text for localized captures (e.g. "botox clinic austin tx") and also set location.'),
5558
+ location: import_zod3.z.string().optional().describe("City, region, country, or service area for localized Google results."),
5559
+ gl: import_zod3.z.string().length(2).default("us").describe("Google country code inferred from the requested market."),
5515
5560
  hl: import_zod3.z.string().default("en").describe("Google interface/content language inferred from the user request."),
5516
- device: import_zod3.z.enum(["desktop", "mobile"]).default("desktop").describe("SERP device context. Use mobile only when the user asks for mobile rankings or mobile SERP evidence."),
5517
- proxyMode: import_zod3.z.enum(["location", "configured", "none"]).default(DEFAULT_PROXY_MODE).describe('Leave unset \u2014 routing is automatic. Set "location" only to force city/ZIP targeting.'),
5518
- proxyZip: import_zod3.z.string().regex(/^\d{5}$/).optional().describe("Optional US ZIP override for residential location proxy targeting. Use only with proxyMode location when a precise city-center or ZIP proxy is needed."),
5519
- pages: import_zod3.z.number().int().min(1).max(2).default(1).describe("Number of Google result pages to capture. Use 1 normally and 2 only when the user needs deeper ranking evidence."),
5520
- debug: import_zod3.z.boolean().default(false).describe("Include sanitized browser, proxy, and location diagnostics. Use true when debugging localization, CAPTCHA, proxy selection, or capture reliability."),
5521
- includePageSnapshots: import_zod3.z.boolean().default(false).describe("Also capture ranking-page snapshots for selected SERP URLs through the same product capture path."),
5522
- pageSnapshotLimit: import_zod3.z.number().int().min(0).max(10).default(0).describe("Maximum ranking-page snapshots to capture when includePageSnapshots is true. Use 0 when only SERP evidence is needed.")
5561
+ device: import_zod3.z.enum(["desktop", "mobile"]).default("desktop").describe("SERP device context. Use mobile only for mobile rankings/evidence."),
5562
+ proxyMode: import_zod3.z.enum(["location", "configured", "none"]).default(DEFAULT_PROXY_MODE).describe('Leave unset (clean egress). Do NOT set "location" just because a city was named \u2014 that comes from city-in-query wording. "location" forces residential geo-IP, is frequently CAPTCHA-blocked, and accepts failures.'),
5563
+ proxyZip: import_zod3.z.string().regex(/^\d{5}$/).optional().describe('US ZIP for residential geo-IP targeting. Only meaningful with proxyMode "location".'),
5564
+ pages: import_zod3.z.number().int().min(1).max(2).default(1).describe("Google result pages to capture. Use 2 only for deeper ranking evidence."),
5565
+ debug: import_zod3.z.boolean().default(false).describe("Include sanitized browser/proxy/location diagnostics."),
5566
+ includePageSnapshots: import_zod3.z.boolean().default(false).describe("Also capture ranking-page snapshots for selected SERP URLs."),
5567
+ pageSnapshotLimit: import_zod3.z.number().int().min(0).max(10).default(0).describe("Maximum ranking-page snapshots when includePageSnapshots is true.")
5523
5568
  };
5524
5569
  var ScreenshotInputSchema = {
5525
5570
  url: import_zod3.z.string().url().describe("URL to capture as a full-page screenshot. Use http or https. Pass allowLocal: true to capture localhost or private-network URLs during development."),
@@ -5527,15 +5572,15 @@ var ScreenshotInputSchema = {
5527
5572
  allowLocal: import_zod3.z.boolean().default(false).describe("Allow localhost and private-network URLs (127.x, 192.168.x, 10.x, etc.). For local development only \u2014 not for production use.")
5528
5573
  };
5529
5574
  var CaptureSerpPageSnapshotsInputSchema = {
5530
- urls: import_zod3.z.array(import_zod3.z.string().url()).min(1).max(25).describe("Public HTTP/HTTPS URLs to capture as SERP Intelligence page snapshots. Do not pass localhost, private IPs, file URLs, or internal admin URLs."),
5575
+ urls: import_zod3.z.array(import_zod3.z.string().url()).min(1).max(25).describe("Public HTTP/HTTPS URLs to capture. Do not pass localhost, private IPs, file URLs, or internal admin URLs."),
5531
5576
  targets: import_zod3.z.array(import_zod3.z.object({
5532
5577
  url: import_zod3.z.string().url().describe("Public HTTP/HTTPS URL to capture."),
5533
- sourceKind: import_zod3.z.enum(["organic", "ai_citation", "local_pack_website", "configured_target", "site_subject"]).default("configured_target").describe("Why this page is being captured for SERP Intelligence evidence."),
5578
+ sourceKind: import_zod3.z.enum(["organic", "ai_citation", "local_pack_website", "configured_target", "site_subject"]).default("configured_target").describe("Why this page is being captured."),
5534
5579
  sourcePosition: import_zod3.z.number().int().min(1).optional().describe("Ranking or citation position when the page came from SERP evidence.")
5535
- }).strict()).min(1).max(25).optional().describe("Structured page snapshot targets. Use this instead of urls when source kind or position should be preserved."),
5536
- maxConcurrency: import_zod3.z.number().int().min(1).max(5).default(2).describe("Parallel page captures. Use 2 normally; higher values can increase proxy/browser pressure."),
5537
- timeoutMs: import_zod3.z.number().int().min(1e3).max(6e4).default(15e3).describe("Per-page capture timeout in milliseconds. Increase for slow pages; timeout artifacts are returned as structured capture failures."),
5538
- debug: import_zod3.z.boolean().default(false).describe("Include sanitized browser/proxy diagnostics for page snapshot debugging. Use true for capture, network, or proxy troubleshooting.")
5580
+ }).strict()).min(1).max(25).optional().describe("Structured targets. Use instead of urls when source kind or position should be preserved."),
5581
+ maxConcurrency: import_zod3.z.number().int().min(1).max(5).default(2).describe("Parallel page captures."),
5582
+ timeoutMs: import_zod3.z.number().int().min(1e3).max(6e4).default(15e3).describe("Per-page capture timeout in milliseconds; timeouts return as structured capture failures."),
5583
+ debug: import_zod3.z.boolean().default(false).describe("Include sanitized browser/proxy diagnostics.")
5539
5584
  };
5540
5585
 
5541
5586
  // src/mcp/rank-tracker-blueprint.ts
@@ -5874,14 +5919,14 @@ function liveWebToolAnnotations(title) {
5874
5919
  function registerSerpIntelligenceCaptureTools(server2, executor) {
5875
5920
  server2.registerTool("capture_serp_snapshot", {
5876
5921
  title: "SERP Intelligence Snapshot",
5877
- description: "Capture a structured SERP Intelligence Google snapshot through POST /serp-intelligence/capture, the same product capture path used by Phoenix. Split query from location and infer gl/hl. Routing is automatic \u2014 leave proxyMode unset. Set debug true when investigating location evidence, proxy behavior, CAPTCHA, or capture reliability.",
5922
+ description: "Capture a structured SERP Intelligence Google snapshot (the product capture path used by Phoenix). Split query from location; leave proxyMode unset.",
5878
5923
  inputSchema: CaptureSerpSnapshotInputSchema,
5879
5924
  outputSchema: CaptureSerpSnapshotOutputSchema,
5880
5925
  annotations: liveWebToolAnnotations("SERP Intelligence Snapshot")
5881
5926
  }, async (input) => formatCaptureSerpSnapshot(await executor.captureSerpSnapshot(input), input));
5882
5927
  server2.registerTool("capture_serp_page_snapshots", {
5883
5928
  title: "SERP Intelligence Page Snapshots",
5884
- description: "Capture public ranking-page evidence through POST /serp-intelligence/page-snapshots, the same product page snapshot path used by Phoenix. Provide urls for simple captures or targets when preserving organic, AI citation, local-pack, configured target, or site-subject source metadata. Private IPs, localhost, file URLs, and internal URLs are rejected by the service. Use timeoutMs for slow pages and debug true for sanitized proxy/browser diagnostics.",
5929
+ description: "Capture public ranking-page evidence as SERP Intelligence page snapshots (the product path used by Phoenix). Provide urls, or targets to preserve source metadata. Private IPs, localhost, file, and internal URLs are rejected.",
5885
5930
  inputSchema: CaptureSerpPageSnapshotsInputSchema,
5886
5931
  outputSchema: CaptureSerpPageSnapshotsOutputSchema,
5887
5932
  annotations: liveWebToolAnnotations("SERP Intelligence Page Snapshots")
@@ -5937,203 +5982,217 @@ function registerPaaExtractorMcpTools(server2, executor, options = {}) {
5937
5982
  if (savesReports) registerSavedReportResources(server2);
5938
5983
  server2.registerTool("harvest_paa", {
5939
5984
  title: "Google PAA + SERP Harvest",
5940
- description: withReportNote('Best default tool for Google search research. Extracts People Also Ask questions plus answers/source URLs, organic SERP, local pack when present, entity IDs (CID/GCID/KG MID), and AI Overview. Infer the user language: split topic from location (e.g. "best hvac company in Denver CO" => query "best hvac company", location "Denver, CO", gl "us", hl "en"). Routing is automatic \u2014 leave proxyMode unset. If a deep harvest exceeds the time budget it returns the questions gathered so far (partial), billed per returned question. Use maxQuestions 30 normally, 100-200 for "full", "deep", "all", or comprehensive research. Deep harvests above 100 questions can run for several minutes with no interim progress \u2014 warn the user before starting one and keep maxQuestions at or below 100 unless they explicitly want a deep harvest. Credits are charged by extracted question; unused request hold is refunded.'),
5985
+ description: withReportNote("Best default tool for Google search research: People Also Ask questions with answers/sources, organic SERP, local pack, entity IDs, and AI Overview. Split topic from location; leave proxyMode unset. Warn the user before maxQuestions above 100 \u2014 deep harvests can run several minutes with no interim progress, billed per extracted question."),
5941
5986
  inputSchema: HarvestPaaInputSchema,
5942
5987
  outputSchema: HarvestPaaOutputSchema,
5943
5988
  annotations: liveWebToolAnnotations("Google PAA + SERP Harvest")
5944
5989
  }, async (input) => formatHarvestPaa(await executor.harvestPaa(input), input));
5945
5990
  server2.registerTool("search_serp", {
5946
5991
  title: "Google SERP Lookup",
5947
- description: withReportNote("Fast Google SERP lookup without PAA expansion. Use when the user asks for rankings, organic results, local pack, quick SERP, or positions. Split topic from location and infer gl/hl from the user request. Routing is automatic \u2014 leave proxyMode unset; set location only to force local-pack targeting."),
5992
+ description: withReportNote("Fast Google SERP lookup without PAA expansion \u2014 rankings, organic results, local pack, positions. Split topic from location; leave proxyMode unset."),
5948
5993
  inputSchema: SearchSerpInputSchema,
5949
5994
  outputSchema: SearchSerpOutputSchema,
5950
5995
  annotations: liveWebToolAnnotations("Google SERP Lookup")
5951
5996
  }, async (input) => formatSearchSerp(await executor.searchSerp(input), input));
5952
5997
  server2.registerTool("extract_url", {
5953
5998
  title: "Single URL Extract",
5954
- description: withReportNote("Extract structured data from one public URL when the user provides one page, asks to inspect/scrape a page, or needs page content, schema, headings, metadata, screenshots, branding, or media assets. Returns structured page fields plus artifact handles for saved reports/screenshots/media when requested. Set depositToVault:true to also save the full page into the user's MCP Memory vault server-side (the full body is NOT returned to chat). Use map_site_urls before extracting a site inventory; use extract_site for multi-page crawling."),
5999
+ description: withReportNote("Extract structured data from one public URL: content, schema, headings, metadata, screenshots, branding, or media assets. Set depositToVault:true to save the full page into the user's MCP Memory vault server-side (not returned to chat)."),
5955
6000
  inputSchema: ExtractUrlInputSchema,
5956
6001
  outputSchema: ExtractUrlOutputSchema,
5957
6002
  annotations: liveWebToolAnnotations("Single URL Extract")
5958
6003
  }, async (input) => formatExtractUrl(await executor.extractUrl(input), input));
5959
6004
  server2.registerTool("map_site_urls", {
5960
6005
  title: "Site URL Map",
5961
- description: withReportNote("Map/crawl a public website when the user asks for a sitemap, URL inventory, broken-link scan, redirect scan, or crawl planning. Returns internal URLs with HTTP status and truncation metadata. Supports up to maxUrls=10000; maps over 500 URLs write the complete inventory to a local CSV file and return a summary plus the file path instead of the full list inline. Use this before extract_site when choosing pages for an audit; use extract_url for one known page."),
6006
+ description: withReportNote("Map/crawl a public website for a sitemap, URL inventory, or broken-link scan. Returns internal URLs with HTTP status; maps over 500 URLs are written to a local CSV file instead of inlined."),
5962
6007
  inputSchema: MapSiteUrlsInputSchema,
5963
6008
  outputSchema: MapSiteUrlsOutputSchema,
5964
6009
  annotations: liveWebToolAnnotations("Site URL Map")
5965
6010
  }, async (input) => formatMapSiteUrls(await executor.mapSiteUrls(input), input));
5966
6011
  server2.registerTool("extract_site", {
5967
6012
  title: "Multi-Page Site Content Crawl",
5968
- description: withReportNote("Crawl a public website and return the page CONTENT across multiple pages (map + scrape). Returns per-page titles and Markdown content. Supports up to maxPages=10000; bulk crawls over 25 pages switch to folder mode: each page is saved as its own Markdown file in a local folder and the response returns a summary plus the folder path instead of inlining all content. This tool returns content only \u2014 for a technical SEO audit (issues, internal link graph, indexability, heading breakdown, image sizes/formats) use audit_site. Use map_site_urls first when URL selection matters; use extract_url for one page."),
6013
+ description: withReportNote("Crawl a public website and return page CONTENT (Markdown) across multiple pages. Bulk crawls over 25 pages are saved as per-page Markdown files in a local folder instead of inlined. Content only \u2014 for a technical SEO audit use audit_site instead."),
5969
6014
  inputSchema: ExtractSiteInputSchema,
5970
6015
  outputSchema: ExtractSiteOutputSchema,
5971
6016
  annotations: liveWebToolAnnotations("Multi-Page Site Content Crawl")
5972
6017
  }, async (input) => formatExtractSite(await executor.extractSite(input), input));
5973
6018
  server2.registerTool("audit_site", {
5974
6019
  title: "Technical SEO Audit",
5975
- description: withReportNote("Run a full technical SEO audit (Screaming-Frog-style) on a public website. Crawls up to maxPages and produces a complete on-page + crawl analysis: per-page title/meta lengths, heading breakdown, indexability and canonical status, schema, an internal link graph (inlinks/orphans/crawl-depth), a link analysis (top internal pages by inlinks, top external domains by link count, broken/orphan counts), an issues report, and an image audit (byte size, format, over-100KB and legacy-format flags). Writes everything to a local folder (report.md, issues.json, pages.jsonl, links.jsonl, link-metrics.jsonl, link-report.md, links-summary.json, external-domains.json, images.jsonl) plus per-page content, and returns a headline summary plus the folder path. Use this when the user wants an audit or analysis; use extract_site when they just want page content."),
6020
+ description: withReportNote("Run a full technical SEO audit (Screaming-Frog-style) on a public website: on-page issues, internal link graph, indexability, heading/image analysis. Writes a folder of analysis files plus per-page content, and returns a summary plus the folder path. Use extract_site instead for plain page content."),
5976
6021
  inputSchema: AuditSiteInputSchema,
5977
6022
  outputSchema: AuditSiteOutputSchema,
5978
6023
  annotations: liveWebToolAnnotations("Technical SEO Audit")
5979
6024
  }, async (input) => formatAuditSite(await executor.auditSite(input), input));
5980
6025
  server2.registerTool("youtube_harvest", {
5981
6026
  title: "YouTube Video Harvest",
5982
- description: withReportNote('Harvest YouTube video metadata when the user wants to find videos by topic, inspect a channel library, compare video angles, or get videoIds for later transcription. Use mode "search" for keyword/topic requests and mode "channel" for @handles, channel IDs, or channel URLs. Returns titles, views, durations, URLs, and videoIds for follow-up transcription. Use youtube_transcribe after selecting one video.'),
6027
+ description: withReportNote('Harvest YouTube video metadata by topic search or channel library. Use mode "search" for keyword/topic requests, mode "channel" for @handles/channel IDs/URLs. Returns titles, views, durations, and videoIds.'),
5983
6028
  inputSchema: YoutubeHarvestInputSchema,
5984
6029
  outputSchema: YoutubeHarvestOutputSchema,
5985
6030
  annotations: liveWebToolAnnotations("YouTube Video Harvest")
5986
6031
  }, async (input) => formatYoutubeHarvest(await executor.youtubeHarvest(input), input));
5987
6032
  server2.registerTool("youtube_transcribe", {
5988
6033
  title: "YouTube Transcription",
5989
- description: withReportNote("Fetch and transcribe captions from a YouTube video. Use this when the user asks what was said in a YouTube video, wants claims/offers/lessons extracted from a video, or provides a YouTube URL for transcript work. Returns full transcript, timestamped chunks, word count, and resolvedInputs. Pass videoId from youtube_harvest results, or pass url when the user pasted a YouTube URL. Use youtube_harvest first when you need to discover videos by topic/channel."),
6034
+ description: withReportNote("Fetch and transcribe captions from a YouTube video. Pass videoId from youtube_harvest, or a url the user pasted. Returns full transcript, timestamped chunks, and word count."),
5990
6035
  inputSchema: YoutubeTranscribeInputSchema,
5991
6036
  outputSchema: YoutubeTranscribeOutputSchema,
5992
6037
  annotations: liveWebToolAnnotations("YouTube Transcription")
5993
6038
  }, async (input) => formatYoutubeTranscribe(await executor.youtubeTranscribe(input), input));
5994
6039
  server2.registerTool("facebook_page_intel", {
5995
6040
  title: "Facebook Advertiser Ad Intel",
5996
- description: withReportNote("Harvest ads from a Facebook advertiser when the user wants current ad copy, creative angles, CTAs, video URLs, or competitive ad intelligence for one brand/page. Returns ad copy, headlines, CTAs, creative type, status, landing URLs, and direct ad video URLs ready for facebook_ad_transcribe. Accepts pageId, libraryId, or a brand/advertiser name as query. Use facebook_ad_search first when the advertiser handle is unknown. For normal public Facebook reels/posts/watch/share URLs, use facebook_video_transcribe instead."),
6041
+ description: withReportNote("Harvest ads from a Facebook advertiser: copy, creative angles, CTAs, and direct video URLs ready for facebook_ad_transcribe. Accepts pageId, libraryId, or a brand/advertiser name."),
5997
6042
  inputSchema: FacebookPageIntelInputSchema,
5998
6043
  outputSchema: FacebookPageIntelOutputSchema,
5999
6044
  annotations: liveWebToolAnnotations("Facebook Advertiser Ad Intel")
6000
6045
  }, async (input) => formatFacebookPageIntel(await executor.facebookPageIntel(input), input));
6001
6046
  server2.registerTool("facebook_ad_search", {
6002
6047
  title: "Facebook Ad Library Search",
6003
- description: withReportNote("Search Facebook Ad Library when the user wants to find advertisers by brand, competitor, niche, or keyword. Returns advertisers with ad counts and library IDs. Use this to discover competitor/page handles, then pass libraryId or pageId to facebook_page_intel for ad details."),
6048
+ description: withReportNote("Search Facebook Ad Library to find advertisers by brand, competitor, niche, or keyword. Returns advertisers with ad counts and library IDs."),
6004
6049
  inputSchema: FacebookAdSearchInputSchema,
6005
6050
  outputSchema: FacebookAdSearchOutputSchema,
6006
6051
  annotations: liveWebToolAnnotations("Facebook Ad Library Search")
6007
6052
  }, async (input) => formatFacebookAdSearch(await executor.facebookAdSearch(input), input));
6008
6053
  server2.registerTool("reddit_thread", {
6009
6054
  title: "Reddit Thread + Comments",
6010
- description: withReportNote("Capture a Reddit post and its comment tree from any reddit.com thread URL. Use this when the user wants Reddit comments, opinions, discussion, or audience voice from a specific thread (find threads first with search_serp, which surfaces reddit.com/comments links). Returns the post title, author, score, body, and a list of comments with author, score, nesting depth, and text. Handles Reddit's bot protection automatically (fetches via old.reddit through a residential proxy with retries); pass maxComments to cap the list."),
6055
+ description: withReportNote("Capture a Reddit post and its comment tree from a reddit.com thread URL \u2014 comments, opinions, audience voice. Handles Reddit's bot protection automatically; pass maxComments to cap the list."),
6011
6056
  inputSchema: RedditThreadInputSchema,
6012
6057
  outputSchema: RedditThreadOutputSchema,
6013
6058
  annotations: liveWebToolAnnotations("Reddit Thread + Comments")
6014
6059
  }, async (input) => formatRedditThread(await executor.redditThread(input), input));
6060
+ server2.registerTool("video_frame_analysis", {
6061
+ title: "Video Breakdown (frame-by-frame + transcript)",
6062
+ description: "Produce a deep frame-by-frame + transcript breakdown of a video \u2014 pacing, hook, visual style, and how to replicate it. Pass a DIRECT video file URL (.mp4/.webm/.mov), not a page URL. Runs asynchronously and costs a flat $1 (refunded on failure): returns a runId immediately; poll video_frame_analysis_status until done.",
6063
+ inputSchema: VideoFrameAnalysisInputSchema,
6064
+ outputSchema: VideoFrameAnalysisOutputSchema,
6065
+ annotations: { title: "Video Breakdown", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }
6066
+ }, async (input) => executor.videoFrameAnalysis(input));
6067
+ server2.registerTool("video_frame_analysis_status", {
6068
+ title: "Video Breakdown Status",
6069
+ description: 'Check progress of a video breakdown started with video_frame_analysis, using its runId. Free to call. When status is "done" it returns the full report and vault path; stop polling on "done" or "failed".',
6070
+ inputSchema: VideoFrameAnalysisStatusInputSchema,
6071
+ outputSchema: VideoFrameAnalysisStatusOutputSchema,
6072
+ annotations: { title: "Video Breakdown Status", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
6073
+ }, async (input) => executor.videoFrameAnalysisStatus(input));
6015
6074
  server2.registerTool("facebook_ad_transcribe", {
6016
6075
  title: "Facebook Ad Transcription",
6017
- description: "Transcribe audio from a Facebook ad video CDN URL. Use this when facebook_page_intel returned a direct videoUrl and the user asks what the ad says, what claims it makes, or wants ad-message extraction. Returns full transcript, timestamped chunks, word count, and resolvedInputs. Use only with the direct videoUrl value from facebook_page_intel results; do not pass public Facebook post/reel/share URLs. Use facebook_video_transcribe for organic Facebook URLs, and facebook_page_intel first when you only have a brand/page/ad library handle.",
6076
+ description: "Transcribe audio from a Facebook ad video CDN URL returned by facebook_page_intel. Use only that direct videoUrl value \u2014 do not pass public Facebook post/reel/share URLs (use facebook_video_transcribe for those).",
6018
6077
  inputSchema: FacebookAdTranscribeInputSchema,
6019
6078
  outputSchema: FacebookAdTranscribeOutputSchema,
6020
6079
  annotations: liveWebToolAnnotations("Facebook Ad Transcription")
6021
6080
  }, async (input) => formatFacebookAdTranscribe(await executor.facebookAdTranscribe(input), input));
6022
6081
  server2.registerTool("google_ads_search", {
6023
6082
  title: "Google Ads Transparency Search",
6024
- description: withReportNote("Search the Google Ads Transparency Center to find advertisers running Google ads (Search, Display, YouTube) by domain or brand/advertiser name. Pass a domain like getviktor.com to see every advertiser account pointing ads at that site, or an advertiser name. Returns advertisers with their advertiser ID and an approximate ad count. Use this to discover an advertiserId, then pass it (or the domain) to google_ads_page_intel for the actual ad creatives."),
6083
+ description: withReportNote("Search the Google Ads Transparency Center to find advertisers by domain or brand name. Returns advertisers with advertiser ID and approximate ad count, to pass to google_ads_page_intel."),
6025
6084
  inputSchema: GoogleAdsSearchInputSchema,
6026
6085
  outputSchema: GoogleAdsSearchOutputSchema,
6027
6086
  annotations: liveWebToolAnnotations("Google Ads Transparency Search")
6028
6087
  }, async (input) => formatGoogleAdsSearch(await executor.googleAdsSearch(input), input));
6029
6088
  server2.registerTool("google_ads_page_intel", {
6030
6089
  title: "Google Ads Advertiser Intel",
6031
- description: withReportNote("Harvest an advertiser's ads from the Google Ads Transparency Center when the user wants a competitor's Google/YouTube ad creatives, copy angles, image URLs, or video ads. Accepts an advertiserId (from google_ads_search) or a domain directly. Returns each creative's format (text/image/video), image URLs, landing info, a detail URL, and \u2014 for video ads \u2014 the YouTube video ID and/or a direct video URL ready for transcription. Use google_ads_search first when you only have a brand name. Transcribe video ads with google_ads_transcribe (video URL) or youtube_transcribe (YouTube ID)."),
6090
+ description: withReportNote("Harvest an advertiser's ad creatives from the Google Ads Transparency Center: format, image URLs, and \u2014 for video ads \u2014 a YouTube video ID or direct video URL. Accepts advertiserId (from google_ads_search) or a domain."),
6032
6091
  inputSchema: GoogleAdsPageIntelInputSchema,
6033
6092
  outputSchema: GoogleAdsPageIntelOutputSchema,
6034
6093
  annotations: liveWebToolAnnotations("Google Ads Advertiser Intel")
6035
6094
  }, async (input) => formatGoogleAdsPageIntel(await executor.googleAdsPageIntel(input), input));
6036
6095
  server2.registerTool("google_ads_transcribe", {
6037
6096
  title: "Google Ad Video Transcription",
6038
- description: "Transcribe audio from a Google video ad. Use this when google_ads_page_intel returned a direct videoUrl (a googlevideo.com playback URL) for a video creative and the user asks what the ad says or wants its claims/messaging. Returns full transcript, timestamped chunks, and word count. For YouTube-hosted ads, pass the returned youtubeVideoId to youtube_transcribe instead; run google_ads_page_intel first when you only have an advertiser/domain.",
6097
+ description: "Transcribe audio from a Google video ad's direct videoUrl (a googlevideo.com playback URL) returned by google_ads_page_intel. For YouTube-hosted ads, use youtube_transcribe with the returned youtubeVideoId instead.",
6039
6098
  inputSchema: GoogleAdsTranscribeInputSchema,
6040
6099
  outputSchema: GoogleAdsTranscribeOutputSchema,
6041
6100
  annotations: liveWebToolAnnotations("Google Ad Video Transcription")
6042
6101
  }, async (input) => formatGoogleAdsTranscribe(await executor.googleAdsTranscribe(input), input));
6043
6102
  server2.registerTool("facebook_video_transcribe", {
6044
6103
  title: "Facebook Organic Video Transcription",
6045
- description: withReportNote("Transcribe audio from an organic Facebook reel, video, watch, post, or share URL, including fb.watch links. Use this when the user pastes a normal Facebook video page URL and wants the transcript or downloadable MP4. Renders the Facebook page in a browser, selects the best matching public Facebook CDN MP4 URL from page state, then returns sourceUrl, resolved pageUrl, videoId, ownerName, selectedQuality, bitrate, videoDurationSec, extracted MP4 URL, full transcript, and timestamped chunks."),
6104
+ description: withReportNote("Transcribe audio from an organic Facebook reel/video/post/share URL (including fb.watch). Renders the page, selects the best public CDN MP4, and returns the transcript plus resolved video metadata."),
6046
6105
  inputSchema: FacebookVideoTranscribeInputSchema,
6047
6106
  outputSchema: FacebookVideoTranscribeOutputSchema,
6048
6107
  annotations: liveWebToolAnnotations("Facebook Organic Video Transcription")
6049
6108
  }, async (input) => formatFacebookVideoTranscribe(await executor.facebookVideoTranscribe(input), input));
6050
6109
  server2.registerTool("instagram_profile_content", {
6051
6110
  title: "Instagram Profile Content Discovery",
6052
- description: withReportNote("Discover Instagram profile grid content links for a handle or profile URL. Use this when the user wants a person or brand account content inventory before selecting posts/reels to download. Returns profile stats, collected post/reel/tv URLs, shortcodes, type counts, browser details, pagination attempts, stop reason, and limitations."),
6111
+ description: withReportNote("Discover Instagram profile grid content links (posts/reels/tv) for a handle or profile URL, for later selection with instagram_media_download. Returns profile stats and collected URLs."),
6053
6112
  inputSchema: InstagramProfileContentInputSchema,
6054
6113
  outputSchema: InstagramProfileContentOutputSchema,
6055
6114
  annotations: liveWebToolAnnotations("Instagram Profile Content Discovery")
6056
6115
  }, async (input) => formatInstagramProfileContent(await executor.instagramProfileContent(input), input));
6057
6116
  server2.registerTool("instagram_media_download", {
6058
6117
  title: "Instagram Post/Reel Media Download",
6059
- description: withReportNote("Extract and download media from one Instagram post, reel, or tv URL. Use after instagram_profile_content or when the user gives a specific Instagram URL and wants the image, caption/text, reel audio/video tracks, optional muxed MP4, or optional transcript. Reels commonly expose separate video-only and audio-only MP4 tracks; this tool selects the best video and audio tracks and attempts muxing when ffmpeg is available."),
6118
+ description: withReportNote("Extract and download media from one Instagram post, reel, or tv URL \u2014 image, caption, video/audio tracks, optional muxed MP4, or transcript. Selects the best video/audio track pair and muxes when ffmpeg is available."),
6060
6119
  inputSchema: InstagramMediaDownloadInputSchema,
6061
6120
  outputSchema: InstagramMediaDownloadOutputSchema,
6062
6121
  annotations: liveWebToolAnnotations("Instagram Post/Reel Media Download")
6063
6122
  }, async (input) => formatInstagramMediaDownload(await executor.instagramMediaDownload(input), input));
6064
6123
  server2.registerTool("maps_place_intel", {
6065
6124
  title: "Google Maps Business Profile Details",
6066
- description: withReportNote('Extract Google Maps business intelligence for one known/named business: rating, review count, category, address, phone, website, hours, booking URL, review histogram, review topics, about attributes, entity IDs, and optional review cards. Do not use this for category searches, local market prospect lists, or requests for multiple GMB/GBP profiles; use maps_search first for those. Split business name from location (e.g. "Elite Roofing Denver CO" => businessName "Elite Roofing", location "Denver, CO"). Pass includeReviews true when the user asks for reviews/customer pain.'),
6125
+ description: withReportNote("Extract Google Maps business intelligence for one known/named business: rating, reviews, category, address, phone, hours, entity IDs. Not for category searches or multi-business prospect lists \u2014 use maps_search for those. Split business name from location."),
6067
6126
  inputSchema: MapsPlaceIntelInputSchema,
6068
6127
  outputSchema: MapsPlaceIntelOutputSchema,
6069
6128
  annotations: liveWebToolAnnotations("Google Maps Business Profile Details")
6070
6129
  }, async (input) => formatMapsPlaceIntel(await executor.mapsPlaceIntel(input), input));
6071
6130
  server2.registerTool("maps_search", {
6072
6131
  title: "Google Maps Business Search",
6073
- description: withReportNote('Search Google Maps for multiple businesses/profiles by category, niche, keyword, or local market. Use this when the user asks for several Google Business Profiles, GMBs, GBPs, leads, prospects, competitors, or "more than the 3-pack." Routing is automatic and location-aware \u2014 leave proxyMode unset; pass proxyZip only to force a specific ZIP. Returns up to 50 candidates with names, place URLs, CIDs when available, ratings, review counts, profile metadata, and sanitized attempt telemetry. Default maxResults is 10; maximum is 50. Use maps_place_intel afterward only when a selected business needs full details and reviews.'),
6132
+ description: withReportNote("Search Google Maps for multiple businesses by category, niche, or local market \u2014 leads, prospects, competitors, or beyond the 3-pack. Leave proxyMode unset. Returns up to 50 candidates (default 10) with names, place URLs, CIDs, ratings."),
6074
6133
  inputSchema: MapsSearchInputSchema,
6075
6134
  outputSchema: MapsSearchOutputSchema,
6076
6135
  annotations: liveWebToolAnnotations("Google Maps Business Search")
6077
6136
  }, async (input) => formatMapsSearch(await executor.mapsSearch(input), input));
6078
6137
  server2.registerTool("directory_workflow", {
6079
6138
  title: "Directory Workflow: Markets + Maps",
6080
- description: withReportNote('Build directory/prospecting datasets by selecting US city markets from the free Census Population Estimates city/place dataset, optionally joining configured US ZIPS/Lead Magician ZIP groups, then running Google Maps business searches for each city in parallel. Use this when the user wants "all cities over 100k population in a state", "build a directory CSV", "find markets then get Maps data", or similar location-database + Maps workflows. Set minPopulation, state, query, maxResultsPerCity, and concurrency. Routing is automatic per city. Saved CSV rows include source_location, result_position, business_name, review_stars, review_count, category, address, phone, hours_status, website_url, directions_url, place_url, cid, cid_decimal, city population, and ZIP groups. Structured city results include sanitized attempt telemetry. Use maps_place_intel only when a selected profile needs deeper review topics, profile review count confirmation, or review cards. For local Lead Magician ZIP enrichment, set MCP_SCRAPER_USZIPS_CSV_PATH on the API server or pass usZipsCsvPath only in local/test mode.'),
6139
+ description: withReportNote('Build directory/prospecting datasets: selects US city markets from Census population data, optionally joins configured ZIP groups, then runs Google Maps business searches per city in parallel. Use for "all cities over 100k population in a state" or market+Maps workflows. Saves a CSV of results per city. For ZIP enrichment, set MCP_SCRAPER_USZIPS_CSV_PATH on the server, or pass usZipsCsvPath in local/test mode.'),
6081
6140
  inputSchema: DirectoryWorkflowInputSchema,
6082
6141
  outputSchema: DirectoryWorkflowOutputSchema,
6083
6142
  annotations: liveWebToolAnnotations("Directory Workflow: Markets + Maps")
6084
6143
  }, async (input) => formatDirectoryWorkflow(await executor.directoryWorkflow(input), input));
6085
6144
  server2.registerTool("workflow_list", {
6086
6145
  title: "Workflow Catalog",
6087
- description: "List MCP Scraper higher-level workflows and AI-facing recipes. Use this when the user asks what MCP Scraper can do beyond single tools, or asks for market analysis, ICP research, forum/review acquisition, full brand design briefings, CRO audits, competitive positioning, content gap briefs, or AI search visibility audits. Returns runnable workflow ids plus recipe guidance for the best tool chain.",
6146
+ description: "List MCP Scraper higher-level workflows and recipes \u2014 market analysis, ICP research, CRO audits, competitive positioning, content gap briefs, AI search visibility, and more. Returns runnable workflow ids plus tool-chain guidance.",
6088
6147
  inputSchema: WorkflowListInputSchema,
6089
6148
  outputSchema: WorkflowListOutputSchema,
6090
6149
  annotations: localPlanningToolAnnotations("Workflow Catalog")
6091
6150
  }, async (input) => formatWorkflowList(await executor.workflowList(input), input));
6092
6151
  server2.registerTool("workflow_suggest", {
6093
6152
  title: "Workflow Intent Router",
6094
- description: 'Route a high-level business or research goal to the right MCP Scraper workflow/tool chain. Use before running work when the user says things like "do market analysis", "research my ICP", "acquire forum and review language", "build a brand design brief", "run a CRO audit", "compare competitors", "make a content gap brief", or "audit AI search visibility". This tool does not spend credits; it tells the AI exactly which workflow/tool chain to run next.',
6153
+ description: "Route a high-level business/research goal (market analysis, ICP research, CRO audit, competitor comparison, content gap brief, AI search visibility, etc) to the right MCP Scraper workflow/tool chain. Free; tells you what to run next.",
6095
6154
  inputSchema: WorkflowSuggestInputSchema,
6096
6155
  outputSchema: WorkflowSuggestOutputSchema,
6097
6156
  annotations: localPlanningToolAnnotations("Workflow Intent Router")
6098
6157
  }, async (input) => formatWorkflowSuggest(input));
6099
6158
  server2.registerTool("workflow_run", {
6100
6159
  title: "Run Workflow",
6101
- description: withReportNote("Start a higher-level MCP Scraper workflow. Use after workflow_suggest or workflow_list. Runnable workflow ids: directory, agent-packet, local-competitive-audit, map-comparison, serp-comparison, paa-expansion-brief, ai-overview-language. This is the main MCP tool for market analysis, ICP evidence packets, local competitive audits, Maps/SERP comparisons, content gap briefs, and AI Overview language guidance. Stepwise workflows (e.g. agent-packet) run ONE leg per call and return runId, the step output, and nextStep \u2014 when nextStep is present, call workflow_step with the runId to run the next leg, and keep calling it until done is true. This keeps each call short instead of one long blocking run, so report each step result to the user as it arrives."),
6160
+ description: withReportNote("Start a higher-level MCP Scraper workflow (directory, agent-packet, local-competitive-audit, map-comparison, serp-comparison, paa-expansion-brief, ai-overview-language). Use after workflow_suggest or workflow_list. Stepwise workflows return runId + nextStep \u2014 call workflow_step with the runId until done is true."),
6102
6161
  inputSchema: WorkflowRunInputSchema,
6103
6162
  outputSchema: WorkflowRunOutputSchema,
6104
6163
  annotations: liveWebToolAnnotations("Run Workflow")
6105
6164
  }, async (input) => formatWorkflowRun(await executor.workflowRun(input), input));
6106
6165
  server2.registerTool("workflow_step", {
6107
6166
  title: "Advance Workflow Step",
6108
- description: withReportNote("Run the next leg of a stepwise MCP Scraper workflow started with workflow_run. Pass the runId. Each call executes exactly one logical step (typically one live harvest), persists that step's artifacts, and returns the step output plus nextStep. Keep calling workflow_step with the same runId until done is true, reporting each step result to the user as it lands. Use this instead of waiting on one long workflow call \u2014 it avoids client timeouts on long multi-step jobs."),
6167
+ description: withReportNote("Run the next leg of a stepwise workflow started with workflow_run. Pass the runId; each call executes one step and returns its output plus nextStep. Keep calling with the same runId until done is true."),
6109
6168
  inputSchema: WorkflowStepInputSchema,
6110
6169
  outputSchema: WorkflowStepOutputSchema,
6111
6170
  annotations: liveWebToolAnnotations("Advance Workflow Step")
6112
6171
  }, async (input) => formatWorkflowStep(await executor.workflowStep(input), input));
6113
6172
  server2.registerTool("workflow_status", {
6114
6173
  title: "Workflow Status",
6115
- description: "Fetch a hosted workflow run by id and list its current status and artifacts. Use when a workflow may still be running, when the model needs to re-open a run, inspect artifact ids, recover from a long workflow, or decide whether to call workflow_step or workflow_artifact_read next. Use only a runId returned by workflow_run/workflow_step/workflow_status; do not construct one yourself.",
6174
+ description: "Fetch a hosted workflow run by id and list its current status and artifacts, to re-open a run or recover artifact ids. Use only a runId returned by workflow_run/workflow_step/workflow_status.",
6116
6175
  inputSchema: WorkflowStatusInputSchema,
6117
6176
  outputSchema: WorkflowStatusOutputSchema,
6118
6177
  annotations: liveWebToolAnnotations("Workflow Status")
6119
6178
  }, async (input) => formatWorkflowStatus(await executor.workflowStatus(input), input));
6120
6179
  server2.registerTool("workflow_artifact_read", {
6121
6180
  title: "Read Workflow Artifact",
6122
- description: "Read a workflow artifact back into MCP context by run id and artifact id. Use this before writing final deliverables so the answer is grounded in generated evidence.json, CSVs, Markdown briefs, reports, and task files instead of memory. Use workflow_status first when artifact ids are unknown. Use only artifactId values returned by workflow_run/workflow_step/workflow_status; do not construct one yourself. Use maxBytes to limit large CSV/JSON artifacts.",
6181
+ description: "Read a workflow artifact back into context by run id and artifact id, so final deliverables are grounded in generated evidence rather than memory. Use workflow_status first when artifact ids are unknown. Use maxBytes to limit large artifacts.",
6123
6182
  inputSchema: WorkflowArtifactReadInputSchema,
6124
6183
  outputSchema: WorkflowArtifactReadOutputSchema,
6125
6184
  annotations: liveWebToolAnnotations("Read Workflow Artifact")
6126
6185
  }, async (input) => formatWorkflowArtifactRead(await executor.workflowArtifactRead(input), input));
6127
6186
  server2.registerTool("rank_tracker_workflow", {
6128
6187
  title: "Rank Tracker Blueprint Builder",
6129
- description: "Generate a build-ready database, cron/heartbeat, ingestion, metrics, and AI implementation prompt for a rank tracker powered by MCP Scraper. Supports Maps rankings through directory_workflow/maps_search, organic rankings through search_serp, AI Overview citation tracking, and People Also Ask source presence tracking. This tool is local planning only; it does not call the web or spend credits.",
6188
+ description: "Generate a build-ready database schema, cron plan, and implementation prompt for a rank tracker powered by MCP Scraper (Maps, organic, AI Overview, or PAA tracking). Local planning only \u2014 does not call the web or spend credits.",
6130
6189
  inputSchema: RankTrackerBlueprintInputSchema,
6131
6190
  outputSchema: RankTrackerBlueprintOutputSchema,
6132
6191
  annotations: localPlanningToolAnnotations("Rank Tracker Blueprint Builder")
6133
6192
  }, async (input) => buildRankTrackerBlueprint(input));
6134
6193
  server2.registerTool("credits_info", {
6135
6194
  title: "MCP Scraper Credits & Costs",
6136
- description: "Answer questions about MCP Scraper credits, usage limits, and concurrency upgrades: current credit balance, what a specific tool/action costs, the full cost table, current concurrency limit, the extra-slot price, billing URL, and the terminal checkout command. Does not expose payment methods or credit card information.",
6195
+ description: "Answer questions about MCP Scraper credits, usage limits, and concurrency upgrades \u2014 balance, tool costs, concurrency limits, billing URL. Does not expose payment methods or card information.",
6137
6196
  inputSchema: CreditsInfoInputSchema,
6138
6197
  outputSchema: CreditsInfoOutputSchema,
6139
6198
  annotations: {