meta-ads-mcp 1.0.13__tar.gz → 1.0.15__tar.gz
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.
Potentially problematic release.
This version of meta-ads-mcp might be problematic. Click here for more details.
- {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/PKG-INFO +4 -2
- {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/README.md +3 -1
- {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/meta_ads_mcp/__init__.py +1 -1
- {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/meta_ads_mcp/core/ads.py +69 -75
- {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/meta_ads_mcp/core/api.py +3 -2
- {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/meta_ads_mcp/core/authentication.py +2 -1
- {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/meta_ads_mcp/core/duplication.py +2 -2
- {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/pyproject.toml +1 -1
- {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/server.json +2 -2
- {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/tests/test_account_info_access_fix.py +7 -7
- {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/tests/test_budget_update.py +2 -2
- meta_ads_mcp-1.0.15/tests/test_create_ad_creative_simple.py +127 -0
- meta_ads_mcp-1.0.15/tests/test_create_simple_creative_e2e.py +49 -0
- {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/tests/test_dsa_beneficiary.py +23 -23
- {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/tests/test_dsa_integration.py +14 -14
- {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/tests/test_duplication.py +1 -1
- {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/tests/test_duplication_regression.py +1 -1
- {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/tests/test_dynamic_creatives.py +12 -14
- {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/tests/test_estimate_audience_size.py +1 -1
- {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/tests/test_get_account_pages.py +8 -8
- {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/tests/test_insights_actions_and_values_e2e.py +1 -1
- {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/tests/test_insights_pagination.py +1 -1
- {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/tests/test_mobile_app_adset_creation.py +1 -1
- {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/tests/test_targeting.py +2 -2
- {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/.github/workflows/publish-mcp.yml +0 -0
- {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/.github/workflows/publish.yml +0 -0
- {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/.github/workflows/test.yml +0 -0
- {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/.gitignore +0 -0
- {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/CUSTOM_META_APP.md +0 -0
- {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/Dockerfile +0 -0
- {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/LICENSE +0 -0
- {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/LOCAL_INSTALLATION.md +0 -0
- {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/META_API_NOTES.md +0 -0
- {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/RELEASE.md +0 -0
- {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/STREAMABLE_HTTP_SETUP.md +0 -0
- {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/examples/README.md +0 -0
- {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/examples/example_http_client.py +0 -0
- {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/future_improvements.md +0 -0
- {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/images/meta-ads-example.png +0 -0
- {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/meta_ads_auth.sh +0 -0
- {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/meta_ads_mcp/__main__.py +0 -0
- {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/meta_ads_mcp/core/__init__.py +0 -0
- {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/meta_ads_mcp/core/accounts.py +0 -0
- {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/meta_ads_mcp/core/ads_library.py +0 -0
- {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/meta_ads_mcp/core/adsets.py +0 -0
- {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/meta_ads_mcp/core/auth.py +0 -0
- {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/meta_ads_mcp/core/budget_schedules.py +0 -0
- {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/meta_ads_mcp/core/callback_server.py +0 -0
- {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/meta_ads_mcp/core/campaigns.py +0 -0
- {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/meta_ads_mcp/core/http_auth_integration.py +0 -0
- {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/meta_ads_mcp/core/insights.py +0 -0
- {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/meta_ads_mcp/core/openai_deep_research.py +0 -0
- {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/meta_ads_mcp/core/pipeboard_auth.py +0 -0
- {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/meta_ads_mcp/core/reports.py +0 -0
- {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/meta_ads_mcp/core/resources.py +0 -0
- {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/meta_ads_mcp/core/server.py +0 -0
- {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/meta_ads_mcp/core/targeting.py +0 -0
- {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/meta_ads_mcp/core/utils.py +0 -0
- {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/requirements.txt +0 -0
- {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/setup.py +0 -0
- {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/smithery.yaml +0 -0
- {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/tests/README.md +0 -0
- {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/tests/README_REGRESSION_TESTS.md +0 -0
- {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/tests/__init__.py +0 -0
- {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/tests/conftest.py +0 -0
- {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/tests/e2e_account_info_search_issue.py +0 -0
- {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/tests/test_account_search.py +0 -0
- {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/tests/test_budget_update_e2e.py +0 -0
- {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/tests/test_estimate_audience_size_e2e.py +0 -0
- {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/tests/test_get_ad_creatives_fix.py +0 -0
- {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/tests/test_get_ad_image_quality_improvements.py +0 -0
- {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/tests/test_get_ad_image_regression.py +0 -0
- {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/tests/test_http_transport.py +0 -0
- {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/tests/test_integration_openai_mcp.py +0 -0
- {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/tests/test_is_dynamic_creative_adset.py +0 -0
- {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/tests/test_mobile_app_adset_issue.py +0 -0
- {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/tests/test_openai.py +0 -0
- {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/tests/test_openai_mcp_deep_research.py +0 -0
- {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/tests/test_page_discovery.py +0 -0
- {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/tests/test_page_discovery_integration.py +0 -0
- {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/tests/test_targeting_search_e2e.py +0 -0
- {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/tests/test_update_ad_creative_id.py +0 -0
- {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/tests/test_upload_ad_image.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: meta-ads-mcp
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.15
|
|
4
4
|
Summary: Model Context Protocol (MCP) server for interacting with Meta Ads API
|
|
5
5
|
Project-URL: Homepage, https://github.com/pipeboard-co/meta-ads-mcp
|
|
6
6
|
Project-URL: Bug Tracker, https://github.com/pipeboard-co/meta-ads-mcp/issues
|
|
@@ -25,12 +25,14 @@ Description-Content-Type: text/markdown
|
|
|
25
25
|
|
|
26
26
|
# Meta Ads MCP
|
|
27
27
|
|
|
28
|
-
A [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) server for interacting with Meta Ads
|
|
28
|
+
A [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) server for interacting with Meta Ads. Analyze, manage and optimize Meta advertising campaigns through an AI interface. Use an LLM to retrieve performance data, visualize ad creatives, and provide strategic insights for your ads on Facebook, Instagram, and other Meta platforms.
|
|
29
29
|
|
|
30
30
|
> **DISCLAIMER:** This is an unofficial third-party tool and is not associated with, endorsed by, or affiliated with Meta in any way. This project is maintained independently and uses Meta's public APIs according to their terms of service. Meta, Facebook, Instagram, and other Meta brand names are trademarks of their respective owners.
|
|
31
31
|
|
|
32
32
|
[](https://github.com/user-attachments/assets/3e605cee-d289-414b-814c-6299e7f3383e)
|
|
33
33
|
|
|
34
|
+
[](https://lobehub.com/mcp/nictuku-meta-ads-mcp)
|
|
35
|
+
|
|
34
36
|
mcp-name: co.pipeboard/meta-ads-mcp
|
|
35
37
|
|
|
36
38
|
## Community & Support
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
# Meta Ads MCP
|
|
2
2
|
|
|
3
|
-
A [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) server for interacting with Meta Ads
|
|
3
|
+
A [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) server for interacting with Meta Ads. Analyze, manage and optimize Meta advertising campaigns through an AI interface. Use an LLM to retrieve performance data, visualize ad creatives, and provide strategic insights for your ads on Facebook, Instagram, and other Meta platforms.
|
|
4
4
|
|
|
5
5
|
> **DISCLAIMER:** This is an unofficial third-party tool and is not associated with, endorsed by, or affiliated with Meta in any way. This project is maintained independently and uses Meta's public APIs according to their terms of service. Meta, Facebook, Instagram, and other Meta brand names are trademarks of their respective owners.
|
|
6
6
|
|
|
7
7
|
[](https://github.com/user-attachments/assets/3e605cee-d289-414b-814c-6299e7f3383e)
|
|
8
8
|
|
|
9
|
+
[](https://lobehub.com/mcp/nictuku-meta-ads-mcp)
|
|
10
|
+
|
|
9
11
|
mcp-name: co.pipeboard/meta-ads-mcp
|
|
10
12
|
|
|
11
13
|
## Community & Support
|
|
@@ -831,32 +831,18 @@ async def create_ad_creative(
|
|
|
831
831
|
if description and descriptions:
|
|
832
832
|
return json.dumps({"error": "Cannot specify both 'description' and 'descriptions'. Use 'description' for single description or 'descriptions' for multiple."}, indent=2)
|
|
833
833
|
|
|
834
|
-
#
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
if headline:
|
|
839
|
-
final_headlines = [headline]
|
|
840
|
-
elif headlines:
|
|
841
|
-
final_headlines = headlines
|
|
842
|
-
|
|
843
|
-
if description:
|
|
844
|
-
final_descriptions = [description]
|
|
845
|
-
elif descriptions:
|
|
846
|
-
final_descriptions = descriptions
|
|
847
|
-
|
|
848
|
-
# Validate dynamic creative parameters
|
|
849
|
-
if final_headlines:
|
|
850
|
-
if len(final_headlines) > 5:
|
|
834
|
+
# Validate dynamic creative parameters (plural forms only)
|
|
835
|
+
if headlines:
|
|
836
|
+
if len(headlines) > 5:
|
|
851
837
|
return json.dumps({"error": "Maximum 5 headlines allowed for dynamic creatives"}, indent=2)
|
|
852
|
-
for i, h in enumerate(
|
|
838
|
+
for i, h in enumerate(headlines):
|
|
853
839
|
if len(h) > 40:
|
|
854
840
|
return json.dumps({"error": f"Headline {i+1} exceeds 40 character limit"}, indent=2)
|
|
855
841
|
|
|
856
|
-
if
|
|
857
|
-
if len(
|
|
842
|
+
if descriptions:
|
|
843
|
+
if len(descriptions) > 5:
|
|
858
844
|
return json.dumps({"error": "Maximum 5 descriptions allowed for dynamic creatives"}, indent=2)
|
|
859
|
-
for i, d in enumerate(
|
|
845
|
+
for i, d in enumerate(descriptions):
|
|
860
846
|
if len(d) > 125:
|
|
861
847
|
return json.dumps({"error": f"Description {i+1} exceeds 125 character limit"}, indent=2)
|
|
862
848
|
|
|
@@ -866,8 +852,9 @@ async def create_ad_creative(
|
|
|
866
852
|
}
|
|
867
853
|
|
|
868
854
|
# Choose between asset_feed_spec (dynamic creative) or object_story_spec (traditional)
|
|
869
|
-
|
|
870
|
-
|
|
855
|
+
# ONLY use asset_feed_spec when user explicitly provides plural parameters (headlines/descriptions)
|
|
856
|
+
if headlines or descriptions:
|
|
857
|
+
# Use asset_feed_spec for dynamic creatives with multiple variants
|
|
871
858
|
asset_feed_spec = {
|
|
872
859
|
"ad_formats": ["SINGLE_IMAGE"],
|
|
873
860
|
"images": [{"hash": image_hash}],
|
|
@@ -875,12 +862,12 @@ async def create_ad_creative(
|
|
|
875
862
|
}
|
|
876
863
|
|
|
877
864
|
# Handle headlines
|
|
878
|
-
if
|
|
879
|
-
asset_feed_spec["headlines"] = [{"text": headline_text} for headline_text in
|
|
865
|
+
if headlines:
|
|
866
|
+
asset_feed_spec["headlines"] = [{"text": headline_text} for headline_text in headlines]
|
|
880
867
|
|
|
881
868
|
# Handle descriptions
|
|
882
|
-
if
|
|
883
|
-
asset_feed_spec["descriptions"] = [{"text": description_text} for description_text in
|
|
869
|
+
if descriptions:
|
|
870
|
+
asset_feed_spec["descriptions"] = [{"text": description_text} for description_text in descriptions]
|
|
884
871
|
|
|
885
872
|
# Add message as primary_texts if provided
|
|
886
873
|
if message:
|
|
@@ -897,7 +884,7 @@ async def create_ad_creative(
|
|
|
897
884
|
"page_id": page_id
|
|
898
885
|
}
|
|
899
886
|
else:
|
|
900
|
-
# Use traditional object_story_spec for
|
|
887
|
+
# Use traditional object_story_spec with link_data for simple creatives
|
|
901
888
|
creative_data["object_story_spec"] = {
|
|
902
889
|
"page_id": page_id,
|
|
903
890
|
"link_data": {
|
|
@@ -909,17 +896,25 @@ async def create_ad_creative(
|
|
|
909
896
|
# Add optional parameters if provided
|
|
910
897
|
if message:
|
|
911
898
|
creative_data["object_story_spec"]["link_data"]["message"] = message
|
|
899
|
+
|
|
900
|
+
# Add headline (singular) to link_data
|
|
901
|
+
if headline:
|
|
902
|
+
creative_data["object_story_spec"]["link_data"]["name"] = headline
|
|
903
|
+
|
|
904
|
+
# Add description (singular) to link_data
|
|
905
|
+
if description:
|
|
906
|
+
creative_data["object_story_spec"]["link_data"]["description"] = description
|
|
907
|
+
|
|
908
|
+
# Add call_to_action to link_data for simple creatives
|
|
909
|
+
if call_to_action_type:
|
|
910
|
+
creative_data["object_story_spec"]["link_data"]["call_to_action"] = {
|
|
911
|
+
"type": call_to_action_type
|
|
912
|
+
}
|
|
912
913
|
|
|
913
914
|
# Add dynamic creative spec if provided
|
|
914
915
|
if dynamic_creative_spec:
|
|
915
916
|
creative_data["dynamic_creative_spec"] = dynamic_creative_spec
|
|
916
917
|
|
|
917
|
-
# Only add call_to_action to object_story_spec if we're not using asset_feed_spec
|
|
918
|
-
if call_to_action_type and "asset_feed_spec" not in creative_data:
|
|
919
|
-
creative_data["object_story_spec"]["link_data"]["call_to_action"] = {
|
|
920
|
-
"type": call_to_action_type
|
|
921
|
-
}
|
|
922
|
-
|
|
923
918
|
if instagram_actor_id:
|
|
924
919
|
creative_data["instagram_actor_id"] = instagram_actor_id
|
|
925
920
|
|
|
@@ -998,32 +993,18 @@ async def update_ad_creative(
|
|
|
998
993
|
if description and descriptions:
|
|
999
994
|
return json.dumps({"error": "Cannot specify both 'description' and 'descriptions'. Use 'description' for single description or 'descriptions' for multiple."}, indent=2)
|
|
1000
995
|
|
|
1001
|
-
#
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
if headline:
|
|
1006
|
-
final_headlines = [headline]
|
|
1007
|
-
elif headlines:
|
|
1008
|
-
final_headlines = headlines
|
|
1009
|
-
|
|
1010
|
-
if description:
|
|
1011
|
-
final_descriptions = [description]
|
|
1012
|
-
elif descriptions:
|
|
1013
|
-
final_descriptions = descriptions
|
|
1014
|
-
|
|
1015
|
-
# Validate dynamic creative parameters
|
|
1016
|
-
if final_headlines:
|
|
1017
|
-
if len(final_headlines) > 5:
|
|
996
|
+
# Validate dynamic creative parameters (plural forms only)
|
|
997
|
+
if headlines:
|
|
998
|
+
if len(headlines) > 5:
|
|
1018
999
|
return json.dumps({"error": "Maximum 5 headlines allowed for dynamic creatives"}, indent=2)
|
|
1019
|
-
for i, h in enumerate(
|
|
1000
|
+
for i, h in enumerate(headlines):
|
|
1020
1001
|
if len(h) > 40:
|
|
1021
1002
|
return json.dumps({"error": f"Headline {i+1} exceeds 40 character limit"}, indent=2)
|
|
1022
1003
|
|
|
1023
|
-
if
|
|
1024
|
-
if len(
|
|
1004
|
+
if descriptions:
|
|
1005
|
+
if len(descriptions) > 5:
|
|
1025
1006
|
return json.dumps({"error": "Maximum 5 descriptions allowed for dynamic creatives"}, indent=2)
|
|
1026
|
-
for i, d in enumerate(
|
|
1007
|
+
for i, d in enumerate(descriptions):
|
|
1027
1008
|
if len(d) > 125:
|
|
1028
1009
|
return json.dumps({"error": f"Description {i+1} exceeds 125 character limit"}, indent=2)
|
|
1029
1010
|
|
|
@@ -1033,45 +1014,58 @@ async def update_ad_creative(
|
|
|
1033
1014
|
if name:
|
|
1034
1015
|
update_data["name"] = name
|
|
1035
1016
|
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
if final_headlines or final_descriptions or dynamic_creative_spec:
|
|
1017
|
+
# Choose between asset_feed_spec (dynamic creative) or object_story_spec (traditional)
|
|
1018
|
+
# ONLY use asset_feed_spec when user explicitly provides plural parameters (headlines/descriptions)
|
|
1019
|
+
if headlines or descriptions or dynamic_creative_spec:
|
|
1020
|
+
# Handle dynamic creative assets via asset_feed_spec
|
|
1041
1021
|
asset_feed_spec = {}
|
|
1042
1022
|
|
|
1043
1023
|
# Add required ad_formats field for dynamic creatives
|
|
1044
1024
|
asset_feed_spec["ad_formats"] = ["SINGLE_IMAGE"]
|
|
1045
1025
|
|
|
1046
1026
|
# Handle headlines
|
|
1047
|
-
if
|
|
1048
|
-
asset_feed_spec["headlines"] = [{"text": headline_text} for headline_text in
|
|
1027
|
+
if headlines:
|
|
1028
|
+
asset_feed_spec["headlines"] = [{"text": headline_text} for headline_text in headlines]
|
|
1049
1029
|
|
|
1050
1030
|
# Handle descriptions
|
|
1051
|
-
if
|
|
1052
|
-
asset_feed_spec["descriptions"] = [{"text": description_text} for description_text in
|
|
1031
|
+
if descriptions:
|
|
1032
|
+
asset_feed_spec["descriptions"] = [{"text": description_text} for description_text in descriptions]
|
|
1053
1033
|
|
|
1054
1034
|
# Add message as primary_texts if provided
|
|
1055
1035
|
if message:
|
|
1056
1036
|
asset_feed_spec["primary_texts"] = [{"text": message}]
|
|
1057
1037
|
|
|
1038
|
+
# Add call_to_action_types if provided
|
|
1039
|
+
if call_to_action_type:
|
|
1040
|
+
asset_feed_spec["call_to_action_types"] = [call_to_action_type]
|
|
1041
|
+
|
|
1058
1042
|
update_data["asset_feed_spec"] = asset_feed_spec
|
|
1043
|
+
else:
|
|
1044
|
+
# Use traditional object_story_spec with link_data for simple creatives
|
|
1045
|
+
if message or headline or description or call_to_action_type:
|
|
1046
|
+
update_data["object_story_spec"] = {"link_data": {}}
|
|
1047
|
+
|
|
1048
|
+
if message:
|
|
1049
|
+
update_data["object_story_spec"]["link_data"]["message"] = message
|
|
1050
|
+
|
|
1051
|
+
# Add headline (singular) to link_data
|
|
1052
|
+
if headline:
|
|
1053
|
+
update_data["object_story_spec"]["link_data"]["name"] = headline
|
|
1054
|
+
|
|
1055
|
+
# Add description (singular) to link_data
|
|
1056
|
+
if description:
|
|
1057
|
+
update_data["object_story_spec"]["link_data"]["description"] = description
|
|
1058
|
+
|
|
1059
|
+
# Add call_to_action to link_data for simple creatives
|
|
1060
|
+
if call_to_action_type:
|
|
1061
|
+
update_data["object_story_spec"]["link_data"]["call_to_action"] = {
|
|
1062
|
+
"type": call_to_action_type
|
|
1063
|
+
}
|
|
1059
1064
|
|
|
1060
1065
|
# Add dynamic creative spec if provided
|
|
1061
1066
|
if dynamic_creative_spec:
|
|
1062
1067
|
update_data["dynamic_creative_spec"] = dynamic_creative_spec
|
|
1063
1068
|
|
|
1064
|
-
# Handle call_to_action - add to asset_feed_spec if using dynamic creative, otherwise to object_story_spec
|
|
1065
|
-
if call_to_action_type:
|
|
1066
|
-
if "asset_feed_spec" in update_data:
|
|
1067
|
-
update_data["asset_feed_spec"]["call_to_action_types"] = [call_to_action_type]
|
|
1068
|
-
else:
|
|
1069
|
-
if "object_story_spec" not in update_data:
|
|
1070
|
-
update_data["object_story_spec"] = {"link_data": {}}
|
|
1071
|
-
update_data["object_story_spec"]["link_data"]["call_to_action"] = {
|
|
1072
|
-
"type": call_to_action_type
|
|
1073
|
-
}
|
|
1074
|
-
|
|
1075
1069
|
# Prepare the API endpoint for updating the creative
|
|
1076
1070
|
endpoint = f"{creative_id}"
|
|
1077
1071
|
|
|
@@ -6,7 +6,8 @@ import httpx
|
|
|
6
6
|
import asyncio
|
|
7
7
|
import functools
|
|
8
8
|
import os
|
|
9
|
-
from .
|
|
9
|
+
from . import auth
|
|
10
|
+
from .auth import needs_authentication, auth_manager, start_callback_server, shutdown_callback_server
|
|
10
11
|
from .utils import logger
|
|
11
12
|
|
|
12
13
|
# Constants
|
|
@@ -203,7 +204,7 @@ def meta_api_tool(func):
|
|
|
203
204
|
# If access_token is not in kwargs or not kwargs['access_token'], try to get it from auth_manager
|
|
204
205
|
if 'access_token' not in kwargs or not kwargs['access_token']:
|
|
205
206
|
try:
|
|
206
|
-
access_token = await get_current_access_token()
|
|
207
|
+
access_token = await auth.get_current_access_token()
|
|
207
208
|
if access_token:
|
|
208
209
|
kwargs['access_token'] = access_token
|
|
209
210
|
logger.debug("Using access token from auth_manager")
|
|
@@ -29,7 +29,8 @@ from typing import Optional
|
|
|
29
29
|
import asyncio
|
|
30
30
|
import os
|
|
31
31
|
from .api import meta_api_tool
|
|
32
|
-
from .
|
|
32
|
+
from . import auth
|
|
33
|
+
from .auth import start_callback_server, shutdown_callback_server, auth_manager
|
|
33
34
|
from .server import mcp_server
|
|
34
35
|
from .utils import logger, META_APP_SECRET
|
|
35
36
|
from .pipeboard_auth import pipeboard_auth_manager
|
|
@@ -6,7 +6,7 @@ import httpx
|
|
|
6
6
|
from typing import Optional, Dict, Any, List, Union
|
|
7
7
|
from .server import mcp_server
|
|
8
8
|
from .api import meta_api_tool
|
|
9
|
-
from .
|
|
9
|
+
from . import auth
|
|
10
10
|
from .http_auth_integration import FastMCPAuthIntegration
|
|
11
11
|
|
|
12
12
|
|
|
@@ -204,7 +204,7 @@ async def _forward_duplication_request(resource_type: str, resource_id: str, acc
|
|
|
204
204
|
|
|
205
205
|
# Use provided access_token parameter if no Facebook token found in context
|
|
206
206
|
if not facebook_token:
|
|
207
|
-
facebook_token = access_token if access_token else await get_current_access_token()
|
|
207
|
+
facebook_token = access_token if access_token else await auth.get_current_access_token()
|
|
208
208
|
|
|
209
209
|
# Validate we have both required tokens
|
|
210
210
|
if not pipeboard_token:
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-09-16/server.schema.json",
|
|
3
3
|
"name": "co.pipeboard/meta-ads-mcp",
|
|
4
4
|
"description": "Facebook / Meta Ads automation with AI: analyze performance, test creatives, optimize spend.",
|
|
5
|
-
"version": "1.0.
|
|
5
|
+
"version": "1.0.15",
|
|
6
6
|
"remotes": [
|
|
7
7
|
{
|
|
8
8
|
"type": "streamable-http",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
{
|
|
14
14
|
"registryType": "pypi",
|
|
15
15
|
"identifier": "meta-ads-mcp",
|
|
16
|
-
"version": "1.0.
|
|
16
|
+
"version": "1.0.15",
|
|
17
17
|
"transport": {
|
|
18
18
|
"type": "stdio"
|
|
19
19
|
}
|
|
@@ -38,7 +38,7 @@ class TestAccountInfoAccessFix:
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
with patch('meta_ads_mcp.core.accounts.make_api_request', new_callable=AsyncMock) as mock_api:
|
|
41
|
-
with patch('meta_ads_mcp.core.
|
|
41
|
+
with patch('meta_ads_mcp.core.auth.get_current_access_token', new_callable=AsyncMock) as mock_auth:
|
|
42
42
|
mock_auth.return_value = "test_access_token"
|
|
43
43
|
mock_api.return_value = mock_account_response
|
|
44
44
|
|
|
@@ -95,7 +95,7 @@ class TestAccountInfoAccessFix:
|
|
|
95
95
|
}
|
|
96
96
|
|
|
97
97
|
with patch('meta_ads_mcp.core.accounts.make_api_request', new_callable=AsyncMock) as mock_api:
|
|
98
|
-
with patch('meta_ads_mcp.core.
|
|
98
|
+
with patch('meta_ads_mcp.core.auth.get_current_access_token', new_callable=AsyncMock) as mock_auth:
|
|
99
99
|
mock_auth.return_value = "test_access_token"
|
|
100
100
|
|
|
101
101
|
# First call returns permission error, second call returns accessible accounts
|
|
@@ -158,7 +158,7 @@ class TestAccountInfoAccessFix:
|
|
|
158
158
|
}
|
|
159
159
|
|
|
160
160
|
with patch('meta_ads_mcp.core.accounts.make_api_request', new_callable=AsyncMock) as mock_api:
|
|
161
|
-
with patch('meta_ads_mcp.core.
|
|
161
|
+
with patch('meta_ads_mcp.core.auth.get_current_access_token', new_callable=AsyncMock) as mock_auth:
|
|
162
162
|
mock_auth.return_value = "test_access_token"
|
|
163
163
|
mock_api.return_value = mock_error_response
|
|
164
164
|
|
|
@@ -180,7 +180,7 @@ class TestAccountInfoAccessFix:
|
|
|
180
180
|
async def test_account_info_missing_account_id_error(self):
|
|
181
181
|
"""Test that missing account_id parameter returns appropriate error"""
|
|
182
182
|
|
|
183
|
-
with patch('meta_ads_mcp.core.
|
|
183
|
+
with patch('meta_ads_mcp.core.auth.get_current_access_token', new_callable=AsyncMock) as mock_auth:
|
|
184
184
|
mock_auth.return_value = "test_access_token"
|
|
185
185
|
|
|
186
186
|
result = await get_account_info(account_id=None)
|
|
@@ -207,7 +207,7 @@ class TestAccountInfoAccessFix:
|
|
|
207
207
|
}
|
|
208
208
|
|
|
209
209
|
with patch('meta_ads_mcp.core.accounts.make_api_request', new_callable=AsyncMock) as mock_api:
|
|
210
|
-
with patch('meta_ads_mcp.core.
|
|
210
|
+
with patch('meta_ads_mcp.core.auth.get_current_access_token', new_callable=AsyncMock) as mock_auth:
|
|
211
211
|
mock_auth.return_value = "test_access_token"
|
|
212
212
|
mock_api.return_value = mock_account_response
|
|
213
213
|
|
|
@@ -236,7 +236,7 @@ class TestAccountInfoAccessFix:
|
|
|
236
236
|
}
|
|
237
237
|
|
|
238
238
|
with patch('meta_ads_mcp.core.accounts.make_api_request', new_callable=AsyncMock) as mock_api:
|
|
239
|
-
with patch('meta_ads_mcp.core.
|
|
239
|
+
with patch('meta_ads_mcp.core.auth.get_current_access_token', new_callable=AsyncMock) as mock_auth:
|
|
240
240
|
mock_auth.return_value = "test_access_token"
|
|
241
241
|
mock_api.return_value = mock_account_response
|
|
242
242
|
|
|
@@ -272,7 +272,7 @@ class TestAccountInfoAccessRegression:
|
|
|
272
272
|
}
|
|
273
273
|
|
|
274
274
|
with patch('meta_ads_mcp.core.accounts.make_api_request', new_callable=AsyncMock) as mock_api:
|
|
275
|
-
with patch('meta_ads_mcp.core.
|
|
275
|
+
with patch('meta_ads_mcp.core.auth.get_current_access_token', new_callable=AsyncMock) as mock_auth:
|
|
276
276
|
mock_auth.return_value = "test_access_token"
|
|
277
277
|
mock_api.return_value = mock_account_response
|
|
278
278
|
|
|
@@ -40,7 +40,7 @@ class TestBudgetUpdateFunctionality:
|
|
|
40
40
|
def mock_auth_manager(self):
|
|
41
41
|
"""Mock for the authentication manager"""
|
|
42
42
|
with patch('meta_ads_mcp.core.api.auth_manager') as mock, \
|
|
43
|
-
patch('meta_ads_mcp.core.
|
|
43
|
+
patch('meta_ads_mcp.core.auth.get_current_access_token') as mock_get_token:
|
|
44
44
|
# Mock a valid access token
|
|
45
45
|
mock.get_current_access_token.return_value = "test_access_token"
|
|
46
46
|
mock.is_token_valid.return_value = True
|
|
@@ -433,7 +433,7 @@ class TestBudgetUpdateIntegration:
|
|
|
433
433
|
# Test that the function accepts the new parameters
|
|
434
434
|
with patch('meta_ads_mcp.core.adsets.make_api_request') as mock_api, \
|
|
435
435
|
patch('meta_ads_mcp.core.api.auth_manager') as mock_auth, \
|
|
436
|
-
patch('meta_ads_mcp.core.
|
|
436
|
+
patch('meta_ads_mcp.core.auth.get_current_access_token') as mock_get_token:
|
|
437
437
|
|
|
438
438
|
mock_api.return_value = {"id": "test_id", "daily_budget": "5000"}
|
|
439
439
|
mock_auth.get_current_access_token.return_value = "test_access_token"
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""Test that create_ad_creative handles simple creatives correctly."""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
import json
|
|
5
|
+
from unittest.mock import AsyncMock, patch
|
|
6
|
+
from meta_ads_mcp.core.ads import create_ad_creative
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@pytest.mark.asyncio
|
|
10
|
+
async def test_simple_creative_uses_object_story_spec():
|
|
11
|
+
"""Test that singular headline/description uses object_story_spec, not asset_feed_spec."""
|
|
12
|
+
|
|
13
|
+
# Mock the make_api_request function
|
|
14
|
+
with patch('meta_ads_mcp.core.ads.make_api_request') as mock_api, \
|
|
15
|
+
patch('meta_ads_mcp.core.ads._discover_pages_for_account') as mock_discover:
|
|
16
|
+
|
|
17
|
+
# Mock page discovery
|
|
18
|
+
mock_discover.return_value = {
|
|
19
|
+
"success": True,
|
|
20
|
+
"page_id": "123456789",
|
|
21
|
+
"page_name": "Test Page"
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
# Mock creative creation response
|
|
25
|
+
mock_api.side_effect = [
|
|
26
|
+
# First call: Create creative
|
|
27
|
+
{"id": "creative_123"},
|
|
28
|
+
# Second call: Get creative details
|
|
29
|
+
{
|
|
30
|
+
"id": "creative_123",
|
|
31
|
+
"name": "Test Creative",
|
|
32
|
+
"status": "ACTIVE"
|
|
33
|
+
}
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
# Call create_ad_creative with singular headline and description
|
|
37
|
+
result = await create_ad_creative(
|
|
38
|
+
account_id="act_701351919139047",
|
|
39
|
+
image_hash="test_hash_123",
|
|
40
|
+
name="Math Problem - Hormozi",
|
|
41
|
+
link_url="https://adrocketx.ai/",
|
|
42
|
+
message="If you're spending 4+ hours per campaign...",
|
|
43
|
+
headline="Stop paying yourself $12.50/hour",
|
|
44
|
+
description="AI builds campaigns in 3min. 156% higher conversions. Free beta.",
|
|
45
|
+
call_to_action_type="LEARN_MORE",
|
|
46
|
+
access_token="test_token"
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
# Check that make_api_request was called
|
|
50
|
+
assert mock_api.call_count == 2
|
|
51
|
+
|
|
52
|
+
# Get the creative_data that was sent to the API
|
|
53
|
+
create_call_args = mock_api.call_args_list[0]
|
|
54
|
+
endpoint = create_call_args[0][0]
|
|
55
|
+
creative_data = create_call_args[0][2]
|
|
56
|
+
|
|
57
|
+
print("Creative data sent to API:")
|
|
58
|
+
print(json.dumps(creative_data, indent=2))
|
|
59
|
+
|
|
60
|
+
# Verify it uses object_story_spec, NOT asset_feed_spec
|
|
61
|
+
assert "object_story_spec" in creative_data, "Should use object_story_spec for simple creatives"
|
|
62
|
+
assert "asset_feed_spec" not in creative_data, "Should NOT use asset_feed_spec for simple creatives"
|
|
63
|
+
|
|
64
|
+
# Verify object_story_spec structure
|
|
65
|
+
assert "link_data" in creative_data["object_story_spec"]
|
|
66
|
+
link_data = creative_data["object_story_spec"]["link_data"]
|
|
67
|
+
|
|
68
|
+
# Verify simple creative fields are in link_data
|
|
69
|
+
assert link_data["image_hash"] == "test_hash_123"
|
|
70
|
+
assert link_data["link"] == "https://adrocketx.ai/"
|
|
71
|
+
assert link_data["message"] == "If you're spending 4+ hours per campaign..."
|
|
72
|
+
|
|
73
|
+
# The issue: headline and description should be in link_data for simple creatives
|
|
74
|
+
# Not in asset_feed_spec
|
|
75
|
+
print("\nlink_data structure:")
|
|
76
|
+
print(json.dumps(link_data, indent=2))
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@pytest.mark.asyncio
|
|
80
|
+
async def test_dynamic_creative_uses_asset_feed_spec():
|
|
81
|
+
"""Test that plural headlines/descriptions uses asset_feed_spec."""
|
|
82
|
+
|
|
83
|
+
with patch('meta_ads_mcp.core.ads.make_api_request') as mock_api, \
|
|
84
|
+
patch('meta_ads_mcp.core.ads._discover_pages_for_account') as mock_discover:
|
|
85
|
+
|
|
86
|
+
# Mock page discovery
|
|
87
|
+
mock_discover.return_value = {
|
|
88
|
+
"success": True,
|
|
89
|
+
"page_id": "123456789",
|
|
90
|
+
"page_name": "Test Page"
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
# Mock creative creation response
|
|
94
|
+
mock_api.side_effect = [
|
|
95
|
+
{"id": "creative_456"},
|
|
96
|
+
{"id": "creative_456", "name": "Dynamic Creative", "status": "ACTIVE"}
|
|
97
|
+
]
|
|
98
|
+
|
|
99
|
+
# Call with PLURAL headlines and descriptions (dynamic creative)
|
|
100
|
+
result = await create_ad_creative(
|
|
101
|
+
account_id="act_701351919139047",
|
|
102
|
+
image_hash="test_hash_456",
|
|
103
|
+
name="Dynamic Creative Test",
|
|
104
|
+
link_url="https://example.com/",
|
|
105
|
+
message="Test message",
|
|
106
|
+
headlines=["Headline 1", "Headline 2"],
|
|
107
|
+
descriptions=["Description 1", "Description 2"],
|
|
108
|
+
access_token="test_token"
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
# Get the creative_data that was sent to the API
|
|
112
|
+
create_call_args = mock_api.call_args_list[0]
|
|
113
|
+
creative_data = create_call_args[0][2]
|
|
114
|
+
|
|
115
|
+
print("\nDynamic creative data sent to API:")
|
|
116
|
+
print(json.dumps(creative_data, indent=2))
|
|
117
|
+
|
|
118
|
+
# Verify it uses asset_feed_spec for dynamic creatives
|
|
119
|
+
assert "asset_feed_spec" in creative_data, "Should use asset_feed_spec for dynamic creatives"
|
|
120
|
+
|
|
121
|
+
# Verify asset_feed_spec structure
|
|
122
|
+
asset_feed_spec = creative_data["asset_feed_spec"]
|
|
123
|
+
assert "headlines" in asset_feed_spec
|
|
124
|
+
assert len(asset_feed_spec["headlines"]) == 2
|
|
125
|
+
assert "descriptions" in asset_feed_spec
|
|
126
|
+
assert len(asset_feed_spec["descriptions"]) == 2
|
|
127
|
+
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""End-to-end test for creating simple creatives with singular headline/description."""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
from meta_ads_mcp.core.ads import create_ad_creative
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@pytest.mark.skip(reason="Requires authentication - run manually with: pytest tests/test_create_simple_creative_e2e.py -v")
|
|
10
|
+
@pytest.mark.asyncio
|
|
11
|
+
async def test_create_simple_creative_with_real_api():
|
|
12
|
+
"""Test creating a simple creative with singular headline/description using real Meta API."""
|
|
13
|
+
|
|
14
|
+
# Account and image details from user
|
|
15
|
+
account_id = "act_3182643988557192"
|
|
16
|
+
image_hash = "ca228ac8ff3a66dca9435c90dd6953d6"
|
|
17
|
+
|
|
18
|
+
# Create a simple creative with singular headline and description
|
|
19
|
+
result = await create_ad_creative(
|
|
20
|
+
account_id=account_id,
|
|
21
|
+
image_hash=image_hash,
|
|
22
|
+
name="E2E Test - Simple Creative",
|
|
23
|
+
link_url="https://example.com/",
|
|
24
|
+
message="This is a test message for the ad.",
|
|
25
|
+
headline="Test Headline",
|
|
26
|
+
description="Test description for ad.",
|
|
27
|
+
call_to_action_type="LEARN_MORE"
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
print("\n=== API Response ===")
|
|
31
|
+
print(result)
|
|
32
|
+
|
|
33
|
+
result_data = json.loads(result)
|
|
34
|
+
|
|
35
|
+
# Check if there's an error
|
|
36
|
+
if "error" in result_data:
|
|
37
|
+
pytest.fail(f"Creative creation failed: {result_data['error']}")
|
|
38
|
+
|
|
39
|
+
# Verify success
|
|
40
|
+
assert "success" in result_data or "creative_id" in result_data or "id" in result_data, \
|
|
41
|
+
f"Expected success response, got: {result_data}"
|
|
42
|
+
|
|
43
|
+
print("\n✅ Simple creative created successfully!")
|
|
44
|
+
|
|
45
|
+
if "creative_id" in result_data:
|
|
46
|
+
print(f"Creative ID: {result_data['creative_id']}")
|
|
47
|
+
elif "details" in result_data and "id" in result_data["details"]:
|
|
48
|
+
print(f"Creative ID: {result_data['details']['id']}")
|
|
49
|
+
|