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.
@@ -0,0 +1,379 @@
1
+ """Campaign-related functionality for Meta Ads API."""
2
+
3
+ import json
4
+ from typing import List, Optional, Dict, Any, Union
5
+ from .api import meta_api_tool, make_api_request, ensure_act_prefix
6
+ from .accounts import get_ad_accounts
7
+ from .server import mcp_server
8
+
9
+
10
+ @mcp_server.tool()
11
+ @meta_api_tool
12
+ async def get_campaigns(
13
+ account_id: str,
14
+ access_token: Optional[str] = None,
15
+ limit: int = 10,
16
+ status_filter: str = "",
17
+ objective_filter: Union[str, List[str]] = "",
18
+ after: str = ""
19
+ ) -> str:
20
+ """
21
+ Get campaigns for a Meta Ads account with optional filtering.
22
+
23
+ Note: By default, the Meta API returns a subset of available fields.
24
+ Other fields like 'effective_status', 'spend_cap', 'budget_remaining',
25
+ 'promoted_object', 'source_campaign_id', etc., might be available but
26
+ require specifying them in the API call (currently not exposed by this
27
+ tool's parameters).
28
+
29
+ Args:
30
+ account_id: Meta Ads account ID (format: act_XXXXXXXXX)
31
+ access_token: Meta API access token (optional - will use cached token if not provided)
32
+ limit: Maximum number of campaigns to return (default: 10)
33
+ status_filter: Filter by effective status (e.g., 'ACTIVE', 'PAUSED', 'ARCHIVED').
34
+ Maps to the 'effective_status' API parameter, which expects an array
35
+ (this function handles the required JSON formatting). Leave empty for all statuses.
36
+ objective_filter: Filter by campaign objective(s). Can be a single objective string or a list of objectives.
37
+ Valid objectives: 'OUTCOME_AWARENESS', 'OUTCOME_TRAFFIC', 'OUTCOME_ENGAGEMENT',
38
+ 'OUTCOME_LEADS', 'OUTCOME_SALES', 'OUTCOME_APP_PROMOTION'.
39
+ Examples: 'OUTCOME_LEADS' or ['OUTCOME_LEADS', 'OUTCOME_SALES'].
40
+ Leave empty for all objectives.
41
+ after: Pagination cursor to get the next set of results
42
+ """
43
+ # Require explicit account_id
44
+ if not account_id:
45
+ return json.dumps({"error": "No account ID specified"}, indent=2)
46
+
47
+ account_id = ensure_act_prefix(account_id)
48
+ endpoint = f"{account_id}/campaigns"
49
+ params = {
50
+ "fields": "id,name,objective,status,daily_budget,lifetime_budget,buying_type,start_time,stop_time,created_time,updated_time,bid_strategy,special_ad_categories",
51
+ "limit": limit
52
+ }
53
+
54
+ # Build filtering array for complex filtering
55
+ filters = []
56
+
57
+ if status_filter:
58
+ # API expects an array, encode it as a JSON string
59
+ params["effective_status"] = json.dumps([status_filter])
60
+
61
+ # Handle objective filtering - supports both single string and list of objectives
62
+ if objective_filter:
63
+ # Convert single string to list for consistent handling
64
+ objectives = [objective_filter] if isinstance(objective_filter, str) else objective_filter
65
+
66
+ # Filter out empty strings
67
+ objectives = [obj for obj in objectives if obj]
68
+
69
+ if objectives:
70
+ filters.append({
71
+ "field": "objective",
72
+ "operator": "IN",
73
+ "value": objectives
74
+ })
75
+
76
+ # Add filtering parameter if we have filters
77
+ if filters:
78
+ params["filtering"] = json.dumps(filters)
79
+
80
+ if after:
81
+ params["after"] = after
82
+
83
+ data = await make_api_request(endpoint, access_token, params)
84
+
85
+ return json.dumps(data, indent=2)
86
+
87
+
88
+ @mcp_server.tool()
89
+ @meta_api_tool
90
+ async def get_campaign_details(campaign_id: str, access_token: Optional[str] = None) -> str:
91
+ """
92
+ Get detailed information about a specific campaign.
93
+
94
+ Note: This function requests a specific set of fields ('id,name,objective,status,...').
95
+ The Meta API offers many other fields for campaigns (e.g., 'effective_status', 'source_campaign_id', etc.)
96
+ that could be added to the 'fields' parameter in the code if needed.
97
+
98
+ Args:
99
+ campaign_id: Meta Ads campaign ID
100
+ access_token: Meta API access token (optional - will use cached token if not provided)
101
+ """
102
+ if not campaign_id:
103
+ return json.dumps({"error": "No campaign ID provided"}, indent=2)
104
+
105
+ endpoint = f"{campaign_id}"
106
+ params = {
107
+ "fields": "id,name,objective,status,daily_budget,lifetime_budget,buying_type,start_time,stop_time,created_time,updated_time,bid_strategy,special_ad_categories,special_ad_category_country,budget_remaining,configured_status"
108
+ }
109
+
110
+ data = await make_api_request(endpoint, access_token, params)
111
+
112
+ return json.dumps(data, indent=2)
113
+
114
+
115
+ @meta_api_tool
116
+ async def create_campaign(
117
+ account_id: str,
118
+ name: str,
119
+ objective: str,
120
+ access_token: Optional[str] = None,
121
+ status: str = "PAUSED",
122
+ special_ad_categories: Optional[List[str]] = None,
123
+ daily_budget: Optional[int] = None,
124
+ lifetime_budget: Optional[int] = None,
125
+ buying_type: Optional[str] = None,
126
+ bid_strategy: str = "LOWEST_COST_WITHOUT_CAP",
127
+ bid_cap: Optional[int] = None,
128
+ spend_cap: Optional[int] = None,
129
+ campaign_budget_optimization: Optional[bool] = None,
130
+ ab_test_control_setups: Optional[List[Dict[str, Any]]] = None,
131
+ use_adset_level_budgets: bool = False
132
+ ) -> str:
133
+ """
134
+ Create a new campaign in a Meta Ads account.
135
+
136
+ Note: Campaigns do not support start_time for scheduling -- set start_time on the ad set instead.
137
+
138
+ Args:
139
+ account_id: Meta Ads account ID (format: act_XXXXXXXXX)
140
+ name: Campaign name
141
+ objective: Campaign objective (ODAX, outcome-based). Must be one of:
142
+ OUTCOME_AWARENESS, OUTCOME_TRAFFIC, OUTCOME_ENGAGEMENT,
143
+ OUTCOME_LEADS, OUTCOME_SALES, OUTCOME_APP_PROMOTION.
144
+ Note: Legacy objectives like BRAND_AWARENESS, LINK_CLICKS,
145
+ CONVERSIONS, APP_INSTALLS, etc. are not valid for new
146
+ campaigns and will cause a 400 error. Use the outcome-based
147
+ values above (e.g., BRAND_AWARENESS -> OUTCOME_AWARENESS).
148
+ access_token: Meta API access token (optional - will use cached token if not provided)
149
+ status: Initial campaign status (default: PAUSED)
150
+ special_ad_categories: List of special ad categories if applicable
151
+ daily_budget: Daily budget in account currency (in cents) as a string (only used if use_adset_level_budgets=False)
152
+ lifetime_budget: Lifetime budget in account currency (in cents) as a string (only used if use_adset_level_budgets=False)
153
+ buying_type: Buying type (e.g., 'AUCTION')
154
+ bid_strategy: Bid strategy (default: LOWEST_COST_WITHOUT_CAP). Must be one of: 'LOWEST_COST_WITHOUT_CAP', 'LOWEST_COST_WITH_BID_CAP', 'COST_CAP', 'LOWEST_COST_WITH_MIN_ROAS'. WARNING: If you use LOWEST_COST_WITH_BID_CAP or COST_CAP, all child ad sets will require bid_amount to be set.
155
+ bid_cap: Bid cap in account currency (in cents) as a string
156
+ spend_cap: Spending limit for the campaign in account currency (in cents) as a string
157
+ campaign_budget_optimization: Whether to enable campaign budget optimization (only used if use_adset_level_budgets=False)
158
+ ab_test_control_setups: Settings for A/B testing (e.g., [{"name":"Creative A", "ad_format":"SINGLE_IMAGE"}])
159
+ use_adset_level_budgets: If True, budgets will be set at the ad set level instead of campaign level (default: False)
160
+ """
161
+ # Check required parameters
162
+ if not account_id:
163
+ return json.dumps({"error": "No account ID provided"}, indent=2)
164
+
165
+ if not name:
166
+ return json.dumps({"error": "No campaign name provided"}, indent=2)
167
+
168
+ if not objective:
169
+ return json.dumps({"error": "No campaign objective provided"}, indent=2)
170
+
171
+ account_id = ensure_act_prefix(account_id)
172
+
173
+ # Track whether the user explicitly provided special_ad_categories
174
+ _user_provided_categories = special_ad_categories is not None
175
+
176
+ # Special_ad_categories is required by the API, set default if not provided
177
+ if special_ad_categories is None:
178
+ special_ad_categories = []
179
+
180
+ # Only warn if user omitted special_ad_categories entirely.
181
+ # If they explicitly passed [] they are saying none are needed.
182
+ compliance_warning = None
183
+ if objective == "OUTCOME_LEADS" and not special_ad_categories and not _user_provided_categories:
184
+ compliance_warning = (
185
+ "Warning: Campaign objective is OUTCOME_LEADS but no special_ad_categories were specified. "
186
+ "If this campaign is for a regulated industry (insurance, housing, employment, credit), "
187
+ "you must set special_ad_categories (e.g., FINANCIAL_PRODUCTS_SERVICES, HOUSING, EMPLOYMENT, CREDIT) "
188
+ "to comply with Meta advertising policies. Ads without the correct category may be rejected."
189
+ )
190
+
191
+ # For this example, we'll add a fixed daily budget if none is provided and we're not using ad set level budgets
192
+ if not daily_budget and not lifetime_budget and not use_adset_level_budgets:
193
+ daily_budget = "1000" # Default to $10 USD
194
+
195
+ endpoint = f"{account_id}/campaigns"
196
+
197
+ params = {
198
+ "name": name,
199
+ "objective": objective,
200
+ "status": status,
201
+ "special_ad_categories": json.dumps(special_ad_categories) # Properly format as JSON string
202
+ }
203
+
204
+ # Only set campaign-level budgets if we're not using ad set level budgets
205
+ if not use_adset_level_budgets:
206
+ # Convert budget values to strings if they aren't already
207
+ if daily_budget is not None:
208
+ params["daily_budget"] = str(daily_budget)
209
+
210
+ if lifetime_budget is not None:
211
+ params["lifetime_budget"] = str(lifetime_budget)
212
+
213
+ if campaign_budget_optimization is not None:
214
+ params["campaign_budget_optimization"] = "true" if campaign_budget_optimization else "false"
215
+ else:
216
+ # Meta API v24 requires is_adset_budget_sharing_enabled when not using campaign budget
217
+ params["is_adset_budget_sharing_enabled"] = "false"
218
+
219
+ # Add new parameters
220
+ if buying_type:
221
+ params["buying_type"] = buying_type
222
+
223
+ if bid_strategy and not use_adset_level_budgets:
224
+ params["bid_strategy"] = bid_strategy
225
+
226
+ if bid_cap is not None:
227
+ params["bid_cap"] = str(bid_cap)
228
+
229
+ if spend_cap is not None:
230
+ params["spend_cap"] = str(spend_cap)
231
+
232
+ if ab_test_control_setups:
233
+ params["ab_test_control_setups"] = json.dumps(ab_test_control_setups)
234
+
235
+ try:
236
+ data = await make_api_request(endpoint, access_token, params, method="POST")
237
+
238
+ # Add a note about budget strategy if using ad set level budgets
239
+ if use_adset_level_budgets:
240
+ data["budget_strategy"] = "ad_set_level"
241
+ data["note"] = "Campaign created with ad set level budgets. Set budgets when creating ad sets within this campaign."
242
+
243
+ if compliance_warning:
244
+ data["compliance_warning"] = compliance_warning
245
+
246
+ return json.dumps(data, indent=2)
247
+ except Exception as e:
248
+ error_msg = str(e)
249
+ return json.dumps({
250
+ "error": "Failed to create campaign",
251
+ "details": error_msg,
252
+ "params_sent": params
253
+ }, indent=2)
254
+
255
+
256
+ @meta_api_tool
257
+ async def update_campaign(
258
+ campaign_id: str,
259
+ access_token: Optional[str] = None,
260
+ name: Optional[str] = None,
261
+ status: Optional[str] = None,
262
+ special_ad_categories: Optional[List[str]] = None,
263
+ daily_budget: Optional[int] = None,
264
+ lifetime_budget: Optional[int] = None,
265
+ bid_strategy: Optional[str] = None,
266
+ bid_cap: Optional[int] = None,
267
+ spend_cap: Optional[int] = None,
268
+ campaign_budget_optimization: Optional[bool] = None,
269
+ objective: Optional[str] = None, # Add objective if it's updatable
270
+ use_adset_level_budgets: Optional[bool] = None, # Add other updatable fields as needed based on API docs
271
+ ) -> str:
272
+ """
273
+ Update an existing campaign in a Meta Ads account.
274
+
275
+ Note: Campaigns do not support start_time for scheduling -- set start_time on the ad set instead.
276
+
277
+ Args:
278
+ campaign_id: Meta Ads campaign ID
279
+ access_token: Meta API access token (optional - will use cached token if not provided)
280
+ name: New campaign name
281
+ status: New campaign status (e.g., 'ACTIVE', 'PAUSED')
282
+ special_ad_categories: List of special ad categories if applicable
283
+ daily_budget: New daily budget in account currency (in cents) as a string.
284
+ Set to empty string "" to remove the daily budget.
285
+ lifetime_budget: New lifetime budget in account currency (in cents) as a string.
286
+ Set to empty string "" to remove the lifetime budget.
287
+ bid_strategy: New bid strategy
288
+ bid_cap: New bid cap in account currency (in cents) as a string
289
+ spend_cap: New spending limit for the campaign in account currency (in cents) as a string
290
+ campaign_budget_optimization: Enable/disable campaign budget optimization
291
+ objective: New campaign objective (Note: May not always be updatable)
292
+ use_adset_level_budgets: If True, removes campaign-level budgets to switch to ad set level budgets
293
+ """
294
+ if not campaign_id:
295
+ return json.dumps({"error": "No campaign ID provided"}, indent=2)
296
+
297
+ endpoint = f"{campaign_id}"
298
+
299
+ params = {}
300
+
301
+ # Add parameters to the request only if they are provided
302
+ if name is not None:
303
+ params["name"] = name
304
+ if status is not None:
305
+ params["status"] = status
306
+ if special_ad_categories is not None:
307
+ # Note: Updating special_ad_categories might have specific API rules or might not be allowed after creation.
308
+ # The API might require an empty list `[]` to clear categories. Check Meta Docs.
309
+ params["special_ad_categories"] = json.dumps(special_ad_categories)
310
+
311
+ # Handle budget parameters based on use_adset_level_budgets setting
312
+ if use_adset_level_budgets is not None:
313
+ if use_adset_level_budgets:
314
+ # Remove campaign-level budgets when switching to ad set level budgets
315
+ params["daily_budget"] = ""
316
+ params["lifetime_budget"] = ""
317
+ if campaign_budget_optimization is not None:
318
+ params["campaign_budget_optimization"] = "false"
319
+ else:
320
+ # If switching back to campaign-level budgets, use the provided budget values
321
+ if daily_budget is not None:
322
+ if daily_budget == "":
323
+ params["daily_budget"] = ""
324
+ else:
325
+ params["daily_budget"] = str(daily_budget)
326
+ if lifetime_budget is not None:
327
+ if lifetime_budget == "":
328
+ params["lifetime_budget"] = ""
329
+ else:
330
+ params["lifetime_budget"] = str(lifetime_budget)
331
+ if campaign_budget_optimization is not None:
332
+ params["campaign_budget_optimization"] = "true" if campaign_budget_optimization else "false"
333
+ else:
334
+ # Normal budget updates when not changing budget strategy
335
+ if daily_budget is not None:
336
+ # To remove budget, set to empty string
337
+ if daily_budget == "":
338
+ params["daily_budget"] = ""
339
+ else:
340
+ params["daily_budget"] = str(daily_budget)
341
+ if lifetime_budget is not None:
342
+ # To remove budget, set to empty string
343
+ if lifetime_budget == "":
344
+ params["lifetime_budget"] = ""
345
+ else:
346
+ params["lifetime_budget"] = str(lifetime_budget)
347
+ if campaign_budget_optimization is not None:
348
+ params["campaign_budget_optimization"] = "true" if campaign_budget_optimization else "false"
349
+
350
+ if bid_strategy is not None:
351
+ params["bid_strategy"] = bid_strategy
352
+ if bid_cap is not None:
353
+ params["bid_cap"] = str(bid_cap)
354
+ if spend_cap is not None:
355
+ params["spend_cap"] = str(spend_cap)
356
+ if objective is not None:
357
+ params["objective"] = objective # Caution: Objective changes might reset learning or be restricted
358
+
359
+ if not params:
360
+ return json.dumps({"error": "No update parameters provided"}, indent=2)
361
+
362
+ try:
363
+ # Use POST method for updates as per Meta API documentation
364
+ data = await make_api_request(endpoint, access_token, params, method="POST")
365
+
366
+ # Add a note about budget strategy if switching to ad set level budgets
367
+ if use_adset_level_budgets is not None and use_adset_level_budgets:
368
+ data["budget_strategy"] = "ad_set_level"
369
+ data["note"] = "Campaign updated to use ad set level budgets. Set budgets when creating ad sets within this campaign."
370
+
371
+ return json.dumps(data, indent=2)
372
+ except Exception as e:
373
+ error_msg = str(e)
374
+ # Include campaign_id in error for better context
375
+ return json.dumps({
376
+ "error": f"Failed to update campaign {campaign_id}",
377
+ "details": error_msg,
378
+ "params_sent": params # Be careful about logging sensitive data if any
379
+ }, indent=2)