meta-ads-mcp 0.10.7__tar.gz → 0.11.0__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.
Files changed (77) hide show
  1. {meta_ads_mcp-0.10.7 → meta_ads_mcp-0.11.0}/PKG-INFO +31 -2
  2. {meta_ads_mcp-0.10.7 → meta_ads_mcp-0.11.0}/README.md +30 -1
  3. {meta_ads_mcp-0.10.7 → meta_ads_mcp-0.11.0}/meta_ads_mcp/__init__.py +2 -3
  4. {meta_ads_mcp-0.10.7 → meta_ads_mcp-0.11.0}/meta_ads_mcp/core/accounts.py +4 -4
  5. {meta_ads_mcp-0.10.7 → meta_ads_mcp-0.11.0}/meta_ads_mcp/core/ads.py +109 -67
  6. {meta_ads_mcp-0.10.7 → meta_ads_mcp-0.11.0}/meta_ads_mcp/core/ads_library.py +5 -5
  7. {meta_ads_mcp-0.10.7 → meta_ads_mcp-0.11.0}/meta_ads_mcp/core/adsets.py +28 -34
  8. {meta_ads_mcp-0.10.7 → meta_ads_mcp-0.11.0}/meta_ads_mcp/core/api.py +1 -1
  9. {meta_ads_mcp-0.10.7 → meta_ads_mcp-0.11.0}/meta_ads_mcp/core/auth.py +1 -1
  10. {meta_ads_mcp-0.10.7 → meta_ads_mcp-0.11.0}/meta_ads_mcp/core/authentication.py +2 -1
  11. {meta_ads_mcp-0.10.7 → meta_ads_mcp-0.11.0}/meta_ads_mcp/core/budget_schedules.py +1 -1
  12. {meta_ads_mcp-0.10.7 → meta_ads_mcp-0.11.0}/meta_ads_mcp/core/campaigns.py +40 -40
  13. {meta_ads_mcp-0.10.7 → meta_ads_mcp-0.11.0}/meta_ads_mcp/core/duplication.py +4 -4
  14. {meta_ads_mcp-0.10.7 → meta_ads_mcp-0.11.0}/meta_ads_mcp/core/insights.py +2 -2
  15. {meta_ads_mcp-0.10.7 → meta_ads_mcp-0.11.0}/meta_ads_mcp/core/openai_deep_research.py +4 -4
  16. {meta_ads_mcp-0.10.7 → meta_ads_mcp-0.11.0}/meta_ads_mcp/core/pipeboard_auth.py +1 -1
  17. {meta_ads_mcp-0.10.7 → meta_ads_mcp-0.11.0}/meta_ads_mcp/core/reports.py +3 -3
  18. {meta_ads_mcp-0.10.7 → meta_ads_mcp-0.11.0}/meta_ads_mcp/core/targeting.py +14 -14
  19. {meta_ads_mcp-0.10.7 → meta_ads_mcp-0.11.0}/pyproject.toml +1 -1
  20. {meta_ads_mcp-0.10.7 → meta_ads_mcp-0.11.0}/tests/test_dsa_beneficiary.py +1 -1
  21. {meta_ads_mcp-0.10.7 → meta_ads_mcp-0.11.0}/tests/test_dsa_integration.py +1 -1
  22. {meta_ads_mcp-0.10.7 → meta_ads_mcp-0.11.0}/tests/test_dynamic_creatives.py +2 -1
  23. {meta_ads_mcp-0.10.7 → meta_ads_mcp-0.11.0}/tests/test_insights_actions_and_values.py +7 -10
  24. {meta_ads_mcp-0.10.7 → meta_ads_mcp-0.11.0}/tests/test_targeting.py +15 -6
  25. meta_ads_mcp-0.11.0/tests/test_upload_ad_image.py +134 -0
  26. {meta_ads_mcp-0.10.7 → meta_ads_mcp-0.11.0}/.github/workflows/publish.yml +0 -0
  27. {meta_ads_mcp-0.10.7 → meta_ads_mcp-0.11.0}/.github/workflows/test.yml +0 -0
  28. {meta_ads_mcp-0.10.7 → meta_ads_mcp-0.11.0}/.gitignore +0 -0
  29. {meta_ads_mcp-0.10.7 → meta_ads_mcp-0.11.0}/CUSTOM_META_APP.md +0 -0
  30. {meta_ads_mcp-0.10.7 → meta_ads_mcp-0.11.0}/Dockerfile +0 -0
  31. {meta_ads_mcp-0.10.7 → meta_ads_mcp-0.11.0}/LICENSE +0 -0
  32. {meta_ads_mcp-0.10.7 → meta_ads_mcp-0.11.0}/LOCAL_INSTALLATION.md +0 -0
  33. {meta_ads_mcp-0.10.7 → meta_ads_mcp-0.11.0}/META_API_NOTES.md +0 -0
  34. {meta_ads_mcp-0.10.7 → meta_ads_mcp-0.11.0}/RELEASE.md +0 -0
  35. {meta_ads_mcp-0.10.7 → meta_ads_mcp-0.11.0}/STREAMABLE_HTTP_SETUP.md +0 -0
  36. {meta_ads_mcp-0.10.7 → meta_ads_mcp-0.11.0}/examples/README.md +0 -0
  37. {meta_ads_mcp-0.10.7 → meta_ads_mcp-0.11.0}/examples/example_http_client.py +0 -0
  38. {meta_ads_mcp-0.10.7 → meta_ads_mcp-0.11.0}/future_improvements.md +0 -0
  39. {meta_ads_mcp-0.10.7 → meta_ads_mcp-0.11.0}/images/meta-ads-example.png +0 -0
  40. {meta_ads_mcp-0.10.7 → meta_ads_mcp-0.11.0}/meta_ads_auth.sh +0 -0
  41. {meta_ads_mcp-0.10.7 → meta_ads_mcp-0.11.0}/meta_ads_mcp/__main__.py +0 -0
  42. {meta_ads_mcp-0.10.7 → meta_ads_mcp-0.11.0}/meta_ads_mcp/core/__init__.py +0 -0
  43. {meta_ads_mcp-0.10.7 → meta_ads_mcp-0.11.0}/meta_ads_mcp/core/callback_server.py +0 -0
  44. {meta_ads_mcp-0.10.7 → meta_ads_mcp-0.11.0}/meta_ads_mcp/core/http_auth_integration.py +0 -0
  45. {meta_ads_mcp-0.10.7 → meta_ads_mcp-0.11.0}/meta_ads_mcp/core/resources.py +0 -0
  46. {meta_ads_mcp-0.10.7 → meta_ads_mcp-0.11.0}/meta_ads_mcp/core/server.py +0 -0
  47. {meta_ads_mcp-0.10.7 → meta_ads_mcp-0.11.0}/meta_ads_mcp/core/utils.py +0 -0
  48. {meta_ads_mcp-0.10.7 → meta_ads_mcp-0.11.0}/requirements.txt +0 -0
  49. {meta_ads_mcp-0.10.7 → meta_ads_mcp-0.11.0}/setup.py +0 -0
  50. {meta_ads_mcp-0.10.7 → meta_ads_mcp-0.11.0}/smithery.yaml +0 -0
  51. {meta_ads_mcp-0.10.7 → meta_ads_mcp-0.11.0}/tests/README.md +0 -0
  52. {meta_ads_mcp-0.10.7 → meta_ads_mcp-0.11.0}/tests/README_REGRESSION_TESTS.md +0 -0
  53. {meta_ads_mcp-0.10.7 → meta_ads_mcp-0.11.0}/tests/__init__.py +0 -0
  54. {meta_ads_mcp-0.10.7 → meta_ads_mcp-0.11.0}/tests/conftest.py +0 -0
  55. {meta_ads_mcp-0.10.7 → meta_ads_mcp-0.11.0}/tests/e2e_account_info_search_issue.py +0 -0
  56. {meta_ads_mcp-0.10.7 → meta_ads_mcp-0.11.0}/tests/test_account_info_access_fix.py +0 -0
  57. {meta_ads_mcp-0.10.7 → meta_ads_mcp-0.11.0}/tests/test_account_search.py +0 -0
  58. {meta_ads_mcp-0.10.7 → meta_ads_mcp-0.11.0}/tests/test_budget_update.py +0 -0
  59. {meta_ads_mcp-0.10.7 → meta_ads_mcp-0.11.0}/tests/test_budget_update_e2e.py +0 -0
  60. {meta_ads_mcp-0.10.7 → meta_ads_mcp-0.11.0}/tests/test_duplication.py +0 -0
  61. {meta_ads_mcp-0.10.7 → meta_ads_mcp-0.11.0}/tests/test_duplication_regression.py +0 -0
  62. {meta_ads_mcp-0.10.7 → meta_ads_mcp-0.11.0}/tests/test_estimate_audience_size.py +0 -0
  63. {meta_ads_mcp-0.10.7 → meta_ads_mcp-0.11.0}/tests/test_estimate_audience_size_e2e.py +0 -0
  64. {meta_ads_mcp-0.10.7 → meta_ads_mcp-0.11.0}/tests/test_get_account_pages.py +0 -0
  65. {meta_ads_mcp-0.10.7 → meta_ads_mcp-0.11.0}/tests/test_get_ad_creatives_fix.py +0 -0
  66. {meta_ads_mcp-0.10.7 → meta_ads_mcp-0.11.0}/tests/test_get_ad_image_quality_improvements.py +0 -0
  67. {meta_ads_mcp-0.10.7 → meta_ads_mcp-0.11.0}/tests/test_get_ad_image_regression.py +0 -0
  68. {meta_ads_mcp-0.10.7 → meta_ads_mcp-0.11.0}/tests/test_http_transport.py +0 -0
  69. {meta_ads_mcp-0.10.7 → meta_ads_mcp-0.11.0}/tests/test_integration_openai_mcp.py +0 -0
  70. {meta_ads_mcp-0.10.7 → meta_ads_mcp-0.11.0}/tests/test_mobile_app_adset_creation.py +0 -0
  71. {meta_ads_mcp-0.10.7 → meta_ads_mcp-0.11.0}/tests/test_mobile_app_adset_issue.py +0 -0
  72. {meta_ads_mcp-0.10.7 → meta_ads_mcp-0.11.0}/tests/test_openai.py +0 -0
  73. {meta_ads_mcp-0.10.7 → meta_ads_mcp-0.11.0}/tests/test_openai_mcp_deep_research.py +0 -0
  74. {meta_ads_mcp-0.10.7 → meta_ads_mcp-0.11.0}/tests/test_page_discovery.py +0 -0
  75. {meta_ads_mcp-0.10.7 → meta_ads_mcp-0.11.0}/tests/test_page_discovery_integration.py +0 -0
  76. {meta_ads_mcp-0.10.7 → meta_ads_mcp-0.11.0}/tests/test_targeting_search_e2e.py +0 -0
  77. {meta_ads_mcp-0.10.7 → meta_ads_mcp-0.11.0}/tests/test_update_ad_creative_id.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meta-ads-mcp
