mcp-scraper 0.18.0 → 0.19.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 (41) hide show
  1. package/README.md +2 -2
  2. package/dist/bin/api-server.cjs +448 -74
  3. package/dist/bin/api-server.cjs.map +1 -1
  4. package/dist/bin/api-server.js +2 -2
  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 +2137 -1778
  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.js +2 -2
  16. package/dist/{chunk-RMPPYKUV.js → chunk-2HDMYW4B.js} +7 -218
  17. package/dist/chunk-2HDMYW4B.js.map +1 -0
  18. package/dist/chunk-D7ZT27HY.js +244 -0
  19. package/dist/chunk-D7ZT27HY.js.map +1 -0
  20. package/dist/{chunk-FMEDPBIV.js → chunk-O2S5TOCG.js} +383 -22
  21. package/dist/chunk-O2S5TOCG.js.map +1 -0
  22. package/dist/chunk-OQHYDW4Q.js +7 -0
  23. package/dist/chunk-OQHYDW4Q.js.map +1 -0
  24. package/dist/{chunk-BX5RCOG5.js → chunk-Q44DN6T2.js} +2 -2
  25. package/dist/{chunk-BX5RCOG5.js.map → chunk-Q44DN6T2.js.map} +1 -1
  26. package/dist/index.js +4 -3
  27. package/dist/index.js.map +1 -1
  28. package/dist/{server-MQDCAR6I.js → server-VWXDE64Y.js} +69 -59
  29. package/dist/server-VWXDE64Y.js.map +1 -0
  30. package/dist/{worker-JQTS437L.js → worker-AM2DHUWG.js} +6 -6
  31. package/docs/mcp-tool-manifest.generated.json +401 -10
  32. package/docs/specs/meta-ad-creative-media-resolution-spec.md +31 -0
  33. package/package.json +1 -1
  34. package/dist/chunk-FMEDPBIV.js.map +0 -1
  35. package/dist/chunk-HPV4VOQX.js +0 -27
  36. package/dist/chunk-HPV4VOQX.js.map +0 -1
  37. package/dist/chunk-RLWCHLV3.js +0 -7
  38. package/dist/chunk-RLWCHLV3.js.map +0 -1
  39. package/dist/chunk-RMPPYKUV.js.map +0 -1
  40. package/dist/server-MQDCAR6I.js.map +0 -1
  41. /package/dist/{worker-JQTS437L.js.map → worker-AM2DHUWG.js.map} +0 -0
@@ -13647,6 +13647,39 @@ var init_audio_resolver = __esm({
13647
13647
  });
13648
13648
 
13649
13649
  // src/services/media-transcription.ts
13650
+ function transcriptWordCount(text) {
13651
+ return text.trim() ? text.trim().split(/\s+/).length : 0;
13652
+ }
13653
+ function assessTranscriptSignal(text, chunks, durationHintSec) {
13654
+ const words = transcriptWordCount(text);
13655
+ const chunkDurationSec = chunks.reduce((max, chunk) => {
13656
+ const end = Number(chunk.timestamp?.[1]);
13657
+ return Number.isFinite(end) && end > max ? end : max;
13658
+ }, 0);
13659
+ const mediaDurationSec = typeof durationHintSec === "number" && Number.isFinite(durationHintSec) && durationHintSec > 0 ? durationHintSec : chunkDurationSec > 0 ? chunkDurationSec : null;
13660
+ const wordsPerMinute = mediaDurationSec && mediaDurationSec > 0 ? Math.round(words / mediaDurationSec * 600) / 10 : null;
13661
+ const status = words === 0 ? "empty" : words < 5 ? "low_speech_signal" : "speech_detected";
13662
+ const retryRecommended = words <= 2;
13663
+ const warnings = [];
13664
+ if (status === "empty") {
13665
+ warnings.push("No speech was transcribed. This does not distinguish silent media from an inaccessible or expired playback URL.");
13666
+ } else if (status === "low_speech_signal") {
13667
+ 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.");
13668
+ }
13669
+ if (retryRecommended) {
13670
+ warnings.push("For a connected Meta ad, resolve the creative effectiveObjectStoryId and retry with facebook_video_transcribe when a public post URL is available.");
13671
+ }
13672
+ return {
13673
+ status,
13674
+ speechDetected: status === "speech_detected",
13675
+ confidence: status === "speech_detected" ? "medium" : "low",
13676
+ basis: "transcript_word_count_and_timing",
13677
+ mediaDurationSec,
13678
+ wordsPerMinute,
13679
+ retryRecommended,
13680
+ warnings
13681
+ };
13682
+ }
13650
13683
  function transcriptMarkdown(title, text, chunks, durationMs) {
13651
13684
  const fmtTs2 = (s) => `${Math.floor(s / 60)}:${String(Math.floor(s % 60)).padStart(2, "0")}`;
13652
13685
  const lines = [title, "", `*Transcribed in ${(durationMs / 1e3).toFixed(1)}s*`, "", "## Full Text", "", text, ""];
@@ -13676,7 +13709,8 @@ async function transcribeMediaUrl(mediaUrl, markdownTitle = "# Media Transcript"
13676
13709
  chunks,
13677
13710
  durationMs,
13678
13711
  costUsd,
13679
- markdown: transcriptMarkdown(markdownTitle, text, chunks, durationMs)
13712
+ markdown: transcriptMarkdown(markdownTitle, text, chunks, durationMs),
13713
+ transcriptSignal: assessTranscriptSignal(text, chunks)
13680
13714
  };
13681
13715
  }
13682
13716
  var import_client3;
