Skip to content

Commit f3a982f

Browse files
authored
fix(creatives): strip deprecated standard_enhancements from GET responses (#109)
Meta still emits standard_enhancements inside creative_features_spec on GET responses but rejects it on POST with error_subcode 3858504. LLM clients frequently copy GET payloads straight into the next mutation, which silently re-introduces the deprecated key and blocks creative writes. Strip the key from get_creative_details and get_ad_creatives at both the top-level creative_features_spec and degrees_of_freedom_spec.creative_features_spec so the model never sees it. Bumps version to 1.0.98.
1 parent 496c988 commit f3a982f

4 files changed

Lines changed: 104 additions & 5 deletions

File tree

‎meta_ads_mcp/core/ads.py‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,26 @@ def _translate_asset_customization_rules(
185185
)
186186

187187

188+
def _strip_deprecated_standard_enhancements(creative: Dict[str, Any]) -> None:
189+
"""Drop the deprecated standard_enhancements key from a creative dict in place.
190+
191+
Meta still emits `standard_enhancements` inside `creative_features_spec` on GET
192+
responses but rejects it on POST with error_subcode 3858504. LLMs frequently copy
193+
GET responses straight into the next mutation, so stripping it here prevents the
194+
deprecated field from being re-introduced via the model.
195+
"""
196+
if not isinstance(creative, dict):
197+
return
198+
cfs = creative.get("creative_features_spec")
199+
if isinstance(cfs, dict):
200+
cfs.pop("standard_enhancements", None)
201+
dof = creative.get("degrees_of_freedom_spec")
202+
if isinstance(dof, dict):
203+
dof_cfs = dof.get("creative_features_spec")
204+
if isinstance(dof_cfs, dict):
205+
dof_cfs.pop("standard_enhancements", None)
206+
207+
188208
def _translate_video_customization_rules(
189209
rules: List[Dict[str, Any]],
190210
videos_array: List[Dict[str, Any]],
@@ -536,6 +556,8 @@ async def get_creative_details(creative_id: str, access_token: Optional[str] = N
536556
except Exception:
537557
pass # Non-critical
538558

559+
_strip_deprecated_standard_enhancements(data)
560+
539561
return json.dumps(data, indent=2)
540562

541563

@@ -685,6 +707,9 @@ async def get_ad_creatives(ad_id: str, access_token: Optional[str] = None) -> st
685707
except Exception:
686708
pass # Non-critical
687709

710+
for creative in data['data']:
711+
_strip_deprecated_standard_enhancements(creative)
712+
688713
return json.dumps(data, indent=2)
689714

690715

‎pyproject.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "meta-ads-mcp"
7-
version = "1.0.97"
7+
version = "1.0.98"
88
description = "Model Context Protocol (MCP) server for Meta Ads - Use Remote MCP at pipeboard.co for easiest setup"
99
readme = "README.md"
1010
requires-python = ">=3.10"

‎tests/test_get_ad_creatives_fix.py‎

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -125,18 +125,54 @@ async def test_get_ad_creatives_no_ad_id(self):
125125

126126
async def test_get_ad_creatives_empty_data(self):
127127
"""Test get_ad_creatives when API returns empty data."""
128-
128+
129129
empty_data = {"data": []}
130-
130+
131131
with patch('meta_ads_mcp.core.ads.make_api_request', new_callable=AsyncMock) as mock_api:
132132
mock_api.return_value = empty_data
133-
133+
134134
result = await get_ad_creatives(access_token="test_token", ad_id="120228922933270272")
135135
result_data = json.loads(result)
136-
136+
137137
assert "data" in result_data
138138
assert len(result_data["data"]) == 0
139139

140+
async def test_get_ad_creatives_strips_standard_enhancements(self):
141+
"""The deprecated standard_enhancements key still surfaces on GET responses;
142+
strip it so LLMs do not echo it into a subsequent POST (error_subcode 3858504)."""
143+
144+
creatives_with_se = {
145+
"data": [
146+
{
147+
"id": "creative_1",
148+
"name": "Has SE",
149+
"status": "ACTIVE",
150+
"creative_features_spec": {
151+
"standard_enhancements": {"enroll_status": "OPT_IN"},
152+
"image_touchups": {"enroll_status": "OPT_IN"},
153+
},
154+
"degrees_of_freedom_spec": {
155+
"creative_features_spec": {
156+
"standard_enhancements": {"enroll_status": "OPT_IN"},
157+
"profile_card": {"enroll_status": "OPT_IN"},
158+
}
159+
},
160+
}
161+
]
162+
}
163+
164+
with patch('meta_ads_mcp.core.ads.make_api_request', new_callable=AsyncMock) as mock_api:
165+
mock_api.return_value = creatives_with_se
166+
167+
result = await get_ad_creatives(access_token="test_token", ad_id="120228922933270272")
168+
result_data = json.loads(result)
169+
creative = result_data["data"][0]
170+
171+
assert "standard_enhancements" not in creative["creative_features_spec"]
172+
assert creative["creative_features_spec"]["image_touchups"]["enroll_status"] == "OPT_IN"
173+
assert "standard_enhancements" not in creative["degrees_of_freedom_spec"]["creative_features_spec"]
174+
assert creative["degrees_of_freedom_spec"]["creative_features_spec"]["profile_card"]["enroll_status"] == "OPT_IN"
175+
140176

141177
def test_ad_creative_images_is_dict():
142178
"""Test that ad_creative_images is a dictionary, not a function.

‎tests/test_get_creative_details.py‎

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,3 +180,41 @@ async def test_get_creative_details_api_error():
180180

181181
data = parse_result(result)
182182
assert "error" in data
183+
184+
185+
@pytest.mark.asyncio
186+
async def test_get_creative_details_strips_standard_enhancements():
187+
"""Meta still emits the deprecated standard_enhancements key on GET; strip it
188+
so the LLM does not copy it into a subsequent POST and trip error_subcode 3858504."""
189+
mock_main_response = {
190+
"id": "creative_se",
191+
"name": "Has SE",
192+
"status": "ACTIVE",
193+
"creative_features_spec": {
194+
"standard_enhancements": {"enroll_status": "OPT_IN"},
195+
"image_touchups": {"enroll_status": "OPT_IN"},
196+
},
197+
}
198+
mock_dcs_response = {}
199+
mock_dof_response = {
200+
"degrees_of_freedom_spec": {
201+
"creative_features_spec": {
202+
"standard_enhancements": {"enroll_status": "OPT_IN"},
203+
"profile_card": {"enroll_status": "OPT_IN"},
204+
}
205+
}
206+
}
207+
mock_ps_response = {}
208+
209+
with patch("meta_ads_mcp.core.ads.make_api_request") as mock_api:
210+
mock_api.side_effect = [mock_main_response, mock_dcs_response, mock_dof_response, mock_ps_response]
211+
212+
result = await get_creative_details(
213+
creative_id="creative_se", access_token="test_token"
214+
)
215+
216+
data = parse_result(result)
217+
assert "standard_enhancements" not in data["creative_features_spec"]
218+
assert data["creative_features_spec"]["image_touchups"]["enroll_status"] == "OPT_IN"
219+
assert "standard_enhancements" not in data["degrees_of_freedom_spec"]["creative_features_spec"]
220+
assert data["degrees_of_freedom_spec"]["creative_features_spec"]["profile_card"]["enroll_status"] == "OPT_IN"

0 commit comments

Comments
 (0)