@thenavidm/facebook-ad-library-mcp 0.1.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 (73) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +359 -0
  3. package/SKILL.md +115 -0
  4. package/dist/adlibrary/harvest.d.ts +35 -0
  5. package/dist/adlibrary/harvest.js +153 -0
  6. package/dist/adlibrary/harvest.js.map +1 -0
  7. package/dist/adlibrary/normalize.d.ts +25 -0
  8. package/dist/adlibrary/normalize.js +221 -0
  9. package/dist/adlibrary/normalize.js.map +1 -0
  10. package/dist/adlibrary/types.d.ts +101 -0
  11. package/dist/adlibrary/types.js +17 -0
  12. package/dist/adlibrary/types.js.map +1 -0
  13. package/dist/adlibrary/url.d.ts +10 -0
  14. package/dist/adlibrary/url.js +29 -0
  15. package/dist/adlibrary/url.js.map +1 -0
  16. package/dist/archive/client.d.ts +45 -0
  17. package/dist/archive/client.js +124 -0
  18. package/dist/archive/client.js.map +1 -0
  19. package/dist/backends/apify.d.ts +37 -0
  20. package/dist/backends/apify.js +152 -0
  21. package/dist/backends/apify.js.map +1 -0
  22. package/dist/backends/browser.d.ts +51 -0
  23. package/dist/backends/browser.js +202 -0
  24. package/dist/backends/browser.js.map +1 -0
  25. package/dist/backends/index.d.ts +8 -0
  26. package/dist/backends/index.js +25 -0
  27. package/dist/backends/index.js.map +1 -0
  28. package/dist/backends/scrapecreators.d.ts +29 -0
  29. package/dist/backends/scrapecreators.js +185 -0
  30. package/dist/backends/scrapecreators.js.map +1 -0
  31. package/dist/config.d.ts +18 -0
  32. package/dist/config.js +33 -0
  33. package/dist/config.js.map +1 -0
  34. package/dist/doctor.d.ts +10 -0
  35. package/dist/doctor.js +131 -0
  36. package/dist/doctor.js.map +1 -0
  37. package/dist/errors.d.ts +16 -0
  38. package/dist/errors.js +25 -0
  39. package/dist/errors.js.map +1 -0
  40. package/dist/format/ads.d.ts +16 -0
  41. package/dist/format/ads.js +97 -0
  42. package/dist/format/ads.js.map +1 -0
  43. package/dist/index.d.ts +9 -0
  44. package/dist/index.js +84 -0
  45. package/dist/index.js.map +1 -0
  46. package/dist/server.d.ts +21 -0
  47. package/dist/server.js +156 -0
  48. package/dist/server.js.map +1 -0
  49. package/dist/store/snapshots.d.ts +29 -0
  50. package/dist/store/snapshots.js +83 -0
  51. package/dist/store/snapshots.js.map +1 -0
  52. package/dist/tools/index.d.ts +5 -0
  53. package/dist/tools/index.js +14 -0
  54. package/dist/tools/index.js.map +1 -0
  55. package/dist/tools/kit.d.ts +70 -0
  56. package/dist/tools/kit.js +90 -0
  57. package/dist/tools/kit.js.map +1 -0
  58. package/dist/tools/search.d.ts +5 -0
  59. package/dist/tools/search.js +165 -0
  60. package/dist/tools/search.js.map +1 -0
  61. package/dist/tools/session.d.ts +3 -0
  62. package/dist/tools/session.js +23 -0
  63. package/dist/tools/session.js.map +1 -0
  64. package/dist/tools/track.d.ts +9 -0
  65. package/dist/tools/track.js +68 -0
  66. package/dist/tools/track.js.map +1 -0
  67. package/dist/tools/transparency.d.ts +5 -0
  68. package/dist/tools/transparency.js +53 -0
  69. package/dist/tools/transparency.js.map +1 -0
  70. package/dist/transport/http.d.ts +23 -0
  71. package/dist/transport/http.js +88 -0
  72. package/dist/transport/http.js.map +1 -0
  73. package/package.json +64 -0
