Skip to content

Commit 817ad40

Browse files
committed
Enhance adset functionality by adding support for Dynamic Creative. Updated create_adset and update_adset functions to include an optional is_dynamic_creative parameter. Modified get_adsets and get_adset_details to return is_dynamic_creative in the fields. Added tests to verify the inclusion of this parameter in API requests and responses.
1 parent 37f927c commit 817ad40

3 files changed

Lines changed: 97 additions & 5 deletions

File tree

‎meta_ads_mcp/core/ads.py‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,10 @@ async def create_ad(
111111
tracking_specs: Optional tracking specifications (e.g., for pixel events).
112112
Example: [{"action.type":"offsite_conversion","fb_pixel":["YOUR_PIXEL_ID"]}]
113113
access_token: Meta API access token (optional - will use cached token if not provided)
114+
115+
Note:
116+
Dynamic Creative creatives require the parent ad set to have `is_dynamic_creative=true`.
117+
Otherwise, ad creation will fail with error_subcode 1885998.
114118
"""
115119
# Check required parameters
116120
if not account_id:

‎meta_ads_mcp/core/adsets.py‎

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,14 @@ async def get_adsets(account_id: str, access_token: Optional[str] = None, limit:
2727
if campaign_id:
2828
endpoint = f"{campaign_id}/adsets"
2929
params = {
30-
"fields": "id,name,campaign_id,status,daily_budget,lifetime_budget,targeting,bid_amount,bid_strategy,optimization_goal,billing_event,start_time,end_time,created_time,updated_time,frequency_control_specs{event,interval_days,max_frequency}",
30+
"fields": "id,name,campaign_id,status,daily_budget,lifetime_budget,targeting,bid_amount,bid_strategy,optimization_goal,billing_event,start_time,end_time,created_time,updated_time,is_dynamic_creative,frequency_control_specs{event,interval_days,max_frequency}",
3131
"limit": limit
3232
}
3333
else:
3434
# Use account endpoint if no campaign_id is given
3535
endpoint = f"{account_id}/adsets"
3636
params = {
37-
"fields": "id,name,campaign_id,status,daily_budget,lifetime_budget,targeting,bid_amount,bid_strategy,optimization_goal,billing_event,start_time,end_time,created_time,updated_time,frequency_control_specs{event,interval_days,max_frequency}",
37+
"fields": "id,name,campaign_id,status,daily_budget,lifetime_budget,targeting,bid_amount,bid_strategy,optimization_goal,billing_event,start_time,end_time,created_time,updated_time,is_dynamic_creative,frequency_control_specs{event,interval_days,max_frequency}",
3838
"limit": limit
3939
}
4040
# Note: Removed the attempt to add campaign_id to params for the account endpoint case,
@@ -67,7 +67,7 @@ async def get_adset_details(adset_id: str, access_token: Optional[str] = None) -
6767
endpoint = f"{adset_id}"
6868
# Explicitly prioritize frequency_control_specs in the fields request
6969
params = {
70-
"fields": "id,name,campaign_id,status,frequency_control_specs{event,interval_days,max_frequency},daily_budget,lifetime_budget,targeting,bid_amount,bid_strategy,optimization_goal,billing_event,start_time,end_time,created_time,updated_time,attribution_spec,destination_type,promoted_object,pacing_type,budget_remaining,dsa_beneficiary"
70+
"fields": "id,name,campaign_id,status,frequency_control_specs{event,interval_days,max_frequency},daily_budget,lifetime_budget,targeting,bid_amount,bid_strategy,optimization_goal,billing_event,start_time,end_time,created_time,updated_time,attribution_spec,destination_type,promoted_object,pacing_type,budget_remaining,dsa_beneficiary,is_dynamic_creative"
7171
}
7272

7373
data = await make_api_request(endpoint, access_token, params)
@@ -100,6 +100,7 @@ async def create_adset(
100100
dsa_beneficiary: Optional[str] = None,
101101
promoted_object: Optional[Dict[str, Any]] = None,
102102
destination_type: Optional[str] = None,
103+
is_dynamic_creative: Optional[bool] = None,
103104
access_token: Optional[str] = None
104105
) -> str:
105106
"""
@@ -125,8 +126,9 @@ async def create_adset(
125126
Optional fields: custom_event_type, pixel_id, page_id.
126127
Example: {"application_id": "123456789012345", "object_store_url": "https://apps.apple.com/app/id123456789"}
127128
destination_type: Where users are directed after clicking the ad (e.g., 'APP_STORE', 'DEEPLINK', 'APP_INSTALL', 'ON_AD').
128-
Required for mobile app campaigns and lead generation campaigns.
129-
Use 'ON_AD' for lead generation campaigns where user interaction happens within the ad.
129+
Required for mobile app campaigns and lead generation campaigns.
130+
Use 'ON_AD' for lead generation campaigns where user interaction happens within the ad.
131+
is_dynamic_creative: Enable Dynamic Creative for this ad set (required when using dynamic creatives with asset_feed_spec/dynamic_creative_spec).
130132
access_token: Meta API access token (optional - will use cached token if not provided)
131133
"""
132134
# Check required parameters
@@ -249,6 +251,10 @@ async def create_adset(
249251
if destination_type:
250252
params["destination_type"] = destination_type
251253

254+
# Enable Dynamic Creative if requested
255+
if is_dynamic_creative is not None:
256+
params["is_dynamic_creative"] = "true" if bool(is_dynamic_creative) else "false"
257+
252258
try:
253259
data = await make_api_request(endpoint, access_token, params, method="POST")
254260
return json.dumps(data, indent=2)
@@ -290,6 +296,7 @@ async def create_adset(
290296
async def update_adset(adset_id: str, frequency_control_specs: Optional[List[Dict[str, Any]]] = None, bid_strategy: Optional[str] = None,
291297
bid_amount: Optional[int] = None, status: Optional[str] = None, targeting: Optional[Dict[str, Any]] = None,
292298
optimization_goal: Optional[str] = None, daily_budget: Optional[int] = None, lifetime_budget: Optional[int] = None,
299+
is_dynamic_creative: Optional[bool] = None,
293300
access_token: Optional[str] = None) -> str:
294301
"""
295302
Update an ad set with new settings including frequency caps and budgets.
@@ -306,6 +313,7 @@ async def update_adset(adset_id: str, frequency_control_specs: Optional[List[Dic
306313
optimization_goal: Conversion optimization goal (e.g., 'LINK_CLICKS', 'CONVERSIONS', 'APP_INSTALLS', etc.)
307314
daily_budget: Daily budget in account currency (in cents) as a string
308315
lifetime_budget: Lifetime budget in account currency (in cents) as a string
316+
is_dynamic_creative: Enable/disable Dynamic Creative for this ad set.
309317
access_token: Meta API access token (optional - will use cached token if not provided)
310318
"""
311319
if not adset_id:
@@ -342,6 +350,9 @@ async def update_adset(adset_id: str, frequency_control_specs: Optional[List[Dic
342350
if lifetime_budget is not None:
343351
params['lifetime_budget'] = str(lifetime_budget)
344352

353+
if is_dynamic_creative is not None:
354+
params['is_dynamic_creative'] = "true" if bool(is_dynamic_creative) else "false"
355+
345356
if not params:
346357
return json.dumps({"error": "No update parameters provided"}, indent=2)
347358

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import json
2+
import pytest
3+
from unittest.mock import AsyncMock, patch
4+
5+
from meta_ads_mcp.core.adsets import create_adset, update_adset, get_adsets, get_adset_details
6+
7+
8+
@pytest.mark.asyncio
9+
async def test_create_adset_includes_is_dynamic_creative_true():
10+
sample_response = {"id": "adset_1", "name": "DC Adset"}
11+
with patch('meta_ads_mcp.core.adsets.make_api_request', new_callable=AsyncMock) as mock_api:
12+
mock_api.return_value = sample_response
13+
14+
result = await create_adset(
15+
account_id="act_123",
16+
campaign_id="cmp_1",
17+
name="DC Adset",
18+
optimization_goal="LINK_CLICKS",
19+
billing_event="IMPRESSIONS",
20+
targeting={"geo_locations": {"countries": ["US"]}},
21+
is_dynamic_creative=True,
22+
access_token="test_token",
23+
)
24+
25+
assert json.loads(result)["id"] == "adset_1"
26+
# Verify param was sent as string boolean
27+
call_args = mock_api.call_args
28+
params = call_args[0][2]
29+
assert params["is_dynamic_creative"] == "true"
30+
31+
32+
@pytest.mark.asyncio
33+
async def test_update_adset_includes_is_dynamic_creative_false():
34+
sample_response = {"success": True}
35+
with patch('meta_ads_mcp.core.adsets.make_api_request', new_callable=AsyncMock) as mock_api:
36+
mock_api.return_value = sample_response
37+
38+
result = await update_adset(
39+
adset_id="120",
40+
is_dynamic_creative=False,
41+
access_token="test_token",
42+
)
43+
44+
assert json.loads(result)["success"] is True
45+
call_args = mock_api.call_args
46+
params = call_args[0][2]
47+
assert params["is_dynamic_creative"] == "false"
48+
49+
50+
@pytest.mark.asyncio
51+
async def test_get_adsets_fields_include_is_dynamic_creative():
52+
sample_response = {"data": []}
53+
with patch('meta_ads_mcp.core.adsets.make_api_request', new_callable=AsyncMock) as mock_api:
54+
mock_api.return_value = sample_response
55+
56+
result = await get_adsets(account_id="act_123", access_token="test_token", limit=1)
57+
assert json.loads(result)["data"] == []
58+
59+
call_args = mock_api.call_args
60+
params = call_args[0][2]
61+
assert "is_dynamic_creative" in params.get("fields", "")
62+
63+
64+
@pytest.mark.asyncio
65+
async def test_get_adset_details_fields_include_is_dynamic_creative():
66+
sample_response = {"id": "120", "name": "Test", "is_dynamic_creative": True}
67+
with patch('meta_ads_mcp.core.adsets.make_api_request', new_callable=AsyncMock) as mock_api:
68+
mock_api.return_value = sample_response
69+
70+
result = await get_adset_details(adset_id="120", access_token="test_token")
71+
assert json.loads(result)["id"] == "120"
72+
73+
call_args = mock_api.call_args
74+
params = call_args[0][2]
75+
assert "is_dynamic_creative" in params.get("fields", "")
76+
77+

0 commit comments

Comments
 (0)