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
@@ -5271,18 +5271,30 @@ async function ledgerExistsForStripePI(stripePI) {
5271
5271
  });
5272
5272
  return res.rows.length > 0;
5273
5273
  }
5274
- async function reconcileBalanceMc(userId) {
5274
+ async function reconcileBalanceMc(userId, knownBalanceMc) {
5275
5275
  const db = getDb();
5276
- await ensureMigrated(Number(userId));
5277
5276
  const res = await db.execute({
5278
- sql: `SELECT COALESCE(SUM(remaining_mc), 0) AS balance_mc FROM credit_lots WHERE user_id = ? AND remaining_mc > 0 AND expires_at > datetime('now')`,
5277
+ sql: `SELECT
5278
+ COUNT(*) AS lot_count,
5279
+ COALESCE(SUM(CASE WHEN remaining_mc > 0 AND expires_at > datetime('now') THEN remaining_mc ELSE 0 END), 0) AS balance_mc
5280
+ FROM credit_lots WHERE user_id = ?`,
5279
5281
  args: [userId]
5280
5282
  });
5281
- const balanceMc = Number(res.rows[0]?.balance_mc ?? 0);
5282
- await db.execute({
5283
- sql: "UPDATE users SET balance_mc = ? WHERE id = ? AND balance_mc != ?",
5284
- args: [balanceMc, userId, balanceMc]
5285
- });
5283
+ let balanceMc = Number(res.rows[0]?.balance_mc ?? 0);
5284
+ if (Number(res.rows[0]?.lot_count ?? 0) === 0) {
5285
+ await ensureMigrated(Number(userId));
5286
+ const migrated = await db.execute({
5287
+ sql: `SELECT COALESCE(SUM(remaining_mc), 0) AS balance_mc FROM credit_lots WHERE user_id = ? AND remaining_mc > 0 AND expires_at > datetime('now')`,
5288
+ args: [userId]
5289
+ });
5290
+ balanceMc = Number(migrated.rows[0]?.balance_mc ?? 0);
5291
+ }
5292
+ if (knownBalanceMc !== balanceMc) {
5293
+ await db.execute({
5294
+ sql: "UPDATE users SET balance_mc = ? WHERE id = ? AND balance_mc != ?",
5295
+ args: [balanceMc, userId, balanceMc]
5296
+ });
5297
+ }
5286
5298
  return balanceMc;
5287
5299
  }
5288
5300
  var import_http, import_node_crypto, import_zod, DB_URL, DB_TOKEN, _db, _rateLimitSchemaReady, CREDIT_LOT_TTL, SiteAuditJobRowSchema, SiteAuditPhaseLogRowSchema;
@@ -6372,8 +6384,8 @@ async function downloadAsset(url, destDir, filename) {
6372
6384
  }
6373
6385
  const writer = (0, import_node_fs.createWriteStream)(dest);
6374
6386
  await (0, import_promises2.pipeline)(import_node_stream.Readable.fromWeb(res.body), writer);
6375
- const { statSync: statSync2 } = await import("fs");
6376
- const sizeBytes = statSync2(dest).size;
6387
+ const { statSync: statSync3 } = await import("fs");
6388
+ const sizeBytes = statSync3(dest).size;
6377
6389
  return { savedPath: dest, sizeBytes, mimeType };
6378
6390
  }
