meta-ads-mcp 0.7.10__py3-none-any.whl → 0.9.0__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.10"
10
+ __version__ = "0.9.0"
11
11
 
12
12
  __all__ = [
13
13
  'get_ad_accounts',
meta_ads_mcp/core/ads.py CHANGED
@@ -519,6 +519,7 @@ async def update_ad(
519
519
  status: str = None,
520
520
  bid_amount: int = None,
521
521
  tracking_specs = None,
522
+ creative_id: str = None,
522
523
  access_token: str = None
523
524
  ) -> str:
524
525
  """
@@ -529,6 +530,7 @@ async def update_ad(
529
530
  status: Update ad status (ACTIVE, PAUSED, etc.)
530
531
  bid_amount: Bid amount in account currency (in cents for USD)
531
532
  tracking_specs: Optional tracking specifications (e.g., for pixel events).
533
+ creative_id: ID of the creative to associate with this ad (changes the ad's image/content)
532
534
  access_token: Meta API access token (optional - will use cached token if not provided)
533
535
  """
534
536
  if not ad_id:
@@ -542,14 +544,19 @@ async def update_ad(
542
544
  params["bid_amount"] = str(bid_amount)
543
545
  if tracking_specs is not None: # Add tracking_specs to params if provided
544
546
  params["tracking_specs"] = json.dumps(tracking_specs) # Needs to be JSON encoded string
547
+ if creative_id is not None:
548
+ # Creative parameter needs to be a JSON object containing creative_id
549
+ params["creative"] = json.dumps({"creative_id": creative_id})
545
550
 
546
551
  if not params:
547
- return json.dumps({"error": "No update parameters provided (status, bid_amount, or tracking_specs)"}, indent=2)
552
+ return json.dumps({"error": "No update parameters provided (status, bid_amount, tracking_specs, or creative_id)"}, indent=2)
548
553
 
549
554
  endpoint = f"{ad_id}"
550
- data = await make_api_request(endpoint, access_token, params, method='POST')
551
-
552
- return json.dumps(data, indent=2)
555
+ try:
556
+ data = await make_api_request(endpoint, access_token, params, method='POST')
557
+ return json.dumps(data, indent=2)
558
+ except Exception as e:
559
+ return json.dumps({"error": f"Failed to update ad: {str(e)}"}, indent=2)
553
560
 
554
561
 
555
562
  @mcp_server.tool()
@@ -633,7 +640,10 @@ async def create_ad_creative(
633
640
  link_url: str = None,
634
641
  message: str = None,
635
642
  headline: str = None,
643
+ headlines: List[str] = None,
636
644
  description: str = None,
645
+ descriptions: List[str] = None,
646
+ dynamic_creative_spec: Dict[str, Any] = None,
637
647
  call_to_action_type: str = None,
638
648
  instagram_actor_id: str = None
639
649
  ) -> str:
@@ -648,8 +658,11 @@ async def create_ad_creative(
648
658
  page_id: Facebook Page ID to be used for the ad
649
659
  link_url: Destination URL for the ad
650
660
  message: Ad copy/text
651
- headline: Ad headline
652
- description: Ad description
661
+ headline: Single headline for simple ads (cannot be used with headlines)
662
+ headlines: List of headlines for dynamic creative testing (cannot be used with headline)
663
+ description: Single description for simple ads (cannot be used with descriptions)
664
+ descriptions: List of descriptions for dynamic creative testing (cannot be used with description)
665
+ dynamic_creative_spec: Dynamic creative optimization settings
653
666
  call_to_action_type: Call to action button type (e.g., 'LEARN_MORE', 'SIGN_UP', 'SHOP_NOW')
654
667
  instagram_actor_id: Optional Instagram account ID for Instagram placements
655
668
 
@@ -697,29 +710,98 @@ async def create_ad_creative(
697
710
  "suggestion": "Please provide a page_id parameter or use get_account_pages to find available pages"
698
711
  }, indent=2)
699
712
 
713
+ # Validate headline/description parameters - cannot mix simple and complex
714
+ if headline and headlines:
715
+ return json.dumps({"error": "Cannot specify both 'headline' and 'headlines'. Use 'headline' for single headline or 'headlines' for multiple."}, indent=2)
716
+
717
+ if description and descriptions:
718
+ return json.dumps({"error": "Cannot specify both 'description' and 'descriptions'. Use 'description' for single description or 'descriptions' for multiple."}, indent=2)
719
+
720
+ # Convert simple parameters to complex format for internal processing
721
+ final_headlines = None
722
+ final_descriptions = None
723
+
724
+ if headline:
725
+ final_headlines = [headline]
726
+ elif headlines:
727
+ final_headlines = headlines
728
+
729
+ if description:
730
+ final_descriptions = [description]
731
+ elif descriptions:
732
+ final_descriptions = descriptions
733
+
734
+ # Validate dynamic creative parameters
735
+ if final_headlines:
736
+ if len(final_headlines) > 5:
737
+ return json.dumps({"error": "Maximum 5 headlines allowed for dynamic creatives"}, indent=2)
738
+ for i, h in enumerate(final_headlines):
739
+ if len(h) > 40:
740
+ return json.dumps({"error": f"Headline {i+1} exceeds 40 character limit"}, indent=2)
741
+
742
+ if final_descriptions:
743
+ if len(final_descriptions) > 5:
744
+ return json.dumps({"error": "Maximum 5 descriptions allowed for dynamic creatives"}, indent=2)
745
+ for i, d in enumerate(final_descriptions):
746
+ if len(d) > 125:
747
+ return json.dumps({"error": f"Description {i+1} exceeds 125 character limit"}, indent=2)
748
+
700
749
  # Prepare the creative data
701
750
  creative_data = {
702
- "name": name,
703
- "object_story_spec": {
751
+ "name": name
752
+ }
753
+
754
+ # Choose between asset_feed_spec (dynamic creative) or object_story_spec (traditional)
755
+ if final_headlines or final_descriptions:
756
+ # Use asset_feed_spec for dynamic creatives
757
+ asset_feed_spec = {
758
+ "ad_formats": ["SINGLE_IMAGE"],
759
+ "images": [{"hash": image_hash}],
760
+ "link_urls": [{"website_url": link_url if link_url else "https://facebook.com"}]
761
+ }
762
+
763
+ # Handle headlines
764
+ if final_headlines:
765
+ asset_feed_spec["headlines"] = [{"text": headline_text} for headline_text in final_headlines]
766
+
767
+ # Handle descriptions
768
+ if final_descriptions:
769
+ asset_feed_spec["descriptions"] = [{"text": description_text} for description_text in final_descriptions]
770
+
771
+ # Add message as primary_texts if provided
772
+ if message:
773
+ asset_feed_spec["primary_texts"] = [{"text": message}]
774
+
775
+ # Add call_to_action_types if provided
776
+ if call_to_action_type:
777
+ asset_feed_spec["call_to_action_types"] = [call_to_action_type]
778
+
779
+ creative_data["asset_feed_spec"] = asset_feed_spec
780
+
781
+ # For dynamic creatives, we need a simplified object_story_spec
782
+ creative_data["object_story_spec"] = {
783
+ "page_id": page_id
784
+ }
785
+ else:
786
+ # Use traditional object_story_spec for single creative
787
+ creative_data["object_story_spec"] = {
704
788
  "page_id": page_id,
705
789
  "link_data": {
706
790
  "image_hash": image_hash,
707
791
  "link": link_url if link_url else "https://facebook.com"
708
792
  }
709
793
  }
710
- }
711
-
712
- # Add optional parameters if provided
713
- if message:
714
- creative_data["object_story_spec"]["link_data"]["message"] = message
715
-
716
- if headline:
717
- creative_data["object_story_spec"]["link_data"]["name"] = headline
718
794
 
719
- if description:
720
- creative_data["object_story_spec"]["link_data"]["description"] = description
795
+ # Add optional parameters if provided
796
+ if message:
797
+ creative_data["object_story_spec"]["link_data"]["message"] = message
721
798
 
722
- if call_to_action_type:
799
+ # Add dynamic creative spec if provided
800
+ if dynamic_creative_spec:
801
+ creative_data["dynamic_creative_spec"] = dynamic_creative_spec
802
+
803
+ # Only add call_to_action to object_story_spec if we're not using asset_feed_spec
804
+ if call_to_action_type and "asset_feed_spec" not in creative_data:
723
805
  creative_data["object_story_spec"]["link_data"]["call_to_action"] = {
724
806
  "type": call_to_action_type
725
807
  }
@@ -739,7 +821,7 @@ async def create_ad_creative(
739
821
  creative_id = data["id"]
740
822
  creative_endpoint = f"{creative_id}"
741
823
  creative_params = {
742
- "fields": "id,name,status,thumbnail_url,image_url,image_hash,object_story_spec,url_tags,link_url"
824
+ "fields": "id,name,status,thumbnail_url,image_url,image_hash,object_story_spec,asset_feed_spec,url_tags,link_url"
743
825
  }
744
826
 
745
827
  creative_details = await make_api_request(creative_endpoint, access_token, creative_params)
@@ -759,6 +841,154 @@ async def create_ad_creative(
759
841
  }, indent=2)
760
842
 
761
843
 
844
+ @mcp_server.tool()
845
+ @meta_api_tool
846
+ async def update_ad_creative(
847
+ access_token: str = None,
848
+ creative_id: str = None,
849
+ name: str = None,
850
+ message: str = None,
851
+ headline: str = None,
852
+ headlines: List[str] = None,
853
+ description: str = None,
854
+ descriptions: List[str] = None,
855
+ dynamic_creative_spec: Dict[str, Any] = None,
856
+ call_to_action_type: str = None
857
+ ) -> str:
858
+ """
859
+ Update an existing ad creative with new content or settings.
860
+
861
+ Args:
862
+ access_token: Meta API access token (optional - will use cached token if not provided)
863
+ creative_id: Meta Ads creative ID to update
864
+ name: New creative name
865
+ message: New ad copy/text
866
+ headline: Single headline for simple ads (cannot be used with headlines)
867
+ headlines: New list of headlines for dynamic creative testing (cannot be used with headline)
868
+ description: Single description for simple ads (cannot be used with descriptions)
869
+ descriptions: New list of descriptions for dynamic creative testing (cannot be used with description)
870
+ dynamic_creative_spec: New dynamic creative optimization settings
871
+ call_to_action_type: New call to action button type
872
+
873
+ Returns:
874
+ JSON response with updated creative details
875
+ """
876
+ # Check required parameters
877
+ if not creative_id:
878
+ return json.dumps({"error": "No creative ID provided"}, indent=2)
879
+
880
+ # Validate headline/description parameters - cannot mix simple and complex
881
+ if headline and headlines:
882
+ return json.dumps({"error": "Cannot specify both 'headline' and 'headlines'. Use 'headline' for single headline or 'headlines' for multiple."}, indent=2)
883
+
884
+ if description and descriptions:
885
+ return json.dumps({"error": "Cannot specify both 'description' and 'descriptions'. Use 'description' for single description or 'descriptions' for multiple."}, indent=2)
886
+
887
+ # Convert simple parameters to complex format for internal processing
888
+ final_headlines = None
889
+ final_descriptions = None
890
+
891
+ if headline:
892
+ final_headlines = [headline]
893
+ elif headlines:
894
+ final_headlines = headlines
895
+
896
+ if description:
897
+ final_descriptions = [description]
898
+ elif descriptions:
899
+ final_descriptions = descriptions
900
+
901
+ # Validate dynamic creative parameters
902
+ if final_headlines:
903
+ if len(final_headlines) > 5:
904
+ return json.dumps({"error": "Maximum 5 headlines allowed for dynamic creatives"}, indent=2)
905
+ for i, h in enumerate(final_headlines):
906
+ if len(h) > 40:
907
+ return json.dumps({"error": f"Headline {i+1} exceeds 40 character limit"}, indent=2)
908
+
909
+ if final_descriptions:
910
+ if len(final_descriptions) > 5:
911
+ return json.dumps({"error": "Maximum 5 descriptions allowed for dynamic creatives"}, indent=2)
912
+ for i, d in enumerate(final_descriptions):
913
+ if len(d) > 125:
914
+ return json.dumps({"error": f"Description {i+1} exceeds 125 character limit"}, indent=2)
915
+
916
+ # Prepare the update data
917
+ update_data = {}
918
+
919
+ if name:
920
+ update_data["name"] = name
921
+
922
+ if message:
923
+ update_data["object_story_spec"] = {"link_data": {"message": message}}
924
+
925
+ # Handle dynamic creative assets via asset_feed_spec
926
+ if final_headlines or final_descriptions or dynamic_creative_spec:
927
+ asset_feed_spec = {}
928
+
929
+ # Add required ad_formats field for dynamic creatives
930
+ asset_feed_spec["ad_formats"] = ["SINGLE_IMAGE"]
931
+
932
+ # Handle headlines
933
+ if final_headlines:
934
+ asset_feed_spec["headlines"] = [{"text": headline_text} for headline_text in final_headlines]
935
+
936
+ # Handle descriptions
937
+ if final_descriptions:
938
+ asset_feed_spec["descriptions"] = [{"text": description_text} for description_text in final_descriptions]
939
+
940
+ # Add message as primary_texts if provided
941
+ if message:
942
+ asset_feed_spec["primary_texts"] = [{"text": message}]
943
+
944
+ update_data["asset_feed_spec"] = asset_feed_spec
945
+
946
+ # Add dynamic creative spec if provided
947
+ if dynamic_creative_spec:
948
+ update_data["dynamic_creative_spec"] = dynamic_creative_spec
949
+
950
+ # Handle call_to_action - add to asset_feed_spec if using dynamic creative, otherwise to object_story_spec
951
+ if call_to_action_type:
952
+ if "asset_feed_spec" in update_data:
953
+ update_data["asset_feed_spec"]["call_to_action_types"] = [call_to_action_type]
954
+ else:
955
+ if "object_story_spec" not in update_data:
956
+ update_data["object_story_spec"] = {"link_data": {}}
957
+ update_data["object_story_spec"]["link_data"]["call_to_action"] = {
958
+ "type": call_to_action_type
959
+ }
960
+
961
+ # Prepare the API endpoint for updating the creative
962
+ endpoint = f"{creative_id}"
963
+
964
+ try:
965
+ # Make API request to update the creative
966
+ data = await make_api_request(endpoint, access_token, update_data, method="POST")
967
+
968
+ # If successful, get more details about the updated creative
969
+ if "id" in data:
970
+ creative_endpoint = f"{creative_id}"
971
+ creative_params = {
972
+ "fields": "id,name,status,thumbnail_url,image_url,image_hash,object_story_spec,url_tags,link_url,dynamic_creative_spec"
973
+ }
974
+
975
+ creative_details = await make_api_request(creative_endpoint, access_token, creative_params)
976
+ return json.dumps({
977
+ "success": True,
978
+ "creative_id": creative_id,
979
+ "details": creative_details
980
+ }, indent=2)
981
+
982
+ return json.dumps(data, indent=2)
983
+
984
+ except Exception as e:
985
+ return json.dumps({
986
+ "error": "Failed to update ad creative",
987
+ "details": str(e),
988
+ "update_data_sent": update_data
989
+ }, indent=2)
990
+
991
+
762
992
  async def _discover_pages_for_account(account_id: str, access_token: str) -> dict:
763
993
  """
764
994
  Internal function to discover pages for an account using multiple approaches.
@@ -982,69 +1212,143 @@ async def get_account_pages(access_token: str = None, account_id: str = None) ->
982
1212
  account_id = f"act_{account_id}"
983
1213
 
984
1214
  try:
985
- # Try all approaches that might work
1215
+ # Collect all page IDs from multiple approaches
1216
+ all_page_ids = set()
986
1217
 
987
- # Approach 1: Get active ads and extract page IDs
988
- endpoint = f"{account_id}/ads"
989
- params = {
990
- "fields": "creative{object_story_spec{page_id}}",
991
- "limit": 100
992
- }
1218
+ # Approach 1: Get user's personal pages (broad scope)
1219
+ try:
1220
+ endpoint = "me/accounts"
1221
+ params = {
1222
+ "fields": "id,name,username,category,fan_count,link,verification_status,picture"
1223
+ }
1224
+ user_pages_data = await make_api_request(endpoint, access_token, params)
1225
+ if "data" in user_pages_data:
1226
+ for page in user_pages_data["data"]:
1227
+ if "id" in page:
1228
+ all_page_ids.add(page["id"])
1229
+ except Exception:
1230
+ pass
993
1231
 
994
- ads_data = await make_api_request(endpoint, access_token, params)
1232
+ # Approach 2: Try business manager pages
1233
+ try:
1234
+ # Strip 'act_' prefix to get raw account ID for business endpoints
1235
+ raw_account_id = account_id.replace("act_", "")
1236
+ endpoint = f"{raw_account_id}/owned_pages"
1237
+ params = {
1238
+ "fields": "id,name,username,category,fan_count,link,verification_status,picture"
1239
+ }
1240
+ business_pages_data = await make_api_request(endpoint, access_token, params)
1241
+ if "data" in business_pages_data:
1242
+ for page in business_pages_data["data"]:
1243
+ if "id" in page:
1244
+ all_page_ids.add(page["id"])
1245
+ except Exception:
1246
+ pass
995
1247
 
996
- # Extract unique page IDs from ads
997
- page_ids = set()
998
- if "data" in ads_data:
999
- for ad in ads_data.get("data", []):
1000
- if "creative" in ad and "creative" in ad and "object_story_spec" in ad["creative"] and "page_id" in ad["creative"]["object_story_spec"]:
1001
- page_ids.add(ad["creative"]["object_story_spec"]["page_id"])
1248
+ # Approach 3: Try ad account client pages
1249
+ try:
1250
+ endpoint = f"{account_id}/client_pages"
1251
+ params = {
1252
+ "fields": "id,name,username,category,fan_count,link,verification_status,picture"
1253
+ }
1254
+ client_pages_data = await make_api_request(endpoint, access_token, params)
1255
+ if "data" in client_pages_data:
1256
+ for page in client_pages_data["data"]:
1257
+ if "id" in page:
1258
+ all_page_ids.add(page["id"])
1259
+ except Exception:
1260
+ pass
1002
1261
 
1003
- # If we found page IDs, get details for each
1004
- if page_ids:
1005
- page_details = {"data": []}
1262
+ # Approach 4: Extract page IDs from all ad creatives (broader creative search)
1263
+ try:
1264
+ endpoint = f"{account_id}/adcreatives"
1265
+ params = {
1266
+ "fields": "id,name,object_story_spec,link_url,call_to_action,image_hash",
1267
+ "limit": 100
1268
+ }
1269
+ creatives_data = await make_api_request(endpoint, access_token, params)
1270
+ if "data" in creatives_data:
1271
+ for creative in creatives_data["data"]:
1272
+ if "object_story_spec" in creative and "page_id" in creative["object_story_spec"]:
1273
+ all_page_ids.add(creative["object_story_spec"]["page_id"])
1274
+ except Exception:
1275
+ pass
1006
1276
 
1007
- for page_id in page_ids:
1008
- page_endpoint = f"{page_id}"
1009
- page_params = {
1010
- "fields": "id,name,username,category,fan_count,link,verification_status,picture"
1011
- }
1012
-
1013
- page_data = await make_api_request(page_endpoint, access_token, page_params)
1014
- if "id" in page_data:
1015
- page_details["data"].append(page_data)
1277
+ # Approach 5: Get active ads and extract page IDs from creatives
1278
+ try:
1279
+ endpoint = f"{account_id}/ads"
1280
+ params = {
1281
+ "fields": "creative{object_story_spec{page_id},link_url,call_to_action}",
1282
+ "limit": 100
1283
+ }
1284
+ ads_data = await make_api_request(endpoint, access_token, params)
1285
+ if "data" in ads_data:
1286
+ for ad in ads_data.get("data", []):
1287
+ if "creative" in ad and "object_story_spec" in ad["creative"] and "page_id" in ad["creative"]["object_story_spec"]:
1288
+ all_page_ids.add(ad["creative"]["object_story_spec"]["page_id"])
1289
+ except Exception:
1290
+ pass
1291
+
1292
+ # Approach 6: Try promoted_objects endpoint
1293
+ try:
1294
+ endpoint = f"{account_id}/promoted_objects"
1295
+ params = {
1296
+ "fields": "page_id,object_store_url,product_set_id,application_id"
1297
+ }
1298
+ promoted_objects_data = await make_api_request(endpoint, access_token, params)
1299
+ if "data" in promoted_objects_data:
1300
+ for obj in promoted_objects_data["data"]:
1301
+ if "page_id" in obj:
1302
+ all_page_ids.add(obj["page_id"])
1303
+ except Exception:
1304
+ pass
1305
+
1306
+ # Approach 7: Extract page IDs from tracking_specs in ads (most reliable)
1307
+ try:
1308
+ endpoint = f"{account_id}/ads"
1309
+ params = {
1310
+ "fields": "id,name,status,creative,tracking_specs",
1311
+ "limit": 100
1312
+ }
1313
+ tracking_ads_data = await make_api_request(endpoint, access_token, params)
1314
+ if "data" in tracking_ads_data:
1315
+ for ad in tracking_ads_data.get("data", []):
1316
+ tracking_specs = ad.get("tracking_specs", [])
1317
+ if isinstance(tracking_specs, list):
1318
+ for spec in tracking_specs:
1319
+ if isinstance(spec, dict) and "page" in spec:
1320
+ page_list = spec["page"]
1321
+ if isinstance(page_list, list):
1322
+ for page_id in page_list:
1323
+ if isinstance(page_id, (str, int)) and str(page_id).isdigit():
1324
+ all_page_ids.add(str(page_id))
1325
+ except Exception:
1326
+ pass
1016
1327
 
1017
- if page_details["data"]:
1018
- return json.dumps(page_details, indent=2)
1019
-
1020
- # Approach 2: Try client_pages endpoint
1021
- endpoint = f"{account_id}/client_pages"
1022
- params = {
1023
- "fields": "id,name,username,category,fan_count,link,verification_status,picture"
1024
- }
1025
-
1026
- client_pages_data = await make_api_request(endpoint, access_token, params)
1027
-
1028
- if "data" in client_pages_data and client_pages_data["data"]:
1029
- return json.dumps(client_pages_data, indent=2)
1030
-
1031
- # Approach 3: Try promoted_objects endpoint to find page IDs
1032
- endpoint = f"{account_id}/promoted_objects"
1033
- params = {
1034
- "fields": "page_id"
1035
- }
1036
-
1037
- promoted_objects_data = await make_api_request(endpoint, access_token, params)
1038
-
1039
- if "data" in promoted_objects_data and promoted_objects_data["data"]:
1040
- page_ids = set()
1041
- for obj in promoted_objects_data["data"]:
1042
- if "page_id" in obj:
1043
- page_ids.add(obj["page_id"])
1328
+ # Approach 8: Try campaigns and extract page info
1329
+ try:
1330
+ endpoint = f"{account_id}/campaigns"
1331
+ params = {
1332
+ "fields": "id,name,promoted_object,objective",
1333
+ "limit": 50
1334
+ }
1335
+ campaigns_data = await make_api_request(endpoint, access_token, params)
1336
+ if "data" in campaigns_data:
1337
+ for campaign in campaigns_data["data"]:
1338
+ if "promoted_object" in campaign and "page_id" in campaign["promoted_object"]:
1339
+ all_page_ids.add(campaign["promoted_object"]["page_id"])
1340
+ except Exception:
1341
+ pass
1044
1342
 
1045
- if page_ids:
1046
- page_details = {"data": []}
1047
- for page_id in page_ids:
1343
+ # If we found any page IDs, get details for each
1344
+ if all_page_ids:
1345
+ page_details = {
1346
+ "data": [],
1347
+ "total_pages_found": len(all_page_ids)
1348
+ }
1349
+
1350
+ for page_id in all_page_ids:
1351
+ try:
1048
1352
  page_endpoint = f"{page_id}"
1049
1353
  page_params = {
1050
1354
  "fields": "id,name,username,category,fan_count,link,verification_status,picture"
@@ -1053,62 +1357,15 @@ async def get_account_pages(access_token: str = None, account_id: str = None) ->
1053
1357
  page_data = await make_api_request(page_endpoint, access_token, page_params)
1054
1358
  if "id" in page_data:
1055
1359
  page_details["data"].append(page_data)
1056
-
1057
- if page_details["data"]:
1058
- return json.dumps(page_details, indent=2)
1059
-
1060
- # Approach 4: Extract page IDs from tracking_specs in ads
1061
- # Inspired by praveen92y's implementation for robust page detection
1062
- # This approach is often the most reliable as confirmed by community feedback
1063
- endpoint = f"{account_id}/ads"
1064
- params = {
1065
- "fields": "id,name,adset_id,campaign_id,status,creative,created_time,updated_time,bid_amount,conversion_domain,tracking_specs",
1066
- "limit": 100
1067
- }
1068
-
1069
- tracking_ads_data = await make_api_request(endpoint, access_token, params)
1070
-
1071
- tracking_page_ids = set()
1072
- if "data" in tracking_ads_data:
1073
- for ad in tracking_ads_data.get("data", []):
1074
- tracking_specs = ad.get("tracking_specs", [])
1075
- if isinstance(tracking_specs, list):
1076
- for spec in tracking_specs:
1077
- # If 'page' key exists, add all page IDs
1078
- if isinstance(spec, dict) and "page" in spec:
1079
- page_list = spec["page"]
1080
- if isinstance(page_list, list):
1081
- for page_id in page_list:
1082
- # Validate page ID format (should be numeric string)
1083
- if isinstance(page_id, (str, int)) and str(page_id).isdigit():
1084
- tracking_page_ids.add(str(page_id))
1085
-
1086
- if tracking_page_ids:
1087
- page_details = {"data": [], "source": "tracking_specs", "note": "Page IDs extracted from active ads - these are the most reliable for ad creation"}
1088
- for page_id in tracking_page_ids:
1089
- page_endpoint = f"{page_id}"
1090
- page_params = {
1091
- "fields": "id,name,username,category,fan_count,link,verification_status,picture"
1092
- }
1093
-
1094
- page_data = await make_api_request(page_endpoint, access_token, page_params)
1095
- if "id" in page_data:
1096
- # Add additional context about this page ID being suitable for ads
1097
- page_data["_meta"] = {
1098
- "suitable_for_ads": True,
1099
- "found_in_tracking_specs": True,
1100
- "recommended_for_create_ad_creative": True
1101
- }
1102
- page_details["data"].append(page_data)
1103
- else:
1360
+ else:
1361
+ page_details["data"].append({
1362
+ "id": page_id,
1363
+ "error": "Page details not accessible"
1364
+ })
1365
+ except Exception as e:
1104
1366
  page_details["data"].append({
1105
- "id": page_id,
1106
- "error": "Page details not found",
1107
- "_meta": {
1108
- "suitable_for_ads": True,
1109
- "found_in_tracking_specs": True,
1110
- "note": "Page ID exists in ads but details not accessible - you can still use this ID for ad creation"
1111
- }
1367
+ "id": page_id,
1368
+ "error": f"Failed to get page details: {str(e)}"
1112
1369
  })
1113
1370
 
1114
1371
  if page_details["data"]:
@@ -1117,18 +1374,17 @@ async def get_account_pages(access_token: str = None, account_id: str = None) ->
1117
1374
  # If all approaches failed, return empty data with a message
1118
1375
  return json.dumps({
1119
1376
  "data": [],
1120
- "message": "No pages found associated with this account using automated methods",
1121
- "troubleshooting": {
1122
- "suggestion_1": "If you have existing ads, run 'get_ads' and look for page IDs in the 'tracking_specs' field",
1123
- "suggestion_2": "Use the exact page ID from existing ads' tracking_specs for creating new ad creatives",
1124
- "suggestion_3": "Verify your page ID format - it should be a numeric string (e.g., '123456789')",
1125
- "suggestion_4": "Check for digit transpositions or formatting errors in your page ID"
1126
- },
1127
- "note": "Based on community feedback, page IDs from existing ads' tracking_specs are the most reliable for ad creation"
1377
+ "message": "No pages found associated with this account",
1378
+ "suggestion": "Create a Facebook page and connect it to this ad account, or ensure existing pages are properly connected through Business Manager"
1128
1379
  }, indent=2)
1129
1380
 
1130
1381
  except Exception as e:
1131
1382
  return json.dumps({
1132
1383
  "error": "Failed to get account pages",
1133
1384
  "details": str(e)
1134
- }, indent=2)
1385
+ }, indent=2)
1386
+
1387
+
1388
+
1389
+
1390
+
meta_ads_mcp/core/api.py CHANGED
@@ -240,22 +240,43 @@ def meta_api_tool(func):
240
240
  logger.error("ISSUE DETECTED: Pipeboard authentication configured but no valid token available")
241
241
  logger.error("ACTION REQUIRED: Complete authentication via Pipeboard service")
242
242
 
243
- return json.dumps({
244
- "error": {
245
- "message": "Authentication Required",
246
- "details": {
247
- "description": "You need to authenticate with the Meta API before using this tool",
248
- "action_required": "Please authenticate first",
249
- "auth_url": auth_url,
250
- "configuration_status": {
251
- "app_id_configured": bool(app_id) and app_id != "YOUR_META_APP_ID",
252
- "pipeboard_enabled": bool(os.environ.get('PIPEBOARD_API_TOKEN')),
253
- },
254
- "troubleshooting": "Check logs for TOKEN VALIDATION FAILED messages",
255
- "markdown_link": f"[Click here to authenticate with Meta Ads API]({auth_url})"
243
+ # Provide different guidance based on authentication method
244
+ if using_pipeboard:
245
+ return json.dumps({
246
+ "error": {
247
+ "message": "Pipeboard Authentication Required",
248
+ "details": {
249
+ "description": "Your Pipeboard API token is invalid or has expired",
250
+ "action_required": "Update your Pipeboard token",
251
+ "setup_url": "https://pipeboard.co/setup",
252
+ "token_url": "https://pipeboard.co/api-tokens",
253
+ "configuration_status": {
254
+ "app_id_configured": bool(app_id) and app_id != "YOUR_META_APP_ID",
255
+ "pipeboard_enabled": True,
256
+ },
257
+ "troubleshooting": "Go to https://pipeboard.co/setup to verify your account setup, then visit https://pipeboard.co/api-tokens to obtain a new API token",
258
+ "setup_link": "[Verify your Pipeboard account setup](https://pipeboard.co/setup)",
259
+ "token_link": "[Get a new Pipeboard API token](https://pipeboard.co/api-tokens)"
260
+ }
261
+ }
262
+ }, indent=2)
263
+ else:
264
+ return json.dumps({
265
+ "error": {
266
+ "message": "Authentication Required",
267
+ "details": {
268
+ "description": "You need to authenticate with the Meta API before using this tool",
269
+ "action_required": "Please authenticate first",
270
+ "auth_url": auth_url,
271
+ "configuration_status": {
272
+ "app_id_configured": bool(app_id) and app_id != "YOUR_META_APP_ID",
273
+ "pipeboard_enabled": False,
274
+ },
275
+ "troubleshooting": "Check logs for TOKEN VALIDATION FAILED messages",
276
+ "markdown_link": f"[Click here to authenticate with Meta Ads API]({auth_url})"
277
+ }
256
278
  }
257
- }
258
- }, indent=2)
279
+ }, indent=2)
259
280
 
260
281
  # Call the original function
261
282
  result = await func(*args, **kwargs)
@@ -25,7 +25,29 @@ async def get_insights(access_token: str = None, object_id: str = None,
25
25
  last_3d, last_7d, last_14d, last_28d, last_30d, last_90d, last_week_mon_sun,
26
26
  last_week_sun_sat, last_quarter, last_year, this_week_mon_today, this_week_sun_today, this_year
27
27
  Dictionary example: {"since":"2023-01-01","until":"2023-01-31"}
28
- breakdown: Optional breakdown dimension (e.g., age, gender, country)
28
+ breakdown: Optional breakdown dimension. Valid values include:
29
+ Demographic: age, gender, country, region, dma
30
+ Platform/Device: device_platform, platform_position, publisher_platform, impression_device
31
+ Creative Assets: ad_format_asset, body_asset, call_to_action_asset, description_asset,
32
+ image_asset, link_url_asset, title_asset, video_asset, media_asset_url,
33
+ media_creator, media_destination_url, media_format, media_origin_url,
34
+ media_text_content, media_type, creative_relaxation_asset_type,
35
+ flexible_format_asset_type, gen_ai_asset_type
36
+ Campaign/Ad Attributes: breakdown_ad_objective, breakdown_reporting_ad_id, app_id, product_id
37
+ Conversion Tracking: coarse_conversion_value, conversion_destination, standard_event_content_type,
38
+ signal_source_bucket, is_conversion_id_modeled, fidelity_type, redownload
39
+ Time-based: hourly_stats_aggregated_by_advertiser_time_zone,
40
+ hourly_stats_aggregated_by_audience_time_zone, frequency_value
41
+ Extensions/Landing: ad_extension_domain, ad_extension_url, landing_destination,
42
+ mdsa_landing_destination
43
+ Attribution: sot_attribution_model_type, sot_attribution_window, sot_channel,
44
+ sot_event_type, sot_source
45
+ Mobile/SKAN: skan_campaign_id, skan_conversion_id, skan_version, postback_sequence_index
46
+ CRM/Business: crm_advertiser_l12_territory_ids, crm_advertiser_subvertical_id,
47
+ crm_advertiser_vertical_id, crm_ult_advertiser_id, user_persona_id, user_persona_name
48
+ Advanced: hsid, is_auto_advance, is_rendered_as_delayed_skip_ad, mmm, place_page_id,
49
+ marketing_messages_btn_name, impression_view_time_advertiser_hour_v2, comscore_market,
50
+ comscore_market_code
29
51
  level: Level of aggregation (ad, adset, campaign, account)
30
52
  """
31
53
  if not object_id:
@@ -77,6 +77,50 @@ class MetaAdsDataManager:
77
77
  logger.error(f"Error fetching ads for {account_id}: {e}")
78
78
  return []
79
79
 
80
+ async def _get_pages_for_account(self, access_token: str, account_id: str) -> List[Dict[str, Any]]:
81
+ """Get pages associated with an account"""
82
+ try:
83
+ # Import the page discovery function from ads module
84
+ from .ads import _discover_pages_for_account
85
+
86
+ # Ensure account_id has the 'act_' prefix
87
+ if not account_id.startswith("act_"):
88
+ account_id = f"act_{account_id}"
89
+
90
+ page_discovery_result = await _discover_pages_for_account(account_id, access_token)
91
+
92
+ if not page_discovery_result.get("success"):
93
+ return []
94
+
95
+ # Return page data in a consistent format
96
+ return [{
97
+ "id": page_discovery_result["page_id"],
98
+ "name": page_discovery_result.get("page_name", "Unknown"),
99
+ "source": page_discovery_result.get("source", "unknown"),
100
+ "account_id": account_id
101
+ }]
102
+ except Exception as e:
103
+ logger.error(f"Error fetching pages for {account_id}: {e}")
104
+ return []
105
+
106
+ async def _get_businesses(self, access_token: str, user_id: str = "me", limit: int = 25) -> List[Dict[str, Any]]:
107
+ """Get businesses accessible by the current user"""
108
+ try:
109
+ endpoint = f"{user_id}/businesses"
110
+ params = {
111
+ "fields": "id,name,created_time,verification_status",
112
+ "limit": limit
113
+ }
114
+
115
+ data = await make_api_request(endpoint, access_token, params)
116
+
117
+ if "data" in data:
118
+ return data["data"]
119
+ return []
120
+ except Exception as e:
121
+ logger.error(f"Error fetching businesses: {e}")
122
+ return []
123
+
80
124
  async def search_records(self, query: str, access_token: str) -> List[str]:
81
125
  """Search Meta Ads data and return matching record IDs
82
126
 
@@ -176,6 +220,60 @@ class MetaAdsDataManager:
176
220
  },
177
221
  "raw_data": ad
178
222
  }
223
+
224
+ # If query specifically mentions "page" or "pages", also search pages
225
+ if any(term in ['page', 'pages', 'facebook page'] for term in query_terms):
226
+ for account in accounts[:5]: # Limit to first 5 accounts for performance
227
+ pages = await self._get_pages_for_account(access_token, account['id'])
228
+ for page in pages:
229
+ page_text = f"{page.get('name', '')} {page.get('source', '')}".lower()
230
+
231
+ if any(term in page_text for term in query_terms):
232
+ page_record_id = f"page:{page['id']}"
233
+ matching_ids.append(page_record_id)
234
+
235
+ # Cache the page data
236
+ self._cache[page_record_id] = {
237
+ "id": page_record_id,
238
+ "type": "page",
239
+ "title": f"Facebook Page: {page.get('name', 'Unnamed Page')}",
240
+ "text": f"Facebook Page {page.get('name', 'Unnamed')} (ID: {page.get('id', 'N/A')}) - Source: {page.get('source', 'Unknown')}, Account: {account.get('name', 'Unknown')}",
241
+ "metadata": {
242
+ "page_id": page.get('id'),
243
+ "page_name": page.get('name'),
244
+ "source": page.get('source'),
245
+ "account_id": account.get('id'),
246
+ "account_name": account.get('name'),
247
+ "data_type": "meta_ads_page"
248
+ },
249
+ "raw_data": page
250
+ }
251
+
252
+ # If query specifically mentions "business" or "businesses", also search businesses
253
+ if any(term in ['business', 'businesses', 'company', 'companies'] for term in query_terms):
254
+ businesses = await self._get_businesses(access_token, limit=25)
255
+ for business in businesses:
256
+ business_text = f"{business.get('name', '')} {business.get('verification_status', '')}".lower()
257
+
258
+ if any(term in business_text for term in query_terms):
259
+ business_record_id = f"business:{business['id']}"
260
+ matching_ids.append(business_record_id)
261
+
262
+ # Cache the business data
263
+ self._cache[business_record_id] = {
264
+ "id": business_record_id,
265
+ "type": "business",
266
+ "title": f"Business: {business.get('name', 'Unnamed Business')}",
267
+ "text": f"Meta Business {business.get('name', 'Unnamed')} (ID: {business.get('id', 'N/A')}) - Created: {business.get('created_time', 'Unknown')}, Verification: {business.get('verification_status', 'Unknown')}",
268
+ "metadata": {
269
+ "business_id": business.get('id'),
270
+ "business_name": business.get('name'),
271
+ "created_time": business.get('created_time'),
272
+ "verification_status": business.get('verification_status'),
273
+ "data_type": "meta_ads_business"
274
+ },
275
+ "raw_data": business
276
+ }
179
277
 
180
278
  except Exception as e:
181
279
  logger.error(f"Error during search operation: {e}")
@@ -219,7 +317,7 @@ async def search(
219
317
  Search through Meta Ads data and return matching record IDs.
220
318
 
221
319
  This tool is required for OpenAI ChatGPT Deep Research integration.
222
- It searches across ad accounts, campaigns, and ads to find relevant records
320
+ It searches across ad accounts, campaigns, ads, pages, and businesses to find relevant records
223
321
  based on the provided query.
224
322
 
225
323
  Args:
@@ -233,6 +331,8 @@ async def search(
233
331
  search(query="active campaigns")
234
332
  search(query="account spending")
235
333
  search(query="facebook ads performance")
334
+ search(query="facebook pages")
335
+ search(query="user businesses")
236
336
  """
237
337
  if not query:
238
338
  return json.dumps({
@@ -285,6 +385,7 @@ async def fetch(
285
385
  fetch(id="account:act_123456789")
286
386
  fetch(id="campaign:23842588888640185")
287
387
  fetch(id="ad:23842614006130185")
388
+ fetch(id="page:123456789")
288
389
  """
289
390
  if not id:
290
391
  return json.dumps({
@@ -120,7 +120,7 @@ class PipeboardAuthManager:
120
120
  else:
121
121
  logger.info("Pipeboard authentication not enabled. Set PIPEBOARD_API_TOKEN environment variable to enable.")
122
122
  self.token_info = None
123
- self._load_cached_token()
123
+ # Note: Token caching is disabled to always fetch fresh tokens from Pipeboard
124
124
 
125
125
  def _get_token_cache_path(self) -> Path:
126
126
  """Get the platform-specific path for token cache file"""
@@ -320,18 +320,7 @@ class PipeboardAuthManager:
320
320
  logger.error("Please set PIPEBOARD_API_TOKEN environment variable")
321
321
  return None
322
322
 
323
- # Check if we already have a valid token
324
- if not force_refresh and self.token_info and not self.token_info.is_expired():
325
- logger.debug("Using existing valid token")
326
- return self.token_info.access_token
327
-
328
- # If we have a token but it's expired, log that information
329
- if not force_refresh and self.token_info and self.token_info.is_expired():
330
- logger.error("TOKEN VALIDATION FAILED: Existing token is expired")
331
- if self.token_info.expires_at:
332
- logger.error(f"Token expiration time: {self.token_info.expires_at}")
333
-
334
- logger.info(f"Getting new token (force_refresh={force_refresh})")
323
+ logger.info("Getting fresh token from Pipeboard (caching disabled)")
335
324
 
336
325
  # If force refresh or no token/expired token, get a new one from Pipeboard
337
326
  try:
@@ -398,8 +387,7 @@ class PipeboardAuthManager:
398
387
  token_type=data.get("token_type", "bearer")
399
388
  )
400
389
 
401
- # Save to cache
402
- self._save_token_to_cache()
390
+ # Note: Token caching is disabled
403
391
 
404
392
  masked_token = self.token_info.access_token[:10] + "..." + self.token_info.access_token[-5:] if self.token_info.access_token else "None"
405
393
  logger.info(f"Successfully retrieved access token: {masked_token}")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meta-ads-mcp
3
- Version: 0.7.10
3
+ Version: 0.9.0
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
@@ -111,9 +111,11 @@ For detailed step-by-step instructions, authentication setup, debugging, and tro
111
111
  - **Automated Monitoring**: Ask any MCP-compatible LLM to track performance metrics and alert you about significant changes
112
112
  - **Budget Optimization**: Get recommendations for reallocating budget to better-performing ad sets
113
113
  - **Creative Improvement**: Receive feedback on ad copy, imagery, and calls-to-action
114
+ - **Dynamic Creative Testing**: Easy API for both simple ads (single headline/description) and advanced A/B testing (multiple headlines/descriptions)
114
115
  - **Campaign Management**: Request changes to campaigns, ad sets, and ads (all changes require explicit confirmation)
115
116
  - **Cross-Platform Integration**: Works with Facebook, Instagram, and all Meta ad platforms
116
117
  - **Universal LLM Support**: Compatible with any MCP client including Claude Desktop, Cursor, Cherry Studio, and more
118
+ - **Enhanced Search**: Generic search function includes page searching when queries mention "page" or "pages"
117
119
  - **Simple Authentication**: Easy setup with secure OAuth authentication
118
120
  - **Cross-Platform Support**: Works on Windows, macOS, and Linux
119
121
 
@@ -260,14 +262,32 @@ For local installation configuration, authentication options, and advanced techn
260
262
  - `page_id`: Facebook Page ID for the ad
261
263
  - `link_url`: Destination URL
262
264
  - `message`: Ad copy/text
263
- - `headline`: Ad headline
264
- - `description`: Ad description
265
+ - `headline`: Single headline for simple ads (cannot be used with headlines)
266
+ - `headlines`: List of headlines for dynamic creative testing (cannot be used with headline)
267
+ - `description`: Single description for simple ads (cannot be used with descriptions)
268
+ - `descriptions`: List of descriptions for dynamic creative testing (cannot be used with description)
269
+ - `dynamic_creative_spec`: Dynamic creative optimization settings
265
270
  - `call_to_action_type`: CTA button type (e.g., 'LEARN_MORE')
266
271
  - `instagram_actor_id`: Optional Instagram account ID
267
272
  - `access_token` (optional): Meta API access token
268
273
  - Returns: Confirmation with new creative details
269
274
 
270
- 15. `mcp_meta_ads_upload_ad_image`
275
+ 15. `mcp_meta_ads_update_ad_creative`
276
+ - Update an existing ad creative with new content or settings
277
+ - Inputs:
278
+ - `creative_id`: Meta Ads creative ID to update
279
+ - `name`: New creative name
280
+ - `message`: New ad copy/text
281
+ - `headline`: Single headline for simple ads (cannot be used with headlines)
282
+ - `headlines`: New list of headlines for dynamic creative testing (cannot be used with headline)
283
+ - `description`: Single description for simple ads (cannot be used with descriptions)
284
+ - `descriptions`: New list of descriptions for dynamic creative testing (cannot be used with description)
285
+ - `dynamic_creative_spec`: New dynamic creative optimization settings
286
+ - `call_to_action_type`: New call to action button type
287
+ - `access_token` (optional): Meta API access token (will use cached token if not provided)
288
+ - Returns: Confirmation with updated creative details
289
+
290
+ 16. `mcp_meta_ads_upload_ad_image`
271
291
  - Upload an image to use in Meta Ads creatives
272
292
  - Inputs:
273
293
  - `account_id`: Meta Ads account ID (format: act_XXXXXXXXX)
@@ -276,14 +296,14 @@ For local installation configuration, authentication options, and advanced techn
276
296
  - `access_token` (optional): Meta API access token
277
297
  - Returns: JSON response with image details including hash
278
298
 
279
- 16. `mcp_meta_ads_get_ad_image`
299
+ 17. `mcp_meta_ads_get_ad_image`
280
300
  - Get, download, and visualize a Meta ad image in one step
281
301
  - Inputs:
282
302
  - `access_token` (optional): Meta API access token (will use cached token if not provided)
283
303
  - `ad_id`: Meta Ads ad ID
284
304
  - Returns: The ad image ready for direct visual analysis
285
305
 
286
- 17. `mcp_meta_ads_update_ad`
306
+ 18. `mcp_meta_ads_update_ad`
287
307
  - Update an ad with new settings
288
308
  - Inputs:
289
309
  - `ad_id`: Meta Ads ad ID
@@ -292,7 +312,7 @@ For local installation configuration, authentication options, and advanced techn
292
312
  - `access_token` (optional): Meta API access token (will use cached token if not provided)
293
313
  - Returns: Confirmation with updated ad details and a confirmation link
294
314
 
295
- 18. `mcp_meta_ads_update_adset`
315
+ 19. `mcp_meta_ads_update_adset`
296
316
  - Update an ad set with new settings including frequency caps
297
317
  - Inputs:
298
318
  - `adset_id`: Meta Ads ad set ID
@@ -304,7 +324,7 @@ For local installation configuration, authentication options, and advanced techn
304
324
  - `access_token` (optional): Meta API access token (will use cached token if not provided)
305
325
  - Returns: Confirmation with updated ad set details and a confirmation link
306
326
 
307
- 19. `mcp_meta_ads_get_insights`
327
+ 20. `mcp_meta_ads_get_insights`
308
328
  - Get performance insights for a campaign, ad set, ad or account
309
329
  - Inputs:
310
330
  - `access_token` (optional): Meta API access token (will use cached token if not provided)
@@ -314,13 +334,13 @@ For local installation configuration, authentication options, and advanced techn
314
334
  - `level`: Level of aggregation (ad, adset, campaign, account)
315
335
  - Returns: Performance metrics for the specified object
316
336
 
317
- 20. `mcp_meta_ads_get_login_link`
337
+ 21. `mcp_meta_ads_get_login_link`
318
338
  - Get a clickable login link for Meta Ads authentication
319
339
  - Inputs:
320
340
  - `access_token` (optional): Meta API access token (will use cached token if not provided)
321
341
  - Returns: A clickable resource link for Meta authentication
322
342
 
323
- 21. `mcp_meta-ads_create_budget_schedule`
343
+ 22. `mcp_meta-ads_create_budget_schedule`
324
344
  - Create a budget schedule for a Meta Ads campaign.
325
345
  - Inputs:
326
346
  - `campaign_id`: Meta Ads campaign ID.
@@ -331,7 +351,7 @@ For local installation configuration, authentication options, and advanced techn
331
351
  - `access_token` (optional): Meta API access token.
332
352
  - Returns: JSON string with the ID of the created budget schedule or an error message.
333
353
 
334
- 22. `mcp_meta_ads_search_interests`
354
+ 23. `mcp_meta_ads_search_interests`
335
355
  - Search for interest targeting options by keyword
336
356
  - Inputs:
337
357
  - `access_token` (optional): Meta API access token (will use cached token if not provided)
@@ -339,7 +359,7 @@ For local installation configuration, authentication options, and advanced techn
339
359
  - `limit`: Maximum number of results to return (default: 25)
340
360
  - Returns: Interest data with id, name, audience_size, and path fields
341
361
 
342
- 23. `mcp_meta_ads_get_interest_suggestions`
362
+ 24. `mcp_meta_ads_get_interest_suggestions`
343
363
  - Get interest suggestions based on existing interests
344
364
  - Inputs:
345
365
  - `access_token` (optional): Meta API access token (will use cached token if not provided)
@@ -379,6 +399,14 @@ For local installation configuration, authentication options, and advanced techn
379
399
  - `limit`: Maximum number of results to return (default: 25)
380
400
  - Returns: Location data with key, name, type, and geographic hierarchy information
381
401
 
402
+ 28. `mcp_meta_ads_search` (Enhanced)
403
+ - Generic search across accounts, campaigns, ads, and pages
404
+ - Automatically includes page searching when query mentions "page" or "pages"
405
+ - Inputs:
406
+ - `access_token` (optional): Meta API access token (will use cached token if not provided)
407
+ - `query`: Search query string (e.g., "Injury Payouts pages", "active campaigns")
408
+ - Returns: List of matching record IDs in ChatGPT-compatible format
409
+
382
410
  ## Privacy and Security
383
411
 
384
412
  Meta Ads MCP follows security best practices with secure token management and automatic authentication handling.
@@ -1,11 +1,11 @@
1
- meta_ads_mcp/__init__.py,sha256=Y0Oj0zMOmh2TX_E4iBNehH9NFlM1w9O3injKT8FJMbw,1493
1
+ meta_ads_mcp/__init__.py,sha256=HV-_j707lfnxzSKt6hrk2hD1PPP3y6Rfp7rlwCJBDFQ,1492
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=lUq5te8SDFHS2gn8IlwVa0svyFyqJBMvWtuxy1hacFI,44428
5
+ meta_ads_mcp/core/ads.py,sha256=3UpoktML-5hJ_k2u5KOk3-VcFe_goz531jLi6-qJhqU,54929
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
- meta_ads_mcp/core/api.py,sha256=aAzM6Q75VQOFXtr5D-mDmBRhxWK4wsiODsJYnR3mpDI,14994
8
+ meta_ads_mcp/core/api.py,sha256=kWpIafvSsxnesfb5TqndA7ozKoIspby5e_6Jl23L7hY,16447
9
9
  meta_ads_mcp/core/auth.py,sha256=2CjFbxpJM3OR3OzCipB8l_-l2xQ1nioGfdI3ZDMnjHM,23629
10
10
  meta_ads_mcp/core/authentication.py,sha256=-AJxa3a5ZshRCvmJThBaNwCAJ1D2_qOgUkvu539c_MY,10159
11
11
  meta_ads_mcp/core/budget_schedules.py,sha256=UxseExsvKAiPwfDCY9aycT4kys4xqeNytyq-yyDOxrs,2901
@@ -13,16 +13,16 @@ meta_ads_mcp/core/callback_server.py,sha256=LIAJv9DW--83kdZ7VWWZal8xEprYjRZ8iug4
13
13
  meta_ads_mcp/core/campaigns.py,sha256=0yDVgi7rN4eMQk1_w0A2vnoXd8y0t8R77Ji4gna1Gj4,14030
14
14
  meta_ads_mcp/core/duplication.py,sha256=UUmTDFx9o5ZsPQG2Rb9c4ZyuKUVN3FfTjebfTIHHdo4,18984
15
15
  meta_ads_mcp/core/http_auth_integration.py,sha256=lGpKhfzJcyWugBcYEvypY-qnlt-3UDBLqh7xAUH0DGw,12473
16
- meta_ads_mcp/core/insights.py,sha256=Qr1wq-1VT9HwF4w11rIRM4IBYdrksJ-6EOv3p33ZtKw,2613
17
- meta_ads_mcp/core/openai_deep_research.py,sha256=Ocs8bmNNBLZQLmWfL6azlC3RNzevVzV5WgcEp4H2wdY,13240
18
- meta_ads_mcp/core/pipeboard_auth.py,sha256=yT2-e9dpmkMOg6rMJWaQAMg4DZu4RxlDV5zkTm-G--o,25168
16
+ meta_ads_mcp/core/insights.py,sha256=unhcCaYjgsir62llCdIDg0F-PHISiHune08uYG5IXTM,4707
17
+ meta_ads_mcp/core/openai_deep_research.py,sha256=gLQlzIUBd-VyBMYjJ4vchMadQ-4aT3PEMqd9GLq6MYw,18805
18
+ meta_ads_mcp/core/pipeboard_auth.py,sha256=ZwEQy8r0TwobFRQ5gmlSjhIfvlUmMtfWNlpQjXCUhl0,24582
19
19
  meta_ads_mcp/core/reports.py,sha256=Dv3hfsPOR7IZ9WrYrKd_6SNgZl-USIphg7knva3UYAw,5747
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
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,,
24
+ meta_ads_mcp-0.9.0.dist-info/METADATA,sha256=SHl5v6vr5zhzMmsITPis8sgIGXyCOWCBRI66tdcJQ7A,22434
25
+ meta_ads_mcp-0.9.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
26
+ meta_ads_mcp-0.9.0.dist-info/entry_points.txt,sha256=Dv2RkoBjRJBqj6CyhwqGIiwPCD-SCL1-7B9-zmVRuv0,57
27
+ meta_ads_mcp-0.9.0.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
28
+ meta_ads_mcp-0.9.0.dist-info/RECORD,,