mcp-scraper 0.18.0 → 0.20.0

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 (59) hide show
  1. package/README.md +2 -2
  2. package/dist/bin/api-server.cjs +635 -109
  3. package/dist/bin/api-server.cjs.map +1 -1
  4. package/dist/bin/api-server.js +3 -3
  5. package/dist/bin/mcp-scraper-cli.cjs +1 -1
  6. package/dist/bin/mcp-scraper-cli.cjs.map +1 -1
  7. package/dist/bin/mcp-scraper-cli.js +1 -1
  8. package/dist/bin/mcp-scraper-install.cjs +2 -2
  9. package/dist/bin/mcp-scraper-install.cjs.map +1 -1
  10. package/dist/bin/mcp-scraper-install.js +2 -2
  11. package/dist/bin/mcp-stdio-server.cjs +2228 -1783
  12. package/dist/bin/mcp-stdio-server.cjs.map +1 -1
  13. package/dist/bin/mcp-stdio-server.js +5 -4
  14. package/dist/bin/mcp-stdio-server.js.map +1 -1
  15. package/dist/bin/paa-harvest.cjs.map +1 -1
  16. package/dist/bin/paa-harvest.js +3 -3
  17. package/dist/{chunk-GL4BW4CP.js → chunk-22VEGGTW.js} +21 -9
  18. package/dist/{chunk-GL4BW4CP.js.map → chunk-22VEGGTW.js.map} +1 -1
  19. package/dist/{chunk-RUGJE5EB.js → chunk-EJK25QOW.js} +2 -2
  20. package/dist/chunk-JWIE5NCR.js +284 -0
  21. package/dist/chunk-JWIE5NCR.js.map +1 -0
  22. package/dist/{chunk-FMEDPBIV.js → chunk-KE7KE2Q2.js} +482 -35
  23. package/dist/chunk-KE7KE2Q2.js.map +1 -0
  24. package/dist/chunk-PZB3TJWK.js +7 -0
  25. package/dist/chunk-PZB3TJWK.js.map +1 -0
  26. package/dist/{chunk-BX5RCOG5.js → chunk-Q44DN6T2.js} +2 -2
  27. package/dist/{chunk-BX5RCOG5.js.map → chunk-Q44DN6T2.js.map} +1 -1
  28. package/dist/chunk-UN7VMHZL.js +244 -0
  29. package/dist/chunk-UN7VMHZL.js.map +1 -0
  30. package/dist/{chunk-RMPPYKUV.js → chunk-XDFSLSSH.js} +7 -218
  31. package/dist/chunk-XDFSLSSH.js.map +1 -0
  32. package/dist/{chunk-HE2LQPJ2.js → chunk-ZRKFW5FB.js} +2 -2
  33. package/dist/{db-LIOTIWVN.js → db-YHZYG7D2.js} +2 -2
  34. package/dist/{extract-bundle-U4D5LW5W.js → extract-bundle-OSUPAHCE.js} +58 -5
  35. package/dist/extract-bundle-OSUPAHCE.js.map +1 -0
  36. package/dist/index.cjs.map +1 -1
  37. package/dist/index.js +5 -4
  38. package/dist/index.js.map +1 -1
  39. package/dist/{server-MQDCAR6I.js → server-VDWIPV7F.js} +100 -363
  40. package/dist/server-VDWIPV7F.js.map +1 -0
  41. package/dist/{site-extract-repository-NVSZH35Y.js → site-extract-repository-GWKGK46Z.js} +3 -3
  42. package/dist/{worker-JQTS437L.js → worker-O5PZTTPY.js} +8 -8
  43. package/docs/mcp-tool-manifest.generated.json +564 -30
  44. package/docs/specs/meta-ad-creative-media-resolution-spec.md +31 -0
  45. package/docs/specs/unified-credit-and-scheduled-execution-billing-spec.md +1007 -0
  46. package/package.json +1 -1
  47. package/dist/chunk-FMEDPBIV.js.map +0 -1
  48. package/dist/chunk-HPV4VOQX.js +0 -27
  49. package/dist/chunk-HPV4VOQX.js.map +0 -1
  50. package/dist/chunk-RLWCHLV3.js +0 -7
  51. package/dist/chunk-RLWCHLV3.js.map +0 -1
  52. package/dist/chunk-RMPPYKUV.js.map +0 -1
  53. package/dist/extract-bundle-U4D5LW5W.js.map +0 -1
  54. package/dist/server-MQDCAR6I.js.map +0 -1
  55. /package/dist/{chunk-RUGJE5EB.js.map → chunk-EJK25QOW.js.map} +0 -0
  56. /package/dist/{chunk-HE2LQPJ2.js.map → chunk-ZRKFW5FB.js.map} +0 -0
  57. /package/dist/{db-LIOTIWVN.js.map → db-YHZYG7D2.js.map} +0 -0
  58. /package/dist/{site-extract-repository-NVSZH35Y.js.map → site-extract-repository-GWKGK46Z.js.map} +0 -0
  59. /package/dist/{worker-JQTS437L.js.map → worker-O5PZTTPY.js.map} +0 -0
@@ -9,15 +9,17 @@ import {
9
9
  } from "./chunk-R7EETU7Z.js";
10
10
  import {
11
11
  browserServiceProfileName,
12
- browserServiceProfileSaveChanges
13
- } from "./chunk-HPV4VOQX.js";
12
+ browserServiceProfileSaveChanges,
13
+ recordVendorUsage,
14
+ vendorCostUsd
15
+ } from "./chunk-UN7VMHZL.js";
14
16
  import {
15
17
  DEFAULT_MAPS_PROXY_MODE,
16
18
  DEFAULT_PROXY_MODE
17
19
  } from "./chunk-XGIPATLV.js";
18
20
  import {
19
21
  PACKAGE_VERSION
20
- } from "./chunk-RLWCHLV3.js";
22
+ } from "./chunk-PZB3TJWK.js";
21
23
  import {
22
24
  MC_PER_CREDIT
23
25
  } from "./chunk-HL33CGJF.js";
@@ -317,10 +319,14 @@ seam is noted so you can chain them.
317
319
 
318
320
  ## Facebook
