meta-ads-mcp 1.0.13__tar.gz → 1.0.16__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 (84) hide show
  1. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.16}/PKG-INFO +40 -2
  2. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.16}/README.md +39 -1
  3. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.16}/STREAMABLE_HTTP_SETUP.md +10 -0
  4. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.16}/meta_ads_mcp/__init__.py +1 -1
  5. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.16}/meta_ads_mcp/core/ads.py +69 -75
  6. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.16}/meta_ads_mcp/core/api.py +3 -2
  7. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.16}/meta_ads_mcp/core/authentication.py +2 -1
  8. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.16}/meta_ads_mcp/core/campaigns.py +35 -1
  9. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.16}/meta_ads_mcp/core/duplication.py +2 -2
  10. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.16}/pyproject.toml +1 -1
  11. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.16}/server.json +2 -2
  12. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.16}/tests/test_account_info_access_fix.py +7 -7
  13. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.16}/tests/test_budget_update.py +2 -2
  14. meta_ads_mcp-1.0.16/tests/test_campaign_objective_filter.py +518 -0
  15. meta_ads_mcp-1.0.16/tests/test_create_ad_creative_simple.py +127 -0
  16. meta_ads_mcp-1.0.16/tests/test_create_simple_creative_e2e.py +49 -0
  17. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.16}/tests/test_dsa_beneficiary.py +23 -23
  18. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.16}/tests/test_dsa_integration.py +14 -14
  19. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.16}/tests/test_duplication.py +1 -1
  20. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.16}/tests/test_duplication_regression.py +1 -1
  21. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.16}/tests/test_dynamic_creatives.py +12 -14
  22. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.16}/tests/test_estimate_audience_size.py +1 -1
  23. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.16}/tests/test_get_account_pages.py +8 -8
  24. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.16}/tests/test_insights_actions_and_values_e2e.py +1 -1
  25. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.16}/tests/test_insights_pagination.py +1 -1
  26. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.16}/tests/test_mobile_app_adset_creation.py +1 -1
  27. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.16}/tests/test_targeting.py +2 -2
  28. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.16}/.github/workflows/publish-mcp.yml +0 -0
  29. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.16}/.github/workflows/publish.yml +0 -0
  30. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.16}/.github/workflows/test.yml +0 -0
  31. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.16}/.gitignore +0 -0
  32. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.16}/CUSTOM_META_APP.md +0 -0
  33. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.16}/Dockerfile +0 -0
  34. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.16}/LICENSE +0 -0
  35. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.16}/LOCAL_INSTALLATION.md +0 -0
  36. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.16}/META_API_NOTES.md +0 -0
  37. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.16}/RELEASE.md +0 -0
  38. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.16}/examples/README.md +0 -0
  39. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.16}/examples/example_http_client.py +0 -0
  40. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.16}/future_improvements.md +0 -0
  41. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.16}/images/meta-ads-example.png +0 -0
  42. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.16}/meta_ads_auth.sh +0 -0
  43. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.16}/meta_ads_mcp/__main__.py +0 -0
  44. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.16}/meta_ads_mcp/core/__init__.py +0 -0
  45. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.16}/meta_ads_mcp/core/accounts.py +0 -0
  46. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.16}/meta_ads_mcp/core/ads_library.py +0 -0
  47. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.16}/meta_ads_mcp/core/adsets.py +0 -0
  48. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.16}/meta_ads_mcp/core/auth.py +0 -0
  49. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.16}/meta_ads_mcp/core/budget_schedules.py +0 -0
  50. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.16}/meta_ads_mcp/core/callback_server.py +0 -0
  51. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.16}/meta_ads_mcp/core/http_auth_integration.py +0 -0
  52. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.16}/meta_ads_mcp/core/insights.py +0 -0
  53. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.16}/meta_ads_mcp/core/openai_deep_research.py +0 -0
  54. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.16}/meta_ads_mcp/core/pipeboard_auth.py +0 -0
  55. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.16}/meta_ads_mcp/core/reports.py +0 -0
  56. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.16}/meta_ads_mcp/core/resources.py +0 -0
  57. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.16}/meta_ads_mcp/core/server.py +0 -0
  58. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.16}/meta_ads_mcp/core/targeting.py +0 -0
  59. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.16}/meta_ads_mcp/core/utils.py +0 -0
  60. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.16}/requirements.txt +0 -0
  61. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.16}/setup.py +0 -0
  62. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.16}/smithery.yaml +0 -0
  63. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.16}/tests/README.md +0 -0
  64. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.16}/tests/README_REGRESSION_TESTS.md +0 -0
  65. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.16}/tests/__init__.py +0 -0
  66. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.16}/tests/conftest.py +0 -0
  67. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.16}/tests/e2e_account_info_search_issue.py +0 -0
  68. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.16}/tests/test_account_search.py +0 -0
  69. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.16}/tests/test_budget_update_e2e.py +0 -0
  70. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.16}/tests/test_estimate_audience_size_e2e.py +0 -0
  71. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.16}/tests/test_get_ad_creatives_fix.py +0 -0
  72. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.16}/tests/test_get_ad_image_quality_improvements.py +0 -0
  73. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.16}/tests/test_get_ad_image_regression.py +0 -0
  74. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.16}/tests/test_http_transport.py +0 -0
  75. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.16}/tests/test_integration_openai_mcp.py +0 -0
  76. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.16}/tests/test_is_dynamic_creative_adset.py +0 -0
  77. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.16}/tests/test_mobile_app_adset_issue.py +0 -0
  78. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.16}/tests/test_openai.py +0 -0
  79. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.16}/tests/test_openai_mcp_deep_research.py +0 -0
  80. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.16}/tests/test_page_discovery.py +0 -0
  81. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.16}/tests/test_page_discovery_integration.py +0 -0
  82. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.16}/tests/test_targeting_search_e2e.py +0 -0
  83. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.16}/tests/test_update_ad_creative_id.py +0 -0
  84. {meta_ads_mcp-1.0.13 → meta_ads_mcp-1.0.16}/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.16
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
@@ -66,6 +68,16 @@ The fastest and most reliable way to get started is to **[🚀 Get started with
66
68
 
67
69
  That's it! You can now ask Claude to analyze your Meta ad campaigns, get performance insights, and manage your advertising.
68
70
 
71
+ #### Advanced: Direct Token Authentication (Claude)
72
+
73
+ For direct token-based authentication without the interactive flow, use this URL format when adding the integration:
74
+
75
+ ```
76
+ https://mcp.pipeboard.co/meta-ads-mcp?token=YOUR_PIPEBOARD_TOKEN
77
+ ```
78
+
79
+ Get your token at [pipeboard.co/api-tokens](https://pipeboard.co/api-tokens).
80
+
69
81
  ### For Cursor Users
70
82
 
71
83
  Add the following to your `~/.cursor/mcp.json`. Once you enable the remote MCP, click on "Needs login" to finish the login process.
@@ -81,12 +93,38 @@ Add the following to your `~/.cursor/mcp.json`. Once you enable the remote MCP,
81
93
  }
82
94
  ```
83
95
 
96
+ #### Advanced: Direct Token Authentication (Cursor)
97
+
98
+ If you prefer to authenticate without the interactive login flow, you can include your Pipeboard API token directly in the URL:
99
+
100
+ ```json
101
+ {
102
+ "mcpServers": {
103
+ "meta-ads-remote": {
104
+ "url": "https://mcp.pipeboard.co/meta-ads-mcp?token=YOUR_PIPEBOARD_TOKEN"
105
+ }
106
+ }
107
+ }
108
+ ```
109
+
110
+ Get your token at [pipeboard.co/api-tokens](https://pipeboard.co/api-tokens).
111
+
84
112
  ### For Other MCP Clients
85
113
 
86
114
  Use the Remote MCP URL: `https://mcp.pipeboard.co/meta-ads-mcp`
87
115
 
88
116
  **[📖 Get detailed setup instructions for your AI client here](https://pipeboard.co)**
89
117
 
118
+ #### Advanced: Direct Token Authentication (Other Clients)
119
+
120
+ For MCP clients that support token-based authentication, you can append your Pipeboard API token to the URL:
121
+
122
+ ```
123
+ https://mcp.pipeboard.co/meta-ads-mcp?token=YOUR_PIPEBOARD_TOKEN
124
+ ```
125
+
126
+ This bypasses the interactive login flow and authenticates immediately. Get your token at [pipeboard.co/api-tokens](https://pipeboard.co/api-tokens).
127
+
90
128
  ## Local Installation (Technical Users Only)
91
129
 
92
130
  If you're a developer or need to customize the installation, you can run Meta Ads MCP locally. **Most marketers should use the Remote MCP above instead!** For complete technical setup instructions, see our **[Local Installation Guide](LOCAL_INSTALLATION.md)**.
@@ -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
@@ -41,6 +43,16 @@ The fastest and most reliable way to get started is to **[🚀 Get started with
41
43
 
42
44
  That's it! You can now ask Claude to analyze your Meta ad campaigns, get performance insights, and manage your advertising.
43
45
 
46
+ #### Advanced: Direct Token Authentication (Claude)
47
+
48
+ For direct token-based authentication without the interactive flow, use this URL format when adding the integration:
49
+
50
+ ```
51
+ https://mcp.pipeboard.co/meta-ads-mcp?token=YOUR_PIPEBOARD_TOKEN
52
+ ```
53
+
54
+ Get your token at [pipeboard.co/api-tokens](https://pipeboard.co/api-tokens).
55
+
44
56
  ### For Cursor Users
45
57
 
46
58
  Add the following to your `~/.cursor/mcp.json`. Once you enable the remote MCP, click on "Needs login" to finish the login process.
@@ -56,12 +68,38 @@ Add the following to your `~/.cursor/mcp.json`. Once you enable the remote MCP,
56
68
  }
57
69
  ```
58
70
 
71
+ #### Advanced: Direct Token Authentication (Cursor)
72
+
73
+ If you prefer to authenticate without the interactive login flow, you can include your Pipeboard API token directly in the URL:
74
+
75
+ ```json
76
+ {
77
+ "mcpServers": {
78
+ "meta-ads-remote": {
79
+ "url": "https://mcp.pipeboard.co/meta-ads-mcp?token=YOUR_PIPEBOARD_TOKEN"
80
+ }
81
+ }
82
+ }
83
+ ```
84
+
85
+ Get your token at [pipeboard.co/api-tokens](https://pipeboard.co/api-tokens).
86
+
59
87
  ### For Other MCP Clients
60
88
 
61
89
  Use the Remote MCP URL: `https://mcp.pipeboard.co/meta-ads-mcp`
62
90
 
63
91
  **[📖 Get detailed setup instructions for your AI client here](https://pipeboard.co)**
64
92
 
93
+ #### Advanced: Direct Token Authentication (Other Clients)
94
+
95
+ For MCP clients that support token-based authentication, you can append your Pipeboard API token to the URL:
96
+
97
+ ```
98
+ https://mcp.pipeboard.co/meta-ads-mcp?token=YOUR_PIPEBOARD_TOKEN
99
+ ```
100
+
101
+ This bypasses the interactive login flow and authenticates immediately. Get your token at [pipeboard.co/api-tokens](https://pipeboard.co/api-tokens).
102
+
65
103
  ## Local Installation (Technical Users Only)
66
104
 
67
105
  If you're a developer or need to customize the installation, you can run Meta Ads MCP locally. **Most marketers should use the Remote MCP above instead!** For complete technical setup instructions, see our **[Local Installation Guide](LOCAL_INSTALLATION.md)**.
@@ -83,6 +83,16 @@ curl -H "Authorization: Bearer your_pipeboard_token" \
83
83
  -d '{"jsonrpc":"2.0","method":"tools/list","id":1}'
84
84
  ```
85
85
 
86
+ #### Remote MCP: Token in URL
87
+
88
+ When using the hosted Remote MCP at `https://mcp.pipeboard.co/meta-ads-mcp`, you can alternatively authenticate by including the token as a URL parameter:
89
+
90
+ ```
91
+ https://mcp.pipeboard.co/meta-ads-mcp?token=YOUR_PIPEBOARD_TOKEN
92
+ ```
93
+
94
+ This is particularly useful for MCP clients that don't support interactive authentication flows.
95
+
86
96
  ### Alternative Method: Direct Meta Token
87
97
 
88
98
  If you have a Meta Developer App, you can use a direct access token via the `X-META-ACCESS-TOKEN` header. This is less common.
@@ -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.16"
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
@@ -9,7 +9,14 @@ from .server import mcp_server
9
9
 
10
10
  @mcp_server.tool()
11
11
  @meta_api_tool
12
- async def get_campaigns(account_id: str, access_token: Optional[str] = None, limit: int = 10, status_filter: str = "", after: str = "") -> str:
12
+ async def get_campaigns(
13
+ account_id: str,
14
+ access_token: Optional[str] = None,
15
+ limit: int = 10,
16
+ status_filter: str = "",
17
+ objective_filter: Union[str, List[str]] = "",
18
+ after: str = ""
19
+ ) -> str:
13
20
  """
14
21
  Get campaigns for a Meta Ads account with optional filtering.
15
22
 
@@ -26,6 +33,11 @@ async def get_campaigns(account_id: str, access_token: Optional[str] = None, lim
26
33
  status_filter: Filter by effective status (e.g., 'ACTIVE', 'PAUSED', 'ARCHIVED').
27
34
  Maps to the 'effective_status' API parameter, which expects an array
28
35
  (this function handles the required JSON formatting). Leave empty for all statuses.
36
+ objective_filter: Filter by campaign objective(s). Can be a single objective string or a list of objectives.
37
+ Valid objectives: 'OUTCOME_AWARENESS', 'OUTCOME_TRAFFIC', 'OUTCOME_ENGAGEMENT',
38
+ 'OUTCOME_LEADS', 'OUTCOME_SALES', 'OUTCOME_APP_PROMOTION'.
39
+ Examples: 'OUTCOME_LEADS' or ['OUTCOME_LEADS', 'OUTCOME_SALES'].
40
+ Leave empty for all objectives.
29
41
  after: Pagination cursor to get the next set of results
30
42
  """
31
43
  # Require explicit account_id
@@ -38,10 +50,32 @@ async def get_campaigns(account_id: str, access_token: Optional[str] = None, lim
38
50
  "limit": limit
39
51
  }
40
52
 
53
+ # Build filtering array for complex filtering
54
+ filters = []
55
+
41
56
  if status_filter:
42
57
  # API expects an array, encode it as a JSON string
43
58
  params["effective_status"] = json.dumps([status_filter])
44
59
 
60
+ # Handle objective filtering - supports both single string and list of objectives
61
+ if objective_filter:
62
+ # Convert single string to list for consistent handling
63
+ objectives = [objective_filter] if isinstance(objective_filter, str) else objective_filter
64
+
65
+ # Filter out empty strings
66
+ objectives = [obj for obj in objectives if obj]
67
+
68
+ if objectives:
69
+ filters.append({
70
+ "field": "objective",
71
+ "operator": "IN",
72
+ "value": objectives
73
+ })
74
+
75
+ # Add filtering parameter if we have filters
76
+ if filters:
77
+ params["filtering"] = json.dumps(filters)
78
+
45
79
  if after:
46
80
  params["after"] = after
47
81
 
@@ -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.16"
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.16",
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.16",
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"