mcp-scraper 0.2.19 → 0.2.20

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 (71) hide show
  1. package/README.md +3 -3
  2. package/dist/bin/api-server.cjs +1020 -348
  3. package/dist/bin/api-server.cjs.map +1 -1
  4. package/dist/bin/api-server.js +3 -3
  5. package/dist/bin/browser-agent-stdio-server.cjs +314 -55
  6. package/dist/bin/browser-agent-stdio-server.cjs.map +1 -1
  7. package/dist/bin/browser-agent-stdio-server.js +2 -2
  8. package/dist/bin/mcp-scraper-cli.cjs +321 -167
  9. package/dist/bin/mcp-scraper-cli.cjs.map +1 -1
  10. package/dist/bin/mcp-scraper-cli.js +3 -2
  11. package/dist/bin/mcp-scraper-cli.js.map +1 -1
  12. package/dist/bin/mcp-scraper-combined-stdio-server.cjs +1128 -530
  13. package/dist/bin/mcp-scraper-combined-stdio-server.cjs.map +1 -1
  14. package/dist/bin/mcp-scraper-combined-stdio-server.js +5 -4
  15. package/dist/bin/mcp-scraper-combined-stdio-server.js.map +1 -1
  16. package/dist/bin/mcp-scraper-install.cjs +3 -3
  17. package/dist/bin/mcp-scraper-install.cjs.map +1 -1
  18. package/dist/bin/mcp-scraper-install.js +2 -2
  19. package/dist/bin/mcp-stdio-server.cjs +726 -387
  20. package/dist/bin/mcp-stdio-server.cjs.map +1 -1
  21. package/dist/bin/mcp-stdio-server.js +3 -2
  22. package/dist/bin/mcp-stdio-server.js.map +1 -1
  23. package/dist/bin/paa-harvest.cjs +5 -3
  24. package/dist/bin/paa-harvest.cjs.map +1 -1
  25. package/dist/bin/paa-harvest.js +2 -1
  26. package/dist/bin/paa-harvest.js.map +1 -1
  27. package/dist/{chunk-TL7YTFLH.js → chunk-DBQDG7EH.js} +99 -27
  28. package/dist/chunk-DBQDG7EH.js.map +1 -0
  29. package/dist/{chunk-F44RBOJ5.js → chunk-H2R232HK.js} +216 -108
  30. package/dist/chunk-H2R232HK.js.map +1 -0
  31. package/dist/{chunk-76P4DKWR.js → chunk-L4OWOUGR.js} +315 -56
  32. package/dist/chunk-L4OWOUGR.js.map +1 -0
  33. package/dist/chunk-LFATOGDF.js +111 -0
  34. package/dist/chunk-LFATOGDF.js.map +1 -0
  35. package/dist/{chunk-MCBOZ2JF.js → chunk-MLNCKQ2B.js} +3 -3
  36. package/dist/chunk-MLNCKQ2B.js.map +1 -0
  37. package/dist/{chunk-4UEVYYDW.js → chunk-O5J7E4VX.js} +394 -57
  38. package/dist/chunk-O5J7E4VX.js.map +1 -0
  39. package/dist/{chunk-BSYPATSM.js → chunk-SXTXMFEQ.js} +2 -2
  40. package/dist/{chunk-CQTAKXBN.js → chunk-XGUDTDZ2.js} +7 -104
  41. package/dist/chunk-XGUDTDZ2.js.map +1 -0
  42. package/dist/chunk-XKUDVN2E.js +7 -0
  43. package/dist/chunk-XKUDVN2E.js.map +1 -0
  44. package/dist/{db-P5X6UQ3E.js → db-BE4JVB3V.js} +8 -2
  45. package/dist/index.cjs +5 -3
  46. package/dist/index.cjs.map +1 -1
  47. package/dist/index.d.cts +2 -2
  48. package/dist/index.d.ts +2 -2
  49. package/dist/index.js +2 -1
  50. package/dist/index.js.map +1 -1
  51. package/dist/{server-CRUSZA2Q.js → server-7NE45K2A.js} +211 -37
  52. package/dist/server-7NE45K2A.js.map +1 -0
  53. package/dist/{worker-OZSWIS3F.js → worker-MIYG2B2I.js} +5 -4
  54. package/dist/{worker-OZSWIS3F.js.map → worker-MIYG2B2I.js.map} +1 -1
  55. package/docs/adr/0001-in-page-graphql-interception-for-anti-bot-scraping.md +2 -2
  56. package/docs/mcp-tool-craft-lint.generated.md +51 -0
  57. package/docs/mcp-tool-manifest.generated.json +1406 -0
  58. package/docs/specs/local-competitive-audit-spec.md +1 -2
  59. package/docs/specs/mcp-tool-definition-quality-audit-spec.md +1602 -0
  60. package/package.json +4 -1
  61. package/dist/chunk-4UEVYYDW.js.map +0 -1
  62. package/dist/chunk-76P4DKWR.js.map +0 -1
  63. package/dist/chunk-CQTAKXBN.js.map +0 -1
  64. package/dist/chunk-F44RBOJ5.js.map +0 -1
  65. package/dist/chunk-MCBOZ2JF.js.map +0 -1
  66. package/dist/chunk-RU2HZD55.js +0 -7
  67. package/dist/chunk-RU2HZD55.js.map +0 -1
  68. package/dist/chunk-TL7YTFLH.js.map +0 -1
  69. package/dist/server-CRUSZA2Q.js.map +0 -1
  70. /package/dist/{chunk-BSYPATSM.js.map → chunk-SXTXMFEQ.js.map} +0 -0
  71. /package/dist/{db-P5X6UQ3E.js.map → db-BE4JVB3V.js.map} +0 -0
@@ -1,9 +1,13 @@
1
1
  import {
2
2
  sanitizeVendorName
3
3
  } from "./chunk-M2S27J6Z.js";
4
+ import {
5
+ DEFAULT_MAPS_PROXY_MODE,
6
+ DEFAULT_PROXY_MODE
7
+ } from "./chunk-LFATOGDF.js";
4
8
  import {
5
9
  PACKAGE_VERSION
6
- } from "./chunk-RU2HZD55.js";
10
+ } from "./chunk-XKUDVN2E.js";
7
11
 
8
12
  // src/harvest-timeout.ts
9
13
  var VERCEL_FUNCTION_MAX_MS = 3e5;
@@ -642,13 +646,25 @@ ${videoRows}`,
642
646
  }
643
647
  };
644
648
  }
649
+ function structuredTranscriptChunks(chunks) {
650
+ return chunks.map((c) => ({
651
+ startSec: Number.isFinite(c.timestamp?.[0]) ? c.timestamp[0] : 0,
652
+ endSec: Number.isFinite(c.timestamp?.[1]) ? c.timestamp[1] : 0,
653
+ text: c.text
654
+ }));
655
+ }
656
+ function wordCount(text) {
657
+ return text.trim() ? text.trim().split(/\s+/).length : 0;
658
+ }
645
659
  function formatYoutubeTranscribe(raw, input) {
646
660
  const parsed = parseData(raw);
647
661
  if ("error" in parsed) return { content: [{ type: "text", text: parsed.error }], isError: true };
648
662
  const d = parsed.data;
649
663
  const text = d.text ?? "";
650
664
  const chunks = d.chunks ?? [];
665
+ const videoId = d.videoId ?? input.videoId ?? null;
651
666
  const durSec = d.durationMs ? (d.durationMs / 1e3).toFixed(0) : "\u2014";
667
+ const words = wordCount(text);
652
668
  const chunkRows = chunks.slice(0, 50).map((c) => {
653
669
  const sec = Number.isFinite(c.timestamp[0]) ? Math.floor(c.timestamp[0]) : 0;
654
670
  const mm = String(Math.floor(sec / 60)).padStart(2, "0");
@@ -656,8 +672,8 @@ function formatYoutubeTranscribe(raw, input) {
656
672
  return `| ${mm}:${ss} | ${cell(truncate(c.text, 120))} |`;
657
673
  }).join("\n");
658
674
  const full = [
659
- `# YouTube Transcript: \`${input.videoId}\``,
660
- `**Duration:** ${durSec}s \xB7 **${text.split(" ").length} words**`,
675
+ `# YouTube Transcript: \`${videoId ?? input.url ?? "video"}\``,
676
+ `**Duration:** ${durSec}s \xB7 **${words} words**`,
661
677
  `
662
678
  ## Full Transcript
663
679
  ${text}`,
