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 +1 -1
- meta_ads_mcp/core/ads.py +396 -140
- meta_ads_mcp/core/api.py +36 -15
- meta_ads_mcp/core/insights.py +23 -1
- meta_ads_mcp/core/openai_deep_research.py +102 -1
- meta_ads_mcp/core/pipeboard_auth.py +3 -15
- {meta_ads_mcp-0.7.10.dist-info → meta_ads_mcp-0.9.0.dist-info}/METADATA +40 -12
- {meta_ads_mcp-0.7.10.dist-info → meta_ads_mcp-0.9.0.dist-info}/RECORD +11 -11
- {meta_ads_mcp-0.7.10.dist-info → meta_ads_mcp-0.9.0.dist-info}/WHEEL +0 -0
- {meta_ads_mcp-0.7.10.dist-info → meta_ads_mcp-0.9.0.dist-info}/entry_points.txt +0 -0
- {meta_ads_mcp-0.7.10.dist-info → meta_ads_mcp-0.9.0.dist-info}/licenses/LICENSE +0 -0
meta_ads_mcp/__init__.py
CHANGED
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
|
|
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
|
-
|
|
551
|
-
|
|
552
|
-
|
|
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:
|
|
652
|
-
|
|
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
|
-
|
|
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
|
-
|
|
720
|
-
|
|
795
|
+
# Add optional parameters if provided
|
|
796
|
+
if message:
|
|
797
|
+
creative_data["object_story_spec"]["link_data"]["message"] = message
|
|
721
798
|
|
|
722
|
-
if
|
|
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
|
-
#
|
|
1215
|
+
# Collect all page IDs from multiple approaches
|
|
1216
|
+
all_page_ids = set()
|
|
986
1217
|
|
|
987
|
-
# Approach 1: Get
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
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
|
-
#
|
|
1004
|
-
|
|
1005
|
-
|
|
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
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
}
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
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
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
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
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
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
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
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": "
|
|
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
|
|
1121
|
-
"
|
|
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
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
"
|
|
247
|
-
"
|
|
248
|
-
"
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
"
|
|
252
|
-
"
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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)
|
meta_ads_mcp/core/insights.py
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
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.
|
|
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`:
|
|
264
|
-
- `
|
|
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. `
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
17
|
-
meta_ads_mcp/core/openai_deep_research.py,sha256=
|
|
18
|
-
meta_ads_mcp/core/pipeboard_auth.py,sha256=
|
|
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.
|
|
25
|
-
meta_ads_mcp-0.
|
|
26
|
-
meta_ads_mcp-0.
|
|
27
|
-
meta_ads_mcp-0.
|
|
28
|
-
meta_ads_mcp-0.
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|