319
321
  - Find advertisers -> **facebook_ad_search** (returns \`advertisers[]\` with pageId, libraryId).
320
- - One advertiser's ads -> **facebook_page_intel** (takes pageId, libraryId, or query; returns
321
- \`ads[].videoUrl\`).
322
- - Transcribe an ad video -> **facebook_ad_transcribe** (takes a videoUrl from \`facebook_page_intel\`).
323
- - Transcribe an organic reel/video/post -> **facebook_video_transcribe** (takes the Facebook url directly).
322
+ - Public Ad Library scan -> **facebook_page_intel**. Prefer pageId/libraryId. Query mode is broad discovery,
323
+ can mix advertisers, and must be checked through matchConfidence/warnings. Paused non-political ads may
324
+ be absent even when the connected ad account still has them.
325
+ - Connected ad-account creative -> **meta_ad_creative_media** (takes connectionId + adId). This is the
326
+ preferred paused/dark-ad path and returns actual image content plus exact video follow-up arguments.
327
+ - Direct Graph/Ad Library CDN video -> **facebook_ad_transcribe**. Use transient source URLs immediately.
328
+ - Public reel/video/post -> **facebook_video_transcribe**. Use the public candidate returned by
329
+ \`meta_ad_creative_media\` when Graph did not provide a direct source; dark posts may not resolve publicly.
324
330
 
325
331
  ## Google Ads (Ads Transparency Center)
326
332
  - Find advertisers by domain or brand -> **google_ads_search** (returns \`advertisers[]\` with advertiserId).
@@ -362,8 +368,8 @@ Multi-step orchestrations \u2014 prefer these over hand-chaining primitives when
362
368
  recording) locates a DOM target and returns a ready-to-use timed annotation -> **browser_replay_stop**
363
369
  ends it -> feed collected annotations to **browser_replay_annotate**, or just
364
370
  **browser_replay_download** the plain MP4. **browser_list_replays** recovers replay ids.
365
- - Video breakdown is async: **video_frame_analysis** takes a DIRECT media file URL (resolve
366
- YouTube/Facebook/Instagram page URLs to one first, e.g. via \`facebook_page_intel\`'s \`videoUrl\`) and
371
+ - Video breakdown is async: **video_frame_analysis** takes a supported public video page or direct media URL
372
+ (for connected Meta ads, use the exact sourceUrl/public candidate returned by \`meta_ad_creative_media\`) and
367
373
  returns a \`runId\` immediately -> poll **video_frame_analysis_status** with that \`runId\` until \`status\`
368
374
  is \`done\`.
369
375
 
@@ -372,6 +378,8 @@ Multi-step orchestrations \u2014 prefer these over hand-chaining primitives when
372
378
  sites. It returns a saved folder/artifact plus a summary, not the full content inline.
373
379
  - Browser sessions and media transcription cost credits and take longer \u2014 prefer the cheapest tool that
374
380
  answers the question (plain search/extract before browser agents).
381
+ - Use the hosted browser as a controlled resolver for validated public Facebook post/reel redirects only
382
+ when connected Graph media did not provide a playable source. It is not a bypass for URL/SSRF restrictions.
375
383
  - Large results are saved to disk or an artifact and returned as a summary plus a path or artifactId;
376
384
  read it back for full detail rather than expecting the whole payload inline.
377
385
 
@@ -704,6 +712,75 @@ function summaryEnvelope(executiveSummary, offloaded) {
704
712
  ].join("\n");
705
713
  }
706
714
 
715
+ // src/services/media-transcription.ts
716
+ import { fal } from "@fal-ai/client";
717
+ function transcriptWordCount(text) {
718
+ return text.trim() ? text.trim().split(/\s+/).length : 0;
719
+ }
720
+ function assessTranscriptSignal(text, chunks, durationHintSec) {
721
+ const words = transcriptWordCount(text);
722
+ const chunkDurationSec = chunks.reduce((max, chunk) => {
723
+ const end = Number(chunk.timestamp?.[1]);
724
+ return Number.isFinite(end) && end > max ? end : max;
725
+ }, 0);
726
+ const mediaDurationSec = typeof durationHintSec === "number" && Number.isFinite(durationHintSec) && durationHintSec > 0 ? durationHintSec : chunkDurationSec > 0 ? chunkDurationSec : null;
727
+ const wordsPerMinute = mediaDurationSec && mediaDurationSec > 0 ? Math.round(words / mediaDurationSec * 600) / 10 : null;
728
+ const status = words === 0 ? "empty" : words < 5 ? "low_speech_signal" : "speech_detected";
729
+ const retryRecommended = words <= 2;
730
+ const warnings = [];
731
+ if (status === "empty") {
732
+ warnings.push("No speech was transcribed. This does not distinguish silent media from an inaccessible or expired playback URL.");
733
+ } else if (status === "low_speech_signal") {
734
+ warnings.push("Very little speech was transcribed. Review the media or retry from a durable public post/reel URL before treating this as a complete transcript.");
735
+ }
736
+ if (retryRecommended) {
737
+ warnings.push("For a connected Meta ad, resolve the creative effectiveObjectStoryId and retry with facebook_video_transcribe when a public post URL is available.");
738
+ }
739
+ return {
740
+ status,
741
+ speechDetected: status === "speech_detected",
742
+ confidence: status === "speech_detected" ? "medium" : "low",
743
+ basis: "transcript_word_count_and_timing",
744
+ mediaDurationSec,
745
+ wordsPerMinute,
746
+ retryRecommended,
747
+ warnings
748
+ };
749
+ }
750
+ function transcriptMarkdown(title, text, chunks, durationMs) {
751
+ const fmtTs = (s) => `${Math.floor(s / 60)}:${String(Math.floor(s % 60)).padStart(2, "0")}`;
752
+ const lines = [title, "", `*Transcribed in ${(durationMs / 1e3).toFixed(1)}s*`, "", "## Full Text", "", text, ""];
753
+ if (chunks.length) {
754
+ lines.push("## Timestamped Segments", "");
755
+ for (const ch of chunks) {
756
+ lines.push(`**[${fmtTs(ch.timestamp[0])} -> ${fmtTs(ch.timestamp[1])}]** ${ch.text.trim()}`, "");
757
+ }
758
+ }
759
+ return lines.join("\n");
760
+ }
761
+ async function transcribeMediaUrl(mediaUrl, markdownTitle = "# Media Transcript") {
762
+ const startMs = Date.now();
763
+ const result = await fal.subscribe("fal-ai/wizper", {
764
+ input: { audio_url: mediaUrl, task: "transcribe", language: "en" },
765
+ logs: false,
766
+ pollInterval: 3e3
767
+ });
768
+ const data = result.data;
769
+ const text = data.text ?? "";
770
+ const chunks = data.chunks ?? [];
771
+ const durationMs = Date.now() - startMs;
772
+ void recordVendorUsage({ vendor: "fal_wizper", model: "fal-ai/wizper", units: durationMs / 1e3, unitType: "compute_sec" });
773
+ const costUsd = vendorCostUsd("fal_wizper", durationMs / 1e3);
774
+ return {
775
+ text,
776
+ chunks,
777
+ durationMs,
778
+ costUsd,
779
+ markdown: transcriptMarkdown(markdownTitle, text, chunks, durationMs),
780
+ transcriptSignal: assessTranscriptSignal(text, chunks)
781
+ };
782
+ }
783
+
707
784
  // src/mcp/mcp-response-formatter.ts
708
785
  var INLINE_BUDGET_BYTES = Number(process.env.MCP_SCRAPER_INLINE_BUDGET ?? 5e4);
709
786
  var STRUCTURED_ARRAY_CAP = 25;
@@ -1371,9 +1448,24 @@ function buildSeoExport(siteUrl, pages, branding) {
1371
1448
  branding: branding ?? void 0
1372
1449
  };
1373
1450
  }
1451
+ function formatBackgroundJobStarted(toolLabel, data) {
1452
+ if (data.status !== "pending" || typeof data.jobId !== "string") return null;
1453
+ const jobId = data.jobId;
1454
+ const full = [
1455
+ `# ${toolLabel} \u2014 export started`,
1456
+ `**Job ID:** \`${jobId}\``,
1457
+ `
1458
+ Running in the background \u2014 this can take a while for large sites.`,
1459
+ `
1460
+ Poll \`check_site_export\` with this jobId to get the download link once it's ready.`
1461
+ ].join("\n");
1462
+ return { content: [{ type: "text", text: full }], structuredContent: { jobId, status: "pending" } };
1463
+ }
1374
1464
  async function formatExtractSite(raw, input, ctx) {
1375
1465
  const parsed = parseData(raw);
1376
1466
  if ("error" in parsed) return { content: [{ type: "text", text: parsed.error }], isError: true };
1467
+ const started = formatBackgroundJobStarted("Multi-Page Site Content Crawl", parsed.data);
1468
+ if (started) return started;
1377
1469
  const d = parsed.data;
1378
1470
  const pages = d.pages ?? [];
1379
1471
  const schemaTypesOf = (p) => p.kpo?.type ?? (Array.isArray(p.schema) && p.schema.length ? [`${p.schema.length} block(s)`] : []);
@@ -1491,6 +1583,8 @@ ${pageDetails}${tips}`;
1491
1583
  async function formatAuditSite(raw, input, ctx) {
1492
1584
  const parsed = parseData(raw);
1493
1585
  if ("error" in parsed) return { content: [{ type: "text", text: parsed.error }], isError: true };
1586
+ const started = formatBackgroundJobStarted("Technical SEO Audit", parsed.data);
1587
+ if (started) return started;
1494
1588
  const d = parsed.data;
1495
1589
  const pages = d.pages ?? [];
1496
1590
  const schemaTypesOf = (p) => p.kpo?.type ?? (Array.isArray(p.schema) && p.schema.length ? [`${p.schema.length} block(s)`] : []);
@@ -1562,6 +1656,42 @@ ${imgLine}`
1562
1656
  ].join("\n");
