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