search_ad_library
Search ads by keywords and countries across the Meta Ad Library to support competitive research and ad transparency.
Instructions
Search the Meta Ad Library for ads. Allows searching by keywords, countries, and ad type. Useful for competitive research and transparency.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| search_terms | Yes | Keywords to search for in ads | |
| ad_reached_countries | Yes | Comma-separated country codes where ads were shown (e.g. 'US,GB,KR') | |
| ad_type | No | Ad type filter: ALL, POLITICAL_AND_ISSUE_ADS, HOUSING_ADS, etc. | |
| fields | No | Comma-separated fields to return | |
| limit | No | Number of results (default 25) | |
| after | No | Pagination cursor for next page | |
| order_by | No | Sort order for results |
Implementation Reference
- src/tools/ad_library.ts:19-36 (handler)The async handler function that executes the search_ad_library tool logic. It builds query params from user input, calls client.get('/ads_archive', params), and returns the JSON response with rate limit info.
async ({ search_terms, ad_reached_countries, ad_type, fields, limit, after, order_by }) => { try { const params: Record<string, unknown> = { search_terms, ad_reached_countries, }; if (ad_type) params.ad_type = ad_type; if (fields) params.fields = fields; if (limit) params.limit = limit; if (after) params.after = after; if (order_by) params.order_by = order_by; const { data, rateLimit } = await client.get(`/ads_archive`, params); return { content: [{ type: "text" as const, text: JSON.stringify({ ...data as object, _rateLimit: rateLimit }, null, 2) }] }; } catch (error) { return { content: [{ type: "text" as const, text: `Failed: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); - src/tools/ad_library.ts:10-18 (schema)Zod schema definitions for the search_ad_library tool's input parameters: search_terms, ad_reached_countries, ad_type, fields, limit, after, order_by.
{ search_terms: z.string().describe("Keywords to search for in ads"), ad_reached_countries: z.string().describe("Comma-separated country codes where ads were shown (e.g. 'US,GB,KR')"), ad_type: z.string().optional().describe("Ad type filter: ALL, POLITICAL_AND_ISSUE_ADS, HOUSING_ADS, etc."), fields: z.string().optional().describe("Comma-separated fields to return"), limit: z.number().optional().default(25).describe("Number of results (default 25)"), after: z.string().optional().describe("Pagination cursor for next page"), order_by: z.string().optional().describe("Sort order for results"), }, - src/tools/ad_library.ts:5-37 (registration)The registerAdLibraryTools function that registers 'search_ad_library' as an MCP tool on the server using server.tool().
export function registerAdLibraryTools(server: McpServer, client: AdsClient): void { // ─── search_ad_library ──────────────────────────────────────── server.tool( "search_ad_library", "Search the Meta Ad Library for ads. Allows searching by keywords, countries, and ad type. Useful for competitive research and transparency.", { search_terms: z.string().describe("Keywords to search for in ads"), ad_reached_countries: z.string().describe("Comma-separated country codes where ads were shown (e.g. 'US,GB,KR')"), ad_type: z.string().optional().describe("Ad type filter: ALL, POLITICAL_AND_ISSUE_ADS, HOUSING_ADS, etc."), fields: z.string().optional().describe("Comma-separated fields to return"), limit: z.number().optional().default(25).describe("Number of results (default 25)"), after: z.string().optional().describe("Pagination cursor for next page"), order_by: z.string().optional().describe("Sort order for results"), }, async ({ search_terms, ad_reached_countries, ad_type, fields, limit, after, order_by }) => { try { const params: Record<string, unknown> = { search_terms, ad_reached_countries, }; if (ad_type) params.ad_type = ad_type; if (fields) params.fields = fields; if (limit) params.limit = limit; if (after) params.after = after; if (order_by) params.order_by = order_by; const { data, rateLimit } = await client.get(`/ads_archive`, params); return { content: [{ type: "text" as const, text: JSON.stringify({ ...data as object, _rateLimit: rateLimit }, null, 2) }] }; } catch (error) { return { content: [{ type: "text" as const, text: `Failed: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); } - src/index.ts:96-96 (registration)Registration call in the main index.ts that wires up the tool registration function with the server and client.
registerAdLibraryTools(server, client); - src/services/ads-client.ts:115-321 (helper)The AdsClient class providing the get() method (line 180-184) used by the handler to call Meta's Graph API (baseUrl https://graph.facebook.com/v25.0) with rate limit parsing and error formatting.
private async request( method: string, path: string, params?: Record<string, unknown> ): Promise<ClientResponse> { if (!this.config.accessToken) { throw new Error( "META_ADS_ACCESS_TOKEN is not configured. Set it as an environment variable." ); } let url = `${this.baseUrl}${path}`; const init: RequestInit = { method, signal: AbortSignal.timeout(30_000) }; if (method === "GET" || method === "DELETE") { const qs = new URLSearchParams(); qs.set("access_token", this.config.accessToken); if (params) { for (const [k, v] of Object.entries(params)) { if (v !== undefined && v !== null && v !== "") { qs.set(k, String(v)); } } } url += `?${qs.toString()}`; } else { const body: Record<string, unknown> = { access_token: this.config.accessToken, ...params, }; init.headers = { "Content-Type": "application/json" }; init.body = JSON.stringify(body); } const res = await fetch(url, init); if (!res.ok) { const text = await res.text().catch(() => ""); let errorMsg: string; try { const errorBody = JSON.parse(text); errorMsg = this.formatError(errorBody); } catch { errorMsg = `Meta Ads API ${method} ${path} (${res.status}): ${text}`; } throw new Error(errorMsg); } const { rateLimit, businessRateLimit } = this.parseRateLimit(res.headers); const contentType = res.headers.get("content-type") || ""; if (contentType.includes("application/json")) { const data = await res.json(); if (data.error) { throw new Error(this.formatError(data)); } return { data, rateLimit, businessRateLimit }; } const text = await res.text(); return { data: text || { success: true }, rateLimit, businessRateLimit }; } // --- Convenience methods --- async get( path: string, params?: Record<string, unknown> ): Promise<ClientResponse> { return this.request("GET", path, params); } async post( path: string, params?: Record<string, unknown> ): Promise<ClientResponse> { return this.request("POST", path, params); } async delete( path: string, params?: Record<string, unknown> ): Promise<ClientResponse> { return this.request("DELETE", path, params); } // --- Upload (URL-based) --- async upload( path: string, fileUrl: string, params?: Record<string, unknown> ): Promise<ClientResponse> { return this.post(path, { ...params, url: fileUrl }); } // --- Account helpers --- get accountPath(): string { return `/act_${this.accountId}`; } get accountId(): string { if (!this.config.adAccountId) { throw new Error( "META_AD_ACCOUNT_ID is not configured. Set it as an environment variable." ); } return this.config.adAccountId; } get pixelId(): string { if (!this.config.pixelId) { throw new Error( "META_PIXEL_ID is not configured. Set it as an environment variable." ); } return this.config.pixelId; } get businessId(): string { if (!this.config.businessId) { throw new Error( "META_BUSINESS_ID is not configured. Set it as an environment variable." ); } return this.config.businessId; } // --- Token management --- /** Exchange short-lived token for long-lived token */ async exchangeToken(shortToken: string): Promise<ClientResponse> { if (!this.config.appId || !this.config.appSecret) { throw new Error( "META_APP_ID and META_APP_SECRET are required for token exchange." ); } const qs = new URLSearchParams({ grant_type: "fb_exchange_token", client_id: this.config.appId, client_secret: this.config.appSecret, fb_exchange_token: shortToken, }); const url = `${this.baseUrl}/oauth/access_token?${qs.toString()}`; const res = await fetch(url, { signal: AbortSignal.timeout(30_000) }); if (!res.ok) { const text = await res.text().catch(() => ""); throw new Error(`Token exchange failed (${res.status}): ${text}`); } const data = await res.json(); if (data.error) { throw new Error(this.formatError(data)); } return { data }; } /** Refresh a long-lived token */ async refreshToken(longToken: string): Promise<ClientResponse> { const qs = new URLSearchParams({ grant_type: "fb_exchange_token", access_token: longToken, }); const url = `${this.baseUrl}/oauth/access_token?${qs.toString()}`; const res = await fetch(url, { signal: AbortSignal.timeout(30_000) }); if (!res.ok) { const text = await res.text().catch(() => ""); throw new Error(`Token refresh failed (${res.status}): ${text}`); } const data = await res.json(); if (data.error) { throw new Error(this.formatError(data)); } return { data }; } /** Debug a token to inspect its properties */ async debugToken(inputToken: string): Promise<ClientResponse> { if (!this.config.appId || !this.config.appSecret) { throw new Error( "META_APP_ID and META_APP_SECRET are required for token debug." ); } const appToken = `${this.config.appId}|${this.config.appSecret}`; const qs = new URLSearchParams({ input_token: inputToken, access_token: appToken, }); const url = `${this.baseUrl}/debug_token?${qs.toString()}`; const res = await fetch(url, { signal: AbortSignal.timeout(30_000) }); if (!res.ok) { const text = await res.text().catch(() => ""); throw new Error(`Token debug failed (${res.status}): ${text}`); } const data = await res.json(); if (data.error) { throw new Error(this.formatError(data)); } return { data }; } }