6379
6391
  async function harvestPageMedia(html, pageUrl, options = {}) {
@@ -10746,6 +10758,15 @@ function registrableDomain2(host) {
10746
10758
  const parts = h.split(".");
10747
10759
  return parts.length <= 2 ? h : parts.slice(-2).join(".");
10748
10760
  }
10761
+ function safeImageFilename(url, index) {
10762
+ try {
10763
+ const u = new URL(url);
10764
+ const base = (0, import_node_path3.basename)(u.pathname).replace(/[^a-zA-Z0-9._-]/g, "_").slice(0, 80);
10765
+ return base || `image-${index}`;
10766
+ } catch {
10767
+ return `image-${index}`;
10768
+ }
10769
+ }
10749
10770
  function slugFactory() {
10750
10771
  const counts = /* @__PURE__ */ new Map();
10751
10772
  return (url) => {
@@ -10768,7 +10789,9 @@ async function* pageChunks(jobId) {
10768
10789
  }
10769
10790
  async function assembleExtractArtifacts(job, extras = {}) {
10770
10791
  const dir = (0, import_node_path3.join)((0, import_node_os3.tmpdir)(), `extract-bundle-${job.id}-${Date.now()}`);
10792
+ const downloadImages = job.options.downloadImages === true;
10771
10793
  (0, import_node_fs3.mkdirSync)((0, import_node_path3.join)(dir, "pages"), { recursive: true });
10794
+ if (downloadImages) (0, import_node_fs3.mkdirSync)((0, import_node_path3.join)(dir, "images"), { recursive: true });
10772
10795
  try {
10773
10796
  const metas = [];
10774
10797
  const statusByUrl = /* @__PURE__ */ new Map();
@@ -10802,6 +10825,10 @@ async function assembleExtractArtifacts(job, extras = {}) {
10802
10825
  } catch {
10803
10826
  siteReg = "";
10804
10827
  }
10828
+ const imageLimit = (0, import_p_limit3.default)(IMAGE_DOWNLOAD_CONCURRENCY);
10829
+ const imageDownloads = [];
10830
+ let imagesQueued = 0;
10831
+ let imagesFailed = 0;
10805
10832
  for await (const chunk of pageChunks(job.id)) {
10806
10833
  for (const p of chunk) {
10807
10834
  if (p.bodyMarkdown) {
@@ -10819,6 +10846,20 @@ ${p.bodyMarkdown}
10819
10846
  `
10820
10847
  );
10821
10848
  }
10849
+ if (downloadImages && p.imageLinks?.length && imagesQueued < MAX_IMAGES_PER_SITE) {
10850
+ const pageDir = (0, import_node_path3.join)(dir, "images", slug(p.url));
10851
+ (0, import_node_fs3.mkdirSync)(pageDir, { recursive: true });
10852
+ for (const [idx, imgUrl] of p.imageLinks.slice(0, MAX_IMAGES_PER_PAGE).entries()) {
10853
+ if (imagesQueued >= MAX_IMAGES_PER_SITE) break;
10854
+ imagesQueued++;
10855
+ const filename = safeImageFilename(imgUrl, idx);
10856
+ imageDownloads.push(
10857
+ imageLimit(() => downloadAsset(imgUrl, pageDir, filename)).then(() => void 0, () => {
10858
+ imagesFailed++;
10859
+ })
10860
+ );
10861
+ }
10862
+ }
10822
10863
  const from = normalize2(p.url);
10823
10864
  const oc = { int: 0, ext: 0 };
10824
10865
  for (const l of p.outlinks ?? []) {
@@ -10866,6 +10907,7 @@ ${p.bodyMarkdown}
10866
10907
  }
10867
10908
  gz.end();
10868
10909
  await (0, import_node_events.once)(linksOut, "finish");
10910
+ await Promise.all(imageDownloads);
10869
10911
  const depth = /* @__PURE__ */ new Map();
10870
10912
  const start = normalize2(job.startUrl);
10871
10913
  depth.set(start, 0);
@@ -10957,6 +10999,15 @@ ${renderImageSection(extras.imageAudit)}`;
10957
10999
  }
10958
11000
  if (extras.seoAudit) (0, import_node_fs3.writeFileSync)((0, import_node_path3.join)(dir, "seo-audit.json"), JSON.stringify(extras.seoAudit, null, 2));
10959
11001
  if (extras.branding) (0, import_node_fs3.writeFileSync)((0, import_node_path3.join)(dir, "branding.json"), JSON.stringify(extras.branding, null, 2));
11002
+ if (downloadImages) {
11003
+ (0, import_node_fs3.writeFileSync)((0, import_node_path3.join)(dir, "images-download-summary.json"), JSON.stringify({
11004
+ queued: imagesQueued,
11005
+ downloaded: imagesQueued - imagesFailed,
11006
+ failed: imagesFailed,
11007
+ perPageCap: MAX_IMAGES_PER_PAGE,
11008
+ perSiteCap: MAX_IMAGES_PER_SITE
11009
+ }, null, 2));
11010
+ }
10960
11011
  const artifactFiles = [
10961
11012
  { name: "report.md", type: "text/markdown" },
10962
11013
  { name: "issues.json", type: "application/json" },
@@ -10973,13 +11024,20 @@ ${renderImageSection(extras.imageAudit)}`;
10973
11024
  }
10974
11025
  if (extras.seoAudit) artifactFiles.push({ name: "seo-audit.json", type: "application/json" });
10975
11026
  if (extras.branding) artifactFiles.push({ name: "branding.json", type: "application/json" });
11027
+ if (downloadImages) artifactFiles.push({ name: "images-download-summary.json", type: "application/json" });
10976
11028
  const zip = new import_yazl.ZipFile();
10977
11029
  const zipOut = (0, import_node_fs3.createWriteStream)((0, import_node_path3.join)(dir, "bundle.zip"));
10978
11030
  zip.outputStream.pipe(zipOut);
10979
11031
  for (const f of artifactFiles) zip.addFile((0, import_node_path3.join)(dir, f.name), f.name);
10980
11032
  if (pagesWithBody > 0) {
10981
- const { readdirSync: readdirSync2 } = await import("fs");
10982
- for (const f of readdirSync2((0, import_node_path3.join)(dir, "pages"))) zip.addFile((0, import_node_path3.join)(dir, "pages", f), `pages/${f}`);
11033
+ for (const f of (0, import_node_fs3.readdirSync)((0, import_node_path3.join)(dir, "pages"))) zip.addFile((0, import_node_path3.join)(dir, "pages", f), `pages/${f}`);
11034
+ }
11035
+ if (downloadImages && imagesQueued > imagesFailed) {
11036
+ const imagesDir = (0, import_node_path3.join)(dir, "images");
11037
+ for (const rel of (0, import_node_fs3.readdirSync)(imagesDir, { recursive: true })) {
11038
+ const full = (0, import_node_path3.join)(imagesDir, rel);
11039
+ if ((0, import_node_fs3.statSync)(full).isFile()) zip.addFile(full, `images/${rel.split("\\").join("/")}`);
11040
+ }
10983
11041
  }
10984
11042
  zip.end();
10985
11043
  await (0, import_node_events.once)(zipOut, "finish");
@@ -10993,7 +11051,7 @@ ${renderImageSection(extras.imageAudit)}`;
10993
11051
  (0, import_node_fs3.rmSync)(dir, { recursive: true, force: true });
10994
11052
  }
10995
11053
  }
10996
- var import_node_fs3, import_node_path3, import_node_os3, import_node_zlib, import_node_events, import_yazl, CHUNK;
11054
+ var import_node_fs3, import_node_path3, import_node_os3, import_node_zlib, import_node_events, import_yazl, import_p_limit3, CHUNK, MAX_IMAGES_PER_PAGE, MAX_IMAGES_PER_SITE, IMAGE_DOWNLOAD_CONCURRENCY;
10997
11055
  var init_extract_bundle = __esm({
10998
11056
  "src/api/extract-bundle.ts"() {
10999
11057
  "use strict";
@@ -11003,12 +11061,17 @@ var init_extract_bundle = __esm({
11003
11061
  import_node_zlib = require("zlib");
11004
11062
  import_node_events = require("events");
11005
11063
  import_yazl = require("yazl");
11064
+ import_p_limit3 = __toESM(require("p-limit"), 1);
11006
11065
  init_db();
11007
11066
  init_blob_store();
11008
11067
  init_seo_issues();
11009
11068
  init_seo_link_report();
11010
11069
  init_image_audit();
11070
+ init_media_extractor();
11011
11071
  CHUNK = 400;
11072
+ MAX_IMAGES_PER_PAGE = 20;
11073
+ MAX_IMAGES_PER_SITE = 500;
11074
+ IMAGE_DOWNLOAD_CONCURRENCY = 8;
11012
11075
  }
11013
11076
  });
11014
11077
 
@@ -13647,6 +13710,39 @@ var init_audio_resolver = __esm({
13647
13710
  });
13648
13711
 
13649
13712
  // src/services/media-transcription.ts
13713
+ function transcriptWordCount(text) {
13714
+ return text.trim() ? text.trim().split(/\s+/).length : 0;
13715
+ }
13716
+ function assessTranscriptSignal(text, chunks, durationHintSec) {
13717
+ const words = transcriptWordCount(text);
13718
+ const chunkDurationSec = chunks.reduce((max, chunk) => {
13719
+ const end = Number(chunk.timestamp?.[1]);
13720
+ return Number.isFinite(end) && end > max ? end : max;
13721
+ }, 0);
13722
+ const mediaDurationSec = typeof durationHintSec === "number" && Number.isFinite(durationHintSec) && durationHintSec > 0 ? durationHintSec : chunkDurationSec > 0 ? chunkDurationSec : null;
13723
+ const wordsPerMinute = mediaDurationSec && mediaDurationSec > 0 ? Math.round(words / mediaDurationSec * 600) / 10 : null;
13724
+ const status = words === 0 ? "empty" : words < 5 ? "low_speech_signal" : "speech_detected";
13725
+ const retryRecommended = words <= 2;
13726
+ const warnings = [];
13727
+ if (status === "empty") {
13728
+ warnings.push("No speech was transcribed. This does not distinguish silent media from an inaccessible or expired playback URL.");
13729
+ } else if (status === "low_speech_signal") {
13730
+ 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.");
13731
+ }
13732
+ if (retryRecommended) {
13733
+ warnings.push("For a connected Meta ad, resolve the creative effectiveObjectStoryId and retry with facebook_video_transcribe when a public post URL is available.");
13734
+ }
13735
+ return {
13736
+ status,
13737
+ speechDetected: status === "speech_detected",
13738
+ confidence: status === "speech_detected" ? "medium" : "low",
13739
+ basis: "transcript_word_count_and_timing",
13740
+ mediaDurationSec,
13741
+ wordsPerMinute,
13742
+ retryRecommended,
13743
+ warnings
13744
+ };
13745
+ }
13650
13746
  function transcriptMarkdown(title, text, chunks, durationMs) {
13651
13747
  const fmtTs2 = (s) => `${Math.floor(s / 60)}:${String(Math.floor(s % 60)).padStart(2, "0")}`;
13652
13748
  const lines = [title, "", `*Transcribed in ${(durationMs / 1e3).toFixed(1)}s*`, "", "## Full Text", "", text, ""];
@@ -13676,7 +13772,8 @@ async function transcribeMediaUrl(mediaUrl, markdownTitle = "# Media Transcript"
13676
13772
  chunks,
13677
13773
  durationMs,
13678
13774
  costUsd,
13679
- markdown: transcriptMarkdown(markdownTitle, text, chunks, durationMs)
13775
+ markdown: transcriptMarkdown(markdownTitle, text, chunks, durationMs),
13776
+ transcriptSignal: assessTranscriptSignal(text, chunks)
13680
13777
  };
13681
13778
  }
13682
13779
  var import_client3;
@@ -14015,6 +14112,7 @@ var init_server_schemas = __esm({
14015
14112
  rotateProxies: import_zod13.z.boolean().optional(),
14016
14113
  rotateProxyEvery: import_zod13.z.number().int().min(1).max(100).optional(),
14017
14114
  background: import_zod13.z.boolean().optional(),
14115
+ downloadImages: import_zod13.z.boolean().optional(),
14018
14116
  formats: import_zod13.z.array(import_zod13.z.enum(["markdown", "links", "json", "images", "branding", "issues"])).optional()
14019
14117
  });
14020
14118
  YoutubeHarvestBodySchema = import_zod13.z.object({
@@ -14969,11 +15067,13 @@ var init_FacebookAdExtractor = __esm({
14969
15067
  if (n) nameFreq.set(n, (nameFreq.get(n) ?? 0) + 1);
14970
15068
  }
14971
15069
  const advertiserName = nameFreq.size ? [...nameFreq.entries()].sort((a, b) => b[1] - a[1])[0][0] : null;
15070
+ const matchedAdvertisers = [...nameFreq.entries()].map(([name, adCount]) => ({ name, adCount })).sort((a, b) => b.adCount - a.adCount).slice(0, 20);
14972
15071
  return {
14973
15072
  listingUrl,
14974
15073
  scannedAt: (/* @__PURE__ */ new Date()).toISOString(),
14975
15074
  advertiserPageId,
14976
15075
  advertiserName,
15076
+ matchedAdvertisers,
14977
15077
  summary: {
14978
15078
  totalAds,
14979
15079
  activeCount,
@@ -15496,6 +15596,45 @@ function buildPageIntelUrl(body, country) {
15496
15596
  if (body.pageId?.trim()) return `https://www.facebook.com/ads/library/?active_status=all&ad_type=all&country=${country}&is_targeted_country=false&media_type=all&search_type=page&view_all_page_id=${body.pageId.trim()}`;
15497
15597
  return `https://www.facebook.com/ads/library/?active_status=all&ad_type=all&country=${country}&q=${encodeURIComponent(body.query.trim())}&search_type=keyword_unordered`;
15498
15598
  }
15599
+ function normalizeAdvertiserName(value) {
15600
+ return value.toLowerCase().normalize("NFKD").replace(/[^a-z0-9]+/g, " ").trim().replace(/\s+/g, " ");
15601
+ }
15602
+ function advertiserTokenOverlap(query, candidate) {
15603
+ const queryTokens = new Set(normalizeAdvertiserName(query).split(" ").filter(Boolean));
15604
+ const candidateTokens = new Set(normalizeAdvertiserName(candidate).split(" ").filter(Boolean));
15605
+ if (queryTokens.size === 0 || candidateTokens.size === 0) return 0;
15606
+ let overlap = 0;
15607
+ for (const token of queryTokens) if (candidateTokens.has(token)) overlap++;
15608
+ return overlap / queryTokens.size;
15609
+ }
15610
+ function assessPageIntelMatch(body, result) {
15611
+ if (body.pageId) return { inputMode: "pageId", matchConfidence: "high", matchReason: "Exact Facebook page ID filter.", warnings: [] };
15612
+ if (body.libraryId) return { inputMode: "libraryId", matchConfidence: "high", matchReason: "Exact Ad Library archive ID filter.", warnings: [] };
15613
+ const query = body.query?.trim() ?? "";
15614
+ const normalizedQuery = normalizeAdvertiserName(query);
15615
+ const matches = result.matchedAdvertisers ?? [];
15616
+ const exact = matches.find((match) => normalizeAdvertiserName(match.name) === normalizedQuery);
15617
+ const top = matches[0];
15618
+ const topShare = top && result.summary.totalAds > 0 ? top.adCount / result.summary.totalAds : 0;
15619
+ const overlap = top ? advertiserTokenOverlap(query, top.name) : 0;
15620
+ const warnings = [];
15621
+ let matchConfidence;
15622
+ let matchReason;
15623
+ if (exact && exact.adCount / Math.max(1, result.summary.totalAds) >= 0.7) {
15624
+ matchConfidence = "high";
15625
+ matchReason = "The exact advertiser name accounts for most returned ads.";
15626
+ } else if (top && overlap >= 0.75 && topShare >= 0.5) {
15627
+ matchConfidence = "medium";
15628
+ matchReason = "The leading advertiser overlaps strongly with the query, but keyword search is not page-scoped.";
15629
+ } else {
15630
+ matchConfidence = "low";
15631
+ matchReason = "Keyword search did not identify one dominant exact advertiser match.";
15632
+ }
15633
+ if (matches.length > 1) warnings.push(`Keyword search mixed ${matches.length} advertiser names. Confirm the intended pageId before using these ads for performance or creative analysis.`);
15634
+ if (matchConfidence === "low") warnings.push("Low-specificity Ad Library match: treat these results as discovery candidates, not as verified ads from the requested brand.");
15635
+ warnings.push("Paused, cancelled, or otherwise inactive non-political ads may be absent from Ad Library. Use meta_ad_creative_media for connected ad-account creatives.");
15636
+ return { inputMode: "query", matchConfidence, matchReason, warnings };
15637
+ }
15499
15638
  function configuredKernelProxy() {
15500
15639
  return (process.env.BROWSER_SERVICE_PROXY_ID ?? process.env.KERNEL_PROXY_ID)?.trim() || void 0;
15501
15640
  }
@@ -15643,8 +15782,10 @@ var init_facebook_ad_routes = __esm({
15643
15782
  await logRequestEvent({ userId: fbUser.id, source: "facebook_page_intel", status: "failed", query: body.pageId ?? body.query ?? body.libraryId ?? "", error: "soft-block: empty result refunded" });
15644
15783
  return c.json({ error: "soft-block: no ads returned (refunded)" }, 503);
15645
15784
  }
15646
- await logRequestEvent({ userId: fbUser.id, source: "facebook_page_intel", status: "done", query: body.pageId ?? body.query ?? body.libraryId ?? "", resultCount: result.ads.length, result });
15647
- return c.json(result);
15785
+ const match = assessPageIntelMatch(body, result);
15786
+ const output = { ...result, ...match };
15787
+ await logRequestEvent({ userId: fbUser.id, source: "facebook_page_intel", status: "done", query: body.pageId ?? body.query ?? body.libraryId ?? "", resultCount: result.ads.length, result: output });
15788
+ return c.json(output);
15648
15789
  } catch (err) {
15649
15790
  const msg = err instanceof Error ? err.message : String(err);
15650
15791
  if (debited && !refunded) await creditMc(fbUser.id, MC_COSTS.fb_ad, LedgerOperation.FB_AD_REFUND, "failed call");
@@ -15745,7 +15886,8 @@ var init_facebook_ad_routes = __esm({
15745
15886
  text: transcript.text,
15746
15887
  chunks: transcript.chunks,
15747
15888
  durationMs: transcript.durationMs,
15748
- markdown: transcript.markdown
15889
+ markdown: transcript.markdown,
15890
+ transcriptSignal: assessTranscriptSignal(transcript.text, transcript.chunks, video.durationSec)
15749
15891
  };
15750
15892
  await logRequestEvent({ userId: fbUser.id, source: "facebook_video_transcribe", status: "done", query: sourceUrl.href, resultCount: transcript.chunks.length, result });
15751
15893
  return c.json(result);
@@ -19957,9 +20099,24 @@ function buildSeoExport(siteUrl, pages, branding) {
19957
20099
  branding: branding ?? void 0
19958
20100
  };
19959
20101
  }
20102
+ function formatBackgroundJobStarted(toolLabel, data) {
20103
+ if (data.status !== "pending" || typeof data.jobId !== "string") return null;
20104
+ const jobId = data.jobId;
20105
+ const full = [
20106
+ `# ${toolLabel} \u2014 export started`,
20107
+ `**Job ID:** \`${jobId}\``,
20108
+ `
20109
+ Running in the background \u2014 this can take a while for large sites.`,
20110
+ `
20111
+ Poll \`check_site_export\` with this jobId to get the download link once it's ready.`
20112
+ ].join("\n");
20113
+ return { content: [{ type: "text", text: full }], structuredContent: { jobId, status: "pending" } };
20114
+ }
19960
20115
  async function formatExtractSite(raw, input, ctx) {
19961
20116
  const parsed = parseData(raw);
19962
20117
  if ("error" in parsed) return { content: [{ type: "text", text: parsed.error }], isError: true };
20118
+ const started = formatBackgroundJobStarted("Multi-Page Site Content Crawl", parsed.data);
20119
+ if (started) return started;
19963
20120
  const d = parsed.data;
19964
20121
  const pages = d.pages ?? [];
19965
20122
  const schemaTypesOf = (p) => p.kpo?.type ?? (Array.isArray(p.schema) && p.schema.length ? [`${p.schema.length} block(s)`] : []);
@@ -20077,6 +20234,8 @@ ${pageDetails}${tips}`;
20077
20234
  async function formatAuditSite(raw, input, ctx) {
20078
20235
  const parsed = parseData(raw);
20079
20236
  if ("error" in parsed) return { content: [{ type: "text", text: parsed.error }], isError: true };
20237
+ const started = formatBackgroundJobStarted("Technical SEO Audit", parsed.data);
20238
+ if (started) return started;
20080
20239
  const d = parsed.data;
20081
20240
  const pages = d.pages ?? [];
20082
20241
  const schemaTypesOf = (p) => p.kpo?.type ?? (Array.isArray(p.schema) && p.schema.length ? [`${p.schema.length} block(s)`] : []);
@@ -20148,6 +20307,42 @@ ${imgLine}`
20148
20307
  ].join("\n");
20149
20308
  return { content: [{ type: "text", text: full }], structuredContent };
20150
20309
  }
20310
+ function formatCheckSiteExport(raw, input) {
20311
+ const parsed = parseData(raw);
20312
+ if ("error" in parsed) return { content: [{ type: "text", text: parsed.error }], isError: true };
20313
+ const d = parsed.data;
20314
+ const bundle = (d.artifacts ?? []).find((a) => a.key.endsWith("bundle.zip")) ?? null;
20315
+ const progress = d.totalUrls ? `${d.doneUrls ?? 0}/${d.totalUrls} pages` : "starting";
20316
+ const body = d.status === "complete" && bundle ? `
20317
+ ## \u2705 Ready
20318
+ **Download:** ${bundle.url}
20319
+ **Size:** ${(bundle.bytes / 1e6).toFixed(1)} MB` : d.status === "complete" ? `
20320
+ ## \u26A0\uFE0F Complete, but no bundle was produced
20321
+ Check the job's artifacts \u2014 nothing matched \`bundle.zip\`.` : d.status === "failed" ? `
20322
+ ## \u274C Failed
20323
+ ${d.error ?? "no error message recorded"}` : `
20324
+ ## \u23F3 Not ready yet
20325
+ Status: ${d.status} (${progress}). Poll again shortly.`;
20326
+ const full = [
20327
+ `# Site Export: ${d.startUrl ?? input.jobId}`,
20328
+ `**Job ID:** \`${d.jobId}\``,
20329
+ `**Status:** ${d.status}`,
20330
+ body
20331
+ ].join("\n");
20332
+ return {
20333
+ ...oneBlock(full),
20334
+ structuredContent: {
20335
+ jobId: d.jobId,
20336
+ status: d.status,
20337
+ startUrl: d.startUrl,
20338
+ totalUrls: d.totalUrls,
20339
+ doneUrls: d.doneUrls,
20340
+ bundleUrl: bundle?.url ?? null,
20341
+ bundleBytes: bundle?.bytes ?? null,
20342
+ error: d.error ?? null
20343
+ }
20344
+ };
20345
+ }
20151
20346
  function formatYoutubeHarvest(raw, input) {
20152
20347
  const parsed = parseData(raw);
20153
20348
  if ("error" in parsed) return { content: [{ type: "text", text: parsed.error }], isError: true };
@@ -20257,6 +20452,7 @@ function formatFacebookPageIntel(raw, input) {
20257
20452
  const advertiser = d.advertiserName ?? input.query ?? input.pageId ?? input.libraryId ?? "Advertiser";
20258
20453
  const ads = d.ads ?? [];
20259
20454
  const s = d.summary ?? { totalAds: 0, activeCount: 0, videoCount: 0, imageCount: 0 };
20455
+ const warnings = d.warnings ?? [];
20260
20456
  const adBlocks = ads.map((ad, i) => [
20261
20457
  `### Ad ${i + 1}${ad.libraryId ? ` \xB7 \`${ad.libraryId}\`` : ""} \u2014 ${ad.status ?? "\u2014"} \xB7 ${ad.creativeType ?? "\u2014"} \xB7 ${ad.startDate ?? ad.started ?? "\u2014"}`,
20262
20458
  ad.headline ? `**Headline:** ${ad.headline}` : "",
@@ -20269,19 +20465,28 @@ function formatFacebookPageIntel(raw, input) {
20269
20465
  const full = [
20270
20466
  `# Facebook Ad Intel: ${advertiser}`,
20271
20467
  `**${s.totalAds} ads** \xB7 ${s.activeCount} active \xB7 ${s.videoCount} video \xB7 ${s.imageCount} image`,
20468
+ d.matchConfidence ? `**Match confidence:** ${d.matchConfidence}${d.matchReason ? ` \u2014 ${d.matchReason}` : ""}` : "",
20469
+ warnings.length ? `
20470
+ > \u26A0\uFE0F ${warnings.join("\n> \u26A0\uFE0F ")}` : "",
20272
20471
  `
20273
20472
  ${adBlocks}`,
20274
20473
  `
20275
20474
  ---
20276
20475
  \u{1F4A1} **Tips**
20277
- - Transcribe video ads: use \`facebook_ad_transcribe\` with the direct \`videoUrl\` above
20278
- - Transcribe organic Facebook reels/posts: use \`facebook_video_transcribe\` with the public Facebook URL
20279
- - Find other advertisers: use \`facebook_ad_search\``
20476
+ - Connected, paused, or dark ad: use \`meta_ad_creative_media\` with its adId
20477
+ - Ad Library direct video URL: use \`facebook_ad_transcribe\` immediately
20478
+ - Public Facebook post/reel URL: use \`facebook_video_transcribe\`
20479
+ - Find the exact advertiser pageId first: use \`facebook_ad_search\``
20280
20480
  ].filter(Boolean).join("\n");
20281
20481
  return {
20282
20482
  ...oneBlock(full),
20283
20483
  structuredContent: {
20284
20484
  advertiserName: d.advertiserName ?? null,
20485
+ inputMode: d.inputMode ?? (input.pageId ? "pageId" : input.libraryId ? "libraryId" : "query"),
20486
+ matchConfidence: d.matchConfidence ?? (input.query ? "low" : "high"),
20487
+ matchReason: d.matchReason ?? (input.query ? "Keyword search result was not confidence-scored by the server." : "Exact identifier filter."),
20488
+ warnings,
20489
+ matchedAdvertisers: d.matchedAdvertisers ?? [],
20285
20490
  totalAds: s.totalAds ?? 0,
20286
20491
  activeCount: s.activeCount ?? 0,
20287
20492
  videoCount: s.videoCount ?? 0,
@@ -21210,6 +21415,7 @@ function formatFacebookAdTranscribe(raw, input) {
21210
21415
  const chunks = d.chunks ?? [];
21211
21416
  const durSec = d.durationMs ? (d.durationMs / 1e3).toFixed(0) : "\u2014";
21212
21417
  const words = wordCount(text);
21418
+ const signal = d.transcriptSignal ?? assessTranscriptSignal(text, chunks);
21213
21419
  const chunkRows = chunks.slice(0, 50).map((c) => {
21214
21420
  const sec = Number.isFinite(c.timestamp[0]) ? Math.floor(c.timestamp[0]) : 0;
21215
21421
  const mm = String(Math.floor(sec / 60)).padStart(2, "0");
@@ -21218,7 +21424,9 @@ function formatFacebookAdTranscribe(raw, input) {
21218
21424
  }).join("\n");
21219
21425
  const full = [
21220
21426
  `# Facebook Ad Transcript`,
21221
- `**Duration:** ${durSec}s \xB7 **${words} words**`,
21427
+ `**Transcription elapsed:** ${durSec}s \xB7 **${words} words** \xB7 **Signal:** ${signal.status}`,
21428
+ signal.warnings.length ? `
21429
+ > \u26A0\uFE0F ${signal.warnings.join("\n> \u26A0\uFE0F ")}` : "",
21222
21430
  `
21223
21431
  ## Full Transcript
21224
21432
  ${text}`,
@@ -21240,6 +21448,7 @@ ${chunkRows}` : "",
21240
21448
  durationMs: typeof d.durationMs === "number" ? d.durationMs : null,
21241
21449
  transcriptText: text,
21242
21450
  chunks: structuredTranscriptChunks(chunks),
21451
+ transcriptSignal: signal,
21243
21452
  resolvedInputs: {
21244
21453
  videoUrl: input.videoUrl
21245
21454
  }
@@ -21256,6 +21465,7 @@ function formatFacebookVideoTranscribe(raw, input) {
21256
21465
  const durSec = d.durationMs ? (d.durationMs / 1e3).toFixed(0) : "\u2014";
21257
21466
  const videoDuration = typeof d.videoDurationSec === "number" ? `${Math.round(d.videoDurationSec)}s` : "\u2014";
21258
21467
  const label = d.videoId ? `Facebook Organic Video \`${d.videoId}\`` : "Facebook Organic Video";
21468
+ const signal = d.transcriptSignal ?? assessTranscriptSignal(text, chunks, d.videoDurationSec);
21259
21469
  const chunkRows = chunks.slice(0, 50).map((c) => {
21260
21470
  const sec = Number.isFinite(c.timestamp[0]) ? Math.floor(c.timestamp[0]) : 0;
21261
21471
  const mm = String(Math.floor(sec / 60)).padStart(2, "0");
@@ -21266,6 +21476,9 @@ function formatFacebookVideoTranscribe(raw, input) {
21266
21476
  `# ${label} Transcript`,
21267
21477
  d.ownerName ? `**Owner:** ${d.ownerName}` : "",
21268
21478
  `**Video duration:** ${videoDuration} \xB7 **Transcribed in:** ${durSec}s \xB7 **${wordCount2} words**`,
21479
+ `**Transcript signal:** ${signal.status} (${signal.confidence} heuristic confidence)`,
21480
+ signal.warnings.length ? `
21481
+ > \u26A0\uFE0F ${signal.warnings.join("\n> \u26A0\uFE0F ")}` : "",
21269
21482
  d.pageUrl ? `**Page URL:** ${d.pageUrl}` : `**Page URL:** ${input.url}`,
21270
21483
  d.videoUrl ? `**Extracted MP4:** \`${d.videoUrl}\`` : "",
21271
21484
  `
@@ -21298,7 +21511,8 @@ ${chunkRows}` : "",
21298
21511
  startSec: Number.isFinite(c.timestamp[0]) ? c.timestamp[0] : 0,
21299
21512
  endSec: Number.isFinite(c.timestamp[1]) ? c.timestamp[1] : 0,
21300
21513
  text: c.text
21301
- }))
21514
+ })),
21515
+ transcriptSignal: signal
21302
21516
  }
21303
21517
  };
21304
21518
  }
@@ -21614,6 +21828,7 @@ var init_mcp_response_formatter = __esm({
21614
21828
  init_image_audit();
21615
21829
  init_report_artifact_offload();
21616
21830
  init_rates();
21831
+ init_media_transcription();
21617
21832
  INLINE_BUDGET_BYTES = Number(process.env.MCP_SCRAPER_INLINE_BUDGET ?? 5e4);
21618
21833
  STRUCTURED_ARRAY_CAP = 25;
21619
21834
  reportSavingEnabled = true;
@@ -24945,7 +25160,7 @@ async function capturePageSnapshot(target, options = {}) {
24945
25160
  async function capturePageSnapshots(targets, options = {}) {
24946
25161
  const timeoutMs = normalizeTimeoutMs(options.timeoutMs);
24947
25162
  const maxConcurrency = normalizeMaxConcurrency(options.maxConcurrency);
24948
- const limit = (0, import_p_limit3.default)(maxConcurrency);
25163
+ const limit = (0, import_p_limit4.default)(maxConcurrency);
24949
25164
  const pageSnapshotArtifacts = await Promise.all(
24950
25165
  targets.map((target) => limit(() => capturePageSnapshot(target, { ...options, timeoutMs })))
24951
25166
  );
@@ -24970,12 +25185,12 @@ async function capturePageSnapshots(targets, options = {}) {
24970
25185
  }
24971
25186
  };
24972
25187
  }
24973
- var import_node_crypto10, import_p_limit3, DEFAULT_TIMEOUT_MS, DEFAULT_MAX_CONCURRENCY, DEFAULT_MAX_CONTENT_CHARS;
25188
+ var import_node_crypto10, import_p_limit4, DEFAULT_TIMEOUT_MS, DEFAULT_MAX_CONCURRENCY, DEFAULT_MAX_CONTENT_CHARS;
24974
25189
  var init_page_snapshot_extractor = __esm({
24975
25190
  "src/serp-intelligence/page-snapshot-extractor.ts"() {
24976
25191
  "use strict";
24977
25192
  import_node_crypto10 = require("crypto");
24978
- import_p_limit3 = __toESM(require("p-limit"), 1);
25193
+ import_p_limit4 = __toESM(require("p-limit"), 1);
24979
25194
  init_kpo_extractor();
24980
25195
  init_url_utils();
24981
25196
  DEFAULT_TIMEOUT_MS = 15e3;
@@ -27642,7 +27857,7 @@ var PACKAGE_VERSION;
27642
27857
  var init_version = __esm({
27643
27858
  "src/version.ts"() {
27644
27859
  "use strict";
27645
- PACKAGE_VERSION = "0.18.0";
27860
+ PACKAGE_VERSION = "0.20.0";
27646
27861
  }
27647
27862
  });
27648
27863
 
@@ -27682,10 +27897,14 @@ seam is noted so you can chain them.
27682
27897
 
27683
27898
  ## Facebook
27684
27899
  - Find advertisers -> **facebook_ad_search** (returns \`advertisers[]\` with pageId, libraryId).
27685
- - One advertiser's ads -> **facebook_page_intel** (takes pageId, libraryId, or query; returns
27686
- \`ads[].videoUrl\`).
27687
- - Transcribe an ad video -> **facebook_ad_transcribe** (takes a videoUrl from \`facebook_page_intel\`).
27688
- - Transcribe an organic reel/video/post -> **facebook_video_transcribe** (takes the Facebook url directly).
27900
+ - Public Ad Library scan -> **facebook_page_intel**. Prefer pageId/libraryId. Query mode is broad discovery,
27901
+ can mix advertisers, and must be checked through matchConfidence/warnings. Paused non-political ads may
27902
+ be absent even when the connected ad account still has them.
27903
+ - Connected ad-account creative -> **meta_ad_creative_media** (takes connectionId + adId). This is the
27904
+ preferred paused/dark-ad path and returns actual image content plus exact video follow-up arguments.
27905
+ - Direct Graph/Ad Library CDN video -> **facebook_ad_transcribe**. Use transient source URLs immediately.
27906
+ - Public reel/video/post -> **facebook_video_transcribe**. Use the public candidate returned by
27907
+ \`meta_ad_creative_media\` when Graph did not provide a direct source; dark posts may not resolve publicly.
27689
27908
 
27690
27909
  ## Google Ads (Ads Transparency Center)
27691
27910
  - Find advertisers by domain or brand -> **google_ads_search** (returns \`advertisers[]\` with advertiserId).
@@ -27727,8 +27946,8 @@ Multi-step orchestrations \u2014 prefer these over hand-chaining primitives when
27727
27946
  recording) locates a DOM target and returns a ready-to-use timed annotation -> **browser_replay_stop**
27728
27947
  ends it -> feed collected annotations to **browser_replay_annotate**, or just
27729
27948
  **browser_replay_download** the plain MP4. **browser_list_replays** recovers replay ids.
27730
- - Video breakdown is async: **video_frame_analysis** takes a DIRECT media file URL (resolve
27731
- YouTube/Facebook/Instagram page URLs to one first, e.g. via \`facebook_page_intel\`'s \`videoUrl\`) and
27949
+ - Video breakdown is async: **video_frame_analysis** takes a supported public video page or direct media URL
27950
+ (for connected Meta ads, use the exact sourceUrl/public candidate returned by \`meta_ad_creative_media\`) and
27732
27951
  returns a \`runId\` immediately -> poll **video_frame_analysis_status** with that \`runId\` until \`status\`
27733
27952
  is \`done\`.
27734
27953
 
@@ -27737,6 +27956,8 @@ Multi-step orchestrations \u2014 prefer these over hand-chaining primitives when
27737
27956
  sites. It returns a saved folder/artifact plus a summary, not the full content inline.
27738
27957
  - Browser sessions and media transcription cost credits and take longer \u2014 prefer the cheapest tool that
27739
27958
  answers the question (plain search/extract before browser agents).
27959
+ - Use the hosted browser as a controlled resolver for validated public Facebook post/reel redirects only
27960
+ when connected Graph media did not provide a playable source. It is not a bypass for URL/SSRF restrictions.
27740
27961
  - Large results are saved to disk or an artifact and returned as a summary plus a path or artifactId;
27741
27962
  read it back for full detail rather than expecting the whole payload inline.
27742
27963
 
@@ -27814,8 +28035,216 @@ var init_output_schema_registry = __esm({
27814
28035
  }
27815
28036
  });
27816
28037
 
28038
+ // src/mcp/meta-ad-creative-media.ts
28039
+ function asRecord(value) {
28040
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
28041
+ }
28042
+ function parseToolPayload(result) {
28043
+ const structured = asRecord(result.structuredContent);
28044
+ if (structured) return structured;
28045
+ for (const block of result.content ?? []) {
28046
+ if (block.type !== "text") continue;
28047
+ try {
28048
+ const parsed = JSON.parse(block.text);
28049
+ const row = asRecord(parsed);
28050
+ if (row) return row;
28051
+ } catch {
28052
+ continue;
28053
+ }
28054
+ }
28055
+ return null;
28056
+ }
28057
+ function allowedMetaImageUrl(raw) {
28058
+ try {
28059
+ const url = new URL(raw);
28060
+ if (url.protocol !== "https:") return null;
28061
+ const host = url.hostname.toLowerCase().replace(/\.$/, "");
28062
+ if (!META_IMAGE_HOSTS.some((suffix2) => host === suffix2 || host.endsWith(`.${suffix2}`))) return null;
28063
+ return url;
28064
+ } catch {
28065
+ return null;
28066
+ }
28067
+ }
28068
+ function detectImageMime(bytes) {
28069
+ if (bytes.length >= 3 && bytes[0] === 255 && bytes[1] === 216 && bytes[2] === 255) return "image/jpeg";
28070
+ 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";
28071
+ if (bytes.length >= 12 && String.fromCharCode(...bytes.slice(0, 4)) === "RIFF" && String.fromCharCode(...bytes.slice(8, 12)) === "WEBP") return "image/webp";
28072
+ if (bytes.length >= 6) {
28073
+ const signature = String.fromCharCode(...bytes.slice(0, 6));
28074
+ if (signature === "GIF87a" || signature === "GIF89a") return "image/gif";
28075
+ }
28076
+ return null;
28077
+ }
28078
+ async function readBoundedBody(response, maxBytes) {
28079
+ const contentLength = Number(response.headers.get("content-length") ?? "");
28080
+ if (Number.isFinite(contentLength) && contentLength > maxBytes) throw new Error("image_too_large");
28081
+ if (!response.body) {
28082
+ const bytes2 = new Uint8Array(await response.arrayBuffer());
28083
+ if (bytes2.byteLength > maxBytes) throw new Error("image_too_large");
28084
+ return bytes2;
28085
+ }
28086
+ const reader = response.body.getReader();
28087
+ const chunks = [];
28088
+ let total = 0;
28089
+ try {
28090
+ while (true) {
28091
+ const { done, value } = await reader.read();
28092
+ if (done) break;
28093
+ if (!value) continue;
28094
+ total += value.byteLength;
28095
+ if (total > maxBytes) throw new Error("image_too_large");
28096
+ chunks.push(value);
28097
+ }
28098
+ } finally {
28099
+ reader.releaseLock();
28100
+ }
28101
+ const bytes = new Uint8Array(total);
28102
+ let offset = 0;
28103
+ for (const chunk of chunks) {
28104
+ bytes.set(chunk, offset);
28105
+ offset += chunk.byteLength;
28106
+ }
28107
+ return bytes;
28108
+ }
28109
+ async function fetchMetaImage(rawUrl, maxBytes) {
28110
+ let url = allowedMetaImageUrl(rawUrl);
28111
+ if (!url) throw new Error("image_host_not_allowed");
28112
+ for (let redirect = 0; redirect <= MAX_REDIRECTS; redirect++) {
28113
+ const response = await fetch(url, {
28114
+ redirect: "manual",
28115
+ headers: { Accept: "image/jpeg,image/png,image/webp,image/gif;q=0.8" },
28116
+ signal: AbortSignal.timeout(12e3)
28117
+ });
28118
+ if ([301, 302, 303, 307, 308].includes(response.status)) {
28119
+ const location2 = response.headers.get("location");
28120
+ if (!location2 || redirect === MAX_REDIRECTS) throw new Error("image_redirect_rejected");
28121
+ url = allowedMetaImageUrl(new URL(location2, url).href);
28122
+ if (!url) throw new Error("image_redirect_host_not_allowed");
28123
+ continue;
28124
+ }
28125
+ if (!response.ok) throw new Error(`image_fetch_${response.status}`);
28126
+ const bytes = await readBoundedBody(response, maxBytes);
28127
+ const mimeType = detectImageMime(bytes);
28128
+ if (!mimeType) throw new Error("image_content_invalid");
28129
+ return { bytes, mimeType, finalUrl: url.href };
28130
+ }
28131
+ throw new Error("image_redirect_rejected");
28132
+ }
28133
+ function firstString(row, keys) {
28134
+ for (const key of keys) {
28135
+ const value = row[key];
28136
+ if (typeof value === "string" && value.trim()) return value;
28137
+ }
28138
+ return null;
28139
+ }
28140
+ function displayUrl(raw) {
28141
+ try {
28142
+ const url = new URL(raw);
28143
+ return `${url.origin}${url.pathname}`;
28144
+ } catch {
28145
+ return "[invalid media URL]";
28146
+ }
28147
+ }
28148
+ async function buildMetaAdCreativeMediaResult(executor, input) {
28149
+ const providerResult = await executor.readServiceConnection({
28150
+ connectionId: input.connectionId,
28151
+ tool: "resolve-ad-creative-media",
28152
+ args: { adId: input.adId }
28153
+ });
28154
+ if (providerResult.isError) return providerResult;
28155
+ const payload = parseToolPayload(providerResult);
28156
+ const resolved = asRecord(payload?.["result"]) ?? payload;
28157
+ if (!resolved || typeof resolved["creativeId"] !== "string") {
28158
+ return {
28159
+ 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." }],
28160
+ isError: true
28161
+ };
28162
+ }
28163
+ const warnings = Array.isArray(resolved["warnings"]) ? resolved["warnings"].filter((value) => typeof value === "string") : [];
28164
+ const limitations = Array.isArray(resolved["limitations"]) ? resolved["limitations"].filter((value) => typeof value === "string") : [];
28165
+ const images = Array.isArray(resolved["images"]) ? resolved["images"].map(asRecord).filter((value) => value !== null) : [];
28166
+ const videos = Array.isArray(resolved["videos"]) ? resolved["videos"].map(asRecord).filter((value) => value !== null) : [];
28167
+ const nextActions = Array.isArray(resolved["nextActions"]) ? resolved["nextActions"].map(asRecord).filter((value) => value !== null) : [];
28168
+ const content = [];
28169
+ const inlineImages = [];
28170
+ if (input.imageMode === "inline_preview") {
28171
+ let remaining = MAX_TOTAL_IMAGE_BYTES;
28172
+ const seen = /* @__PURE__ */ new Set();
28173
+ for (const image of images) {
28174
+ if (inlineImages.length >= input.maxInlineImages || remaining <= 0) break;
28175
+ const sourceUrl = firstString(image, ["url", "thumbnailUrl", "permalinkUrl"]);
28176
+ if (!sourceUrl || seen.has(sourceUrl)) continue;
28177
+ seen.add(sourceUrl);
28178
+ try {
28179
+ const fetched = await fetchMetaImage(sourceUrl, Math.min(MAX_IMAGE_BYTES, remaining));
28180
+ const contentIndex = content.length + 1;
28181
+ content.push({ type: "image", data: Buffer.from(fetched.bytes).toString("base64"), mimeType: fetched.mimeType });
28182
+ inlineImages.push({ sourceUrl: fetched.finalUrl, mimeType: fetched.mimeType, bytes: fetched.bytes.byteLength, contentIndex });
28183
+ remaining -= fetched.bytes.byteLength;
28184
+ } catch (error) {
28185
+ const code = error instanceof Error ? error.message : "image_fetch_failed";
28186
+ warnings.push(`Creative image preview could not be attached (${code}) for ${displayUrl(sourceUrl)}.`);
28187
+ }
28188
+ }
28189
+ }
28190
+ const mediaType = typeof resolved["mediaType"] === "string" ? resolved["mediaType"] : "unknown";
28191
+ const publicPostCandidateUrl = typeof resolved["publicPostCandidateUrl"] === "string" ? resolved["publicPostCandidateUrl"] : null;
28192
+ const summary = [
28193
+ "# Meta Ad Creative Media",
28194
+ `**Ad:** ${typeof resolved["adName"] === "string" ? resolved["adName"] : input.adId} (\`${input.adId}\`)`,
28195
+ `**Creative:** ${typeof resolved["creativeName"] === "string" ? resolved["creativeName"] : resolved["creativeId"]} (\`${resolved["creativeId"]}\`)`,
28196
+ `**Media:** ${mediaType} \xB7 ${images.length} image descriptor(s) \xB7 ${videos.length} video descriptor(s) \xB7 ${inlineImages.length} inline visual preview(s)`,
28197
+ publicPostCandidateUrl ? `**Public post candidate:** ${publicPostCandidateUrl} (unverified; dark/paused ads may not be public)` : "",
28198
+ nextActions.length ? `
28199
+ ## Recommended next calls
28200
+ ${nextActions.map((action) => `- \`${String(action["tool"] ?? "")}\`: ${String(action["reason"] ?? "")}`).join("\n")}` : "",
28201
+ warnings.length ? `
28202
+ ## Warnings
28203
+ ${warnings.map((warning) => `- ${warning}`).join("\n")}` : "",
28204
+ limitations.length ? `
28205
+ ## Limitations
28206
+ ${limitations.map((limitation) => `- ${limitation}`).join("\n")}` : ""
28207
+ ].filter(Boolean).join("\n");
28208
+ content.unshift({ type: "text", text: summary });
28209
+ return {
28210
+ content,
28211
+ structuredContent: {
28212
+ ok: true,
28213
+ adId: String(resolved["adId"] ?? input.adId),
28214
+ adName: typeof resolved["adName"] === "string" ? resolved["adName"] : null,
28215
+ adAccountId: typeof resolved["adAccountId"] === "string" ? resolved["adAccountId"] : null,
28216
+ creativeId: String(resolved["creativeId"]),
28217
+ creativeName: typeof resolved["creativeName"] === "string" ? resolved["creativeName"] : null,
28218
+ effectiveObjectStoryId: typeof resolved["effectiveObjectStoryId"] === "string" ? resolved["effectiveObjectStoryId"] : null,
28219
+ sourceFacebookPostId: typeof resolved["sourceFacebookPostId"] === "string" ? resolved["sourceFacebookPostId"] : null,
28220
+ objectStoryId: typeof resolved["objectStoryId"] === "string" ? resolved["objectStoryId"] : null,
28221
+ pageId: typeof resolved["pageId"] === "string" ? resolved["pageId"] : null,
28222
+ postId: typeof resolved["postId"] === "string" ? resolved["postId"] : null,
28223
+ publicPostCandidateUrl,
28224
+ publicPostStatus: "unverified",
28225
+ mediaType,
28226
+ images,
28227
+ videos,
28228
+ inlineImages,
28229
+ nextActions,
28230
+ warnings,
28231
+ limitations
28232
+ }
28233
+ };
28234
+ }
28235
+ var META_IMAGE_HOSTS, MAX_IMAGE_BYTES, MAX_TOTAL_IMAGE_BYTES, MAX_REDIRECTS;
28236
+ var init_meta_ad_creative_media = __esm({
28237
+ "src/mcp/meta-ad-creative-media.ts"() {
28238
+ "use strict";
28239
+ META_IMAGE_HOSTS = ["facebook.com", "fbcdn.net", "fbsbx.com", "cdninstagram.com"];
28240
+ MAX_IMAGE_BYTES = 15e5;
28241
+ MAX_TOTAL_IMAGE_BYTES = 25e5;
28242
+ MAX_REDIRECTS = 3;
28243
+ }
28244
+ });
28245
+
27817
28246
  // src/mcp/mcp-tool-schemas.ts
27818
- var import_zod33, HarvestPaaInputSchema, ExtractUrlInputSchema, DiffPageInputSchema, MapSiteUrlsInputSchema, ExtractSiteInputSchema, AuditSiteInputSchema, YoutubeHarvestInputSchema, YoutubeTranscribeInputSchema, FacebookPageIntelInputSchema, FacebookAdSearchInputSchema, RedditThreadInputSchema, VideoFrameAnalysisInputSchema, VideoFrameAnalysisStatusInputSchema, FacebookAdTranscribeInputSchema, FacebookVideoTranscribeInputSchema, GoogleAdsSearchInputSchema, GoogleAdsPageIntelInputSchema, GoogleAdsTranscribeInputSchema, InstagramProfileContentInputSchema, InstagramMediaDownloadInputSchema, MapsPlaceIntelInputSchema, TrustpilotReviewsInputSchema, G2ReviewsInputSchema, ReviewCardSchema, MapsSearchInputSchema, DirectoryWorkflowInputSchema, ArtifactPointerOutputSchema, RankTrackerModeSchema, RankTrackerBlueprintInputSchema, NullableString, MapsSearchAttemptOutput, MapsSearchOutputSchema, DirectoryMapsBusinessOutput, DirectoryWorkflowOutputSchema, RankTrackerToolPlanOutput, RankTrackerTableOutput, RankTrackerCronJobOutput, RankTrackerBlueprintOutputSchema, OrganicResultOutput, AiOverviewOutput, EntityIdsOutput, HarvestPaaOutputSchema, SearchSerpOutputSchema, ExtractUrlOutputSchema, DiffPageOutputSchema, ExtractSiteOutputSchema, AuditSiteOutputSchema, MapsPlaceIntelOutputSchema, TrustpilotReviewsOutputSchema, G2ReviewsOutputSchema, CreditsInfoOutputSchema, MapSiteUrlsOutputSchema, YoutubeHarvestOutputSchema, FacebookAdSearchOutputSchema, VideoFrameAnalysisOutputSchema, VideoFrameAnalysisStatusOutputSchema, RedditThreadOutputSchema, FacebookPageIntelOutputSchema, GoogleAdsSearchOutputSchema, GoogleAdsPageIntelOutputSchema, FacebookVideoTranscribeOutputSchema, TranscriptChunkOutput, InstagramBrowserOutput, InstagramPaginationOutput, InstagramProfileContentOutputSchema, InstagramMediaTrackOutput, InstagramDownloadOutput, InstagramMediaDownloadOutputSchema, YoutubeTranscribeOutputSchema, FacebookAdTranscribeOutputSchema, GoogleAdsTranscribeOutputSchema, CaptureSerpSnapshotOutputSchema, CaptureSerpPageSnapshotsOutputSchema, CreditsInfoInputSchema, WorkflowIdSchema2, WorkflowListInputSchema, WorkflowSuggestInputSchema, WorkflowRunInputSchema, WorkflowStepInputSchema, WorkflowStatusInputSchema, WorkflowArtifactReadInputSchema, WorkflowRecipeOutput, WorkflowDefinitionOutput, WorkflowArtifactOutput, WorkflowListOutputSchema, WorkflowSuggestOutputSchema, WorkflowRunOutputSchema, WorkflowStepOutputSchema, WorkflowStatusOutputSchema, WorkflowArtifactReadOutputSchema, SearchSerpInputSchema, CaptureSerpSnapshotInputSchema, ScreenshotInputSchema, CaptureSerpPageSnapshotsInputSchema, ReportArtifactReadInputSchema, ReportArtifactReadOutputSchema, ListServiceConnectionsInputSchema, ListServiceConnectionsOutputSchema, ReadServiceConnectionInputSchema, ReadServiceConnectionOutputSchema, ImportServiceConnectionToMemoryInputSchema, ImportServiceConnectionToMemoryOutputSchema, DescribeServiceConnectionToolInputSchema, DescribeServiceConnectionToolOutputSchema, ConnectedDataContinuationSchema, ExportConnectedServiceDataInputSchema, ConnectedDataArtifactSchema, ExportConnectedServiceDataOutputSchema, RenewConnectedDataExportDownloadInputSchema, RenewConnectedDataExportDownloadOutputSchema, CallServiceConnectionActionInputSchema, CallServiceConnectionActionOutputSchema, SetScheduledActionConnectionsInputSchema, SetScheduledActionConnectionsOutputSchema, SlackSendMessageInputSchema, SlackSendMessageOutputSchema, GmailSendMessageInputSchema, GmailSendMessageOutputSchema, GoogleCalendarCreateEventInputSchema, GoogleCalendarCreateEventOutputSchema, ZoomCreateMeetingInputSchema, ZoomCreateMeetingOutputSchema;
28247
+ var import_zod33, HarvestPaaInputSchema, ExtractUrlInputSchema, DiffPageInputSchema, MapSiteUrlsInputSchema, ExtractSiteInputSchema, AuditSiteInputSchema, CheckSiteExportInputSchema, YoutubeHarvestInputSchema, YoutubeTranscribeInputSchema, FacebookPageIntelInputSchema, FacebookAdSearchInputSchema, RedditThreadInputSchema, VideoFrameAnalysisInputSchema, VideoFrameAnalysisStatusInputSchema, FacebookAdTranscribeInputSchema, FacebookVideoTranscribeInputSchema, GoogleAdsSearchInputSchema, GoogleAdsPageIntelInputSchema, GoogleAdsTranscribeInputSchema, InstagramProfileContentInputSchema, InstagramMediaDownloadInputSchema, MapsPlaceIntelInputSchema, TrustpilotReviewsInputSchema, G2ReviewsInputSchema, ReviewCardSchema, MapsSearchInputSchema, DirectoryWorkflowInputSchema, ArtifactPointerOutputSchema, RankTrackerModeSchema, RankTrackerBlueprintInputSchema, NullableString, MapsSearchAttemptOutput, MapsSearchOutputSchema, DirectoryMapsBusinessOutput, DirectoryWorkflowOutputSchema, RankTrackerToolPlanOutput, RankTrackerTableOutput, RankTrackerCronJobOutput, RankTrackerBlueprintOutputSchema, OrganicResultOutput, AiOverviewOutput, EntityIdsOutput, HarvestPaaOutputSchema, SearchSerpOutputSchema, ExtractUrlOutputSchema, DiffPageOutputSchema, ExtractSiteOutputSchema, AuditSiteOutputSchema, CheckSiteExportOutputSchema, MapsPlaceIntelOutputSchema, TrustpilotReviewsOutputSchema, G2ReviewsOutputSchema, CreditsInfoOutputSchema, MapSiteUrlsOutputSchema, YoutubeHarvestOutputSchema, FacebookAdSearchOutputSchema, VideoFrameAnalysisOutputSchema, VideoFrameAnalysisStatusOutputSchema, RedditThreadOutputSchema, FacebookPageIntelOutputSchema, GoogleAdsSearchOutputSchema, GoogleAdsPageIntelOutputSchema, TranscriptSignalOutput, FacebookVideoTranscribeOutputSchema, TranscriptChunkOutput, InstagramBrowserOutput, InstagramPaginationOutput, InstagramProfileContentOutputSchema, InstagramMediaTrackOutput, InstagramDownloadOutput, InstagramMediaDownloadOutputSchema, YoutubeTranscribeOutputSchema, FacebookAdTranscribeOutputSchema, GoogleAdsTranscribeOutputSchema, CaptureSerpSnapshotOutputSchema, CaptureSerpPageSnapshotsOutputSchema, CreditsInfoInputSchema, WorkflowIdSchema2, WorkflowListInputSchema, WorkflowSuggestInputSchema, WorkflowRunInputSchema, WorkflowStepInputSchema, WorkflowStatusInputSchema, WorkflowArtifactReadInputSchema, WorkflowRecipeOutput, WorkflowDefinitionOutput, WorkflowArtifactOutput, WorkflowListOutputSchema, WorkflowSuggestOutputSchema, WorkflowRunOutputSchema, WorkflowStepOutputSchema, WorkflowStatusOutputSchema, WorkflowArtifactReadOutputSchema, SearchSerpInputSchema, CaptureSerpSnapshotInputSchema, ScreenshotInputSchema, CaptureSerpPageSnapshotsInputSchema, ReportArtifactReadInputSchema, ReportArtifactReadOutputSchema, ListServiceConnectionsInputSchema, ListServiceConnectionsOutputSchema, ReadServiceConnectionInputSchema, ReadServiceConnectionOutputSchema, MetaAdCreativeMediaInputSchema, MetaAdCreativeMediaOutputSchema, ImportServiceConnectionToMemoryInputSchema, ImportServiceConnectionToMemoryOutputSchema, DescribeServiceConnectionToolInputSchema, DescribeServiceConnectionToolOutputSchema, ConnectedDataContinuationSchema, ExportConnectedServiceDataInputSchema, ConnectedDataArtifactSchema, ExportConnectedServiceDataOutputSchema, RenewConnectedDataExportDownloadInputSchema, RenewConnectedDataExportDownloadOutputSchema, CallServiceConnectionActionInputSchema, CallServiceConnectionActionOutputSchema, SetScheduledActionConnectionsInputSchema, SetScheduledActionConnectionsOutputSchema, SlackSendMessageInputSchema, SlackSendMessageOutputSchema, GmailSendMessageInputSchema, GmailSendMessageOutputSchema, GoogleCalendarCreateEventInputSchema, GoogleCalendarCreateEventOutputSchema, ZoomCreateMeetingInputSchema, ZoomCreateMeetingOutputSchema;
27819
28248
  var init_mcp_tool_schemas = __esm({
27820
28249
  "src/mcp/mcp-tool-schemas.ts"() {
27821
28250
  "use strict";
@@ -27857,13 +28286,20 @@ var init_mcp_tool_schemas = __esm({
27857
28286
  maxPages: import_zod33.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."),
27858
28287
  rotateProxies: import_zod33.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."),
27859
28288
  rotateProxyEvery: import_zod33.z.number().int().min(1).max(100).optional().describe("When rotateProxies is on, pages fetched per proxy before rotating. Default 30."),
27860
- formats: import_zod33.z.array(import_zod33.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.")
28289
+ formats: import_zod33.z.array(import_zod33.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."),
28290
+ background: import_zod33.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."),
28291
+ downloadImages: import_zod33.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.")
27861
28292
  };
27862
28293
  AuditSiteInputSchema = {
27863
28294
  url: import_zod33.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."),
27864
28295
  maxPages: import_zod33.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."),
27865
28296
  rotateProxies: import_zod33.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."),
27866
- rotateProxyEvery: import_zod33.z.number().int().min(1).max(100).optional().describe("When rotateProxies is on, pages fetched per proxy before rotating. Default 30.")
28297
+ rotateProxyEvery: import_zod33.z.number().int().min(1).max(100).optional().describe("When rotateProxies is on, pages fetched per proxy before rotating. Default 30."),
28298
+ background: import_zod33.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."),
28299
+ downloadImages: import_zod33.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.")
28300
+ };
28301
+ CheckSiteExportInputSchema = {
28302
+ jobId: import_zod33.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").')
27867
28303
  };
27868
28304
  YoutubeHarvestInputSchema = {
27869
28305
  mode: import_zod33.z.enum(["search", "channel"]).describe("Use search for topic/keyword requests. Use channel when the user provides @handle, channel ID, or channel URL."),
@@ -27878,7 +28314,7 @@ var init_mcp_tool_schemas = __esm({
27878
28314
  FacebookPageIntelInputSchema = {
27879
28315
  pageId: import_zod33.z.string().optional().describe("Facebook advertiser/page ID. Use only a value returned by facebook_ad_search or copied from Ad Library."),
27880
28316
  libraryId: import_zod33.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."),
27881
- query: import_zod33.z.string().optional().describe("Advertiser or brand name when pageId/libraryId is not known. One of pageId, libraryId, or query is required."),
28317
+ query: import_zod33.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."),
27882
28318
  maxAds: import_zod33.z.number().int().min(1).max(200).default(50).describe("Maximum ads to inspect. Default 50, maximum 200."),
27883
28319
  country: import_zod33.z.string().length(2).default("US").describe("Two-letter Ad Library country code. Default US.")
27884
28320
  };
@@ -27902,10 +28338,10 @@ var init_mcp_tool_schemas = __esm({
27902
28338
  runId: import_zod33.z.string().min(1).describe("The runId returned by video_frame_analysis.")
27903
28339
  };
27904
28340
  FacebookAdTranscribeInputSchema = {
27905
- videoUrl: import_zod33.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.")
28341
+ videoUrl: import_zod33.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.")
27906
28342
  };
27907
28343
  FacebookVideoTranscribeInputSchema = {
27908
- url: import_zod33.z.string().url().describe("Organic Facebook reel/video/watch/post/share URL from facebook.com, m.facebook.com, or fb.watch."),
28344
+ url: import_zod33.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."),
27909
28345
  quality: import_zod33.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.")
27910
28346
  };
27911
28347
  GoogleAdsSearchInputSchema = {
@@ -28266,36 +28702,52 @@ var init_mcp_tool_schemas = __esm({
28266
28702
  };
28267
28703
  ExtractSiteOutputSchema = {
28268
28704
  url: import_zod33.z.string(),
28269
- pageCount: import_zod33.z.number().int().min(0),
28705
+ pageCount: import_zod33.z.number().int().min(0).optional().describe("Absent when background is true \u2014 the crawl has not finished yet."),
28270
28706
  pages: import_zod33.z.array(import_zod33.z.object({
28271
28707
  url: import_zod33.z.string(),
28272
28708
  title: NullableString,
28273
28709
  schemaTypes: import_zod33.z.array(import_zod33.z.string())
28274
- })),
28275
- durationMs: import_zod33.z.number().min(0),
28710
+ })).optional().describe("Absent when background is true \u2014 the crawl has not finished yet."),
28711
+ durationMs: import_zod33.z.number().min(0).optional().describe("Absent when background is true \u2014 the crawl has not finished yet."),
28276
28712
  truncatedCount: import_zod33.z.number().int().min(0).optional(),
28277
- artifact: ArtifactPointerOutputSchema.optional()
28713
+ artifact: ArtifactPointerOutputSchema.optional(),
28714
+ jobId: import_zod33.z.string().optional().describe("Present when background (or downloadImages) was set \u2014 poll with check_site_export."),
28715
+ status: import_zod33.z.enum(["pending"]).optional().describe("Present when background (or downloadImages) was set."),
28716
+ statusUrl: import_zod33.z.string().optional().describe("Present when background (or downloadImages) was set \u2014 informational; use check_site_export with jobId, not this URL directly.")
28278
28717
  };
28279
28718
  AuditSiteOutputSchema = {
28280
28719
  url: import_zod33.z.string(),
28281
- pageCount: import_zod33.z.number().int().min(0),
28282
- durationMs: import_zod33.z.number().min(0),
28283
- bulkFolder: import_zod33.z.string().nullable(),
28284
- issues: import_zod33.z.record(import_zod33.z.string(), import_zod33.z.number()),
28720
+ pageCount: import_zod33.z.number().int().min(0).optional().describe("Absent when background is true \u2014 the audit has not finished yet."),
28721
+ durationMs: import_zod33.z.number().min(0).optional().describe("Absent when background is true \u2014 the audit has not finished yet."),
28722
+ bulkFolder: import_zod33.z.string().nullable().optional().describe("Absent when background is true \u2014 the audit has not finished yet."),
28723
+ issues: import_zod33.z.record(import_zod33.z.string(), import_zod33.z.number()).optional().describe("Absent when background is true \u2014 the audit has not finished yet."),
28285
28724
  images: import_zod33.z.object({
28286
28725
  unique: import_zod33.z.number().int().min(0),
28287
28726
  totalBytes: import_zod33.z.number().min(0),
28288
28727
  over100kb: import_zod33.z.number().int().min(0),
28289
28728
  legacyFormat: import_zod33.z.number().int().min(0)
28290
- }),
28729
+ }).optional().describe("Absent when background is true \u2014 the audit has not finished yet."),
28291
28730
  links: import_zod33.z.object({
28292
28731
  internal: import_zod33.z.number().int().min(0),
28293
28732
  external: import_zod33.z.number().int().min(0),
28294
28733
  orphans: import_zod33.z.number().int().min(0),
28295
28734
  brokenInternal: import_zod33.z.number().int().min(0),
28296
28735
  externalDomains: import_zod33.z.number().int().min(0)
28297
- }),
28298
- artifact: ArtifactPointerOutputSchema.optional()
28736
+ }).optional().describe("Absent when background is true \u2014 the audit has not finished yet."),
28737
+ artifact: ArtifactPointerOutputSchema.optional(),
28738
+ jobId: import_zod33.z.string().optional().describe("Present when background (or downloadImages) was set \u2014 poll with check_site_export."),
28739
+ status: import_zod33.z.enum(["pending"]).optional().describe("Present when background (or downloadImages) was set."),
28740
+ statusUrl: import_zod33.z.string().optional().describe("Present when background (or downloadImages) was set \u2014 informational; use check_site_export with jobId, not this URL directly.")
28741
+ };
28742
+ CheckSiteExportOutputSchema = {
28743
+ jobId: import_zod33.z.string(),
28744
+ status: import_zod33.z.enum(["pending", "running", "complete", "failed"]),
28745
+ startUrl: import_zod33.z.string().optional(),
28746
+ totalUrls: import_zod33.z.number().int().min(0).optional(),
28747
+ doneUrls: import_zod33.z.number().int().min(0).optional(),
28748
+ bundleUrl: import_zod33.z.string().nullable().describe("Downloadable zip URL once status is complete; null otherwise."),
28749
+ bundleBytes: import_zod33.z.number().int().min(0).nullable().describe("Zip size in bytes once status is complete; null otherwise."),
28750
+ error: import_zod33.z.string().nullable().optional().describe("Present with a message when status is failed.")
28299
28751
  };
28300
28752
  MapsPlaceIntelOutputSchema = {
28301
28753
  name: import_zod33.z.string(),
@@ -28460,6 +28912,11 @@ var init_mcp_tool_schemas = __esm({
28460
28912
  };
28461
28913
  FacebookPageIntelOutputSchema = {
28462
28914
  advertiserName: NullableString,
28915
+ inputMode: import_zod33.z.enum(["pageId", "libraryId", "query"]),
28916
+ matchConfidence: import_zod33.z.enum(["high", "medium", "low"]),
28917
+ matchReason: import_zod33.z.string(),
28918
+ warnings: import_zod33.z.array(import_zod33.z.string()),
28919
+ matchedAdvertisers: import_zod33.z.array(import_zod33.z.object({ name: import_zod33.z.string(), adCount: import_zod33.z.number().int().min(0) })),
28463
28920
  totalAds: import_zod33.z.number().int().min(0),
28464
28921
  activeCount: import_zod33.z.number().int().min(0),
28465
28922
  videoCount: import_zod33.z.number().int().min(0),
@@ -28514,6 +28971,16 @@ var init_mcp_tool_schemas = __esm({
28514
28971
  variations: import_zod33.z.number().int().nullable()
28515
28972
  }))
28516
28973
  };
28974
+ TranscriptSignalOutput = import_zod33.z.object({
28975
+ status: import_zod33.z.enum(["speech_detected", "low_speech_signal", "empty"]),
28976
+ speechDetected: import_zod33.z.boolean(),
28977
+ confidence: import_zod33.z.enum(["medium", "low"]),
28978
+ basis: import_zod33.z.literal("transcript_word_count_and_timing"),
28979
+ mediaDurationSec: import_zod33.z.number().nullable(),
28980
+ wordsPerMinute: import_zod33.z.number().nullable(),
28981
+ retryRecommended: import_zod33.z.boolean(),
28982
+ warnings: import_zod33.z.array(import_zod33.z.string())
28983
+ });
28517
28984
  FacebookVideoTranscribeOutputSchema = {
28518
28985
  sourceUrl: import_zod33.z.string().url(),
28519
28986
  pageUrl: import_zod33.z.string().url(),
@@ -28530,7 +28997,8 @@ var init_mcp_tool_schemas = __esm({
28530
28997
  startSec: import_zod33.z.number(),
28531
28998
  endSec: import_zod33.z.number(),
28532
28999
  text: import_zod33.z.string()
28533
- }))
29000
+ })),
29001
+ transcriptSignal: TranscriptSignalOutput
28534
29002
  };
28535
29003
  TranscriptChunkOutput = import_zod33.z.object({
28536
29004
  startSec: import_zod33.z.number(),
@@ -28652,6 +29120,7 @@ var init_mcp_tool_schemas = __esm({
28652
29120
  durationMs: import_zod33.z.number().nullable(),
28653
29121
  transcriptText: import_zod33.z.string(),
28654
29122
  chunks: import_zod33.z.array(TranscriptChunkOutput),
29123
+ transcriptSignal: TranscriptSignalOutput,
28655
29124
  resolvedInputs: import_zod33.z.object({
28656
29125
  videoUrl: import_zod33.z.string().url()
28657
29126
  })
@@ -28884,6 +29353,43 @@ var init_mcp_tool_schemas = __esm({
28884
29353
  result: import_zod33.z.unknown().optional(),
28885
29354
  error: NullableString
28886
29355
  };
29356
+ MetaAdCreativeMediaInputSchema = {
29357
+ connectionId: import_zod33.z.string().min(1).describe("Tenant-owned Meta Marketing connectionId from list_service_connections."),
29358
+ adId: import_zod33.z.string().regex(/^\d{5,30}$/).describe("Meta ad ID from the connected ad account. This is not an Ad Library archive ID."),
29359
+ imageMode: import_zod33.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."),
29360
+ maxInlineImages: import_zod33.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.")
29361
+ };
29362
+ MetaAdCreativeMediaOutputSchema = {
29363
+ ok: import_zod33.z.boolean(),
29364
+ adId: import_zod33.z.string(),
29365
+ adName: NullableString,
29366
+ adAccountId: NullableString,
29367
+ creativeId: import_zod33.z.string(),
29368
+ creativeName: NullableString,
29369
+ effectiveObjectStoryId: NullableString,
29370
+ sourceFacebookPostId: NullableString,
29371
+ objectStoryId: NullableString,
29372
+ pageId: NullableString,
29373
+ postId: NullableString,
29374
+ publicPostCandidateUrl: NullableString,
29375
+ publicPostStatus: import_zod33.z.literal("unverified"),
29376
+ mediaType: import_zod33.z.enum(["image", "video", "carousel", "mixed", "unknown"]),
29377
+ images: import_zod33.z.array(import_zod33.z.record(import_zod33.z.string(), import_zod33.z.unknown())),
29378
+ videos: import_zod33.z.array(import_zod33.z.record(import_zod33.z.string(), import_zod33.z.unknown())),
29379
+ inlineImages: import_zod33.z.array(import_zod33.z.object({
29380
+ sourceUrl: import_zod33.z.string().url(),
29381
+ mimeType: import_zod33.z.string(),
29382
+ bytes: import_zod33.z.number().int().min(0),
29383
+ contentIndex: import_zod33.z.number().int().min(1)
29384
+ })),
29385
+ nextActions: import_zod33.z.array(import_zod33.z.object({
29386
+ tool: import_zod33.z.enum(["facebook_ad_transcribe", "facebook_video_transcribe", "video_frame_analysis"]),
29387
+ args: import_zod33.z.record(import_zod33.z.string(), import_zod33.z.unknown()),
29388
+ reason: import_zod33.z.string()
29389
+ })),
29390
+ warnings: import_zod33.z.array(import_zod33.z.string()),
29391
+ limitations: import_zod33.z.array(import_zod33.z.string())
29392
+ };
28887
29393
  ImportServiceConnectionToMemoryInputSchema = {
28888
29394
  connectionId: import_zod33.z.string().min(1).max(200).describe("A tenant-owned connectionId from list_service_connections."),
28889
29395
  providerConfigKey: import_zod33.z.string().min(1).max(200).describe("The exact providerConfigKey returned with that connection. It is matched together with connectionId against the authenticated caller."),
@@ -29553,6 +30059,13 @@ function registerPaaExtractorMcpTools(server, executor, options = {}) {
29553
30059
  outputSchema: recordOutputSchema("audit_site", AuditSiteOutputSchema),
29554
30060
  annotations: liveWebToolAnnotations("Technical SEO Audit")
29555
30061
  }, async (input) => formatAuditSite(await executor.auditSite(input), input, ctx));
30062
+ server.registerTool("check_site_export", {
30063
+ title: "Check Site Export",
30064
+ 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.",
30065
+ inputSchema: CheckSiteExportInputSchema,
30066
+ outputSchema: recordOutputSchema("check_site_export", CheckSiteExportOutputSchema),
30067
+ annotations: liveWebToolAnnotations("Check Site Export")
30068
+ }, async (input) => formatCheckSiteExport(await executor.checkSiteExport(input), input));
29556
30069
  server.registerTool("youtube_harvest", {
29557
30070
  title: "YouTube Video Harvest",
29558
30071
  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.',
@@ -29569,7 +30082,7 @@ function registerPaaExtractorMcpTools(server, executor, options = {}) {
29569
30082
  }, async (input) => formatYoutubeTranscribe(await executor.youtubeTranscribe(input), input));
29570
30083
  server.registerTool("facebook_page_intel", {
29571
30084
  title: "Facebook Advertiser Ad Intel",
29572
- 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.",
30085
+ 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.",
29573
30086
  inputSchema: FacebookPageIntelInputSchema,
29574
30087
  outputSchema: recordOutputSchema("facebook_page_intel", FacebookPageIntelOutputSchema),
29575
30088
  annotations: liveWebToolAnnotations("Facebook Advertiser Ad Intel")
@@ -29604,7 +30117,7 @@ function registerPaaExtractorMcpTools(server, executor, options = {}) {
29604
30117
  }, async (input) => executor.videoFrameAnalysisStatus(input));
29605
30118
  server.registerTool("facebook_ad_transcribe", {
29606
30119
  title: "Facebook Ad Transcription",
29607
- 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).",
30120
+ 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.",
29608
30121
  inputSchema: FacebookAdTranscribeInputSchema,
29609
30122
  outputSchema: recordOutputSchema("facebook_ad_transcribe", FacebookAdTranscribeOutputSchema),
29610
30123
  annotations: liveWebToolAnnotations("Facebook Ad Transcription")
@@ -29632,7 +30145,7 @@ function registerPaaExtractorMcpTools(server, executor, options = {}) {
29632
30145
  }, async (input) => formatGoogleAdsTranscribe(await executor.googleAdsTranscribe(input), input));
29633
30146
  server.registerTool("facebook_video_transcribe", {
29634
30147
  title: "Facebook Organic Video Transcription",
29635
- 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.",
30148
+ 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.",
29636
30149
  inputSchema: FacebookVideoTranscribeInputSchema,
29637
30150
  outputSchema: recordOutputSchema("facebook_video_transcribe", FacebookVideoTranscribeOutputSchema),
29638
30151
  annotations: liveWebToolAnnotations("Facebook Organic Video Transcription")
@@ -29810,6 +30323,13 @@ function registerPaaExtractorMcpTools(server, executor, options = {}) {
29810
30323
  outputSchema: recordOutputSchema("read_service_connection", ReadServiceConnectionOutputSchema),
29811
30324
  annotations: { title: "Read Connected Service", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }
29812
30325
  }, async (input) => executor.readServiceConnection(input));
30326
+ server.registerTool("meta_ad_creative_media", {
30327
+ title: "View Meta Ad Creative Media",
30328
+ 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.",
30329
+ inputSchema: MetaAdCreativeMediaInputSchema,
30330
+ outputSchema: recordOutputSchema("meta_ad_creative_media", MetaAdCreativeMediaOutputSchema),
30331
+ annotations: { title: "View Meta Ad Creative Media", readOnlyHint: true, destructiveHint: false, idempotentHint: false, openWorldHint: true }
30332
+ }, async (input) => buildMetaAdCreativeMediaResult(executor, input));
29813
30333
  server.registerTool("import_service_connection_to_memory", {
29814
30334
  title: "Import Connected Service Snapshot to Memory",
29815
30335
  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.",
@@ -29866,6 +30386,7 @@ var init_paa_mcp_server = __esm({
29866
30386
  init_server_instructions();
29867
30387
  init_output_schema_registry();
29868
30388
  init_report_artifact_offload();
30389
+ init_meta_ad_creative_media();
29869
30390
  init_mcp_tool_schemas();
29870
30391
  init_rank_tracker_blueprint();
29871
30392
  init_mcp_response_formatter();
@@ -30062,6 +30583,9 @@ var init_http_mcp_tool_executor = __esm({
30062
30583
  auditSite(input) {
30063
30584
  return this.call("/extract-site", input);
30064
30585
  }
30586
+ checkSiteExport(input) {
30587
+ return this.getJson(`/extract-site/status/${encodeURIComponent(input.jobId)}`);
30588
+ }
30065
30589
  youtubeHarvest(input) {
30066
30590
  return this.call("/youtube/harvest", input);
30067
30591
  }
@@ -40070,7 +40594,7 @@ function cleanString(value, max = 300) {
40070
40594
  const trimmed = value.trim();
40071
40595
  return trimmed ? trimmed.slice(0, max) : null;
40072
40596
  }
40073
- function firstString(record, keys, max = 300) {
40597
+ function firstString2(record, keys, max = 300) {
40074
40598
  for (const key of keys) {
40075
40599
  const value = cleanString(record[key], max);
40076
40600
  if (value) return value;
@@ -40134,10 +40658,10 @@ function safeControlErrorPayload(body, responseStatus) {
40134
40658
  const status = controlErrorStatus(responseStatus);
40135
40659
  const unwrapped = unwrapData(body);
40136
40660
  const record = isRecord2(unwrapped) ? unwrapped : isRecord2(body) ? body : null;
40137
- const candidateCode = record ? firstString(record, ["code", "errorCode", "error_code"], 100) : null;
40661
+ const candidateCode = record ? firstString2(record, ["code", "errorCode", "error_code"], 100) : null;
40138
40662
  const normalizedCandidateCode = candidateCode ? CONTROL_ERROR_CODE_ALIASES.get(candidateCode) ?? candidateCode : null;
40139
40663
  const code = normalizedCandidateCode && SAFE_CONTROL_ERROR_CODES.has(normalizedCandidateCode) ? normalizedCandidateCode : defaultControlErrorCode(status);
40140
- const candidateMessage = record ? firstString(record, ["error", "message"], 500) : null;
40664
+ const candidateMessage = record ? firstString2(record, ["error", "message"], 500) : null;
40141
40665
  const message = normalizedCandidateCode && SAFE_CONTROL_ERROR_CODES.has(normalizedCandidateCode) && candidateMessage ? candidateMessage.replace(/[\u0000-\u001f\u007f]/g, " ") : defaultControlErrorMessage(status);
40142
40666
  const retryable = record && typeof record.retryable === "boolean" ? record.retryable : status === 429 || status >= 500;
40143
40667
  return { status, code, message, retryable };
@@ -40182,8 +40706,8 @@ function sanitizeScheduleConnectionSelections(value) {
40182
40706
  const seen = /* @__PURE__ */ new Set();
40183
40707
  for (const item of value) {
40184
40708
  if (!isRecord2(item)) throw new ScheduleConnectionValidationError("Each service connection must be an object.");
40185
- const connectionId = firstString(item, ["connectionId", "connection_id"]);
40186
- const providerConfigKey = firstString(item, ["providerConfigKey", "provider_config_key", "integrationId", "integration_id"]);
40709
+ const connectionId = firstString2(item, ["connectionId", "connection_id"]);
40710
+ const providerConfigKey = firstString2(item, ["providerConfigKey", "provider_config_key", "integrationId", "integration_id"]);
40187
40711
  const allowedTools = cleanTools(item.allowedTools ?? item.allowed_tools ?? item.allowedCapabilities ?? item.allowed_capabilities);
40188
40712
  if (!connectionId || !providerConfigKey) throw new ScheduleConnectionValidationError("Each service connection needs a connectionId and providerConfigKey.");
40189
40713
  if (allowedTools.length === 0) throw new ScheduleConnectionValidationError("Each selected service needs at least one allowed tool.");
@@ -40200,14 +40724,14 @@ async function getNangoCatalog() {
40200
40724
  const result = [];
40201
40725
  for (const row of rows) {
40202
40726
  if (!isRecord2(row)) continue;
40203
- const providerConfigKey = firstString(row, ["providerConfigKey", "provider_config_key", "integrationId", "integration_id", "id"]);
40727
+ const providerConfigKey = firstString2(row, ["providerConfigKey", "provider_config_key", "integrationId", "integration_id", "id"]);
40204
40728
  if (!providerConfigKey) continue;
40205
- const provider = firstString(row, ["provider"]);
40206
- const label = firstString(row, ["label", "displayName", "display_name", "name"]) || providerConfigKey;
40207
- const description = firstString(row, ["description"], 500);
40729
+ const provider = firstString2(row, ["provider"]);
40730
+ const label = firstString2(row, ["label", "displayName", "display_name", "name"]) || providerConfigKey;
40731
+ const description = firstString2(row, ["description"], 500);
40208
40732
  const logoUrl = cleanHttpsUrl(row.logoUrl ?? row.logo_url ?? row.logo);
40209
40733
  const docsUrl = cleanHttpsUrl(row.docsUrl ?? row.docs_url ?? row.docs);
40210
- const authMode = firstString(row, ["authMode", "auth_mode"], 100);
40734
+ const authMode = firstString2(row, ["authMode", "auth_mode"], 100);
40211
40735
  const categories = cleanStringArray(row.categories);
40212
40736
  const disabledTools = DISABLED_NANGO_TOOLS[providerConfigKey];
40213
40737
  const safeDefaultAllowedTools = cleanTools(
@@ -40234,14 +40758,14 @@ async function getNangoCatalog() {
40234
40758
  requiredFeaturesByTool[cleanTool] = cleanStringArray(features, 32, 200);
40235
40759
  }
40236
40760
  }
40237
- const platformSetupStatus = firstString(row, [
40761
+ const platformSetupStatus = firstString2(row, [
40238
40762
  "platformSetupStatus",
40239
40763
  "platform_setup_status",
40240
40764
  "setupStatus",
40241
40765
  "setup_status"
40242
40766
  ], 100);
40243
- const appReviewStatus = row.appReviewLimited === true || row.app_review_limited === true ? "limited" : firstString(row, ["appReviewStatus", "app_review_status", "reviewStatus", "review_status"], 100);
40244
- const appReviewNote = firstString(row, ["appReviewNote", "app_review_note", "reviewNote", "review_note"], 500);
40767
+ const appReviewStatus = row.appReviewLimited === true || row.app_review_limited === true ? "limited" : firstString2(row, ["appReviewStatus", "app_review_status", "reviewStatus", "review_status"], 100);
40768
+ const appReviewNote = firstString2(row, ["appReviewNote", "app_review_note", "reviewNote", "review_note"], 500);
40245
40769
  const connectionSyncRequiredTools = [...CONNECTION_SYNC_REQUIRED_TOOLS[providerConfigKey] ?? []];
40246
40770
  const connectionSyncOptionalTools = [...CONNECTION_SYNC_OPTIONAL_TOOLS[providerConfigKey] ?? []];
40247
40771
  result.push({
@@ -40275,12 +40799,12 @@ async function getNangoConnections(identity) {
40275
40799
  const result = [];
40276
40800
  for (const row of rows) {
40277
40801
  if (!isRecord2(row)) continue;
40278
- const connectionId = firstString(row, ["connectionId", "connection_id", "id"]);
40279
- const providerConfigKey = firstString(row, ["providerConfigKey", "provider_config_key", "integrationId", "integration_id", "provider"]);
40802
+ const connectionId = firstString2(row, ["connectionId", "connection_id", "id"]);
40803
+ const providerConfigKey = firstString2(row, ["providerConfigKey", "provider_config_key", "integrationId", "integration_id", "provider"]);
40280
40804
  if (!connectionId || !providerConfigKey) continue;
40281
- const provider = firstString(row, ["provider"]);
40282
- const label = firstString(row, ["label", "accountLabel", "account_label", "displayName", "display_name", "endUserEmail", "end_user_email"]) || providerConfigKey;
40283
- const rawStatus = firstString(row, ["status"], 64) || "connected";
40805
+ const provider = firstString2(row, ["provider"]);
40806
+ const label = firstString2(row, ["label", "accountLabel", "account_label", "displayName", "display_name", "endUserEmail", "end_user_email"]) || providerConfigKey;
40807
+ const rawStatus = firstString2(row, ["status"], 64) || "connected";
40284
40808
  const reconnectRequired = row.reconnectRequired === true || row.reconnect_required === true || rawStatus === "needs_reauth" || rawStatus === "reauth_required" || rawStatus === "invalid" || rawStatus === "error" || rawStatus === "revoked" || rawStatus === "disabled";
40285
40809
  const toolCapabilities = [];
40286
40810
  const rawToolCapabilities = row.toolCapabilities ?? row.tool_capabilities;
@@ -40322,11 +40846,11 @@ async function getNangoConnections(identity) {
40322
40846
  permissionVerification,
40323
40847
  mcpEndpoint: null,
40324
40848
  schemaDiscovery: "compatibility_describe",
40325
- toolRevision: firstString(row, ["toolRevision", "tool_revision"], 200),
40326
- vaultName: firstString(row, ["vaultName", "vault_name"], 100),
40327
- tableName: firstString(row, ["tableName", "table_name"], 100),
40328
- createdAt: firstString(row, ["createdAt", "created_at"], 100),
40329
- updatedAt: firstString(row, ["updatedAt", "updated_at"], 100)
40849
+ toolRevision: firstString2(row, ["toolRevision", "tool_revision"], 200),
40850
+ vaultName: firstString2(row, ["vaultName", "vault_name"], 100),
40851
+ tableName: firstString2(row, ["tableName", "table_name"], 100),
40852
+ createdAt: firstString2(row, ["createdAt", "created_at"], 100),
40853
+ updatedAt: firstString2(row, ["updatedAt", "updated_at"], 100)
40330
40854
  });
40331
40855
  }
40332
40856
  return result;
@@ -40334,12 +40858,12 @@ async function getNangoConnections(identity) {
40334
40858
  function sanitizeConnectSession(body) {
40335
40859
  const data = unwrapData(body);
40336
40860
  if (!isRecord2(data)) throw new NangoControlError("The connection service returned an invalid connect session.");
40337
- const connectLink = firstString(data, ["connectLink", "connect_link"], 2e3);
40861
+ const connectLink = firstString2(data, ["connectLink", "connect_link"], 2e3);
40338
40862
  if (!connectLink) throw new NangoControlError("The connection service did not return a connect link.");
40339
40863
  return {
40340
40864
  connectLink,
40341
- sessionToken: firstString(data, ["sessionToken", "session_token", "token"], 2e3),
40342
- expiresAt: firstString(data, ["expiresAt", "expires_at"], 100)
40865
+ sessionToken: firstString2(data, ["sessionToken", "session_token", "token"], 2e3),
40866
+ expiresAt: firstString2(data, ["expiresAt", "expires_at"], 100)
40343
40867
  };
40344
40868
  }
40345
40869
  async function createNangoConnectSession(identity, providerConfigKey) {
@@ -40367,7 +40891,7 @@ function sanitizeBindingRows(body, defaultScheduleActionId) {
40367
40891
  const rows = arrayFromPayload(data, ["bindings", "connections"]);
40368
40892
  for (const row of rows) {
40369
40893
  if (isRecord2(row) && Array.isArray(row.connections)) {
40370
- const scheduleActionId = firstString(row, ["scheduleActionId", "schedule_action_id", "scheduleId", "schedule_id"]) || defaultScheduleActionId;
40894
+ const scheduleActionId = firstString2(row, ["scheduleActionId", "schedule_action_id", "scheduleId", "schedule_id"]) || defaultScheduleActionId;
40371
40895
  row.connections.forEach((connection) => flattened.push({ row: connection, scheduleActionId }));
40372
40896
  } else {
40373
40897
  flattened.push({ row, scheduleActionId: defaultScheduleActionId });
@@ -40377,15 +40901,15 @@ function sanitizeBindingRows(body, defaultScheduleActionId) {
40377
40901
  const result = [];
40378
40902
  for (const item of flattened) {
40379
40903
  if (!isRecord2(item.row)) continue;
40380
- const connectionId = firstString(item.row, ["connectionId", "connection_id"]);
40381
- const providerConfigKey = firstString(item.row, ["providerConfigKey", "provider_config_key", "integrationId", "integration_id", "provider"]);
40904
+ const connectionId = firstString2(item.row, ["connectionId", "connection_id"]);
40905
+ const providerConfigKey = firstString2(item.row, ["providerConfigKey", "provider_config_key", "integrationId", "integration_id", "provider"]);
40382
40906
  if (!connectionId || !providerConfigKey) continue;
40383
40907
  result.push({
40384
- scheduleActionId: firstString(item.row, ["scheduleActionId", "schedule_action_id", "scheduleId", "schedule_id"]) || item.scheduleActionId,
40908
+ scheduleActionId: firstString2(item.row, ["scheduleActionId", "schedule_action_id", "scheduleId", "schedule_id"]) || item.scheduleActionId,
40385
40909
  connectionId,
40386
40910
  providerConfigKey,
40387
40911
  allowedTools: cleanTools(item.row.allowedTools ?? item.row.allowed_tools ?? item.row.allowedCapabilities ?? item.row.allowed_capabilities),
40388
- label: firstString(item.row, ["label", "accountLabel", "account_label", "displayName", "display_name"])
40912
+ label: firstString2(item.row, ["label", "accountLabel", "account_label", "displayName", "display_name"])
40389
40913
  });
40390
40914
  }
40391
40915
  return result;
@@ -40767,7 +41291,7 @@ function cleanString2(value, max = 300) {
40767
41291
  const trimmed = value.trim();
40768
41292
  return trimmed ? trimmed.slice(0, max) : null;
40769
41293
  }
40770
- function firstString2(record, keys, max = 300) {
41294
+ function firstString3(record, keys, max = 300) {
40771
41295
  for (const key of keys) {
40772
41296
  const value = cleanString2(record[key], max);
40773
41297
  if (value) return value;
@@ -40839,7 +41363,7 @@ async function getResendCatalog() {
40839
41363
  const result = [];
40840
41364
  for (const row of rows) {
40841
41365
  if (!isRecord3(row)) continue;
40842
- const id = firstString2(row, ["providerConfigKey", "provider_config_key", "id"]);
41366
+ const id = firstString3(row, ["providerConfigKey", "provider_config_key", "id"]);
40843
41367
  if (id !== RESEND_PROVIDER_CONFIG_KEY) continue;
40844
41368
  const safeDefaultAllowedTools = cleanTools2(
40845
41369
  row.safeDefaultAllowedTools ?? row.safe_default_allowed_tools ?? row.readTools ?? row.read_tools
@@ -40849,8 +41373,8 @@ async function getResendCatalog() {
40849
41373
  result.push({
40850
41374
  providerConfigKey: RESEND_PROVIDER_CONFIG_KEY,
40851
41375
  provider: RESEND_PROVIDER_CONFIG_KEY,
40852
- label: firstString2(row, ["label", "displayName", "display_name", "name"]) || "Resend",
40853
- description: firstString2(row, ["description"], 500) || "Connect Resend through its official remote MCP to read email operations and run explicitly enabled delivery workflows.",
41376
+ label: firstString3(row, ["label", "displayName", "display_name", "name"]) || "Resend",
41377
+ description: firstString3(row, ["description"], 500) || "Connect Resend through its official remote MCP to read email operations and run explicitly enabled delivery workflows.",
40854
41378
  logoUrl: cleanHttpsUrl2(row.logoUrl ?? row.logo_url ?? row.logo) || RESEND_LOGO_URL,
40855
41379
  docsUrl: cleanHttpsUrl2(row.docsUrl ?? row.docs_url ?? row.docs) || RESEND_DOCS_URL,
40856
41380
  authMode: "REMOTE_MCP_OAUTH2",
@@ -40860,7 +41384,7 @@ async function getResendCatalog() {
40860
41384
  adminBlockedTools: adminBlockedTools.length > 0 ? adminBlockedTools : [...RESEND_ADMIN_BLOCKED_TOOLS],
40861
41385
  connectionSyncSupported: true,
40862
41386
  connectionSyncRequiredTools: [...RESEND_CONNECTION_SYNC_REQUIRED_TOOLS],
40863
- platformSetupStatus: firstString2(row, ["platformSetupStatus", "platform_setup_status", "setupStatus", "setup_status"], 100) || "ready",
41387
+ platformSetupStatus: firstString3(row, ["platformSetupStatus", "platform_setup_status", "setupStatus", "setup_status"], 100) || "ready",
40864
41388
  appReviewStatus: null,
40865
41389
  appReviewNote: "Secret-returning webhook creation, API-key, OAuth-grant, and raw editor-session access is permanently blocked from agents and schedules.",
40866
41390
  transport: "remote_mcp"
@@ -40875,10 +41399,10 @@ async function getResendConnections(identity) {
40875
41399
  const result = [];
40876
41400
  for (const row of rows) {
40877
41401
  if (!isRecord3(row)) continue;
40878
- const connectionId = firstString2(row, ["connectionId", "connection_id", "id"]);
40879
- const providerConfigKey = firstString2(row, ["providerConfigKey", "provider_config_key", "provider"]) || RESEND_PROVIDER_CONFIG_KEY;
41402
+ const connectionId = firstString3(row, ["connectionId", "connection_id", "id"]);
41403
+ const providerConfigKey = firstString3(row, ["providerConfigKey", "provider_config_key", "provider"]) || RESEND_PROVIDER_CONFIG_KEY;
40880
41404
  if (!connectionId || providerConfigKey !== RESEND_PROVIDER_CONFIG_KEY) continue;
40881
- const rawStatus = (firstString2(row, ["status"], 64) || "needs_reauth").toLowerCase();
41405
+ const rawStatus = (firstString3(row, ["status"], 64) || "needs_reauth").toLowerCase();
40882
41406
  const pending = ["pending", "pending_oauth", "authorizing", "authorization_pending"].includes(rawStatus);
40883
41407
  const active = ["active", "connected", "ready"].includes(rawStatus);
40884
41408
  const reconnectRequired = row.reconnectRequired === true || row.reconnect_required === true || !pending && !active;
@@ -40886,7 +41410,7 @@ async function getResendConnections(identity) {
40886
41410
  connectionId,
40887
41411
  providerConfigKey: RESEND_PROVIDER_CONFIG_KEY,
40888
41412
  provider: RESEND_PROVIDER_CONFIG_KEY,
40889
- label: firstString2(row, ["label", "accountLabel", "account_label", "displayName", "display_name"]) || "Resend account",
41413
+ label: firstString3(row, ["label", "accountLabel", "account_label", "displayName", "display_name"]) || "Resend account",
40890
41414
  status: pending ? "pending_oauth" : reconnectRequired ? "needs_reauth" : "connected",
40891
41415
  reconnectRequired,
40892
41416
  actionsEnabled: row.actionsEnabled === true || row.actions_enabled === true,
@@ -40895,11 +41419,11 @@ async function getResendConnections(identity) {
40895
41419
  adminBlockedTools: cleanTools2(row.adminBlockedTools ?? row.admin_blocked_tools).length > 0 ? cleanTools2(row.adminBlockedTools ?? row.admin_blocked_tools) : [...RESEND_ADMIN_BLOCKED_TOOLS],
40896
41420
  mcpEndpoint: null,
40897
41421
  schemaDiscovery: "compatibility_describe",
40898
- toolRevision: firstString2(row, ["toolRevision", "tool_revision"], 200),
40899
- vaultName: firstString2(row, ["vaultName", "vault_name"], 100),
40900
- tableName: firstString2(row, ["tableName", "table_name"], 100),
40901
- createdAt: firstString2(row, ["createdAt", "created_at"], 100),
40902
- updatedAt: firstString2(row, ["updatedAt", "updated_at"], 100),
41422
+ toolRevision: firstString3(row, ["toolRevision", "tool_revision"], 200),
41423
+ vaultName: firstString3(row, ["vaultName", "vault_name"], 100),
41424
+ tableName: firstString3(row, ["tableName", "table_name"], 100),
41425
+ createdAt: firstString3(row, ["createdAt", "created_at"], 100),
41426
+ updatedAt: firstString3(row, ["updatedAt", "updated_at"], 100),
40903
41427
  transport: "remote_mcp"
40904
41428
  });
40905
41429
  }
@@ -40913,7 +41437,7 @@ function sanitizeConnectSession2(body) {
40913
41437
  return {
40914
41438
  connectLink,
40915
41439
  sessionToken: null,
40916
- expiresAt: firstString2(data, ["expiresAt", "expires_at"], 100)
41440
+ expiresAt: firstString3(data, ["expiresAt", "expires_at"], 100)
40917
41441
  };
40918
41442
  }
40919
41443
  async function createResendConnectSession(identity, redirectUri) {
@@ -40975,16 +41499,16 @@ async function describeResendTool(identity, connectionId, tool) {
40975
41499
  const data = unwrapData2(body);
40976
41500
  const rawTool = isRecord3(data) && isRecord3(data.tool) ? data.tool : data;
40977
41501
  if (!isRecord3(rawTool)) throw new ResendControlError("Resend returned an invalid tool description.");
40978
- const name = firstString2(rawTool, ["name"], 200);
40979
- const classification = firstString2(rawTool, ["classification", "kind"], 20);
41502
+ const name = firstString3(rawTool, ["name"], 200);
41503
+ const classification = firstString3(rawTool, ["classification", "kind"], 20);
40980
41504
  const inputSchema = rawTool.inputSchema ?? rawTool.input_schema;
40981
41505
  if (!name || name !== tool || classification !== "read" && classification !== "action" || !isRecord3(inputSchema)) {
40982
41506
  throw new ResendControlError("Resend returned an invalid tool description.");
40983
41507
  }
40984
41508
  return {
40985
41509
  name,
40986
- title: firstString2(rawTool, ["title"], 300),
40987
- description: firstString2(rawTool, ["description"], 2e3),
41510
+ title: firstString3(rawTool, ["title"], 300),
41511
+ description: firstString3(rawTool, ["description"], 2e3),
40988
41512
  inputSchema,
40989
41513
  classification
40990
41514
  };
@@ -41532,7 +42056,7 @@ var init_server = __esm({
41532
42056
  const user = await getUserByApiKey(key);
41533
42057
  if (!user) return c.json({ error: "Invalid or inactive API key" }, 401);
41534
42058
  const refreshed = await applyMonthlyFreeRefresh(user);
41535
- const balanceMc = await reconcileBalanceMc(refreshed.id);
42059
+ const balanceMc = await reconcileBalanceMc(refreshed.id, refreshed.balance_mc);
41536
42060
  c.set("user", { ...refreshed, balance_mc: balanceMc });
41537
42061
  return next();
41538
42062
  });
@@ -41551,7 +42075,7 @@ var init_server = __esm({
41551
42075
  const user = await getUserById(userId);
41552
42076
  if (!user) return c.json({ error: "User not found" }, 401);
41553
42077
  const refreshed = await applyMonthlyFreeRefresh(user);
41554
- const balanceMc = await reconcileBalanceMc(refreshed.id);
42078
+ const balanceMc = await reconcileBalanceMc(refreshed.id, refreshed.balance_mc);
41555
42079
  c.set("sessionUser", { ...refreshed, balance_mc: balanceMc });
41556
42080
  return next();
41557
42081
  });
@@ -41741,7 +42265,7 @@ var init_server = __esm({
41741
42265
  }
41742
42266
  const refreshed = await applyMonthlyFreeRefresh(foundUser);
41743
42267
  const [balanceMc, stats] = await Promise.all([
41744
- reconcileBalanceMc(refreshed.id),
42268
+ reconcileBalanceMc(refreshed.id, refreshed.balance_mc),
41745
42269
  getUserStats(refreshed.id)
41746
42270
  ]);
41747
42271
  const user = { ...refreshed, balance_mc: balanceMc };
@@ -43009,7 +43533,8 @@ var init_server = __esm({
43009
43533
  if (checked.error || !checked.parsed) return c.json({ error: checked.error ?? "Invalid URL" }, 400);
43010
43534
  const parsed = checked.parsed;
43011
43535
  const user = c.get("user");
43012
- if (body.background === true) {
43536
+ const downloadImages = body.downloadImages === true;
43537
+ if (body.background === true || downloadImages) {
43013
43538
  const affordablePages = Math.floor(user.balance_mc / MC_COSTS.page_scrape);
43014
43539
  if (affordablePages < 1) return c.json(insufficientBalanceResponse(user.balance_mc, MC_COSTS.page_scrape), 402);
43015
43540
  const maxPages = Math.min(Math.min(1e4, Math.max(1, body.maxPages ?? 1e4)), affordablePages);
@@ -43024,6 +43549,7 @@ var init_server = __esm({
43024
43549
  concurrency: concurrencyLimitForUser(user),
43025
43550
  urlsPerBrowser: body.rotateProxyEvery ?? 10,
43026
43551
  formats: body.formats,
43552
+ downloadImages,
43027
43553
  heldMc
43028
43554
  });
43029
43555
  jobCreated = true;
@@ -43332,7 +43858,7 @@ var init_server = __esm({
43332
43858
  });
43333
43859
  app.get("/billing/balance", auth2, async (c) => {
43334
43860
  const user = c.get("user");
43335
- const balanceMc = await reconcileBalanceMc(user.id);
43861
+ const balanceMc = user.balance_mc;
43336
43862
  const ledger = await getLedger(user.id, 20);
43337
43863
  const freeCredits = await getFreeCreditBreakdown(user.id);
43338
43864
  return c.json({
@@ -43344,7 +43870,7 @@ var init_server = __esm({
43344
43870
  });
43345
43871
  app.post("/billing/credits", auth2, async (c) => {
43346
43872
  const user = c.get("user");
43347
- const balanceMc = await reconcileBalanceMc(user.id);
43873
+ const balanceMc = user.balance_mc;
43348
43874
  const body = await c.req.json().catch(() => ({}));
43349
43875
  const query = body.item?.trim().toLowerCase();
43350
43876
  const costs = CREDIT_COST_CATALOG.map(({ aliases, ...cost }) => cost);