meta-ads-mcp 0.10.9__tar.gz → 0.11.1__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.9 → meta_ads_mcp-0.11.1}/PKG-INFO +1 -1
  2. {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/meta_ads_mcp/__init__.py +1 -1
  3. {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/meta_ads_mcp/core/accounts.py +4 -4
  4. {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/meta_ads_mcp/core/ads.py +176 -177
  5. {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/meta_ads_mcp/core/ads_library.py +5 -5
  6. {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/meta_ads_mcp/core/adsets.py +28 -34
  7. {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/meta_ads_mcp/core/api.py +1 -1
  8. {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/meta_ads_mcp/core/auth.py +1 -1
  9. {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/meta_ads_mcp/core/authentication.py +2 -1
  10. {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/meta_ads_mcp/core/budget_schedules.py +1 -1
  11. {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/meta_ads_mcp/core/campaigns.py +33 -39
  12. {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/meta_ads_mcp/core/duplication.py +4 -4
  13. {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/meta_ads_mcp/core/insights.py +2 -2
  14. {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/meta_ads_mcp/core/openai_deep_research.py +4 -4
  15. {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/meta_ads_mcp/core/pipeboard_auth.py +1 -1
  16. {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/meta_ads_mcp/core/reports.py +3 -3
  17. {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/meta_ads_mcp/core/targeting.py +14 -14
  18. {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/pyproject.toml +1 -1
  19. {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/tests/test_dsa_beneficiary.py +1 -1
  20. {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/tests/test_dsa_integration.py +1 -1
  21. {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/tests/test_dynamic_creatives.py +2 -1
  22. {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/tests/test_insights_actions_and_values.py +7 -10
  23. {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/tests/test_targeting.py +15 -6
  24. {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/.github/workflows/publish.yml +0 -0
  25. {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/.github/workflows/test.yml +0 -0
  26. {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/.gitignore +0 -0
  27. {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/CUSTOM_META_APP.md +0 -0
  28. {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/Dockerfile +0 -0
  29. {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/LICENSE +0 -0
  30. {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/LOCAL_INSTALLATION.md +0 -0
  31. {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/META_API_NOTES.md +0 -0
  32. {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/README.md +0 -0
  33. {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/RELEASE.md +0 -0
  34. {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/STREAMABLE_HTTP_SETUP.md +0 -0
  35. {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/examples/README.md +0 -0
  36. {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/examples/example_http_client.py +0 -0
  37. {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/future_improvements.md +0 -0
  38. {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/images/meta-ads-example.png +0 -0
  39. {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/meta_ads_auth.sh +0 -0
  40. {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/meta_ads_mcp/__main__.py +0 -0
  41. {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/meta_ads_mcp/core/__init__.py +0 -0
  42. {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/meta_ads_mcp/core/callback_server.py +0 -0
  43. {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/meta_ads_mcp/core/http_auth_integration.py +0 -0
  44. {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/meta_ads_mcp/core/resources.py +0 -0
  45. {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/meta_ads_mcp/core/server.py +0 -0
  46. {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/meta_ads_mcp/core/utils.py +0 -0
  47. {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/requirements.txt +0 -0
  48. {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/setup.py +0 -0
  49. {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/smithery.yaml +0 -0
  50. {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/tests/README.md +0 -0
  51. {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/tests/README_REGRESSION_TESTS.md +0 -0
  52. {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/tests/__init__.py +0 -0
  53. {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/tests/conftest.py +0 -0
  54. {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/tests/e2e_account_info_search_issue.py +0 -0
  55. {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/tests/test_account_info_access_fix.py +0 -0
  56. {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/tests/test_account_search.py +0 -0
  57. {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/tests/test_budget_update.py +0 -0
  58. {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/tests/test_budget_update_e2e.py +0 -0
  59. {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/tests/test_duplication.py +0 -0
  60. {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/tests/test_duplication_regression.py +0 -0
  61. {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/tests/test_estimate_audience_size.py +0 -0
  62. {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/tests/test_estimate_audience_size_e2e.py +0 -0
  63. {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/tests/test_get_account_pages.py +0 -0
  64. {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/tests/test_get_ad_creatives_fix.py +0 -0
  65. {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/tests/test_get_ad_image_quality_improvements.py +0 -0
  66. {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/tests/test_get_ad_image_regression.py +0 -0
  67. {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/tests/test_http_transport.py +0 -0
  68. {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/tests/test_integration_openai_mcp.py +0 -0
  69. {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/tests/test_mobile_app_adset_creation.py +0 -0
  70. {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/tests/test_mobile_app_adset_issue.py +0 -0
  71. {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/tests/test_openai.py +0 -0
  72. {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/tests/test_openai_mcp_deep_research.py +0 -0
  73. {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/tests/test_page_discovery.py +0 -0
  74. {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/tests/test_page_discovery_integration.py +0 -0
  75. {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/tests/test_targeting_search_e2e.py +0 -0
  76. {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/tests/test_update_ad_creative_id.py +0 -0
  77. {meta_ads_mcp-0.10.9 → meta_ads_mcp-0.11.1}/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: 0.10.9
3
+ Version: 0.11.1
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
@@ -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__ = "0.10.9"
9
+ __version__ = "0.11.1"
10
10
 
11
11
  __all__ = [
12
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 {
@@ -14,29 +14,27 @@ from .utils import download_image, try_multiple_download_methods, ad_creative_im
14
14
  from .server import mcp_server
15
15
 
16
16
 
17
+ # Only register the save_ad_image_locally function if explicitly enabled via environment variable
18
+ ENABLE_SAVE_AD_IMAGE_LOCALLY = bool(os.environ.get("META_ADS_ENABLE_SAVE_AD_IMAGE_LOCALLY", ""))
19
+
20
+
17
21
  @mcp_server.tool()
18
22
  @meta_api_tool
19
- async def get_ads(access_token: str = None, account_id: str = None, limit: int = 10,
23
+ async def get_ads(account_id: str, access_token: Optional[str] = None, limit: int = 10,
20
24
  campaign_id: str = "", adset_id: str = "") -> str:
21
25
  """
22
26
  Get ads for a Meta Ads account with optional filtering.
23
27
 
24
28
  Args:
25
- access_token: Meta API access token (optional - will use cached token if not provided)
26
29
  account_id: Meta Ads account ID (format: act_XXXXXXXXX)
30
+ access_token: Meta API access token (optional - will use cached token if not provided)
27
31
  limit: Maximum number of ads to return (default: 10)
28
32
  campaign_id: Optional campaign ID to filter by
29
33
  adset_id: Optional ad set ID to filter by
30
34
  """
31
- # If no account ID is specified, try to get the first one for the user
35
+ # Require explicit account_id
32
36
  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)
37
+ return json.dumps({"error": "No account ID specified"}, indent=2)
40
38
 
41
39
  # Prioritize adset_id over campaign_id - use adset-specific endpoint
42
40
  if adset_id:
@@ -67,13 +65,13 @@ async def get_ads(access_token: str = None, account_id: str = None, limit: int =
67
65
 
68
66
  @mcp_server.tool()
69
67
  @meta_api_tool
70
- async def get_ad_details(access_token: str = None, ad_id: str = None) -> str:
68
+ async def get_ad_details(ad_id: str, access_token: Optional[str] = None) -> str:
71
69
  """
72
70
  Get detailed information about a specific ad.
73
71
 
74
72
  Args:
75
- access_token: Meta API access token (optional - will use cached token if not provided)
76
73
  ad_id: Meta Ads ad ID
74
+ access_token: Meta API access token (optional - will use cached token if not provided)
77
75
  """
78
76
  if not ad_id:
79
77
  return json.dumps({"error": "No ad ID provided"}, indent=2)
@@ -91,14 +89,14 @@ async def get_ad_details(access_token: str = None, ad_id: str = None) -> str:
91
89
  @mcp_server.tool()
92
90
  @meta_api_tool
93
91
  async def create_ad(
94
- account_id: str = None,
95
- name: str = None,
96
- adset_id: str = None,
97
- creative_id: str = None,
92
+ account_id: str,
93
+ name: str,
94
+ adset_id: str,
95
+ creative_id: str,
98
96
  status: str = "PAUSED",
99
- bid_amount = None,
97
+ bid_amount: Optional[int] = None,
100
98
  tracking_specs: Optional[List[Dict[str, Any]]] = None,
101
- access_token: str = None
99
+ access_token: Optional[str] = None
102
100
  ) -> str:
103
101
  """
104
102
  Create a new ad with an existing creative.
@@ -158,13 +156,13 @@ async def create_ad(
158
156
 
159
157
  @mcp_server.tool()
160
158
  @meta_api_tool
161
- async def get_ad_creatives(access_token: str = None, ad_id: str = None) -> str:
159
+ async def get_ad_creatives(ad_id: str, access_token: Optional[str] = None) -> str:
162
160
  """
163
161
  Get creative details for a specific ad. Best if combined with get_ad_image to get the full image.
164
162
 
165
163
  Args:
166
- access_token: Meta API access token (optional - will use cached token if not provided)
167
164
  ad_id: Meta Ads ad ID
165
+ access_token: Meta API access token (optional - will use cached token if not provided)
168
166
  """
169
167
  if not ad_id:
170
168
  return json.dumps({"error": "No ad ID provided"}, indent=2)
@@ -186,13 +184,13 @@ async def get_ad_creatives(access_token: str = None, ad_id: str = None) -> str:
186
184
 
187
185
  @mcp_server.tool()
188
186
  @meta_api_tool
189
- async def get_ad_image(access_token: str = None, ad_id: str = None) -> Image:
187
+ async def get_ad_image(ad_id: str, access_token: Optional[str] = None) -> Image:
190
188
  """
191
189
  Get, download, and visualize a Meta ad image in one step. Useful to see the image in the LLM.
192
190
 
193
191
  Args:
194
- access_token: Meta API access token (optional - will use cached token if not provided)
195
192
  ad_id: Meta Ads ad ID
193
+ access_token: Meta API access token (optional - will use cached token if not provided)
196
194
 
197
195
  Returns:
198
196
  The ad image ready for direct visual analysis
@@ -386,141 +384,142 @@ async def get_ad_image(access_token: str = None, ad_id: str = None) -> Image:
386
384
  return f"Error processing image: {str(e)}"
387
385
 
388
386
 
389
- @mcp_server.tool()
390
- @meta_api_tool
391
- async def save_ad_image_locally(access_token: str = None, ad_id: str = None, output_dir: str = "ad_images") -> str:
392
- """
393
- Get, download, and save a Meta ad image locally, returning the file path.
394
-
395
- Args:
396
- access_token: Meta API access token (optional - will use cached token if not provided)
397
- ad_id: Meta Ads ad ID
398
- output_dir: Directory to save the image file (default: 'ad_images')
399
-
400
- Returns:
401
- The file path to the saved image, or an error message string.
402
- """
403
- if not ad_id:
404
- return json.dumps({"error": "No ad ID provided"}, indent=2)
387
+ if ENABLE_SAVE_AD_IMAGE_LOCALLY:
388
+ @mcp_server.tool()
389
+ @meta_api_tool
390
+ async def save_ad_image_locally(ad_id: str, access_token: Optional[str] = None, output_dir: str = "ad_images") -> str:
391
+ """
392
+ Get, download, and save a Meta ad image locally, returning the file path.
405
393
 
406
- print(f"Attempting to get and save creative image for ad {ad_id}")
407
-
408
- # First, get creative and account IDs
409
- ad_endpoint = f"{ad_id}"
410
- ad_params = {
411
- "fields": "creative{id},account_id"
412
- }
413
-
414
- ad_data = await make_api_request(ad_endpoint, access_token, ad_params)
415
-
416
- if "error" in ad_data:
417
- return json.dumps({"error": f"Could not get ad data - {json.dumps(ad_data)}"}, indent=2)
418
-
419
- account_id = ad_data.get("account_id")
420
- if not account_id:
421
- return json.dumps({"error": "No account ID found for ad"}, indent=2)
422
-
423
- if "creative" not in ad_data:
424
- return json.dumps({"error": "No creative found for this ad"}, indent=2)
394
+ Args:
395
+ ad_id: Meta Ads ad ID
396
+ access_token: Meta API access token (optional - will use cached token if not provided)
397
+ output_dir: Directory to save the image file (default: 'ad_images')
425
398
 
426
- creative_data = ad_data.get("creative", {})
427
- creative_id = creative_data.get("id")
428
- if not creative_id:
429
- return json.dumps({"error": "No creative ID found"}, indent=2)
430
-
431
- # Get creative details to find image hash
432
- creative_endpoint = f"{creative_id}"
433
- creative_params = {
434
- "fields": "id,name,image_hash,asset_feed_spec"
435
- }
436
- creative_details = await make_api_request(creative_endpoint, access_token, creative_params)
437
-
438
- image_hashes = []
439
- if "image_hash" in creative_details:
440
- image_hashes.append(creative_details["image_hash"])
441
- if "asset_feed_spec" in creative_details and "images" in creative_details["asset_feed_spec"]:
442
- for image in creative_details["asset_feed_spec"]["images"]:
443
- if "hash" in image:
444
- image_hashes.append(image["hash"])
445
-
446
- if not image_hashes:
447
- # Fallback attempt (as in get_ad_image)
448
- creative_json = await get_ad_creatives(ad_id=ad_id, access_token=access_token) # Ensure ad_id is passed correctly
449
- creative_data_list = json.loads(creative_json)
450
- if 'data' in creative_data_list and creative_data_list['data']:
451
- first_creative = creative_data_list['data'][0]
452
- if 'object_story_spec' in first_creative and 'link_data' in first_creative['object_story_spec'] and 'image_hash' in first_creative['object_story_spec']['link_data']:
453
- image_hashes.append(first_creative['object_story_spec']['link_data']['image_hash'])
454
- elif 'image_hash' in first_creative: # Check direct hash on creative data
455
- image_hashes.append(first_creative['image_hash'])
399
+ Returns:
400
+ The file path to the saved image, or an error message string.
401
+ """
402
+ if not ad_id:
403
+ return json.dumps({"error": "No ad ID provided"}, indent=2)
404
+
405
+ print(f"Attempting to get and save creative image for ad {ad_id}")
406
+
407
+ # First, get creative and account IDs
408
+ ad_endpoint = f"{ad_id}"
409
+ ad_params = {
410
+ "fields": "creative{id},account_id"
411
+ }
412
+
413
+ ad_data = await make_api_request(ad_endpoint, access_token, ad_params)
414
+
415
+ if "error" in ad_data:
416
+ return json.dumps({"error": f"Could not get ad data - {json.dumps(ad_data)}"}, indent=2)
417
+
418
+ account_id = ad_data.get("account_id")
419
+ if not account_id:
420
+ return json.dumps({"error": "No account ID found for ad"}, indent=2)
421
+
422
+ if "creative" not in ad_data:
423
+ return json.dumps({"error": "No creative found for this ad"}, indent=2)
424
+
425
+ creative_data = ad_data.get("creative", {})
426
+ creative_id = creative_data.get("id")
427
+ if not creative_id:
428
+ return json.dumps({"error": "No creative ID found"}, indent=2)
429
+
430
+ # Get creative details to find image hash
431
+ creative_endpoint = f"{creative_id}"
432
+ creative_params = {
433
+ "fields": "id,name,image_hash,asset_feed_spec"
434
+ }
435
+ creative_details = await make_api_request(creative_endpoint, access_token, creative_params)
436
+
437
+ image_hashes = []
438
+ if "image_hash" in creative_details:
439
+ image_hashes.append(creative_details["image_hash"])
440
+ if "asset_feed_spec" in creative_details and "images" in creative_details["asset_feed_spec"]:
441
+ for image in creative_details["asset_feed_spec"]["images"]:
442
+ if "hash" in image:
443
+ image_hashes.append(image["hash"])
444
+
445
+ if not image_hashes:
446
+ # Fallback attempt (as in get_ad_image)
447
+ creative_json = await get_ad_creatives(ad_id=ad_id, access_token=access_token) # Ensure ad_id is passed correctly
448
+ creative_data_list = json.loads(creative_json)
449
+ if 'data' in creative_data_list and creative_data_list['data']:
450
+ first_creative = creative_data_list['data'][0]
451
+ if 'object_story_spec' in first_creative and 'link_data' in first_creative['object_story_spec'] and 'image_hash' in first_creative['object_story_spec']['link_data']:
452
+ image_hashes.append(first_creative['object_story_spec']['link_data']['image_hash'])
453
+ elif 'image_hash' in first_creative: # Check direct hash on creative data
454
+ image_hashes.append(first_creative['image_hash'])
456
455
 
457
456
 
458
- if not image_hashes:
459
- return json.dumps({"error": "No image hashes found in creative or fallback"}, indent=2)
457
+ if not image_hashes:
458
+ return json.dumps({"error": "No image hashes found in creative or fallback"}, indent=2)
460
459
 
461
- print(f"Found image hashes: {image_hashes}")
462
-
463
- # Fetch image data using the first hash
464
- image_endpoint = f"act_{account_id}/adimages"
465
- hashes_str = f'["{image_hashes[0]}"]'
466
- image_params = {
467
- "fields": "hash,url,width,height,name,status",
468
- "hashes": hashes_str
469
- }
470
-
471
- print(f"Requesting image data with params: {image_params}")
472
- image_data = await make_api_request(image_endpoint, access_token, image_params)
473
-
474
- if "error" in image_data:
475
- return json.dumps({"error": f"Failed to get image data - {json.dumps(image_data)}"}, indent=2)
476
-
477
- if "data" not in image_data or not image_data["data"]:
478
- return json.dumps({"error": "No image data returned from API"}, indent=2)
460
+ print(f"Found image hashes: {image_hashes}")
479
461
 
480
- first_image = image_data["data"][0]
481
- image_url = first_image.get("url")
482
-
483
- if not image_url:
484
- return json.dumps({"error": "No valid image URL found in API response"}, indent=2)
462
+ # Fetch image data using the first hash
463
+ image_endpoint = f"act_{account_id}/adimages"
464
+ hashes_str = f'["{image_hashes[0]}"]'
465
+ image_params = {
466
+ "fields": "hash,url,width,height,name,status",
467
+ "hashes": hashes_str
468
+ }
485
469
 
486
- print(f"Downloading image from URL: {image_url}")
487
-
488
- # Download and Save Image
489
- image_bytes = await download_image(image_url)
490
-
491
- if not image_bytes:
492
- return json.dumps({"error": "Failed to download image"}, indent=2)
470
+ print(f"Requesting image data with params: {image_params}")
471
+ image_data = await make_api_request(image_endpoint, access_token, image_params)
493
472
 
494
- try:
495
- # Ensure output directory exists
496
- if not os.path.exists(output_dir):
497
- os.makedirs(output_dir)
473
+ if "error" in image_data:
474
+ return json.dumps({"error": f"Failed to get image data - {json.dumps(image_data)}"}, indent=2)
475
+
476
+ if "data" not in image_data or not image_data["data"]:
477
+ return json.dumps({"error": "No image data returned from API"}, indent=2)
498
478
 
499
- # Create a filename (e.g., using ad_id and image hash)
500
- file_extension = ".jpg" # Default extension, could try to infer from headers later
501
- filename = f"{ad_id}_{image_hashes[0]}{file_extension}"
502
- filepath = os.path.join(output_dir, filename)
503
-
504
- # Save the image bytes to the file
505
- with open(filepath, "wb") as f:
506
- f.write(image_bytes)
479
+ first_image = image_data["data"][0]
480
+ image_url = first_image.get("url")
481
+
482
+ if not image_url:
483
+ return json.dumps({"error": "No valid image URL found in API response"}, indent=2)
507
484
 
508
- print(f"Image saved successfully to: {filepath}")
509
- return json.dumps({"filepath": filepath}, indent=2) # Return JSON with filepath
485
+ print(f"Downloading image from URL: {image_url}")
486
+
487
+ # Download and Save Image
488
+ image_bytes = await download_image(image_url)
489
+
490
+ if not image_bytes:
491
+ return json.dumps({"error": "Failed to download image"}, indent=2)
492
+
493
+ try:
494
+ # Ensure output directory exists
495
+ if not os.path.exists(output_dir):
496
+ os.makedirs(output_dir)
497
+
498
+ # Create a filename (e.g., using ad_id and image hash)
499
+ file_extension = ".jpg" # Default extension, could try to infer from headers later
500
+ filename = f"{ad_id}_{image_hashes[0]}{file_extension}"
501
+ filepath = os.path.join(output_dir, filename)
502
+
503
+ # Save the image bytes to the file
504
+ with open(filepath, "wb") as f:
505
+ f.write(image_bytes)
506
+
507
+ print(f"Image saved successfully to: {filepath}")
508
+ return json.dumps({"filepath": filepath}, indent=2) # Return JSON with filepath
510
509
 
511
- except Exception as e:
512
- return json.dumps({"error": f"Failed to save image: {str(e)}"}, indent=2)
510
+ except Exception as e:
511
+ return json.dumps({"error": f"Failed to save image: {str(e)}"}, indent=2)
513
512
 
514
513
 
515
514
  @mcp_server.tool()
516
515
  @meta_api_tool
517
516
  async def update_ad(
518
517
  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
518
+ status: Optional[str] = None,
519
+ bid_amount: Optional[int] = None,
520
+ tracking_specs: Optional[List[Dict[str, Any]]] = None,
521
+ creative_id: Optional[str] = None,
522
+ access_token: Optional[str] = None
524
523
  ) -> str:
525
524
  """
526
525
  Update an ad with new settings.
@@ -562,18 +561,18 @@ async def update_ad(
562
561
  @mcp_server.tool()
563
562
  @meta_api_tool
564
563
  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
564
+ account_id: str,
565
+ access_token: Optional[str] = None,
566
+ file: Optional[str] = None,
567
+ image_url: Optional[str] = None,
568
+ name: Optional[str] = None
570
569
  ) -> str:
571
570
  """
572
571
  Upload an image to use in Meta Ads creatives.
573
572
 
574
573
  Args:
575
- access_token: Meta API access token (optional - will use cached token if not provided)
576
574
  account_id: Meta Ads account ID (format: act_XXXXXXXXX)
575
+ access_token: Meta API access token (optional - will use cached token if not provided)
577
576
  file: Data URL or raw base64 string of the image (e.g., "data:image/png;base64,iVBORw0KG...")
578
577
  image_url: Direct URL to an image to fetch and upload
579
578
  name: Optional name for the image (default: filename)
@@ -731,29 +730,29 @@ async def upload_ad_image(
731
730
  @mcp_server.tool()
732
731
  @meta_api_tool
733
732
  async def create_ad_creative(
734
- access_token: str = None,
735
- account_id: str = None,
736
- name: str = None,
737
- image_hash: str = None,
738
- page_id: str = None,
739
- link_url: str = None,
740
- message: str = None,
741
- headline: str = None,
742
- headlines: List[str] = None,
743
- description: str = None,
744
- descriptions: List[str] = None,
745
- dynamic_creative_spec: Dict[str, Any] = None,
746
- call_to_action_type: str = None,
747
- instagram_actor_id: str = None
733
+ account_id: str,
734
+ image_hash: str,
735
+ access_token: Optional[str] = None,
736
+ name: Optional[str] = None,
737
+ page_id: Optional[str] = None,
738
+ link_url: Optional[str] = None,
739
+ message: Optional[str] = None,
740
+ headline: Optional[str] = None,
741
+ headlines: Optional[List[str]] = None,
742
+ description: Optional[str] = None,
743
+ descriptions: Optional[List[str]] = None,
744
+ dynamic_creative_spec: Optional[Dict[str, Any]] = None,
745
+ call_to_action_type: Optional[str] = None,
746
+ instagram_actor_id: Optional[str] = None
748
747
  ) -> str:
749
748
  """
750
749
  Create a new ad creative using an uploaded image hash.
751
750
 
752
751
  Args:
753
- access_token: Meta API access token (optional - will use cached token if not provided)
754
752
  account_id: Meta Ads account ID (format: act_XXXXXXXXX)
755
- name: Creative name
756
753
  image_hash: Hash of the uploaded image
754
+ access_token: Meta API access token (optional - will use cached token if not provided)
755
+ name: Creative name
757
756
  page_id: Facebook Page ID to be used for the ad
758
757
  link_url: Destination URL for the ad
759
758
  message: Ad copy/text
@@ -943,23 +942,23 @@ async def create_ad_creative(
943
942
  @mcp_server.tool()
944
943
  @meta_api_tool
945
944
  async def update_ad_creative(
946
- access_token: str = None,
947
- creative_id: str = None,
948
- name: str = None,
949
- message: str = None,
950
- headline: str = None,
951
- headlines: List[str] = None,
952
- description: str = None,
953
- descriptions: List[str] = None,
954
- dynamic_creative_spec: Dict[str, Any] = None,
955
- call_to_action_type: str = None
945
+ creative_id: str,
946
+ access_token: Optional[str] = None,
947
+ name: Optional[str] = None,
948
+ message: Optional[str] = None,
949
+ headline: Optional[str] = None,
950
+ headlines: Optional[List[str]] = None,
951
+ description: Optional[str] = None,
952
+ descriptions: Optional[List[str]] = None,
953
+ dynamic_creative_spec: Optional[Dict[str, Any]] = None,
954
+ call_to_action_type: Optional[str] = None
956
955
  ) -> str:
957
956
  """
958
957
  Update an existing ad creative with new content or settings.
959
958
 
960
959
  Args:
961
- access_token: Meta API access token (optional - will use cached token if not provided)
962
960
  creative_id: Meta Ads creative ID to update
961
+ access_token: Meta API access token (optional - will use cached token if not provided)
963
962
  name: New creative name
964
963
  message: New ad copy/text
965
964
  headline: Single headline for simple ads (cannot be used with headlines)
@@ -1252,13 +1251,13 @@ async def _search_pages_by_name_core(access_token: str, account_id: str, search_
1252
1251
 
1253
1252
  @mcp_server.tool()
1254
1253
  @meta_api_tool
1255
- async def search_pages_by_name(access_token: str = None, account_id: str = None, search_term: str = None) -> str:
1254
+ async def search_pages_by_name(account_id: str, access_token: Optional[str] = None, search_term: Optional[str] = None) -> str:
1256
1255
  """
1257
1256
  Search for pages by name within an account.
1258
1257
 
1259
1258
  Args:
1260
- access_token: Meta API access token (optional - will use cached token if not provided)
1261
1259
  account_id: Meta Ads account ID (format: act_XXXXXXXXX)
1260
+ access_token: Meta API access token (optional - will use cached token if not provided)
1262
1261
  search_term: Search term to find pages by name (optional - returns all pages if not provided)
1263
1262
 
1264
1263
  Returns:
@@ -1275,13 +1274,13 @@ async def search_pages_by_name(access_token: str = None, account_id: str = None,
1275
1274
 
1276
1275
  @mcp_server.tool()
1277
1276
  @meta_api_tool
1278
- async def get_account_pages(access_token: str = None, account_id: str = None) -> str:
1277
+ async def get_account_pages(account_id: str, access_token: Optional[str] = None) -> str:
1279
1278
  """
1280
1279
  Get pages associated with a Meta Ads account.
1281
1280
 
1282
1281
  Args:
1283
- access_token: Meta API access token (optional - will use cached token if not provided)
1284
1282
  account_id: Meta Ads account ID (format: act_XXXXXXXXX)
1283
+ access_token: Meta API access token (optional - will use cached token if not provided)
1285
1284
 
1286
1285
  Returns:
1287
1286
  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