meta-ads-mcp 0.3.2__py3-none-any.whl → 0.3.5__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.3.2"
10
+ __version__ = "0.3.5"
11
11
 
12
12
  __all__ = [
13
13
  'get_ad_accounts',
@@ -9,6 +9,8 @@ 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
13
+ from .budget_schedules import create_budget_schedule
12
14
 
13
15
  __all__ = [
14
16
  'mcp_server',
@@ -31,4 +33,6 @@ __all__ = [
31
33
  'login_cli',
32
34
  'login',
33
35
  'main',
36
+ 'search_ads_archive',
37
+ 'create_budget_schedule',
34
38
  ]
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)
@@ -0,0 +1,71 @@
1
+ """Budget Schedule-related functionality for Meta Ads API."""
2
+
3
+ import json
4
+ from typing import Optional, Dict, Any
5
+
6
+ from .api import meta_api_tool, make_api_request
7
+ from .server import mcp_server
8
+ # Assuming no other specific dependencies from adsets.py are needed for this single function.
9
+ # If other utilities from adsets.py (like get_ad_accounts) were needed, they'd be imported here.
10
+
11
+ @mcp_server.tool()
12
+ @meta_api_tool
13
+ async def create_budget_schedule(
14
+ campaign_id: str,
15
+ budget_value: int,
16
+ budget_value_type: str,
17
+ time_start: int,
18
+ time_end: int,
19
+ access_token: str = None
20
+ ) -> str:
21
+ """
22
+ Create a budget schedule for a Meta Ads campaign.
23
+
24
+ Allows scheduling budget increases based on anticipated high-demand periods.
25
+ The times should be provided as Unix timestamps.
26
+
27
+ Args:
28
+ campaign_id: Meta Ads campaign ID.
29
+ budget_value: Amount of budget increase. Interpreted based on budget_value_type.
30
+ budget_value_type: Type of budget value - "ABSOLUTE" or "MULTIPLIER".
31
+ time_start: Unix timestamp for when the high demand period should start.
32
+ time_end: Unix timestamp for when the high demand period should end.
33
+ access_token: Meta API access token (optional - will use cached token if not provided).
34
+
35
+ Returns:
36
+ A JSON string containing the ID of the created budget schedule or an error message.
37
+ """
38
+ if not campaign_id:
39
+ return json.dumps({"error": "Campaign ID is required"}, indent=2)
40
+ if budget_value is None: # Check for None explicitly
41
+ return json.dumps({"error": "Budget value is required"}, indent=2)
42
+ if not budget_value_type:
43
+ return json.dumps({"error": "Budget value type is required"}, indent=2)
44
+ if budget_value_type not in ["ABSOLUTE", "MULTIPLIER"]:
45
+ return json.dumps({"error": "Invalid budget_value_type. Must be ABSOLUTE or MULTIPLIER"}, indent=2)
46
+ if time_start is None: # Check for None explicitly to allow 0
47
+ return json.dumps({"error": "Time start is required"}, indent=2)
48
+ if time_end is None: # Check for None explicitly to allow 0
49
+ return json.dumps({"error": "Time end is required"}, indent=2)
50
+
51
+ endpoint = f"{campaign_id}/budget_schedules"
52
+
53
+ params = {
54
+ "budget_value": budget_value,
55
+ "budget_value_type": budget_value_type,
56
+ "time_start": time_start,
57
+ "time_end": time_end,
58
+ }
59
+
60
+ try:
61
+ data = await make_api_request(endpoint, access_token, params, method="POST")
62
+ return json.dumps(data, indent=2)
63
+ except Exception as e:
64
+ error_msg = str(e)
65
+ # Include details about the error and the parameters sent for easier debugging
66
+ return json.dumps({
67
+ "error": "Failed to create budget schedule",
68
+ "details": error_msg,
69
+ "campaign_id": campaign_id,
70
+ "params_sent": params
71
+ }, indent=2)
@@ -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 (empty for all, or 'ACTIVE', 'PAUSED', etc.)
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
- params["effective_status"] = status_filter
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.2
3
+ Version: 0.3.5
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
@@ -11,7 +11,7 @@ Keywords: ads,api,claude,facebook,mcp,meta
11
11
  Classifier: License :: OSI Approved :: MIT License
12
12
  Classifier: Operating System :: OS Independent
13
13
  Classifier: Programming Language :: Python :: 3
14
- Requires-Python: >=3.11
14
+ Requires-Python: >=3.9.6
15
15
  Requires-Dist: httpx>=0.26.0
16
16
  Requires-Dist: mcp[cli]>=1.6.0
17
17
  Requires-Dist: pathlib>=1.0.1
@@ -27,11 +27,7 @@ A [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) server for in
27
27
 
28
28
  > **DISCLAIMER:** This is an unofficial third-party tool and is not associated with, endorsed by, or affiliated with Meta in any way. This project is maintained independently and uses Meta's public APIs according to their terms of service. Meta, Facebook, Instagram, and other Meta brand names are trademarks of their respective owners.
29
29
 
30
- <a href="https://glama.ai/mcp/servers/@pipeboard-co/meta-ads-mcp">
31
- <img width="380" height="200" src="https://glama.ai/mcp/servers/@pipeboard-co/meta-ads-mcp/badge" alt="Meta Ads MCP server" />
32
- </a>
33
-
34
- Screenshot: using an LLM to understand your ad performance.
30
+ **Screenshot**: Using an LLM to understand your ad performance:
35
31
 
36
32
  ![Meta Ads MCP in action: Visualize ad performance metrics and creative details directly in Claude or your favorite MCP client, with rich insights about campaign reach, engagement, and costs](./images/meta-ads-example.png)
37
33
 
@@ -94,7 +90,7 @@ python -m meta_ads_mcp --app-id YOUR_META_ADS_APP_ID
94
90
  Before using the MCP server, you'll need to set up a Meta Developer App:
95
91
 
96
92
  1. Go to [Meta for Developers](https://developers.facebook.com/) and create a new app
97
- 2. Choose the "Consumer" app type
93
+ 2. Choose the "Business" app type
98
94
  3. In your app settings, add the "Marketing API" product
99
95
  4. Configure your app's OAuth redirect URI to include `http://localhost:8888/callback`
100
96
  5. Note your App ID (Client ID) for use with the MCP
@@ -129,7 +125,14 @@ Add this to your `claude_desktop_config.json` to integrate with Claude or `~/.cu
129
125
  - `account_id`: Meta Ads account ID (format: act_XXXXXXXXX)
130
126
  - Returns: Detailed information about the specified account
131
127
 
132
- 3. `mcp_meta_ads_get_campaigns`
128
+ 3. `mcp_meta_ads_get_account_pages`
129
+ - Get pages associated with a Meta Ads account
130
+ - Inputs:
131
+ - `access_token` (optional): Meta API access token (will use cached token if not provided)
132
+ - `account_id`: Meta Ads account ID (format: act_XXXXXXXXX) or "me" for the current user's pages
133
+ - Returns: List of pages associated with the account, useful for ad creation and management
134
+
135
+ 4. `mcp_meta_ads_get_campaigns`
133
136
  - Get campaigns for a Meta Ads account with optional filtering
134
137
  - Inputs:
135
138
  - `access_token` (optional): Meta API access token (will use cached token if not provided)
@@ -138,14 +141,14 @@ Add this to your `claude_desktop_config.json` to integrate with Claude or `~/.cu
138
141
  - `status_filter`: Filter by status (empty for all, or 'ACTIVE', 'PAUSED', etc.)
139
142
  - Returns: List of campaigns matching the criteria
140
143
 
141
- 4. `mcp_meta_ads_get_campaign_details`
144
+ 5. `mcp_meta_ads_get_campaign_details`
142
145
  - Get detailed information about a specific campaign
143
146
  - Inputs:
144
147
  - `access_token` (optional): Meta API access token (will use cached token if not provided)
145
148
  - `campaign_id`: Meta Ads campaign ID
146
149
  - Returns: Detailed information about the specified campaign
147
150
 
148
- 5. `mcp_meta_ads_create_campaign`
151
+ 6. `mcp_meta_ads_create_campaign`
149
152
  - Create a new campaign in a Meta Ads account
150
153
  - Inputs:
151
154
  - `access_token` (optional): Meta API access token (will use cached token if not provided)
@@ -158,7 +161,7 @@ Add this to your `claude_desktop_config.json` to integrate with Claude or `~/.cu
158
161
  - `lifetime_budget`: Lifetime budget in account currency (in cents)
159
162
  - Returns: Confirmation with new campaign details
160
163
 
161
- 6. `mcp_meta_ads_get_adsets`
164
+ 7. `mcp_meta_ads_get_adsets`
162
165
  - Get ad sets for a Meta Ads account with optional filtering by campaign
163
166
  - Inputs:
164
167
  - `access_token` (optional): Meta API access token (will use cached token if not provided)
@@ -167,45 +170,101 @@ Add this to your `claude_desktop_config.json` to integrate with Claude or `~/.cu
167
170
  - `campaign_id`: Optional campaign ID to filter by
168
171
  - Returns: List of ad sets matching the criteria
169
172
 
170
- 7. `mcp_meta_ads_get_adset_details`
173
+ 8. `mcp_meta_ads_get_adset_details`
171
174
  - Get detailed information about a specific ad set
172
175
  - Inputs:
173
176
  - `access_token` (optional): Meta API access token (will use cached token if not provided)
174
177
  - `adset_id`: Meta Ads ad set ID
175
178
  - Returns: Detailed information about the specified ad set
176
179
 
177
- 8. `mcp_meta_ads_get_ads`
178
- - Get ads for a Meta Ads account with optional filtering
180
+ 9. `mcp_meta_ads_create_adset`
181
+ - Create a new ad set in a Meta Ads account
179
182
  - Inputs:
180
- - `access_token` (optional): Meta API access token (will use cached token if not provided)
181
183
  - `account_id`: Meta Ads account ID (format: act_XXXXXXXXX)
182
- - `limit`: Maximum number of ads to return (default: 10)
183
- - `campaign_id`: Optional campaign ID to filter by
184
- - `adset_id`: Optional ad set ID to filter by
185
- - Returns: List of ads matching the criteria
186
-
187
- 9. `mcp_meta_ads_get_ad_details`
188
- - Get detailed information about a specific ad
189
- - Inputs:
190
- - `access_token` (optional): Meta API access token (will use cached token if not provided)
191
- - `ad_id`: Meta Ads ad ID
192
- - Returns: Detailed information about the specified ad
184
+ - `campaign_id`: Meta Ads campaign ID this ad set belongs to
185
+ - `name`: Ad set name
186
+ - `status`: Initial ad set status (default: PAUSED)
187
+ - `daily_budget`: Daily budget in account currency (in cents) as a string
188
+ - `lifetime_budget`: Lifetime budget in account currency (in cents) as a string
189
+ - `targeting`: Targeting specifications (e.g., age, location, interests)
190
+ - `optimization_goal`: Conversion optimization goal (e.g., 'LINK_CLICKS')
191
+ - `billing_event`: How you're charged (e.g., 'IMPRESSIONS')
192
+ - `bid_amount`: Bid amount in account currency (in cents)
193
+ - `bid_strategy`: Bid strategy (e.g., 'LOWEST_COST')
194
+ - `start_time`, `end_time`: Optional start/end times (ISO 8601)
195
+ - `access_token` (optional): Meta API access token
196
+ - Returns: Confirmation with new ad set details
197
+
198
+ 10. `mcp_meta_ads_get_ads`
199
+ - Get ads for a Meta Ads account with optional filtering
200
+ - Inputs:
201
+ - `access_token` (optional): Meta API access token (will use cached token if not provided)
202
+ - `account_id`: Meta Ads account ID (format: act_XXXXXXXXX)
203
+ - `limit`: Maximum number of ads to return (default: 10)
204
+ - `campaign_id`: Optional campaign ID to filter by
205
+ - `adset_id`: Optional ad set ID to filter by
206
+ - Returns: List of ads matching the criteria
207
+
208
+ 11. `mcp_meta_ads_create_ad`
209
+ - Create a new ad with an existing creative
210
+ - Inputs:
211
+ - `account_id`: Meta Ads account ID (format: act_XXXXXXXXX)
212
+ - `name`: Ad name
213
+ - `adset_id`: Ad set ID where this ad will be placed
214
+ - `creative_id`: ID of an existing creative to use
215
+ - `status`: Initial ad status (default: PAUSED)
216
+ - `bid_amount`: Optional bid amount (in cents)
217
+ - `tracking_specs`: Optional tracking specifications
218
+ - `access_token` (optional): Meta API access token
219
+ - Returns: Confirmation with new ad details
220
+
221
+ 12. `mcp_meta_ads_get_ad_details`
222
+ - Get detailed information about a specific ad
223
+ - Inputs:
224
+ - `access_token` (optional): Meta API access token (will use cached token if not provided)
225
+ - `ad_id`: Meta Ads ad ID
226
+ - Returns: Detailed information about the specified ad
193
227
 
194
- 10. `mcp_meta_ads_get_ad_creatives`
228
+ 13. `mcp_meta_ads_get_ad_creatives`
195
229
  - Get creative details for a specific ad
196
230
  - Inputs:
197
231
  - `access_token` (optional): Meta API access token (will use cached token if not provided)
198
232
  - `ad_id`: Meta Ads ad ID
199
233
  - Returns: Creative details including text, images, and URLs
200
234
 
201
- 11. `mcp_meta_ads_get_ad_image`
235
+ 14. `mcp_meta_ads_create_ad_creative`
236
+ - Create a new ad creative using an uploaded image hash
237
+ - Inputs:
238
+ - `account_id`: Meta Ads account ID (format: act_XXXXXXXXX)
239
+ - `name`: Creative name
240
+ - `image_hash`: Hash of the uploaded image
241
+ - `page_id`: Facebook Page ID for the ad
242
+ - `link_url`: Destination URL
243
+ - `message`: Ad copy/text
244
+ - `headline`: Ad headline
245
+ - `description`: Ad description
246
+ - `call_to_action_type`: CTA button type (e.g., 'LEARN_MORE')
247
+ - `instagram_actor_id`: Optional Instagram account ID
248
+ - `access_token` (optional): Meta API access token
249
+ - Returns: Confirmation with new creative details
250
+
251
+ 15. `mcp_meta_ads_upload_ad_image`
252
+ - Upload an image to use in Meta Ads creatives
253
+ - Inputs:
254
+ - `account_id`: Meta Ads account ID (format: act_XXXXXXXXX)
255
+ - `image_path`: Path to the image file to upload
256
+ - `name`: Optional name for the image
257
+ - `access_token` (optional): Meta API access token
258
+ - Returns: JSON response with image details including hash
259
+
260
+ 16. `mcp_meta_ads_get_ad_image`
202
261
  - Get, download, and visualize a Meta ad image in one step
203
262
  - Inputs:
204
263
  - `access_token` (optional): Meta API access token (will use cached token if not provided)
205
264
  - `ad_id`: Meta Ads ad ID
206
265
  - Returns: The ad image ready for direct visual analysis
207
266
 
208
- 12. `mcp_meta_ads_update_ad`
267
+ 17. `mcp_meta_ads_update_ad`
209
268
  - Update an ad with new settings
210
269
  - Inputs:
211
270
  - `ad_id`: Meta Ads ad ID
@@ -214,7 +273,7 @@ Add this to your `claude_desktop_config.json` to integrate with Claude or `~/.cu
214
273
  - `access_token` (optional): Meta API access token (will use cached token if not provided)
215
274
  - Returns: Confirmation with updated ad details and a confirmation link
216
275
 
217
- 13. `mcp_meta_ads_update_adset`
276
+ 18. `mcp_meta_ads_update_adset`
218
277
  - Update an ad set with new settings including frequency caps
219
278
  - Inputs:
220
279
  - `adset_id`: Meta Ads ad set ID
@@ -226,7 +285,7 @@ Add this to your `claude_desktop_config.json` to integrate with Claude or `~/.cu
226
285
  - `access_token` (optional): Meta API access token (will use cached token if not provided)
227
286
  - Returns: Confirmation with updated ad set details and a confirmation link
228
287
 
229
- 14. `mcp_meta_ads_get_insights`
288
+ 19. `mcp_meta_ads_get_insights`
230
289
  - Get performance insights for a campaign, ad set, ad or account
231
290
  - Inputs:
232
291
  - `access_token` (optional): Meta API access token (will use cached token if not provided)
@@ -236,7 +295,7 @@ Add this to your `claude_desktop_config.json` to integrate with Claude or `~/.cu
236
295
  - `level`: Level of aggregation (ad, adset, campaign, account)
237
296
  - Returns: Performance metrics for the specified object
238
297
 
239
- 15. `mcp_meta_ads_debug_image_download`
298
+ 20. `mcp_meta_ads_debug_image_download`
240
299
  - Debug image download issues and report detailed diagnostics
241
300
  - Inputs:
242
301
  - `access_token` (optional): Meta API access token (will use cached token if not provided)
@@ -244,12 +303,23 @@ Add this to your `claude_desktop_config.json` to integrate with Claude or `~/.cu
244
303
  - `ad_id`: Meta Ads ad ID (optional, used if url is not provided)
245
304
  - Returns: Diagnostic information about image download attempts
246
305
 
247
- 16. `mcp_meta_ads_get_login_link`
306
+ 21. `mcp_meta_ads_get_login_link`
248
307
  - Get a clickable login link for Meta Ads authentication
249
308
  - Inputs:
250
309
  - `access_token` (optional): Meta API access token (will use cached token if not provided)
251
310
  - Returns: A clickable resource link for Meta authentication
252
311
 
312
+ 22. `mcp_meta-ads_create_budget_schedule`
313
+ - Create a budget schedule for a Meta Ads campaign.
314
+ - Inputs:
315
+ - `campaign_id`: Meta Ads campaign ID.
316
+ - `budget_value`: Amount of budget increase.
317
+ - `budget_value_type`: Type of budget value ("ABSOLUTE" or "MULTIPLIER").
318
+ - `time_start`: Unix timestamp for when the high demand period should start.
319
+ - `time_end`: Unix timestamp for when the high demand period should end.
320
+ - `access_token` (optional): Meta API access token.
321
+ - Returns: JSON string with the ID of the created budget schedule or an error message.
322
+
253
323
  ## Authentication
254
324
 
255
325
  The Meta Ads MCP uses Meta's OAuth 2.0 authentication flow, designed for desktop apps:
@@ -1,22 +1,24 @@
1
- meta_ads_mcp/__init__.py,sha256=Xa5R_8nWJqcRh6Jps90mcuihcp4bJDKGXGg1bwnoSz4,1236
1
+ meta_ads_mcp/__init__.py,sha256=RSDlsZrR_mt2PklzoAeG_4555QHPTM1ZJrvr5aVadjU,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=6T8iqrQrw9VHhKtncLqYWyDk8jeSBPs79hs1CSu-fLU,952
4
+ meta_ads_mcp/core/__init__.py,sha256=NvRA_socbKPEXFXIYdso5jBHb8cEEpF_2Mwhe3Obguw,1105
5
5
  meta_ads_mcp/core/accounts.py,sha256=Nmp7lPxO9wmq25jWV7_H0LIqnEbBhpCVBlLGW2HUaq0,2277
6
- meta_ads_mcp/core/ads.py,sha256=LMZOo6agi6tQl4JJPmrDUn-91n7DzfxG2TChmwcrWOY,12544
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
12
+ meta_ads_mcp/core/budget_schedules.py,sha256=UxseExsvKAiPwfDCY9aycT4kys4xqeNytyq-yyDOxrs,2901
11
13
  meta_ads_mcp/core/callback_server.py,sha256=AUymElaVwHqFyqB2wgqf6A68KsqwtKoYmY-7JZZt8Ks,43286
12
- meta_ads_mcp/core/campaigns.py,sha256=20DHMwHppZuoZBg0owkf1BfmPBhWzkQcFgh5rQa-pAU,10480
14
+ meta_ads_mcp/core/campaigns.py,sha256=TQHDhJ0s7cLbo5-zd2Bk8YgwToWBoK3YBMFG8fZbEHI,10757
13
15
  meta_ads_mcp/core/insights.py,sha256=XAm4uu83gWp84PEGqAJ3GFIqlvg7prh6MdD71JfvBCo,18072
14
16
  meta_ads_mcp/core/pipeboard_auth.py,sha256=VvbxEB8ZOhnMccLU7HI1HgaPWHCl5NGrzZCm-zzHze4,22798
15
17
  meta_ads_mcp/core/resources.py,sha256=-zIIfZulpo76vcKv6jhAlQq91cR2SZ3cjYZt3ek3x0w,1236
16
18
  meta_ads_mcp/core/server.py,sha256=5WofyJZGzeDhbGzLXPhQjT0XnZwo0syeK8TM_XnJo4Q,5507
17
19
  meta_ads_mcp/core/utils.py,sha256=EPmpBX3OZaTWRS_YuEk_PLLyLXj7DeR6Ks8WoaZ5JGQ,6366
18
- meta_ads_mcp-0.3.2.dist-info/METADATA,sha256=Q_05xZMow3h9ova-mvfiL210rWvy-Ix9B_sZEetjN4k,16212
19
- meta_ads_mcp-0.3.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
20
- meta_ads_mcp-0.3.2.dist-info/entry_points.txt,sha256=Dv2RkoBjRJBqj6CyhwqGIiwPCD-SCL1-7B9-zmVRuv0,57
21
- meta_ads_mcp-0.3.2.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
22
- meta_ads_mcp-0.3.2.dist-info/RECORD,,
20
+ meta_ads_mcp-0.3.5.dist-info/METADATA,sha256=lbTJVtr81f-WwPI87zeKUKqGe0HzZrlC5c-WJeZKv2c,19650
21
+ meta_ads_mcp-0.3.5.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
22
+ meta_ads_mcp-0.3.5.dist-info/entry_points.txt,sha256=Dv2RkoBjRJBqj6CyhwqGIiwPCD-SCL1-7B9-zmVRuv0,57
23
+ meta_ads_mcp-0.3.5.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
24
+ meta_ads_mcp-0.3.5.dist-info/RECORD,,