@@ -14969,11 +15003,13 @@ var init_FacebookAdExtractor = __esm({
14969
15003
  if (n) nameFreq.set(n, (nameFreq.get(n) ?? 0) + 1);
14970
15004
  }
14971
15005
  const advertiserName = nameFreq.size ? [...nameFreq.entries()].sort((a, b) => b[1] - a[1])[0][0] : null;
15006
+ const matchedAdvertisers = [...nameFreq.entries()].map(([name, adCount]) => ({ name, adCount })).sort((a, b) => b.adCount - a.adCount).slice(0, 20);
14972
15007
  return {
14973
15008
  listingUrl,
14974
15009
  scannedAt: (/* @__PURE__ */ new Date()).toISOString(),
14975
15010
  advertiserPageId,
14976
15011
  advertiserName,
15012
+ matchedAdvertisers,
14977
15013
  summary: {
14978
15014
  totalAds,
14979
15015
  activeCount,
@@ -15496,6 +15532,45 @@ function buildPageIntelUrl(body, country) {
15496
15532
  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
15533
  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
15534
  }
15535
+ function normalizeAdvertiserName(value) {
15536
+ return value.toLowerCase().normalize("NFKD").replace(/[^a-z0-9]+/g, " ").trim().replace(/\s+/g, " ");
15537
+ }
15538
+ function advertiserTokenOverlap(query, candidate) {
15539
+ const queryTokens = new Set(normalizeAdvertiserName(query).split(" ").filter(Boolean));
15540
+ const candidateTokens = new Set(normalizeAdvertiserName(candidate).split(" ").filter(Boolean));
15541
+ if (queryTokens.size === 0 || candidateTokens.size === 0) return 0;
15542
+ let overlap = 0;
15543
+ for (const token of queryTokens) if (candidateTokens.has(token)) overlap++;
15544
+ return overlap / queryTokens.size;
15545
+ }
15546
+ function assessPageIntelMatch(body, result) {
15547
+ if (body.pageId) return { inputMode: "pageId", matchConfidence: "high", matchReason: "Exact Facebook page ID filter.", warnings: [] };
15548
+ if (body.libraryId) return { inputMode: "libraryId", matchConfidence: "high", matchReason: "Exact Ad Library archive ID filter.", warnings: [] };
15549
+ const query = body.query?.trim() ?? "";
15550
+ const normalizedQuery = normalizeAdvertiserName(query);
15551
+ const matches = result.matchedAdvertisers ?? [];
15552
+ const exact = matches.find((match) => normalizeAdvertiserName(match.name) === normalizedQuery);
15553
+ const top = matches[0];
15554
+ const topShare = top && result.summary.totalAds > 0 ? top.adCount / result.summary.totalAds : 0;
15555
+ const overlap = top ? advertiserTokenOverlap(query, top.name) : 0;
15556
+ const warnings = [];
15557
+ let matchConfidence;
15558
+ let matchReason;
15559
+ if (exact && exact.adCount / Math.max(1, result.summary.totalAds) >= 0.7) {
15560
+ matchConfidence = "high";
15561
+ matchReason = "The exact advertiser name accounts for most returned ads.";
15562
+ } else if (top && overlap >= 0.75 && topShare >= 0.5) {
15563
+ matchConfidence = "medium";
15564
+ matchReason = "The leading advertiser overlaps strongly with the query, but keyword search is not page-scoped.";
15565
+ } else {
15566
+ matchConfidence = "low";
15567
+ matchReason = "Keyword search did not identify one dominant exact advertiser match.";
15568
+ }
15569
+ 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.`);
15570
+ if (matchConfidence === "low") warnings.push("Low-specificity Ad Library match: treat these results as discovery candidates, not as verified ads from the requested brand.");
15571
+ 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.");
15572
+ return { inputMode: "query", matchConfidence, matchReason, warnings };
15573
+ }
15499
15574
  function configuredKernelProxy() {
15500
15575
  return (process.env.BROWSER_SERVICE_PROXY_ID ?? process.env.KERNEL_PROXY_ID)?.trim() || void 0;
15501
15576
  }
@@ -15643,8 +15718,10 @@ var init_facebook_ad_routes = __esm({
15643
15718
  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
15719
  return c.json({ error: "soft-block: no ads returned (refunded)" }, 503);
15645
15720
  }
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);
15721
+ const match = assessPageIntelMatch(body, result);
15722
+ const output = { ...result, ...match };
15723
+ await logRequestEvent({ userId: fbUser.id, source: "facebook_page_intel", status: "done", query: body.pageId ?? body.query ?? body.libraryId ?? "", resultCount: result.ads.length, result: output });
15724
+ return c.json(output);
15648
15725
  } catch (err) {
15649
15726
  const msg = err instanceof Error ? err.message : String(err);
15650
15727
  if (debited && !refunded) await creditMc(fbUser.id, MC_COSTS.fb_ad, LedgerOperation.FB_AD_REFUND, "failed call");
@@ -15745,7 +15822,8 @@ var init_facebook_ad_routes = __esm({
15745
15822
  text: transcript.text,
15746
15823
  chunks: transcript.chunks,
15747
15824
  durationMs: transcript.durationMs,
15748
- markdown: transcript.markdown
15825
+ markdown: transcript.markdown,
15826
+ transcriptSignal: assessTranscriptSignal(transcript.text, transcript.chunks, video.durationSec)
15749
15827
  };
15750
15828
  await logRequestEvent({ userId: fbUser.id, source: "facebook_video_transcribe", status: "done", query: sourceUrl.href, resultCount: transcript.chunks.length, result });
15751
15829
  return c.json(result);
@@ -20257,6 +20335,7 @@ function formatFacebookPageIntel(raw, input) {
20257
20335
  const advertiser = d.advertiserName ?? input.query ?? input.pageId ?? input.libraryId ?? "Advertiser";
20258
20336
  const ads = d.ads ?? [];
20259
20337
  const s = d.summary ?? { totalAds: 0, activeCount: 0, videoCount: 0, imageCount: 0 };
20338
+ const warnings = d.warnings ?? [];
20260
20339
  const adBlocks = ads.map((ad, i) => [
20261
20340
  `### Ad ${i + 1}${ad.libraryId ? ` \xB7 \`${ad.libraryId}\`` : ""} \u2014 ${ad.status ?? "\u2014"} \xB7 ${ad.creativeType ?? "\u2014"} \xB7 ${ad.startDate ?? ad.started ?? "\u2014"}`,
20262
20341
  ad.headline ? `**Headline:** ${ad.headline}` : "",
@@ -20269,19 +20348,28 @@ function formatFacebookPageIntel(raw, input) {
20269
20348
  const full = [
20270
20349
  `# Facebook Ad Intel: ${advertiser}`,
20271
20350
  `**${s.totalAds} ads** \xB7 ${s.activeCount} active \xB7 ${s.videoCount} video \xB7 ${s.imageCount} image`,
20351
+ d.matchConfidence ? `**Match confidence:** ${d.matchConfidence}${d.matchReason ? ` \u2014 ${d.matchReason}` : ""}` : "",
20352
+ warnings.length ? `
20353
+ > \u26A0\uFE0F ${warnings.join("\n> \u26A0\uFE0F ")}` : "",
20272
20354
  `
20273
20355
  ${adBlocks}`,
20274
20356
  `
20275
20357
  ---
20276
20358
  \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\``
20359
+ - Connected, paused, or dark ad: use \`meta_ad_creative_media\` with its adId
20360
+ - Ad Library direct video URL: use \`facebook_ad_transcribe\` immediately
20361
+ - Public Facebook post/reel URL: use \`facebook_video_transcribe\`
20362
+ - Find the exact advertiser pageId first: use \`facebook_ad_search\``
20280
20363
  ].filter(Boolean).join("\n");
20281
20364
  return {
20282
20365
  ...oneBlock(full),
20283
20366
  structuredContent: {
20284
20367
  advertiserName: d.advertiserName ?? null,
20368
+ inputMode: d.inputMode ?? (input.pageId ? "pageId" : input.libraryId ? "libraryId" : "query"),
20369
+ matchConfidence: d.matchConfidence ?? (input.query ? "low" : "high"),
20370
+ matchReason: d.matchReason ?? (input.query ? "Keyword search result was not confidence-scored by the server." : "Exact identifier filter."),
20371
+ warnings,
20372
+ matchedAdvertisers: d.matchedAdvertisers ?? [],
20285
20373
  totalAds: s.totalAds ?? 0,
20286
20374
  activeCount: s.activeCount ?? 0,
20287
20375
  videoCount: s.videoCount ?? 0,
@@ -21210,6 +21298,7 @@ function formatFacebookAdTranscribe(raw, input) {
21210
21298
  const chunks = d.chunks ?? [];
21211
21299
  const durSec = d.durationMs ? (d.durationMs / 1e3).toFixed(0) : "\u2014";
21212
21300
  const words = wordCount(text);
21301
+ const signal = d.transcriptSignal ?? assessTranscriptSignal(text, chunks);
21213
21302
  const chunkRows = chunks.slice(0, 50).map((c) => {
21214
21303
  const sec = Number.isFinite(c.timestamp[0]) ? Math.floor(c.timestamp[0]) : 0;
21215
21304
  const mm = String(Math.floor(sec / 60)).padStart(2, "0");
@@ -21218,7 +21307,9 @@ function formatFacebookAdTranscribe(raw, input) {
21218
21307
  }).join("\n");
21219
21308
  const full = [
21220
21309
  `# Facebook Ad Transcript`,
21221
- `**Duration:** ${durSec}s \xB7 **${words} words**`,
21310
+ `**Transcription elapsed:** ${durSec}s \xB7 **${words} words** \xB7 **Signal:** ${signal.status}`,
21311
+ signal.warnings.length ? `
21312
+ > \u26A0\uFE0F ${signal.warnings.join("\n> \u26A0\uFE0F ")}` : "",
21222
21313
  `
21223
21314
  ## Full Transcript
21224
21315
  ${text}`,
@@ -21240,6 +21331,7 @@ ${chunkRows}` : "",
21240
21331
  durationMs: typeof d.durationMs === "number" ? d.durationMs : null,
21241
21332
  transcriptText: text,
21242
21333
  chunks: structuredTranscriptChunks(chunks),
21334
+ transcriptSignal: signal,
21243
21335
  resolvedInputs: {
21244
21336
  videoUrl: input.videoUrl
21245
21337
  }
@@ -21256,6 +21348,7 @@ function formatFacebookVideoTranscribe(raw, input) {
21256
21348
  const durSec = d.durationMs ? (d.durationMs / 1e3).toFixed(0) : "\u2014";
21257
21349
  const videoDuration = typeof d.videoDurationSec === "number" ? `${Math.round(d.videoDurationSec)}s` : "\u2014";
21258
21350
  const label = d.videoId ? `Facebook Organic Video \`${d.videoId}\`` : "Facebook Organic Video";
21351
+ const signal = d.transcriptSignal ?? assessTranscriptSignal(text, chunks, d.videoDurationSec);
21259
21352
  const chunkRows = chunks.slice(0, 50).map((c) => {
21260
21353
  const sec = Number.isFinite(c.timestamp[0]) ? Math.floor(c.timestamp[0]) : 0;
21261
21354
  const mm = String(Math.floor(sec / 60)).padStart(2, "0");
@@ -21266,6 +21359,9 @@ function formatFacebookVideoTranscribe(raw, input) {
21266
21359
  `# ${label} Transcript`,
21267
21360
  d.ownerName ? `**Owner:** ${d.ownerName}` : "",
21268
21361
  `**Video duration:** ${videoDuration} \xB7 **Transcribed in:** ${durSec}s \xB7 **${wordCount2} words**`,
21362
+ `**Transcript signal:** ${signal.status} (${signal.confidence} heuristic confidence)`,
21363
+ signal.warnings.length ? `
21364
+ > \u26A0\uFE0F ${signal.warnings.join("\n> \u26A0\uFE0F ")}` : "",
21269
21365
  d.pageUrl ? `**Page URL:** ${d.pageUrl}` : `**Page URL:** ${input.url}`,
21270
21366
  d.videoUrl ? `**Extracted MP4:** \`${d.videoUrl}\`` : "",
21271
21367
  `
@@ -21298,7 +21394,8 @@ ${chunkRows}` : "",
21298
21394
  startSec: Number.isFinite(c.timestamp[0]) ? c.timestamp[0] : 0,
21299
21395
  endSec: Number.isFinite(c.timestamp[1]) ? c.timestamp[1] : 0,
21300
21396
  text: c.text
21301
- }))
21397
+ })),
21398
+ transcriptSignal: signal
21302
21399
  }
21303
21400
  };
21304
21401
  }
@@ -21614,6 +21711,7 @@ var init_mcp_response_formatter = __esm({
21614
21711
  init_image_audit();
21615
21712
  init_report_artifact_offload();
21616
21713
  init_rates();
21714
+ init_media_transcription();
21617
21715
  INLINE_BUDGET_BYTES = Number(process.env.MCP_SCRAPER_INLINE_BUDGET ?? 5e4);
21618
21716
  STRUCTURED_ARRAY_CAP = 25;
21619
21717
  reportSavingEnabled = true;
@@ -27642,7 +27740,7 @@ var PACKAGE_VERSION;
27642
27740
  var init_version = __esm({
27643
27741
  "src/version.ts"() {
27644
27742
  "use strict";
27645
- PACKAGE_VERSION = "0.18.0";
27743
+ PACKAGE_VERSION = "0.19.0";
27646
27744
  }
27647
27745
  });
27648
27746
 
@@ -27682,10 +27780,14 @@ seam is noted so you can chain them.
27682
27780
 
27683
27781
  ## Facebook
27684
27782
  - 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).
27783
+ - Public Ad Library scan -> **facebook_page_intel**. Prefer pageId/libraryId. Query mode is broad discovery,
27784
+ can mix advertisers, and must be checked through matchConfidence/warnings. Paused non-political ads may
27785
+ be absent even when the connected ad account still has them.
27786
+ - Connected ad-account creative -> **meta_ad_creative_media** (takes connectionId + adId). This is the
27787
+ preferred paused/dark-ad path and returns actual image content plus exact video follow-up arguments.
27788
+ - Direct Graph/Ad Library CDN video -> **facebook_ad_transcribe**. Use transient source URLs immediately.
27789
+ - Public reel/video/post -> **facebook_video_transcribe**. Use the public candidate returned by
27790
+ \`meta_ad_creative_media\` when Graph did not provide a direct source; dark posts may not resolve publicly.
27689
27791
 
27690
27792
  ## Google Ads (Ads Transparency Center)
27691
27793
  - Find advertisers by domain or brand -> **google_ads_search** (returns \`advertisers[]\` with advertiserId).
@@ -27727,8 +27829,8 @@ Multi-step orchestrations \u2014 prefer these over hand-chaining primitives when
27727
27829
  recording) locates a DOM target and returns a ready-to-use timed annotation -> **browser_replay_stop**
27728
27830
  ends it -> feed collected annotations to **browser_replay_annotate**, or just
27729
27831
  **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
27832
+ - Video breakdown is async: **video_frame_analysis** takes a supported public video page or direct media URL
27833
+ (for connected Meta ads, use the exact sourceUrl/public candidate returned by \`meta_ad_creative_media\`) and
27732
27834
  returns a \`runId\` immediately -> poll **video_frame_analysis_status** with that \`runId\` until \`status\`
27733
27835
  is \`done\`.
27734
27836
 
@@ -27737,6 +27839,8 @@ Multi-step orchestrations \u2014 prefer these over hand-chaining primitives when
27737
27839
  sites. It returns a saved folder/artifact plus a summary, not the full content inline.
27738
27840
  - Browser sessions and media transcription cost credits and take longer \u2014 prefer the cheapest tool that
27739
27841
  answers the question (plain search/extract before browser agents).
27842
+ - Use the hosted browser as a controlled resolver for validated public Facebook post/reel redirects only
27843
+ when connected Graph media did not provide a playable source. It is not a bypass for URL/SSRF restrictions.
27740
27844
  - Large results are saved to disk or an artifact and returned as a summary plus a path or artifactId;
27741
27845
  read it back for full detail rather than expecting the whole payload inline.
27742
27846
 
@@ -27814,8 +27918,216 @@ var init_output_schema_registry = __esm({
27814
27918
  }
27815
27919
  });
27816
27920
 
27921
+ // src/mcp/meta-ad-creative-media.ts
27922
+ function asRecord(value) {
27923
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
27924
+ }
27925
+ function parseToolPayload(result) {
27926
+ const structured = asRecord(result.structuredContent);
27927
+ if (structured) return structured;
27928
+ for (const block of result.content ?? []) {
27929
+ if (block.type !== "text") continue;
27930
+ try {
27931
+ const parsed = JSON.parse(block.text);
27932
+ const row = asRecord(parsed);
27933
+ if (row) return row;
27934
+ } catch {
27935
+ continue;
27936
+ }
27937
+ }
27938
+ return null;
27939
+ }
27940
+ function allowedMetaImageUrl(raw) {
27941
+ try {
27942
+ const url = new URL(raw);
27943
+ if (url.protocol !== "https:") return null;
27944
+ const host = url.hostname.toLowerCase().replace(/\.$/, "");
27945
+ if (!META_IMAGE_HOSTS.some((suffix2) => host === suffix2 || host.endsWith(`.${suffix2}`))) return null;
27946
+ return url;
27947
+ } catch {
27948
+ return null;
27949
+ }
27950
+ }
27951
+ function detectImageMime(bytes) {
27952
+ if (bytes.length >= 3 && bytes[0] === 255 && bytes[1] === 216 && bytes[2] === 255) return "image/jpeg";
27953
+ 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";
27954
+ if (bytes.length >= 12 && String.fromCharCode(...bytes.slice(0, 4)) === "RIFF" && String.fromCharCode(...bytes.slice(8, 12)) === "WEBP") return "image/webp";
27955
+ if (bytes.length >= 6) {
27956
+ const signature = String.fromCharCode(...bytes.slice(0, 6));
27957
+ if (signature === "GIF87a" || signature === "GIF89a") return "image/gif";
27958
+ }
27959
+ return null;
27960
+ }
27961
+ async function readBoundedBody(response, maxBytes) {
27962
+ const contentLength = Number(response.headers.get("content-length") ?? "");
27963
+ if (Number.isFinite(contentLength) && contentLength > maxBytes) throw new Error("image_too_large");
27964
+ if (!response.body) {
27965
+ const bytes2 = new Uint8Array(await response.arrayBuffer());
27966
+ if (bytes2.byteLength > maxBytes) throw new Error("image_too_large");
27967
+ return bytes2;
27968
+ }
27969
+ const reader = response.body.getReader();
27970
+ const chunks = [];
27971
+ let total = 0;
27972
+ try {
27973
+ while (true) {
27974
+ const { done, value } = await reader.read();
27975
+ if (done) break;
27976
+ if (!value) continue;
27977
+ total += value.byteLength;
27978
+ if (total > maxBytes) throw new Error("image_too_large");
27979
+ chunks.push(value);
27980
+ }
27981
+ } finally {
27982
+ reader.releaseLock();
27983
+ }
27984
+ const bytes = new Uint8Array(total);
27985
+ let offset = 0;
27986
+ for (const chunk of chunks) {
27987
+ bytes.set(chunk, offset);
27988
+ offset += chunk.byteLength;
27989
+ }
27990
+ return bytes;
27991
+ }
27992
+ async function fetchMetaImage(rawUrl, maxBytes) {
27993
+ let url = allowedMetaImageUrl(rawUrl);
27994
+ if (!url) throw new Error("image_host_not_allowed");
27995
+ for (let redirect = 0; redirect <= MAX_REDIRECTS; redirect++) {
27996
+ const response = await fetch(url, {
27997
+ redirect: "manual",
27998
+ headers: { Accept: "image/jpeg,image/png,image/webp,image/gif;q=0.8" },
27999
+ signal: AbortSignal.timeout(12e3)
28000
+ });
28001
+ if ([301, 302, 303, 307, 308].includes(response.status)) {
28002
+ const location2 = response.headers.get("location");
28003
+ if (!location2 || redirect === MAX_REDIRECTS) throw new Error("image_redirect_rejected");
28004
+ url = allowedMetaImageUrl(new URL(location2, url).href);
28005
+ if (!url) throw new Error("image_redirect_host_not_allowed");
28006
+ continue;
28007
+ }
28008
+ if (!response.ok) throw new Error(`image_fetch_${response.status}`);
28009
+ const bytes = await readBoundedBody(response, maxBytes);
28010
+ const mimeType = detectImageMime(bytes);
28011
+ if (!mimeType) throw new Error("image_content_invalid");
28012
+ return { bytes, mimeType, finalUrl: url.href };
28013
+ }
28014
+ throw new Error("image_redirect_rejected");
28015
+ }
28016
+ function firstString(row, keys) {
28017
+ for (const key of keys) {
28018
+ const value = row[key];
28019
+ if (typeof value === "string" && value.trim()) return value;
28020
+ }
28021
+ return null;
28022
+ }
28023
+ function displayUrl(raw) {
28024
+ try {
28025
+ const url = new URL(raw);
28026
+ return `${url.origin}${url.pathname}`;
28027
+ } catch {
28028
+ return "[invalid media URL]";
28029
+ }
28030
+ }
28031
+ async function buildMetaAdCreativeMediaResult(executor, input) {
28032
+ const providerResult = await executor.readServiceConnection({
28033
+ connectionId: input.connectionId,
28034
+ tool: "resolve-ad-creative-media",
28035
+ args: { adId: input.adId }
28036
+ });
28037
+ if (providerResult.isError) return providerResult;
28038
+ const payload = parseToolPayload(providerResult);
28039
+ const resolved = asRecord(payload?.["result"]) ?? payload;
28040
+ if (!resolved || typeof resolved["creativeId"] !== "string") {
28041
+ return {
28042
+ 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." }],
28043
+ isError: true
28044
+ };
28045
+ }
28046
+ const warnings = Array.isArray(resolved["warnings"]) ? resolved["warnings"].filter((value) => typeof value === "string") : [];
28047
+ const limitations = Array.isArray(resolved["limitations"]) ? resolved["limitations"].filter((value) => typeof value === "string") : [];
28048
+ const images = Array.isArray(resolved["images"]) ? resolved["images"].map(asRecord).filter((value) => value !== null) : [];
28049
+ const videos = Array.isArray(resolved["videos"]) ? resolved["videos"].map(asRecord).filter((value) => value !== null) : [];
28050
+ const nextActions = Array.isArray(resolved["nextActions"]) ? resolved["nextActions"].map(asRecord).filter((value) => value !== null) : [];
28051
+ const content = [];
28052
+ const inlineImages = [];
28053
+ if (input.imageMode === "inline_preview") {
28054
+ let remaining = MAX_TOTAL_IMAGE_BYTES;
28055
+ const seen = /* @__PURE__ */ new Set();
28056
+ for (const image of images) {
28057
+ if (inlineImages.length >= input.maxInlineImages || remaining <= 0) break;
28058
+ const sourceUrl = firstString(image, ["url", "thumbnailUrl", "permalinkUrl"]);
28059
+ if (!sourceUrl || seen.has(sourceUrl)) continue;
28060
+ seen.add(sourceUrl);
28061
+ try {
28062
+ const fetched = await fetchMetaImage(sourceUrl, Math.min(MAX_IMAGE_BYTES, remaining));
28063
+ const contentIndex = content.length + 1;
28064
+ content.push({ type: "image", data: Buffer.from(fetched.bytes).toString("base64"), mimeType: fetched.mimeType });
28065
+ inlineImages.push({ sourceUrl: fetched.finalUrl, mimeType: fetched.mimeType, bytes: fetched.bytes.byteLength, contentIndex });
28066
+ remaining -= fetched.bytes.byteLength;
28067
+ } catch (error) {
28068
+ const code = error instanceof Error ? error.message : "image_fetch_failed";
28069
+ warnings.push(`Creative image preview could not be attached (${code}) for ${displayUrl(sourceUrl)}.`);
28070
+ }
28071
+ }
28072
+ }
28073
+ const mediaType = typeof resolved["mediaType"] === "string" ? resolved["mediaType"] : "unknown";
28074
+ const publicPostCandidateUrl = typeof resolved["publicPostCandidateUrl"] === "string" ? resolved["publicPostCandidateUrl"] : null;
28075
+ const summary = [
28076
+ "# Meta Ad Creative Media",
28077
+ `**Ad:** ${typeof resolved["adName"] === "string" ? resolved["adName"] : input.adId} (\`${input.adId}\`)`,
28078
+ `**Creative:** ${typeof resolved["creativeName"] === "string" ? resolved["creativeName"] : resolved["creativeId"]} (\`${resolved["creativeId"]}\`)`,
28079
+ `**Media:** ${mediaType} \xB7 ${images.length} image descriptor(s) \xB7 ${videos.length} video descriptor(s) \xB7 ${inlineImages.length} inline visual preview(s)`,
28080
+ publicPostCandidateUrl ? `**Public post candidate:** ${publicPostCandidateUrl} (unverified; dark/paused ads may not be public)` : "",
28081
+ nextActions.length ? `
28082
+ ## Recommended next calls
28083
+ ${nextActions.map((action) => `- \`${String(action["tool"] ?? "")}\`: ${String(action["reason"] ?? "")}`).join("\n")}` : "",
28084
+ warnings.length ? `
28085
+ ## Warnings
28086
+ ${warnings.map((warning) => `- ${warning}`).join("\n")}` : "",
28087
+ limitations.length ? `
28088
+ ## Limitations
28089
+ ${limitations.map((limitation) => `- ${limitation}`).join("\n")}` : ""
28090
+ ].filter(Boolean).join("\n");
28091
+ content.unshift({ type: "text", text: summary });
28092
+ return {
28093
+ content,
28094
+ structuredContent: {
28095
+ ok: true,
28096
+ adId: String(resolved["adId"] ?? input.adId),
28097
+ adName: typeof resolved["adName"] === "string" ? resolved["adName"] : null,
28098
+ adAccountId: typeof resolved["adAccountId"] === "string" ? resolved["adAccountId"] : null,
28099
+ creativeId: String(resolved["creativeId"]),
28100
+ creativeName: typeof resolved["creativeName"] === "string" ? resolved["creativeName"] : null,
28101
+ effectiveObjectStoryId: typeof resolved["effectiveObjectStoryId"] === "string" ? resolved["effectiveObjectStoryId"] : null,
28102
+ sourceFacebookPostId: typeof resolved["sourceFacebookPostId"] === "string" ? resolved["sourceFacebookPostId"] : null,
28103
+ objectStoryId: typeof resolved["objectStoryId"] === "string" ? resolved["objectStoryId"] : null,
28104
+ pageId: typeof resolved["pageId"] === "string" ? resolved["pageId"] : null,
28105
+ postId: typeof resolved["postId"] === "string" ? resolved["postId"] : null,
28106
+ publicPostCandidateUrl,
28107
+ publicPostStatus: "unverified",
28108
+ mediaType,
28109
+ images,
28110
+ videos,
28111
+ inlineImages,
28112
+ nextActions,
28113
+ warnings,
28114
+ limitations
28115
+ }
28116
+ };
28117
+ }
28118
+ var META_IMAGE_HOSTS, MAX_IMAGE_BYTES, MAX_TOTAL_IMAGE_BYTES, MAX_REDIRECTS;
28119
+ var init_meta_ad_creative_media = __esm({
28120
+ "src/mcp/meta-ad-creative-media.ts"() {
28121
+ "use strict";
28122
+ META_IMAGE_HOSTS = ["facebook.com", "fbcdn.net", "fbsbx.com", "cdninstagram.com"];
28123
+ MAX_IMAGE_BYTES = 15e5;
28124
+ MAX_TOTAL_IMAGE_BYTES = 25e5;
28125
+ MAX_REDIRECTS = 3;
28126
+ }
28127
+ });
28128
+
27817
28129
  // 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;
28130
+ 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, 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
28131
  var init_mcp_tool_schemas = __esm({
27820
28132
  "src/mcp/mcp-tool-schemas.ts"() {
27821
28133
  "use strict";
@@ -27878,7 +28190,7 @@ var init_mcp_tool_schemas = __esm({
27878
28190
  FacebookPageIntelInputSchema = {
27879
28191
  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
28192
  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."),
28193
+ 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
28194
  maxAds: import_zod33.z.number().int().min(1).max(200).default(50).describe("Maximum ads to inspect. Default 50, maximum 200."),
27883
28195
  country: import_zod33.z.string().length(2).default("US").describe("Two-letter Ad Library country code. Default US.")
27884
28196
  };
@@ -27902,10 +28214,10 @@ var init_mcp_tool_schemas = __esm({
27902
28214
  runId: import_zod33.z.string().min(1).describe("The runId returned by video_frame_analysis.")
27903
28215
  };
27904
28216
  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.")
28217
+ 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
28218
  };
27907
28219
  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."),
28220
+ 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
28221
  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
28222
  };
27911
28223
  GoogleAdsSearchInputSchema = {
@@ -28460,6 +28772,11 @@ var init_mcp_tool_schemas = __esm({
28460
28772
  };
28461
28773
  FacebookPageIntelOutputSchema = {
28462
28774
  advertiserName: NullableString,
28775
+ inputMode: import_zod33.z.enum(["pageId", "libraryId", "query"]),
28776
+ matchConfidence: import_zod33.z.enum(["high", "medium", "low"]),
28777
+ matchReason: import_zod33.z.string(),
28778
+ warnings: import_zod33.z.array(import_zod33.z.string()),
28779
+ matchedAdvertisers: import_zod33.z.array(import_zod33.z.object({ name: import_zod33.z.string(), adCount: import_zod33.z.number().int().min(0) })),
28463
28780
  totalAds: import_zod33.z.number().int().min(0),
28464
28781
  activeCount: import_zod33.z.number().int().min(0),
28465
28782
  videoCount: import_zod33.z.number().int().min(0),
@@ -28514,6 +28831,16 @@ var init_mcp_tool_schemas = __esm({
28514
28831
  variations: import_zod33.z.number().int().nullable()
28515
28832
  }))
28516
28833
  };
28834
+ TranscriptSignalOutput = import_zod33.z.object({
28835
+ status: import_zod33.z.enum(["speech_detected", "low_speech_signal", "empty"]),
28836
+ speechDetected: import_zod33.z.boolean(),
28837
+ confidence: import_zod33.z.enum(["medium", "low"]),
28838
+ basis: import_zod33.z.literal("transcript_word_count_and_timing"),
28839
+ mediaDurationSec: import_zod33.z.number().nullable(),
28840
+ wordsPerMinute: import_zod33.z.number().nullable(),
28841
+ retryRecommended: import_zod33.z.boolean(),
28842
+ warnings: import_zod33.z.array(import_zod33.z.string())
28843
+ });
28517
28844
  FacebookVideoTranscribeOutputSchema = {
28518
28845
  sourceUrl: import_zod33.z.string().url(),
28519
28846
  pageUrl: import_zod33.z.string().url(),
@@ -28530,7 +28857,8 @@ var init_mcp_tool_schemas = __esm({
28530
28857
  startSec: import_zod33.z.number(),
28531
28858
  endSec: import_zod33.z.number(),
28532
28859
  text: import_zod33.z.string()
28533
- }))
28860
+ })),
28861
+ transcriptSignal: TranscriptSignalOutput
28534
28862
  };
28535
28863
  TranscriptChunkOutput = import_zod33.z.object({
28536
28864
  startSec: import_zod33.z.number(),
@@ -28652,6 +28980,7 @@ var init_mcp_tool_schemas = __esm({
28652
28980
  durationMs: import_zod33.z.number().nullable(),
28653
28981
  transcriptText: import_zod33.z.string(),
28654
28982
  chunks: import_zod33.z.array(TranscriptChunkOutput),
28983
+ transcriptSignal: TranscriptSignalOutput,
28655
28984
  resolvedInputs: import_zod33.z.object({
28656
28985
  videoUrl: import_zod33.z.string().url()
28657
28986
  })
@@ -28884,6 +29213,43 @@ var init_mcp_tool_schemas = __esm({
28884
29213
  result: import_zod33.z.unknown().optional(),
28885
29214
  error: NullableString
28886
29215
  };
29216
+ MetaAdCreativeMediaInputSchema = {
29217
+ connectionId: import_zod33.z.string().min(1).describe("Tenant-owned Meta Marketing connectionId from list_service_connections."),
29218
+ 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."),
29219
+ 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."),
29220
+ 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.")
29221
+ };
29222
+ MetaAdCreativeMediaOutputSchema = {
29223
+ ok: import_zod33.z.boolean(),
29224
+ adId: import_zod33.z.string(),
29225
+ adName: NullableString,
29226
+ adAccountId: NullableString,
29227
+ creativeId: import_zod33.z.string(),
29228
+ creativeName: NullableString,
29229
+ effectiveObjectStoryId: NullableString,
29230
+ sourceFacebookPostId: NullableString,
29231
+ objectStoryId: NullableString,
29232
+ pageId: NullableString,
29233
+ postId: NullableString,
29234
+ publicPostCandidateUrl: NullableString,
29235
+ publicPostStatus: import_zod33.z.literal("unverified"),
29236
+ mediaType: import_zod33.z.enum(["image", "video", "carousel", "mixed", "unknown"]),
29237
+ images: import_zod33.z.array(import_zod33.z.record(import_zod33.z.string(), import_zod33.z.unknown())),
29238
+ videos: import_zod33.z.array(import_zod33.z.record(import_zod33.z.string(), import_zod33.z.unknown())),
29239
+ inlineImages: import_zod33.z.array(import_zod33.z.object({
29240
+ sourceUrl: import_zod33.z.string().url(),
29241
+ mimeType: import_zod33.z.string(),
29242
+ bytes: import_zod33.z.number().int().min(0),
29243
+ contentIndex: import_zod33.z.number().int().min(1)
29244
+ })),
29245
+ nextActions: import_zod33.z.array(import_zod33.z.object({
29246
+ tool: import_zod33.z.enum(["facebook_ad_transcribe", "facebook_video_transcribe", "video_frame_analysis"]),
29247
+ args: import_zod33.z.record(import_zod33.z.string(), import_zod33.z.unknown()),
29248
+ reason: import_zod33.z.string()
29249
+ })),
29250
+ warnings: import_zod33.z.array(import_zod33.z.string()),
29251
+ limitations: import_zod33.z.array(import_zod33.z.string())
29252
+ };
28887
29253
  ImportServiceConnectionToMemoryInputSchema = {
28888
29254
  connectionId: import_zod33.z.string().min(1).max(200).describe("A tenant-owned connectionId from list_service_connections."),
28889
29255
  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."),
@@ -29569,7 +29935,7 @@ function registerPaaExtractorMcpTools(server, executor, options = {}) {
29569
29935
  }, async (input) => formatYoutubeTranscribe(await executor.youtubeTranscribe(input), input));
29570
29936
  server.registerTool("facebook_page_intel", {
29571
29937
  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.",
29938
+ 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
29939
  inputSchema: FacebookPageIntelInputSchema,
29574
29940
  outputSchema: recordOutputSchema("facebook_page_intel", FacebookPageIntelOutputSchema),
29575
29941
  annotations: liveWebToolAnnotations("Facebook Advertiser Ad Intel")
@@ -29604,7 +29970,7 @@ function registerPaaExtractorMcpTools(server, executor, options = {}) {
29604
29970
  }, async (input) => executor.videoFrameAnalysisStatus(input));
29605
29971
  server.registerTool("facebook_ad_transcribe", {
29606
29972
  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).",
29973
+ 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
29974
  inputSchema: FacebookAdTranscribeInputSchema,
29609
29975
  outputSchema: recordOutputSchema("facebook_ad_transcribe", FacebookAdTranscribeOutputSchema),
29610
29976
  annotations: liveWebToolAnnotations("Facebook Ad Transcription")
@@ -29632,7 +29998,7 @@ function registerPaaExtractorMcpTools(server, executor, options = {}) {
29632
29998
  }, async (input) => formatGoogleAdsTranscribe(await executor.googleAdsTranscribe(input), input));
29633
29999
  server.registerTool("facebook_video_transcribe", {
29634
30000
  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.",
30001
+ 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
30002
  inputSchema: FacebookVideoTranscribeInputSchema,
29637
30003
  outputSchema: recordOutputSchema("facebook_video_transcribe", FacebookVideoTranscribeOutputSchema),
29638
30004
  annotations: liveWebToolAnnotations("Facebook Organic Video Transcription")
@@ -29810,6 +30176,13 @@ function registerPaaExtractorMcpTools(server, executor, options = {}) {
29810
30176
  outputSchema: recordOutputSchema("read_service_connection", ReadServiceConnectionOutputSchema),
29811
30177
  annotations: { title: "Read Connected Service", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }
29812
30178
  }, async (input) => executor.readServiceConnection(input));
30179
+ server.registerTool("meta_ad_creative_media", {
30180
+ title: "View Meta Ad Creative Media",
30181
+ 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.",
30182
+ inputSchema: MetaAdCreativeMediaInputSchema,
30183
+ outputSchema: recordOutputSchema("meta_ad_creative_media", MetaAdCreativeMediaOutputSchema),
30184
+ annotations: { title: "View Meta Ad Creative Media", readOnlyHint: true, destructiveHint: false, idempotentHint: false, openWorldHint: true }
30185
+ }, async (input) => buildMetaAdCreativeMediaResult(executor, input));
29813
30186
  server.registerTool("import_service_connection_to_memory", {
29814
30187
  title: "Import Connected Service Snapshot to Memory",
29815
30188
  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 +30239,7 @@ var init_paa_mcp_server = __esm({
29866
30239
  init_server_instructions();
29867
30240
  init_output_schema_registry();
29868
30241
  init_report_artifact_offload();
30242
+ init_meta_ad_creative_media();
29869
30243
  init_mcp_tool_schemas();
29870
30244
  init_rank_tracker_blueprint();
29871
30245
  init_mcp_response_formatter();
@@ -40070,7 +40444,7 @@ function cleanString(value, max = 300) {
40070
40444
  const trimmed = value.trim();
40071
40445
  return trimmed ? trimmed.slice(0, max) : null;
40072
40446
  }
40073
- function firstString(record, keys, max = 300) {
40447
+ function firstString2(record, keys, max = 300) {
40074
40448
  for (const key of keys) {
40075
40449
  const value = cleanString(record[key], max);
40076
40450
  if (value) return value;
@@ -40134,10 +40508,10 @@ function safeControlErrorPayload(body, responseStatus) {
40134
40508
  const status = controlErrorStatus(responseStatus);
40135
40509
  const unwrapped = unwrapData(body);
40136
40510
  const record = isRecord2(unwrapped) ? unwrapped : isRecord2(body) ? body : null;
40137
- const candidateCode = record ? firstString(record, ["code", "errorCode", "error_code"], 100) : null;
40511
+ const candidateCode = record ? firstString2(record, ["code", "errorCode", "error_code"], 100) : null;
40138
40512
  const normalizedCandidateCode = candidateCode ? CONTROL_ERROR_CODE_ALIASES.get(candidateCode) ?? candidateCode : null;
40139
40513
  const code = normalizedCandidateCode && SAFE_CONTROL_ERROR_CODES.has(normalizedCandidateCode) ? normalizedCandidateCode : defaultControlErrorCode(status);
40140
- const candidateMessage = record ? firstString(record, ["error", "message"], 500) : null;
40514
+ const candidateMessage = record ? firstString2(record, ["error", "message"], 500) : null;
40141
40515
  const message = normalizedCandidateCode && SAFE_CONTROL_ERROR_CODES.has(normalizedCandidateCode) && candidateMessage ? candidateMessage.replace(/[\u0000-\u001f\u007f]/g, " ") : defaultControlErrorMessage(status);
40142
40516
  const retryable = record && typeof record.retryable === "boolean" ? record.retryable : status === 429 || status >= 500;
40143
40517
  return { status, code, message, retryable };
@@ -40182,8 +40556,8 @@ function sanitizeScheduleConnectionSelections(value) {
40182
40556
  const seen = /* @__PURE__ */ new Set();
40183
40557
  for (const item of value) {
40184
40558
  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"]);
40559
+ const connectionId = firstString2(item, ["connectionId", "connection_id"]);
40560
+ const providerConfigKey = firstString2(item, ["providerConfigKey", "provider_config_key", "integrationId", "integration_id"]);
40187
40561
  const allowedTools = cleanTools(item.allowedTools ?? item.allowed_tools ?? item.allowedCapabilities ?? item.allowed_capabilities);
40188
40562
  if (!connectionId || !providerConfigKey) throw new ScheduleConnectionValidationError("Each service connection needs a connectionId and providerConfigKey.");
40189
40563
  if (allowedTools.length === 0) throw new ScheduleConnectionValidationError("Each selected service needs at least one allowed tool.");
@@ -40200,14 +40574,14 @@ async function getNangoCatalog() {
40200
40574
  const result = [];
40201
40575
  for (const row of rows) {
40202
40576
  if (!isRecord2(row)) continue;
40203
- const providerConfigKey = firstString(row, ["providerConfigKey", "provider_config_key", "integrationId", "integration_id", "id"]);
40577
+ const providerConfigKey = firstString2(row, ["providerConfigKey", "provider_config_key", "integrationId", "integration_id", "id"]);
40204
40578
  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);
40579
+ const provider = firstString2(row, ["provider"]);
40580
+ const label = firstString2(row, ["label", "displayName", "display_name", "name"]) || providerConfigKey;
40581
+ const description = firstString2(row, ["description"], 500);
40208
40582
  const logoUrl = cleanHttpsUrl(row.logoUrl ?? row.logo_url ?? row.logo);
40209
40583
  const docsUrl = cleanHttpsUrl(row.docsUrl ?? row.docs_url ?? row.docs);
40210
- const authMode = firstString(row, ["authMode", "auth_mode"], 100);
40584
+ const authMode = firstString2(row, ["authMode", "auth_mode"], 100);
40211
40585
  const categories = cleanStringArray(row.categories);
40212
40586
  const disabledTools = DISABLED_NANGO_TOOLS[providerConfigKey];
40213
40587
  const safeDefaultAllowedTools = cleanTools(
@@ -40234,14 +40608,14 @@ async function getNangoCatalog() {
40234
40608
  requiredFeaturesByTool[cleanTool] = cleanStringArray(features, 32, 200);
40235
40609
  }
40236
40610
  }
40237
- const platformSetupStatus = firstString(row, [
40611
+ const platformSetupStatus = firstString2(row, [
40238
40612
  "platformSetupStatus",
40239
40613
  "platform_setup_status",
40240
40614
  "setupStatus",
40241
40615
  "setup_status"
40242
40616
  ], 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);
40617
+ const appReviewStatus = row.appReviewLimited === true || row.app_review_limited === true ? "limited" : firstString2(row, ["appReviewStatus", "app_review_status", "reviewStatus", "review_status"], 100);
40618
+ const appReviewNote = firstString2(row, ["appReviewNote", "app_review_note", "reviewNote", "review_note"], 500);
40245
40619
  const connectionSyncRequiredTools = [...CONNECTION_SYNC_REQUIRED_TOOLS[providerConfigKey] ?? []];
40246
40620
  const connectionSyncOptionalTools = [...CONNECTION_SYNC_OPTIONAL_TOOLS[providerConfigKey] ?? []];
40247
40621
  result.push({
@@ -40275,12 +40649,12 @@ async function getNangoConnections(identity) {
40275
40649
  const result = [];
40276
40650
  for (const row of rows) {
40277
40651
  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"]);
40652
+ const connectionId = firstString2(row, ["connectionId", "connection_id", "id"]);
40653
+ const providerConfigKey = firstString2(row, ["providerConfigKey", "provider_config_key", "integrationId", "integration_id", "provider"]);
40280
40654
  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";
40655
+ const provider = firstString2(row, ["provider"]);
40656
+ const label = firstString2(row, ["label", "accountLabel", "account_label", "displayName", "display_name", "endUserEmail", "end_user_email"]) || providerConfigKey;
40657
+ const rawStatus = firstString2(row, ["status"], 64) || "connected";
40284
40658
  const reconnectRequired = row.reconnectRequired === true || row.reconnect_required === true || rawStatus === "needs_reauth" || rawStatus === "reauth_required" || rawStatus === "invalid" || rawStatus === "error" || rawStatus === "revoked" || rawStatus === "disabled";
40285
40659
  const toolCapabilities = [];
40286
40660
  const rawToolCapabilities = row.toolCapabilities ?? row.tool_capabilities;
@@ -40322,11 +40696,11 @@ async function getNangoConnections(identity) {
40322
40696
  permissionVerification,
40323
40697
  mcpEndpoint: null,
40324
40698
  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)
40699
+ toolRevision: firstString2(row, ["toolRevision", "tool_revision"], 200),
40700
+ vaultName: firstString2(row, ["vaultName", "vault_name"], 100),
40701
+ tableName: firstString2(row, ["tableName", "table_name"], 100),
40702
+ createdAt: firstString2(row, ["createdAt", "created_at"], 100),
40703
+ updatedAt: firstString2(row, ["updatedAt", "updated_at"], 100)
40330
40704
  });
40331
40705
  }
40332
40706
  return result;
@@ -40334,12 +40708,12 @@ async function getNangoConnections(identity) {
40334
40708
  function sanitizeConnectSession(body) {
40335
40709
  const data = unwrapData(body);
40336
40710
  if (!isRecord2(data)) throw new NangoControlError("The connection service returned an invalid connect session.");
40337
- const connectLink = firstString(data, ["connectLink", "connect_link"], 2e3);
40711
+ const connectLink = firstString2(data, ["connectLink", "connect_link"], 2e3);
40338
40712
  if (!connectLink) throw new NangoControlError("The connection service did not return a connect link.");
40339
40713
  return {
40340
40714
  connectLink,
40341
- sessionToken: firstString(data, ["sessionToken", "session_token", "token"], 2e3),
40342
- expiresAt: firstString(data, ["expiresAt", "expires_at"], 100)
40715
+ sessionToken: firstString2(data, ["sessionToken", "session_token", "token"], 2e3),
40716
+ expiresAt: firstString2(data, ["expiresAt", "expires_at"], 100)
40343
40717
  };
40344
40718
  }
40345
40719
  async function createNangoConnectSession(identity, providerConfigKey) {
@@ -40367,7 +40741,7 @@ function sanitizeBindingRows(body, defaultScheduleActionId) {
40367
40741
  const rows = arrayFromPayload(data, ["bindings", "connections"]);
40368
40742
  for (const row of rows) {
40369
40743
  if (isRecord2(row) && Array.isArray(row.connections)) {
40370
- const scheduleActionId = firstString(row, ["scheduleActionId", "schedule_action_id", "scheduleId", "schedule_id"]) || defaultScheduleActionId;
40744
+ const scheduleActionId = firstString2(row, ["scheduleActionId", "schedule_action_id", "scheduleId", "schedule_id"]) || defaultScheduleActionId;
40371
40745
  row.connections.forEach((connection) => flattened.push({ row: connection, scheduleActionId }));
40372
40746
  } else {
40373
40747
  flattened.push({ row, scheduleActionId: defaultScheduleActionId });
@@ -40377,15 +40751,15 @@ function sanitizeBindingRows(body, defaultScheduleActionId) {
40377
40751
  const result = [];
40378
40752
  for (const item of flattened) {
40379
40753
  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"]);
40754
+ const connectionId = firstString2(item.row, ["connectionId", "connection_id"]);
40755
+ const providerConfigKey = firstString2(item.row, ["providerConfigKey", "provider_config_key", "integrationId", "integration_id", "provider"]);
40382
40756
  if (!connectionId || !providerConfigKey) continue;
40383
40757
  result.push({
40384
- scheduleActionId: firstString(item.row, ["scheduleActionId", "schedule_action_id", "scheduleId", "schedule_id"]) || item.scheduleActionId,
40758
+ scheduleActionId: firstString2(item.row, ["scheduleActionId", "schedule_action_id", "scheduleId", "schedule_id"]) || item.scheduleActionId,
40385
40759
  connectionId,
40386
40760
  providerConfigKey,
40387
40761
  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"])
40762
+ label: firstString2(item.row, ["label", "accountLabel", "account_label", "displayName", "display_name"])
40389
40763
  });
40390
40764
  }
40391
40765
  return result;
@@ -40767,7 +41141,7 @@ function cleanString2(value, max = 300) {
40767
41141
  const trimmed = value.trim();
40768
41142
  return trimmed ? trimmed.slice(0, max) : null;
40769
41143
  }
40770
- function firstString2(record, keys, max = 300) {
41144
+ function firstString3(record, keys, max = 300) {
40771
41145
  for (const key of keys) {
40772
41146
  const value = cleanString2(record[key], max);
40773
41147
  if (value) return value;
@@ -40839,7 +41213,7 @@ async function getResendCatalog() {
40839
41213
  const result = [];
40840
41214
  for (const row of rows) {
40841
41215
  if (!isRecord3(row)) continue;
40842
- const id = firstString2(row, ["providerConfigKey", "provider_config_key", "id"]);
41216
+ const id = firstString3(row, ["providerConfigKey", "provider_config_key", "id"]);
40843
41217
  if (id !== RESEND_PROVIDER_CONFIG_KEY) continue;
40844
41218
  const safeDefaultAllowedTools = cleanTools2(
40845
41219
  row.safeDefaultAllowedTools ?? row.safe_default_allowed_tools ?? row.readTools ?? row.read_tools
@@ -40849,8 +41223,8 @@ async function getResendCatalog() {
40849
41223
  result.push({
40850
41224
  providerConfigKey: RESEND_PROVIDER_CONFIG_KEY,
40851
41225
  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.",
41226
+ label: firstString3(row, ["label", "displayName", "display_name", "name"]) || "Resend",
41227
+ description: firstString3(row, ["description"], 500) || "Connect Resend through its official remote MCP to read email operations and run explicitly enabled delivery workflows.",
40854
41228
  logoUrl: cleanHttpsUrl2(row.logoUrl ?? row.logo_url ?? row.logo) || RESEND_LOGO_URL,
40855
41229
  docsUrl: cleanHttpsUrl2(row.docsUrl ?? row.docs_url ?? row.docs) || RESEND_DOCS_URL,
40856
41230
  authMode: "REMOTE_MCP_OAUTH2",
@@ -40860,7 +41234,7 @@ async function getResendCatalog() {
40860
41234
  adminBlockedTools: adminBlockedTools.length > 0 ? adminBlockedTools : [...RESEND_ADMIN_BLOCKED_TOOLS],
40861
41235
  connectionSyncSupported: true,
40862
41236
  connectionSyncRequiredTools: [...RESEND_CONNECTION_SYNC_REQUIRED_TOOLS],
40863
- platformSetupStatus: firstString2(row, ["platformSetupStatus", "platform_setup_status", "setupStatus", "setup_status"], 100) || "ready",
41237
+ platformSetupStatus: firstString3(row, ["platformSetupStatus", "platform_setup_status", "setupStatus", "setup_status"], 100) || "ready",
40864
41238
  appReviewStatus: null,
40865
41239
  appReviewNote: "Secret-returning webhook creation, API-key, OAuth-grant, and raw editor-session access is permanently blocked from agents and schedules.",
40866
41240
  transport: "remote_mcp"
@@ -40875,10 +41249,10 @@ async function getResendConnections(identity) {
40875
41249
  const result = [];
40876
41250
  for (const row of rows) {
40877
41251
  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;
41252
+ const connectionId = firstString3(row, ["connectionId", "connection_id", "id"]);
41253
+ const providerConfigKey = firstString3(row, ["providerConfigKey", "provider_config_key", "provider"]) || RESEND_PROVIDER_CONFIG_KEY;
40880
41254
  if (!connectionId || providerConfigKey !== RESEND_PROVIDER_CONFIG_KEY) continue;
40881
- const rawStatus = (firstString2(row, ["status"], 64) || "needs_reauth").toLowerCase();
41255
+ const rawStatus = (firstString3(row, ["status"], 64) || "needs_reauth").toLowerCase();
40882
41256
  const pending = ["pending", "pending_oauth", "authorizing", "authorization_pending"].includes(rawStatus);
40883
41257
  const active = ["active", "connected", "ready"].includes(rawStatus);
40884
41258
  const reconnectRequired = row.reconnectRequired === true || row.reconnect_required === true || !pending && !active;
@@ -40886,7 +41260,7 @@ async function getResendConnections(identity) {
40886
41260
  connectionId,
40887
41261
  providerConfigKey: RESEND_PROVIDER_CONFIG_KEY,
40888
41262
  provider: RESEND_PROVIDER_CONFIG_KEY,
40889
- label: firstString2(row, ["label", "accountLabel", "account_label", "displayName", "display_name"]) || "Resend account",
41263
+ label: firstString3(row, ["label", "accountLabel", "account_label", "displayName", "display_name"]) || "Resend account",
40890
41264
  status: pending ? "pending_oauth" : reconnectRequired ? "needs_reauth" : "connected",
40891
41265
  reconnectRequired,
40892
41266
  actionsEnabled: row.actionsEnabled === true || row.actions_enabled === true,
@@ -40895,11 +41269,11 @@ async function getResendConnections(identity) {
40895
41269
  adminBlockedTools: cleanTools2(row.adminBlockedTools ?? row.admin_blocked_tools).length > 0 ? cleanTools2(row.adminBlockedTools ?? row.admin_blocked_tools) : [...RESEND_ADMIN_BLOCKED_TOOLS],
40896
41270
  mcpEndpoint: null,
40897
41271
  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),
41272
+ toolRevision: firstString3(row, ["toolRevision", "tool_revision"], 200),
41273
+ vaultName: firstString3(row, ["vaultName", "vault_name"], 100),
41274
+ tableName: firstString3(row, ["tableName", "table_name"], 100),
41275
+ createdAt: firstString3(row, ["createdAt", "created_at"], 100),
41276
+ updatedAt: firstString3(row, ["updatedAt", "updated_at"], 100),
40903
41277
  transport: "remote_mcp"
40904
41278
  });
40905
41279
  }
@@ -40913,7 +41287,7 @@ function sanitizeConnectSession2(body) {
40913
41287
  return {
40914
41288
  connectLink,
40915
41289
  sessionToken: null,
40916
- expiresAt: firstString2(data, ["expiresAt", "expires_at"], 100)
41290
+ expiresAt: firstString3(data, ["expiresAt", "expires_at"], 100)
40917
41291
  };
40918
41292
  }
40919
41293
  async function createResendConnectSession(identity, redirectUri) {
@@ -40975,16 +41349,16 @@ async function describeResendTool(identity, connectionId, tool) {
40975
41349
  const data = unwrapData2(body);
40976
41350
  const rawTool = isRecord3(data) && isRecord3(data.tool) ? data.tool : data;
40977
41351
  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);
41352
+ const name = firstString3(rawTool, ["name"], 200);
41353
+ const classification = firstString3(rawTool, ["classification", "kind"], 20);
40980
41354
  const inputSchema = rawTool.inputSchema ?? rawTool.input_schema;
40981
41355
  if (!name || name !== tool || classification !== "read" && classification !== "action" || !isRecord3(inputSchema)) {
40982
41356
  throw new ResendControlError("Resend returned an invalid tool description.");
40983
41357
  }
40984
41358
  return {
40985
41359
  name,
40986
- title: firstString2(rawTool, ["title"], 300),
40987
- description: firstString2(rawTool, ["description"], 2e3),
41360
+ title: firstString3(rawTool, ["title"], 300),
41361
+ description: firstString3(rawTool, ["description"], 2e3),
40988
41362
  inputSchema,
40989
41363
  classification
40990
41364
  };