meta-ads-mcp-python 1.0.79__py3-none-any.whl
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.
- meta_ads_mcp/__init__.py +79 -0
- meta_ads_mcp/__main__.py +10 -0
- meta_ads_mcp/core/__init__.py +55 -0
- meta_ads_mcp/core/accounts.py +141 -0
- meta_ads_mcp/core/ads.py +2751 -0
- meta_ads_mcp/core/ads_library.py +74 -0
- meta_ads_mcp/core/adsets.py +666 -0
- meta_ads_mcp/core/api.py +431 -0
- meta_ads_mcp/core/auth.py +567 -0
- meta_ads_mcp/core/authentication.py +207 -0
- meta_ads_mcp/core/budget_schedules.py +70 -0
- meta_ads_mcp/core/callback_server.py +256 -0
- meta_ads_mcp/core/campaigns.py +379 -0
- meta_ads_mcp/core/duplication.py +523 -0
- meta_ads_mcp/core/http_auth_integration.py +307 -0
- meta_ads_mcp/core/insights.py +161 -0
- meta_ads_mcp/core/mcc.py +232 -0
- meta_ads_mcp/core/openai_deep_research.py +418 -0
- meta_ads_mcp/core/pipeboard_auth.py +510 -0
- meta_ads_mcp/core/reports.py +135 -0
- meta_ads_mcp/core/resources.py +46 -0
- meta_ads_mcp/core/server.py +391 -0
- meta_ads_mcp/core/targeting.py +542 -0
- meta_ads_mcp/core/utils.py +225 -0
- meta_ads_mcp/settings.py +33 -0
- meta_ads_mcp_python-1.0.79.dist-info/METADATA +187 -0
- meta_ads_mcp_python-1.0.79.dist-info/RECORD +29 -0
- meta_ads_mcp_python-1.0.79.dist-info/WHEEL +4 -0
- meta_ads_mcp_python-1.0.79.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,542 @@
|
|
|
1
|
+
"""Targeting search functionality for Meta Ads API."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Optional, List, Dict, Any, Union
|
|
5
|
+
import os
|
|
6
|
+
from .api import meta_api_tool, make_api_request
|
|
7
|
+
from .server import mcp_server
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@mcp_server.tool()
|
|
11
|
+
@meta_api_tool
|
|
12
|
+
async def search_interests(query: str, access_token: Optional[str] = None, limit: int = 25) -> str:
|
|
13
|
+
"""
|
|
14
|
+
Search for interest targeting options by keyword.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
query: Search term for interests (e.g., "baseball", "cooking", "travel")
|
|
18
|
+
access_token: Meta API access token (optional - will use cached token if not provided)
|
|
19
|
+
limit: Maximum number of results to return (default: 25)
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
JSON string containing interest data with id, name, audience_size, and path fields
|
|
23
|
+
"""
|
|
24
|
+
if not query:
|
|
25
|
+
return json.dumps({"error": "No search query provided"}, indent=2)
|
|
26
|
+
|
|
27
|
+
endpoint = "search"
|
|
28
|
+
params = {
|
|
29
|
+
"type": "adinterest",
|
|
30
|
+
"q": query,
|
|
31
|
+
"limit": limit
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
data = await make_api_request(endpoint, access_token, params)
|
|
35
|
+
|
|
36
|
+
return json.dumps(data, indent=2)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@mcp_server.tool()
|
|
40
|
+
@meta_api_tool
|
|
41
|
+
async def get_interest_suggestions(interest_list: List[str], access_token: Optional[str] = None, limit: int = 25) -> str:
|
|
42
|
+
"""
|
|
43
|
+
Get interest suggestions based on existing interests.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
interest_list: List of interest names to get suggestions for (e.g., ["Basketball", "Soccer"])
|
|
47
|
+
access_token: Meta API access token (optional - will use cached token if not provided)
|
|
48
|
+
limit: Maximum number of suggestions to return (default: 25)
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
JSON string containing suggested interests with id, name, audience_size, and description fields
|
|
52
|
+
"""
|
|
53
|
+
if not interest_list:
|
|
54
|
+
return json.dumps({"error": "No interest list provided"}, indent=2)
|
|
55
|
+
|
|
56
|
+
endpoint = "search"
|
|
57
|
+
params = {
|
|
58
|
+
"type": "adinterestsuggestion",
|
|
59
|
+
"interest_list": json.dumps(interest_list),
|
|
60
|
+
"limit": limit
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
data = await make_api_request(endpoint, access_token, params)
|
|
64
|
+
|
|
65
|
+
return json.dumps(data, indent=2)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@mcp_server.tool()
|
|
69
|
+
@meta_api_tool
|
|
70
|
+
async def estimate_audience_size(
|
|
71
|
+
access_token: Optional[str] = None,
|
|
72
|
+
account_id: Optional[Union[str, int]] = None,
|
|
73
|
+
targeting: Optional[Dict[str, Any]] = None,
|
|
74
|
+
optimization_goal: str = "REACH",
|
|
75
|
+
# Backwards compatibility for simple interest validation
|
|
76
|
+
interest_list: Optional[List[str]] = None,
|
|
77
|
+
interest_fbid_list: Optional[List[str]] = None
|
|
78
|
+
) -> str:
|
|
79
|
+
"""
|
|
80
|
+
Estimate audience size for targeting specifications using Meta's delivery_estimate API.
|
|
81
|
+
|
|
82
|
+
This function provides comprehensive audience estimation for complex targeting combinations
|
|
83
|
+
including demographics, geography, interests, and behaviors. It also maintains backwards
|
|
84
|
+
compatibility for simple interest validation.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
access_token: Meta API access token (optional - will use cached token if not provided)
|
|
88
|
+
account_id: Meta Ads account ID (format: act_XXXXXXXXX) - required for comprehensive estimation
|
|
89
|
+
targeting: Complete targeting specification including demographics, geography, interests, etc.
|
|
90
|
+
Example: {
|
|
91
|
+
"age_min": 25,
|
|
92
|
+
"age_max": 65,
|
|
93
|
+
"geo_locations": {"countries": ["PL"]},
|
|
94
|
+
"flexible_spec": [
|
|
95
|
+
{"interests": [{"id": "6003371567474"}]},
|
|
96
|
+
{"interests": [{"id": "6003462346642"}]}
|
|
97
|
+
]
|
|
98
|
+
}
|
|
99
|
+
optimization_goal: Optimization goal for estimation (default: "REACH").
|
|
100
|
+
Options: "REACH", "LINK_CLICKS", "IMPRESSIONS", "CONVERSIONS", etc.
|
|
101
|
+
interest_list: [DEPRECATED - for backwards compatibility] List of interest names to validate
|
|
102
|
+
interest_fbid_list: [DEPRECATED - for backwards compatibility] List of interest IDs to validate
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
JSON string with audience estimation results including estimated_audience_size,
|
|
106
|
+
reach_estimate, and targeting validation
|
|
107
|
+
"""
|
|
108
|
+
# Coerce numeric IDs to strings
|
|
109
|
+
if account_id is not None:
|
|
110
|
+
account_id = str(account_id)
|
|
111
|
+
|
|
112
|
+
# Handle backwards compatibility - simple interest validation
|
|
113
|
+
# Check if we're in backwards compatibility mode (interest params provided OR no comprehensive params)
|
|
114
|
+
is_backwards_compatible_call = (interest_list or interest_fbid_list) or (not account_id and not targeting)
|
|
115
|
+
|
|
116
|
+
if is_backwards_compatible_call and not targeting:
|
|
117
|
+
if not interest_list and not interest_fbid_list:
|
|
118
|
+
return json.dumps({"error": "No interest list or FBID list provided"}, indent=2)
|
|
119
|
+
|
|
120
|
+
endpoint = "search"
|
|
121
|
+
params = {
|
|
122
|
+
"type": "adinterestvalid"
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if interest_list:
|
|
126
|
+
params["interest_list"] = json.dumps(interest_list)
|
|
127
|
+
|
|
128
|
+
if interest_fbid_list:
|
|
129
|
+
params["interest_fbid_list"] = json.dumps(interest_fbid_list)
|
|
130
|
+
|
|
131
|
+
data = await make_api_request(endpoint, access_token, params)
|
|
132
|
+
|
|
133
|
+
return json.dumps(data, indent=2)
|
|
134
|
+
|
|
135
|
+
# Comprehensive audience estimation using delivery_estimate API
|
|
136
|
+
if not account_id:
|
|
137
|
+
return json.dumps({
|
|
138
|
+
"error": "account_id is required for comprehensive audience estimation",
|
|
139
|
+
"details": "For simple interest validation, use interest_list or interest_fbid_list parameters"
|
|
140
|
+
}, indent=2)
|
|
141
|
+
|
|
142
|
+
if not targeting:
|
|
143
|
+
return json.dumps({
|
|
144
|
+
"error": "targeting specification is required for comprehensive audience estimation",
|
|
145
|
+
"example": {
|
|
146
|
+
"age_min": 25,
|
|
147
|
+
"age_max": 65,
|
|
148
|
+
"geo_locations": {"countries": ["US"]},
|
|
149
|
+
"flexible_spec": [
|
|
150
|
+
{"interests": [{"id": "6003371567474"}]}
|
|
151
|
+
]
|
|
152
|
+
}
|
|
153
|
+
}, indent=2)
|
|
154
|
+
|
|
155
|
+
# Preflight validation: require at least one location OR a custom audience
|
|
156
|
+
def _has_location_or_custom_audience(t: Dict[str, Any]) -> bool:
|
|
157
|
+
if not isinstance(t, dict):
|
|
158
|
+
return False
|
|
159
|
+
geo = t.get("geo_locations") or {}
|
|
160
|
+
if isinstance(geo, dict):
|
|
161
|
+
for key in [
|
|
162
|
+
"countries",
|
|
163
|
+
"regions",
|
|
164
|
+
"cities",
|
|
165
|
+
"zips",
|
|
166
|
+
"geo_markets",
|
|
167
|
+
"country_groups"
|
|
168
|
+
]:
|
|
169
|
+
val = geo.get(key)
|
|
170
|
+
if isinstance(val, list) and len(val) > 0:
|
|
171
|
+
return True
|
|
172
|
+
# Top-level custom audiences
|
|
173
|
+
ca = t.get("custom_audiences")
|
|
174
|
+
if isinstance(ca, list) and len(ca) > 0:
|
|
175
|
+
return True
|
|
176
|
+
# Custom audiences within flexible_spec
|
|
177
|
+
flex = t.get("flexible_spec")
|
|
178
|
+
if isinstance(flex, list):
|
|
179
|
+
for spec in flex:
|
|
180
|
+
if isinstance(spec, dict):
|
|
181
|
+
ca_spec = spec.get("custom_audiences")
|
|
182
|
+
if isinstance(ca_spec, list) and len(ca_spec) > 0:
|
|
183
|
+
return True
|
|
184
|
+
return False
|
|
185
|
+
|
|
186
|
+
if not _has_location_or_custom_audience(targeting):
|
|
187
|
+
return json.dumps({
|
|
188
|
+
"error": "Missing target audience location",
|
|
189
|
+
"details": "Select at least one location in targeting.geo_locations or include a custom audience.",
|
|
190
|
+
"action_required": "Add geo_locations with countries/regions/cities/zips or include custom_audiences.",
|
|
191
|
+
"example": {
|
|
192
|
+
"geo_locations": {"countries": ["US"]},
|
|
193
|
+
"age_min": 25,
|
|
194
|
+
"age_max": 65
|
|
195
|
+
}
|
|
196
|
+
}, indent=2)
|
|
197
|
+
|
|
198
|
+
# Build reach estimate request (using correct Meta API endpoint)
|
|
199
|
+
endpoint = f"{account_id}/reachestimate"
|
|
200
|
+
params = {
|
|
201
|
+
"targeting_spec": targeting
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
# Note: reachestimate endpoint doesn't support optimization_goal or objective parameters
|
|
205
|
+
|
|
206
|
+
try:
|
|
207
|
+
data = await make_api_request(endpoint, access_token, params, method="GET")
|
|
208
|
+
|
|
209
|
+
# Surface Graph API errors directly for better diagnostics.
|
|
210
|
+
# If reachestimate fails, optionally attempt a fallback using delivery_estimate.
|
|
211
|
+
if isinstance(data, dict) and "error" in data:
|
|
212
|
+
# Special handling for Missing Target Audience Location error (subcode 1885364)
|
|
213
|
+
try:
|
|
214
|
+
err_wrapper = data.get("error", {})
|
|
215
|
+
details_obj = err_wrapper.get("details", {})
|
|
216
|
+
raw_err = details_obj.get("error", {}) if isinstance(details_obj, dict) else {}
|
|
217
|
+
if (
|
|
218
|
+
isinstance(raw_err, dict) and (
|
|
219
|
+
raw_err.get("error_subcode") == 1885364 or
|
|
220
|
+
raw_err.get("error_user_title") == "Missing Target Audience Location"
|
|
221
|
+
)
|
|
222
|
+
):
|
|
223
|
+
return json.dumps({
|
|
224
|
+
"error": "Missing target audience location",
|
|
225
|
+
"details": raw_err.get("error_user_msg") or "Select at least one location, or choose a custom audience.",
|
|
226
|
+
"endpoint_used": f"{account_id}/reachestimate",
|
|
227
|
+
"action_required": "Add geo_locations with at least one of countries/regions/cities/zips or include custom_audiences.",
|
|
228
|
+
"blame_field_specs": raw_err.get("error_data", {}).get("blame_field_specs") if isinstance(raw_err.get("error_data"), dict) else None
|
|
229
|
+
}, indent=2)
|
|
230
|
+
except Exception:
|
|
231
|
+
pass
|
|
232
|
+
# Allow disabling fallback via environment variable
|
|
233
|
+
# Default: fallback disabled unless explicitly enabled by setting DISABLE flag to "0"
|
|
234
|
+
disable_fallback = os.environ.get("META_MCP_DISABLE_DELIVERY_FALLBACK", "1") == "1"
|
|
235
|
+
if disable_fallback:
|
|
236
|
+
return json.dumps({
|
|
237
|
+
"error": "Graph API returned an error for reachestimate",
|
|
238
|
+
"details": data.get("error"),
|
|
239
|
+
"endpoint_used": f"{account_id}/reachestimate",
|
|
240
|
+
"request_params": {
|
|
241
|
+
"has_targeting_spec": bool(targeting),
|
|
242
|
+
},
|
|
243
|
+
"note": "delivery_estimate fallback disabled via META_MCP_DISABLE_DELIVERY_FALLBACK"
|
|
244
|
+
}, indent=2)
|
|
245
|
+
|
|
246
|
+
# Try fallback to delivery_estimate endpoint
|
|
247
|
+
try:
|
|
248
|
+
fallback_endpoint = f"{account_id}/delivery_estimate"
|
|
249
|
+
fallback_params = {
|
|
250
|
+
"targeting_spec": json.dumps(targeting),
|
|
251
|
+
# Some API versions accept optimization_goal here
|
|
252
|
+
"optimization_goal": optimization_goal
|
|
253
|
+
}
|
|
254
|
+
fallback_data = await make_api_request(fallback_endpoint, access_token, fallback_params, method="GET")
|
|
255
|
+
|
|
256
|
+
# If fallback returns usable data, format similarly
|
|
257
|
+
if isinstance(fallback_data, dict) and "data" in fallback_data and len(fallback_data["data"]) > 0:
|
|
258
|
+
estimate_data = fallback_data["data"][0]
|
|
259
|
+
formatted_response = {
|
|
260
|
+
"success": True,
|
|
261
|
+
"account_id": account_id,
|
|
262
|
+
"targeting": targeting,
|
|
263
|
+
"optimization_goal": optimization_goal,
|
|
264
|
+
"estimated_audience_size": estimate_data.get("estimate_mau", 0),
|
|
265
|
+
"estimate_details": {
|
|
266
|
+
"monthly_active_users": estimate_data.get("estimate_mau", 0),
|
|
267
|
+
"daily_outcomes_curve": estimate_data.get("estimate_dau", []),
|
|
268
|
+
"bid_estimate": estimate_data.get("bid_estimates", {}),
|
|
269
|
+
"unsupported_targeting": estimate_data.get("unsupported_targeting", [])
|
|
270
|
+
},
|
|
271
|
+
"raw_response": fallback_data,
|
|
272
|
+
"fallback_endpoint_used": "delivery_estimate"
|
|
273
|
+
}
|
|
274
|
+
return json.dumps(formatted_response, indent=2)
|
|
275
|
+
|
|
276
|
+
# Fallback returned but not in expected format
|
|
277
|
+
return json.dumps({
|
|
278
|
+
"error": "Graph API returned an error for reachestimate; delivery_estimate fallback did not return usable data",
|
|
279
|
+
"reachestimate_error": data.get("error"),
|
|
280
|
+
"fallback_endpoint_used": "delivery_estimate",
|
|
281
|
+
"fallback_raw_response": fallback_data,
|
|
282
|
+
"endpoint_used": f"{account_id}/reachestimate",
|
|
283
|
+
"request_params": {
|
|
284
|
+
"has_targeting_spec": bool(targeting)
|
|
285
|
+
}
|
|
286
|
+
}, indent=2)
|
|
287
|
+
except Exception as _fallback_exc:
|
|
288
|
+
return json.dumps({
|
|
289
|
+
"error": "Graph API returned an error for reachestimate; delivery_estimate fallback also failed",
|
|
290
|
+
"reachestimate_error": data.get("error"),
|
|
291
|
+
"fallback_endpoint_used": "delivery_estimate",
|
|
292
|
+
"fallback_exception": str(_fallback_exc),
|
|
293
|
+
"endpoint_used": f"{account_id}/reachestimate",
|
|
294
|
+
"request_params": {
|
|
295
|
+
"has_targeting_spec": bool(targeting)
|
|
296
|
+
}
|
|
297
|
+
}, indent=2)
|
|
298
|
+
|
|
299
|
+
# Format the response for easier consumption
|
|
300
|
+
if "data" in data:
|
|
301
|
+
response_data = data["data"]
|
|
302
|
+
# Case 1: delivery_estimate-like list structure
|
|
303
|
+
if isinstance(response_data, list) and len(response_data) > 0:
|
|
304
|
+
estimate_data = response_data[0]
|
|
305
|
+
formatted_response = {
|
|
306
|
+
"success": True,
|
|
307
|
+
"account_id": account_id,
|
|
308
|
+
"targeting": targeting,
|
|
309
|
+
"optimization_goal": optimization_goal,
|
|
310
|
+
"estimated_audience_size": estimate_data.get("estimate_mau", 0),
|
|
311
|
+
"estimate_details": {
|
|
312
|
+
"monthly_active_users": estimate_data.get("estimate_mau", 0),
|
|
313
|
+
"daily_outcomes_curve": estimate_data.get("estimate_dau", []),
|
|
314
|
+
"bid_estimate": estimate_data.get("bid_estimates", {}),
|
|
315
|
+
"unsupported_targeting": estimate_data.get("unsupported_targeting", [])
|
|
316
|
+
},
|
|
317
|
+
"raw_response": data
|
|
318
|
+
}
|
|
319
|
+
return json.dumps(formatted_response, indent=2)
|
|
320
|
+
# Case 1b: explicit handling for empty list responses
|
|
321
|
+
if isinstance(response_data, list) and len(response_data) == 0:
|
|
322
|
+
return json.dumps({
|
|
323
|
+
"error": "No estimation data returned from Meta API",
|
|
324
|
+
"raw_response": data,
|
|
325
|
+
"debug_info": {
|
|
326
|
+
"response_keys": list(data.keys()) if isinstance(data, dict) else "not_a_dict",
|
|
327
|
+
"response_type": str(type(data)),
|
|
328
|
+
"endpoint_used": f"{account_id}/reachestimate"
|
|
329
|
+
}
|
|
330
|
+
}, indent=2)
|
|
331
|
+
# Case 2: reachestimate dict structure with bounds
|
|
332
|
+
if isinstance(response_data, dict):
|
|
333
|
+
lower = response_data.get("users_lower_bound", response_data.get("estimate_mau_lower_bound"))
|
|
334
|
+
upper = response_data.get("users_upper_bound", response_data.get("estimate_mau_upper_bound"))
|
|
335
|
+
estimate_ready = response_data.get("estimate_ready")
|
|
336
|
+
midpoint = None
|
|
337
|
+
try:
|
|
338
|
+
if isinstance(lower, (int, float)) and isinstance(upper, (int, float)):
|
|
339
|
+
midpoint = int((lower + upper) / 2)
|
|
340
|
+
except Exception:
|
|
341
|
+
midpoint = None
|
|
342
|
+
formatted_response = {
|
|
343
|
+
"success": True,
|
|
344
|
+
"account_id": account_id,
|
|
345
|
+
"targeting": targeting,
|
|
346
|
+
"optimization_goal": optimization_goal,
|
|
347
|
+
"estimated_audience_size": midpoint if midpoint is not None else 0,
|
|
348
|
+
"estimate_details": {
|
|
349
|
+
"users_lower_bound": lower,
|
|
350
|
+
"users_upper_bound": upper,
|
|
351
|
+
"estimate_ready": estimate_ready
|
|
352
|
+
},
|
|
353
|
+
"raw_response": data
|
|
354
|
+
}
|
|
355
|
+
return json.dumps(formatted_response, indent=2)
|
|
356
|
+
else:
|
|
357
|
+
return json.dumps({
|
|
358
|
+
"error": "No estimation data returned from Meta API",
|
|
359
|
+
"raw_response": data,
|
|
360
|
+
"debug_info": {
|
|
361
|
+
"response_keys": list(data.keys()) if isinstance(data, dict) else "not_a_dict",
|
|
362
|
+
"response_type": str(type(data)),
|
|
363
|
+
"endpoint_used": f"{account_id}/reachestimate"
|
|
364
|
+
}
|
|
365
|
+
}, indent=2)
|
|
366
|
+
|
|
367
|
+
except Exception as e:
|
|
368
|
+
# Try fallback to delivery_estimate first when an exception occurs (unless disabled)
|
|
369
|
+
# Default: fallback disabled unless explicitly enabled by setting DISABLE flag to "0"
|
|
370
|
+
disable_fallback = os.environ.get("META_MCP_DISABLE_DELIVERY_FALLBACK", "1") == "1"
|
|
371
|
+
if not disable_fallback:
|
|
372
|
+
try:
|
|
373
|
+
fallback_endpoint = f"{account_id}/delivery_estimate"
|
|
374
|
+
fallback_params = {
|
|
375
|
+
"targeting_spec": json.dumps(targeting) if isinstance(targeting, dict) else targeting,
|
|
376
|
+
"optimization_goal": optimization_goal
|
|
377
|
+
}
|
|
378
|
+
fallback_data = await make_api_request(fallback_endpoint, access_token, fallback_params, method="GET")
|
|
379
|
+
|
|
380
|
+
if isinstance(fallback_data, dict) and "data" in fallback_data and len(fallback_data["data"]) > 0:
|
|
381
|
+
estimate_data = fallback_data["data"][0]
|
|
382
|
+
formatted_response = {
|
|
383
|
+
"success": True,
|
|
384
|
+
"account_id": account_id,
|
|
385
|
+
"targeting": targeting,
|
|
386
|
+
"optimization_goal": optimization_goal,
|
|
387
|
+
"estimated_audience_size": estimate_data.get("estimate_mau", 0),
|
|
388
|
+
"estimate_details": {
|
|
389
|
+
"monthly_active_users": estimate_data.get("estimate_mau", 0),
|
|
390
|
+
"daily_outcomes_curve": estimate_data.get("estimate_dau", []),
|
|
391
|
+
"bid_estimate": estimate_data.get("bid_estimates", {}),
|
|
392
|
+
"unsupported_targeting": estimate_data.get("unsupported_targeting", [])
|
|
393
|
+
},
|
|
394
|
+
"raw_response": fallback_data,
|
|
395
|
+
"fallback_endpoint_used": "delivery_estimate"
|
|
396
|
+
}
|
|
397
|
+
return json.dumps(formatted_response, indent=2)
|
|
398
|
+
except Exception as _fallback_exc:
|
|
399
|
+
# If fallback also fails, proceed to detailed error handling below
|
|
400
|
+
pass
|
|
401
|
+
|
|
402
|
+
# Check if this is the specific Business Manager system user permission error
|
|
403
|
+
error_str = str(e)
|
|
404
|
+
if "100" in error_str and "33" in error_str:
|
|
405
|
+
# Try to provide fallback estimation using individual interests if available
|
|
406
|
+
interests_found = []
|
|
407
|
+
if targeting and "interests" in targeting:
|
|
408
|
+
interests_found.extend([interest.get("id") for interest in targeting["interests"] if interest.get("id")])
|
|
409
|
+
elif targeting and "flexible_spec" in targeting:
|
|
410
|
+
for spec in targeting["flexible_spec"]:
|
|
411
|
+
if "interests" in spec:
|
|
412
|
+
interests_found.extend([interest.get("id") for interest in spec["interests"] if interest.get("id")])
|
|
413
|
+
|
|
414
|
+
if interests_found:
|
|
415
|
+
# Attempt to get individual interest data as fallback
|
|
416
|
+
try:
|
|
417
|
+
fallback_result = await estimate_audience_size(
|
|
418
|
+
access_token=access_token,
|
|
419
|
+
interest_fbid_list=interests_found
|
|
420
|
+
)
|
|
421
|
+
fallback_data = json.loads(fallback_result)
|
|
422
|
+
|
|
423
|
+
return json.dumps({
|
|
424
|
+
"comprehensive_targeting_failed": True,
|
|
425
|
+
"error_code": "100-33",
|
|
426
|
+
"fallback_used": True,
|
|
427
|
+
"details": {
|
|
428
|
+
"issue": "reachestimate endpoint returned error - possibly due to targeting parameters or account limitations",
|
|
429
|
+
"solution": "Individual interest validation used as fallback - comprehensive targeting may have specific requirements",
|
|
430
|
+
"endpoint_used": f"{account_id}/reachestimate"
|
|
431
|
+
},
|
|
432
|
+
"individual_interest_data": fallback_data,
|
|
433
|
+
"note": "Individual interest audience sizes provided as fallback. Comprehensive targeting via reachestimate endpoint failed."
|
|
434
|
+
}, indent=2)
|
|
435
|
+
except:
|
|
436
|
+
pass
|
|
437
|
+
|
|
438
|
+
return json.dumps({
|
|
439
|
+
"error": "reachestimate endpoint returned error (previously was incorrectly using delivery_estimate)",
|
|
440
|
+
"error_code": "100-33",
|
|
441
|
+
"details": {
|
|
442
|
+
"issue": "The endpoint returned an error, possibly due to targeting parameters or account limitations",
|
|
443
|
+
"endpoint_used": f"{account_id}/reachestimate",
|
|
444
|
+
"previous_issue": "Code was previously using non-existent delivery_estimate endpoint - now fixed",
|
|
445
|
+
"available_alternative": "Use interest_list or interest_fbid_list parameters for individual interest validation"
|
|
446
|
+
},
|
|
447
|
+
"raw_error": error_str
|
|
448
|
+
}, indent=2)
|
|
449
|
+
else:
|
|
450
|
+
return json.dumps({
|
|
451
|
+
"error": f"Failed to get audience estimation from reachestimate endpoint: {str(e)}",
|
|
452
|
+
"details": "Check targeting parameters and account permissions",
|
|
453
|
+
"error_type": "general_api_error",
|
|
454
|
+
"endpoint_used": f"{account_id}/reachestimate"
|
|
455
|
+
}, indent=2)
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
@mcp_server.tool()
|
|
459
|
+
@meta_api_tool
|
|
460
|
+
async def search_behaviors(access_token: Optional[str] = None, limit: int = 50) -> str:
|
|
461
|
+
"""
|
|
462
|
+
Get all available behavior targeting options.
|
|
463
|
+
|
|
464
|
+
Args:
|
|
465
|
+
access_token: Meta API access token (optional - will use cached token if not provided)
|
|
466
|
+
limit: Maximum number of results to return (default: 50)
|
|
467
|
+
|
|
468
|
+
Returns:
|
|
469
|
+
JSON string containing behavior targeting options with id, name, audience_size bounds, path, and description
|
|
470
|
+
"""
|
|
471
|
+
endpoint = "search"
|
|
472
|
+
params = {
|
|
473
|
+
"type": "adTargetingCategory",
|
|
474
|
+
"class": "behaviors",
|
|
475
|
+
"limit": limit
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
data = await make_api_request(endpoint, access_token, params)
|
|
479
|
+
|
|
480
|
+
return json.dumps(data, indent=2)
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
@mcp_server.tool()
|
|
484
|
+
@meta_api_tool
|
|
485
|
+
async def search_demographics(access_token: Optional[str] = None, demographic_class: str = "demographics", limit: int = 50) -> str:
|
|
486
|
+
"""
|
|
487
|
+
Get demographic targeting options.
|
|
488
|
+
|
|
489
|
+
Args:
|
|
490
|
+
access_token: Meta API access token (optional - will use cached token if not provided)
|
|
491
|
+
demographic_class: Type of demographics to retrieve. Options: 'demographics', 'life_events',
|
|
492
|
+
'industries', 'income', 'family_statuses', 'user_device', 'user_os' (default: 'demographics')
|
|
493
|
+
limit: Maximum number of results to return (default: 50)
|
|
494
|
+
|
|
495
|
+
Returns:
|
|
496
|
+
JSON string containing demographic targeting options with id, name, audience_size bounds, path, and description
|
|
497
|
+
"""
|
|
498
|
+
endpoint = "search"
|
|
499
|
+
params = {
|
|
500
|
+
"type": "adTargetingCategory",
|
|
501
|
+
"class": demographic_class,
|
|
502
|
+
"limit": limit
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
data = await make_api_request(endpoint, access_token, params)
|
|
506
|
+
|
|
507
|
+
return json.dumps(data, indent=2)
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
@mcp_server.tool()
|
|
511
|
+
@meta_api_tool
|
|
512
|
+
async def search_geo_locations(query: str, access_token: Optional[str] = None,
|
|
513
|
+
location_types: Optional[List[str]] = None, limit: int = 25) -> str:
|
|
514
|
+
"""
|
|
515
|
+
Search for geographic targeting locations.
|
|
516
|
+
|
|
517
|
+
Args:
|
|
518
|
+
query: Search term for locations (e.g., "New York", "California", "Japan")
|
|
519
|
+
access_token: Meta API access token (optional - will use cached token if not provided)
|
|
520
|
+
location_types: Types of locations to search. Options: ['country', 'region', 'city', 'zip',
|
|
521
|
+
'geo_market', 'electoral_district']. If not specified, searches all types.
|
|
522
|
+
limit: Maximum number of results to return (default: 25)
|
|
523
|
+
|
|
524
|
+
Returns:
|
|
525
|
+
JSON string containing location data with key, name, type, and geographic hierarchy information
|
|
526
|
+
"""
|
|
527
|
+
if not query:
|
|
528
|
+
return json.dumps({"error": "No search query provided"}, indent=2)
|
|
529
|
+
|
|
530
|
+
endpoint = "search"
|
|
531
|
+
params = {
|
|
532
|
+
"type": "adgeolocation",
|
|
533
|
+
"q": query,
|
|
534
|
+
"limit": limit
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
if location_types:
|
|
538
|
+
params["location_types"] = json.dumps(location_types)
|
|
539
|
+
|
|
540
|
+
data = await make_api_request(endpoint, access_token, params)
|
|
541
|
+
|
|
542
|
+
return json.dumps(data, indent=2)
|