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 +1 -1
- meta_ads_mcp/api.py +55 -1
- meta_ads_mcp/core/__init__.py +2 -0
- meta_ads_mcp/core/ads.py +463 -1
- meta_ads_mcp/core/ads_library.py +69 -0
- meta_ads_mcp/core/adsets.py +1 -1
- meta_ads_mcp/core/api.py +1 -1
- meta_ads_mcp/core/auth.py +13 -2
- meta_ads_mcp/core/authentication.py +1 -1
- meta_ads_mcp/core/callback_server.py +55 -1
- meta_ads_mcp/core/campaigns.py +5 -2
- {meta_ads_mcp-0.3.1.dist-info → meta_ads_mcp-0.3.3.dist-info}/METADATA +69 -132
- meta_ads_mcp-0.3.3.dist-info/RECORD +23 -0
- meta_ads_mcp-0.3.1.dist-info/RECORD +0 -22
- {meta_ads_mcp-0.3.1.dist-info → meta_ads_mcp-0.3.3.dist-info}/WHEEL +0 -0
- {meta_ads_mcp-0.3.1.dist-info → meta_ads_mcp-0.3.3.dist-info}/entry_points.txt +0 -0
- {meta_ads_mcp-0.3.1.dist-info → meta_ads_mcp-0.3.3.dist-info}/licenses/LICENSE +0 -0
meta_ads_mcp/__init__.py
CHANGED
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:
|
meta_ads_mcp/core/__init__.py
CHANGED
|
@@ -9,6 +9,7 @@ from .insights import get_insights, debug_image_download
|
|
|
9
9
|
from .authentication import get_login_link
|
|
10
10
|
from .server import login_cli, main
|
|
11
11
|
from .auth import login
|
|
12
|
+
from .ads_library import search_ads_archive
|
|
12
13
|
|
|
13
14
|
__all__ = [
|
|
14
15
|
'mcp_server',
|
|
@@ -31,4 +32,5 @@ __all__ = [
|
|
|
31
32
|
'login_cli',
|
|
32
33
|
'login',
|
|
33
34
|
'main',
|
|
35
|
+
'search_ads_archive',
|
|
34
36
|
]
|
meta_ads_mcp/core/ads.py
CHANGED
|
@@ -5,6 +5,8 @@ from typing import Optional, Dict, Any, List
|
|
|
5
5
|
import io
|
|
6
6
|
from PIL import Image as PILImage
|
|
7
7
|
from mcp.server.fastmcp import Image
|
|
8
|
+
import os
|
|
9
|
+
import time
|
|
8
10
|
|
|
9
11
|
from .api import meta_api_tool, make_api_request
|
|
10
12
|
from .accounts import get_ad_accounts
|
|
@@ -317,6 +319,132 @@ async def get_ad_image(access_token: str = None, ad_id: str = None) -> Image:
|
|
|
317
319
|
return f"Error processing image: {str(e)}"
|
|
318
320
|
|
|
319
321
|
|
|
322
|
+
@mcp_server.tool()
|
|
323
|
+
@meta_api_tool
|
|
324
|
+
async def save_ad_image_locally(access_token: str = None, ad_id: str = None, output_dir: str = "ad_images") -> str:
|
|
325
|
+
"""
|
|
326
|
+
Get, download, and save a Meta ad image locally, returning the file path.
|
|
327
|
+
|
|
328
|
+
Args:
|
|
329
|
+
access_token: Meta API access token (optional - will use cached token if not provided)
|
|
330
|
+
ad_id: Meta Ads ad ID
|
|
331
|
+
output_dir: Directory to save the image file (default: 'ad_images')
|
|
332
|
+
|
|
333
|
+
Returns:
|
|
334
|
+
The file path to the saved image, or an error message string.
|
|
335
|
+
"""
|
|
336
|
+
if not ad_id:
|
|
337
|
+
return json.dumps({"error": "No ad ID provided"}, indent=2)
|
|
338
|
+
|
|
339
|
+
print(f"Attempting to get and save creative image for ad {ad_id}")
|
|
340
|
+
|
|
341
|
+
# First, get creative and account IDs
|
|
342
|
+
ad_endpoint = f"{ad_id}"
|
|
343
|
+
ad_params = {
|
|
344
|
+
"fields": "creative{id},account_id"
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
ad_data = await make_api_request(ad_endpoint, access_token, ad_params)
|
|
348
|
+
|
|
349
|
+
if "error" in ad_data:
|
|
350
|
+
return json.dumps({"error": f"Could not get ad data - {json.dumps(ad_data)}"}, indent=2)
|
|
351
|
+
|
|
352
|
+
account_id = ad_data.get("account_id")
|
|
353
|
+
if not account_id:
|
|
354
|
+
return json.dumps({"error": "No account ID found for ad"}, indent=2)
|
|
355
|
+
|
|
356
|
+
if "creative" not in ad_data:
|
|
357
|
+
return json.dumps({"error": "No creative found for this ad"}, indent=2)
|
|
358
|
+
|
|
359
|
+
creative_data = ad_data.get("creative", {})
|
|
360
|
+
creative_id = creative_data.get("id")
|
|
361
|
+
if not creative_id:
|
|
362
|
+
return json.dumps({"error": "No creative ID found"}, indent=2)
|
|
363
|
+
|
|
364
|
+
# Get creative details to find image hash
|
|
365
|
+
creative_endpoint = f"{creative_id}"
|
|
366
|
+
creative_params = {
|
|
367
|
+
"fields": "id,name,image_hash,asset_feed_spec"
|
|
368
|
+
}
|
|
369
|
+
creative_details = await make_api_request(creative_endpoint, access_token, creative_params)
|
|
370
|
+
|
|
371
|
+
image_hashes = []
|
|
372
|
+
if "image_hash" in creative_details:
|
|
373
|
+
image_hashes.append(creative_details["image_hash"])
|
|
374
|
+
if "asset_feed_spec" in creative_details and "images" in creative_details["asset_feed_spec"]:
|
|
375
|
+
for image in creative_details["asset_feed_spec"]["images"]:
|
|
376
|
+
if "hash" in image:
|
|
377
|
+
image_hashes.append(image["hash"])
|
|
378
|
+
|
|
379
|
+
if not image_hashes:
|
|
380
|
+
# Fallback attempt (as in get_ad_image)
|
|
381
|
+
creative_json = await get_ad_creatives(ad_id=ad_id, access_token=access_token) # Ensure ad_id is passed correctly
|
|
382
|
+
creative_data_list = json.loads(creative_json)
|
|
383
|
+
if 'data' in creative_data_list and creative_data_list['data']:
|
|
384
|
+
first_creative = creative_data_list['data'][0]
|
|
385
|
+
if 'object_story_spec' in first_creative and 'link_data' in first_creative['object_story_spec'] and 'image_hash' in first_creative['object_story_spec']['link_data']:
|
|
386
|
+
image_hashes.append(first_creative['object_story_spec']['link_data']['image_hash'])
|
|
387
|
+
elif 'image_hash' in first_creative: # Check direct hash on creative data
|
|
388
|
+
image_hashes.append(first_creative['image_hash'])
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
if not image_hashes:
|
|
392
|
+
return json.dumps({"error": "No image hashes found in creative or fallback"}, indent=2)
|
|
393
|
+
|
|
394
|
+
print(f"Found image hashes: {image_hashes}")
|
|
395
|
+
|
|
396
|
+
# Fetch image data using the first hash
|
|
397
|
+
image_endpoint = f"act_{account_id}/adimages"
|
|
398
|
+
hashes_str = f'["{image_hashes[0]}"]'
|
|
399
|
+
image_params = {
|
|
400
|
+
"fields": "hash,url,width,height,name,status",
|
|
401
|
+
"hashes": hashes_str
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
print(f"Requesting image data with params: {image_params}")
|
|
405
|
+
image_data = await make_api_request(image_endpoint, access_token, image_params)
|
|
406
|
+
|
|
407
|
+
if "error" in image_data:
|
|
408
|
+
return json.dumps({"error": f"Failed to get image data - {json.dumps(image_data)}"}, indent=2)
|
|
409
|
+
|
|
410
|
+
if "data" not in image_data or not image_data["data"]:
|
|
411
|
+
return json.dumps({"error": "No image data returned from API"}, indent=2)
|
|
412
|
+
|
|
413
|
+
first_image = image_data["data"][0]
|
|
414
|
+
image_url = first_image.get("url")
|
|
415
|
+
|
|
416
|
+
if not image_url:
|
|
417
|
+
return json.dumps({"error": "No valid image URL found in API response"}, indent=2)
|
|
418
|
+
|
|
419
|
+
print(f"Downloading image from URL: {image_url}")
|
|
420
|
+
|
|
421
|
+
# Download and Save Image
|
|
422
|
+
image_bytes = await download_image(image_url)
|
|
423
|
+
|
|
424
|
+
if not image_bytes:
|
|
425
|
+
return json.dumps({"error": "Failed to download image"}, indent=2)
|
|
426
|
+
|
|
427
|
+
try:
|
|
428
|
+
# Ensure output directory exists
|
|
429
|
+
if not os.path.exists(output_dir):
|
|
430
|
+
os.makedirs(output_dir)
|
|
431
|
+
|
|
432
|
+
# Create a filename (e.g., using ad_id and image hash)
|
|
433
|
+
file_extension = ".jpg" # Default extension, could try to infer from headers later
|
|
434
|
+
filename = f"{ad_id}_{image_hashes[0]}{file_extension}"
|
|
435
|
+
filepath = os.path.join(output_dir, filename)
|
|
436
|
+
|
|
437
|
+
# Save the image bytes to the file
|
|
438
|
+
with open(filepath, "wb") as f:
|
|
439
|
+
f.write(image_bytes)
|
|
440
|
+
|
|
441
|
+
print(f"Image saved successfully to: {filepath}")
|
|
442
|
+
return json.dumps({"filepath": filepath}, indent=2) # Return JSON with filepath
|
|
443
|
+
|
|
444
|
+
except Exception as e:
|
|
445
|
+
return json.dumps({"error": f"Failed to save image: {str(e)}"}, indent=2)
|
|
446
|
+
|
|
447
|
+
|
|
320
448
|
@mcp_server.tool()
|
|
321
449
|
@meta_api_tool
|
|
322
450
|
async def update_ad(
|
|
@@ -354,4 +482,338 @@ async def update_ad(
|
|
|
354
482
|
endpoint = f"{ad_id}"
|
|
355
483
|
data = await make_api_request(endpoint, access_token, params, method='POST')
|
|
356
484
|
|
|
357
|
-
return json.dumps(data, indent=2)
|
|
485
|
+
return json.dumps(data, indent=2)
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
@mcp_server.tool()
|
|
489
|
+
@meta_api_tool
|
|
490
|
+
async def upload_ad_image(
|
|
491
|
+
access_token: str = None,
|
|
492
|
+
account_id: str = None,
|
|
493
|
+
image_path: str = None,
|
|
494
|
+
name: str = None
|
|
495
|
+
) -> str:
|
|
496
|
+
"""
|
|
497
|
+
Upload an image to use in Meta Ads creatives.
|
|
498
|
+
|
|
499
|
+
Args:
|
|
500
|
+
access_token: Meta API access token (optional - will use cached token if not provided)
|
|
501
|
+
account_id: Meta Ads account ID (format: act_XXXXXXXXX)
|
|
502
|
+
image_path: Path to the image file to upload
|
|
503
|
+
name: Optional name for the image (default: filename)
|
|
504
|
+
|
|
505
|
+
Returns:
|
|
506
|
+
JSON response with image details including hash for creative creation
|
|
507
|
+
"""
|
|
508
|
+
# Check required parameters
|
|
509
|
+
if not account_id:
|
|
510
|
+
return json.dumps({"error": "No account ID provided"}, indent=2)
|
|
511
|
+
|
|
512
|
+
if not image_path:
|
|
513
|
+
return json.dumps({"error": "No image path provided"}, indent=2)
|
|
514
|
+
|
|
515
|
+
# Ensure account_id has the 'act_' prefix for API compatibility
|
|
516
|
+
if not account_id.startswith("act_"):
|
|
517
|
+
account_id = f"act_{account_id}"
|
|
518
|
+
|
|
519
|
+
# Check if image file exists
|
|
520
|
+
if not os.path.exists(image_path):
|
|
521
|
+
return json.dumps({"error": f"Image file not found: {image_path}"}, indent=2)
|
|
522
|
+
|
|
523
|
+
try:
|
|
524
|
+
# Read image file
|
|
525
|
+
with open(image_path, "rb") as img_file:
|
|
526
|
+
image_bytes = img_file.read()
|
|
527
|
+
|
|
528
|
+
# Get image filename if name not provided
|
|
529
|
+
if not name:
|
|
530
|
+
name = os.path.basename(image_path)
|
|
531
|
+
|
|
532
|
+
# Prepare the API endpoint for uploading images
|
|
533
|
+
endpoint = f"{account_id}/adimages"
|
|
534
|
+
|
|
535
|
+
# We need to convert the binary data to base64 for API upload
|
|
536
|
+
import base64
|
|
537
|
+
encoded_image = base64.b64encode(image_bytes).decode('utf-8')
|
|
538
|
+
|
|
539
|
+
# Prepare POST parameters
|
|
540
|
+
params = {
|
|
541
|
+
"bytes": encoded_image,
|
|
542
|
+
"name": name
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
# Make API request to upload the image
|
|
546
|
+
print(f"Uploading image to Facebook Ad Account {account_id}")
|
|
547
|
+
data = await make_api_request(endpoint, access_token, params, method="POST")
|
|
548
|
+
|
|
549
|
+
return json.dumps(data, indent=2)
|
|
550
|
+
|
|
551
|
+
except Exception as e:
|
|
552
|
+
return json.dumps({
|
|
553
|
+
"error": "Failed to upload image",
|
|
554
|
+
"details": str(e)
|
|
555
|
+
}, indent=2)
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
@mcp_server.tool()
|
|
559
|
+
@meta_api_tool
|
|
560
|
+
async def create_ad_creative(
|
|
561
|
+
access_token: str = None,
|
|
562
|
+
account_id: str = None,
|
|
563
|
+
name: str = None,
|
|
564
|
+
image_hash: str = None,
|
|
565
|
+
page_id: str = None,
|
|
566
|
+
link_url: str = None,
|
|
567
|
+
message: str = None,
|
|
568
|
+
headline: str = None,
|
|
569
|
+
description: str = None,
|
|
570
|
+
call_to_action_type: str = None,
|
|
571
|
+
instagram_actor_id: str = None
|
|
572
|
+
) -> str:
|
|
573
|
+
"""
|
|
574
|
+
Create a new ad creative using an uploaded image hash.
|
|
575
|
+
|
|
576
|
+
Args:
|
|
577
|
+
access_token: Meta API access token (optional - will use cached token if not provided)
|
|
578
|
+
account_id: Meta Ads account ID (format: act_XXXXXXXXX)
|
|
579
|
+
name: Creative name
|
|
580
|
+
image_hash: Hash of the uploaded image
|
|
581
|
+
page_id: Facebook Page ID to be used for the ad
|
|
582
|
+
link_url: Destination URL for the ad
|
|
583
|
+
message: Ad copy/text
|
|
584
|
+
headline: Ad headline
|
|
585
|
+
description: Ad description
|
|
586
|
+
call_to_action_type: Call to action button type (e.g., 'LEARN_MORE', 'SIGN_UP', 'SHOP_NOW')
|
|
587
|
+
instagram_actor_id: Optional Instagram account ID for Instagram placements
|
|
588
|
+
|
|
589
|
+
Returns:
|
|
590
|
+
JSON response with created creative details
|
|
591
|
+
"""
|
|
592
|
+
# Check required parameters
|
|
593
|
+
if not account_id:
|
|
594
|
+
return json.dumps({"error": "No account ID provided"}, indent=2)
|
|
595
|
+
|
|
596
|
+
if not image_hash:
|
|
597
|
+
return json.dumps({"error": "No image hash provided"}, indent=2)
|
|
598
|
+
|
|
599
|
+
if not name:
|
|
600
|
+
name = f"Creative {int(time.time())}"
|
|
601
|
+
|
|
602
|
+
# Ensure account_id has the 'act_' prefix
|
|
603
|
+
if not account_id.startswith("act_"):
|
|
604
|
+
account_id = f"act_{account_id}"
|
|
605
|
+
|
|
606
|
+
# If no page ID is provided, try to find a page associated with the account
|
|
607
|
+
if not page_id:
|
|
608
|
+
try:
|
|
609
|
+
# Query to get pages associated with the account
|
|
610
|
+
pages_endpoint = f"{account_id}/assigned_pages"
|
|
611
|
+
pages_params = {
|
|
612
|
+
"fields": "id,name",
|
|
613
|
+
"limit": 1
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
pages_data = await make_api_request(pages_endpoint, access_token, pages_params)
|
|
617
|
+
|
|
618
|
+
if "data" in pages_data and pages_data["data"]:
|
|
619
|
+
page_id = pages_data["data"][0]["id"]
|
|
620
|
+
print(f"Using page ID: {page_id} ({pages_data['data'][0].get('name', 'Unknown')})")
|
|
621
|
+
else:
|
|
622
|
+
return json.dumps({
|
|
623
|
+
"error": "No page ID provided and no pages found for this account",
|
|
624
|
+
"suggestion": "Please provide a page_id parameter"
|
|
625
|
+
}, indent=2)
|
|
626
|
+
except Exception as e:
|
|
627
|
+
return json.dumps({
|
|
628
|
+
"error": "Error finding page for account",
|
|
629
|
+
"details": str(e),
|
|
630
|
+
"suggestion": "Please provide a page_id parameter"
|
|
631
|
+
}, indent=2)
|
|
632
|
+
|
|
633
|
+
# Prepare the creative data
|
|
634
|
+
creative_data = {
|
|
635
|
+
"name": name,
|
|
636
|
+
"object_story_spec": {
|
|
637
|
+
"page_id": page_id,
|
|
638
|
+
"link_data": {
|
|
639
|
+
"image_hash": image_hash,
|
|
640
|
+
"link": link_url if link_url else "https://facebook.com"
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
# Add optional parameters if provided
|
|
646
|
+
if message:
|
|
647
|
+
creative_data["object_story_spec"]["link_data"]["message"] = message
|
|
648
|
+
|
|
649
|
+
if headline:
|
|
650
|
+
creative_data["object_story_spec"]["link_data"]["name"] = headline
|
|
651
|
+
|
|
652
|
+
if description:
|
|
653
|
+
creative_data["object_story_spec"]["link_data"]["description"] = description
|
|
654
|
+
|
|
655
|
+
if call_to_action_type:
|
|
656
|
+
creative_data["object_story_spec"]["link_data"]["call_to_action"] = {
|
|
657
|
+
"type": call_to_action_type
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
if instagram_actor_id:
|
|
661
|
+
creative_data["instagram_actor_id"] = instagram_actor_id
|
|
662
|
+
|
|
663
|
+
# Prepare the API endpoint for creating a creative
|
|
664
|
+
endpoint = f"{account_id}/adcreatives"
|
|
665
|
+
|
|
666
|
+
try:
|
|
667
|
+
# Make API request to create the creative
|
|
668
|
+
data = await make_api_request(endpoint, access_token, creative_data, method="POST")
|
|
669
|
+
|
|
670
|
+
# If successful, get more details about the created creative
|
|
671
|
+
if "id" in data:
|
|
672
|
+
creative_id = data["id"]
|
|
673
|
+
creative_endpoint = f"{creative_id}"
|
|
674
|
+
creative_params = {
|
|
675
|
+
"fields": "id,name,status,thumbnail_url,image_url,image_hash,object_story_spec,url_tags,link_url"
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
creative_details = await make_api_request(creative_endpoint, access_token, creative_params)
|
|
679
|
+
return json.dumps({
|
|
680
|
+
"success": True,
|
|
681
|
+
"creative_id": creative_id,
|
|
682
|
+
"details": creative_details
|
|
683
|
+
}, indent=2)
|
|
684
|
+
|
|
685
|
+
return json.dumps(data, indent=2)
|
|
686
|
+
|
|
687
|
+
except Exception as e:
|
|
688
|
+
return json.dumps({
|
|
689
|
+
"error": "Failed to create ad creative",
|
|
690
|
+
"details": str(e),
|
|
691
|
+
"creative_data_sent": creative_data
|
|
692
|
+
}, indent=2)
|
|
693
|
+
|
|
694
|
+
|
|
695
|
+
@mcp_server.tool()
|
|
696
|
+
@meta_api_tool
|
|
697
|
+
async def get_account_pages(access_token: str = None, account_id: str = None) -> str:
|
|
698
|
+
"""
|
|
699
|
+
Get pages associated with a Meta Ads account.
|
|
700
|
+
|
|
701
|
+
Args:
|
|
702
|
+
access_token: Meta API access token (optional - will use cached token if not provided)
|
|
703
|
+
account_id: Meta Ads account ID (format: act_XXXXXXXXX)
|
|
704
|
+
|
|
705
|
+
Returns:
|
|
706
|
+
JSON response with pages associated with the account
|
|
707
|
+
"""
|
|
708
|
+
# Check required parameters
|
|
709
|
+
if not account_id:
|
|
710
|
+
return json.dumps({"error": "No account ID provided"}, indent=2)
|
|
711
|
+
|
|
712
|
+
# Handle special case for 'me'
|
|
713
|
+
if account_id == "me":
|
|
714
|
+
try:
|
|
715
|
+
endpoint = "me/accounts"
|
|
716
|
+
params = {
|
|
717
|
+
"fields": "id,name,username,category,fan_count,link,verification_status,picture"
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
user_pages_data = await make_api_request(endpoint, access_token, params)
|
|
721
|
+
return json.dumps(user_pages_data, indent=2)
|
|
722
|
+
except Exception as e:
|
|
723
|
+
return json.dumps({
|
|
724
|
+
"error": "Failed to get user pages",
|
|
725
|
+
"details": str(e)
|
|
726
|
+
}, indent=2)
|
|
727
|
+
|
|
728
|
+
# Ensure account_id has the 'act_' prefix for regular accounts
|
|
729
|
+
if not account_id.startswith("act_"):
|
|
730
|
+
account_id = f"act_{account_id}"
|
|
731
|
+
|
|
732
|
+
try:
|
|
733
|
+
# Try all approaches that might work
|
|
734
|
+
|
|
735
|
+
# Approach 1: Get active ads and extract page IDs
|
|
736
|
+
endpoint = f"{account_id}/ads"
|
|
737
|
+
params = {
|
|
738
|
+
"fields": "creative{object_story_spec{page_id}}",
|
|
739
|
+
"limit": 100
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
ads_data = await make_api_request(endpoint, access_token, params)
|
|
743
|
+
|
|
744
|
+
# Extract unique page IDs from ads
|
|
745
|
+
page_ids = set()
|
|
746
|
+
if "data" in ads_data:
|
|
747
|
+
for ad in ads_data.get("data", []):
|
|
748
|
+
if "creative" in ad and "creative" in ad and "object_story_spec" in ad["creative"] and "page_id" in ad["creative"]["object_story_spec"]:
|
|
749
|
+
page_ids.add(ad["creative"]["object_story_spec"]["page_id"])
|
|
750
|
+
|
|
751
|
+
# If we found page IDs, get details for each
|
|
752
|
+
if page_ids:
|
|
753
|
+
page_details = {"data": []}
|
|
754
|
+
|
|
755
|
+
for page_id in page_ids:
|
|
756
|
+
page_endpoint = f"{page_id}"
|
|
757
|
+
page_params = {
|
|
758
|
+
"fields": "id,name,username,category,fan_count,link,verification_status,picture"
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
page_data = await make_api_request(page_endpoint, access_token, page_params)
|
|
762
|
+
if "id" in page_data:
|
|
763
|
+
page_details["data"].append(page_data)
|
|
764
|
+
|
|
765
|
+
if page_details["data"]:
|
|
766
|
+
return json.dumps(page_details, indent=2)
|
|
767
|
+
|
|
768
|
+
# Approach 2: Try client_pages endpoint
|
|
769
|
+
endpoint = f"{account_id}/client_pages"
|
|
770
|
+
params = {
|
|
771
|
+
"fields": "id,name,username,category,fan_count,link,verification_status,picture"
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
client_pages_data = await make_api_request(endpoint, access_token, params)
|
|
775
|
+
|
|
776
|
+
if "data" in client_pages_data and client_pages_data["data"]:
|
|
777
|
+
return json.dumps(client_pages_data, indent=2)
|
|
778
|
+
|
|
779
|
+
# Approach 3: Try promoted_objects endpoint to find page IDs
|
|
780
|
+
endpoint = f"{account_id}/promoted_objects"
|
|
781
|
+
params = {
|
|
782
|
+
"fields": "page_id"
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
promoted_objects_data = await make_api_request(endpoint, access_token, params)
|
|
786
|
+
|
|
787
|
+
if "data" in promoted_objects_data and promoted_objects_data["data"]:
|
|
788
|
+
page_ids = set()
|
|
789
|
+
for obj in promoted_objects_data["data"]:
|
|
790
|
+
if "page_id" in obj:
|
|
791
|
+
page_ids.add(obj["page_id"])
|
|
792
|
+
|
|
793
|
+
if page_ids:
|
|
794
|
+
page_details = {"data": []}
|
|
795
|
+
for page_id in page_ids:
|
|
796
|
+
page_endpoint = f"{page_id}"
|
|
797
|
+
page_params = {
|
|
798
|
+
"fields": "id,name,username,category,fan_count,link,verification_status,picture"
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
page_data = await make_api_request(page_endpoint, access_token, page_params)
|
|
802
|
+
if "id" in page_data:
|
|
803
|
+
page_details["data"].append(page_data)
|
|
804
|
+
|
|
805
|
+
if page_details["data"]:
|
|
806
|
+
return json.dumps(page_details, indent=2)
|
|
807
|
+
|
|
808
|
+
# If all approaches failed, return empty data with a message
|
|
809
|
+
return json.dumps({
|
|
810
|
+
"data": [],
|
|
811
|
+
"message": "No pages found associated with this account",
|
|
812
|
+
"suggestion": "You may need to create a page or provide a page_id explicitly when creating ads"
|
|
813
|
+
}, indent=2)
|
|
814
|
+
|
|
815
|
+
except Exception as e:
|
|
816
|
+
return json.dumps({
|
|
817
|
+
"error": "Failed to get account pages",
|
|
818
|
+
"details": str(e)
|
|
819
|
+
}, indent=2)
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Adds Library-related functionality for Meta Ads API."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Optional, List, Dict, Any
|
|
5
|
+
from .api import meta_api_tool, make_api_request
|
|
6
|
+
from .server import mcp_server
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@mcp_server.tool()
|
|
10
|
+
@meta_api_tool
|
|
11
|
+
async def search_ads_archive(
|
|
12
|
+
access_token: str = None,
|
|
13
|
+
search_terms: str = None,
|
|
14
|
+
ad_type: str = "ALL",
|
|
15
|
+
ad_reached_countries: List[str] = None,
|
|
16
|
+
limit: int = 25, # Default limit, adjust as needed
|
|
17
|
+
fields: str = "ad_creation_time,ad_creative_body,ad_creative_link_caption,ad_creative_link_description,ad_creative_link_title,ad_delivery_start_time,ad_delivery_stop_time,ad_snapshot_url,currency,demographic_distribution,funding_entity,impressions,page_id,page_name,publisher_platform,region_distribution,spend"
|
|
18
|
+
) -> str:
|
|
19
|
+
"""
|
|
20
|
+
Search the Facebook Ads Library archive.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
access_token: Meta API access token (optional - will use cached token if not provided).
|
|
24
|
+
search_terms: The search query for ads.
|
|
25
|
+
ad_type: Type of ads to search for (e.g., POLITICAL_AND_ISSUE_ADS, HOUSING_ADS, ALL).
|
|
26
|
+
ad_reached_countries: List of country codes (e.g., ["US", "GB"]).
|
|
27
|
+
limit: Maximum number of ads to return.
|
|
28
|
+
fields: Comma-separated string of fields to retrieve for each ad.
|
|
29
|
+
|
|
30
|
+
Example Usage via curl equivalent:
|
|
31
|
+
curl -G \\
|
|
32
|
+
-d "search_terms='california'" \\
|
|
33
|
+
-d "ad_type=POLITICAL_AND_ISSUE_ADS" \\
|
|
34
|
+
-d "ad_reached_countries=['US']" \\
|
|
35
|
+
-d "fields=ad_snapshot_url,spend" \\
|
|
36
|
+
-d "access_token=<ACCESS_TOKEN>" \\
|
|
37
|
+
"https://graph.facebook.com/<API_VERSION>/ads_archive"
|
|
38
|
+
"""
|
|
39
|
+
if not access_token:
|
|
40
|
+
# Attempt to get token implicitly if not provided - meta_api_tool handles this
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
if not search_terms:
|
|
44
|
+
return json.dumps({"error": "search_terms parameter is required"}, indent=2)
|
|
45
|
+
|
|
46
|
+
if not ad_reached_countries:
|
|
47
|
+
return json.dumps({"error": "ad_reached_countries parameter is required"}, indent=2)
|
|
48
|
+
|
|
49
|
+
endpoint = "ads_archive"
|
|
50
|
+
params = {
|
|
51
|
+
"search_terms": search_terms,
|
|
52
|
+
"ad_type": ad_type,
|
|
53
|
+
"ad_reached_countries": json.dumps(ad_reached_countries), # API expects a JSON array string
|
|
54
|
+
"limit": limit,
|
|
55
|
+
"fields": fields,
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
data = await make_api_request(endpoint, access_token, params, method="GET")
|
|
60
|
+
return json.dumps(data, indent=2)
|
|
61
|
+
except Exception as e:
|
|
62
|
+
error_msg = str(e)
|
|
63
|
+
# Consider logging the full error for debugging
|
|
64
|
+
# print(f"Error calling Ads Library API: {error_msg}")
|
|
65
|
+
return json.dumps({
|
|
66
|
+
"error": "Failed to search ads archive",
|
|
67
|
+
"details": error_msg,
|
|
68
|
+
"params_sent": {k: v for k, v in params.items() if k != 'access_token'} # Avoid logging token
|
|
69
|
+
}, indent=2)
|
meta_ads_mcp/core/adsets.py
CHANGED
|
@@ -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
|
-
|
|
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:
|
meta_ads_mcp/core/campaigns.py
CHANGED
|
@@ -23,7 +23,9 @@ async def get_campaigns(access_token: str = None, account_id: str = None, limit:
|
|
|
23
23
|
access_token: Meta API access token (optional - will use cached token if not provided)
|
|
24
24
|
account_id: Meta Ads account ID (format: act_XXXXXXXXX)
|
|
25
25
|
limit: Maximum number of campaigns to return (default: 10)
|
|
26
|
-
status_filter: Filter by status (
|
|
26
|
+
status_filter: Filter by effective status (e.g., 'ACTIVE', 'PAUSED', 'ARCHIVED').
|
|
27
|
+
Maps to the 'effective_status' API parameter, which expects an array
|
|
28
|
+
(this function handles the required JSON formatting). Leave empty for all statuses.
|
|
27
29
|
"""
|
|
28
30
|
# If no account ID is specified, try to get the first one for the user
|
|
29
31
|
if not account_id:
|
|
@@ -42,7 +44,8 @@ async def get_campaigns(access_token: str = None, account_id: str = None, limit:
|
|
|
42
44
|
}
|
|
43
45
|
|
|
44
46
|
if status_filter:
|
|
45
|
-
|
|
47
|
+
# API expects an array, encode it as a JSON string
|
|
48
|
+
params["effective_status"] = json.dumps([status_filter])
|
|
46
49
|
|
|
47
50
|
data = await make_api_request(endpoint, access_token, params)
|
|
48
51
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: meta-ads-mcp
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.3
|
|
4
4
|
Summary: Model Calling Protocol (MCP) plugin for interacting with Meta Ads API
|
|
5
5
|
Project-URL: Homepage, https://github.com/nictuku/meta-ads-mcp
|
|
6
6
|
Project-URL: Bug Tracker, https://github.com/nictuku/meta-ads-mcp/issues
|
|
@@ -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
|
-
|
|
34
|
+
Screenshot: using an LLM to understand your ad performance.
|
|
35
35
|
|
|
36
36
|

|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
-
###
|
|
92
|
+
### Create a Meta Developer App (Required)
|
|
101
93
|
|
|
102
|
-
|
|
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
|
-
|
|
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. `
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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)
|
|
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
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
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`
|
|
403
|
-
- macOS: `~/Library/Application Support/meta-ads-mcp/token_cache.json`
|
|
404
|
-
- Linux: `~/.config/meta-ads-mcp/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
|
|
409
|
-
|
|
410
|
-
|
|
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.
|
|
433
|
-
2.
|
|
434
|
-
3.
|
|
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
|
-
-
|
|
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
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|