meta-ads-mcp 0.3.2__py3-none-any.whl → 0.3.3__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/__init__.py +2 -0
- meta_ads_mcp/core/ads.py +463 -1
- meta_ads_mcp/core/ads_library.py +69 -0
- meta_ads_mcp/core/campaigns.py +5 -2
- {meta_ads_mcp-0.3.2.dist-info → meta_ads_mcp-0.3.3.dist-info}/METADATA +27 -20
- {meta_ads_mcp-0.3.2.dist-info → meta_ads_mcp-0.3.3.dist-info}/RECORD +10 -9
- {meta_ads_mcp-0.3.2.dist-info → meta_ads_mcp-0.3.3.dist-info}/WHEEL +0 -0
- {meta_ads_mcp-0.3.2.dist-info → meta_ads_mcp-0.3.3.dist-info}/entry_points.txt +0 -0
- {meta_ads_mcp-0.3.2.dist-info → meta_ads_mcp-0.3.3.dist-info}/licenses/LICENSE +0 -0
meta_ads_mcp/__init__.py
CHANGED
meta_ads_mcp/core/__init__.py
CHANGED
|
@@ -9,6 +9,7 @@ from .insights import get_insights, debug_image_download
|
|
|
9
9
|
from .authentication import get_login_link
|
|
10
10
|
from .server import login_cli, main
|
|
11
11
|
from .auth import login
|
|
12
|
+
from .ads_library import search_ads_archive
|
|
12
13
|
|
|
13
14
|
__all__ = [
|
|
14
15
|
'mcp_server',
|
|
@@ -31,4 +32,5 @@ __all__ = [
|
|
|
31
32
|
'login_cli',
|
|
32
33
|
'login',
|
|
33
34
|
'main',
|
|
35
|
+
'search_ads_archive',
|
|
34
36
|
]
|
meta_ads_mcp/core/ads.py
CHANGED
|
@@ -5,6 +5,8 @@ from typing import Optional, Dict, Any, List
|
|
|
5
5
|
import io
|
|
6
6
|
from PIL import Image as PILImage
|
|
7
7
|
from mcp.server.fastmcp import Image
|
|
8
|
+
import os
|
|
9
|
+
import time
|
|
8
10
|
|
|
9
11
|
from .api import meta_api_tool, make_api_request
|
|
10
12
|
from .accounts import get_ad_accounts
|
|
@@ -317,6 +319,132 @@ async def get_ad_image(access_token: str = None, ad_id: str = None) -> Image:
|
|
|
317
319
|
return f"Error processing image: {str(e)}"
|
|
318
320
|
|
|
319
321
|
|
|
322
|
+
@mcp_server.tool()
|
|
323
|
+
@meta_api_tool
|
|
324
|
+
async def save_ad_image_locally(access_token: str = None, ad_id: str = None, output_dir: str = "ad_images") -> str:
|
|
325
|
+
"""
|
|
326
|
+
Get, download, and save a Meta ad image locally, returning the file path.
|
|
327
|
+
|
|
328
|
+
Args:
|
|
329
|
+
access_token: Meta API access token (optional - will use cached token if not provided)
|
|
330
|
+
ad_id: Meta Ads ad ID
|
|
331
|
+
output_dir: Directory to save the image file (default: 'ad_images')
|
|
332
|
+
|
|
333
|
+
Returns:
|
|
334
|
+
The file path to the saved image, or an error message string.
|
|
335
|
+
"""
|
|
336
|
+
if not ad_id:
|
|
337
|
+
return json.dumps({"error": "No ad ID provided"}, indent=2)
|
|
338
|
+
|
|
339
|
+
print(f"Attempting to get and save creative image for ad {ad_id}")
|
|
340
|
+
|
|
341
|
+
# First, get creative and account IDs
|
|
342
|
+
ad_endpoint = f"{ad_id}"
|
|
343
|
+
ad_params = {
|
|
344
|
+
"fields": "creative{id},account_id"
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
ad_data = await make_api_request(ad_endpoint, access_token, ad_params)
|
|
348
|
+
|
|
349
|
+
if "error" in ad_data:
|
|
350
|
+
return json.dumps({"error": f"Could not get ad data - {json.dumps(ad_data)}"}, indent=2)
|
|
351
|
+
|
|
352
|
+
account_id = ad_data.get("account_id")
|
|
353
|
+
if not account_id:
|
|
354
|
+
return json.dumps({"error": "No account ID found for ad"}, indent=2)
|
|
355
|
+
|
|
356
|
+
if "creative" not in ad_data:
|
|
357
|
+
return json.dumps({"error": "No creative found for this ad"}, indent=2)
|
|
358
|
+
|
|
359
|
+
creative_data = ad_data.get("creative", {})
|
|
360
|
+
creative_id = creative_data.get("id")
|
|
361
|
+
if not creative_id:
|
|
362
|
+
return json.dumps({"error": "No creative ID found"}, indent=2)
|
|
363
|
+
|
|
364
|
+
# Get creative details to find image hash
|
|
365
|
+
creative_endpoint = f"{creative_id}"
|
|
366
|
+
creative_params = {
|
|
367
|
+
"fields": "id,name,image_hash,asset_feed_spec"
|
|
368
|
+
}
|
|
369
|
+
creative_details = await make_api_request(creative_endpoint, access_token, creative_params)
|
|
370
|
+
|
|
371
|
+
image_hashes = []
|
|
372
|
+
if "image_hash" in creative_details:
|
|
373
|
+
image_hashes.append(creative_details["image_hash"])
|
|
374
|
+
if "asset_feed_spec" in creative_details and "images" in creative_details["asset_feed_spec"]:
|
|
375
|
+
for image in creative_details["asset_feed_spec"]["images"]:
|
|
376
|
+
if "hash" in image:
|
|
377
|
+
image_hashes.append(image["hash"])
|
|
378
|
+
|
|
379
|
+
if not image_hashes:
|
|
380
|
+
# Fallback attempt (as in get_ad_image)
|
|
381
|
+
creative_json = await get_ad_creatives(ad_id=ad_id, access_token=access_token) # Ensure ad_id is passed correctly
|
|
382
|
+
creative_data_list = json.loads(creative_json)
|
|
383
|
+
if 'data' in creative_data_list and creative_data_list['data']:
|
|
384
|
+
first_creative = creative_data_list['data'][0]
|
|
385
|
+
if 'object_story_spec' in first_creative and 'link_data' in first_creative['object_story_spec'] and 'image_hash' in first_creative['object_story_spec']['link_data']:
|
|
386
|
+
image_hashes.append(first_creative['object_story_spec']['link_data']['image_hash'])
|
|
387
|
+
elif 'image_hash' in first_creative: # Check direct hash on creative data
|
|
388
|
+
image_hashes.append(first_creative['image_hash'])
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
if not image_hashes:
|
|
392
|
+
return json.dumps({"error": "No image hashes found in creative or fallback"}, indent=2)
|
|
393
|
+
|
|
394
|
+
print(f"Found image hashes: {image_hashes}")
|
|
395
|
+
|
|
396
|
+
# Fetch image data using the first hash
|
|
397
|
+
image_endpoint = f"act_{account_id}/adimages"
|
|
398
|
+
hashes_str = f'["{image_hashes[0]}"]'
|
|
399
|
+
image_params = {
|
|
400
|
+
"fields": "hash,url,width,height,name,status",
|
|
401
|
+
"hashes": hashes_str
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
print(f"Requesting image data with params: {image_params}")
|
|
405
|
+
image_data = await make_api_request(image_endpoint, access_token, image_params)
|
|
406
|
+
|
|
407
|
+
if "error" in image_data:
|
|
408
|
+
return json.dumps({"error": f"Failed to get image data - {json.dumps(image_data)}"}, indent=2)
|
|
409
|
+
|
|
410
|
+
if "data" not in image_data or not image_data["data"]:
|
|
411
|
+
return json.dumps({"error": "No image data returned from API"}, indent=2)
|
|
412
|
+
|
|
413
|
+
first_image = image_data["data"][0]
|
|
414
|
+
image_url = first_image.get("url")
|
|
415
|
+
|
|
416
|
+
if not image_url:
|
|
417
|
+
return json.dumps({"error": "No valid image URL found in API response"}, indent=2)
|
|
418
|
+
|
|
419
|
+
print(f"Downloading image from URL: {image_url}")
|
|
420
|
+
|
|
421
|
+
# Download and Save Image
|
|
422
|
+
image_bytes = await download_image(image_url)
|
|
423
|
+
|
|
424
|
+
if not image_bytes:
|
|
425
|
+
return json.dumps({"error": "Failed to download image"}, indent=2)
|
|
426
|
+
|
|
427
|
+
try:
|
|
428
|
+
# Ensure output directory exists
|
|
429
|
+
if not os.path.exists(output_dir):
|
|
430
|
+
os.makedirs(output_dir)
|
|
431
|
+
|
|
432
|
+
# Create a filename (e.g., using ad_id and image hash)
|
|
433
|
+
file_extension = ".jpg" # Default extension, could try to infer from headers later
|
|
434
|
+
filename = f"{ad_id}_{image_hashes[0]}{file_extension}"
|
|
435
|
+
filepath = os.path.join(output_dir, filename)
|
|
436
|
+
|
|
437
|
+
# Save the image bytes to the file
|
|
438
|
+
with open(filepath, "wb") as f:
|
|
439
|
+
f.write(image_bytes)
|
|
440
|
+
|
|
441
|
+
print(f"Image saved successfully to: {filepath}")
|
|
442
|
+
return json.dumps({"filepath": filepath}, indent=2) # Return JSON with filepath
|
|
443
|
+
|
|
444
|
+
except Exception as e:
|
|
445
|
+
return json.dumps({"error": f"Failed to save image: {str(e)}"}, indent=2)
|
|
446
|
+
|
|
447
|
+
|
|
320
448
|
@mcp_server.tool()
|
|
321
449
|
@meta_api_tool
|
|
322
450
|
async def update_ad(
|
|
@@ -354,4 +482,338 @@ async def update_ad(
|
|
|
354
482
|
endpoint = f"{ad_id}"
|
|
355
483
|
data = await make_api_request(endpoint, access_token, params, method='POST')
|
|
356
484
|
|
|
357
|
-
return json.dumps(data, indent=2)
|
|
485
|
+
return json.dumps(data, indent=2)
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
@mcp_server.tool()
|
|
489
|
+
@meta_api_tool
|
|
490
|
+
async def upload_ad_image(
|
|
491
|
+
access_token: str = None,
|
|
492
|
+
account_id: str = None,
|
|
493
|
+
image_path: str = None,
|
|
494
|
+
name: str = None
|
|
495
|
+
) -> str:
|
|
496
|
+
"""
|
|
497
|
+
Upload an image to use in Meta Ads creatives.
|
|
498
|
+
|
|
499
|
+
Args:
|
|
500
|
+
access_token: Meta API access token (optional - will use cached token if not provided)
|
|
501
|
+
account_id: Meta Ads account ID (format: act_XXXXXXXXX)
|
|
502
|
+
image_path: Path to the image file to upload
|
|
503
|
+
name: Optional name for the image (default: filename)
|
|
504
|
+
|
|
505
|
+
Returns:
|
|
506
|
+
JSON response with image details including hash for creative creation
|
|
507
|
+
"""
|
|
508
|
+
# Check required parameters
|
|
509
|
+
if not account_id:
|
|
510
|
+
return json.dumps({"error": "No account ID provided"}, indent=2)
|
|
511
|
+
|
|
512
|
+
if not image_path:
|
|
513
|
+
return json.dumps({"error": "No image path provided"}, indent=2)
|
|
514
|
+
|
|
515
|
+
# Ensure account_id has the 'act_' prefix for API compatibility
|
|
516
|
+
if not account_id.startswith("act_"):
|
|
517
|
+
account_id = f"act_{account_id}"
|
|
518
|
+
|
|
519
|
+
# Check if image file exists
|
|
520
|
+
if not os.path.exists(image_path):
|
|
521
|
+
return json.dumps({"error": f"Image file not found: {image_path}"}, indent=2)
|
|
522
|
+
|
|
523
|
+
try:
|
|
524
|
+
# Read image file
|
|
525
|
+
with open(image_path, "rb") as img_file:
|
|
526
|
+
image_bytes = img_file.read()
|
|
527
|
+
|
|
528
|
+
# Get image filename if name not provided
|
|
529
|
+
if not name:
|
|
530
|
+
name = os.path.basename(image_path)
|
|
531
|
+
|
|
532
|
+
# Prepare the API endpoint for uploading images
|
|
533
|
+
endpoint = f"{account_id}/adimages"
|
|
534
|
+
|
|
535
|
+
# We need to convert the binary data to base64 for API upload
|
|
536
|
+
import base64
|
|
537
|
+
encoded_image = base64.b64encode(image_bytes).decode('utf-8')
|
|
538
|
+
|
|
539
|
+
# Prepare POST parameters
|
|
540
|
+
params = {
|
|
541
|
+
"bytes": encoded_image,
|
|
542
|
+
"name": name
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
# Make API request to upload the image
|
|
546
|
+
print(f"Uploading image to Facebook Ad Account {account_id}")
|
|
547
|
+
data = await make_api_request(endpoint, access_token, params, method="POST")
|
|
548
|
+
|
|
549
|
+
return json.dumps(data, indent=2)
|
|
550
|
+
|
|
551
|
+
except Exception as e:
|
|
552
|
+
return json.dumps({
|
|
553
|
+
"error": "Failed to upload image",
|
|
554
|
+
"details": str(e)
|
|
555
|
+
}, indent=2)
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
@mcp_server.tool()
|
|
559
|
+
@meta_api_tool
|
|
560
|
+
async def create_ad_creative(
|
|
561
|
+
access_token: str = None,
|
|
562
|
+
account_id: str = None,
|
|
563
|
+
name: str = None,
|
|
564
|
+
image_hash: str = None,
|
|
565
|
+
page_id: str = None,
|
|
566
|
+
link_url: str = None,
|
|
567
|
+
message: str = None,
|
|
568
|
+
headline: str = None,
|
|
569
|
+
description: str = None,
|
|
570
|
+
call_to_action_type: str = None,
|
|
571
|
+
instagram_actor_id: str = None
|
|
572
|
+
) -> str:
|
|
573
|
+
"""
|
|
574
|
+
Create a new ad creative using an uploaded image hash.
|
|
575
|
+
|
|
576
|
+
Args:
|
|
577
|
+
access_token: Meta API access token (optional - will use cached token if not provided)
|
|
578
|
+
account_id: Meta Ads account ID (format: act_XXXXXXXXX)
|
|
579
|
+
name: Creative name
|
|
580
|
+
image_hash: Hash of the uploaded image
|
|
581
|
+
page_id: Facebook Page ID to be used for the ad
|
|
582
|
+
link_url: Destination URL for the ad
|
|
583
|
+
message: Ad copy/text
|
|
584
|
+
headline: Ad headline
|
|
585
|
+
description: Ad description
|
|
586
|
+
call_to_action_type: Call to action button type (e.g., 'LEARN_MORE', 'SIGN_UP', 'SHOP_NOW')
|
|
587
|
+
instagram_actor_id: Optional Instagram account ID for Instagram placements
|
|
588
|
+
|
|
589
|
+
Returns:
|
|
590
|
+
JSON response with created creative details
|
|
591
|
+
"""
|
|
592
|
+
# Check required parameters
|
|
593
|
+
if not account_id:
|
|
594
|
+
return json.dumps({"error": "No account ID provided"}, indent=2)
|
|
595
|
+
|
|
596
|
+
if not image_hash:
|
|
597
|
+
return json.dumps({"error": "No image hash provided"}, indent=2)
|
|
598
|
+
|
|
599
|
+
if not name:
|
|
600
|
+
name = f"Creative {int(time.time())}"
|
|
601
|
+
|
|
602
|
+
# Ensure account_id has the 'act_' prefix
|
|
603
|
+
if not account_id.startswith("act_"):
|
|
604
|
+
account_id = f"act_{account_id}"
|
|
605
|
+
|
|
606
|
+
# If no page ID is provided, try to find a page associated with the account
|
|
607
|
+
if not page_id:
|
|
608
|
+
try:
|
|
609
|
+
# Query to get pages associated with the account
|
|
610
|
+
pages_endpoint = f"{account_id}/assigned_pages"
|
|
611
|
+
pages_params = {
|
|
612
|
+
"fields": "id,name",
|
|
613
|
+
"limit": 1
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
pages_data = await make_api_request(pages_endpoint, access_token, pages_params)
|
|
617
|
+
|
|
618
|
+
if "data" in pages_data and pages_data["data"]:
|
|
619
|
+
page_id = pages_data["data"][0]["id"]
|
|
620
|
+
print(f"Using page ID: {page_id} ({pages_data['data'][0].get('name', 'Unknown')})")
|
|
621
|
+
else:
|
|
622
|
+
return json.dumps({
|
|
623
|
+
"error": "No page ID provided and no pages found for this account",
|
|
624
|
+
"suggestion": "Please provide a page_id parameter"
|
|
625
|
+
}, indent=2)
|
|
626
|
+
except Exception as e:
|
|
627
|
+
return json.dumps({
|
|
628
|
+
"error": "Error finding page for account",
|
|
629
|
+
"details": str(e),
|
|
630
|
+
"suggestion": "Please provide a page_id parameter"
|
|
631
|
+
}, indent=2)
|
|
632
|
+
|
|
633
|
+
# Prepare the creative data
|
|
634
|
+
creative_data = {
|
|
635
|
+
"name": name,
|
|
636
|
+
"object_story_spec": {
|
|
637
|
+
"page_id": page_id,
|
|
638
|
+
"link_data": {
|
|
639
|
+
"image_hash": image_hash,
|
|
640
|
+
"link": link_url if link_url else "https://facebook.com"
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
# Add optional parameters if provided
|
|
646
|
+
if message:
|
|
647
|
+
creative_data["object_story_spec"]["link_data"]["message"] = message
|
|
648
|
+
|
|
649
|
+
if headline:
|
|
650
|
+
creative_data["object_story_spec"]["link_data"]["name"] = headline
|
|
651
|
+
|
|
652
|
+
if description:
|
|
653
|
+
creative_data["object_story_spec"]["link_data"]["description"] = description
|
|
654
|
+
|
|
655
|
+
if call_to_action_type:
|
|
656
|
+
creative_data["object_story_spec"]["link_data"]["call_to_action"] = {
|
|
657
|
+
"type": call_to_action_type
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
if instagram_actor_id:
|
|
661
|
+
creative_data["instagram_actor_id"] = instagram_actor_id
|
|
662
|
+
|
|
663
|
+
# Prepare the API endpoint for creating a creative
|
|
664
|
+
endpoint = f"{account_id}/adcreatives"
|
|
665
|
+
|
|
666
|
+
try:
|
|
667
|
+
# Make API request to create the creative
|
|
668
|
+
data = await make_api_request(endpoint, access_token, creative_data, method="POST")
|
|
669
|
+
|
|
670
|
+
# If successful, get more details about the created creative
|
|
671
|
+
if "id" in data:
|
|
672
|
+
creative_id = data["id"]
|
|
673
|
+
creative_endpoint = f"{creative_id}"
|
|
674
|
+
creative_params = {
|
|
675
|
+
"fields": "id,name,status,thumbnail_url,image_url,image_hash,object_story_spec,url_tags,link_url"
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
creative_details = await make_api_request(creative_endpoint, access_token, creative_params)
|
|
679
|
+
return json.dumps({
|
|
680
|
+
"success": True,
|
|
681
|
+
"creative_id": creative_id,
|
|
682
|
+
"details": creative_details
|
|
683
|
+
}, indent=2)
|
|
684
|
+
|
|
685
|
+
return json.dumps(data, indent=2)
|
|
686
|
+
|
|
687
|
+
except Exception as e:
|
|
688
|
+
return json.dumps({
|
|
689
|
+
"error": "Failed to create ad creative",
|
|
690
|
+
"details": str(e),
|
|
691
|
+
"creative_data_sent": creative_data
|
|
692
|
+
}, indent=2)
|
|
693
|
+
|
|
694
|
+
|
|
695
|
+
@mcp_server.tool()
|
|
696
|
+
@meta_api_tool
|
|
697
|
+
async def get_account_pages(access_token: str = None, account_id: str = None) -> str:
|
|
698
|
+
"""
|
|
699
|
+
Get pages associated with a Meta Ads account.
|
|
700
|
+
|
|
701
|
+
Args:
|
|
702
|
+
access_token: Meta API access token (optional - will use cached token if not provided)
|
|
703
|
+
account_id: Meta Ads account ID (format: act_XXXXXXXXX)
|
|
704
|
+
|
|
705
|
+
Returns:
|
|
706
|
+
JSON response with pages associated with the account
|
|
707
|
+
"""
|
|
708
|
+
# Check required parameters
|
|
709
|
+
if not account_id:
|
|
710
|
+
return json.dumps({"error": "No account ID provided"}, indent=2)
|
|
711
|
+
|
|
712
|
+
# Handle special case for 'me'
|
|
713
|
+
if account_id == "me":
|
|
714
|
+
try:
|
|
715
|
+
endpoint = "me/accounts"
|
|
716
|
+
params = {
|
|
717
|
+
"fields": "id,name,username,category,fan_count,link,verification_status,picture"
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
user_pages_data = await make_api_request(endpoint, access_token, params)
|
|
721
|
+
return json.dumps(user_pages_data, indent=2)
|
|
722
|
+
except Exception as e:
|
|
723
|
+
return json.dumps({
|
|
724
|
+
"error": "Failed to get user pages",
|
|
725
|
+
"details": str(e)
|
|
726
|
+
}, indent=2)
|
|
727
|
+
|
|
728
|
+
# Ensure account_id has the 'act_' prefix for regular accounts
|
|
729
|
+
if not account_id.startswith("act_"):
|
|
730
|
+
account_id = f"act_{account_id}"
|
|
731
|
+
|
|
732
|
+
try:
|
|
733
|
+
# Try all approaches that might work
|
|
734
|
+
|
|
735
|
+
# Approach 1: Get active ads and extract page IDs
|
|
736
|
+
endpoint = f"{account_id}/ads"
|
|
737
|
+
params = {
|
|
738
|
+
"fields": "creative{object_story_spec{page_id}}",
|
|
739
|
+
"limit": 100
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
ads_data = await make_api_request(endpoint, access_token, params)
|
|
743
|
+
|
|
744
|
+
# Extract unique page IDs from ads
|
|
745
|
+
page_ids = set()
|
|
746
|
+
if "data" in ads_data:
|
|
747
|
+
for ad in ads_data.get("data", []):
|
|
748
|
+
if "creative" in ad and "creative" in ad and "object_story_spec" in ad["creative"] and "page_id" in ad["creative"]["object_story_spec"]:
|
|
749
|
+
page_ids.add(ad["creative"]["object_story_spec"]["page_id"])
|
|
750
|
+
|
|
751
|
+
# If we found page IDs, get details for each
|
|
752
|
+
if page_ids:
|
|
753
|
+
page_details = {"data": []}
|
|
754
|
+
|
|
755
|
+
for page_id in page_ids:
|
|
756
|
+
page_endpoint = f"{page_id}"
|
|
757
|
+
page_params = {
|
|
758
|
+
"fields": "id,name,username,category,fan_count,link,verification_status,picture"
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
page_data = await make_api_request(page_endpoint, access_token, page_params)
|
|
762
|
+
if "id" in page_data:
|
|
763
|
+
page_details["data"].append(page_data)
|
|
764
|
+
|
|
765
|
+
if page_details["data"]:
|
|
766
|
+
return json.dumps(page_details, indent=2)
|
|
767
|
+
|
|
768
|
+
# Approach 2: Try client_pages endpoint
|
|
769
|
+
endpoint = f"{account_id}/client_pages"
|
|
770
|
+
params = {
|
|
771
|
+
"fields": "id,name,username,category,fan_count,link,verification_status,picture"
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
client_pages_data = await make_api_request(endpoint, access_token, params)
|
|
775
|
+
|
|
776
|
+
if "data" in client_pages_data and client_pages_data["data"]:
|
|
777
|
+
return json.dumps(client_pages_data, indent=2)
|
|
778
|
+
|
|
779
|
+
# Approach 3: Try promoted_objects endpoint to find page IDs
|
|
780
|
+
endpoint = f"{account_id}/promoted_objects"
|
|
781
|
+
params = {
|
|
782
|
+
"fields": "page_id"
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
promoted_objects_data = await make_api_request(endpoint, access_token, params)
|
|
786
|
+
|
|
787
|
+
if "data" in promoted_objects_data and promoted_objects_data["data"]:
|
|
788
|
+
page_ids = set()
|
|
789
|
+
for obj in promoted_objects_data["data"]:
|
|
790
|
+
if "page_id" in obj:
|
|
791
|
+
page_ids.add(obj["page_id"])
|
|
792
|
+
|
|
793
|
+
if page_ids:
|
|
794
|
+
page_details = {"data": []}
|
|
795
|
+
for page_id in page_ids:
|
|
796
|
+
page_endpoint = f"{page_id}"
|
|
797
|
+
page_params = {
|
|
798
|
+
"fields": "id,name,username,category,fan_count,link,verification_status,picture"
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
page_data = await make_api_request(page_endpoint, access_token, page_params)
|
|
802
|
+
if "id" in page_data:
|
|
803
|
+
page_details["data"].append(page_data)
|
|
804
|
+
|
|
805
|
+
if page_details["data"]:
|
|
806
|
+
return json.dumps(page_details, indent=2)
|
|
807
|
+
|
|
808
|
+
# If all approaches failed, return empty data with a message
|
|
809
|
+
return json.dumps({
|
|
810
|
+
"data": [],
|
|
811
|
+
"message": "No pages found associated with this account",
|
|
812
|
+
"suggestion": "You may need to create a page or provide a page_id explicitly when creating ads"
|
|
813
|
+
}, indent=2)
|
|
814
|
+
|
|
815
|
+
except Exception as e:
|
|
816
|
+
return json.dumps({
|
|
817
|
+
"error": "Failed to get account pages",
|
|
818
|
+
"details": str(e)
|
|
819
|
+
}, indent=2)
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Adds Library-related functionality for Meta Ads API."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Optional, List, Dict, Any
|
|
5
|
+
from .api import meta_api_tool, make_api_request
|
|
6
|
+
from .server import mcp_server
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@mcp_server.tool()
|
|
10
|
+
@meta_api_tool
|
|
11
|
+
async def search_ads_archive(
|
|
12
|
+
access_token: str = None,
|
|
13
|
+
search_terms: str = None,
|
|
14
|
+
ad_type: str = "ALL",
|
|
15
|
+
ad_reached_countries: List[str] = None,
|
|
16
|
+
limit: int = 25, # Default limit, adjust as needed
|
|
17
|
+
fields: str = "ad_creation_time,ad_creative_body,ad_creative_link_caption,ad_creative_link_description,ad_creative_link_title,ad_delivery_start_time,ad_delivery_stop_time,ad_snapshot_url,currency,demographic_distribution,funding_entity,impressions,page_id,page_name,publisher_platform,region_distribution,spend"
|
|
18
|
+
) -> str:
|
|
19
|
+
"""
|
|
20
|
+
Search the Facebook Ads Library archive.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
access_token: Meta API access token (optional - will use cached token if not provided).
|
|
24
|
+
search_terms: The search query for ads.
|
|
25
|
+
ad_type: Type of ads to search for (e.g., POLITICAL_AND_ISSUE_ADS, HOUSING_ADS, ALL).
|
|
26
|
+
ad_reached_countries: List of country codes (e.g., ["US", "GB"]).
|
|
27
|
+
limit: Maximum number of ads to return.
|
|
28
|
+
fields: Comma-separated string of fields to retrieve for each ad.
|
|
29
|
+
|
|
30
|
+
Example Usage via curl equivalent:
|
|
31
|
+
curl -G \\
|
|
32
|
+
-d "search_terms='california'" \\
|
|
33
|
+
-d "ad_type=POLITICAL_AND_ISSUE_ADS" \\
|
|
34
|
+
-d "ad_reached_countries=['US']" \\
|
|
35
|
+
-d "fields=ad_snapshot_url,spend" \\
|
|
36
|
+
-d "access_token=<ACCESS_TOKEN>" \\
|
|
37
|
+
"https://graph.facebook.com/<API_VERSION>/ads_archive"
|
|
38
|
+
"""
|
|
39
|
+
if not access_token:
|
|
40
|
+
# Attempt to get token implicitly if not provided - meta_api_tool handles this
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
if not search_terms:
|
|
44
|
+
return json.dumps({"error": "search_terms parameter is required"}, indent=2)
|
|
45
|
+
|
|
46
|
+
if not ad_reached_countries:
|
|
47
|
+
return json.dumps({"error": "ad_reached_countries parameter is required"}, indent=2)
|
|
48
|
+
|
|
49
|
+
endpoint = "ads_archive"
|
|
50
|
+
params = {
|
|
51
|
+
"search_terms": search_terms,
|
|
52
|
+
"ad_type": ad_type,
|
|
53
|
+
"ad_reached_countries": json.dumps(ad_reached_countries), # API expects a JSON array string
|
|
54
|
+
"limit": limit,
|
|
55
|
+
"fields": fields,
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
data = await make_api_request(endpoint, access_token, params, method="GET")
|
|
60
|
+
return json.dumps(data, indent=2)
|
|
61
|
+
except Exception as e:
|
|
62
|
+
error_msg = str(e)
|
|
63
|
+
# Consider logging the full error for debugging
|
|
64
|
+
# print(f"Error calling Ads Library API: {error_msg}")
|
|
65
|
+
return json.dumps({
|
|
66
|
+
"error": "Failed to search ads archive",
|
|
67
|
+
"details": error_msg,
|
|
68
|
+
"params_sent": {k: v for k, v in params.items() if k != 'access_token'} # Avoid logging token
|
|
69
|
+
}, indent=2)
|
meta_ads_mcp/core/campaigns.py
CHANGED
|
@@ -23,7 +23,9 @@ async def get_campaigns(access_token: str = None, account_id: str = None, limit:
|
|
|
23
23
|
access_token: Meta API access token (optional - will use cached token if not provided)
|
|
24
24
|
account_id: Meta Ads account ID (format: act_XXXXXXXXX)
|
|
25
25
|
limit: Maximum number of campaigns to return (default: 10)
|
|
26
|
-
status_filter: Filter by status (
|
|
26
|
+
status_filter: Filter by effective status (e.g., 'ACTIVE', 'PAUSED', 'ARCHIVED').
|
|
27
|
+
Maps to the 'effective_status' API parameter, which expects an array
|
|
28
|
+
(this function handles the required JSON formatting). Leave empty for all statuses.
|
|
27
29
|
"""
|
|
28
30
|
# If no account ID is specified, try to get the first one for the user
|
|
29
31
|
if not account_id:
|
|
@@ -42,7 +44,8 @@ async def get_campaigns(access_token: str = None, account_id: str = None, limit:
|
|
|
42
44
|
}
|
|
43
45
|
|
|
44
46
|
if status_filter:
|
|
45
|
-
|
|
47
|
+
# API expects an array, encode it as a JSON string
|
|
48
|
+
params["effective_status"] = json.dumps([status_filter])
|
|
46
49
|
|
|
47
50
|
data = await make_api_request(endpoint, access_token, params)
|
|
48
51
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: meta-ads-mcp
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.3
|
|
4
4
|
Summary: Model Calling Protocol (MCP) plugin for interacting with Meta Ads API
|
|
5
5
|
Project-URL: Homepage, https://github.com/nictuku/meta-ads-mcp
|
|
6
6
|
Project-URL: Bug Tracker, https://github.com/nictuku/meta-ads-mcp/issues
|
|
@@ -129,7 +129,14 @@ Add this to your `claude_desktop_config.json` to integrate with Claude or `~/.cu
|
|
|
129
129
|
- `account_id`: Meta Ads account ID (format: act_XXXXXXXXX)
|
|
130
130
|
- Returns: Detailed information about the specified account
|
|
131
131
|
|
|
132
|
-
3. `
|
|
132
|
+
3. `mcp_meta_ads_get_account_pages`
|
|
133
|
+
- Get pages associated with a Meta Ads account
|
|
134
|
+
- Inputs:
|
|
135
|
+
- `access_token` (optional): Meta API access token (will use cached token if not provided)
|
|
136
|
+
- `account_id`: Meta Ads account ID (format: act_XXXXXXXXX) or "me" for the current user's pages
|
|
137
|
+
- Returns: List of pages associated with the account, useful for ad creation and management
|
|
138
|
+
|
|
139
|
+
4. `mcp_meta_ads_get_campaigns`
|
|
133
140
|
- Get campaigns for a Meta Ads account with optional filtering
|
|
134
141
|
- Inputs:
|
|
135
142
|
- `access_token` (optional): Meta API access token (will use cached token if not provided)
|
|
@@ -138,14 +145,14 @@ Add this to your `claude_desktop_config.json` to integrate with Claude or `~/.cu
|
|
|
138
145
|
- `status_filter`: Filter by status (empty for all, or 'ACTIVE', 'PAUSED', etc.)
|
|
139
146
|
- Returns: List of campaigns matching the criteria
|
|
140
147
|
|
|
141
|
-
|
|
148
|
+
5. `mcp_meta_ads_get_campaign_details`
|
|
142
149
|
- Get detailed information about a specific campaign
|
|
143
150
|
- Inputs:
|
|
144
151
|
- `access_token` (optional): Meta API access token (will use cached token if not provided)
|
|
145
152
|
- `campaign_id`: Meta Ads campaign ID
|
|
146
153
|
- Returns: Detailed information about the specified campaign
|
|
147
154
|
|
|
148
|
-
|
|
155
|
+
6. `mcp_meta_ads_create_campaign`
|
|
149
156
|
- Create a new campaign in a Meta Ads account
|
|
150
157
|
- Inputs:
|
|
151
158
|
- `access_token` (optional): Meta API access token (will use cached token if not provided)
|
|
@@ -158,7 +165,7 @@ Add this to your `claude_desktop_config.json` to integrate with Claude or `~/.cu
|
|
|
158
165
|
- `lifetime_budget`: Lifetime budget in account currency (in cents)
|
|
159
166
|
- Returns: Confirmation with new campaign details
|
|
160
167
|
|
|
161
|
-
|
|
168
|
+
7. `mcp_meta_ads_get_adsets`
|
|
162
169
|
- Get ad sets for a Meta Ads account with optional filtering by campaign
|
|
163
170
|
- Inputs:
|
|
164
171
|
- `access_token` (optional): Meta API access token (will use cached token if not provided)
|
|
@@ -167,14 +174,14 @@ Add this to your `claude_desktop_config.json` to integrate with Claude or `~/.cu
|
|
|
167
174
|
- `campaign_id`: Optional campaign ID to filter by
|
|
168
175
|
- Returns: List of ad sets matching the criteria
|
|
169
176
|
|
|
170
|
-
|
|
177
|
+
8. `mcp_meta_ads_get_adset_details`
|
|
171
178
|
- Get detailed information about a specific ad set
|
|
172
179
|
- Inputs:
|
|
173
180
|
- `access_token` (optional): Meta API access token (will use cached token if not provided)
|
|
174
181
|
- `adset_id`: Meta Ads ad set ID
|
|
175
182
|
- Returns: Detailed information about the specified ad set
|
|
176
183
|
|
|
177
|
-
|
|
184
|
+
9. `mcp_meta_ads_get_ads`
|
|
178
185
|
- Get ads for a Meta Ads account with optional filtering
|
|
179
186
|
- Inputs:
|
|
180
187
|
- `access_token` (optional): Meta API access token (will use cached token if not provided)
|
|
@@ -184,28 +191,28 @@ Add this to your `claude_desktop_config.json` to integrate with Claude or `~/.cu
|
|
|
184
191
|
- `adset_id`: Optional ad set ID to filter by
|
|
185
192
|
- Returns: List of ads matching the criteria
|
|
186
193
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
194
|
+
10. `mcp_meta_ads_get_ad_details`
|
|
195
|
+
- Get detailed information about a specific ad
|
|
196
|
+
- Inputs:
|
|
197
|
+
- `access_token` (optional): Meta API access token (will use cached token if not provided)
|
|
198
|
+
- `ad_id`: Meta Ads ad ID
|
|
199
|
+
- Returns: Detailed information about the specified ad
|
|
193
200
|
|
|
194
|
-
|
|
201
|
+
11. `mcp_meta_ads_get_ad_creatives`
|
|
195
202
|
- Get creative details for a specific ad
|
|
196
203
|
- Inputs:
|
|
197
204
|
- `access_token` (optional): Meta API access token (will use cached token if not provided)
|
|
198
205
|
- `ad_id`: Meta Ads ad ID
|
|
199
206
|
- Returns: Creative details including text, images, and URLs
|
|
200
207
|
|
|
201
|
-
|
|
208
|
+
12. `mcp_meta_ads_get_ad_image`
|
|
202
209
|
- Get, download, and visualize a Meta ad image in one step
|
|
203
210
|
- Inputs:
|
|
204
211
|
- `access_token` (optional): Meta API access token (will use cached token if not provided)
|
|
205
212
|
- `ad_id`: Meta Ads ad ID
|
|
206
213
|
- Returns: The ad image ready for direct visual analysis
|
|
207
214
|
|
|
208
|
-
|
|
215
|
+
13. `mcp_meta_ads_update_ad`
|
|
209
216
|
- Update an ad with new settings
|
|
210
217
|
- Inputs:
|
|
211
218
|
- `ad_id`: Meta Ads ad ID
|
|
@@ -214,7 +221,7 @@ Add this to your `claude_desktop_config.json` to integrate with Claude or `~/.cu
|
|
|
214
221
|
- `access_token` (optional): Meta API access token (will use cached token if not provided)
|
|
215
222
|
- Returns: Confirmation with updated ad details and a confirmation link
|
|
216
223
|
|
|
217
|
-
|
|
224
|
+
14. `mcp_meta_ads_update_adset`
|
|
218
225
|
- Update an ad set with new settings including frequency caps
|
|
219
226
|
- Inputs:
|
|
220
227
|
- `adset_id`: Meta Ads ad set ID
|
|
@@ -226,7 +233,7 @@ Add this to your `claude_desktop_config.json` to integrate with Claude or `~/.cu
|
|
|
226
233
|
- `access_token` (optional): Meta API access token (will use cached token if not provided)
|
|
227
234
|
- Returns: Confirmation with updated ad set details and a confirmation link
|
|
228
235
|
|
|
229
|
-
|
|
236
|
+
15. `mcp_meta_ads_get_insights`
|
|
230
237
|
- Get performance insights for a campaign, ad set, ad or account
|
|
231
238
|
- Inputs:
|
|
232
239
|
- `access_token` (optional): Meta API access token (will use cached token if not provided)
|
|
@@ -236,7 +243,7 @@ Add this to your `claude_desktop_config.json` to integrate with Claude or `~/.cu
|
|
|
236
243
|
- `level`: Level of aggregation (ad, adset, campaign, account)
|
|
237
244
|
- Returns: Performance metrics for the specified object
|
|
238
245
|
|
|
239
|
-
|
|
246
|
+
16. `mcp_meta_ads_debug_image_download`
|
|
240
247
|
- Debug image download issues and report detailed diagnostics
|
|
241
248
|
- Inputs:
|
|
242
249
|
- `access_token` (optional): Meta API access token (will use cached token if not provided)
|
|
@@ -244,7 +251,7 @@ Add this to your `claude_desktop_config.json` to integrate with Claude or `~/.cu
|
|
|
244
251
|
- `ad_id`: Meta Ads ad ID (optional, used if url is not provided)
|
|
245
252
|
- Returns: Diagnostic information about image download attempts
|
|
246
253
|
|
|
247
|
-
|
|
254
|
+
17. `mcp_meta_ads_get_login_link`
|
|
248
255
|
- Get a clickable login link for Meta Ads authentication
|
|
249
256
|
- Inputs:
|
|
250
257
|
- `access_token` (optional): Meta API access token (will use cached token if not provided)
|
|
@@ -1,22 +1,23 @@
|
|
|
1
|
-
meta_ads_mcp/__init__.py,sha256=
|
|
1
|
+
meta_ads_mcp/__init__.py,sha256=xQxyDpHFTg1FE9C3YEk88za2I8lvr8Mg0h9dMTxc5dw,1236
|
|
2
2
|
meta_ads_mcp/__main__.py,sha256=XaQt3iXftG_7f0Zu7Wop9SeFgrD2WBn0EQOaPMc27d8,207
|
|
3
3
|
meta_ads_mcp/api.py,sha256=z0pW1pV3hE75IeG9QTqB3K7QoQOUxUg2MBQ9IjAWUYA,84363
|
|
4
|
-
meta_ads_mcp/core/__init__.py,sha256=
|
|
4
|
+
meta_ads_mcp/core/__init__.py,sha256=MOCvj3SkLvr5hFHqyjWOx7L4oReKLCoZaotwspJLwZQ,1022
|
|
5
5
|
meta_ads_mcp/core/accounts.py,sha256=Nmp7lPxO9wmq25jWV7_H0LIqnEbBhpCVBlLGW2HUaq0,2277
|
|
6
|
-
meta_ads_mcp/core/ads.py,sha256=
|
|
6
|
+
meta_ads_mcp/core/ads.py,sha256=b_81GlGHIM4jISvuDZmHNyc6uW7uD3ovX68ezBci9MM,29747
|
|
7
|
+
meta_ads_mcp/core/ads_library.py,sha256=onStn9UkRqYDC60gOPS-iKDtP1plz6DygUb7hUZ0Jw8,2807
|
|
7
8
|
meta_ads_mcp/core/adsets.py,sha256=WBPNaI7ITnUOnGMus4_0MX15DslOCzfM5q1zF1VWs2s,12408
|
|
8
9
|
meta_ads_mcp/core/api.py,sha256=9Whcs2orILhPiWkAR3qGmJNouYE5uri_e_Jzeh5Hjn8,14208
|
|
9
10
|
meta_ads_mcp/core/auth.py,sha256=pDARBh3NBNqCpxflVrVvR4VsWuIveFxQmb9-P-gLFDM,20730
|
|
10
11
|
meta_ads_mcp/core/authentication.py,sha256=2MG13r28OlIcOIgPSRrGXJ2-4JSt3ifU-oB9tiOsrKQ,6511
|
|
11
12
|
meta_ads_mcp/core/callback_server.py,sha256=AUymElaVwHqFyqB2wgqf6A68KsqwtKoYmY-7JZZt8Ks,43286
|
|
12
|
-
meta_ads_mcp/core/campaigns.py,sha256=
|
|
13
|
+
meta_ads_mcp/core/campaigns.py,sha256=TQHDhJ0s7cLbo5-zd2Bk8YgwToWBoK3YBMFG8fZbEHI,10757
|
|
13
14
|
meta_ads_mcp/core/insights.py,sha256=XAm4uu83gWp84PEGqAJ3GFIqlvg7prh6MdD71JfvBCo,18072
|
|
14
15
|
meta_ads_mcp/core/pipeboard_auth.py,sha256=VvbxEB8ZOhnMccLU7HI1HgaPWHCl5NGrzZCm-zzHze4,22798
|
|
15
16
|
meta_ads_mcp/core/resources.py,sha256=-zIIfZulpo76vcKv6jhAlQq91cR2SZ3cjYZt3ek3x0w,1236
|
|
16
17
|
meta_ads_mcp/core/server.py,sha256=5WofyJZGzeDhbGzLXPhQjT0XnZwo0syeK8TM_XnJo4Q,5507
|
|
17
18
|
meta_ads_mcp/core/utils.py,sha256=EPmpBX3OZaTWRS_YuEk_PLLyLXj7DeR6Ks8WoaZ5JGQ,6366
|
|
18
|
-
meta_ads_mcp-0.3.
|
|
19
|
-
meta_ads_mcp-0.3.
|
|
20
|
-
meta_ads_mcp-0.3.
|
|
21
|
-
meta_ads_mcp-0.3.
|
|
22
|
-
meta_ads_mcp-0.3.
|
|
19
|
+
meta_ads_mcp-0.3.3.dist-info/METADATA,sha256=C1jgrXLiHpq2uRgTp259f2B639FevzezEextSF_xyUc,16611
|
|
20
|
+
meta_ads_mcp-0.3.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
21
|
+
meta_ads_mcp-0.3.3.dist-info/entry_points.txt,sha256=Dv2RkoBjRJBqj6CyhwqGIiwPCD-SCL1-7B9-zmVRuv0,57
|
|
22
|
+
meta_ads_mcp-0.3.3.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
23
|
+
meta_ads_mcp-0.3.3.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|