meta-ads-mcp 0.3.1__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 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.1"
10
+ __version__ = "0.3.3"
11
11
 
12
12
  __all__ = [
13
13
  'get_ad_accounts',
meta_ads_mcp/api.py CHANGED
@@ -48,6 +48,13 @@ callback_server_port = None
48
48
  # Global token container for communication between threads
49
49
  token_container = {"token": None, "expires_in": None, "user_id": None}
50
50
 
51
+ # Add these at the top of the file with the other global variables
52
+ callback_server_instance = None
53
+ server_shutdown_timer = None
54
+
55
+ # Add this near other constants in the file
56
+ CALLBACK_SERVER_TIMEOUT = 180 # 3 minutes timeout
57
+
51
58
  # Configuration class to store app ID and other config
52
59
  class MetaConfig:
53
60
  _instance = None
@@ -1875,13 +1882,50 @@ async def try_multiple_download_methods(url: str) -> Optional[bytes]:
1875
1882
 
1876
1883
  return None
1877
1884
 
1885
+ def shutdown_callback_server():
1886
+ """Shutdown the callback server if it's running"""
1887
+ global callback_server_thread, callback_server_running, callback_server_port, callback_server_instance, server_shutdown_timer
1888
+
1889
+ with callback_server_lock:
1890
+ if not callback_server_running:
1891
+ print("Callback server is not running")
1892
+ return
1893
+
1894
+ if server_shutdown_timer is not None:
1895
+ server_shutdown_timer.cancel()
1896
+ server_shutdown_timer = None
1897
+
1898
+ print(f"Shutting down callback server on port {callback_server_port}")
1899
+
1900
+ # Shutdown the server if it exists
1901
+ if callback_server_instance:
1902
+ try:
1903
+ callback_server_instance.shutdown()
1904
+ callback_server_instance = None
1905
+ callback_server_running = False
1906
+ print("Callback server has been shut down")
1907
+ except Exception as e:
1908
+ print(f"Error shutting down callback server: {e}")
1909
+ else:
1910
+ print("No server instance to shut down")
1911
+
1878
1912
  def start_callback_server():
1879
1913
  """Start the callback server if it's not already running"""
1880
- global callback_server_thread, callback_server_running, callback_server_port
1914
+ global callback_server_thread, callback_server_running, callback_server_port, callback_server_instance, server_shutdown_timer
1881
1915
 
1882
1916
  with callback_server_lock:
1883
1917
  if callback_server_running:
1884
1918
  print(f"Callback server already running on port {callback_server_port}")
1919
+
1920
+ # Reset the shutdown timer if one exists
1921
+ if server_shutdown_timer is not None:
1922
+ server_shutdown_timer.cancel()
1923
+
1924
+ server_shutdown_timer = threading.Timer(CALLBACK_SERVER_TIMEOUT, shutdown_callback_server)
1925
+ server_shutdown_timer.daemon = True
1926
+ server_shutdown_timer.start()
1927
+ print(f"Reset server shutdown timer to {CALLBACK_SERVER_TIMEOUT} seconds")
1928
+
1885
1929
  return callback_server_port
1886
1930
 
1887
1931
  # Find an available port
@@ -1913,6 +1957,7 @@ def start_callback_server():
1913
1957
 
1914
1958
  # Create and start server in a daemon thread
1915
1959
  server = HTTPServer(('localhost', port), handler_class)
1960
+ callback_server_instance = server
1916
1961
  print(f"Callback server starting on port {port}")
1917
1962
 
1918
1963
  # Create a simple flag to signal when the server is ready
@@ -1942,6 +1987,15 @@ def start_callback_server():
1942
1987
 
1943
1988
  callback_server_running = True
1944
1989
 
1990
+ # Set a timer to shutdown the server after CALLBACK_SERVER_TIMEOUT seconds
1991
+ if server_shutdown_timer is not None:
1992
+ server_shutdown_timer.cancel()
1993
+
1994
+ server_shutdown_timer = threading.Timer(CALLBACK_SERVER_TIMEOUT, shutdown_callback_server)
1995
+ server_shutdown_timer.daemon = True
1996
+ server_shutdown_timer.start()
1997
+ print(f"Server will automatically shut down after {CALLBACK_SERVER_TIMEOUT} seconds of inactivity")
1998
+
1945
1999
  # Verify the server is actually accepting connections
1946
2000
  try:
1947
2001
  with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
@@ -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)
@@ -6,7 +6,7 @@ from .api import meta_api_tool, make_api_request
6
6
  from .accounts import get_ad_accounts
7
7
  from .server import mcp_server
8
8
  import asyncio
9
- from .callback_server import start_callback_server, update_confirmation
9
+ from .callback_server import start_callback_server, shutdown_callback_server, update_confirmation
10
10
  import urllib.parse