@@ -670,7 +686,22 @@ ${chunkRows}` : "",
670
686
  ---
671
687
  \u{1F4A1} Harvest more from this channel: use \`youtube_harvest\` with \`mode: "channel"\``
672
688
  ].filter(Boolean).join("\n");
673
- return oneBlock(full);
689
+ return {
690
+ ...oneBlock(full),
691
+ structuredContent: {
692
+ videoId,
693
+ url: videoId ? `https://www.youtube.com/watch?v=${videoId}` : input.url ?? null,
694
+ wordCount: words,
695
+ chunkCount: chunks.length,
696
+ durationMs: typeof d.durationMs === "number" ? d.durationMs : null,
697
+ transcriptText: text,
698
+ chunks: structuredTranscriptChunks(chunks),
699
+ resolvedInputs: {
700
+ videoId,
701
+ url: input.url ?? null
702
+ }
703
+ }
704
+ };
674
705
  }
675
706
  function formatFacebookPageIntel(raw, input) {
676
707
  const parsed = parseData(raw);
@@ -680,12 +711,13 @@ function formatFacebookPageIntel(raw, input) {
680
711
  const ads = d.ads ?? [];
681
712
  const s = d.summary ?? { totalAds: 0, activeCount: 0, videoCount: 0, imageCount: 0 };
682
713
  const adBlocks = ads.map((ad, i) => [
683
- `### Ad ${i + 1}${ad.libraryId ? ` \xB7 \`${ad.libraryId}\`` : ""} \u2014 ${ad.status ?? "\u2014"} \xB7 ${ad.creativeType ?? "\u2014"} \xB7 ${ad.startDate ?? "\u2014"}`,
714
+ `### Ad ${i + 1}${ad.libraryId ? ` \xB7 \`${ad.libraryId}\`` : ""} \u2014 ${ad.status ?? "\u2014"} \xB7 ${ad.creativeType ?? "\u2014"} \xB7 ${ad.startDate ?? ad.started ?? "\u2014"}`,
684
715
  ad.headline ? `**Headline:** ${ad.headline}` : "",
685
716
  ad.primaryText ? `**Copy:** ${truncate(ad.primaryText, 200)}` : "",
686
717
  ad.cta ? `**CTA:** ${ad.cta}` : "",
687
- ad.videoUrl ? `**Video URL:** \`${ad.videoUrl}\`` : "",
688
- ad.variations ? `**Variations:** ${ad.variations}` : ""
718
+ ad.landingUrl ? `**Landing URL:** ${ad.landingUrl}` : "",
719
+ ad.videoUrl ?? ad.videoSrc ? `**Video URL:** \`${ad.videoUrl ?? ad.videoSrc}\`` : "",
720
+ ad.variations ?? ad.clusterCount ? `**Variations:** ${ad.variations ?? ad.clusterCount}` : ""
689
721
  ].filter(Boolean).join("\n")).join("\n\n---\n\n");
690
722
  const full = [
691
723
  `# Facebook Ad Intel: ${advertiser}`,
@@ -711,11 +743,16 @@ ${adBlocks}`,
711
743
  libraryId: ad.libraryId ?? null,
712
744
  status: ad.status ?? null,
713
745
  creativeType: ad.creativeType ?? null,
746
+ primaryText: ad.primaryText ?? null,
714
747
  headline: ad.headline ?? null,
715
748
  cta: ad.cta ?? null,
716
- startDate: ad.startDate ?? null,
717
- videoUrl: ad.videoUrl ?? null,
718
- variations: typeof ad.variations === "number" ? ad.variations : null
749
+ startDate: ad.startDate ?? ad.started ?? null,
750
+ landingUrl: ad.landingUrl ?? null,
751
+ domain: ad.domain ?? null,
752
+ videoUrl: ad.videoUrl ?? ad.videoSrc ?? null,
753
+ imageUrl: ad.imageUrl ?? ad.imageSrc ?? null,
754
+ videoPoster: ad.videoPoster ?? null,
755
+ variations: typeof ad.variations === "number" ? ad.variations : typeof ad.clusterCount === "number" ? ad.clusterCount : null
719
756
  }))
720
757
  }
721
758
  };
@@ -749,8 +786,11 @@ ${rows}`,
749
786
  advertiserCount: advertisers.length,
750
787
  advertisers: advertisers.map((a) => ({
751
788
  name: a.pageName ?? a.name ?? null,
789
+ pageId: a.pageId ?? null,
790
+ pageUrl: a.pageUrl ?? null,
752
791
  adCount: typeof a.adCount === "number" ? a.adCount : null,
753
- libraryId: a.sampleLibraryId ?? a.libraryId ?? null
792
+ libraryId: a.sampleLibraryId ?? a.libraryId ?? null,
793
+ sampleLibraryId: a.sampleLibraryId ?? null
754
794
  }))
755
795
  }
756
796
  };
@@ -848,6 +888,34 @@ function formatWorkflowSuggest(input) {
848
888
  }
849
889
  };
850
890
  }
