Skip to main content
Glama
targeting.py•25.1 kB
"""Targeting search functionality for Meta Ads API.""" import json from typing import Optional, List, Dict, Any import os from .api import meta_api_tool, make_api_request from .server import mcp_server @mcp_server.tool() @meta_api_tool async def search_interests(query: str, access_token: Optional[str] = None, limit: int = 25) -> str: """ Search for interest targeting options by keyword. Args: query: Search term for interests (e.g., "baseball", "cooking", "travel") access_token: Meta API access token (optional - will use cached token if not provided) limit: Maximum number of results to return (default: 25) Returns: JSON string containing interest data with id, name, audience_size, and path fields """ if not query: return json.dumps({"error": "No search query provided"}, indent=2) endpoint = "search" params = { "type": "adinterest", "q": query, "limit": limit } data = await make_api_request(endpoint, access_token, params) return json.dumps(data, indent=2) @mcp_server.tool() @meta_api_tool async def get_interest_suggestions(interest_list: List[str], access_token: Optional[str] = None, limit: int = 25) -> str: """ Get interest suggestions based on existing interests. Args: interest_list: List of interest names to get suggestions for (e.g., ["Basketball", "Soccer"]) access_token: Meta API access token (optional - will use cached token if not provided) limit: Maximum number of suggestions to return (default: 25) Returns: JSON string containing suggested interests with id, name, audience_size, and description fields """ if not interest_list: return json.dumps({"error": "No interest list provided"}, indent=2) endpoint = "search" params = { "type": "adinterestsuggestion", "interest_list": json.dumps(interest_list), "limit": limit } data = await make_api_request(endpoint, access_token, params) return json.dumps(data, indent=2) @mcp_server.tool() @meta_api_tool async def estimate_audience_size( access_token: Optional[str] = None, account_id: Optional[str] = None, targeting: Optional[Dict[str, Any]] = None, optimization_goal: str = "REACH", # Backwards compatibility for simple interest validation interest_list: Optional[List[str]] = None, interest_fbid_list: Optional[List[str]] = None ) -> str: """ Estimate audience size for targeting specifications using Meta's delivery_estimate API. This function provides comprehensive audience estimation for complex targeting combinations including demographics, geography, interests, and behaviors. It also maintains backwards compatibility for simple interest validation. Args: access_token: Meta API access token (optional - will use cached token if not provided) account_id: Meta Ads account ID (format: act_XXXXXXXXX) - required for comprehensive estimation targeting: Complete targeting specification including demographics, geography, interests, etc. Example: { "age_min": 25, "age_max": 65, "geo_locations": {"countries": ["PL"]}, "flexible_spec": [ {"interests": [{"id": "6003371567474"}]}, {"interests": [{"id": "6003462346642"}]} ] } optimization_goal: Optimization goal for estimation (default: "REACH"). Options: "REACH", "LINK_CLICKS", "IMPRESSIONS", "CONVERSIONS", etc. interest_list: [DEPRECATED - for backwards compatibility] List of interest names to validate interest_fbid_list: [DEPRECATED - for backwards compatibility] List of interest IDs to validate Returns: JSON string with audience estimation results including estimated_audience_size, reach_estimate, and targeting validation """ # Handle backwards compatibility - simple interest validation # Check if we're in backwards compatibility mode (interest params provided OR no comprehensive params) is_backwards_compatible_call = (interest_list or interest_fbid_list) or (not account_id and not targeting) if is_backwards_compatible_call and not targeting: if not interest_list and not interest_fbid_list: return json.dumps({"error": "No interest list or FBID list provided"}, indent=2) endpoint = "search" params = { "type": "adinterestvalid" } if interest_list: params["interest_list"] = json.dumps(interest_list) if interest_fbid_list: params["interest_fbid_list"] = json.dumps(interest_fbid_list) data = await make_api_request(endpoint, access_token, params) return json.dumps(data, indent=2) # Comprehensive audience estimation using delivery_estimate API if not account_id: return json.dumps({ "error": "account_id is required for comprehensive audience estimation", "details": "For simple interest validation, use interest_list or interest_fbid_list parameters" }, indent=2) if not targeting: return json.dumps({ "error": "targeting specification is required for comprehensive audience estimation", "example": { "age_min": 25, "age_max": 65, "geo_locations": {"countries": ["US"]}, "flexible_spec": [ {"interests": [{"id": "6003371567474"}]} ] } }, indent=2) # Preflight validation: require at least one location OR a custom audience def _has_location_or_custom_audience(t: Dict[str, Any]) -> bool: if not isinstance(t, dict): return False geo = t.get("geo_locations") or {} if isinstance(geo, dict): for key in [ "countries", "regions", "cities", "zips", "geo_markets", "country_groups" ]: val = geo.get(key) if isinstance(val, list) and len(val) > 0: return True # Top-level custom audiences ca = t.get("custom_audiences") if isinstance(ca, list) and len(ca) > 0: return True # Custom audiences within flexible_spec flex = t.get("flexible_spec") if isinstance(flex, list): for spec in flex: if isinstance(spec, dict): ca_spec = spec.get("custom_audiences") if isinstance(ca_spec, list) and len(ca_spec) > 0: return True return False if not _has_location_or_custom_audience(targeting): return json.dumps({ "error": "Missing target audience location", "details": "Select at least one location in targeting.geo_locations or include a custom audience.", "action_required": "Add geo_locations with countries/regions/cities/zips or include custom_audiences.", "example": { "geo_locations": {"countries": ["US"]}, "age_min": 25, "age_max": 65 } }, indent=2) # Build reach estimate request (using correct Meta API endpoint) endpoint = f"{account_id}/reachestimate" params = { "targeting_spec": targeting } # Note: reachestimate endpoint doesn't support optimization_goal or objective parameters try: data = await make_api_request(endpoint, access_token, params, method="GET") # Surface Graph API errors directly for better diagnostics. # If reachestimate fails, optionally attempt a fallback using delivery_estimate. if isinstance(data, dict) and "error" in data: # Special handling for Missing Target Audience Location error (subcode 1885364) try: err_wrapper = data.get("error", {}) details_obj = err_wrapper.get("details", {}) raw_err = details_obj.get("error", {}) if isinstance(details_obj, dict) else {} if ( isinstance(raw_err, dict) and ( raw_err.get("error_subcode") == 1885364 or raw_err.get("error_user_title") == "Missing Target Audience Location" ) ): return json.dumps({ "error": "Missing target audience location", "details": raw_err.get("error_user_msg") or "Select at least one location, or choose a custom audience.", "endpoint_used": f"{account_id}/reachestimate", "action_required": "Add geo_locations with at least one of countries/regions/cities/zips or include custom_audiences.", "blame_field_specs": raw_err.get("error_data", {}).get("blame_field_specs") if isinstance(raw_err.get("error_data"), dict) else None }, indent=2) except Exception: pass # Allow disabling fallback via environment variable # Default: fallback disabled unless explicitly enabled by setting DISABLE flag to "0" disable_fallback = os.environ.get("META_MCP_DISABLE_DELIVERY_FALLBACK", "1") == "1" if disable_fallback: return json.dumps({ "error": "Graph API returned an error for reachestimate", "details": data.get("error"), "endpoint_used": f"{account_id}/reachestimate", "request_params": { "has_targeting_spec": bool(targeting), }, "note": "delivery_estimate fallback disabled via META_MCP_DISABLE_DELIVERY_FALLBACK" }, indent=2) # Try fallback to delivery_estimate endpoint try: fallback_endpoint = f"{account_id}/delivery_estimate" fallback_params = { "targeting_spec": json.dumps(targeting), # Some API versions accept optimization_goal here "optimization_goal": optimization_goal } fallback_data = await make_api_request(fallback_endpoint, access_token, fallback_params, method="GET") # If fallback returns usable data, format similarly if isinstance(fallback_data, dict) and "data" in fallback_data and len(fallback_data["data"]) > 0: estimate_data = fallback_data["data"][0] formatted_response = { "success": True, "account_id": account_id, "targeting": targeting, "optimization_goal": optimization_goal, "estimated_audience_size": estimate_data.get("estimate_mau", 0), "estimate_details": { "monthly_active_users": estimate_data.get("estimate_mau", 0), "daily_outcomes_curve": estimate_data.get("estimate_dau", []), "bid_estimate": estimate_data.get("bid_estimates", {}), "unsupported_targeting": estimate_data.get("unsupported_targeting", []) }, "raw_response": fallback_data, "fallback_endpoint_used": "delivery_estimate" } return json.dumps(formatted_response, indent=2) # Fallback returned but not in expected format return json.dumps({ "error": "Graph API returned an error for reachestimate; delivery_estimate fallback did not return usable data", "reachestimate_error": data.get("error"), "fallback_endpoint_used": "delivery_estimate", "fallback_raw_response": fallback_data, "endpoint_used": f"{account_id}/reachestimate", "request_params": { "has_targeting_spec": bool(targeting) } }, indent=2) except Exception as _fallback_exc: return json.dumps({ "error": "Graph API returned an error for reachestimate; delivery_estimate fallback also failed", "reachestimate_error": data.get("error"), "fallback_endpoint_used": "delivery_estimate", "fallback_exception": str(_fallback_exc), "endpoint_used": f"{account_id}/reachestimate", "request_params": { "has_targeting_spec": bool(targeting) } }, indent=2) # Format the response for easier consumption if "data" in data: response_data = data["data"] # Case 1: delivery_estimate-like list structure if isinstance(response_data, list) and len(response_data) > 0: estimate_data = response_data[0] formatted_response = { "success": True, "account_id": account_id, "targeting": targeting, "optimization_goal": optimization_goal, "estimated_audience_size": estimate_data.get("estimate_mau", 0), "estimate_details": { "monthly_active_users": estimate_data.get("estimate_mau", 0), "daily_outcomes_curve": estimate_data.get("estimate_dau", []), "bid_estimate": estimate_data.get("bid_estimates", {}), "unsupported_targeting": estimate_data.get("unsupported_targeting", []) }, "raw_response": data } return json.dumps(formatted_response, indent=2) # Case 1b: explicit handling for empty list responses if isinstance(response_data, list) and len(response_data) == 0: return json.dumps({ "error": "No estimation data returned from Meta API", "raw_response": data, "debug_info": { "response_keys": list(data.keys()) if isinstance(data, dict) else "not_a_dict", "response_type": str(type(data)), "endpoint_used": f"{account_id}/reachestimate" } }, indent=2) # Case 2: reachestimate dict structure with bounds if isinstance(response_data, dict): lower = response_data.get("users_lower_bound", response_data.get("estimate_mau_lower_bound")) upper = response_data.get("users_upper_bound", response_data.get("estimate_mau_upper_bound")) estimate_ready = response_data.get("estimate_ready") midpoint = None try: if isinstance(lower, (int, float)) and isinstance(upper, (int, float)): midpoint = int((lower + upper) / 2) except Exception: midpoint = None formatted_response = { "success": True, "account_id": account_id, "targeting": targeting, "optimization_goal": optimization_goal, "estimated_audience_size": midpoint if midpoint is not None else 0, "estimate_details": { "users_lower_bound": lower, "users_upper_bound": upper, "estimate_ready": estimate_ready }, "raw_response": data } return json.dumps(formatted_response, indent=2) else: return json.dumps({ "error": "No estimation data returned from Meta API", "raw_response": data, "debug_info": { "response_keys": list(data.keys()) if isinstance(data, dict) else "not_a_dict", "response_type": str(type(data)), "endpoint_used": f"{account_id}/reachestimate" } }, indent=2) except Exception as e: # Try fallback to delivery_estimate first when an exception occurs (unless disabled) # Default: fallback disabled unless explicitly enabled by setting DISABLE flag to "0" disable_fallback = os.environ.get("META_MCP_DISABLE_DELIVERY_FALLBACK", "1") == "1" if not disable_fallback: try: fallback_endpoint = f"{account_id}/delivery_estimate" fallback_params = { "targeting_spec": json.dumps(targeting) if isinstance(targeting, dict) else targeting, "optimization_goal": optimization_goal } fallback_data = await make_api_request(fallback_endpoint, access_token, fallback_params, method="GET") if isinstance(fallback_data, dict) and "data" in fallback_data and len(fallback_data["data"]) > 0: estimate_data = fallback_data["data"][0] formatted_response = { "success": True, "account_id": account_id, "targeting": targeting, "optimization_goal": optimization_goal, "estimated_audience_size": estimate_data.get("estimate_mau", 0), "estimate_details": { "monthly_active_users": estimate_data.get("estimate_mau", 0), "daily_outcomes_curve": estimate_data.get("estimate_dau", []), "bid_estimate": estimate_data.get("bid_estimates", {}), "unsupported_targeting": estimate_data.get("unsupported_targeting", []) }, "raw_response": fallback_data, "fallback_endpoint_used": "delivery_estimate" } return json.dumps(formatted_response, indent=2) except Exception as _fallback_exc: # If fallback also fails, proceed to detailed error handling below pass # Check if this is the specific Business Manager system user permission error error_str = str(e) if "100" in error_str and "33" in error_str: # Try to provide fallback estimation using individual interests if available interests_found = [] if targeting and "interests" in targeting: interests_found.extend([interest.get("id") for interest in targeting["interests"] if interest.get("id")]) elif targeting and "flexible_spec" in targeting: for spec in targeting["flexible_spec"]: if "interests" in spec: interests_found.extend([interest.get("id") for interest in spec["interests"] if interest.get("id")]) if interests_found: # Attempt to get individual interest data as fallback try: fallback_result = await estimate_audience_size( access_token=access_token, interest_fbid_list=interests_found ) fallback_data = json.loads(fallback_result) return json.dumps({ "comprehensive_targeting_failed": True, "error_code": "100-33", "fallback_used": True, "details": { "issue": "reachestimate endpoint returned error - possibly due to targeting parameters or account limitations", "solution": "Individual interest validation used as fallback - comprehensive targeting may have specific requirements", "endpoint_used": f"{account_id}/reachestimate" }, "individual_interest_data": fallback_data, "note": "Individual interest audience sizes provided as fallback. Comprehensive targeting via reachestimate endpoint failed." }, indent=2) except: pass return json.dumps({ "error": "reachestimate endpoint returned error (previously was incorrectly using delivery_estimate)", "error_code": "100-33", "details": { "issue": "The endpoint returned an error, possibly due to targeting parameters or account limitations", "endpoint_used": f"{account_id}/reachestimate", "previous_issue": "Code was previously using non-existent delivery_estimate endpoint - now fixed", "available_alternative": "Use interest_list or interest_fbid_list parameters for individual interest validation" }, "raw_error": error_str }, indent=2) else: return json.dumps({ "error": f"Failed to get audience estimation from reachestimate endpoint: {str(e)}", "details": "Check targeting parameters and account permissions", "error_type": "general_api_error", "endpoint_used": f"{account_id}/reachestimate" }, indent=2) @mcp_server.tool() @meta_api_tool async def search_behaviors(access_token: Optional[str] = None, limit: int = 50) -> str: """ Get all available behavior targeting options. Args: access_token: Meta API access token (optional - will use cached token if not provided) limit: Maximum number of results to return (default: 50) Returns: JSON string containing behavior targeting options with id, name, audience_size bounds, path, and description """ endpoint = "search" params = { "type": "adTargetingCategory", "class": "behaviors", "limit": limit } data = await make_api_request(endpoint, access_token, params) return json.dumps(data, indent=2) @mcp_server.tool() @meta_api_tool async def search_demographics(access_token: Optional[str] = None, demographic_class: str = "demographics", limit: int = 50) -> str: """ Get demographic targeting options. Args: access_token: Meta API access token (optional - will use cached token if not provided) demographic_class: Type of demographics to retrieve. Options: 'demographics', 'life_events', 'industries', 'income', 'family_statuses', 'user_device', 'user_os' (default: 'demographics') limit: Maximum number of results to return (default: 50) Returns: JSON string containing demographic targeting options with id, name, audience_size bounds, path, and description """ endpoint = "search" params = { "type": "adTargetingCategory", "class": demographic_class, "limit": limit } data = await make_api_request(endpoint, access_token, params) return json.dumps(data, indent=2) @mcp_server.tool() @meta_api_tool async def search_geo_locations(query: str, access_token: Optional[str] = None, location_types: Optional[List[str]] = None, limit: int = 25) -> str: """ Search for geographic targeting locations. Args: query: Search term for locations (e.g., "New York", "California", "Japan") access_token: Meta API access token (optional - will use cached token if not provided) location_types: Types of locations to search. Options: ['country', 'region', 'city', 'zip', 'geo_market', 'electoral_district']. If not specified, searches all types. limit: Maximum number of results to return (default: 25) Returns: JSON string containing location data with key, name, type, and geographic hierarchy information """ if not query: return json.dumps({"error": "No search query provided"}, indent=2) endpoint = "search" params = { "type": "adgeolocation", "q": query, "limit": limit } if location_types: params["location_types"] = json.dumps(location_types) data = await make_api_request(endpoint, access_token, params) return json.dumps(data, indent=2)

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/pipeboard-co/meta-ads-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server