1563
1657
  return { content: [{ type: "text", text: full }], structuredContent };
1564
1658
  }
1659
+ function formatCheckSiteExport(raw, input) {
1660
+ const parsed = parseData(raw);
1661
+ if ("error" in parsed) return { content: [{ type: "text", text: parsed.error }], isError: true };
1662
+ const d = parsed.data;
1663
+ const bundle = (d.artifacts ?? []).find((a) => a.key.endsWith("bundle.zip")) ?? null;
1664
+ const progress = d.totalUrls ? `${d.doneUrls ?? 0}/${d.totalUrls} pages` : "starting";
1665
+ const body = d.status === "complete" && bundle ? `
1666
+ ## \u2705 Ready
1667
+ **Download:** ${bundle.url}
1668
+ **Size:** ${(bundle.bytes / 1e6).toFixed(1)} MB` : d.status === "complete" ? `
1669
+ ## \u26A0\uFE0F Complete, but no bundle was produced
1670
+ Check the job's artifacts \u2014 nothing matched \`bundle.zip\`.` : d.status === "failed" ? `
1671
+ ## \u274C Failed
1672
+ ${d.error ?? "no error message recorded"}` : `
1673
+ ## \u23F3 Not ready yet
1674
+ Status: ${d.status} (${progress}). Poll again shortly.`;
1675
+ const full = [
1676
+ `# Site Export: ${d.startUrl ?? input.jobId}`,
1677
+ `**Job ID:** \`${d.jobId}\``,
1678
+ `**Status:** ${d.status}`,
1679
+ body
1680
+ ].join("\n");
1681
+ return {
1682
+ ...oneBlock(full),
1683
+ structuredContent: {
1684
+ jobId: d.jobId,
1685
+ status: d.status,
1686
+ startUrl: d.startUrl,
1687
+ totalUrls: d.totalUrls,
1688
+ doneUrls: d.doneUrls,
1689
+ bundleUrl: bundle?.url ?? null,
1690
+ bundleBytes: bundle?.bytes ?? null,
1691
+ error: d.error ?? null
1692
+ }
1693
+ };
1694
+ }
1565
1695
  function formatYoutubeHarvest(raw, input) {
1566
1696
  const parsed = parseData(raw);
1567
1697
  if ("error" in parsed) return { content: [{ type: "text", text: parsed.error }], isError: true };
@@ -1671,6 +1801,7 @@ function formatFacebookPageIntel(raw, input) {
1671
1801
  const advertiser = d.advertiserName ?? input.query ?? input.pageId ?? input.libraryId ?? "Advertiser";
1672
1802
  const ads = d.ads ?? [];
1673
1803
  const s = d.summary ?? { totalAds: 0, activeCount: 0, videoCount: 0, imageCount: 0 };
1804
+ const warnings = d.warnings ?? [];
1674
1805
  const adBlocks = ads.map((ad, i) => [
1675
1806
  `### Ad ${i + 1}${ad.libraryId ? ` \xB7 \`${ad.libraryId}\`` : ""} \u2014 ${ad.status ?? "\u2014"} \xB7 ${ad.creativeType ?? "\u2014"} \xB7 ${ad.startDate ?? ad.started ?? "\u2014"}`,
1676
1807
  ad.headline ? `**Headline:** ${ad.headline}` : "",
@@ -1683,19 +1814,28 @@ function formatFacebookPageIntel(raw, input) {
1683
1814
  const full = [
1684
1815
  `# Facebook Ad Intel: ${advertiser}`,
1685
1816
  `**${s.totalAds} ads** \xB7 ${s.activeCount} active \xB7 ${s.videoCount} video \xB7 ${s.imageCount} image`,
1817
+ d.matchConfidence ? `**Match confidence:** ${d.matchConfidence}${d.matchReason ? ` \u2014 ${d.matchReason}` : ""}` : "",
1818
+ warnings.length ? `
1819
+ > \u26A0\uFE0F ${warnings.join("\n> \u26A0\uFE0F ")}` : "",
1686
1820
  `
1687
1821
  ${adBlocks}`,
1688
1822
  `
1689
1823
  ---
1690
1824
  \u{1F4A1} **Tips**
1691
- - Transcribe video ads: use \`facebook_ad_transcribe\` with the direct \`videoUrl\` above
1692
- - Transcribe organic Facebook reels/posts: use \`facebook_video_transcribe\` with the public Facebook URL
1693
- - Find other advertisers: use \`facebook_ad_search\``
1825
+ - Connected, paused, or dark ad: use \`meta_ad_creative_media\` with its adId
1826
+ - Ad Library direct video URL: use \`facebook_ad_transcribe\` immediately
1827
+ - Public Facebook post/reel URL: use \`facebook_video_transcribe\`
1828
+ - Find the exact advertiser pageId first: use \`facebook_ad_search\``
1694
1829
  ].filter(Boolean).join("\n");
1695
1830
  return {
1696
1831
  ...oneBlock(full),
1697
1832
  structuredContent: {
1698
1833
  advertiserName: d.advertiserName ?? null,
1834
+ inputMode: d.inputMode ?? (input.pageId ? "pageId" : input.libraryId ? "libraryId" : "query"),
1835
+ matchConfidence: d.matchConfidence ?? (input.query ? "low" : "high"),
1836
+ matchReason: d.matchReason ?? (input.query ? "Keyword search result was not confidence-scored by the server." : "Exact identifier filter."),
1837
+ warnings,
1838
+ matchedAdvertisers: d.matchedAdvertisers ?? [],
1699
1839
  totalAds: s.totalAds ?? 0,
1700
1840
  activeCount: s.activeCount ?? 0,
1701
1841
  videoCount: s.videoCount ?? 0,
@@ -2624,6 +2764,7 @@ function formatFacebookAdTranscribe(raw, input) {
2624
2764
  const chunks = d.chunks ?? [];
2625
2765
  const durSec = d.durationMs ? (d.durationMs / 1e3).toFixed(0) : "\u2014";
2626
2766
  const words = wordCount(text);
2767
+ const signal = d.transcriptSignal ?? assessTranscriptSignal(text, chunks);
2627
2768
  const chunkRows = chunks.slice(0, 50).map((c) => {
2628
2769
  const sec = Number.isFinite(c.timestamp[0]) ? Math.floor(c.timestamp[0]) : 0;
2629
2770
  const mm = String(Math.floor(sec / 60)).padStart(2, "0");
@@ -2632,7 +2773,9 @@ function formatFacebookAdTranscribe(raw, input) {
2632
2773
  }).join("\n");
2633
2774
  const full = [
2634
2775
  `# Facebook Ad Transcript`,
2635
- `**Duration:** ${durSec}s \xB7 **${words} words**`,
2776
+ `**Transcription elapsed:** ${durSec}s \xB7 **${words} words** \xB7 **Signal:** ${signal.status}`,
2777
+ signal.warnings.length ? `
2778
+ > \u26A0\uFE0F ${signal.warnings.join("\n> \u26A0\uFE0F ")}` : "",
2636
2779
  `
2637
2780
  ## Full Transcript
2638
2781
  ${text}`,
@@ -2654,6 +2797,7 @@ ${chunkRows}` : "",
2654
2797
  durationMs: typeof d.durationMs === "number" ? d.durationMs : null,
2655
2798
  transcriptText: text,
2656
2799
  chunks: structuredTranscriptChunks(chunks),
2800
+ transcriptSignal: signal,
2657
2801
  resolvedInputs: {
2658
2802
  videoUrl: input.videoUrl
2659
2803
  }
@@ -2670,6 +2814,7 @@ function formatFacebookVideoTranscribe(raw, input) {
2670
2814
  const durSec = d.durationMs ? (d.durationMs / 1e3).toFixed(0) : "\u2014";
2671
2815
  const videoDuration = typeof d.videoDurationSec === "number" ? `${Math.round(d.videoDurationSec)}s` : "\u2014";
2672
2816
  const label = d.videoId ? `Facebook Organic Video \`${d.videoId}\`` : "Facebook Organic Video";
2817
+ const signal = d.transcriptSignal ?? assessTranscriptSignal(text, chunks, d.videoDurationSec);
2673
2818
  const chunkRows = chunks.slice(0, 50).map((c) => {
2674
2819
  const sec = Number.isFinite(c.timestamp[0]) ? Math.floor(c.timestamp[0]) : 0;
2675
2820
  const mm = String(Math.floor(sec / 60)).padStart(2, "0");
@@ -2680,6 +2825,9 @@ function formatFacebookVideoTranscribe(raw, input) {
2680
2825
  `# ${label} Transcript`,
2681
2826
  d.ownerName ? `**Owner:** ${d.ownerName}` : "",
2682
2827
  `**Video duration:** ${videoDuration} \xB7 **Transcribed in:** ${durSec}s \xB7 **${wordCount2} words**`,
2828
+ `**Transcript signal:** ${signal.status} (${signal.confidence} heuristic confidence)`,
2829
+ signal.warnings.length ? `
2830
+ > \u26A0\uFE0F ${signal.warnings.join("\n> \u26A0\uFE0F ")}` : "",
2683
2831
  d.pageUrl ? `**Page URL:** ${d.pageUrl}` : `**Page URL:** ${input.url}`,
2684
2832
  d.videoUrl ? `**Extracted MP4:** \`${d.videoUrl}\`` : "",
2685
2833
  `
@@ -2712,7 +2860,8 @@ ${chunkRows}` : "",
2712
2860
  startSec: Number.isFinite(c.timestamp[0]) ? c.timestamp[0] : 0,
2713
2861
  endSec: Number.isFinite(c.timestamp[1]) ? c.timestamp[1] : 0,
2714
2862
  text: c.text
2715
- }))
2863
+ })),
2864
+ transcriptSignal: signal
2716
2865
  }
2717
2866
  };
2718
2867
  }
@@ -3022,6 +3171,208 @@ function recordOutputSchema(name, schema) {
3022
3171
  return ADVERTISE_OUTPUT_SCHEMAS ? schema : void 0;
3023
3172
  }
3024
3173
 
3174
+ // src/mcp/meta-ad-creative-media.ts
3175
+ var META_IMAGE_HOSTS = ["facebook.com", "fbcdn.net", "fbsbx.com", "cdninstagram.com"];
3176
+ var MAX_IMAGE_BYTES = 15e5;
3177
+ var MAX_TOTAL_IMAGE_BYTES = 25e5;
3178
+ var MAX_REDIRECTS = 3;
3179
+ function asRecord(value) {
3180
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
3181
+ }
3182
+ function parseToolPayload(result) {
3183
+ const structured = asRecord(result.structuredContent);
3184
+ if (structured) return structured;
3185
+ for (const block of result.content ?? []) {
3186
+ if (block.type !== "text") continue;
3187
+ try {
3188
+ const parsed = JSON.parse(block.text);
3189
+ const row = asRecord(parsed);
3190
+ if (row) return row;
3191
+ } catch {
3192
+ continue;
3193
+ }
3194
+ }
3195
+ return null;
3196
+ }
3197
+ function allowedMetaImageUrl(raw) {
3198
+ try {
3199
+ const url = new URL(raw);
3200
+ if (url.protocol !== "https:") return null;
3201
+ const host = url.hostname.toLowerCase().replace(/\.$/, "");
3202
+ if (!META_IMAGE_HOSTS.some((suffix) => host === suffix || host.endsWith(`.${suffix}`))) return null;
3203
+ return url;
3204
+ } catch {
3205
+ return null;
3206
+ }
3207
+ }
3208
+ function detectImageMime(bytes) {
3209
+ if (bytes.length >= 3 && bytes[0] === 255 && bytes[1] === 216 && bytes[2] === 255) return "image/jpeg";
3210
+ if (bytes.length >= 8 && bytes[0] === 137 && bytes[1] === 80 && bytes[2] === 78 && bytes[3] === 71 && bytes[4] === 13 && bytes[5] === 10 && bytes[6] === 26 && bytes[7] === 10) return "image/png";
3211
+ if (bytes.length >= 12 && String.fromCharCode(...bytes.slice(0, 4)) === "RIFF" && String.fromCharCode(...bytes.slice(8, 12)) === "WEBP") return "image/webp";
3212
+ if (bytes.length >= 6) {
3213
+ const signature = String.fromCharCode(...bytes.slice(0, 6));
3214
+ if (signature === "GIF87a" || signature === "GIF89a") return "image/gif";
3215
+ }
3216
+ return null;
3217
+ }
3218
+ async function readBoundedBody(response, maxBytes) {
3219
+ const contentLength = Number(response.headers.get("content-length") ?? "");
3220
+ if (Number.isFinite(contentLength) && contentLength > maxBytes) throw new Error("image_too_large");
3221
+ if (!response.body) {
3222
+ const bytes2 = new Uint8Array(await response.arrayBuffer());
3223
+ if (bytes2.byteLength > maxBytes) throw new Error("image_too_large");
3224
+ return bytes2;
3225
+ }
3226
+ const reader = response.body.getReader();
3227
+ const chunks = [];
3228
+ let total = 0;
3229
+ try {
3230
+ while (true) {
3231
+ const { done, value } = await reader.read();
3232
+ if (done) break;
3233
+ if (!value) continue;
3234
+ total += value.byteLength;
3235
+ if (total > maxBytes) throw new Error("image_too_large");
3236
+ chunks.push(value);
3237
+ }
3238
+ } finally {
3239
+ reader.releaseLock();
3240
+ }
3241
+ const bytes = new Uint8Array(total);
3242
+ let offset = 0;
3243
+ for (const chunk of chunks) {
3244
+ bytes.set(chunk, offset);
3245
+ offset += chunk.byteLength;
3246
+ }
3247
+ return bytes;
3248
+ }
3249
+ async function fetchMetaImage(rawUrl, maxBytes) {
3250
+ let url = allowedMetaImageUrl(rawUrl);
3251
+ if (!url) throw new Error("image_host_not_allowed");
3252
+ for (let redirect = 0; redirect <= MAX_REDIRECTS; redirect++) {
3253
+ const response = await fetch(url, {
3254
+ redirect: "manual",
3255
+ headers: { Accept: "image/jpeg,image/png,image/webp,image/gif;q=0.8" },
3256
+ signal: AbortSignal.timeout(12e3)
3257
+ });
3258
+ if ([301, 302, 303, 307, 308].includes(response.status)) {
3259
+ const location = response.headers.get("location");
3260
+ if (!location || redirect === MAX_REDIRECTS) throw new Error("image_redirect_rejected");
3261
+ url = allowedMetaImageUrl(new URL(location, url).href);
3262
+ if (!url) throw new Error("image_redirect_host_not_allowed");
3263
+ continue;
3264
+ }
3265
+ if (!response.ok) throw new Error(`image_fetch_${response.status}`);
3266
+ const bytes = await readBoundedBody(response, maxBytes);
3267
+ const mimeType = detectImageMime(bytes);
3268
+ if (!mimeType) throw new Error("image_content_invalid");
3269
+ return { bytes, mimeType, finalUrl: url.href };
3270
+ }
3271
+ throw new Error("image_redirect_rejected");
3272
+ }
3273
+ function firstString(row, keys) {
3274
+ for (const key of keys) {
3275
+ const value = row[key];
3276
+ if (typeof value === "string" && value.trim()) return value;
3277
+ }
3278
+ return null;
3279
+ }
3280
+ function displayUrl(raw) {
3281
+ try {
3282
+ const url = new URL(raw);
3283
+ return `${url.origin}${url.pathname}`;
3284
+ } catch {
3285
+ return "[invalid media URL]";
3286
+ }
3287
+ }
3288
+ async function buildMetaAdCreativeMediaResult(executor, input) {
3289
+ const providerResult = await executor.readServiceConnection({
3290
+ connectionId: input.connectionId,
3291
+ tool: "resolve-ad-creative-media",
3292
+ args: { adId: input.adId }
3293
+ });
3294
+ if (providerResult.isError) return providerResult;
3295
+ const payload = parseToolPayload(providerResult);
3296
+ const resolved = asRecord(payload?.["result"]) ?? payload;
3297
+ if (!resolved || typeof resolved["creativeId"] !== "string") {
3298
+ return {
3299
+ content: [{ type: "text", text: "Meta returned an invalid creative-media result. Confirm that resolve-ad-creative-media is deployed and this connection can read the ad." }],
3300
+ isError: true
3301
+ };
3302
+ }
3303
+ const warnings = Array.isArray(resolved["warnings"]) ? resolved["warnings"].filter((value) => typeof value === "string") : [];
3304
+ const limitations = Array.isArray(resolved["limitations"]) ? resolved["limitations"].filter((value) => typeof value === "string") : [];
3305
+ const images = Array.isArray(resolved["images"]) ? resolved["images"].map(asRecord).filter((value) => value !== null) : [];
3306
+ const videos = Array.isArray(resolved["videos"]) ? resolved["videos"].map(asRecord).filter((value) => value !== null) : [];
3307
+ const nextActions = Array.isArray(resolved["nextActions"]) ? resolved["nextActions"].map(asRecord).filter((value) => value !== null) : [];
3308
+ const content = [];
3309
+ const inlineImages = [];
3310
+ if (input.imageMode === "inline_preview") {
3311
+ let remaining = MAX_TOTAL_IMAGE_BYTES;
3312
+ const seen = /* @__PURE__ */ new Set();
3313
+ for (const image of images) {
3314
+ if (inlineImages.length >= input.maxInlineImages || remaining <= 0) break;
3315
+ const sourceUrl = firstString(image, ["url", "thumbnailUrl", "permalinkUrl"]);
3316
+ if (!sourceUrl || seen.has(sourceUrl)) continue;
3317
+ seen.add(sourceUrl);
3318
+ try {
3319
+ const fetched = await fetchMetaImage(sourceUrl, Math.min(MAX_IMAGE_BYTES, remaining));
3320
+ const contentIndex = content.length + 1;
3321
+ content.push({ type: "image", data: Buffer.from(fetched.bytes).toString("base64"), mimeType: fetched.mimeType });
3322
+ inlineImages.push({ sourceUrl: fetched.finalUrl, mimeType: fetched.mimeType, bytes: fetched.bytes.byteLength, contentIndex });
3323
+ remaining -= fetched.bytes.byteLength;
3324
+ } catch (error) {
3325
+ const code = error instanceof Error ? error.message : "image_fetch_failed";
3326
+ warnings.push(`Creative image preview could not be attached (${code}) for ${displayUrl(sourceUrl)}.`);
3327
+ }
3328
+ }
3329
+ }
3330
+ const mediaType = typeof resolved["mediaType"] === "string" ? resolved["mediaType"] : "unknown";
3331
+ const publicPostCandidateUrl = typeof resolved["publicPostCandidateUrl"] === "string" ? resolved["publicPostCandidateUrl"] : null;
3332
+ const summary = [
3333
+ "# Meta Ad Creative Media",
3334
+ `**Ad:** ${typeof resolved["adName"] === "string" ? resolved["adName"] : input.adId} (\`${input.adId}\`)`,
3335
+ `**Creative:** ${typeof resolved["creativeName"] === "string" ? resolved["creativeName"] : resolved["creativeId"]} (\`${resolved["creativeId"]}\`)`,
3336
+ `**Media:** ${mediaType} \xB7 ${images.length} image descriptor(s) \xB7 ${videos.length} video descriptor(s) \xB7 ${inlineImages.length} inline visual preview(s)`,
3337
+ publicPostCandidateUrl ? `**Public post candidate:** ${publicPostCandidateUrl} (unverified; dark/paused ads may not be public)` : "",
3338
+ nextActions.length ? `
3339
+ ## Recommended next calls
3340
+ ${nextActions.map((action) => `- \`${String(action["tool"] ?? "")}\`: ${String(action["reason"] ?? "")}`).join("\n")}` : "",
3341
+ warnings.length ? `
3342
+ ## Warnings
3343
+ ${warnings.map((warning) => `- ${warning}`).join("\n")}` : "",
3344
+ limitations.length ? `
3345
+ ## Limitations
3346
+ ${limitations.map((limitation) => `- ${limitation}`).join("\n")}` : ""
3347
+ ].filter(Boolean).join("\n");
3348
+ content.unshift({ type: "text", text: summary });
3349
+ return {
3350
+ content,
3351
+ structuredContent: {
3352
+ ok: true,
3353
+ adId: String(resolved["adId"] ?? input.adId),
3354
+ adName: typeof resolved["adName"] === "string" ? resolved["adName"] : null,
3355
+ adAccountId: typeof resolved["adAccountId"] === "string" ? resolved["adAccountId"] : null,
3356
+ creativeId: String(resolved["creativeId"]),
3357
+ creativeName: typeof resolved["creativeName"] === "string" ? resolved["creativeName"] : null,
3358
+ effectiveObjectStoryId: typeof resolved["effectiveObjectStoryId"] === "string" ? resolved["effectiveObjectStoryId"] : null,
3359
+ sourceFacebookPostId: typeof resolved["sourceFacebookPostId"] === "string" ? resolved["sourceFacebookPostId"] : null,
3360
+ objectStoryId: typeof resolved["objectStoryId"] === "string" ? resolved["objectStoryId"] : null,
3361
+ pageId: typeof resolved["pageId"] === "string" ? resolved["pageId"] : null,
3362
+ postId: typeof resolved["postId"] === "string" ? resolved["postId"] : null,
3363
+ publicPostCandidateUrl,
3364
+ publicPostStatus: "unverified",
3365
+ mediaType,
3366
+ images,
3367
+ videos,
3368
+ inlineImages,
3369
+ nextActions,
3370
+ warnings,
3371
+ limitations
3372
+ }
3373
+ };
3374
+ }
3375
+
3025
3376
  // src/mcp/mcp-tool-schemas.ts
3026
3377
  import { z } from "zod";
3027
3378
  var HarvestPaaInputSchema = {
@@ -3060,13 +3411,20 @@ var ExtractSiteInputSchema = {
3060
3411
  maxPages: 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."),
3061
3412
  rotateProxies: 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."),
3062
3413
  rotateProxyEvery: z.number().int().min(1).max(100).optional().describe("When rotateProxies is on, pages fetched per proxy before rotating. Default 30."),
3063
- formats: z.array(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.")
3414
+ formats: z.array(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."),
3415
+ background: z.boolean().default(false).describe("Run the crawl as a background job instead of blocking this call, returning a jobId immediately \u2014 poll it with check_site_export to get a downloadable zip (all page content, plus real image files if downloadImages is set) once ready. Use for large sites where a synchronous call would be slow."),
3416
+ downloadImages: z.boolean().default(false).describe("Download every discovered image as a real file into the export bundle (not just image URLs/stats). OFF by default \u2014 must be explicitly set true. Implies background regardless of the background flag, since downloading a whole site's images is too slow to run synchronously. Capped at 20 images/page and 500 images/site.")
3064
3417
  };
3065
3418
  var AuditSiteInputSchema = {
3066
3419
  url: 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."),
3067
3420
  maxPages: 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."),
3068
3421
  rotateProxies: 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."),
3069
- rotateProxyEvery: z.number().int().min(1).max(100).optional().describe("When rotateProxies is on, pages fetched per proxy before rotating. Default 30.")
3422
+ rotateProxyEvery: z.number().int().min(1).max(100).optional().describe("When rotateProxies is on, pages fetched per proxy before rotating. Default 30."),
3423
+ background: z.boolean().default(false).describe("Run the audit as a background job instead of blocking this call, returning a jobId immediately \u2014 poll it with check_site_export to get a downloadable zip (full audit report, all page content, plus real image files if downloadImages is set) once ready. Use for large sites where a synchronous call would be slow."),
3424
+ downloadImages: z.boolean().default(false).describe("Download every discovered image as a real file into the export bundle (not just image URLs/stats). OFF by default \u2014 must be explicitly set true. Implies background regardless of the background flag, since downloading a whole site's images is too slow to run synchronously. Capped at 20 images/page and 500 images/site.")
3425
+ };
3426
+ var CheckSiteExportInputSchema = {
3427
+ jobId: z.string().min(1).describe('The jobId returned by extract_site or audit_site when called with background (or downloadImages) set \u2014 poll this until status is "complete" (or "failed").')
3070
3428
  };
3071
3429
  var YoutubeHarvestInputSchema = {
3072
3430
  mode: z.enum(["search", "channel"]).describe("Use search for topic/keyword requests. Use channel when the user provides @handle, channel ID, or channel URL."),
@@ -3081,7 +3439,7 @@ var YoutubeTranscribeInputSchema = {
3081
3439
  var FacebookPageIntelInputSchema = {
3082
3440
  pageId: z.string().optional().describe("Facebook advertiser/page ID. Use only a value returned by facebook_ad_search or copied from Ad Library."),
3083
3441
  libraryId: 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."),
3084
- query: z.string().optional().describe("Advertiser or brand name when pageId/libraryId is not known. One of pageId, libraryId, or query is required."),
3442
+ query: z.string().optional().describe("Broad Ad Library keyword discovery when pageId/libraryId is not known. Results can mix unrelated advertisers; inspect matchConfidence, matchedAdvertisers, and warnings before analysis. One of pageId, libraryId, or query is required."),
3085
3443
  maxAds: z.number().int().min(1).max(200).default(50).describe("Maximum ads to inspect. Default 50, maximum 200."),
3086
3444
  country: z.string().length(2).default("US").describe("Two-letter Ad Library country code. Default US.")
3087
3445
  };
@@ -3105,10 +3463,10 @@ var VideoFrameAnalysisStatusInputSchema = {
3105
3463
  runId: z.string().min(1).describe("The runId returned by video_frame_analysis.")
3106
3464
  };
3107
3465
  var FacebookAdTranscribeInputSchema = {
3108
- videoUrl: 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.")
3466
+ videoUrl: z.string().url().describe("Direct Meta/Facebook CDN video URL from facebook_page_intel or meta_ad_creative_media. Use transient sources immediately. Do not pass a public post/reel/share URL\u2014use facebook_video_transcribe for those.")
3109
3467
  };
3110
3468
  var FacebookVideoTranscribeInputSchema = {
3111
- url: z.string().url().describe("Organic Facebook reel/video/watch/post/share URL from facebook.com, m.facebook.com, or fb.watch."),
3469
+ url: z.string().url().describe("Public Facebook reel/video/watch/post/share URL from facebook.com, m.facebook.com, or fb.watch. For connected account ads, get the correct public candidate from meta_ad_creative_media instead of guessing URL structure."),
3112
3470
  quality: 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.")
3113
3471
  };
3114
3472
  var GoogleAdsSearchInputSchema = {
@@ -3469,36 +3827,52 @@ var DiffPageOutputSchema = {
3469
3827
  };
3470
3828
  var ExtractSiteOutputSchema = {
3471
3829
  url: z.string(),
3472
- pageCount: z.number().int().min(0),
3830
+ pageCount: z.number().int().min(0).optional().describe("Absent when background is true \u2014 the crawl has not finished yet."),
3473
3831
  pages: z.array(z.object({
3474
3832
  url: z.string(),
3475
3833
  title: NullableString,
3476
3834
  schemaTypes: z.array(z.string())
3477
- })),
3478
- durationMs: z.number().min(0),
3835
+ })).optional().describe("Absent when background is true \u2014 the crawl has not finished yet."),
3836
+ durationMs: z.number().min(0).optional().describe("Absent when background is true \u2014 the crawl has not finished yet."),
3479
3837
  truncatedCount: z.number().int().min(0).optional(),
3480
- artifact: ArtifactPointerOutputSchema.optional()
3838
+ artifact: ArtifactPointerOutputSchema.optional(),
3839
+ jobId: z.string().optional().describe("Present when background (or downloadImages) was set \u2014 poll with check_site_export."),
3840
+ status: z.enum(["pending"]).optional().describe("Present when background (or downloadImages) was set."),
3841
+ statusUrl: z.string().optional().describe("Present when background (or downloadImages) was set \u2014 informational; use check_site_export with jobId, not this URL directly.")
3481
3842
  };
3482
3843
  var AuditSiteOutputSchema = {
3483
3844
  url: z.string(),
3484
- pageCount: z.number().int().min(0),
3485
- durationMs: z.number().min(0),
3486
- bulkFolder: z.string().nullable(),
3487
- issues: z.record(z.string(), z.number()),
3845
+ pageCount: z.number().int().min(0).optional().describe("Absent when background is true \u2014 the audit has not finished yet."),
3846
+ durationMs: z.number().min(0).optional().describe("Absent when background is true \u2014 the audit has not finished yet."),
3847
+ bulkFolder: z.string().nullable().optional().describe("Absent when background is true \u2014 the audit has not finished yet."),
3848
+ issues: z.record(z.string(), z.number()).optional().describe("Absent when background is true \u2014 the audit has not finished yet."),
3488
3849
  images: z.object({
3489
3850
  unique: z.number().int().min(0),
3490
3851
  totalBytes: z.number().min(0),
3491
3852
  over100kb: z.number().int().min(0),
3492
3853
  legacyFormat: z.number().int().min(0)
3493
- }),
3854
+ }).optional().describe("Absent when background is true \u2014 the audit has not finished yet."),
3494
3855
  links: z.object({
3495
3856
  internal: z.number().int().min(0),
3496
3857
  external: z.number().int().min(0),
3497
3858
  orphans: z.number().int().min(0),
3498
3859
  brokenInternal: z.number().int().min(0),
3499
3860
  externalDomains: z.number().int().min(0)
3500
- }),
3501
- artifact: ArtifactPointerOutputSchema.optional()
3861
+ }).optional().describe("Absent when background is true \u2014 the audit has not finished yet."),
3862
+ artifact: ArtifactPointerOutputSchema.optional(),
3863
+ jobId: z.string().optional().describe("Present when background (or downloadImages) was set \u2014 poll with check_site_export."),
3864
+ status: z.enum(["pending"]).optional().describe("Present when background (or downloadImages) was set."),
3865
+ statusUrl: z.string().optional().describe("Present when background (or downloadImages) was set \u2014 informational; use check_site_export with jobId, not this URL directly.")
3866
+ };
3867
+ var CheckSiteExportOutputSchema = {
3868
+ jobId: z.string(),
3869
+ status: z.enum(["pending", "running", "complete", "failed"]),
3870
+ startUrl: z.string().optional(),
3871
+ totalUrls: z.number().int().min(0).optional(),
3872
+ doneUrls: z.number().int().min(0).optional(),
3873
+ bundleUrl: z.string().nullable().describe("Downloadable zip URL once status is complete; null otherwise."),
3874
+ bundleBytes: z.number().int().min(0).nullable().describe("Zip size in bytes once status is complete; null otherwise."),
3875
+ error: z.string().nullable().optional().describe("Present with a message when status is failed.")
3502
3876
  };
3503
3877
  var MapsPlaceIntelOutputSchema = {
3504
3878
  name: z.string(),
@@ -3663,6 +4037,11 @@ var RedditThreadOutputSchema = {
3663
4037
  };
3664
4038
  var FacebookPageIntelOutputSchema = {
3665
4039
  advertiserName: NullableString,
4040
+ inputMode: z.enum(["pageId", "libraryId", "query"]),
4041
+ matchConfidence: z.enum(["high", "medium", "low"]),
4042
+ matchReason: z.string(),
4043
+ warnings: z.array(z.string()),
4044
+ matchedAdvertisers: z.array(z.object({ name: z.string(), adCount: z.number().int().min(0) })),
3666
4045
  totalAds: z.number().int().min(0),
3667
4046
  activeCount: z.number().int().min(0),
3668
4047
  videoCount: z.number().int().min(0),
@@ -3717,6 +4096,16 @@ var GoogleAdsPageIntelOutputSchema = {
3717
4096
  variations: z.number().int().nullable()
3718
4097
  }))
3719
4098
  };
4099
+ var TranscriptSignalOutput = z.object({
4100
+ status: z.enum(["speech_detected", "low_speech_signal", "empty"]),
4101
+ speechDetected: z.boolean(),
4102
+ confidence: z.enum(["medium", "low"]),
4103
+ basis: z.literal("transcript_word_count_and_timing"),
4104
+ mediaDurationSec: z.number().nullable(),
4105
+ wordsPerMinute: z.number().nullable(),
4106
+ retryRecommended: z.boolean(),
4107
+ warnings: z.array(z.string())
4108
+ });
3720
4109
  var FacebookVideoTranscribeOutputSchema = {
3721
4110
  sourceUrl: z.string().url(),
3722
4111
  pageUrl: z.string().url(),
@@ -3733,7 +4122,8 @@ var FacebookVideoTranscribeOutputSchema = {
3733
4122
  startSec: z.number(),
3734
4123
  endSec: z.number(),
3735
4124
  text: z.string()
3736
- }))
4125
+ })),
4126
+ transcriptSignal: TranscriptSignalOutput
3737
4127
  };
3738
4128
  var TranscriptChunkOutput = z.object({
3739
4129
  startSec: z.number(),
@@ -3855,6 +4245,7 @@ var FacebookAdTranscribeOutputSchema = {
3855
4245
  durationMs: z.number().nullable(),
3856
4246
  transcriptText: z.string(),
3857
4247
  chunks: z.array(TranscriptChunkOutput),
4248
+ transcriptSignal: TranscriptSignalOutput,
3858
4249
  resolvedInputs: z.object({
3859
4250
  videoUrl: z.string().url()
3860
4251
  })
@@ -4087,6 +4478,43 @@ var ReadServiceConnectionOutputSchema = {
4087
4478
  result: z.unknown().optional(),
4088
4479
  error: NullableString
4089
4480
  };
4481
+ var MetaAdCreativeMediaInputSchema = {
4482
+ connectionId: z.string().min(1).describe("Tenant-owned Meta Marketing connectionId from list_service_connections."),
4483
+ adId: z.string().regex(/^\d{5,30}$/).describe("Meta ad ID from the connected ad account. This is not an Ad Library archive ID."),
4484
+ imageMode: z.enum(["inline_preview", "resource_only", "none"]).default("inline_preview").describe("inline_preview returns bounded MCP image content that a vision-capable client can inspect. resource_only returns descriptors/URLs only. none skips image delivery."),
4485
+ maxInlineImages: z.number().int().min(1).max(4).default(2).describe("Maximum creative image/thumbnail previews to attach as MCP image blocks. Default 2; maximum 4.")
4486
+ };
4487
+ var MetaAdCreativeMediaOutputSchema = {
4488
+ ok: z.boolean(),
4489
+ adId: z.string(),
4490
+ adName: NullableString,
4491
+ adAccountId: NullableString,
4492
+ creativeId: z.string(),
4493
+ creativeName: NullableString,
4494
+ effectiveObjectStoryId: NullableString,
4495
+ sourceFacebookPostId: NullableString,
4496
+ objectStoryId: NullableString,
4497
+ pageId: NullableString,
4498
+ postId: NullableString,
4499
+ publicPostCandidateUrl: NullableString,
4500
+ publicPostStatus: z.literal("unverified"),
4501
+ mediaType: z.enum(["image", "video", "carousel", "mixed", "unknown"]),
4502
+ images: z.array(z.record(z.string(), z.unknown())),
4503
+ videos: z.array(z.record(z.string(), z.unknown())),
4504
+ inlineImages: z.array(z.object({
4505
+ sourceUrl: z.string().url(),
4506
+ mimeType: z.string(),
4507
+ bytes: z.number().int().min(0),
4508
+ contentIndex: z.number().int().min(1)
4509
+ })),
4510
+ nextActions: z.array(z.object({
4511
+ tool: z.enum(["facebook_ad_transcribe", "facebook_video_transcribe", "video_frame_analysis"]),
4512
+ args: z.record(z.string(), z.unknown()),
4513
+ reason: z.string()
4514
+ })),
4515
+ warnings: z.array(z.string()),
4516
+ limitations: z.array(z.string())
4517
+ };
4090
4518
  var ImportServiceConnectionToMemoryInputSchema = {
4091
4519
  connectionId: z.string().min(1).max(200).describe("A tenant-owned connectionId from list_service_connections."),
4092
4520
  providerConfigKey: z.string().min(1).max(200).describe("The exact providerConfigKey returned with that connection. It is matched together with connectionId against the authenticated caller."),
@@ -4748,6 +5176,13 @@ function registerPaaExtractorMcpTools(server, executor, options = {}) {
4748
5176
  outputSchema: recordOutputSchema("audit_site", AuditSiteOutputSchema),
4749
5177
  annotations: liveWebToolAnnotations("Technical SEO Audit")
4750
5178
  }, async (input) => formatAuditSite(await executor.auditSite(input), input, ctx));
5179
+ server.registerTool("check_site_export", {
5180
+ title: "Check Site Export",
5181
+ description: "Poll the status of a background extract_site or audit_site job (one started with background or downloadImages set). Returns a downloadable zip URL (all page content, plus real image files if downloadImages was set) once status is complete.",
5182
+ inputSchema: CheckSiteExportInputSchema,
5183
+ outputSchema: recordOutputSchema("check_site_export", CheckSiteExportOutputSchema),
5184
+ annotations: liveWebToolAnnotations("Check Site Export")
5185
+ }, async (input) => formatCheckSiteExport(await executor.checkSiteExport(input), input));
4751
5186
  server.registerTool("youtube_harvest", {
4752
5187
  title: "YouTube Video Harvest",
4753
5188
  description: '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.',
@@ -4764,7 +5199,7 @@ function registerPaaExtractorMcpTools(server, executor, options = {}) {
4764
5199
  }, async (input) => formatYoutubeTranscribe(await executor.youtubeTranscribe(input), input));
4765
5200
  server.registerTool("facebook_page_intel", {
4766
5201
  title: "Facebook Advertiser Ad Intel",
4767
- description: "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.",
5202
+ description: "Harvest public Ad Library creatives. Prefer exact pageId/libraryId; query is broad keyword discovery and can mix unrelated advertisers, so inspect matchConfidence/warnings before analysis. Paused or inactive non-political ads may be absent from Ad Library\u2014use meta_ad_creative_media with the connected ad account for those. Direct Ad Library videoUrl values go to facebook_ad_transcribe.",
4768
5203
  inputSchema: FacebookPageIntelInputSchema,
4769
5204
  outputSchema: recordOutputSchema("facebook_page_intel", FacebookPageIntelOutputSchema),
4770
5205
  annotations: liveWebToolAnnotations("Facebook Advertiser Ad Intel")
@@ -4799,7 +5234,7 @@ function registerPaaExtractorMcpTools(server, executor, options = {}) {
4799
5234
  }, async (input) => executor.videoFrameAnalysisStatus(input));
4800
5235
  server.registerTool("facebook_ad_transcribe", {
4801
5236
  title: "Facebook Ad Transcription",
4802
- 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).",
5237
+ description: "Transcribe a direct Meta/Facebook CDN video source returned by facebook_page_intel or meta_ad_creative_media. CDN sources can expire, so use them immediately. Do not pass public post/reel/share URLs; use facebook_video_transcribe for those. For a paused/account-owned ad, start with meta_ad_creative_media: it selects the direct Graph source when available and otherwise returns the effective organic-post candidate.",
4803
5238
  inputSchema: FacebookAdTranscribeInputSchema,
4804
5239
  outputSchema: recordOutputSchema("facebook_ad_transcribe", FacebookAdTranscribeOutputSchema),
4805
5240
  annotations: liveWebToolAnnotations("Facebook Ad Transcription")
@@ -4827,7 +5262,7 @@ function registerPaaExtractorMcpTools(server, executor, options = {}) {
4827
5262
  }, async (input) => formatGoogleAdsTranscribe(await executor.googleAdsTranscribe(input), input));
4828
5263
  server.registerTool("facebook_video_transcribe", {
4829
5264
  title: "Facebook Organic Video Transcription",
4830
- description: "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.",
5265
+ description: "Transcribe audio from a public Facebook reel/video/post/share URL (including fb.watch). Renders the public page, selects the best progressive MP4, and returns transcript plus resolved metadata and a low-speech signal. For a connected paused ad, use meta_ad_creative_media first; pass its public post/permalink candidate here only when no direct Graph source is available. Dark/unpublished ads may not have a public route.",
4831
5266
  inputSchema: FacebookVideoTranscribeInputSchema,
4832
5267
  outputSchema: recordOutputSchema("facebook_video_transcribe", FacebookVideoTranscribeOutputSchema),
4833
5268
  annotations: liveWebToolAnnotations("Facebook Organic Video Transcription")
@@ -5005,6 +5440,13 @@ function registerPaaExtractorMcpTools(server, executor, options = {}) {
5005
5440
  outputSchema: recordOutputSchema("read_service_connection", ReadServiceConnectionOutputSchema),
5006
5441
  annotations: { title: "Read Connected Service", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }
5007
5442
  }, async (input) => executor.readServiceConnection(input));
5443
+ server.registerTool("meta_ad_creative_media", {
5444
+ title: "View Meta Ad Creative Media",
5445
+ description: "Preferred connected-account path for viewing a Meta ad creative, especially paused or dark ads that may be absent from Ad Library. Given a tenant-owned Meta connectionId and adId, resolves the ad, creative, effective story/post candidate, image assets, video assets, and transient Graph playback source. Bounded creative images are returned as actual MCP image content for vision-capable clients. For video, follow the returned exact nextActions with facebook_ad_transcribe when Graph returned a direct source, facebook_video_transcribe when only a public post/video candidate is available, or video_frame_analysis for visual breakdown. This tool is read-only and does not itself spend transcription/analysis credits.",
5446
+ inputSchema: MetaAdCreativeMediaInputSchema,
5447
+ outputSchema: recordOutputSchema("meta_ad_creative_media", MetaAdCreativeMediaOutputSchema),
5448
+ annotations: { title: "View Meta Ad Creative Media", readOnlyHint: true, destructiveHint: false, idempotentHint: false, openWorldHint: true }
5449
+ }, async (input) => buildMetaAdCreativeMediaResult(executor, input));
5008
5450
  server.registerTool("import_service_connection_to_memory", {
5009
5451
  title: "Import Connected Service Snapshot to Memory",
5010
5452
  description: "Run exactly one bounded, approved read on a tenant-owned connected service and upsert the redacted result into an existing ordinary Memory vault at a server-generated stable path. The saved document is embedded for RAG and marked as untrusted provider data, never instructions. This is a one-result snapshot: it does not paginate, bulk-import an account, continuously sync changes, propagate deletions, or create normalized tables. Use list_service_connections first and supply an exact current readTools entry; action and admin tools are rejected.",
@@ -5233,6 +5675,9 @@ var HttpMcpToolExecutor = class {
5233
5675
  auditSite(input) {
5234
5676
  return this.call("/extract-site", input);
5235
5677
  }
5678
+ checkSiteExport(input) {
5679
+ return this.getJson(`/extract-site/status/${encodeURIComponent(input.jobId)}`);
5680
+ }
5236
5681
  youtubeHarvest(input) {
5237
5682
  return this.call("/youtube/harvest", input);
5238
5683
  }
@@ -9316,6 +9761,8 @@ export {
9316
9761
  sanitizeAttempts,
9317
9762
  sanitizeHarvestResult,
9318
9763
  buildLinkGraph,
9764
+ assessTranscriptSignal,
9765
+ transcribeMediaUrl,
9319
9766
  createConnectedDataArtifact,
9320
9767
  renewConnectedDataArtifactDownload,
9321
9768
  cleanupExpiredConnectedDataArtifacts,
@@ -9332,4 +9779,4 @@ export {
9332
9779
  registerMemoryMcpTools,
9333
9780
  MemoryMcpToolExecutor
9334
9781
  };
9335
- //# sourceMappingURL=chunk-FMEDPBIV.js.map
9782
+ //# sourceMappingURL=chunk-KE7KE2Q2.js.map