11
11
 
12
12
 
meta_ads_mcp/core/api.py CHANGED
@@ -6,7 +6,7 @@ import httpx
6
6
  import asyncio
7
7
  import functools
8
8
  import os
9
- from .auth import needs_authentication, get_current_access_token, auth_manager, start_callback_server
9
+ from .auth import needs_authentication, get_current_access_token, auth_manager, start_callback_server, shutdown_callback_server
10
10
  from .utils import logger
11
11
 
12
12
  # Constants
meta_ads_mcp/core/auth.py CHANGED
@@ -13,9 +13,10 @@ import requests
13
13
 
14
14
  # Import from the new callback server module
15
15
  from .callback_server import (
16
- start_callback_server,
16
+ start_callback_server,
17
+ shutdown_callback_server,
17
18
  token_container,
18
- update_confirmation
19
+ callback_server_port
19
20
  )
20
21
 
21
22
  # Import the new Pipeboard authentication
@@ -398,6 +399,16 @@ def exchange_token_for_long_lived(short_lived_token):
398
399
 
399
400
  async def get_current_access_token() -> Optional[str]:
400
401
  """Get the current access token from auth manager"""
402
+ # Check for environment variable first - this takes highest precedence
403
+ env_token = os.environ.get("META_ACCESS_TOKEN")
404
+ if env_token:
405
+ logger.debug("Using access token from META_ACCESS_TOKEN environment variable")
406
+ # Basic validation
407
+ if len(env_token) < 20: # Most Meta tokens are much longer
408
+ logger.error(f"TOKEN VALIDATION FAILED: Token from environment variable appears malformed (length: {len(env_token)})")
409
+ return None
410
+ return env_token
411
+
401
412
  # Use the singleton auth manager
402
413
  global auth_manager
403
414
 
@@ -4,7 +4,7 @@ import json
4
4
  import asyncio
5
5
  import os
6
6
  from .api import meta_api_tool
7
- from .auth import start_callback_server, auth_manager, get_current_access_token
7
+ from .auth import start_callback_server, shutdown_callback_server, auth_manager, get_current_access_token
8
8
  from .server import mcp_server
9
9
  from .utils import logger, META_APP_SECRET
10
10
  from .pipeboard_auth import pipeboard_auth_manager
@@ -24,6 +24,11 @@ callback_server_thread = None
24
24
  callback_server_lock = threading.Lock()
25
25
  callback_server_running = False
26
26
  callback_server_port = None
27
+ callback_server_instance = None
28
+ server_shutdown_timer = None
29
+
30
+ # Timeout in seconds before shutting down the callback server
31
+ CALLBACK_SERVER_TIMEOUT = 180 # 3 minutes timeout
27
32
 
28
33
 
29
34
  class CallbackHandler(BaseHTTPRequestHandler):
@@ -877,6 +882,35 @@ class CallbackHandler(BaseHTTPRequestHandler):
877
882
  return
878
883
 
879
884
 
885
+ def shutdown_callback_server():
886
+ """
887
+ Shutdown the callback server if it's running
888
+ """
889
+ global callback_server_thread, callback_server_running, callback_server_port, callback_server_instance, server_shutdown_timer
890
+
891
+ with callback_server_lock:
892
+ if not callback_server_running:
893
+ print("Callback server is not running")
894
+ return
895
+
896
+ if server_shutdown_timer is not None:
897
+ server_shutdown_timer.cancel()
898
+ server_shutdown_timer = None
899
+
900
+ print(f"Shutting down callback server on port {callback_server_port}")
901
+
902
+ # Shutdown the server if it exists
903
+ if callback_server_instance:
904
+ try:
905
+ callback_server_instance.shutdown()
906
+ callback_server_instance = None
907
+ callback_server_running = False
908
+ print("Callback server has been shut down")
909
+ except Exception as e:
910
+ print(f"Error shutting down callback server: {e}")
911
+ else:
912
+ print("No server instance to shut down")
913
+
880
914
  def start_callback_server() -> int:
881
915
  """
882
916
  Start the callback server if it's not already running
@@ -884,11 +918,21 @@ def start_callback_server() -> int:
884
918
  Returns:
885
919
  Port number the server is running on
886
920
  """
887
- global callback_server_thread, callback_server_running, callback_server_port
921
+ global callback_server_thread, callback_server_running, callback_server_port, callback_server_instance, server_shutdown_timer
888
922
 
889
923
  with callback_server_lock:
890
924
  if callback_server_running:
891
925
  print(f"Callback server already running on port {callback_server_port}")
