Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions meta_ads_mcp/core/ads.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,26 @@ def _translate_asset_customization_rules(
)


def _strip_deprecated_standard_enhancements(creative: Dict[str, Any]) -> None:
"""Drop the deprecated standard_enhancements key from a creative dict in place.

Meta still emits `standard_enhancements` inside `creative_features_spec` on GET
responses but rejects it on POST with error_subcode 3858504. LLMs frequently copy
GET responses straight into the next mutation, so stripping it here prevents the
deprecated field from being re-introduced via the model.
"""
if not isinstance(creative, dict):
return
cfs = creative.get("creative_features_spec")
if isinstance(cfs, dict):
cfs.pop("standard_enhancements", None)
dof = creative.get("degrees_of_freedom_spec")
if isinstance(dof, dict):
dof_cfs = dof.get("creative_features_spec")
if isinstance(dof_cfs, dict):
dof_cfs.pop("standard_enhancements", None)


def _translate_video_customization_rules(
rules: List[Dict[str, Any]],
videos_array: List[Dict[str, Any]],
Expand Down Expand Up @@ -536,6 +556,8 @@ async def get_creative_details(creative_id: str, access_token: Optional[str] = N
except Exception:
pass # Non-critical

_strip_deprecated_standard_enhancements(data)

return json.dumps(data, indent=2)


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

for creative in data['data']:
_strip_deprecated_standard_enhancements(creative)

return json.dumps(data, indent=2)


Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "hatchling.build"

[project]
name = "meta-ads-mcp"
version = "1.0.97"
version = "1.0.98"
description = "Model Context Protocol (MCP) server for Meta Ads - Use Remote MCP at pipeboard.co for easiest setup"
readme = "README.md"
requires-python = ">=3.10"
Expand Down
44 changes: 40 additions & 4 deletions tests/test_get_ad_creatives_fix.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,18 +125,54 @@ async def test_get_ad_creatives_no_ad_id(self):

async def test_get_ad_creatives_empty_data(self):
"""Test get_ad_creatives when API returns empty data."""

empty_data = {"data": []}

with patch('meta_ads_mcp.core.ads.make_api_request', new_callable=AsyncMock) as mock_api:
mock_api.return_value = empty_data

result = await get_ad_creatives(access_token="test_token", ad_id="120228922933270272")
result_data = json.loads(result)

assert "data" in result_data
assert len(result_data["data"]) == 0

async def test_get_ad_creatives_strips_standard_enhancements(self):
"""The deprecated standard_enhancements key still surfaces on GET responses;
strip it so LLMs do not echo it into a subsequent POST (error_subcode 3858504)."""

creatives_with_se = {
"data": [
{
"id": "creative_1",
"name": "Has SE",
"status": "ACTIVE",
"creative_features_spec": {
"standard_enhancements": {"enroll_status": "OPT_IN"},
"image_touchups": {"enroll_status": "OPT_IN"},
},
"degrees_of_freedom_spec": {
"creative_features_spec": {
"standard_enhancements": {"enroll_status": "OPT_IN"},
"profile_card": {"enroll_status": "OPT_IN"},
}
},
}
]
}

with patch('meta_ads_mcp.core.ads.make_api_request', new_callable=AsyncMock) as mock_api:
mock_api.return_value = creatives_with_se

result = await get_ad_creatives(access_token="test_token", ad_id="120228922933270272")
result_data = json.loads(result)
creative = result_data["data"][0]

assert "standard_enhancements" not in creative["creative_features_spec"]
assert creative["creative_features_spec"]["image_touchups"]["enroll_status"] == "OPT_IN"
assert "standard_enhancements" not in creative["degrees_of_freedom_spec"]["creative_features_spec"]
assert creative["degrees_of_freedom_spec"]["creative_features_spec"]["profile_card"]["enroll_status"] == "OPT_IN"


def test_ad_creative_images_is_dict():
"""Test that ad_creative_images is a dictionary, not a function.
Expand Down
38 changes: 38 additions & 0 deletions tests/test_get_creative_details.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,3 +180,41 @@ async def test_get_creative_details_api_error():

data = parse_result(result)
assert "error" in data


@pytest.mark.asyncio
async def test_get_creative_details_strips_standard_enhancements():
"""Meta still emits the deprecated standard_enhancements key on GET; strip it
so the LLM does not copy it into a subsequent POST and trip error_subcode 3858504."""
mock_main_response = {
"id": "creative_se",
"name": "Has SE",
"status": "ACTIVE",
"creative_features_spec": {
"standard_enhancements": {"enroll_status": "OPT_IN"},
"image_touchups": {"enroll_status": "OPT_IN"},
},
}
mock_dcs_response = {}
mock_dof_response = {
"degrees_of_freedom_spec": {
"creative_features_spec": {
"standard_enhancements": {"enroll_status": "OPT_IN"},
"profile_card": {"enroll_status": "OPT_IN"},
}
}
}
mock_ps_response = {}

with patch("meta_ads_mcp.core.ads.make_api_request") as mock_api:
mock_api.side_effect = [mock_main_response, mock_dcs_response, mock_dof_response, mock_ps_response]

result = await get_creative_details(
creative_id="creative_se", access_token="test_token"
)

data = parse_result(result)
assert "standard_enhancements" not in data["creative_features_spec"]
assert data["creative_features_spec"]["image_touchups"]["enroll_status"] == "OPT_IN"
assert "standard_enhancements" not in data["degrees_of_freedom_spec"]["creative_features_spec"]
assert data["degrees_of_freedom_spec"]["creative_features_spec"]["profile_card"]["enroll_status"] == "OPT_IN"
Loading