891
+ function workflowStepLines(data) {
892
+ const step = data.step;
893
+ const nextStep = data.nextStep;
894
+ const done = data.done === true;
895
+ const lines = [];
896
+ if (step) {
897
+ const index = Number(step.index ?? 0);
898
+ const totalSteps = step.totalSteps != null ? Number(step.totalSteps) : void 0;
899
+ const stepLabel = totalSteps ? `${index + 1}/${totalSteps}` : `${index + 1}`;
900
+ lines.push(`
901
+ ## Step ${stepLabel}: ${step.title ?? step.id ?? ""}`);
902
+ const output = step.output;
903
+ if (output && Object.keys(output).length) {
904
+ lines.push(Object.entries(output).map(([k, v]) => `- ${k}: ${typeof v === "object" ? JSON.stringify(v) : String(v)}`).join("\n"));
905
+ }
906
+ const warnings = Array.isArray(step.warnings) ? step.warnings : [];
907
+ if (warnings.length) lines.push(`
908
+ **Warnings:**
909
+ ${warnings.map((w) => `- ${w}`).join("\n")}`);
910
+ }
911
+ if (done) {
912
+ lines.push("\n**Done.** All steps complete.");
913
+ } else if (nextStep && typeof nextStep === "object") {
914
+ lines.push(`
915
+ **Next step:** \`${nextStep.id ?? nextStep.index}\` \u2014 call \`workflow_step\` with this run id to continue.`);
916
+ }
917
+ return lines;
918
+ }
851
919
  function formatWorkflowRun(raw, input) {
852
920
  const parsed = parseData(raw);
853
921
  if ("error" in parsed) return { content: [{ type: "text", text: parsed.error }], isError: true };
@@ -861,6 +929,7 @@ function formatWorkflowRun(raw, input) {
861
929
  `**Run ID:** \`${runId || "unknown"}\``,
862
930
  `**Status:** ${status}`,
863
931
  summary?.title ? `**Title:** ${summary.title}` : "",
932
+ ...workflowStepLines(parsed.data),
864
933
  summary?.summary ? `
865
934
  ## Summary
866
935
  ${summary.summary}` : "",
@@ -876,6 +945,42 @@ ${workflowArtifactRows(artifacts)}` : "",
876
945
  input: input.input ?? {},
877
946
  run,
878
947
  summary,
948
+ step: parsed.data.step,
949
+ nextStep: parsed.data.nextStep ?? null,
950
+ done: parsed.data.done === true,
951
+ artifacts
952
+ }
953
+ };
954
+ }
955
+ function formatWorkflowStep(raw, input) {
956
+ const parsed = parseData(raw);
957
+ if ("error" in parsed) return { content: [{ type: "text", text: parsed.error }], isError: true };
958
+ const run = parsed.data.run;
959
+ const summary = parsed.data.summary;
960
+ const artifacts = workflowArtifactsFrom(run);
961
+ const done = parsed.data.done === true;
962
+ const full = [
963
+ `# Workflow Step`,
964
+ `**Run ID:** \`${input.runId}\``,
965
+ `**Status:** ${run?.status ?? (done ? "done" : "running")}`,
966
+ ...workflowStepLines(parsed.data),
967
+ done && summary?.summary ? `
968
+ ## Summary
969
+ ${summary.summary}` : "",
970
+ artifacts.length ? `
971
+ ## Artifacts
972
+ ${workflowArtifactRows(artifacts)}` : "",
973
+ done && artifacts.length ? "\nUse `workflow_artifact_read` with the run id and artifact id to pull CSV, JSON, Markdown, or report content into context." : ""
974
+ ].filter(Boolean).join("\n");
975
+ return {
976
+ ...oneBlock(full),
977
+ structuredContent: {
978
+ runId: input.runId,
979
+ run,
980
+ summary: summary ?? null,
981
+ step: parsed.data.step,
982
+ nextStep: parsed.data.nextStep ?? null,
983
+ done,
879
984
  artifacts
880
985
  }
881
986
  };
@@ -1280,6 +1385,7 @@ function formatFacebookAdTranscribe(raw, input) {
1280
1385
  const text = d.text ?? "";
1281
1386
  const chunks = d.chunks ?? [];
1282
1387
  const durSec = d.durationMs ? (d.durationMs / 1e3).toFixed(0) : "\u2014";
1388
+ const words = wordCount(text);
1283
1389
  const chunkRows = chunks.slice(0, 50).map((c) => {
1284
1390
  const sec = Number.isFinite(c.timestamp[0]) ? Math.floor(c.timestamp[0]) : 0;
1285
1391
  const mm = String(Math.floor(sec / 60)).padStart(2, "0");
@@ -1288,7 +1394,7 @@ function formatFacebookAdTranscribe(raw, input) {
1288
1394
  }).join("\n");
1289
1395
  const full = [
1290
1396
  `# Facebook Ad Transcript`,
1291
- `**Duration:** ${durSec}s \xB7 **${text.split(" ").length} words**`,
1397
+ `**Duration:** ${durSec}s \xB7 **${words} words**`,
1292
1398
  `
1293
1399
  ## Full Transcript
1294
1400
  ${text}`,
@@ -1301,7 +1407,20 @@ ${chunkRows}` : "",
1301
1407
  ---
1302
1408
  \u{1F4A1} Get more ads from this advertiser: use \`facebook_page_intel\`. For public Facebook reel/post URLs, use \`facebook_video_transcribe\`.`
1303
1409
  ].filter(Boolean).join("\n");
1304
- return oneBlock(full);
1410
+ return {
1411
+ ...oneBlock(full),
1412
+ structuredContent: {
1413
+ videoUrl: input.videoUrl,
1414
+ wordCount: words,
1415
+ chunkCount: chunks.length,
1416
+ durationMs: typeof d.durationMs === "number" ? d.durationMs : null,
1417
+ transcriptText: text,
1418
+ chunks: structuredTranscriptChunks(chunks),
1419
+ resolvedInputs: {
1420
+ videoUrl: input.videoUrl
1421
+ }
1422
+ }
1423
+ };
1305
1424
  }
1306
1425
  function formatFacebookVideoTranscribe(raw, input) {
1307
1426
  const parsed = parseData(raw);
@@ -1309,7 +1428,7 @@ function formatFacebookVideoTranscribe(raw, input) {
1309
1428
  const d = parsed.data;
1310
1429
  const text = d.text ?? "";
1311
1430
  const chunks = d.chunks ?? [];
1312
- const wordCount = text.trim() ? text.trim().split(/\s+/).length : 0;
1431
+ const wordCount2 = text.trim() ? text.trim().split(/\s+/).length : 0;
1313
1432
  const durSec = d.durationMs ? (d.durationMs / 1e3).toFixed(0) : "\u2014";
1314
1433
  const videoDuration = typeof d.videoDurationSec === "number" ? `${Math.round(d.videoDurationSec)}s` : "\u2014";
1315
1434
  const label = d.videoId ? `Facebook Organic Video \`${d.videoId}\`` : "Facebook Organic Video";
@@ -1322,7 +1441,7 @@ function formatFacebookVideoTranscribe(raw, input) {
1322
1441
  const full = [
1323
1442
  `# ${label} Transcript`,
1324
1443
  d.ownerName ? `**Owner:** ${d.ownerName}` : "",
1325
- `**Video duration:** ${videoDuration} \xB7 **Transcribed in:** ${durSec}s \xB7 **${wordCount} words**`,
1444
+ `**Video duration:** ${videoDuration} \xB7 **Transcribed in:** ${durSec}s \xB7 **${wordCount2} words**`,
1326
1445
  d.pageUrl ? `**Page URL:** ${d.pageUrl}` : `**Page URL:** ${input.url}`,
1327
1446
  d.videoUrl ? `**Extracted MP4:** \`${d.videoUrl}\`` : "",
1328
1447
  `
@@ -1348,7 +1467,7 @@ ${chunkRows}` : "",
1348
1467
  bitrate: typeof d.bitrate === "number" ? d.bitrate : null,
1349
1468
  videoDurationSec: typeof d.videoDurationSec === "number" ? d.videoDurationSec : null,
1350
1469
  videoUrl: d.videoUrl ?? "",
1351
- wordCount,
1470
+ wordCount: wordCount2,
1352
1471
  chunkCount: chunks.length,
1353
1472
  transcriptText: text,
1354
1473
  chunks: chunks.map((c) => ({
@@ -1359,6 +1478,96 @@ ${chunkRows}` : "",
1359
1478
  }
1360
1479
  };
1361
1480
  }
1481
+ function formatCaptureSerpSnapshot(raw, input) {
1482
+ const parsed = parseData(raw);
1483
+ if ("error" in parsed) return { content: [{ type: "text", text: parsed.error }], isError: true };
1484
+ const d = parsed.data;
1485
+ const providerPayload = d;
1486
+ const organic = Array.isArray(d.organicResults) ? d.organicResults : Array.isArray(d.organic) ? d.organic : [];
1487
+ const localPack = Array.isArray(d.localPack) ? d.localPack : [];
1488
+ const artifacts = Array.isArray(d.artifacts) ? d.artifacts : [];
1489
+ const status = String(d.status ?? d.captureStatus ?? "captured");
1490
+ const query = typeof d.query === "string" ? d.query : typeof input.query === "string" ? input.query : null;
1491
+ const location = typeof d.location === "string" ? d.location : typeof input.location === "string" ? input.location : null;
1492
+ const capturedAt = typeof d.capturedAt === "string" ? d.capturedAt : typeof d.extractedAt === "string" ? d.extractedAt : null;
1493
+ const resultCount = organic.length + localPack.length;
1494
+ const full = [
1495
+ `# SERP Intelligence Snapshot: ${query ?? "query"}`,
1496
+ `**Status:** ${status}`,
1497
+ location ? `**Location:** ${location}` : "",
1498
+ `**Result count:** ${resultCount}`,
1499
+ artifacts.length ? `**Artifacts:** ${artifacts.length}` : "",
1500
+ "",
1501
+ "Use `capture_serp_page_snapshots` when you need page-level evidence for ranking URLs."
1502
+ ].filter(Boolean).join("\n");
1503
+ return {
1504
+ ...oneBlock(full),
1505
+ structuredContent: {
1506
+ schemaVersion: "serp-intelligence.capture.v1",
1507
+ status,
1508
+ query,
1509
+ location,
1510
+ capturedAt,
1511
+ resultCount,
1512
+ snapshotId: typeof d.snapshotId === "string" ? d.snapshotId : typeof d.snapshot_id === "string" ? d.snapshot_id : typeof d.id === "string" ? d.id : null,
1513
+ resolvedInputs: {
1514
+ query: input.query ?? null,
1515
+ location: input.location ?? null,
1516
+ gl: input.gl ?? null,
1517
+ hl: input.hl ?? null,
1518
+ device: input.device ?? null,
1519
+ proxyMode: input.proxyMode ?? null,
1520
+ proxyZip: input.proxyZip ?? null,
1521
+ pages: input.pages ?? null
1522
+ },
1523
+ artifacts,
1524
+ diagnostics: d.diagnostics && typeof d.diagnostics === "object" ? d.diagnostics : null,
1525
+ providerPayload
1526
+ }
1527
+ };
1528
+ }
1529
+ function formatCaptureSerpPageSnapshots(raw, input) {
1530
+ const parsed = parseData(raw);
1531
+ if ("error" in parsed) return { content: [{ type: "text", text: parsed.error }], isError: true };
1532
+ const d = parsed.data;
1533
+ const captures = Array.isArray(d.captures) ? d.captures : Array.isArray(d.results) ? d.results : [];
1534
+ const failedCount = captures.filter((c) => c.status === "failed" || c.error).length;
1535
+ const status = String(d.status ?? (failedCount ? "partial" : "captured"));
1536
+ const rows = captures.slice(0, 25).map((capture, i) => {
1537
+ const url = typeof capture.url === "string" ? capture.url : "";
1538
+ const sourceKind = typeof capture.sourceKind === "string" ? capture.sourceKind : typeof capture.source_kind === "string" ? capture.source_kind : "configured_target";
1539
+ return `| ${i + 1} | ${cell(url)} | ${cell(sourceKind)} | ${cell(String(capture.status ?? (capture.error ? "failed" : "ok")))} |`;
1540
+ }).join("\n");
1541
+ const full = [
1542
+ "# SERP Intelligence Page Snapshots",
1543
+ `**Status:** ${status}`,
1544
+ `**Captured:** ${captures.length}`,
1545
+ `**Failed:** ${failedCount}`,
1546
+ captures.length ? `
1547
+ | # | URL | Source | Status |
1548
+ |---|-----|--------|--------|
1549
+ ${rows}` : ""
1550
+ ].filter(Boolean).join("\n");
1551
+ return {
1552
+ ...oneBlock(full),
1553
+ structuredContent: {
1554
+ schemaVersion: "serp-intelligence.page-snapshots.v1",
1555
+ status,
1556
+ count: captures.length,
1557
+ failedCount,
1558
+ captures,
1559
+ resolvedInputs: {
1560
+ urls: input.urls ?? null,
1561
+ targets: input.targets ?? null,
1562
+ maxConcurrency: input.maxConcurrency ?? null,
1563
+ timeoutMs: input.timeoutMs ?? null,
1564
+ debug: input.debug ?? null
1565
+ },
1566
+ diagnostics: d.diagnostics && typeof d.diagnostics === "object" ? d.diagnostics : null,
1567
+ providerPayload: d
1568
+ }
1569
+ };
1570
+ }
1362
1571
 
1363
1572
  // src/mcp/mcp-tool-schemas.ts
1364
1573
  import { z } from "zod";
@@ -1369,8 +1578,8 @@ var HarvestPaaInputSchema = {
1369
1578
  gl: 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."),
1370
1579
  hl: 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."),
1371
1580
  device: z.enum(["desktop", "mobile"]).default("desktop").describe("SERP device context. Use desktop by default; use mobile only when the user asks for mobile rankings."),
1372
- proxyMode: z.enum(["location", "configured", "none"]).default("location").describe("Proxy targeting mode. Use location by default for US city/state SERPs; it creates a fresh residential proxy ID per attempt. If Google shows a CAPTCHA/challenge, browser-service sessions briefly wait for the automatic solver first, then retry with a fresh proxy/session if the challenge does not clear. Also retries proxy tunnel failure and wrong-location evidence before returning. Use configured only for the static configured proxy. Use none only for direct-network debugging."),
1373
- proxyZip: 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 when city-center targeting needs to be forced. With proxyMode location this ZIP is used for each fresh proxy attempt."),
1581
+ proxyMode: z.enum(["location", "configured", "none"]).default(DEFAULT_PROXY_MODE).describe("Proxy targeting mode. Default configured uses the service proxy without city/ZIP targeting for the highest general success rate. Use location only when the user explicitly needs city/ZIP-targeted residential proxy evidence. Use none only for direct-network debugging."),
1582
+ proxyZip: 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."),
1374
1583
  debug: z.boolean().default(false).describe("Include sanitized browser/session/location diagnostics in the response. Use true when debugging localization, CAPTCHA, or proxy behavior.")
1375
1584
  };
1376
1585
  var ExtractUrlInputSchema = {
@@ -1394,22 +1603,23 @@ var YoutubeHarvestInputSchema = {
1394
1603
  mode: z.enum(["search", "channel"]).describe("Use search for topic/keyword requests. Use channel when the user provides @handle, channel ID, or channel URL."),
1395
1604
  query: z.string().optional().describe("Required when mode is search. The YouTube search topic in the user\u2019s words."),
1396
1605
  channelHandle: z.string().optional().describe("YouTube channel handle, channel ID, or URL. Examples: @mkbhd, UC..., https://youtube.com/@mkbhd."),
1397
- maxVideos: z.number().int().min(1).max(500).default(50).describe("Number of videos to return. Default 50. Increase when user asks for full channel/history.")
1606
+ maxVideos: 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.")
1398
1607
  };
1399
1608
  var YoutubeTranscribeInputSchema = {
1400
- videoId: z.string().min(1).describe("YouTube video ID, e.g. dQw4w9WgXcQ")
1609
+ videoId: 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."),
1610
+ url: 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.")
1401
1611
  };
1402
1612
  var FacebookPageIntelInputSchema = {
1403
- pageId: z.string().optional(),
1404
- libraryId: z.string().optional(),
1613
+ pageId: 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."),
1614
+ libraryId: 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."),
1405
1615
  query: z.string().optional().describe("Advertiser or brand name when pageId/libraryId is not known. One of pageId, libraryId, or query is required."),
1406
- maxAds: z.number().int().min(1).max(200).default(50),
1407
- country: z.string().length(2).default("US")
1616
+ maxAds: 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."),
1617
+ country: 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.")
1408
1618
  };
1409
1619
  var FacebookAdSearchInputSchema = {
1410
1620
  query: z.string().min(1).describe("Advertiser, brand, competitor, niche, or keyword to search in Facebook Ad Library."),
1411
- country: z.string().length(2).default("US"),
1412
- maxResults: z.number().int().min(1).max(20).default(10)
1621
+ country: z.string().length(2).default("US").describe("Two-letter Ad Library country code. Default US. Examples: US, CA, GB, AU."),
1622
+ maxResults: 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.")
1413
1623
  };
1414
1624
  var FacebookAdTranscribeInputSchema = {
1415
1625
  videoUrl: 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.")
@@ -1423,8 +1633,8 @@ var MapsPlaceIntelInputSchema = {
1423
1633
  location: 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.'),
1424
1634
  gl: z.string().length(2).default("us").describe("Google country code inferred from location."),
1425
1635
  hl: z.string().length(2).default("en").describe("Language inferred from user request."),
1426
- includeReviews: z.boolean().default(false).describe("Whether to fetch individual review cards"),
1427
- maxReviews: z.number().int().min(1).max(500).default(50).describe("Max review cards to return (requires includeReviews: true)")
1636
+ includeReviews: 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."),
1637
+ maxReviews: 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.")
1428
1638
  };
1429
1639
  var MapsSearchInputSchema = {
1430
1640
  query: 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.'),
@@ -1432,7 +1642,7 @@ var MapsSearchInputSchema = {
1432
1642
  gl: z.string().length(2).default("us").describe("Google country code inferred from location."),
1433
1643
  hl: z.string().length(2).default("en").describe("Language inferred from user request."),
1434
1644
  maxResults: 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."),
1435
- proxyMode: z.enum(["location", "configured", "none"]).default("location").describe("Proxy targeting mode. Use location by default for US city/state Maps searches; retryable failures create a new residential proxy ID and new browser session for up to 5 attempts. Use configured for the server proxy ID, and none only for local direct-network debugging."),
1645
+ proxyMode: 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."),
1436
1646
  proxyZip: 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."),
1437
1647
  debug: z.boolean().default(false).describe("Include sanitized browser/proxy diagnostics when debugging Maps localization, CAPTCHA, or proxy behavior.")
1438
1648
  };
@@ -1447,8 +1657,8 @@ var DirectoryWorkflowInputSchema = {
1447
1657
  includeZipGroups: 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."),
1448
1658
  usZipsCsvPath: 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."),
1449
1659
  saveCsv: 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."),
1450
- proxyMode: z.enum(["location", "configured", "none"]).default("location").describe("Proxy targeting mode for every city Maps search. Use location by default for US city/state batches; retryable failures create a new residential proxy ID and new browser session for up to 5 attempts per city. Use configured for the server proxy ID, and none only for local direct-network debugging."),
1451
- proxyZip: z.string().regex(/^\d{5}$/).optional().describe("Optional ZIP override for proxy targeting. Normally omit it so each city can use its Lead Magician ZIP group or city/state location."),
1660
+ proxyMode: 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."),
1661
+ proxyZip: 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."),
1452
1662
  debug: z.boolean().default(false).describe("Include sanitized browser/proxy diagnostics in each Maps browser session when supported.")
1453
1663
  };
1454
1664
  var RankTrackerModeSchema = z.enum(["maps", "organic", "ai_overview", "paa"]);
@@ -1762,8 +1972,11 @@ var FacebookAdSearchOutputSchema = {
1762
1972
  advertiserCount: z.number().int().min(0),
1763
1973
  advertisers: z.array(z.object({
1764
1974
  name: NullableString,
1975
+ pageId: NullableString,
1976
+ pageUrl: NullableString,
1765
1977
  adCount: z.number().int().nullable(),
1766
- libraryId: NullableString
1978
+ libraryId: NullableString,
1979
+ sampleLibraryId: NullableString
1767
1980
  }))
1768
1981
  };
1769
1982
  var FacebookPageIntelOutputSchema = {
@@ -1776,10 +1989,15 @@ var FacebookPageIntelOutputSchema = {
1776
1989
  libraryId: NullableString,
1777
1990
  status: NullableString,
1778
1991
  creativeType: NullableString,
1992
+ primaryText: NullableString,
1779
1993
  headline: NullableString,
1780
1994
  cta: NullableString,
1781
1995
  startDate: NullableString,
1996
+ landingUrl: NullableString,
1997
+ domain: NullableString,
1782
1998
  videoUrl: NullableString,
1999
+ imageUrl: NullableString,
2000
+ videoPoster: NullableString,
1783
2001
  variations: z.number().int().nullable()
1784
2002
  }))
1785
2003
  };
@@ -1801,6 +2019,58 @@ var FacebookVideoTranscribeOutputSchema = {
1801
2019
  text: z.string()
1802
2020
  }))
1803
2021
  };
2022
+ var TranscriptChunkOutput = z.object({
2023
+ startSec: z.number(),
2024
+ endSec: z.number(),
2025
+ text: z.string()
2026
+ });
2027
+ var YoutubeTranscribeOutputSchema = {
2028
+ videoId: NullableString,
2029
+ url: NullableString,
2030
+ wordCount: z.number().int().min(0),
2031
+ chunkCount: z.number().int().min(0),
2032
+ durationMs: z.number().nullable(),
2033
+ transcriptText: z.string(),
2034
+ chunks: z.array(TranscriptChunkOutput),
2035
+ resolvedInputs: z.object({
2036
+ videoId: NullableString,
2037
+ url: NullableString
2038
+ })
2039
+ };
2040
+ var FacebookAdTranscribeOutputSchema = {
2041
+ videoUrl: z.string().url(),
2042
+ wordCount: z.number().int().min(0),
2043
+ chunkCount: z.number().int().min(0),
2044
+ durationMs: z.number().nullable(),
2045
+ transcriptText: z.string(),
2046
+ chunks: z.array(TranscriptChunkOutput),
2047
+ resolvedInputs: z.object({
2048
+ videoUrl: z.string().url()
2049
+ })
2050
+ };
2051
+ var CaptureSerpSnapshotOutputSchema = {
2052
+ schemaVersion: z.literal("serp-intelligence.capture.v1"),
2053
+ status: z.string(),
2054
+ query: NullableString,
2055
+ location: NullableString,
2056
+ capturedAt: NullableString,
2057
+ resultCount: z.number().int().min(0).nullable(),
2058
+ snapshotId: NullableString,
2059
+ resolvedInputs: z.record(z.unknown()),
2060
+ artifacts: z.array(z.record(z.unknown())),
2061
+ diagnostics: z.record(z.unknown()).nullable(),
2062
+ providerPayload: z.record(z.unknown())
2063
+ };
2064
+ var CaptureSerpPageSnapshotsOutputSchema = {
2065
+ schemaVersion: z.literal("serp-intelligence.page-snapshots.v1"),
2066
+ status: z.string(),
2067
+ count: z.number().int().min(0),
2068
+ failedCount: z.number().int().min(0),
2069
+ captures: z.array(z.record(z.unknown())),
2070
+ resolvedInputs: z.record(z.unknown()),
2071
+ diagnostics: z.record(z.unknown()).nullable(),
2072
+ providerPayload: z.record(z.unknown())
2073
+ };
1804
2074
  var CreditsInfoInputSchema = {
1805
2075
  item: z.string().optional().describe('Optional tool, action, or feature to look up, e.g. "maps reviews", "extract_url", "YouTube transcription", or "concurrency"'),
1806
2076
  includeLedger: z.boolean().default(false).describe("Whether to include recent credit ledger entries")
@@ -1828,16 +2098,19 @@ var WorkflowSuggestInputSchema = {
1828
2098
  maxSuggestions: z.number().int().min(1).max(8).default(3).describe("Number of matching workflow recipes to return.")
1829
2099
  };
1830
2100
  var WorkflowRunInputSchema = {
1831
- workflowId: WorkflowIdSchema.describe("Workflow to run. Use workflow_list or workflow_suggest first when unsure."),
2101
+ workflowId: WorkflowIdSchema.describe("Workflow to run: directory, 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."),
1832
2102
  input: z.record(z.unknown()).default({}).describe("Workflow-specific input object. Examples: 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?}."),
1833
2103
  webhookUrl: z.string().url().optional().describe("Optional HTTPS webhook to receive the completed hosted workflow run event.")
1834
2104
  };
2105
+ var WorkflowStepInputSchema = {
2106
+ runId: 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).")
2107
+ };
1835
2108
  var WorkflowStatusInputSchema = {
1836
- runId: z.string().min(1).describe("Workflow run id returned by workflow_run.")
2109
+ runId: 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.")
1837
2110
  };
1838
2111
  var WorkflowArtifactReadInputSchema = {
1839
- runId: z.string().min(1).describe("Workflow run id returned by workflow_run or workflow_status."),
1840
- artifactId: z.string().min(1).describe("Artifact id from the run artifact list."),
2112
+ runId: 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."),
2113
+ artifactId: 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."),
1841
2114
  maxBytes: 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.")
1842
2115
  };
1843
2116
  var WorkflowRecipeOutput = z.object({
@@ -1870,6 +2143,18 @@ var WorkflowRunOutputSchema = {
1870
2143
  input: z.record(z.unknown()),
1871
2144
  run: z.record(z.unknown()).optional(),
1872
2145
  summary: z.record(z.unknown()).optional(),
2146
+ step: z.record(z.unknown()).optional(),
2147
+ nextStep: z.record(z.unknown()).nullable().optional(),
2148
+ done: z.boolean().optional(),
2149
+ artifacts: z.array(WorkflowArtifactOutput)
2150
+ };
2151
+ var WorkflowStepOutputSchema = {
2152
+ runId: z.string(),
2153
+ run: z.record(z.unknown()).optional(),
2154
+ summary: z.record(z.unknown()).nullable().optional(),
2155
+ step: z.record(z.unknown()).optional(),
2156
+ nextStep: z.record(z.unknown()).nullable().optional(),
2157
+ done: z.boolean(),
1873
2158
  artifacts: z.array(WorkflowArtifactOutput)
1874
2159
  };
1875
2160
  var WorkflowStatusOutputSchema = {
@@ -1890,8 +2175,8 @@ var SearchSerpInputSchema = {
1890
2175
  gl: z.string().length(2).default("us").describe("Google country code inferred from location or user language."),
1891
2176
  hl: z.string().default("en").describe("Google interface/content language inferred from user request."),
1892
2177
  device: z.enum(["desktop", "mobile"]).default("desktop").describe("SERP device context. Use desktop by default; use mobile only when the user asks for mobile rankings."),
1893
- proxyMode: z.enum(["location", "configured", "none"]).default("location").describe("Proxy targeting mode. Use location by default for US city/state SERPs; it creates a fresh residential proxy ID per attempt. If Google shows a CAPTCHA/challenge, browser-service sessions briefly wait for the automatic solver first, then retry with a fresh proxy/session if the challenge does not clear. Also retries proxy tunnel failure and wrong-location evidence before returning. Use configured only for the static configured proxy. Use none only for direct-network debugging."),
1894
- proxyZip: 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 when city-center targeting needs to be forced. With proxyMode location this ZIP is used for each fresh proxy attempt."),
2178
+ proxyMode: z.enum(["location", "configured", "none"]).default(DEFAULT_PROXY_MODE).describe("Proxy targeting mode. Default configured uses the service proxy without city/ZIP targeting for the highest general SERP success rate. Use location only when the user explicitly needs city/ZIP-targeted residential proxy evidence. Use none only for direct-network debugging."),
2179
+ proxyZip: 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."),
1895
2180
  debug: z.boolean().default(false).describe("Include sanitized browser/session/location diagnostics in the response. Use true when debugging localization, CAPTCHA, or proxy behavior."),
1896
2181
  pages: z.number().int().min(1).max(2).default(1).describe("Number of result pages to fetch (1\u20132)")
1897
2182
  };
@@ -1901,8 +2186,8 @@ var CaptureSerpSnapshotInputSchema = {
1901
2186
  gl: z.string().length(2).default("us").describe("Google country code inferred from the requested market, e.g. us, gb, ca, au."),
1902
2187
  hl: z.string().default("en").describe("Google interface/content language inferred from the user request."),
1903
2188
  device: z.enum(["desktop", "mobile"]).default("desktop").describe("SERP device context. Use mobile only when the user asks for mobile rankings or mobile SERP evidence."),
1904
- proxyMode: z.enum(["location", "configured", "none"]).default("location").describe("Proxy behavior for capture. Use location for localized US residential evidence; it creates a fresh proxy ID per attempt. If Google shows a CAPTCHA/challenge, browser-service sessions briefly wait for the automatic solver first, then retry with a fresh proxy/session if the challenge does not clear. Also retries proxy tunnel failure and wrong-location evidence before returning. Use configured only for the static residential proxy, and none only for direct-network debugging."),
1905
- proxyZip: z.string().regex(/^\d{5}$/).optional().describe("Optional US ZIP override for residential location proxy targeting when a precise city-center or ZIP proxy is needed. With proxyMode location this ZIP is used for each fresh proxy attempt."),
2189
+ proxyMode: z.enum(["location", "configured", "none"]).default(DEFAULT_PROXY_MODE).describe("Proxy behavior for capture. Default configured uses the service proxy without city/ZIP targeting for the highest general capture success rate. Use location only when the user explicitly needs city/ZIP-targeted residential proxy evidence. Use none only for direct-network debugging."),
2190
+ proxyZip: 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."),
1906
2191
  pages: 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."),
1907
2192
  debug: z.boolean().default(false).describe("Include sanitized browser, proxy, and location diagnostics. Use true when debugging localization, CAPTCHA, proxy selection, or capture reliability."),
1908
2193
  includePageSnapshots: z.boolean().default(false).describe("Also capture ranking-page snapshots for selected SERP URLs through the same product capture path."),
@@ -2313,70 +2598,72 @@ function registerPaaExtractorMcpTools(server, executor, options = {}) {
2313
2598
  if (savesReports) registerSavedReportResources(server);
2314
2599
  server.registerTool("harvest_paa", {
2315
2600
  title: "Google PAA + SERP Harvest",
2316
- 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"). For US local SERPs, leave proxyMode as location so the service uses fresh residential proxy IDs across retries, briefly waits for browser-service automatic CAPTCHA/challenge solving when Google blocks a page, and rotates to a new proxy/session if the challenge does not clear or the market evidence is wrong. 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.'),
2601
+ 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"). Omit proxyMode for normal use; the service defaults to the configured browser-service proxy without city/ZIP targeting for the highest general success rate. Use proxyMode location only when the user explicitly needs city/ZIP-targeted residential proxy evidence. 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.'),
2317
2602
  inputSchema: HarvestPaaInputSchema,
2318
2603
  outputSchema: HarvestPaaOutputSchema,
2319
2604
  annotations: liveWebToolAnnotations("Google PAA + SERP Harvest")
2320
2605
  }, async (input) => formatHarvestPaa(await executor.harvestPaa(input), input));
2321
2606
  server.registerTool("search_serp", {
2322
2607
  title: "Google SERP Lookup",
2323
- 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. For US city/state rankings, keep proxyMode as location and pass proxyZip when a city-center ZIP is known; location mode uses fresh residential proxy IDs, briefly waits for browser-service automatic CAPTCHA/challenge solving when Google blocks a page, and rotates to a new proxy/session if the challenge does not clear, the proxy tunnel fails, or location evidence is wrong."),
2608
+ 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. Omit proxyMode for normal use; the service defaults to the configured browser-service proxy without city/ZIP targeting. Use proxyMode location and proxyZip only when the user explicitly needs city/ZIP-targeted residential proxy evidence."),
2324
2609
  inputSchema: SearchSerpInputSchema,
2325
2610
  outputSchema: SearchSerpOutputSchema,
2326
2611
  annotations: liveWebToolAnnotations("Google SERP Lookup")
2327
2612
  }, async (input) => formatSearchSerp(await executor.searchSerp(input), input));
2328
2613
  server.registerTool("extract_url", {
2329
2614
  title: "Single URL Extract",
2330
- description: withReportNote("Extract structured data from one public URL: page content as Markdown, heading structure, JSON-LD schema, entity details, NAP score, metadata, and missing schema fields. Use when the user provides a single URL or asks to inspect/scrape one page."),
2615
+ 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. Use map_site_urls before extracting a site inventory; use extract_site for multi-page crawling."),
2331
2616
  inputSchema: ExtractUrlInputSchema,
2332
2617
  outputSchema: ExtractUrlOutputSchema,
2333
2618
  annotations: liveWebToolAnnotations("Single URL Extract")
2334
2619
  }, async (input) => formatExtractUrl(await executor.extractUrl(input), input));
2335
2620
  server.registerTool("map_site_urls", {
2336
2621
  title: "Site URL Map",
2337
- description: withReportNote("Map/crawl a public website to build a URL inventory with HTTP status codes, broken links, redirects, and site scope. Use before extract_site for audits or when the user asks for a sitemap/URL inventory."),
2622
+ 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. Use this before extract_site when choosing pages for an audit; use extract_url for one known page."),
2338
2623
  inputSchema: MapSiteUrlsInputSchema,
2339
2624
  outputSchema: MapSiteUrlsOutputSchema,
2340
2625
  annotations: liveWebToolAnnotations("Site URL Map")
2341
2626
  }, async (input) => formatMapSiteUrls(await executor.mapSiteUrls(input), input));
2342
2627
  server.registerTool("extract_site", {
2343
2628
  title: "Multi-Page Site Extract",
2344
- description: withReportNote("Run multi-page extraction across a public website. Returns per-page titles, H1s, metadata, headings, schema/entity data, canonical URLs, and content. Use for website audits, competitor audits, and full-site extraction."),
2629
+ description: withReportNote("Run multi-page extraction across a public website when the user asks for a website audit, competitor audit, full-site content crawl, schema inventory, or page metadata review. Returns per-page titles, H1s, metadata, headings, schema/entity data, canonical URLs, and content. Use map_site_urls first when URL selection matters; use extract_url for one page."),
2345
2630
  inputSchema: ExtractSiteInputSchema,
2346
2631
  outputSchema: ExtractSiteOutputSchema,
2347
2632
  annotations: liveWebToolAnnotations("Multi-Page Site Extract")
2348
2633
  }, async (input) => formatExtractSite(await executor.extractSite(input), input));
2349
2634
  server.registerTool("youtube_harvest", {
2350
2635
  title: "YouTube Video Harvest",
2351
- description: withReportNote('Harvest YouTube video metadata by search query or channel handle/ID/URL. Use mode "search" for keyword/topic requests and mode "channel" for @handles, channel IDs, or channel URLs. Returns titles, views, dates, durations, URLs, thumbnails, and videoIds for follow-up transcription.'),
2636
+ 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.'),
2352
2637
  inputSchema: YoutubeHarvestInputSchema,
2353
2638
  outputSchema: YoutubeHarvestOutputSchema,
2354
2639
  annotations: liveWebToolAnnotations("YouTube Video Harvest")
2355
2640
  }, async (input) => formatYoutubeHarvest(await executor.youtubeHarvest(input), input));
2356
2641
  server.registerTool("youtube_transcribe", {
2357
2642
  title: "YouTube Transcription",
2358
- description: withReportNote("Fetch and transcribe captions from a YouTube video. Returns full transcript, timestamped chunks, and word count. Pass a videoId from youtube_harvest results or infer it from a YouTube URL if the user provided one."),
2643
+ 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."),
2359
2644
  inputSchema: YoutubeTranscribeInputSchema,
2645
+ outputSchema: YoutubeTranscribeOutputSchema,
2360
2646
  annotations: liveWebToolAnnotations("YouTube Transcription")
2361
2647
  }, async (input) => formatYoutubeTranscribe(await executor.youtubeTranscribe(input), input));
2362
2648
  server.registerTool("facebook_page_intel", {
2363
2649
  title: "Facebook Advertiser Ad Intel",
2364
- description: withReportNote("Harvest ads from a Facebook advertiser. 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 after facebook_ad_search when possible. For normal public Facebook reels/posts/watch/share URLs, use facebook_video_transcribe instead."),
2650
+ 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."),
2365
2651
  inputSchema: FacebookPageIntelInputSchema,
2366
2652
  outputSchema: FacebookPageIntelOutputSchema,
2367
2653
  annotations: liveWebToolAnnotations("Facebook Advertiser Ad Intel")
2368
2654
  }, async (input) => formatFacebookPageIntel(await executor.facebookPageIntel(input), input));
2369
2655
  server.registerTool("facebook_ad_search", {
2370
2656
  title: "Facebook Ad Library Search",
2371
- description: withReportNote("Search Facebook Ad Library by brand, advertiser, competitor, niche, or keyword. Returns advertisers with ad counts and library IDs. Use to discover competitors, then pass libraryId to facebook_page_intel."),
2657
+ 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."),
2372
2658
  inputSchema: FacebookAdSearchInputSchema,
2373
2659
  outputSchema: FacebookAdSearchOutputSchema,
2374
2660
  annotations: liveWebToolAnnotations("Facebook Ad Library Search")
2375
2661
  }, async (input) => formatFacebookAdSearch(await executor.facebookAdSearch(input), input));
2376
2662
  server.registerTool("facebook_ad_transcribe", {
2377
2663
  title: "Facebook Ad Transcription",
2378
- description: "Transcribe audio from a Facebook ad video CDN URL. Returns full transcript and timestamped chunks. Use only with the direct videoUrl value from facebook_page_intel results, not public Facebook post/reel/share URLs.",
2664
+ 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.",
2379
2665
  inputSchema: FacebookAdTranscribeInputSchema,
2666
+ outputSchema: FacebookAdTranscribeOutputSchema,
2380
2667
  annotations: liveWebToolAnnotations("Facebook Ad Transcription")
2381
2668
  }, async (input) => formatFacebookAdTranscribe(await executor.facebookAdTranscribe(input), input));
2382
2669
  server.registerTool("facebook_video_transcribe", {
@@ -2395,14 +2682,14 @@ function registerPaaExtractorMcpTools(server, executor, options = {}) {
2395
2682
  }, async (input) => formatMapsPlaceIntel(await executor.mapsPlaceIntel(input), input));
2396
2683
  server.registerTool("maps_search", {
2397
2684
  title: "Google Maps Business Search",
2398
- 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." For US city/state Maps searches, keep proxyMode as location so the browser service creates a fresh residential proxy ID and browser session for the market; retryable failures rotate to a new proxy and new browser session for up to 5 attempts. Pass proxyZip only when a specific ZIP or city-center ZIP is known. 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.'),
2685
+ 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." Maps is the default location-targeted surface: omit proxyMode for normal Maps use so the service creates residential proxy evidence for the requested market and rotates on retryable failures. Pass proxyZip only when a specific ZIP or city-center ZIP is known. Use proxyMode configured only when you explicitly do not want city/ZIP proxy targeting. 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.'),
2399
2686
  inputSchema: MapsSearchInputSchema,
2400
2687
  outputSchema: MapsSearchOutputSchema,
2401
2688
  annotations: liveWebToolAnnotations("Google Maps Business Search")
2402
2689
  }, async (input) => formatMapsSearch(await executor.mapsSearch(input), input));
2403
2690
  server.registerTool("directory_workflow", {
2404
2691
  title: "Directory Workflow: Markets + Maps",
2405
- 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. Use concurrency up to 5 for parallel city sessions. Keep proxyMode as location so each city search creates fresh residential proxy IDs and browser sessions; retryable city failures rotate to a new proxy and new browser session for up to 5 attempts. 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.'),
2692
+ 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. Maps workflows default to location-targeted proxying so each city can use city/state or ZIP-group residential proxy evidence; use proxyMode configured only when you explicitly do not want city/ZIP proxy targeting. 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.'),
2406
2693
  inputSchema: DirectoryWorkflowInputSchema,
2407
2694
  outputSchema: DirectoryWorkflowOutputSchema,
2408
2695
  annotations: liveWebToolAnnotations("Directory Workflow: Markets + Maps")
@@ -2423,21 +2710,28 @@ function registerPaaExtractorMcpTools(server, executor, options = {}) {
2423
2710
  }, async (input) => formatWorkflowSuggest(input));
2424
2711
  server.registerTool("workflow_run", {
2425
2712
  title: "Run Workflow",
2426
- description: withReportNote("Run a higher-level MCP Scraper workflow and return the run id, summary, and artifacts. 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 executing market analysis, ICP evidence packets, local competitive audits, Maps/SERP comparisons, content gap briefs, and AI Overview language guidance."),
2713
+ 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."),
2427
2714
  inputSchema: WorkflowRunInputSchema,
2428
2715
  outputSchema: WorkflowRunOutputSchema,
2429
2716
  annotations: liveWebToolAnnotations("Run Workflow")
2430
2717
  }, async (input) => formatWorkflowRun(await executor.workflowRun(input), input));
2718
+ server.registerTool("workflow_step", {
2719
+ title: "Advance Workflow Step",
2720
+ 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."),
2721
+ inputSchema: WorkflowStepInputSchema,
2722
+ outputSchema: WorkflowStepOutputSchema,
2723
+ annotations: liveWebToolAnnotations("Advance Workflow Step")
2724
+ }, async (input) => formatWorkflowStep(await executor.workflowStep(input), input));
2431
2725
  server.registerTool("workflow_status", {
2432
2726
  title: "Workflow Status",
2433
- description: "Fetch a hosted workflow run by id and list its current status and artifacts. Use after workflow_run when the model needs to re-open a run, inspect artifact ids, or recover from a long workflow.",
2727
+ 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.",
2434
2728
  inputSchema: WorkflowStatusInputSchema,
2435
2729
  outputSchema: WorkflowStatusOutputSchema,
2436
2730
  annotations: liveWebToolAnnotations("Workflow Status")
2437
2731
  }, async (input) => formatWorkflowStatus(await executor.workflowStatus(input), input));
2438
2732
  server.registerTool("workflow_artifact_read", {
2439
2733
  title: "Read Workflow Artifact",
2440
- 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 maxBytes to limit large CSV/JSON artifacts.",
2734
+ 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.",
2441
2735
  inputSchema: WorkflowArtifactReadInputSchema,
2442
2736
  outputSchema: WorkflowArtifactReadOutputSchema,
2443
2737
  annotations: liveWebToolAnnotations("Read Workflow Artifact")
@@ -2465,6 +2759,27 @@ function registerPaaExtractorMcpTools(server, executor, options = {}) {
2465
2759
  }
2466
2760
 
2467
2761
  // src/mcp/http-mcp-tool-executor.ts
2762
+ function youtubeVideoIdFromUrl(url) {
2763
+ if (!url) return null;
2764
+ try {
2765
+ const parsed = new URL(url);
2766
+ const host = parsed.hostname.replace(/^www\./, "").replace(/^m\./, "");
2767
+ if (host === "youtu.be") {
2768
+ const id = parsed.pathname.split("/").filter(Boolean)[0];
2769
+ return id || null;
2770
+ }
2771
+ if (host === "youtube.com" || host === "music.youtube.com") {
2772
+ const watchId = parsed.searchParams.get("v");
2773
+ if (watchId) return watchId;
2774
+ const parts = parsed.pathname.split("/").filter(Boolean);
2775
+ const markerIndex = parts.findIndex((part) => ["shorts", "embed", "live"].includes(part));
2776
+ if (markerIndex >= 0 && parts[markerIndex + 1]) return parts[markerIndex + 1];
2777
+ }
2778
+ } catch {
2779
+ return null;
2780
+ }
2781
+ return null;
2782
+ }
2468
2783
  var HttpMcpToolExecutor = class {
2469
2784
  baseUrl;
2470
2785
  apiKey;
@@ -2590,7 +2905,21 @@ var HttpMcpToolExecutor = class {
2590
2905
  return this.call("/youtube/harvest", input);
2591
2906
  }
2592
2907
  youtubeTranscribe(input) {
2593
- return this.call("/youtube/transcribe", input);
2908
+ const videoId = input.videoId?.trim() || youtubeVideoIdFromUrl(input.url);
2909
+ if (!videoId) {
2910
+ return Promise.resolve({
2911
+ content: [{
2912
+ type: "text",
2913
+ text: JSON.stringify({
2914
+ error_code: "youtube_video_id_required",
2915
+ error: "Pass videoId from youtube_harvest or a YouTube url that contains a video id.",
2916
+ retryable: false
2917
+ })
2918
+ }],
2919
+ isError: true
2920
+ });
2921
+ }
2922
+ return this.call("/youtube/transcribe", { videoId });
2594
2923
  }
2595
2924
  facebookPageIntel(input) {
2596
2925
  return this.call("/facebook/page-intel", input);
@@ -2627,6 +2956,10 @@ var HttpMcpToolExecutor = class {
2627
2956
  webhookUrl: input.webhookUrl
2628
2957
  }, Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : 9e5);
2629
2958
  }
2959
+ workflowStep(input) {
2960
+ const timeoutMs = this.httpTimeoutOverrideMs ?? Number(process.env.MCP_SCRAPER_WORKFLOW_TIMEOUT_MS ?? 9e5);
2961
+ return this.call(`/workflows/runs/${encodeURIComponent(input.runId)}/step`, {}, Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : 9e5);
2962
+ }
2630
2963
  workflowStatus(input) {
2631
2964
  return this.getJson(`/workflows/runs/${encodeURIComponent(input.runId)}`);
2632
2965
  }
@@ -2651,6 +2984,10 @@ export {
2651
2984
  harvestTimeoutBudget,
2652
2985
  configureReportSaving,
2653
2986
  outputBaseDir,
2987
+ formatCaptureSerpSnapshot,
2988
+ formatCaptureSerpPageSnapshots,
2989
+ CaptureSerpSnapshotOutputSchema,
2990
+ CaptureSerpPageSnapshotsOutputSchema,
2654
2991
  CaptureSerpSnapshotInputSchema,
2655
2992
  CaptureSerpPageSnapshotsInputSchema,
2656
2993
  liveWebToolAnnotations,
@@ -2658,4 +2995,4 @@ export {
2658
2995
  registerPaaExtractorMcpTools,
2659
2996
  HttpMcpToolExecutor
2660
2997
  };
2661
- //# sourceMappingURL=chunk-4UEVYYDW.js.map
2998
+ //# sourceMappingURL=chunk-O5J7E4VX.js.map