3
- Version: 0.10.7
3
+ Version: 0.11.0
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
@@ -176,7 +176,22 @@ For local installation configuration, authentication options, and advanced techn
176
176
  - `access_token` (optional): Meta API access token (will use cached token if not provided)
177
177
  - `account_id`: Meta Ads account ID (format: act_XXXXXXXXX)
178
178
  - `name`: Campaign name
179
- - `objective`: Campaign objective (AWARENESS, TRAFFIC, ENGAGEMENT, etc.)
179
+ - `objective`: Campaign objective (ODAX, outcome-based). Must be one of:
180
+ - `OUTCOME_AWARENESS`
181
+ - `OUTCOME_TRAFFIC`
182
+ - `OUTCOME_ENGAGEMENT`
183
+ - `OUTCOME_LEADS`
184
+ - `OUTCOME_SALES`
185
+ - `OUTCOME_APP_PROMOTION`
186
+
187
+ Note: Legacy objectives such as `BRAND_AWARENESS`, `LINK_CLICKS`, `CONVERSIONS`, `APP_INSTALLS`, etc. are no longer valid for new campaigns and will cause a 400 error. Use the outcome-based values above. Common mappings:
188
+ - `BRAND_AWARENESS` → `OUTCOME_AWARENESS`
189
+ - `REACH` → `OUTCOME_AWARENESS`
190
+ - `LINK_CLICKS`, `TRAFFIC` → `OUTCOME_TRAFFIC`
191
+ - `POST_ENGAGEMENT`, `PAGE_LIKES`, `EVENT_RESPONSES`, `VIDEO_VIEWS` → `OUTCOME_ENGAGEMENT`
192
+ - `LEAD_GENERATION` → `OUTCOME_LEADS`
193
+ - `CONVERSIONS`, `CATALOG_SALES`, `MESSAGES` (sales-focused flows) → `OUTCOME_SALES`
194
+ - `APP_INSTALLS` → `OUTCOME_APP_PROMOTION`
180
195
  - `status`: Initial campaign status (default: PAUSED)