926
+
927
+ # Reset the shutdown timer if one exists
928
+ if server_shutdown_timer is not None:
929
+ server_shutdown_timer.cancel()
930
+
931
+ server_shutdown_timer = threading.Timer(CALLBACK_SERVER_TIMEOUT, shutdown_callback_server)
932
+ server_shutdown_timer.daemon = True
933
+ server_shutdown_timer.start()
934
+ print(f"Reset server shutdown timer to {CALLBACK_SERVER_TIMEOUT} seconds")
935
+
892
936
  return callback_server_port
893
937
 
894
938
  # Find an available port
@@ -909,6 +953,7 @@ def start_callback_server() -> int:
909
953
  try:
910
954
  # Create and start server in a daemon thread
911
955
  server = HTTPServer(('localhost', port), CallbackHandler)
956
+ callback_server_instance = server
912
957
  print(f"Callback server starting on port {port}")
913
958
 
914
959
  # Create a simple flag to signal when the server is ready
@@ -938,6 +983,15 @@ def start_callback_server() -> int:
938
983
 
939
984
  callback_server_running = True
940
985
 
986
+ # Set a timer to shutdown the server after CALLBACK_SERVER_TIMEOUT seconds
987
+ if server_shutdown_timer is not None:
988
+ server_shutdown_timer.cancel()
989
+
990
+ server_shutdown_timer = threading.Timer(CALLBACK_SERVER_TIMEOUT, shutdown_callback_server)
991
+ server_shutdown_timer.daemon = True
992
+ server_shutdown_timer.start()
993
+ print(f"Server will automatically shut down after {CALLBACK_SERVER_TIMEOUT} seconds of inactivity")
994
+
941
995
  # Verify the server is actually accepting connections
942
996
  try:
943
997
  with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
@@ -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.1
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
@@ -31,7 +31,7 @@ A [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) server for in
31
31
  <img width="380" height="200" src="https://glama.ai/mcp/servers/@pipeboard-co/meta-ads-mcp/badge" alt="Meta Ads MCP server" />
32
32
  </a>
33
33
 
34
- Screenhot: using an LLM to understand your ad performance.
34
+ Screenshot: using an LLM to understand your ad performance.
35
35
 