@@ -0,0 +1,185 @@
1
+ /**
2
+ * ScrapeCreators backend. Needs a key, works serverless, about $1.88 per 1,000 ads.
3
+ *
4
+ * Use this instead of the browser when Chromium cannot run (a serverless
5
+ * function, a container without a browser) or when the volume makes babysitting
6
+ * Meta's rate limits not worth it.
7
+ *
8
+ * Request parameters are taken from their published OpenAPI spec, read
9
+ * 2026-08-31. Their *response* envelope is not published, so nothing here
10
+ * assumes a path into it: we walk the payload for ad-shaped objects. See
11
+ * `adlibrary/harvest.ts`.
12
+ *
13
+ * One thing this backend can do that the browser cannot: `transcribe`, which is
14
+ * server-side speech to text on a video ad.
15
+ */
16
+ import { AdLibraryError } from "../errors.js";
17
+ import { findConnections, parsePayloads } from "../adlibrary/harvest.js";
18
+ import { adFromGraphql } from "../adlibrary/normalize.js";
19
+ const BASE = "https://api.scrapecreators.com";
20
+ // Their vocabulary is uppercase, ours is lowercase. Map at the boundary.
21
+ const STATUS = { active: "ACTIVE", inactive: "INACTIVE", all: "ALL" };
22
+ const MEDIA = {
23
+ all: "ALL",
24
+ image: "IMAGE",
25
+ meme: "MEME",
26
+ video: "VIDEO",
27
+ none: "NONE",
28
+ };
29
+ /** Their cursor field is undocumented, so accept any plausible spelling. */
30
+ function cursorFrom(payload) {
31
+ for (const conn of findConnections(payload)) {
32
+ const info = conn["page_info"];
33
+ if (typeof info === "object" && info !== null) {
34
+ const endCursor = info["end_cursor"];
35
+ if (typeof endCursor === "string") {
36
+ return { cursor: endCursor, hasMore: info["has_next_page"] === true };
37
+ }
38
+ }
39
+ }
40
+ if (typeof payload === "object" && payload !== null) {
41
+ for (const key of ["cursor", "nextCursor", "next_cursor", "paginationToken"]) {
42
+ const value = payload[key];
43
+ if (typeof value === "string" && value)
44
+ return { cursor: value, hasMore: true };
45
+ }
46
+ }
47
+ return { hasMore: false };
48
+ }
49
+ export class ScrapeCreatorsBackend {
50
+ apiKey;
51
+ timeoutMs;
52
+ name = "scrapecreators";
53
+ needsKey = true;
54
+ constructor(apiKey, timeoutMs = 120_000) {
55
+ this.apiKey = apiKey;
56
+ this.timeoutMs = timeoutMs;
57
+ if (!apiKey) {
58
+ throw new AdLibraryError("The scrapecreators backend needs an API key.", {
59
+ backend: this.name,
60
+ hint: "Set SCRAPECREATORS_API_KEY, or use the free browser backend with FBADS_BACKEND=browser.",
61
+ });
62
+ }
63
+ }
64
+ async close() {
65
+ // Nothing to release: this backend is stateless fetch calls.
66
+ }
67
+ async get(path, params) {
68
+ const url = new URL(path, BASE);
69
+ for (const [key, value] of Object.entries(params)) {
70
+ if (value === undefined || value === null || value === "" || value === false)
71
+ continue;
72
+ url.searchParams.set(key, String(value));
73
+ }
74
+ const response = await fetch(url, {
75
+ headers: { "x-api-key": this.apiKey },
76
+ signal: AbortSignal.timeout(this.timeoutMs),
77
+ });
78
+ if (response.status === 401) {
79
+ throw new AdLibraryError("ScrapeCreators rejected the key (401).", {
80
+ backend: this.name,
81
+ hint: "Check SCRAPECREATORS_API_KEY.",
82
+ });
83
+ }
84
+ if (response.status === 402) {
85
+ throw new AdLibraryError("ScrapeCreators is out of credits (402).", {
86
+ backend: this.name,
87
+ hint: "Top up the account, or switch to the free browser backend with FBADS_BACKEND=browser.",
88
+ });
89
+ }
90
+ if (!response.ok) {
91
+ const body = (await response.text()).slice(0, 300);
92
+ throw new AdLibraryError(`ScrapeCreators error ${response.status}: ${body}`, {
93
+ backend: this.name,
94
+ });
95
+ }
96
+ try {
97
+ return await response.json();
98
+ }
99
+ catch {
100
+ throw new AdLibraryError("ScrapeCreators returned a body that is not JSON.", {
101
+ backend: this.name,
102
+ });
103
+ }
104
+ }
105
+ async search(params) {
106
+ if (!params.query && !params.pageId) {
107
+ throw new AdLibraryError("Pass either query or page_id.", { backend: this.name });
108
+ }
109
+ const limit = params.limit ?? 30;
110
+ const common = {
111
+ country: params.country ?? "US",
112
+ status: STATUS[params.activeStatus ?? "active"],
113
+ media_type: MEDIA[params.mediaType ?? "all"],
114
+ cursor: params.cursor,
115
+ };
116
+ const payload = params.pageId
117
+ ? await this.get("/v1/facebook/adLibrary/company/ads", { ...common, pageId: params.pageId })
118
+ : await this.get("/v1/facebook/adLibrary/search/ads", {
119
+ ...common,
120
+ query: params.query,
121
+ ad_type: params.adType === "political_and_issue_ads" ? "POLITICAL_AND_ISSUE_ADS" : "ALL",
122
+ });
123
+ const harvest = parsePayloads(payload);
124
+ const fallback = harvest.cursor ? undefined : cursorFrom(payload);
125
+ const ads = harvest.nodes.map((node) => adFromGraphql(node, this.name));
126
+ const kept = ads.slice(0, limit);
127
+ return {
128
+ backend: this.name,
129
+ count: kept.length,
130
+ hasMore: harvest.hasMore || fallback?.hasMore || ads.length > limit,
131
+ cursor: harvest.cursor ?? fallback?.cursor,
132
+ totalAvailable: harvest.total,
133
+ note: kept.length > 0 ? undefined : "ScrapeCreators returned no ads for these filters.",
134
+ ads: kept,
135
+ };
136
+ }
137
+ async listAdvertisers(query) {
138
+ const payload = await this.get("/v1/facebook/adLibrary/search/companies", { query });
139
+ const items = Array.isArray(payload)
140
+ ? payload
141
+ : (payload?.["searchResults"] ??
142
+ payload?.["results"] ??
143
+ payload?.["companies"] ??
144
+ []);
145
+ const out = [];
146
+ for (const raw of Array.isArray(items) ? items : []) {
147
+ if (typeof raw !== "object" || raw === null)
148
+ continue;
149
+ const item = raw;
150
+ const pageId = item["page_id"] ?? item["pageId"] ?? item["id"];
151
+ if (!pageId)
152
+ continue;
153
+ out.push({
154
+ pageId: String(pageId),
155
+ pageName: (item["name"] ?? item["page_name"] ?? item["pageName"]),
156
+ pageUrl: (item["page_url"] ?? item["pageUrl"]),
157
+ pageLikes: (item["likes"] ?? item["page_like_count"]),
158
+ verified: (item["verified"] ?? item["is_verified"]),
159
+ category: item["category"],
160
+ profilePictureUrl: (item["image_uri"] ?? item["profile_picture_url"]),
161
+ });
162
+ }
163
+ return out;
164
+ }
165
+ async getAd(libraryId) {
166
+ const payload = await this.get("/v1/facebook/adLibrary/ad", { id: libraryId });
167
+ const harvest = parsePayloads(payload);
168
+ const node = harvest.nodes[0];
169
+ return node ? adFromGraphql(node, this.name) : undefined;
170
+ }
171
+ async transcribe(libraryId) {
172
+ const payload = await this.get("/v1/facebook/adLibrary/ad/transcript", { id: libraryId });
173
+ if (typeof payload === "string")
174
+ return payload;
175
+ if (typeof payload === "object" && payload !== null) {
176
+ for (const key of ["transcript", "text", "transcription"]) {
177
+ const value = payload[key];
178
+ if (typeof value === "string" && value)
179
+ return value;
180
+ }
181
+ }
182
+ return undefined;
183
+ }
184
+ }
185
+ //# sourceMappingURL=scrapecreators.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scrapecreators.js","sourceRoot":"","sources":["../../src/backends/scrapecreators.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Settings, all from the environment.
3
+ *
4
+ * Environment variables rather than CLI flags: a user editing a client config is
5
+ * already inside a JSON `env` block, and flags mean editing `args` separately.
6
+ */
7
+ import type { BackendName } from "./adlibrary/types.js";
8
+ export type Config = {
9
+ backend: BackendName;
10
+ headless: boolean;
11
+ scrapeCreatorsKey?: string;
12
+ apifyToken?: string;
13
+ archiveToken?: string;
14
+ storeDir?: string;
15
+ hydrateMs: number;
16
+ scrollWaitMs: number;
17
+ };
18
+ export declare function loadConfig(): Config;
package/dist/config.js ADDED
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Settings, all from the environment.
3
+ *
4
+ * Environment variables rather than CLI flags: a user editing a client config is
5
+ * already inside a JSON `env` block, and flags mean editing `args` separately.
6
+ */
7
+ function intFromEnv(name, fallback) {
8
+ const raw = process.env[name];
9
+ if (!raw)
10
+ return fallback;
11
+ const value = Number.parseInt(raw, 10);
12
+ return Number.isFinite(value) && value > 0 ? value : fallback;
13
+ }
14
+ function backendFromEnv() {
15
+ const raw = (process.env["FBADS_BACKEND"] ?? "browser").toLowerCase();
16
+ if (raw === "scrapecreators" || raw === "apify" || raw === "browser")
17
+ return raw;
18
+ return "browser";
19
+ }
20
+ export function loadConfig() {
21
+ return {
22
+ backend: backendFromEnv(),
23
+ // Headed is for watching why a search came back empty.
24
+ headless: process.env["FBADS_HEADED"] !== "1",
25
+ scrapeCreatorsKey: process.env["SCRAPECREATORS_API_KEY"] || undefined,
26
+ apifyToken: process.env["APIFY_TOKEN"] || undefined,
27
+ archiveToken: process.env["META_ADS_ARCHIVE_TOKEN"] || undefined,
28
+ storeDir: process.env["FBADS_STORE_DIR"] || undefined,
29
+ hydrateMs: intFromEnv("FBADS_HYDRATE_MS", 9000),
30
+ scrollWaitMs: intFromEnv("FBADS_SCROLL_WAIT_MS", 4000),
31
+ };
32
+ }
33
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAeH,SAAS,UAAU,CAAC,IAAY,EAAE,QAAgB;IAChD,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAC9B,IAAI,CAAC,GAAG;QAAE,OAAO,QAAQ,CAAC;IAC1B,MAAM,KAAK,GAAG,MAAM,CAAC,QAAQ,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;IACvC,OAAO,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,QAAQ,CAAC;AAChE,CAAC;AAED,SAAS,cAAc;IACrB,MAAM,GAAG,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,eAAe,CAAC,IAAI,SAAS,CAAC,CAAC,WAAW,EAAE,CAAC;IACtE,IAAI,GAAG,KAAK,gBAAgB,IAAI,GAAG,KAAK,OAAO,IAAI,GAAG,KAAK,SAAS;QAAE,OAAO,GAAG,CAAC;IACjF,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,MAAM,UAAU,UAAU;IACxB,OAAO;QACL,OAAO,EAAE,cAAc,EAAE;QACzB,uDAAuD;QACvD,QAAQ,EAAE,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,KAAK,GAAG;QAC7C,iBAAiB,EAAE,OAAO,CAAC,GAAG,CAAC,wBAAwB,CAAC,IAAI,SAAS;QACrE,UAAU,EAAE,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC,IAAI,SAAS;QACnD,YAAY,EAAE,OAAO,CAAC,GAAG,CAAC,wBAAwB,CAAC,IAAI,SAAS;QAChE,QAAQ,EAAE,OAAO,CAAC,GAAG,CAAC,iBAAiB,CAAC,IAAI,SAAS;QACrD,SAAS,EAAE,UAAU,CAAC,kBAAkB,EAAE,IAAI,CAAC;QAC/C,YAAY,EAAE,UAAU,CAAC,sBAAsB,EAAE,IAAI,CAAC;KACvD,CAAC;AACJ,CAAC"}
@@ -0,0 +1,10 @@
1
+ /**
2
+ * `facebook-ad-library-mcp doctor`
3
+ *
4
+ * The command someone runs when a tool came back empty and they cannot tell
5
+ * whether the problem is their setup, Meta, or the search itself. It answers
6
+ * that without needing an MCP client attached.
7
+ *
8
+ * Exit code 0 means usable. 1 means something needs fixing.
9
+ */
10
+ export declare function runDoctor(): Promise<number>;
package/dist/doctor.js ADDED
@@ -0,0 +1,131 @@
1
+ /**
2
+ * `facebook-ad-library-mcp doctor`
3
+ *
4
+ * The command someone runs when a tool came back empty and they cannot tell
5
+ * whether the problem is their setup, Meta, or the search itself. It answers
6
+ * that without needing an MCP client attached.
7
+ *
8
+ * Exit code 0 means usable. 1 means something needs fixing.
9
+ */
10
+ import { loadConfig } from "./config.js";
11
+ import { buildUrl } from "./adlibrary/url.js";
12
+ import { inlinePayloads, parsePayloads } from "./adlibrary/harvest.js";
13
+ function line(check) {
14
+ return `${check.ok ? " ok " : " fix "} ${check.label}\n ${check.detail}\n`;
15
+ }
16
+ async function checkPlaywright() {
17
+ try {
18
+ await import("playwright");
19
+ }
20
+ catch {
21
+ return {
22
+ label: "Playwright",
23
+ ok: false,
24
+ detail: "Not installed. Run `npx playwright install chromium`, or use a provider backend.",
25
+ };
26
+ }
27
+ try {
28
+ const { chromium } = await import("playwright");
29
+ const browser = await chromium.launch({ headless: true });
30
+ await browser.close();
31
+ return { label: "Playwright", ok: true, detail: "Chromium launches." };
32
+ }
33
+ catch (error) {
34
+ return {
35
+ label: "Playwright",
36
+ ok: false,
37
+ detail: `Installed, but Chromium will not launch: ${error.message}. Run \`npx playwright install chromium\`.`,
38
+ };
39
+ }
40
+ }
41
+ /** The check that matters: can we actually read ads right now. */
42
+ async function checkLiveRead() {
43
+ try {
44
+ const { chromium } = await import("playwright");
45
+ const browser = await chromium.launch({ headless: true });
46
+ try {
47
+ const page = await browser.newPage();
48
+ const bodies = [];
49
+ page.on("response", (response) => {
50
+ if (String(response.url()).includes("/api/graphql")) {
51
+ void response.text().then((b) => bodies.push(b)).catch(() => undefined);
52
+ }
53
+ });
54
+ await page.goto(buildUrl({ query: "shoes", country: "US" }), {
55
+ waitUntil: "domcontentloaded",
56
+ timeout: 60_000,
57
+ });
58
+ await page.waitForTimeout(9000);
59
+ bodies.push(...inlinePayloads(await page.content()));
60
+ const harvest = parsePayloads(bodies);
61
+ if (harvest.captcha) {
62
+ return {
63
+ label: "Live read",
64
+ ok: false,
65
+ detail: "Meta served a captcha. Wait a few minutes, or use a provider backend.",
66
+ };
67
+ }
68
+ if (harvest.nodes.length === 0) {
69
+ return {
70
+ label: "Live read",
71
+ ok: false,
72
+ detail: "Connected, but no ads parsed. Meta may be rate limiting this machine.",
73
+ };
74
+ }
75
+ return {
76
+ label: "Live read",
77
+ ok: true,
78
+ detail: `Read ${harvest.nodes.length} ads for a test search.`,
79
+ };
80
+ }
81
+ finally {
82
+ await browser.close();
83
+ }
84
+ }
85
+ catch (error) {
86
+ return { label: "Live read", ok: false, detail: error.message };
87
+ }
88
+ }
89
+ export async function runDoctor() {
90
+ const config = loadConfig();
91
+ const checks = [];
92
+ checks.push({
93
+ label: "Backend",
94
+ ok: true,
95
+ detail: `${config.backend}${config.backend === "browser" ? " (free, no key)" : " (bills per ad)"}`,
96
+ });
97
+ if (config.backend === "browser") {
98
+ const playwright = await checkPlaywright();
99
+ checks.push(playwright);
100
+ if (playwright.ok)
101
+ checks.push(await checkLiveRead());
102
+ }
103
+ if (config.backend === "scrapecreators") {
104
+ checks.push({
105
+ label: "SCRAPECREATORS_API_KEY",
106
+ ok: Boolean(config.scrapeCreatorsKey),
107
+ detail: config.scrapeCreatorsKey ? "Set." : "Not set. Get one at scrapecreators.com.",
108
+ });
109
+ }
110
+ if (config.backend === "apify") {
111
+ checks.push({
112
+ label: "APIFY_TOKEN",
113
+ ok: Boolean(config.apifyToken),
114
+ detail: config.apifyToken ? "Set." : "Not set. Get one from your Apify account settings.",
115
+ });
116
+ }
117
+ checks.push({
118
+ label: "META_ADS_ARCHIVE_TOKEN",
119
+ ok: true,
120
+ detail: config.archiveToken
121
+ ? "Set. get_eu_transparency will return EU spend and reach."
122
+ : "Not set. Optional: everything works without it except get_eu_transparency.",
123
+ });
124
+ process.stdout.write(`facebook-ad-library-mcp doctor\n\n${checks.map(line).join("")}\n`);
125
+ const failed = checks.filter((c) => !c.ok);
126
+ process.stdout.write(failed.length === 0
127
+ ? "Everything checks out.\n"
128
+ : `${failed.length} thing${failed.length === 1 ? "" : "s"} to fix, listed above.\n`);
129
+ return failed.length === 0 ? 0 : 1;
130
+ }
131
+ //# sourceMappingURL=doctor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"doctor.js","sourceRoot":"","sources":["../src/doctor.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,16 @@
1
+ /**
2
+ * One error type, so every failure reaching a model says what to do next.
3
+ *
4
+ * A tool that fails with "Error: request failed" costs the model a turn and
5
+ * usually a wrong guess. Every message here names the fix: which variable to
6
+ * set, which backend to switch to, how long to wait.
7
+ */
8
+ export declare class AdLibraryError extends Error {
9
+ readonly hint?: string;
10
+ readonly backend?: string;
11
+ constructor(message: string, options?: {
12
+ hint?: string;
13
+ backend?: string;
14
+ });
15
+ toJSON(): Record<string, unknown>;
16
+ }
package/dist/errors.js ADDED
@@ -0,0 +1,25 @@
1
+ /**
2
+ * One error type, so every failure reaching a model says what to do next.
3
+ *
4
+ * A tool that fails with "Error: request failed" costs the model a turn and
5
+ * usually a wrong guess. Every message here names the fix: which variable to
6
+ * set, which backend to switch to, how long to wait.
7
+ */
8
+ export class AdLibraryError extends Error {
9
+ hint;
10
+ backend;
11
+ constructor(message, options = {}) {
12
+ super(message);
13
+ this.name = "AdLibraryError";
14
+ this.hint = options.hint;
15
+ this.backend = options.backend;
16
+ }
17
+ toJSON() {
18
+ return {
19
+ error: this.message,
20
+ ...(this.hint ? { hint: this.hint } : {}),
21
+ ...(this.backend ? { backend: this.backend } : {}),
22
+ };
23
+ }
24
+ }
25
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,MAAM,OAAO,cAAe,SAAQ,KAAK;IAC9B,IAAI,CAAU;IACd,OAAO,CAAU;IAE1B,YAAY,OAAe,EAAE,UAA+C,EAAE;QAC5E,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,gBAAgB,CAAC;QAC7B,IAAI,CAAC,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;QACzB,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC;IACjC,CAAC;IAED,MAAM;QACJ,OAAO;YACL,KAAK,EAAE,IAAI,CAAC,OAAO;YACnB,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YACzC,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACnD,CAAC;IACJ,CAAC;CACF"}
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Shaping ads for a model rather than for a parser.
3
+ *
4
+ * A raw ad card is mostly nulls and CDN URLs. Dumping thirty of them spends
5
+ * thousands of tokens on fields nobody reads, and buries the three that decide
6
+ * whether an ad is worth studying: how long it has run, what it says, and where
7
+ * it sends people.
8
+ *
9
+ * So the list view is compact and the detail view is complete. `get_ad` returns
10
+ * everything; `search_ads` returns enough to choose which ad to look at.
11
+ */
12
+ import type { Ad } from "../adlibrary/types.js";
13
+ /** One ad in a result list. Enough to judge it, not everything about it. */
14
+ export declare function summarise(ad: Ad): Record<string, unknown>;
15
+ /** One ad in full, for `get_ad`. Every creative, every copy variant. */
16
+ export declare function detail(ad: Ad): Record<string, unknown>;
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Shaping ads for a model rather than for a parser.
3
+ *
4
+ * A raw ad card is mostly nulls and CDN URLs. Dumping thirty of them spends
5
+ * thousands of tokens on fields nobody reads, and buries the three that decide
6
+ * whether an ad is worth studying: how long it has run, what it says, and where
7
+ * it sends people.
8
+ *
9
+ * So the list view is compact and the detail view is complete. `get_ad` returns
10
+ * everything; `search_ads` returns enough to choose which ad to look at.
11
+ */
12
+ /** Drop keys that are undefined or empty, so the JSON a model reads has no noise. */
13
+ function compact(input) {
14
+ const out = {};
15
+ for (const [key, value] of Object.entries(input)) {
16
+ if (value === undefined || value === null || value === "")
17
+ continue;
18
+ if (Array.isArray(value) && value.length === 0)
19
+ continue;
20
+ out[key] = value;
21
+ }
22
+ return out;
23
+ }
24
+ function truncate(text, max) {
25
+ if (!text)
26
+ return undefined;
27
+ return text.length <= max ? text : `${text.slice(0, max - 1)}…`;
28
+ }
29
+ /** One ad in a result list. Enough to judge it, not everything about it. */
30
+ export function summarise(ad) {
31
+ const videos = ad.creatives.filter((c) => c.kind === "video").length;
32
+ return compact({
33
+ library_id: ad.libraryId,
34
+ advertiser: ad.pageName,
35
+ page_id: ad.pageId,
36
+ active: ad.isActive,
37
+ started: ad.startedRunning,
38
+ stopped: ad.stoppedRunning,
39
+ days_active: ad.daysActive,
40
+ platforms: ad.platforms,
41
+ format: ad.displayFormat,
42
+ cta: ad.ctaText,
43
+ landing_domain: ad.linkDomain,
44
+ body: truncate(ad.body, 600),
45
+ creatives: ad.creatives.length,
46
+ videos: videos > 0 ? videos : undefined,
47
+ variants_using_creative: ad.variantsUsingCreative,
48
+ spend: ad.spend,
49
+ reach: ad.reachEstimate,
50
+ details: ad.adDetailsUrl,
51
+ });
52
+ }
53
+ /** One ad in full, for `get_ad`. Every creative, every copy variant. */
54
+ export function detail(ad) {
55
+ return compact({
56
+ library_id: ad.libraryId,
57
+ details_url: ad.adDetailsUrl,
58
+ advertiser: compact({
59
+ name: ad.pageName,
60
+ page_id: ad.pageId,
61
+ url: ad.pageUrl,
62
+ likes: ad.pageLikes,
63
+ categories: ad.pageCategories,
64
+ profile_picture: ad.pageProfilePictureUrl,
65
+ }),
66
+ run: compact({
67
+ active: ad.isActive,
68
+ started: ad.startedRunning,
69
+ stopped: ad.stoppedRunning,
70
+ days_active: ad.daysActive,
71
+ platforms: ad.platforms,
72
+ countries: ad.countries,
73
+ categories: ad.categories,
74
+ }),
75
+ copy: compact({
76
+ title: ad.title,
77
+ body: ad.body,
78
+ caption: ad.caption,
79
+ link_description: ad.linkDescription,
80
+ cta: ad.ctaText,
81
+ cta_type: ad.ctaType,
82
+ link_url: ad.linkUrl,
83
+ link_domain: ad.linkDomain,
84
+ }),
85
+ format: ad.displayFormat,
86
+ variants_using_creative: ad.variantsUsingCreative,
87
+ creatives: ad.creatives.map((creative) => compact({ ...creative })),
88
+ transparency: compact({
89
+ spend: ad.spend,
90
+ currency: ad.currency,
91
+ reach: ad.reachEstimate,
92
+ impressions: ad.impressions,
93
+ }),
94
+ source: ad.source,
95
+ });
96
+ }
97
+ //# sourceMappingURL=ads.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ads.js","sourceRoot":"","sources":["../../src/format/ads.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Entry point.
4
+ *
5
+ * `facebook-ad-library-mcp` stdio, which is what MCP clients launch
6
+ * `facebook-ad-library-mcp --http` HTTP, for running it somewhere always on
7
+ * `facebook-ad-library-mcp doctor` check the setup and say what is wrong
8
+ */
9
+ export {};