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.

Files changed (83) hide show
  1. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/PKG-INFO +4 -2
  2. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/README.md +3 -1
  3. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/meta_ads_mcp/__init__.py +1 -1
  4. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/meta_ads_mcp/core/ads.py +69 -75
  5. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/meta_ads_mcp/core/api.py +3 -2
  6. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/meta_ads_mcp/core/authentication.py +2 -1
  7. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/meta_ads_mcp/core/duplication.py +2 -2
  8. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/pyproject.toml +1 -1
  9. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/server.json +2 -2
  10. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/tests/test_account_info_access_fix.py +7 -7
  11. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/tests/test_budget_update.py +2 -2
  12. meta_ads_mcp-1.0.15/tests/test_create_ad_creative_simple.py +127 -0
  13. meta_ads_mcp-1.0.15/tests/test_create_simple_creative_e2e.py +49 -0
  14. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/tests/test_dsa_beneficiary.py +23 -23
  15. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/tests/test_dsa_integration.py +14 -14
  16. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/tests/test_duplication.py +1 -1
  17. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/tests/test_duplication_regression.py +1 -1
  18. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/tests/test_dynamic_creatives.py +12 -14
  19. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/tests/test_estimate_audience_size.py +1 -1
  20. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/tests/test_get_account_pages.py +8 -8
  21. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/tests/test_insights_actions_and_values_e2e.py +1 -1
  22. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/tests/test_insights_pagination.py +1 -1
  23. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/tests/test_mobile_app_adset_creation.py +1 -1
  24. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/tests/test_targeting.py +2 -2
  25. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/.github/workflows/publish-mcp.yml +0 -0
  26. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/.github/workflows/publish.yml +0 -0
  27. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/.github/workflows/test.yml +0 -0
  28. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/.gitignore +0 -0
  29. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/CUSTOM_META_APP.md +0 -0
  30. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/Dockerfile +0 -0
  31. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/LICENSE +0 -0
  32. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/LOCAL_INSTALLATION.md +0 -0
  33. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/META_API_NOTES.md +0 -0
  34. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/RELEASE.md +0 -0
  35. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/STREAMABLE_HTTP_SETUP.md +0 -0
  36. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/examples/README.md +0 -0
  37. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/examples/example_http_client.py +0 -0
  38. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/future_improvements.md +0 -0
  39. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/images/meta-ads-example.png +0 -0
  40. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/meta_ads_auth.sh +0 -0
  41. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/meta_ads_mcp/__main__.py +0 -0
  42. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/meta_ads_mcp/core/__init__.py +0 -0
  43. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/meta_ads_mcp/core/accounts.py +0 -0
  44. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/meta_ads_mcp/core/ads_library.py +0 -0
  45. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/meta_ads_mcp/core/adsets.py +0 -0
  46. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/meta_ads_mcp/core/auth.py +0 -0
  47. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/meta_ads_mcp/core/budget_schedules.py +0 -0
  48. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/meta_ads_mcp/core/callback_server.py +0 -0
  49. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/meta_ads_mcp/core/campaigns.py +0 -0
  50. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/meta_ads_mcp/core/http_auth_integration.py +0 -0
  51. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/meta_ads_mcp/core/insights.py +0 -0
  52. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/meta_ads_mcp/core/openai_deep_research.py +0 -0
  53. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/meta_ads_mcp/core/pipeboard_auth.py +0 -0
  54. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/meta_ads_mcp/core/reports.py +0 -0
  55. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/meta_ads_mcp/core/resources.py +0 -0
  56. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/meta_ads_mcp/core/server.py +0 -0
  57. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/meta_ads_mcp/core/targeting.py +0 -0
  58. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/meta_ads_mcp/core/utils.py +0 -0
  59. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/requirements.txt +0 -0
  60. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/setup.py +0 -0
  61. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/smithery.yaml +0 -0
  62. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/tests/README.md +0 -0
  63. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/tests/README_REGRESSION_TESTS.md +0 -0
  64. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/tests/__init__.py +0 -0
  65. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/tests/conftest.py +0 -0
  66. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/tests/e2e_account_info_search_issue.py +0 -0
  67. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/tests/test_account_search.py +0 -0
  68. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/tests/test_budget_update_e2e.py +0 -0
  69. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/tests/test_estimate_audience_size_e2e.py +0 -0
  70. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/tests/test_get_ad_creatives_fix.py +0 -0
  71. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/tests/test_get_ad_image_quality_improvements.py +0 -0
  72. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/tests/test_get_ad_image_regression.py +0 -0
  73. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/tests/test_http_transport.py +0 -0
  74. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/tests/test_integration_openai_mcp.py +0 -0
  75. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/tests/test_is_dynamic_creative_adset.py +0 -0
  76. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/tests/test_mobile_app_adset_issue.py +0 -0
  77. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/tests/test_openai.py +0 -0
  78. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/tests/test_openai_mcp_deep_research.py +0 -0
  79. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/tests/test_page_discovery.py +0 -0
  80. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/tests/test_page_discovery_integration.py +0 -0
  81. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/tests/test_targeting_search_e2e.py +0 -0
  82. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.15}/tests/test_update_ad_creative_id.py +0 -0
  83. {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.13
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 API. This tool enables AI models to access, analyze, and manage Meta advertising campaigns through a standardized interface, allowing LLMs to retrieve performance data, visualize ad creatives, and provide strategic insights for Facebook, Instagram, and other Meta platforms.
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
  [![Meta Ads MCP Server Demo](https://github.com/user-attachments/assets/3e605cee-d289-414b-814c-6299e7f3383e)](https://github.com/user-attachments/assets/3e605cee-d289-414b-814c-6299e7f3383e)
33
33
 
34
+ [![MCP Badge](https://lobehub.com/badge/mcp/nictuku-meta-ads-mcp)](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 API. This tool enables AI models to access, analyze, and manage Meta advertising campaigns through a standardized interface, allowing LLMs to retrieve performance data, visualize ad creatives, and provide strategic insights for Facebook, Instagram, and other Meta platforms.
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
  [![Meta Ads MCP Server Demo](https://github.com/user-attachments/assets/3e605cee-d289-414b-814c-6299e7f3383e)](https://github.com/user-attachments/assets/3e605cee-d289-414b-814c-6299e7f3383e)
8
8
 
9
+ [![MCP Badge](https://lobehub.com/badge/mcp/nictuku-meta-ads-mcp)](https://lobehub.com/mcp/nictuku-meta-ads-mcp)
10
+
9
11
  mcp-name: co.pipeboard/meta-ads-mcp
10
12
 
11
13
  ## Community & Support
@@ -6,7 +6,7 @@ This package provides a Meta Ads MCP integration
6
6
 
7
7
  from meta_ads_mcp.core.server import main
8
8
 
9
- __version__ = "1.0.13"
9
+ __version__ = "1.0.15"
10
10
 
11
11
  __all__ = [
12
12
  'get_ad_accounts',
@@ -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
- # Convert simple parameters to complex format for internal processing
835
- final_headlines = None
836
- final_descriptions = None
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(final_headlines):
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 final_descriptions:
857
- if len(final_descriptions) > 5:
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(final_descriptions):
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
- if final_headlines or final_descriptions:
870
- # Use asset_feed_spec for dynamic creatives
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 final_headlines:
879
- asset_feed_spec["headlines"] = [{"text": headline_text} for headline_text in final_headlines]
865
+ if headlines:
866
+ asset_feed_spec["headlines"] = [{"text": headline_text} for headline_text in headlines]
880
867
 
881
868
  # Handle descriptions
882
- if final_descriptions:
883
- asset_feed_spec["descriptions"] = [{"text": description_text} for description_text in final_descriptions]
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 single creative
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
- # Convert simple parameters to complex format for internal processing
1002
- final_headlines = None
1003
- final_descriptions = None
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(final_headlines):
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 final_descriptions:
1024
- if len(final_descriptions) > 5:
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(final_descriptions):
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
- if message:
1037
- update_data["object_story_spec"] = {"link_data": {"message": message}}
1038
-
1039
- # Handle dynamic creative assets via asset_feed_spec
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 final_headlines:
1048
- asset_feed_spec["headlines"] = [{"text": headline_text} for headline_text in final_headlines]
1027
+ if headlines:
1028
+ asset_feed_spec["headlines"] = [{"text": headline_text} for headline_text in headlines]
1049
1029
 
1050
1030
  # Handle descriptions
1051
- if final_descriptions:
1052
- asset_feed_spec["descriptions"] = [{"text": description_text} for description_text in final_descriptions]
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 .auth import needs_authentication, get_current_access_token, auth_manager, start_callback_server, shutdown_callback_server
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 .auth import start_callback_server, shutdown_callback_server, auth_manager, get_current_access_token
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 .auth import get_current_access_token
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:
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "meta-ads-mcp"
7
- version = "1.0.13"
7
+ version = "1.0.15"
8
8
  description = "Model Context Protocol (MCP) server for interacting with Meta Ads API"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -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.13",
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.13",
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.api.get_current_access_token', new_callable=AsyncMock) as mock_auth:
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.api.get_current_access_token', new_callable=AsyncMock) as mock_auth:
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.api.get_current_access_token', new_callable=AsyncMock) as mock_auth:
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.api.get_current_access_token', new_callable=AsyncMock) as mock_auth:
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.api.get_current_access_token', new_callable=AsyncMock) as mock_auth:
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.api.get_current_access_token', new_callable=AsyncMock) as mock_auth:
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.api.get_current_access_token', new_callable=AsyncMock) as mock_auth:
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.api.get_current_access_token') as mock_get_token:
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.api.get_current_access_token') as mock_get_token:
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
+