meta-ads-mcp 0.7.8__py3-none-any.whl → 0.7.10__py3-none-any.whl

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.
meta_ads_mcp/__init__.py CHANGED
@@ -7,7 +7,7 @@ with the Claude LLM.
7
7
 
8
8
  from meta_ads_mcp.core.server import main
9
9
 
10
- __version__ = "0.7.8"
10
+ __version__ = "0.7.10"
11
11
 
12
12
  __all__ = [
13
13
  'get_ad_accounts',
meta_ads_mcp/core/ads.py CHANGED
@@ -171,7 +171,7 @@ async def get_ad_creatives(access_token: str = None, ad_id: str = None) -> str:
171
171
 
172
172
  endpoint = f"{ad_id}/adcreatives"
173
173
  params = {
174
- "fields": "id,name,status,thumbnail_url,image_url,image_hash,object_story_spec" # Added image_hash
174
+ "fields": "id,name,status,thumbnail_url,image_url,image_hash,object_story_spec,asset_feed_spec,image_urls_for_viewing"
175
175
  }
176
176
 
177
177
  data = await make_api_request(endpoint, access_token, params)
@@ -279,14 +279,26 @@ async def get_ad_image(access_token: str = None, ad_id: str = None) -> Image:
279
279
  if "data" in creative_data and creative_data["data"]:
280
280
  creative = creative_data["data"][0]
281
281
 
282
- # Try image_urls_for_viewing first (usually higher quality)
282
+ # Prioritize higher quality image URLs in this order:
283
+ # 1. image_urls_for_viewing (usually highest quality)
284
+ # 2. image_url (direct field)
285
+ # 3. object_story_spec.link_data.picture (usually full size)
286
+ # 4. thumbnail_url (last resort - often profile thumbnail)
287
+
283
288
  if "image_urls_for_viewing" in creative and creative["image_urls_for_viewing"]:
284
289
  image_url = creative["image_urls_for_viewing"][0]
285
290
  print(f"Using image_urls_for_viewing: {image_url}")
286
- # Fall back to thumbnail_url
291
+ elif "image_url" in creative and creative["image_url"]:
292
+ image_url = creative["image_url"]
293
+ print(f"Using image_url: {image_url}")
294
+ elif "object_story_spec" in creative and "link_data" in creative["object_story_spec"]:
295
+ link_data = creative["object_story_spec"]["link_data"]
296
+ if "picture" in link_data and link_data["picture"]:
297
+ image_url = link_data["picture"]
298
+ print(f"Using object_story_spec.link_data.picture: {image_url}")
287
299
  elif "thumbnail_url" in creative and creative["thumbnail_url"]:
288
300
  image_url = creative["thumbnail_url"]
289
- print(f"Using thumbnail_url: {image_url}")
301
+ print(f"Using thumbnail_url (fallback): {image_url}")
290
302
 
291
303
  if not image_url:
292
304
  return "Error: No image URLs found in creative"
@@ -658,31 +670,31 @@ async def create_ad_creative(
658
670
  if not account_id.startswith("act_"):
659
671
  account_id = f"act_{account_id}"
660
672
 
661
- # If no page ID is provided, try to find a page associated with the account
673
+ # Enhanced page discovery: If no page ID is provided, use robust discovery methods
662
674
  if not page_id:
663
675
  try:
664
- # Query to get pages associated with the account
665
- pages_endpoint = f"{account_id}/assigned_pages"
666
- pages_params = {
667
- "fields": "id,name",
668
- "limit": 1
669
- }
670
-
671
- pages_data = await make_api_request(pages_endpoint, access_token, pages_params)
676
+ # Use the comprehensive page discovery logic from get_account_pages
677
+ page_discovery_result = await _discover_pages_for_account(account_id, access_token)
672
678
 
673
- if "data" in pages_data and pages_data["data"]:
674
- page_id = pages_data["data"][0]["id"]
675
- print(f"Using page ID: {page_id} ({pages_data['data'][0].get('name', 'Unknown')})")
679
+ if page_discovery_result.get("success"):
680
+ page_id = page_discovery_result["page_id"]
681
+ page_name = page_discovery_result.get("page_name", "Unknown")
682
+ print(f"Auto-discovered page ID: {page_id} ({page_name})")
676
683
  else:
677
684
  return json.dumps({
678
- "error": "No page ID provided and no pages found for this account",
679
- "suggestion": "Please provide a page_id parameter"
685
+ "error": "No page ID provided and no suitable pages found for this account",
686
+ "details": page_discovery_result.get("message", "Page discovery failed"),
687
+ "suggestions": [
688
+ "Use get_account_pages to see available pages",
689
+ "Use search_pages_by_name to find specific pages",
690
+ "Provide a page_id parameter manually"
691
+ ]
680
692
  }, indent=2)
681
693
  except Exception as e:
682
694
  return json.dumps({
683
- "error": "Error finding page for account",
695
+ "error": "Error during page discovery",
684
696
  "details": str(e),
685
- "suggestion": "Please provide a page_id parameter"
697
+ "suggestion": "Please provide a page_id parameter or use get_account_pages to find available pages"
686
698
  }, indent=2)
687
699
 
688
700
  # Prepare the creative data
@@ -747,6 +759,191 @@ async def create_ad_creative(
747
759
  }, indent=2)
748
760
 
749
761
 
762
+ async def _discover_pages_for_account(account_id: str, access_token: str) -> dict:
763
+ """
764
+ Internal function to discover pages for an account using multiple approaches.
765
+ Returns the best available page ID for ad creation.
766
+ """
767
+ try:
768
+ # Approach 1: Extract page IDs from tracking_specs in ads (most reliable)
769
+ endpoint = f"{account_id}/ads"
770
+ params = {
771
+ "fields": "id,name,adset_id,campaign_id,status,creative,created_time,updated_time,bid_amount,conversion_domain,tracking_specs",
772
+ "limit": 100
773
+ }
774
+
775
+ tracking_ads_data = await make_api_request(endpoint, access_token, params)
776
+
777
+ tracking_page_ids = set()
778
+ if "data" in tracking_ads_data:
779
+ for ad in tracking_ads_data.get("data", []):
780
+ tracking_specs = ad.get("tracking_specs", [])
781
+ if isinstance(tracking_specs, list):
782
+ for spec in tracking_specs:
783
+ if isinstance(spec, dict) and "page" in spec:
784
+ page_list = spec["page"]
785
+ if isinstance(page_list, list):
786
+ for page_id in page_list:
787
+ if isinstance(page_id, (str, int)) and str(page_id).isdigit():
788
+ tracking_page_ids.add(str(page_id))
789
+
790
+ if tracking_page_ids:
791
+ # Get details for the first page found
792
+ page_id = list(tracking_page_ids)[0]
793
+ page_endpoint = f"{page_id}"
794
+ page_params = {
795
+ "fields": "id,name,username,category,fan_count,link,verification_status,picture"
796
+ }
797
+
798
+ page_data = await make_api_request(page_endpoint, access_token, page_params)
799
+ if "id" in page_data:
800
+ return {
801
+ "success": True,
802
+ "page_id": page_id,
803
+ "page_name": page_data.get("name", "Unknown"),
804
+ "source": "tracking_specs",
805
+ "note": "Page ID extracted from existing ads - most reliable for ad creation"
806
+ }
807
+
808
+ # Approach 2: Try client_pages endpoint
809
+ endpoint = f"{account_id}/client_pages"
810
+ params = {
811
+ "fields": "id,name,username,category,fan_count,link,verification_status,picture"
812
+ }
813
+
814
+ client_pages_data = await make_api_request(endpoint, access_token, params)
815
+
816
+ if "data" in client_pages_data and client_pages_data["data"]:
817
+ page = client_pages_data["data"][0]
818
+ return {
819
+ "success": True,
820
+ "page_id": page["id"],
821
+ "page_name": page.get("name", "Unknown"),
822
+ "source": "client_pages"
823
+ }
824
+
825
+ # Approach 3: Try assigned_pages endpoint
826
+ pages_endpoint = f"{account_id}/assigned_pages"
827
+ pages_params = {
828
+ "fields": "id,name",
829
+ "limit": 1
830
+ }
831
+
832
+ pages_data = await make_api_request(pages_endpoint, access_token, pages_params)
833
+
834
+ if "data" in pages_data and pages_data["data"]:
835
+ page = pages_data["data"][0]
836
+ return {
837
+ "success": True,
838
+ "page_id": page["id"],
839
+ "page_name": page.get("name", "Unknown"),
840
+ "source": "assigned_pages"
841
+ }
842
+
843
+ # If all approaches failed
844
+ return {
845
+ "success": False,
846
+ "message": "No suitable pages found for this account",
847
+ "note": "Try using get_account_pages to see all available pages or provide page_id manually"
848
+ }
849
+
850
+ except Exception as e:
851
+ return {
852
+ "success": False,
853
+ "message": f"Error during page discovery: {str(e)}"
854
+ }
855
+
856
+
857
+ async def _search_pages_by_name_core(access_token: str, account_id: str, search_term: str = None) -> str:
858
+ """
859
+ Core logic for searching pages by name.
860
+
861
+ Args:
862
+ access_token: Meta API access token
863
+ account_id: Meta Ads account ID (format: act_XXXXXXXXX)
864
+ search_term: Search term to find pages by name (optional - returns all pages if not provided)
865
+
866
+ Returns:
867
+ JSON string with search results
868
+ """
869
+ # Ensure account_id has the 'act_' prefix
870
+ if not account_id.startswith("act_"):
871
+ account_id = f"act_{account_id}"
872
+
873
+ try:
874
+ # Use the internal discovery function directly
875
+ page_discovery_result = await _discover_pages_for_account(account_id, access_token)
876
+
877
+ if not page_discovery_result.get("success"):
878
+ return json.dumps({
879
+ "data": [],
880
+ "message": "No pages found for this account",
881
+ "details": page_discovery_result.get("message", "Page discovery failed")
882
+ }, indent=2)
883
+
884
+ # Create a single page result
885
+ page_data = {
886
+ "id": page_discovery_result["page_id"],
887
+ "name": page_discovery_result.get("page_name", "Unknown"),
888
+ "source": page_discovery_result.get("source", "unknown")
889
+ }
890
+
891
+ all_pages_data = {"data": [page_data]}
892
+
893
+ # Filter pages by search term if provided
894
+ if search_term:
895
+ search_term_lower = search_term.lower()
896
+ filtered_pages = []
897
+
898
+ for page in all_pages_data["data"]:
899
+ page_name = page.get("name", "").lower()
900
+ if search_term_lower in page_name:
901
+ filtered_pages.append(page)
902
+
903
+ return json.dumps({
904
+ "data": filtered_pages,
905
+ "search_term": search_term,
906
+ "total_found": len(filtered_pages),
907
+ "total_available": len(all_pages_data["data"])
908
+ }, indent=2)
909
+ else:
910
+ # Return all pages if no search term provided
911
+ return json.dumps({
912
+ "data": all_pages_data["data"],
913
+ "total_available": len(all_pages_data["data"]),
914
+ "note": "Use search_term parameter to filter pages by name"
915
+ }, indent=2)
916
+
917
+ except Exception as e:
918
+ return json.dumps({
919
+ "error": "Failed to search pages by name",
920
+ "details": str(e)
921
+ }, indent=2)
922
+
923
+
924
+ @mcp_server.tool()
925
+ @meta_api_tool
926
+ async def search_pages_by_name(access_token: str = None, account_id: str = None, search_term: str = None) -> str:
927
+ """
928
+ Search for pages by name within an account.
929
+
930
+ Args:
931
+ access_token: Meta API access token (optional - will use cached token if not provided)
932
+ account_id: Meta Ads account ID (format: act_XXXXXXXXX)
933
+ search_term: Search term to find pages by name (optional - returns all pages if not provided)
934
+
935
+ Returns:
936
+ JSON response with matching pages
937
+ """
938
+ # Check required parameters
939
+ if not account_id:
940
+ return json.dumps({"error": "No account ID provided"}, indent=2)
941
+
942
+ # Call the core function
943
+ result = await _search_pages_by_name_core(access_token, account_id, search_term)
944
+ return result
945
+
946
+
750
947
  @mcp_server.tool()
751
948
  @meta_api_tool
752
949
  async def get_account_pages(access_token: str = None, account_id: str = None) -> str:
@@ -78,23 +78,31 @@ ad_creative_images = {}
78
78
  def extract_creative_image_urls(creative: Dict[str, Any]) -> List[str]:
79
79
  """
80
80
  Extract image URLs from a creative object for direct viewing.
81
+ Prioritizes higher quality images over thumbnails.
81
82
 
82
83
  Args:
83
84
  creative: Meta Ads creative object
84
85
 
85
86
  Returns:
86
- List of image URLs found in the creative
87
+ List of image URLs found in the creative, prioritized by quality
87
88
  """
88
89
  image_urls = []
89
90
 
91
+ # Prioritize higher quality image URLs in this order:
92
+ # 1. image_urls_for_viewing (usually highest quality)
93
+ # 2. image_url (direct field)
94
+ # 3. object_story_spec.link_data.picture (usually full size)
95
+ # 4. asset_feed_spec images (multiple high-quality images)
96
+ # 5. thumbnail_url (last resort - often profile thumbnail)
97
+
98
+ # Check for image_urls_for_viewing (highest priority)
99
+ if "image_urls_for_viewing" in creative and creative["image_urls_for_viewing"]:
100
+ image_urls.extend(creative["image_urls_for_viewing"])
101
+
90
102
  # Check for direct image_url field
91
103
  if "image_url" in creative and creative["image_url"]:
92
104
  image_urls.append(creative["image_url"])
93
105
 
94
- # Check for thumbnail_url field
95
- if "thumbnail_url" in creative and creative["thumbnail_url"]:
96
- image_urls.append(creative["thumbnail_url"])
97
-
98
106
  # Check object_story_spec for image URLs
99
107
  if "object_story_spec" in creative:
100
108
  story_spec = creative["object_story_spec"]
@@ -103,7 +111,7 @@ def extract_creative_image_urls(creative: Dict[str, Any]) -> List[str]:
103
111
  if "link_data" in story_spec:
104
112
  link_data = story_spec["link_data"]
105
113
 
106
- # Check for picture field
114
+ # Check for picture field (usually full size)
107
115
  if "picture" in link_data and link_data["picture"]:
108
116
  image_urls.append(link_data["picture"])
109
117
 
@@ -121,6 +129,10 @@ def extract_creative_image_urls(creative: Dict[str, Any]) -> List[str]:
121
129
  if "url" in image and image["url"]:
122
130
  image_urls.append(image["url"])
123
131
 
132
+ # Check for thumbnail_url field (lowest priority)
133
+ if "thumbnail_url" in creative and creative["thumbnail_url"]:
134
+ image_urls.append(creative["thumbnail_url"])
135
+
124
136
  # Remove duplicates while preserving order
125
137
  seen = set()
126
138
  unique_urls = []
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meta-ads-mcp
3
- Version: 0.7.8
3
+ Version: 0.7.10
4
4
  Summary: Model Context Protocol (MCP) plugin 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
@@ -1,8 +1,8 @@
1
- meta_ads_mcp/__init__.py,sha256=BiSbN8PrAeLKejVnkAfsz06KLVM59vQJmehwssDnEzc,1492
1
+ meta_ads_mcp/__init__.py,sha256=Y0Oj0zMOmh2TX_E4iBNehH9NFlM1w9O3injKT8FJMbw,1493
2
2
  meta_ads_mcp/__main__.py,sha256=XaQt3iXftG_7f0Zu7Wop9SeFgrD2WBn0EQOaPMc27d8,207
3
3
  meta_ads_mcp/core/__init__.py,sha256=6nYdue6yRepkt6JTAoPGhGbS51qfDSvmczRrDwYOG6A,1709
4
4
  meta_ads_mcp/core/accounts.py,sha256=4IAdGLZ4WE4j4pGW6E0qaXcXqbUIW6Wk2kuQUtlmRTQ,4030
5
- meta_ads_mcp/core/ads.py,sha256=aaK70mgfhBJRXr4cdkKag5mjYzvHuHpRttJvTMzPk4Y,36156
5
+ meta_ads_mcp/core/ads.py,sha256=lUq5te8SDFHS2gn8IlwVa0svyFyqJBMvWtuxy1hacFI,44428
6
6
  meta_ads_mcp/core/ads_library.py,sha256=BBGVbtjO5eFV42iiY3XPU-wIV8HupzUKpHgPBrydSvU,3232
7
7
  meta_ads_mcp/core/adsets.py,sha256=vY5JNHmGK1a_sQ5B1LnjxLYXzs5_jOajTTjWHRDJ4_Y,12518
8
8
  meta_ads_mcp/core/api.py,sha256=aAzM6Q75VQOFXtr5D-mDmBRhxWK4wsiODsJYnR3mpDI,14994
@@ -20,9 +20,9 @@ meta_ads_mcp/core/reports.py,sha256=Dv3hfsPOR7IZ9WrYrKd_6SNgZl-USIphg7knva3UYAw,
20
20
  meta_ads_mcp/core/resources.py,sha256=-zIIfZulpo76vcKv6jhAlQq91cR2SZ3cjYZt3ek3x0w,1236
21
21
  meta_ads_mcp/core/server.py,sha256=WhbAag7xdhbGcp7rnU4sKhqXJ8Slapa_ba3T23Yp_2U,17889
22
22
  meta_ads_mcp/core/targeting.py,sha256=3HW1qirEdwaQurlBZGenbIwawcb5J06ghJKRfgu9ZEs,6318
23
- meta_ads_mcp/core/utils.py,sha256=ofKUhyo-5SZoJVuBeTVFPPQCffk0UKpwmDMrd8qQxNc,8715
24
- meta_ads_mcp-0.7.8.dist-info/METADATA,sha256=HjCu4iRgkgfEv5WYS3-E6CchfrkassZHZjMVbgfOphE,20409
25
- meta_ads_mcp-0.7.8.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
26
- meta_ads_mcp-0.7.8.dist-info/entry_points.txt,sha256=Dv2RkoBjRJBqj6CyhwqGIiwPCD-SCL1-7B9-zmVRuv0,57
27
- meta_ads_mcp-0.7.8.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
28
- meta_ads_mcp-0.7.8.dist-info/RECORD,,
23
+ meta_ads_mcp/core/utils.py,sha256=ytj41yC5SqduLrAiZYBSd6OUwlJRaIClTwnnYKpNFds,9387
24
+ meta_ads_mcp-0.7.10.dist-info/METADATA,sha256=DCV6TdsYdyLogsgvOpIu8hBjawrGo1FWiF4PuKp20AU,20410
25
+ meta_ads_mcp-0.7.10.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
26
+ meta_ads_mcp-0.7.10.dist-info/entry_points.txt,sha256=Dv2RkoBjRJBqj6CyhwqGIiwPCD-SCL1-7B9-zmVRuv0,57
27
+ meta_ads_mcp-0.7.10.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
28
+ meta_ads_mcp-0.7.10.dist-info/RECORD,,