181
196
  - `special_ad_categories`: List of special ad categories if applicable
182
197
  - `daily_budget`: Daily budget in account currency (in cents)
@@ -184,6 +199,20 @@ For local installation configuration, authentication options, and advanced techn
184
199
  - `bid_strategy`: Bid strategy. Must be one of: `LOWEST_COST_WITHOUT_CAP`, `LOWEST_COST_WITH_BID_CAP`, `COST_CAP`, `LOWEST_COST_WITH_MIN_ROAS`.
185
200
  - Returns: Confirmation with new campaign details
186
201
 
202
+ - Example:
203
+ ```json
204
+ {
205
+ "name": "2025 - Bedroom Furniture - Awareness",
206
+ "account_id": "act_123456789012345",
207
+ "objective": "OUTCOME_AWARENESS",
208
+ "special_ad_categories": [],
209
+ "status": "PAUSED",
210
+ "buying_type": "AUCTION",
211
+ "bid_strategy": "LOWEST_COST_WITHOUT_CAP",
212
+ "daily_budget": 10000
213
+ }
214
+ ```
215
+
187
216
  7. `mcp_meta_ads_get_adsets`
188
217
  - Get ad sets for a Meta Ads account with optional filtering by campaign
189
218
  - Inputs:
@@ -151,7 +151,22 @@ For local installation configuration, authentication options, and advanced techn
151
151
  - `access_token` (optional): Meta API access token (will use cached token if not provided)
152
152
  - `account_id`: Meta Ads account ID (format: act_XXXXXXXXX)
153
153
  - `name`: Campaign name
154
- - `objective`: Campaign objective (AWARENESS, TRAFFIC, ENGAGEMENT, etc.)
154
+ - `objective`: Campaign objective (ODAX, outcome-based). Must be one of:
155
+ - `OUTCOME_AWARENESS`
156
+ - `OUTCOME_TRAFFIC`
157
+ - `OUTCOME_ENGAGEMENT`
158
+ - `OUTCOME_LEADS`
159
+ - `OUTCOME_SALES`
160
+ - `OUTCOME_APP_PROMOTION`
161
+
162
+ Note: Legacy objectives such as `BRAND_AWARENESS`, `LINK_CLICKS`, `CONVERSIONS`, `APP_INSTALLS`, etc. are no longer valid for new campaigns and will cause a 400 error. Use the outcome-based values above. Common mappings:
163
+ - `BRAND_AWARENESS` → `OUTCOME_AWARENESS`
164
+ - `REACH` → `OUTCOME_AWARENESS`
165
+ - `LINK_CLICKS`, `TRAFFIC` → `OUTCOME_TRAFFIC`
166
+ - `POST_ENGAGEMENT`, `PAGE_LIKES`, `EVENT_RESPONSES`, `VIDEO_VIEWS` → `OUTCOME_ENGAGEMENT`
167
+ - `LEAD_GENERATION` → `OUTCOME_LEADS`
168
+ - `CONVERSIONS`, `CATALOG_SALES`, `MESSAGES` (sales-focused flows) → `OUTCOME_SALES`
169
+ - `APP_INSTALLS` → `OUTCOME_APP_PROMOTION`
155
170
  - `status`: Initial campaign status (default: PAUSED)
156
171
  - `special_ad_categories`: List of special ad categories if applicable
157
172
  - `daily_budget`: Daily budget in account currency (in cents)
@@ -159,6 +174,20 @@ For local installation configuration, authentication options, and advanced techn
159
174
  - `bid_strategy`: Bid strategy. Must be one of: `LOWEST_COST_WITHOUT_CAP`, `LOWEST_COST_WITH_BID_CAP`, `COST_CAP`, `LOWEST_COST_WITH_MIN_ROAS`.