36
36
  ![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
37
 
@@ -55,11 +55,7 @@ Screenhot: using an LLM to understand your ad performance.
55
55
  When using uv no specific installation is needed. We can use uvx to directly run meta-ads-mcp:
56
56
 
57
57
  ```bash
58
- # RECOMMENDED: Use with Pipeboard authentication
59
- export PIPEBOARD_API_TOKEN=your_pipeboard_token # Get your token at https://pipeboard.co
60
- uvx meta-ads-mcp
61
-
62
- # Alternative: Use with direct Meta authentication
58
+ # Run with Meta authentication
63
59
  uvx meta-ads-mcp --app-id YOUR_META_ADS_APP_ID
64
60
  ```
65
61
 
@@ -87,50 +83,26 @@ pip install meta-ads-mcp
87
83
  After installation, you can run it as:
88
84
 
89
85
  ```bash
90
- # RECOMMENDED: Use with Pipeboard authentication
91
- export PIPEBOARD_API_TOKEN=your_pipeboard_token # Get your token at https://pipeboard.co
92
- python -m meta_ads_mcp
93
-
94
- # Alternative: Use with direct Meta authentication
86
+ # Run with Meta authentication
95
87
  python -m meta_ads_mcp --app-id YOUR_META_ADS_APP_ID
96
88
  ```
97
89
 
98
90
  ## Configuration
99
91
 
100
- ### Quick Start with Pipeboard Authentication (Recommended)
92
+ ### Create a Meta Developer App (Required)
101
93
 
102
- The easiest way to configure Meta Ads MCP is using Pipeboard authentication:
103
-
104
- 1. Sign up at [Pipeboard.co](https://pipeboard.co) and generate an API token - **Get your free token at [https://pipeboard.co](https://pipeboard.co)**
105
- 2. Set the environment variable:
106
- ```bash
107
- export PIPEBOARD_API_TOKEN=your_pipeboard_token # Token obtainable via https://pipeboard.co
108
- ```
109
- 3. Run meta-ads-mcp without needing to set up a Meta Developer App:
110
- ```bash
111
- uvx meta-ads-mcp
112
- ```
94
+ Before using the MCP server, you'll need to set up a Meta Developer App:
113
95
 
114
- This method provides longer-lived tokens (60 days), simplified setup, and automatic token renewal.
96
+ 1. Go to [Meta for Developers](https://developers.facebook.com/) and create a new app
97
+ 2. Choose the "Consumer" app type
98
+ 3. In your app settings, add the "Marketing API" product
99
+ 4. Configure your app's OAuth redirect URI to include `http://localhost:8888/callback`
100
+ 5. Note your App ID (Client ID) for use with the MCP
115
101
 
116
102
  ### Usage with Cursor or Claude Desktop
117
103
 
118
104
  Add this to your `claude_desktop_config.json` to integrate with Claude or `~/.cursor/mcp.json` to integrate with Cursor:
119
105
 
120
- ```json
121
- "mcpServers": {
122
- "meta-ads": {
123
- "command": "uvx",
124
- "args": ["meta-ads-mcp"],
125
- "env": {
126
- "PIPEBOARD_API_TOKEN": "your_pipeboard_token" // Token obtainable via https://pipeboard.co
127
- }
128
- }
129
- }
130
- ```
131
-
132
- Or if you prefer direct Meta authentication (using your own Facebook app):
133
-
134
106
  ```json
135
107
  "mcpServers": {
136
108
  "meta-ads": {
@@ -157,7 +129,14 @@ Or if you prefer direct Meta authentication (using your own Facebook app):
157
129
  - `account_id`: Meta Ads account ID (format: act_XXXXXXXXX)
158
130
  - Returns: Detailed information about the specified account
159
131
 
160
- 3. `mcp_meta_ads_get_campaigns`
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`
161
140
  - Get campaigns for a Meta Ads account with optional filtering
162
141
  - Inputs:
163
142
  - `access_token` (optional): Meta API access token (will use cached token if not provided)
@@ -166,14 +145,14 @@ Or if you prefer direct Meta authentication (using your own Facebook app):
166
145
  - `status_filter`: Filter by status (empty for all, or 'ACTIVE', 'PAUSED', etc.)
167
146
  - Returns: List of campaigns matching the criteria
168
147
 
169
- 4. `mcp_meta_ads_get_campaign_details`
148
+ 5. `mcp_meta_ads_get_campaign_details`
170
149
  - Get detailed information about a specific campaign
171
150
  - Inputs:
172
151
  - `access_token` (optional): Meta API access token (will use cached token if not provided)
173
152
  - `campaign_id`: Meta Ads campaign ID
174
153
  - Returns: Detailed information about the specified campaign
175
154
 
176
- 5. `mcp_meta_ads_create_campaign`
155
+ 6. `mcp_meta_ads_create_campaign`
177
156
  - Create a new campaign in a Meta Ads account
178
157
  - Inputs:
179
158
  - `access_token` (optional): Meta API access token (will use cached token if not provided)
@@ -186,7 +165,7 @@ Or if you prefer direct Meta authentication (using your own Facebook app):
186
165
  - `lifetime_budget`: Lifetime budget in account currency (in cents)
187
166
  - Returns: Confirmation with new campaign details
188
167
 
189
- 6. `mcp_meta_ads_get_adsets`
168
+ 7. `mcp_meta_ads_get_adsets`
190
169
  - Get ad sets for a Meta Ads account with optional filtering by campaign
191
170
  - Inputs:
192
171
  - `access_token` (optional): Meta API access token (will use cached token if not provided)
@@ -195,14 +174,14 @@ Or if you prefer direct Meta authentication (using your own Facebook app):
195
174
  - `campaign_id`: Optional campaign ID to filter by
196
175
  - Returns: List of ad sets matching the criteria
197
176
 
198
- 7. `mcp_meta_ads_get_adset_details`
177
+ 8. `mcp_meta_ads_get_adset_details`
199
178
  - Get detailed information about a specific ad set
200
179
  - Inputs:
201
180
  - `access_token` (optional): Meta API access token (will use cached token if not provided)
202
181
  - `adset_id`: Meta Ads ad set ID
203
182
  - Returns: Detailed information about the specified ad set
204
183
 
205
- 8. `mcp_meta_ads_get_ads`
184
+ 9. `mcp_meta_ads_get_ads`
206
185
  - Get ads for a Meta Ads account with optional filtering
207
186
  - Inputs:
208
187
  - `access_token` (optional): Meta API access token (will use cached token if not provided)
@@ -212,28 +191,28 @@ Or if you prefer direct Meta authentication (using your own Facebook app):
212
191
  - `adset_id`: Optional ad set ID to filter by
213
192
  - Returns: List of ads matching the criteria
214
193
 
215
- 9. `mcp_meta_ads_get_ad_details`
216
- - Get detailed information about a specific ad
217
- - Inputs:
218
- - `access_token` (optional): Meta API access token (will use cached token if not provided)
219
- - `ad_id`: Meta Ads ad ID
220
- - Returns: Detailed information about the specified ad
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
221
200
 
222
- 10. `mcp_meta_ads_get_ad_creatives`
201
+ 11. `mcp_meta_ads_get_ad_creatives`
223
202
  - Get creative details for a specific ad
224
203
  - Inputs:
225
204
  - `access_token` (optional): Meta API access token (will use cached token if not provided)
226
205
  - `ad_id`: Meta Ads ad ID
227
206
  - Returns: Creative details including text, images, and URLs
228
207
 
229
- 11. `mcp_meta_ads_get_ad_image`
208
+ 12. `mcp_meta_ads_get_ad_image`
230
209
  - Get, download, and visualize a Meta ad image in one step
231
210
  - Inputs:
232
211
  - `access_token` (optional): Meta API access token (will use cached token if not provided)
233
212
  - `ad_id`: Meta Ads ad ID
234
213
  - Returns: The ad image ready for direct visual analysis
235
214
 
236
- 12. `mcp_meta_ads_update_ad`
215
+ 13. `mcp_meta_ads_update_ad`
237
216
  - Update an ad with new settings
238
217
  - Inputs:
239
218
  - `ad_id`: Meta Ads ad ID
@@ -242,7 +221,7 @@ Or if you prefer direct Meta authentication (using your own Facebook app):
242
221
  - `access_token` (optional): Meta API access token (will use cached token if not provided)
243
222
  - Returns: Confirmation with updated ad details and a confirmation link
244
223
 
245
- 13. `mcp_meta_ads_update_adset`
224
+ 14. `mcp_meta_ads_update_adset`
246
225
  - Update an ad set with new settings including frequency caps
247
226
  - Inputs:
248
227
  - `adset_id`: Meta Ads ad set ID
@@ -254,7 +233,7 @@ Or if you prefer direct Meta authentication (using your own Facebook app):
254
233
  - `access_token` (optional): Meta API access token (will use cached token if not provided)
255
234
  - Returns: Confirmation with updated ad set details and a confirmation link
256
235
 
257
- 14. `mcp_meta_ads_get_insights`
236
+ 15. `mcp_meta_ads_get_insights`
258
237
  - Get performance insights for a campaign, ad set, ad or account
259
238
  - Inputs:
260
239
  - `access_token` (optional): Meta API access token (will use cached token if not provided)
@@ -264,7 +243,7 @@ Or if you prefer direct Meta authentication (using your own Facebook app):
264
243
  - `level`: Level of aggregation (ad, adset, campaign, account)
265
244
  - Returns: Performance metrics for the specified object
266
245
 
267
- 15. `mcp_meta_ads_debug_image_download`
246
+ 16. `mcp_meta_ads_debug_image_download`
268
247
  - Debug image download issues and report detailed diagnostics
269
248
  - Inputs:
270
249
  - `access_token` (optional): Meta API access token (will use cached token if not provided)
@@ -272,56 +251,15 @@ Or if you prefer direct Meta authentication (using your own Facebook app):
272
251
  - `ad_id`: Meta Ads ad ID (optional, used if url is not provided)
273
252
  - Returns: Diagnostic information about image download attempts
274
253
 
275
- 16. `mcp_meta_ads_get_login_link`
254
+ 17. `mcp_meta_ads_get_login_link`
276
255
  - Get a clickable login link for Meta Ads authentication
277
- - NOTE: This method should only be used if you're using your own Facebook app. If using Pipeboard authentication (recommended), set the PIPEBOARD_API_TOKEN environment variable instead (token obtainable via https://pipeboard.co).
278
256
  - Inputs:
279
257
  - `access_token` (optional): Meta API access token (will use cached token if not provided)
280
258
  - Returns: A clickable resource link for Meta authentication
281
259
 
282
- ## Create a Meta Developer App
283
-
284
- Before using the MCP server, you'll need to set up a Meta Developer App:
285
-
286
- 1. Go to [Meta for Developers](https://developers.facebook.com/) and create a new app
287
- 2. Choose the "Consumer" app type
288
- 3. In your app settings, add the "Marketing API" product
289
- 4. Configure your app's OAuth redirect URI to include `http://localhost:8888/callback`
290
- 5. Note your App ID (Client ID) for use with the MCP
291
-
292
260
  ## Authentication
293
261
 
294
- The Meta Ads MCP supports two authentication methods:
295
-
296
- ### 1. Pipeboard Authentication (Recommended ⭐)
297
-
298
- This method uses [Pipeboard.co](https://pipeboard.co) to manage Meta API authentication, providing longer-lived tokens and a simplified flow:
299
-
300
- 1. **Get your Pipeboard token**: Sign up at [https://pipeboard.co](https://pipeboard.co) to generate your free API token
301
- 2. Set the `PIPEBOARD_API_TOKEN` environment variable with your token:
302
- ```bash
303
- export PIPEBOARD_API_TOKEN=your_pipeboard_token
304
- ```
305
- 3. Run the Meta Ads MCP normally - it will automatically detect and use Pipeboard authentication:
306
- ```bash
307
- uvx meta-ads-mcp
308
- ```
309
- 4. The first time you run a command, you'll be provided with a login URL to authorize with Meta
310
-
311
- **Benefits of Pipeboard authentication:**
312
- - ✅ Longer-lived tokens (60 days)
313
- - ✅ No need to configure a Meta Developer App
314
- - ✅ Simpler setup with just an API token
315
- - ✅ Automatic token renewal
316
-
317
- To test the Pipeboard authentication flow:
318
- ```bash
319
- python test_pipeboard_auth.py --api-token YOUR_PIPEBOARD_TOKEN
320
- ```
321
-
322
- ### 2. Direct Meta OAuth (Legacy)
323
-
324
- The traditional OAuth 2.0 flow designed for desktop apps. This method should only be used if you are using your own Facebook app instead of Pipeboard.
262
+ The Meta Ads MCP uses Meta's OAuth 2.0 authentication flow, designed for desktop apps:
325
263
 
326
264
  When authenticating, it will:
327
265
 
@@ -330,7 +268,7 @@ When authenticating, it will:
330
268
  3. Ask you to authorize the app
331
269
  4. Redirect back to the local server to extract and store the token securely
332
270
 
333
- This method requires you to [create a Meta Developer App](#create-a-meta-developer-app) first.
271
+ This method requires you to [create a Meta Developer App](#create-a-meta-developer-app) as described above.
334
272
 
335
273
  ## Troubleshooting and Logging
336
274
 
@@ -348,19 +286,11 @@ Log files are stored in a platform-specific location:
348
286
 
349
287
  #### Authentication Issues
350
288
 
351
- If you're having authentication problems:
352
-
353
- 1. **Recommended: Use Pipeboard Authentication**
354
- - Set `export PIPEBOARD_API_TOKEN=your_token` and retry
355
- - This provides longer-lived tokens and better reliability
356
- - Verify your token in the Pipeboard dashboard
357
-
358
- 2. For App ID issues (when using direct authentication):
359
- If you encounter errors like `(#200) Provide valid app ID`, check the following:
360
- - Ensure you've set up a Meta Developer App correctly
361
- - Verify that you're passing the correct App ID using one of these methods:
362
- - Set the `META_APP_ID` environment variable: `export META_APP_ID=your_app_id`
363
- - Pass it as a command-line argument: `meta-ads-mcp --app-id your_app_id`
289
+ If you encounter errors like `(#200) Provide valid app ID`, check the following:
290
+ - Ensure you've set up a Meta Developer App correctly
291
+ - Verify that you're passing the correct App ID using one of these methods:
292
+ - Set the `META_APP_ID` environment variable: `export META_APP_ID=your_app_id`
293
+ - Pass it as a command-line argument: `meta-ads-mcp --app-id your_app_id`
364
294
 
365
295
  #### API Errors
366
296
 
@@ -399,15 +329,24 @@ uvx meta-ads-mcp --app-id=your_app_id
399
329
  The Meta Ads MCP follows security best practices:
400
330
 
401
331
  1. Tokens are cached in a platform-specific secure location:
402
- - Windows: `%APPDATA%\meta-ads-mcp\token_cache.json` or `%APPDATA%\meta-ads-mcp\pipeboard_token_cache.json`
403
- - macOS: `~/Library/Application Support/meta-ads-mcp/token_cache.json` or `~/Library/Application Support/meta-ads-mcp/pipeboard_token_cache.json`
404
- - Linux: `~/.config/meta-ads-mcp/token_cache.json` or `~/.config/meta-ads-mcp/pipeboard_token_cache.json`
332
+ - Windows: `%APPDATA%\meta-ads-mcp\token_cache.json`
333
+ - macOS: `~/Library/Application Support/meta-ads-mcp/token_cache.json`
334
+ - Linux: `~/.config/meta-ads-mcp/token_cache.json`
405
335
 
406
336
  2. You do not need to provide your access token for each command; it will be automatically retrieved from the cache.
407
337
 
408
- 3. You can set the following environment variables instead of passing them as arguments:
409
- - `META_APP_ID`: Your Meta App ID (Client ID) - for direct OAuth method
410
- - `PIPEBOARD_API_TOKEN`: Your Pipeboard API token - for Pipeboard authentication method
338
+ 3. You can set the `META_APP_ID` environment variable instead of passing it as an argument:
339
+ ```bash
340
+ export META_APP_ID=your_app_id
341
+ uvx meta-ads-mcp
342
+ ```
343
+
344
+ 4. You can provide a direct access token using the `META_ACCESS_TOKEN` environment variable. This bypasses both the local token cache and the Pipeboard authentication method:
345
+ ```bash
346
+ export META_ACCESS_TOKEN=your_access_token
347
+ uvx meta-ads-mcp
348
+ ```
349
+ This is useful for CI/CD pipelines or when you already have a valid access token from another source.
411
350
 
412
351
  ## Testing
413
352
 
@@ -429,10 +368,9 @@ python test_meta_ads_auth.py --app-id YOUR_APP_ID --force-login
429
368
 
430
369
  When using the Meta Ads MCP with an LLM interface (like Claude):
431
370
 
432
- 1. If using direct Meta authentication (your own Facebook app), test authentication by calling the `mcp_meta_ads_get_login_link` tool
433
- 2. If using Pipeboard authentication (recommended), ensure the PIPEBOARD_API_TOKEN environment variable is set (token obtainable via https://pipeboard.co)
434
- 3. Verify account access by calling `mcp_meta_ads_get_ad_accounts`
435
- 4. Check specific account details with `mcp_meta_ads_get_account_info`
371
+ 1. Test authentication by calling the `mcp_meta_ads_get_login_link` tool
372
+ 2. Verify account access by calling `mcp_meta_ads_get_ad_accounts`
373
+ 3. Check specific account details with `mcp_meta_ads_get_account_info`
436
374
 
437
375
  These functions will automatically handle authentication if needed and provide a clickable login link if required.
438
376
 
@@ -443,20 +381,19 @@ These functions will automatically handle authentication if needed and provide a
443
381
  If you encounter authentication issues:
444
382
 
445
383
  1. When using the LLM interface:
446
- - If using direct Meta authentication (your own Facebook app), use the `mcp_meta_ads_get_login_link` tool to generate a fresh authentication link
447
- - If using Pipeboard authentication (recommended), ensure the PIPEBOARD_API_TOKEN environment variable is set (token obtainable via https://pipeboard.co)
384
+ - Use the `mcp_meta_ads_get_login_link` tool to generate a fresh authentication link
448
385
  - Ensure you click the link and complete the authorization flow in your browser
449
386
  - Check that the callback server is running properly (the tool will report this)
450
387
 
451
- 2. When using Pipeboard authentication:
452
- - Verify your `PIPEBOARD_API_TOKEN` is set correctly (token obtainable via https://pipeboard.co)
453
- - Check if you need to complete the authorization process by visiting the provided login URL
454
- - Try forcing a new login: `python test_pipeboard_auth.py --force-login`
455
-
456
- 3. When using direct Meta OAuth:
388
+ 2. When using direct Meta OAuth:
457
389
  - Run with `--force-login` to get a fresh token: `uvx meta-ads-mcp --login --app-id YOUR_APP_ID --force-login`
458
390
  - Make sure the terminal has permissions to open a browser window
459
391
 
392
+ 3. Skip authentication entirely by providing a token directly:
393
+ - If you already have a valid access token, you can bypass the authentication flow:
394
+ - `export META_ACCESS_TOKEN=your_access_token`
395
+ - This will ignore both the local token cache and the Pipeboard authentication
396
+
460
397
  ### API Errors
461
398
 
462
399
  If you receive errors from the Meta API:
@@ -0,0 +1,23 @@
1
+ meta_ads_mcp/__init__.py,sha256=xQxyDpHFTg1FE9C3YEk88za2I8lvr8Mg0h9dMTxc5dw,1236
2
+ meta_ads_mcp/__main__.py,sha256=XaQt3iXftG_7f0Zu7Wop9SeFgrD2WBn0EQOaPMc27d8,207
3
+ meta_ads_mcp/api.py,sha256=z0pW1pV3hE75IeG9QTqB3K7QoQOUxUg2MBQ9IjAWUYA,84363
4
+ meta_ads_mcp/core/__init__.py,sha256=MOCvj3SkLvr5hFHqyjWOx7L4oReKLCoZaotwspJLwZQ,1022
5
+ meta_ads_mcp/core/accounts.py,sha256=Nmp7lPxO9wmq25jWV7_H0LIqnEbBhpCVBlLGW2HUaq0,2277
6
+ meta_ads_mcp/core/ads.py,sha256=b_81GlGHIM4jISvuDZmHNyc6uW7uD3ovX68ezBci9MM,29747
7
+ meta_ads_mcp/core/ads_library.py,sha256=onStn9UkRqYDC60gOPS-iKDtP1plz6DygUb7hUZ0Jw8,2807
8
+ meta_ads_mcp/core/adsets.py,sha256=WBPNaI7ITnUOnGMus4_0MX15DslOCzfM5q1zF1VWs2s,12408
9
+ meta_ads_mcp/core/api.py,sha256=9Whcs2orILhPiWkAR3qGmJNouYE5uri_e_Jzeh5Hjn8,14208
10
+ meta_ads_mcp/core/auth.py,sha256=pDARBh3NBNqCpxflVrVvR4VsWuIveFxQmb9-P-gLFDM,20730
11
+ meta_ads_mcp/core/authentication.py,sha256=2MG13r28OlIcOIgPSRrGXJ2-4JSt3ifU-oB9tiOsrKQ,6511
12
+ meta_ads_mcp/core/callback_server.py,sha256=AUymElaVwHqFyqB2wgqf6A68KsqwtKoYmY-7JZZt8Ks,43286
13
+ meta_ads_mcp/core/campaigns.py,sha256=TQHDhJ0s7cLbo5-zd2Bk8YgwToWBoK3YBMFG8fZbEHI,10757
14
+ meta_ads_mcp/core/insights.py,sha256=XAm4uu83gWp84PEGqAJ3GFIqlvg7prh6MdD71JfvBCo,18072
15
+ meta_ads_mcp/core/pipeboard_auth.py,sha256=VvbxEB8ZOhnMccLU7HI1HgaPWHCl5NGrzZCm-zzHze4,22798
16
+ meta_ads_mcp/core/resources.py,sha256=-zIIfZulpo76vcKv6jhAlQq91cR2SZ3cjYZt3ek3x0w,1236
17
+ meta_ads_mcp/core/server.py,sha256=5WofyJZGzeDhbGzLXPhQjT0XnZwo0syeK8TM_XnJo4Q,5507
18
+ meta_ads_mcp/core/utils.py,sha256=EPmpBX3OZaTWRS_YuEk_PLLyLXj7DeR6Ks8WoaZ5JGQ,6366
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,,
@@ -1,22 +0,0 @@
1
- meta_ads_mcp/__init__.py,sha256=cV-XErlZOBmJRfXvhCP-MGcbTQ8DUmpjEzInKrDrxp4,1236
2
- meta_ads_mcp/__main__.py,sha256=XaQt3iXftG_7f0Zu7Wop9SeFgrD2WBn0EQOaPMc27d8,207
3
- meta_ads_mcp/api.py,sha256=Lz9n2OTANu-BjNklUqfB2hC_oY1LMoSwzO-iVNHsV58,81984
4
- meta_ads_mcp/core/__init__.py,sha256=6T8iqrQrw9VHhKtncLqYWyDk8jeSBPs79hs1CSu-fLU,952
5
- meta_ads_mcp/core/accounts.py,sha256=Nmp7lPxO9wmq25jWV7_H0LIqnEbBhpCVBlLGW2HUaq0,2277
6
- meta_ads_mcp/core/ads.py,sha256=LMZOo6agi6tQl4JJPmrDUn-91n7DzfxG2TChmwcrWOY,12544
7
- meta_ads_mcp/core/adsets.py,sha256=os8MdPdHO6mGIRQdTlx7PxvcLRq8B9BhybQIHLD37Qg,12382
8
- meta_ads_mcp/core/api.py,sha256=M_tM5qdCGso5lKeVJ0g3ss_C5WzUs95oESpdE7-PoZc,14182
9
- meta_ads_mcp/core/auth.py,sha256=3pqoTuPtpvmv7j3qaD_bdcYrAzOJheKJNDru0tduz14,20184
10
- meta_ads_mcp/core/authentication.py,sha256=3AHSXslZdxyg0_s2253aQhpOeguMSu2cSYq4H_auywY,6485
11
- meta_ads_mcp/core/callback_server.py,sha256=b5TzUz9nEk0i5MWujlls5gAsHru__UjTPJQan1xQ_10,40947
12
- meta_ads_mcp/core/campaigns.py,sha256=20DHMwHppZuoZBg0owkf1BfmPBhWzkQcFgh5rQa-pAU,10480
13
- meta_ads_mcp/core/insights.py,sha256=XAm4uu83gWp84PEGqAJ3GFIqlvg7prh6MdD71JfvBCo,18072
14
- meta_ads_mcp/core/pipeboard_auth.py,sha256=VvbxEB8ZOhnMccLU7HI1HgaPWHCl5NGrzZCm-zzHze4,22798
15
- meta_ads_mcp/core/resources.py,sha256=-zIIfZulpo76vcKv6jhAlQq91cR2SZ3cjYZt3ek3x0w,1236
16
- meta_ads_mcp/core/server.py,sha256=5WofyJZGzeDhbGzLXPhQjT0XnZwo0syeK8TM_XnJo4Q,5507
17
- meta_ads_mcp/core/utils.py,sha256=EPmpBX3OZaTWRS_YuEk_PLLyLXj7DeR6Ks8WoaZ5JGQ,6366
18
- meta_ads_mcp-0.3.1.dist-info/METADATA,sha256=OAB-X-RsvdW9cqrxaZb57_JEAglrMKM56ByPsOw-sUk,19594
19
- meta_ads_mcp-0.3.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
20
- meta_ads_mcp-0.3.1.dist-info/entry_points.txt,sha256=Dv2RkoBjRJBqj6CyhwqGIiwPCD-SCL1-7B9-zmVRuv0,57
21
- meta_ads_mcp-0.3.1.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
22
- meta_ads_mcp-0.3.1.dist-info/RECORD,,