160
175
  - Returns: Confirmation with new campaign details
161
176
 
177
+ - Example:
178
+ ```json
179
+ {
180
+ "name": "2025 - Bedroom Furniture - Awareness",
181
+ "account_id": "act_123456789012345",
182
+ "objective": "OUTCOME_AWARENESS",
183
+ "special_ad_categories": [],
184
+ "status": "PAUSED",
185
+ "buying_type": "AUCTION",
186
+ "bid_strategy": "LOWEST_COST_WITHOUT_CAP",
187
+ "daily_budget": 10000
188
+ }
189
+ ```
190
+
162
191
  7. `mcp_meta_ads_get_adsets`
163
192
  - Get ad sets for a Meta Ads account with optional filtering by campaign
164
193
  - Inputs:
@@ -1,13 +1,12 @@
1
1
  """
2
2
  Meta Ads MCP - Python Package
3
3
 
4
- This package provides a Meta Ads Marketing Cloud Platform (MCP) integration
5
- with the Claude LLM.
4
+ This package provides a Meta Ads MCP integration
6
5
  """
7
6
 
8
7
  from meta_ads_mcp.core.server import main
9
8
 
10
- __version__ = "0.10.7"
9
+ __version__ = "0.11.0"
11
10
 
12
11
  __all__ = [
13
12
  'get_ad_accounts',
@@ -1,14 +1,14 @@
1
1
  """Account-related functionality for Meta Ads API."""
2
2
 
3
3
  import json
4
- from typing import Optional
4
+ from typing import Optional, Dict, Any
5
5
  from .api import meta_api_tool, make_api_request
6
6
  from .server import mcp_server
7
7
 
8
8
 
9
9
  @mcp_server.tool()
10
10
  @meta_api_tool
11
- async def get_ad_accounts(access_token: str = None, user_id: str = "me", limit: int = 200) -> str:
11
+ async def get_ad_accounts(access_token: Optional[str] = None, user_id: str = "me", limit: int = 200) -> str:
12
12
  """
13
13
  Get ad accounts accessible by a user.
14
14
 
@@ -30,13 +30,13 @@ async def get_ad_accounts(access_token: str = None, user_id: str = "me", limit:
30
30
 
31
31
  @mcp_server.tool()
32
32
  @meta_api_tool
33
- async def get_account_info(access_token: str = None, account_id: str = None) -> str:
33
+ async def get_account_info(account_id: str, access_token: Optional[str] = None) -> str:
34
34
  """
35
35
  Get detailed information about a specific ad account.
36
36
 
37
37
  Args:
38
+ account_id: Meta Ads account ID (format: act_XXXXXXXXX)
38
39
  access_token: Meta API access token (optional - will use cached token if not provided)
39
- account_id: Meta Ads account ID (format: act_XXXXXXXXX) - REQUIRED
40
40
  """
41
41
  if not account_id:
42
42
  return {
@@ -16,27 +16,21 @@ from .server import mcp_server
16
16
 
17
17
  @mcp_server.tool()
18
18
  @meta_api_tool
19
- async def get_ads(access_token: str = None, account_id: str = None, limit: int = 10,
19
+ async def get_ads(account_id: str, access_token: Optional[str] = None, limit: int = 10,
20
20
  campaign_id: str = "", adset_id: str = "") -> str:
21
21
  """
22
22
  Get ads for a Meta Ads account with optional filtering.
23
23
 
24
24
  Args:
25
- access_token: Meta API access token (optional - will use cached token if not provided)
26
25
  account_id: Meta Ads account ID (format: act_XXXXXXXXX)
26
+ access_token: Meta API access token (optional - will use cached token if not provided)
27
27
  limit: Maximum number of ads to return (default: 10)
28
28
  campaign_id: Optional campaign ID to filter by
29
29
  adset_id: Optional ad set ID to filter by
30
30
  """
31
- # If no account ID is specified, try to get the first one for the user
31
+ # Require explicit account_id
32
32
  if not account_id:
33
- accounts_json = await get_ad_accounts("me", json.dumps({"limit": 1}), access_token)
34
- accounts_data = json.loads(accounts_json)
35
-
36
- if "data" in accounts_data and accounts_data["data"]:
37
- account_id = accounts_data["data"][0]["id"]
38
- else:
39
- return json.dumps({"error": "No account ID specified and no accounts found for user"}, indent=2)
33
+ return json.dumps({"error": "No account ID specified"}, indent=2)
40
34
 
41
35
  # Prioritize adset_id over campaign_id - use adset-specific endpoint
42
36
  if adset_id:
@@ -67,13 +61,13 @@ async def get_ads(access_token: str = None, account_id: str = None, limit: int =
67
61
 
68
62
  @mcp_server.tool()
69
63
  @meta_api_tool
70
- async def get_ad_details(access_token: str = None, ad_id: str = None) -> str:
64
+ async def get_ad_details(ad_id: str, access_token: Optional[str] = None) -> str:
71
65
  """
72
66
  Get detailed information about a specific ad.
73
67
 
74
68
  Args:
75
- access_token: Meta API access token (optional - will use cached token if not provided)
76
69
  ad_id: Meta Ads ad ID
70
+ access_token: Meta API access token (optional - will use cached token if not provided)
77
71
  """
78
72
  if not ad_id:
79
73
  return json.dumps({"error": "No ad ID provided"}, indent=2)
@@ -91,14 +85,14 @@ async def get_ad_details(access_token: str = None, ad_id: str = None) -> str:
91
85
  @mcp_server.tool()
92
86
  @meta_api_tool
93
87
  async def create_ad(
94
- account_id: str = None,
95
- name: str = None,
96
- adset_id: str = None,
97
- creative_id: str = None,
88
+ account_id: str,
89
+ name: str,
90
+ adset_id: str,
91
+ creative_id: str,
98
92
  status: str = "PAUSED",
99
- bid_amount = None,
93
+ bid_amount: Optional[int] = None,
100
94
  tracking_specs: Optional[List[Dict[str, Any]]] = None,
101
- access_token: str = None
95
+ access_token: Optional[str] = None
102
96
  ) -> str:
103
97
  """
104
98
  Create a new ad with an existing creative.
@@ -158,13 +152,13 @@ async def create_ad(
158
152
 
159
153
  @mcp_server.tool()
160
154
  @meta_api_tool
161
- async def get_ad_creatives(access_token: str = None, ad_id: str = None) -> str:
155
+ async def get_ad_creatives(ad_id: str, access_token: Optional[str] = None) -> str:
162
156
  """
163
157
  Get creative details for a specific ad. Best if combined with get_ad_image to get the full image.
164
158
 
165
159
  Args:
166
- access_token: Meta API access token (optional - will use cached token if not provided)
167
160
  ad_id: Meta Ads ad ID
161
+ access_token: Meta API access token (optional - will use cached token if not provided)
168
162
  """
169
163
  if not ad_id:
170
164
  return json.dumps({"error": "No ad ID provided"}, indent=2)
@@ -186,13 +180,13 @@ async def get_ad_creatives(access_token: str = None, ad_id: str = None) -> str:
186
180
 
187
181
  @mcp_server.tool()
188
182
  @meta_api_tool
189
- async def get_ad_image(access_token: str = None, ad_id: str = None) -> Image:
183
+ async def get_ad_image(ad_id: str, access_token: Optional[str] = None) -> Image:
190
184
  """
191
185
  Get, download, and visualize a Meta ad image in one step. Useful to see the image in the LLM.
192
186
 
193
187
  Args:
194
- access_token: Meta API access token (optional - will use cached token if not provided)
195
188
  ad_id: Meta Ads ad ID
189
+ access_token: Meta API access token (optional - will use cached token if not provided)
196
190
 
197
191
  Returns:
198
192
  The ad image ready for direct visual analysis
@@ -388,13 +382,13 @@ async def get_ad_image(access_token: str = None, ad_id: str = None) -> Image:
388
382
 
389
383
  @mcp_server.tool()
390
384
  @meta_api_tool
391
- async def save_ad_image_locally(access_token: str = None, ad_id: str = None, output_dir: str = "ad_images") -> str:
385
+ async def save_ad_image_locally(ad_id: str, access_token: Optional[str] = None, output_dir: str = "ad_images") -> str:
392
386
  """
393
387
  Get, download, and save a Meta ad image locally, returning the file path.
394
388
 
395
389
  Args:
396
- access_token: Meta API access token (optional - will use cached token if not provided)
397
390
  ad_id: Meta Ads ad ID
391
+ access_token: Meta API access token (optional - will use cached token if not provided)
398
392
  output_dir: Directory to save the image file (default: 'ad_images')
399
393
 
400
394
  Returns:
@@ -516,11 +510,11 @@ async def save_ad_image_locally(access_token: str = None, ad_id: str = None, out
516
510
  @meta_api_tool
517
511
  async def update_ad(
518
512
  ad_id: str,
519
- status: str = None,
520
- bid_amount: int = None,
521
- tracking_specs = None,
522
- creative_id: str = None,
523
- access_token: str = None
513
+ status: Optional[str] = None,
514
+ bid_amount: Optional[int] = None,
515
+ tracking_specs: Optional[List[Dict[str, Any]]] = None,
516
+ creative_id: Optional[str] = None,
517
+ access_token: Optional[str] = None
524
518
  ) -> str:
525
519
  """
526
520
  Update an ad with new settings.
@@ -562,18 +556,18 @@ async def update_ad(
562
556
  @mcp_server.tool()
563
557
  @meta_api_tool
564
558
  async def upload_ad_image(
565
- access_token: str = None,
566
- account_id: str = None,
567
- file: str = None,
568
- image_url: str = None,
569
- name: str = None
559
+ account_id: str,
560
+ access_token: Optional[str] = None,
561
+ file: Optional[str] = None,
562
+ image_url: Optional[str] = None,
563
+ name: Optional[str] = None
570
564
  ) -> str:
571
565
  """
572
566
  Upload an image to use in Meta Ads creatives.
573
567
 
574
568
  Args:
575
- access_token: Meta API access token (optional - will use cached token if not provided)
576
569
  account_id: Meta Ads account ID (format: act_XXXXXXXXX)
570
+ access_token: Meta API access token (optional - will use cached token if not provided)
577
571
  file: Data URL or raw base64 string of the image (e.g., "data:image/png;base64,iVBORw0KG...")
578
572
  image_url: Direct URL to an image to fetch and upload
579
573
  name: Optional name for the image (default: filename)
@@ -671,7 +665,55 @@ async def upload_ad_image(
671
665
  print(f"Uploading image to Facebook Ad Account {account_id}")
672
666
  data = await make_api_request(endpoint, access_token, params, method="POST")
673
667
 
674
- return json.dumps(data, indent=2)
668
+ # Normalize/structure the response for callers (e.g., to easily grab image_hash)
669
+ # Typical Graph API response shape:
670
+ # { "images": { "<hash>": { "hash": "<hash>", "url": "...", "width": ..., "height": ..., "name": "...", "status": 1 } } }
671
+ if isinstance(data, dict) and "images" in data and isinstance(data["images"], dict) and data["images"]:
672
+ images_dict = data["images"]
673
+ images_list = []
674
+ for hash_key, info in images_dict.items():
675
+ # Some responses may omit the nested hash, so ensure it's present
676
+ normalized = {
677
+ "hash": (info.get("hash") or hash_key),
678
+ "url": info.get("url"),
679
+ "width": info.get("width"),
680
+ "height": info.get("height"),
681
+ "name": info.get("name"),
682
+ }
683
+ # Drop null/None values
684
+ normalized = {k: v for k, v in normalized.items() if v is not None}
685
+ images_list.append(normalized)
686
+
687
+ # Sort deterministically by hash
688
+ images_list.sort(key=lambda i: i.get("hash", ""))
689
+ primary_hash = images_list[0].get("hash") if images_list else None
690
+
691
+ result = {
692
+ "success": True,
693
+ "account_id": account_id,
694
+ "name": final_name,
695
+ "image_hash": primary_hash,
696
+ "images_count": len(images_list),
697
+ "images": images_list
698
+ }
699
+ return json.dumps(result, indent=2)
700
+
701
+ # If the API returned an error-like structure, surface it consistently
702
+ if isinstance(data, dict) and "error" in data:
703
+ return json.dumps({
704
+ "error": "Failed to upload image",
705
+ "details": data.get("error"),
706
+ "account_id": account_id,
707
+ "name": final_name
708
+ }, indent=2)
709
+
710
+ # Fallback: return a wrapped raw response to avoid breaking callers
711
+ return json.dumps({
712
+ "success": True,
713
+ "account_id": account_id,
714
+ "name": final_name,
715
+ "raw_response": data
716
+ }, indent=2)
675
717
 
676
718
  except Exception as e:
677
719
  return json.dumps({
@@ -683,29 +725,29 @@ async def upload_ad_image(
683
725
  @mcp_server.tool()
684
726
  @meta_api_tool
685
727
  async def create_ad_creative(
686
- access_token: str = None,
687
- account_id: str = None,
688
- name: str = None,
689
- image_hash: str = None,
690
- page_id: str = None,
691
- link_url: str = None,
692
- message: str = None,
693
- headline: str = None,
694
- headlines: List[str] = None,
695
- description: str = None,
696
- descriptions: List[str] = None,
697
- dynamic_creative_spec: Dict[str, Any] = None,
698
- call_to_action_type: str = None,
699
- instagram_actor_id: str = None
728
+ account_id: str,
729
+ image_hash: str,
730
+ access_token: Optional[str] = None,
731
+ name: Optional[str] = None,
732
+ page_id: Optional[str] = None,
733
+ link_url: Optional[str] = None,
734
+ message: Optional[str] = None,
735
+ headline: Optional[str] = None,
736
+ headlines: Optional[List[str]] = None,
737
+ description: Optional[str] = None,
738
+ descriptions: Optional[List[str]] = None,
739
+ dynamic_creative_spec: Optional[Dict[str, Any]] = None,
740
+ call_to_action_type: Optional[str] = None,
741
+ instagram_actor_id: Optional[str] = None
700
742
  ) -> str:
701
743
  """
702
744
  Create a new ad creative using an uploaded image hash.
703
745
 
704
746
  Args:
705
- access_token: Meta API access token (optional - will use cached token if not provided)
706
747
  account_id: Meta Ads account ID (format: act_XXXXXXXXX)
707
- name: Creative name
708
748
  image_hash: Hash of the uploaded image
749
+ access_token: Meta API access token (optional - will use cached token if not provided)
750
+ name: Creative name
709
751
  page_id: Facebook Page ID to be used for the ad
710
752
  link_url: Destination URL for the ad
711
753
  message: Ad copy/text
@@ -895,23 +937,23 @@ async def create_ad_creative(
895
937
  @mcp_server.tool()
896
938
  @meta_api_tool
897
939
  async def update_ad_creative(
898
- access_token: str = None,
899
- creative_id: str = None,
900
- name: str = None,
901
- message: str = None,
902
- headline: str = None,
903
- headlines: List[str] = None,
904
- description: str = None,
905
- descriptions: List[str] = None,
906
- dynamic_creative_spec: Dict[str, Any] = None,
907
- call_to_action_type: str = None
940
+ creative_id: str,
941
+ access_token: Optional[str] = None,
942
+ name: Optional[str] = None,
943
+ message: Optional[str] = None,
944
+ headline: Optional[str] = None,
945
+ headlines: Optional[List[str]] = None,
946
+ description: Optional[str] = None,
947
+ descriptions: Optional[List[str]] = None,
948
+ dynamic_creative_spec: Optional[Dict[str, Any]] = None,
949
+ call_to_action_type: Optional[str] = None
908
950
  ) -> str:
909
951
  """
910
952
  Update an existing ad creative with new content or settings.
911
953
 
912
954
  Args:
913
- access_token: Meta API access token (optional - will use cached token if not provided)
914
955
  creative_id: Meta Ads creative ID to update
956
+ access_token: Meta API access token (optional - will use cached token if not provided)
915
957
  name: New creative name
916
958
  message: New ad copy/text
917
959
  headline: Single headline for simple ads (cannot be used with headlines)
@@ -1204,13 +1246,13 @@ async def _search_pages_by_name_core(access_token: str, account_id: str, search_
1204
1246
 
1205
1247
  @mcp_server.tool()
1206
1248
  @meta_api_tool
1207
- async def search_pages_by_name(access_token: str = None, account_id: str = None, search_term: str = None) -> str:
1249
+ async def search_pages_by_name(account_id: str, access_token: Optional[str] = None, search_term: Optional[str] = None) -> str:
1208
1250
  """
1209
1251
  Search for pages by name within an account.
1210
1252
 
1211
1253
  Args:
1212
- access_token: Meta API access token (optional - will use cached token if not provided)
1213
1254
  account_id: Meta Ads account ID (format: act_XXXXXXXXX)
1255
+ access_token: Meta API access token (optional - will use cached token if not provided)
1214
1256
  search_term: Search term to find pages by name (optional - returns all pages if not provided)
1215
1257
 
1216
1258
  Returns:
@@ -1227,13 +1269,13 @@ async def search_pages_by_name(access_token: str = None, account_id: str = None,
1227
1269
 
1228
1270
  @mcp_server.tool()
1229
1271
  @meta_api_tool
1230
- async def get_account_pages(access_token: str = None, account_id: str = None) -> str:
1272
+ async def get_account_pages(account_id: str, access_token: Optional[str] = None) -> str:
1231
1273
  """
1232
1274
  Get pages associated with a Meta Ads account.
1233
1275
 
1234
1276
  Args:
1235
- access_token: Meta API access token (optional - will use cached token if not provided)
1236
1277
  account_id: Meta Ads account ID (format: act_XXXXXXXXX)
1278
+ access_token: Meta API access token (optional - will use cached token if not provided)
1237
1279
 
1238
1280
  Returns:
1239
1281
  JSON response with pages associated with the account
@@ -14,10 +14,10 @@ if not DISABLE_ADS_LIBRARY:
14
14
  @mcp_server.tool()
15
15
  @meta_api_tool
16
16
  async def search_ads_archive(
17
- access_token: str = None,
18
- search_terms: str = None,
17
+ search_terms: str,
18
+ ad_reached_countries: List[str],
19
+ access_token: Optional[str] = None,
19
20
  ad_type: str = "ALL",
20
- ad_reached_countries: List[str] = None,
21
21
  limit: int = 25, # Default limit, adjust as needed
22
22
  fields: str = "ad_creation_time,ad_creative_body,ad_creative_link_caption,ad_creative_link_description,ad_creative_link_title,ad_delivery_start_time,ad_delivery_stop_time,ad_snapshot_url,currency,demographic_distribution,funding_entity,impressions,page_id,page_name,publisher_platform,region_distribution,spend"
23
23
  ) -> str:
@@ -25,10 +25,10 @@ if not DISABLE_ADS_LIBRARY:
25
25
  Search the Facebook Ads Library archive.
26
26
 
27
27
  Args:
28
- access_token: Meta API access token (optional - will use cached token if not provided).
29
28
  search_terms: The search query for ads.
30
- ad_type: Type of ads to search for (e.g., POLITICAL_AND_ISSUE_ADS, HOUSING_ADS, ALL).
31
29
  ad_reached_countries: List of country codes (e.g., ["US", "GB"]).
30
+ access_token: Meta API access token (optional - will use cached token if not provided).
31
+ ad_type: Type of ads to search for (e.g., POLITICAL_AND_ISSUE_ADS, HOUSING_ADS, ALL).
32
32
  limit: Maximum number of ads to return.
33
33
  fields: Comma-separated string of fields to retrieve for each ad.
34
34
 
@@ -9,25 +9,19 @@ from .server import mcp_server
9
9
 
10
10
  @mcp_server.tool()
11
11
  @meta_api_tool
12
- async def get_adsets(access_token: str = None, account_id: str = None, limit: int = 10, campaign_id: str = "") -> str:
12
+ async def get_adsets(account_id: str, access_token: Optional[str] = None, limit: int = 10, campaign_id: str = "") -> str:
13
13
  """
14
14
  Get ad sets for a Meta Ads account with optional filtering by campaign.
15
15
 
16
16
  Args:
17
- access_token: Meta API access token (optional - will use cached token if not provided)
18
17
  account_id: Meta Ads account ID (format: act_XXXXXXXXX)
18
+ access_token: Meta API access token (optional - will use cached token if not provided)
19
19
  limit: Maximum number of ad sets to return (default: 10)
20
20
  campaign_id: Optional campaign ID to filter by
21
21
  """
22
- # If no account ID is specified, try to get the first one for the user
22
+ # Require explicit account_id
23
23
  if not account_id:
24
- accounts_json = await get_ad_accounts("me", json.dumps({"limit": 1}), access_token)
25
- accounts_data = json.loads(accounts_json)
26
-
27
- if "data" in accounts_data and accounts_data["data"]:
28
- account_id = accounts_data["data"][0]["id"]
29
- else:
30
- return json.dumps({"error": "No account ID specified and no accounts found for user"}, indent=2)
24
+ return json.dumps({"error": "No account ID specified"}, indent=2)
31
25
 
32
26
  # Change endpoint based on whether campaign_id is provided
33
27
  if campaign_id:
@@ -53,12 +47,12 @@ async def get_adsets(access_token: str = None, account_id: str = None, limit: in
53
47
 
54
48
  @mcp_server.tool()
55
49
  @meta_api_tool
56
- async def get_adset_details(access_token: str = None, adset_id: str = None) -> str:
50
+ async def get_adset_details(adset_id: str, access_token: Optional[str] = None) -> str:
57
51
  """
58
52
  Get detailed information about a specific ad set.
59
53
 
60
54
  Args:
61
- adset_id: Meta Ads ad set ID (required)
55
+ adset_id: Meta Ads ad set ID
62
56
  access_token: Meta API access token (optional - will use cached token if not provided)
63
57
 
64
58
  Example:
@@ -90,23 +84,23 @@ async def get_adset_details(access_token: str = None, adset_id: str = None) -> s
90
84
  @mcp_server.tool()
91
85
  @meta_api_tool
92
86
  async def create_adset(
93
- account_id: str = None,
94
- campaign_id: str = None,
95
- name: str = None,
87
+ account_id: str,
88
+ campaign_id: str,
89
+ name: str,
90
+ optimization_goal: str,
91
+ billing_event: str,
96
92
  status: str = "PAUSED",
97
- daily_budget = None,
98
- lifetime_budget = None,
99
- targeting: Dict[str, Any] = None,
100
- optimization_goal: str = None,
101
- billing_event: str = None,
102
- bid_amount = None,
103
- bid_strategy: str = None,
104
- start_time: str = None,
105
- end_time: str = None,
106
- dsa_beneficiary: str = None,
107
- promoted_object: Dict[str, Any] = None,
108
- destination_type: str = None,
109
- access_token: str = None
93
+ daily_budget: Optional[int] = None,
94
+ lifetime_budget: Optional[int] = None,
95
+ targeting: Optional[Dict[str, Any]] = None,
96
+ bid_amount: Optional[int] = None,
97
+ bid_strategy: Optional[str] = None,
98
+ start_time: Optional[str] = None,
99
+ end_time: Optional[str] = None,
100
+ dsa_beneficiary: Optional[str] = None,
101
+ promoted_object: Optional[Dict[str, Any]] = None,
102
+ destination_type: Optional[str] = None,
103
+ access_token: Optional[str] = None
110
104
  ) -> str:
111
105
  """
112
106
  Create a new ad set in a Meta Ads account.
@@ -115,13 +109,13 @@ async def create_adset(
115
109
  account_id: Meta Ads account ID (format: act_XXXXXXXXX)
116
110
  campaign_id: Meta Ads campaign ID this ad set belongs to
117
111
  name: Ad set name
112
+ optimization_goal: Conversion optimization goal (e.g., 'LINK_CLICKS', 'REACH', 'CONVERSIONS', 'APP_INSTALLS')
113
+ billing_event: How you're charged (e.g., 'IMPRESSIONS', 'LINK_CLICKS')
118
114
  status: Initial ad set status (default: PAUSED)
119
115
  daily_budget: Daily budget in account currency (in cents) as a string
120
116
  lifetime_budget: Lifetime budget in account currency (in cents) as a string
121
117
  targeting: Targeting specifications including age, location, interests, etc.
122
118
  Use targeting_automation.advantage_audience=1 for automatic audience finding
123
- optimization_goal: Conversion optimization goal (e.g., 'LINK_CLICKS', 'REACH', 'CONVERSIONS', 'APP_INSTALLS')
124
- billing_event: How you're charged (e.g., 'IMPRESSIONS', 'LINK_CLICKS')
125
119
  bid_amount: Bid amount in account currency (in cents)
126
120
  bid_strategy: Bid strategy (e.g., 'LOWEST_COST', 'LOWEST_COST_WITH_BID_CAP')
127
121
  start_time: Start time in ISO 8601 format (e.g., '2023-12-01T12:00:00-0800')
@@ -293,10 +287,10 @@ async def create_adset(
293
287
 
294
288
  @mcp_server.tool()
295
289
  @meta_api_tool
296
- async def update_adset(adset_id: str, frequency_control_specs: List[Dict[str, Any]] = None, bid_strategy: str = None,
297
- bid_amount: int = None, status: str = None, targeting: Dict[str, Any] = None,
298
- optimization_goal: str = None, daily_budget = None, lifetime_budget = None,
299
- access_token: str = None) -> str:
290
+ async def update_adset(adset_id: str, frequency_control_specs: Optional[List[Dict[str, Any]]] = None, bid_strategy: Optional[str] = None,
291
+ bid_amount: Optional[int] = None, status: Optional[str] = None, targeting: Optional[Dict[str, Any]] = None,
292
+ optimization_goal: Optional[str] = None, daily_budget: Optional[int] = None, lifetime_budget: Optional[int] = None,
293
+ access_token: Optional[str] = None) -> str:
300
294
  """
301
295
  Update an ad set with new settings including frequency caps and budgets.
302
296
 
@@ -316,6 +316,6 @@ def meta_api_tool(func):
316
316
  return result
317
317
  except Exception as e:
318
318
  logger.error(f"Error in {func.__name__}: {str(e)}")
319
- return {"error": str(e)}
319
+ return json.dumps({"error": str(e)}, indent=2)
320
320
 
321
321
  return wrapper
@@ -87,7 +87,7 @@ meta_config = MetaConfig()
87
87
 
88
88
  class TokenInfo:
89
89
  """Stores token information including expiration"""
90
- def __init__(self, access_token: str, expires_in: int = None, user_id: str = None):
90
+ def __init__(self, access_token: str, expires_in: Optional[int] = None, user_id: Optional[str] = None):
91
91
  self.access_token = access_token
92
92
  self.expires_in = expires_in
93
93
  self.user_id = user_id
@@ -25,6 +25,7 @@ Environment Variables:
25
25
  """
26
26
 
27
27
  import json
28
+ from typing import Optional
28
29
  import asyncio
29
30
  import os
30
31
  from .api import meta_api_tool
@@ -37,7 +38,7 @@ from .pipeboard_auth import pipeboard_auth_manager
37
38
  ENABLE_LOGIN_LINK = not bool(os.environ.get("META_ADS_DISABLE_LOGIN_LINK", ""))
38
39
 
39
40
 
40
- async def get_login_link(access_token: str = None) -> str:
41
+ async def get_login_link(access_token: Optional[str] = None) -> str:
41
42
  """
42
43
  Get a clickable login link for Meta Ads authentication.
43
44