meta-ads-mcp 0.2.3__py3-none-any.whl → 0.2.4__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/api.py CHANGED
@@ -26,15 +26,8 @@ META_GRAPH_API_VERSION = "v20.0"
26
26
  META_GRAPH_API_BASE = f"https://graph.facebook.com/{META_GRAPH_API_VERSION}"
27
27
  USER_AGENT = "meta-ads-mcp/1.0"
28
28
 
29
- # Meta App configuration - Using a placeholder app ID. This will be overridden by:
30
- # 1. Command line arguments
31
- # 2. Environment variables
32
- # 3. User input during runtime
33
- META_APP_ID = "YOUR_META_APP_ID" # Will be replaced at runtime
34
-
35
- # Try to load from environment variable
36
- if os.environ.get("META_APP_ID"):
37
- META_APP_ID = os.environ.get("META_APP_ID")
29
+ # Meta App configuration
30
+ META_APP_ID = os.environ.get("META_APP_ID", "") # Default to empty string
38
31
 
39
32
  # Auth constants
40
33
  AUTH_SCOPE = "ads_management,ads_read,business_management"
@@ -55,6 +48,35 @@ callback_server_port = None
55
48
  # Global token container for communication between threads
56
49
  token_container = {"token": None, "expires_in": None, "user_id": None}
57
50
 
51
+ # Configuration class to store app ID and other config
52
+ class MetaConfig:
53
+ _instance = None
54
+
55
+ def __new__(cls):
56
+ if cls._instance is None:
57
+ cls._instance = super(MetaConfig, cls).__new__(cls)
58
+ cls._instance.app_id = os.environ.get("META_APP_ID", "") # Default from env
59
+ cls._instance.initialized = False
60
+ return cls._instance
61
+
62
+ def set_app_id(self, app_id):
63
+ """Set app ID from CLI or other source"""
64
+ if app_id:
65
+ print(f"Setting Meta App ID: {app_id}")
66
+ self.app_id = app_id
67
+ self.initialized = True
68
+
69
+ def get_app_id(self):
70
+ """Get current app ID"""
71
+ return self.app_id
72
+
73
+ def is_configured(self):
74
+ """Check if app has been configured with valid app ID"""
75
+ return bool(self.app_id)
76
+
77
+ # Create global config instance
78
+ meta_config = MetaConfig()
79
+
58
80
  # Callback Handler class definition
59
81
  class CallbackHandler(BaseHTTPRequestHandler):
60
82
  def do_GET(self):
@@ -302,8 +324,8 @@ class AuthManager:
302
324
  """Clear the current token and remove from cache"""
303
325
  self.invalidate_token()
304
326
 
305
- # Initialize auth manager
306
- auth_manager = AuthManager(META_APP_ID)
327
+ # Initialize auth manager with app_id from config
328
+ auth_manager = AuthManager(meta_config.get_app_id())
307
329
 
308
330
  # Function to get token without requiring it as a parameter
309
331
  async def get_current_access_token() -> Optional[str]:
@@ -315,6 +337,24 @@ async def get_current_access_token() -> Optional[str]:
315
337
  """
316
338
  return auth_manager.get_access_token()
317
339
 
340
+ # Function to get current app ID from all possible sources
341
+ def get_current_app_id():
342
+ """Get the current app ID from MetaConfig."""
343
+ # First try to get from our singleton config
344
+ app_id = meta_config.get_app_id()
345
+ if app_id:
346
+ return app_id
347
+
348
+ # If not in config yet, check environment as fallback
349
+ env_app_id = os.environ.get("META_APP_ID", "")
350
+ if env_app_id:
351
+ # Update config for future use
352
+ meta_config.set_app_id(env_app_id)
353
+ return env_app_id
354
+
355
+ # Last resort, return empty string
356
+ return ""
357
+
318
358
  class GraphAPIError(Exception):
319
359
  """Exception raised for errors from the Graph API."""
320
360
  def __init__(self, error_data: Dict[str, Any]):
@@ -345,6 +385,17 @@ async def make_api_request(
345
385
  Returns:
346
386
  API response as a dictionary
347
387
  """
388
+ # Validate access token before proceeding
389
+ if not access_token:
390
+ print("API request attempted with blank access token")
391
+ return {
392
+ "error": "Authentication Required",
393
+ "details": {
394
+ "message": "A valid access token is required to access the Meta API",
395
+ "action_required": "Please authenticate first"
396
+ }
397
+ }
398
+
348
399
  url = f"{META_GRAPH_API_BASE}/{endpoint}"
349
400
 
350
401
  headers = {
@@ -391,6 +442,19 @@ async def make_api_request(
391
442
  # Check for specific FB API errors related to auth
392
443
  if isinstance(error_obj, dict) and error_obj.get("code") in [190, 102, 4, 200, 10]:
393
444
  print(f"Detected Facebook API auth error: {error_obj.get('code')}")
445
+ # For app ID errors, provide more useful error message
446
+ if error_obj.get("code") == 200 and "Provide valid app ID" in error_obj.get("message", ""):
447
+ print("Meta API authentication configuration issue")
448
+ app_id = auth_manager.app_id
449
+ print(f"Current app_id: {app_id}")
450
+ return {
451
+ "error": f"HTTP Error: {e.response.status_code}",
452
+ "details": {
453
+ "message": "Meta API authentication configuration issue. Please check your app credentials.",
454
+ "original_error": error_obj.get("message"),
455
+ "code": error_obj.get("code")
456
+ }
457
+ }
394
458
  auth_manager.invalidate_token()
395
459
 
396
460
  return {"error": f"HTTP Error: {e.response.status_code}", "details": error_info}
@@ -403,6 +467,7 @@ async def make_api_request(
403
467
  def meta_api_tool(func):
404
468
  """Decorator to handle authentication for all Meta API tools"""
405
469
  async def wrapper(*args, **kwargs):
470
+ global needs_authentication
406
471
  # Handle various MCP invocation patterns
407
472
  if len(args) == 1:
408
473
  # MCP might pass a single string argument that contains JSON
@@ -449,39 +514,44 @@ def meta_api_tool(func):
449
514
  # If not, try to get it from the auth manager
450
515
  if not access_token:
451
516
  access_token = await get_current_access_token()
517
+ if access_token:
518
+ kwargs['access_token'] = access_token
452
519
 
453
520
  # If still no token, we need authentication
454
521
  if not access_token:
455
- global needs_authentication
456
522
  needs_authentication = True
457
523
 
458
524
  # Start the callback server
459
525
  port = start_callback_server()
460
526
 
527
+ # Get current app ID from config
528
+ current_app_id = meta_config.get_app_id()
529
+ if not current_app_id:
530
+ return json.dumps({
531
+ "error": "No Meta App ID provided. Please provide a valid app ID via environment variable META_APP_ID or --app-id CLI argument.",
532
+ "help": "This is required for authentication with Meta Graph API."
533
+ }, indent=2)
534
+
535
+ # Update auth manager with current app ID
536
+ auth_manager.app_id = current_app_id
537
+ print(f"Using Meta App ID from config: {current_app_id}")
538
+
461
539
  # Update auth manager's redirect URI with the current port
462
540
  auth_manager.redirect_uri = f"http://localhost:{port}/callback"
463
541
 
464
542
  # Generate the authentication URL
465
543
  login_url = auth_manager.get_auth_url()
466
544
 
467
- # Create a resource response that includes the markdown link format
468
- response = {
469
- "error": "Authentication required to use Meta Ads API",
470
- "login_url": login_url,
471
- "server_status": f"Callback server running on port {port}",
472
- "markdown_link": f"[Click here to authenticate with Meta Ads API]({login_url})",
473
- "message": "IMPORTANT: Please use the Markdown link format in your response to allow the user to click it.",
474
- "instructions_for_llm": "You must present this link as clickable Markdown to the user using the markdown_link format provided.",
475
- "note": "After authenticating, the token will be automatically saved."
476
- }
477
-
478
- # Wait a moment to ensure the server is fully started
479
- await asyncio.sleep(1)
480
-
481
- return json.dumps(response, indent=2)
482
-
483
- # Update kwargs with the token
484
- kwargs['access_token'] = access_token
545
+ # Return a user-friendly authentication required response
546
+ return json.dumps({
547
+ "error": "Authentication Required",
548
+ "details": {
549
+ "message": "You need to authenticate with the Meta API before using this tool",
550
+ "action_required": "Please authenticate using the link below",
551
+ "login_url": login_url,
552
+ "markdown_link": f"[Click here to authenticate with Meta Ads API]({login_url})"
553
+ }
554
+ }, indent=2)
485
555
 
486
556
  # Call the original function
487
557
  try:
@@ -492,6 +562,17 @@ def meta_api_tool(func):
492
562
  # Start the callback server
493
563
  port = start_callback_server()
494
564
 
565
+ # Get current app ID from config
566
+ current_app_id = meta_config.get_app_id()
567
+ if not current_app_id:
568
+ return json.dumps({
569
+ "error": "No Meta App ID provided. Please provide a valid app ID via environment variable META_APP_ID or --app-id CLI argument.",
570
+ "help": "This is required for authentication with Meta Graph API."
571
+ }, indent=2)
572
+
573
+ auth_manager.app_id = current_app_id
574
+ print(f"Using Meta App ID from config: {current_app_id}")
575
+
495
576
  # Update auth manager's redirect URI with the current port
496
577
  auth_manager.redirect_uri = f"http://localhost:{port}/callback"
497
578
 
@@ -514,6 +595,28 @@ def meta_api_tool(func):
514
595
 
515
596
  return json.dumps(response, indent=2)
516
597
 
598
+ # If result is a string (JSON), check for app ID errors and improve them
599
+ if isinstance(result, str):
600
+ try:
601
+ result_obj = json.loads(result)
602
+ if "error" in result_obj and "details" in result_obj and "error" in result_obj["details"]:
603
+ error_obj = result_obj["details"].get("error", {})
604
+ if isinstance(error_obj, dict) and error_obj.get("code") == 200 and "Provide valid app ID" in error_obj.get("message", ""):
605
+ # Replace with more user-friendly message
606
+ app_id = auth_manager.app_id
607
+ return json.dumps({
608
+ "error": "Meta API Configuration Issue",
609
+ "details": {
610
+ "message": "Your Meta API app is not properly configured",
611
+ "action_required": "Check your META_APP_ID environment variable",
612
+ "current_app_id": app_id,
613
+ "original_error": error_obj.get("message")
614
+ }
615
+ }, indent=2)
616
+ except Exception:
617
+ # Not JSON or other parsing error, just continue
618
+ pass
619
+
517
620
  return result
518
621
  except Exception as e:
519
622
  # Handle any unexpected errors
@@ -1171,10 +1274,35 @@ async def get_insights(
1171
1274
  breakdown: Optional breakdown dimension (e.g., age, gender, country)
1172
1275
  level: Level of aggregation (ad, adset, campaign, account)
1173
1276
  """
1277
+ # Import logger
1278
+ from meta_ads_mcp.core.utils import logger
1279
+
1280
+ # Log function call details
1281
+ logger.info(f"get_insights called with object_id: {object_id}, time_range: {time_range}, level: {level}")
1282
+
1283
+ # Log authentication details
1284
+ from meta_ads_mcp.core.auth import meta_config
1285
+ app_id = meta_config.get_app_id()
1286
+ logger.info(f"App ID from meta_config: {app_id}")
1287
+
1288
+ # Validate inputs
1174
1289
  if not object_id:
1175
- return json.dumps({"error": "No object ID provided"}, indent=2)
1290
+ logger.error("No object ID provided for get_insights")
1291
+ return json.dumps({"error": "Missing Required Parameter", "details": {"message": "No object ID provided"}}, indent=2)
1176
1292
 
1293
+ # Check access token (masking for security)
1294
+ token_status = "provided" if access_token else "not provided"
1295
+ logger.info(f"Access token status: {token_status}")
1296
+
1297
+ # Log the specific object ID format for troubleshooting
1298
+ if object_id.startswith("act_"):
1299
+ logger.info(f"Object ID is an account ID: {object_id}")
1300
+ else:
1301
+ logger.info(f"Object ID format: {object_id}")
1302
+
1177
1303
  endpoint = f"{object_id}/insights"
1304
+ logger.info(f"Using endpoint: {endpoint}")
1305
+
1178
1306
  params = {
1179
1307
  "date_preset": time_range,
1180
1308
  "fields": "account_id,account_name,campaign_id,campaign_name,adset_id,adset_name,ad_id,ad_name,impressions,clicks,spend,cpc,cpm,ctr,reach,frequency,actions,conversions,unique_clicks,cost_per_action_type",
@@ -1184,43 +1312,52 @@ async def get_insights(
1184
1312
  if breakdown:
1185
1313
  params["breakdowns"] = breakdown
1186
1314
 
1187
- data = await make_api_request(endpoint, access_token, params)
1315
+ logger.info("Making API request for insights")
1316
+ try:
1317
+ data = await make_api_request(endpoint, access_token, params)
1318
+ logger.info(f"API response received: {'success' if 'error' not in data else 'error'}")
1319
+
1320
+ # Check for specific app ID errors and improve error message
1321
+ if "error" in data:
1322
+ error_details = data.get("details", {}).get("error", {})
1323
+ if isinstance(error_details, dict) and error_details.get("code") == 200:
1324
+ logger.error(f"Authentication configuration error in response: {error_details.get('message')}")
1325
+ return json.dumps({
1326
+ "error": "Meta API Configuration Issue",
1327
+ "details": {
1328
+ "message": "There is an issue with your Meta API configuration",
1329
+ "action_required": "Check your META_APP_ID environment variable or re-authenticate",
1330
+ "current_app_id": app_id
1331
+ }
1332
+ }, indent=2)
1333
+ except Exception as e:
1334
+ logger.error(f"Exception during get_insights API call: {str(e)}")
1335
+ data = {"error": str(e)}
1188
1336
 
1189
1337
  return json.dumps(data, indent=2)
1190
1338
 
1191
1339
  @mcp_server.tool()
1192
1340
  @meta_api_tool
1193
- async def debug_image_download(access_token: str = None, url: str = "", ad_id: str = "") -> str:
1194
- """
1195
- Debug image download issues and report detailed diagnostics.
1196
-
1197
- Args:
1198
- access_token: Meta API access token (optional - will use cached token if not provided)
1199
- url: Direct image URL to test (optional)
1200
- ad_id: Meta Ads ad ID (optional, used if url is not provided)
1201
- """
1202
- results = {
1203
- "diagnostics": {
1204
- "timestamp": str(datetime.datetime.now()),
1205
- "methods_tried": [],
1206
- "request_details": [],
1207
- "network_info": {}
1208
- }
1209
- }
1341
+ async def debug_image_download(url="", ad_id="", access_token=None):
1342
+ """Debug image download issues and report detailed diagnostics."""
1343
+ results = {}
1210
1344
 
1211
- # If no URL provided but ad_id is, get URL from ad creative
1212
- if not url and ad_id:
1213
- print(f"Getting image URL from ad creative for ad {ad_id}")
1214
- # Get the creative details
1215
- creative_json = await get_ad_creatives(access_token=access_token, ad_id=ad_id)
1216
- creative_data = json.loads(creative_json)
1217
- results["creative_data"] = creative_data
1218
-
1219
- # Look for image URL in the creative
1220
- if "full_image_url" in creative_data:
1221
- url = creative_data.get("full_image_url")
1222
- elif "thumbnail_url" in creative_data:
1223
- url = creative_data.get("thumbnail_url")
1345
+ if url:
1346
+ results["image_url"] = url
1347
+ else:
1348
+ # If no URL provided but ad_id is, get URL from ad creative
1349
+ if ad_id:
1350
+ print(f"Getting image URL from ad creative for ad {ad_id}")
1351
+ # Get the creative details
1352
+ creative_json = await get_ad_creatives(access_token=access_token, ad_id=ad_id)
1353
+ creative_data = json.loads(creative_json)
1354
+ results["creative_data"] = creative_data
1355
+
1356
+ # Look for image URL in the creative
1357
+ if "full_image_url" in creative_data:
1358
+ url = creative_data.get("full_image_url")
1359
+ elif "thumbnail_url" in creative_data:
1360
+ url = creative_data.get("thumbnail_url")
1224
1361
 
1225
1362
  if not url:
1226
1363
  return json.dumps({
@@ -1229,29 +1366,13 @@ async def debug_image_download(access_token: str = None, url: str = "", ad_id: s
1229
1366
  }, indent=2)
1230
1367
 
1231
1368
  results["image_url"] = url
1232
- print(f"Debug: Testing image URL: {url}")
1233
-
1234
- # Try to get network information to help debug
1235
- try:
1236
- import socket
1237
- hostname = urlparse(url).netloc
1238
- ip_address = socket.gethostbyname(hostname)
1239
- results["diagnostics"]["network_info"] = {
1240
- "hostname": hostname,
1241
- "ip_address": ip_address,
1242
- "is_facebook_cdn": "fbcdn" in hostname
1243
- }
1244
- except Exception as e:
1245
- results["diagnostics"]["network_info"] = {
1246
- "error": str(e)
1247
- }
1248
1369
 
1249
1370
  # Method 1: Basic download
1250
1371
  method_result = {
1251
1372
  "method": "Basic download with standard headers",
1252
1373
  "success": False
1253
1374
  }
1254
- results["diagnostics"]["methods_tried"].append(method_result)
1375
+ results["diagnostics"] = {"methods_tried": [method_result]}
1255
1376
 
1256
1377
  try:
1257
1378
  headers = {
@@ -1386,228 +1507,160 @@ async def save_ad_image_via_api(access_token: str = None, ad_id: str = None) ->
1386
1507
  if not ad_id:
1387
1508
  return json.dumps({"error": "No ad ID provided"}, indent=2)
1388
1509
 
1389
- # First get the ad's creative ID
1510
+ print(f"Attempting to save image for ad {ad_id} using API methods")
1511
+
1512
+ # First, get creative ID and account ID
1390
1513
  ad_endpoint = f"{ad_id}"
1391
1514
  ad_params = {
1392
- "fields": "creative,account_id"
1515
+ "fields": "creative{id},account_id"
1393
1516
  }
1394
1517
 
1395
1518
  ad_data = await make_api_request(ad_endpoint, access_token, ad_params)
1396
1519
 
1397
1520
  if "error" in ad_data:
1398
- return json.dumps({
1399
- "error": "Could not get ad data",
1400
- "details": ad_data
1401
- }, indent=2)
1402
-
1403
- if "creative" not in ad_data or "id" not in ad_data["creative"]:
1404
- return json.dumps({
1405
- "error": "No creative ID found for this ad",
1406
- "ad_data": ad_data
1407
- }, indent=2)
1521
+ return json.dumps({"error": f"Could not get ad data - {ad_data['error']}"}, indent=2)
1408
1522
 
1409
- creative_id = ad_data["creative"]["id"]
1523
+ # Extract account_id
1410
1524
  account_id = ad_data.get("account_id", "")
1525
+ if not account_id:
1526
+ return json.dumps({"error": "No account ID found"}, indent=2)
1411
1527
 
1412
- # Now get the creative object
1528
+ # Extract creative ID
1529
+ if "creative" not in ad_data:
1530
+ return json.dumps({"error": "No creative found for this ad"}, indent=2)
1531
+
1532
+ creative_data = ad_data.get("creative", {})
1533
+ creative_id = creative_data.get("id")
1534
+ if not creative_id:
1535
+ return json.dumps({"error": "No creative ID found"}, indent=2)
1536
+
1537
+ # Get creative details to find image hash
1413
1538
  creative_endpoint = f"{creative_id}"
1414
1539
  creative_params = {
1415
- "fields": "id,name,thumbnail_url,image_hash,asset_feed_spec"
1540
+ "fields": "id,name,image_hash,thumbnail_url,image_url,object_story_spec"
1416
1541
  }
1417
1542
 
1418
- creative_data = await make_api_request(creative_endpoint, access_token, creative_params)
1419
-
1420
- if "error" in creative_data:
1421
- return json.dumps({
1422
- "error": "Could not get creative data",
1423
- "details": creative_data
1424
- }, indent=2)
1543
+ creative_details = await make_api_request(creative_endpoint, access_token, creative_params)
1425
1544
 
1426
- # Approach 1: Try to get image through adimages endpoint if we have image_hash
1427
- image_hash = None
1428
- if "image_hash" in creative_data:
1429
- image_hash = creative_data["image_hash"]
1430
- elif "asset_feed_spec" in creative_data and "images" in creative_data["asset_feed_spec"] and len(creative_data["asset_feed_spec"]["images"]) > 0:
1431
- image_hash = creative_data["asset_feed_spec"]["images"][0].get("hash")
1545
+ if "error" in creative_details:
1546
+ return json.dumps({"error": f"Could not get creative details - {creative_details['error']}"}, indent=2)
1432
1547
 
1433
- result = {
1548
+ results = {
1434
1549
  "ad_id": ad_id,
1435
1550
  "creative_id": creative_id,
1436
- "attempts": []
1551
+ "account_id": account_id,
1552
+ "creative_details": creative_details
1437
1553
  }
1438
1554
 
1439
- if image_hash and account_id:
1440
- attempt = {
1441
- "method": "adimages endpoint with hash",
1442
- "success": False
1443
- }
1444
- result["attempts"].append(attempt)
1445
-
1446
- try:
1447
- image_endpoint = f"act_{account_id}/adimages"
1448
- image_params = {
1449
- "hashes": [image_hash]
1450
- }
1451
- image_data = await make_api_request(image_endpoint, access_token, image_params)
1452
- attempt["response"] = image_data
1453
-
1454
- if "data" in image_data and len(image_data["data"]) > 0 and "url" in image_data["data"][0]:
1455
- url = image_data["data"][0]["url"]
1456
- attempt["url"] = url
1457
-
1458
- # Try to download the image
1459
- image_bytes = await download_image(url)
1460
- if image_bytes:
1461
- attempt["success"] = True
1462
- attempt["image_size"] = len(image_bytes)
1463
-
1464
- # Save the image
1465
- resource_id = f"ad_creative_{ad_id}_method1"
1466
- resource_uri = f"meta-ads://images/{resource_id}"
1467
- ad_creative_images[resource_id] = {
1468
- "data": image_bytes,
1469
- "mime_type": "image/jpeg",
1470
- "name": f"Ad Creative for {ad_id} (Method 1)"
1471
- }
1472
-
1473
- # Return success with resource info
1474
- result["resource_uri"] = resource_uri
1475
- result["success"] = True
1476
- base64_sample = base64.b64encode(image_bytes[:100]).decode("utf-8") + "..."
1477
- result["base64_sample"] = base64_sample
1478
- except Exception as e:
1479
- attempt["error"] = str(e)
1555
+ # Try to find image hash
1556
+ image_hash = None
1480
1557
 
1481
- # Approach 2: Try directly with the thumbnails endpoint
1482
- attempt = {
1483
- "method": "thumbnails endpoint on creative",
1484
- "success": False
1485
- }
1486
- result["attempts"].append(attempt)
1558
+ # Direct hash on creative
1559
+ if "image_hash" in creative_details:
1560
+ image_hash = creative_details["image_hash"]
1561
+
1562
+ # Look in object_story_spec
1563
+ elif "object_story_spec" in creative_details:
1564
+ spec = creative_details["object_story_spec"]
1565
+
1566
+ # For link ads
1567
+ if "link_data" in spec:
1568
+ link_data = spec["link_data"]
1569
+ if "image_hash" in link_data:
1570
+ image_hash = link_data["image_hash"]
1571
+
1572
+ # For photo ads
1573
+ elif "photo_data" in spec:
1574
+ photo_data = spec["photo_data"]
1575
+ if "image_hash" in photo_data:
1576
+ image_hash = photo_data["image_hash"]
1487
1577
 
1488
- try:
1489
- thumbnails_endpoint = f"{creative_id}/thumbnails"
1490
- thumbnails_params = {}
1491
- thumbnails_data = await make_api_request(thumbnails_endpoint, access_token, thumbnails_params)
1492
- attempt["response"] = thumbnails_data
1493
-
1494
- if "data" in thumbnails_data and len(thumbnails_data["data"]) > 0:
1495
- for thumbnail in thumbnails_data["data"]:
1496
- if "uri" in thumbnail:
1497
- url = thumbnail["uri"]
1498
- attempt["url"] = url
1499
-
1500
- # Try to download the image
1501
- image_bytes = await download_image(url)
1502
- if image_bytes:
1503
- attempt["success"] = True
1504
- attempt["image_size"] = len(image_bytes)
1505
-
1506
- # Save the image if method 1 didn't already succeed
1507
- if "success" not in result or not result["success"]:
1508
- resource_id = f"ad_creative_{ad_id}_method2"
1509
- resource_uri = f"meta-ads://images/{resource_id}"
1510
- ad_creative_images[resource_id] = {
1511
- "data": image_bytes,
1512
- "mime_type": "image/jpeg",
1513
- "name": f"Ad Creative for {ad_id} (Method 2)"
1514
- }
1515
-
1516
- # Return success with resource info
1517
- result["resource_uri"] = resource_uri
1518
- result["success"] = True
1519
- base64_sample = base64.b64encode(image_bytes[:100]).decode("utf-8") + "..."
1520
- result["base64_sample"] = base64_sample
1521
-
1522
- # No need to try more thumbnails if we succeeded
1523
- break
1524
- except Exception as e:
1525
- attempt["error"] = str(e)
1578
+ if not image_hash:
1579
+ return json.dumps({
1580
+ "error": "No image hash found in creative",
1581
+ "creative_details": creative_details
1582
+ }, indent=2)
1526
1583
 
1527
- # Approach 3: Try using the preview shareable link as an alternate source
1528
- attempt = {
1529
- "method": "preview_shareable_link",
1530
- "success": False
1584
+ # Now get image data from the adimages endpoint
1585
+ image_endpoint = f"act_{account_id}/adimages"
1586
+ image_params = {
1587
+ "hashes": [image_hash]
1531
1588
  }
1532
- result["attempts"].append(attempt)
1533
1589
 
1534
- try:
1535
- # Get ad details with preview link
1536
- ad_preview_endpoint = f"{ad_id}"
1537
- ad_preview_params = {
1538
- "fields": "preview_shareable_link"
1539
- }
1540
- ad_preview_data = await make_api_request(ad_preview_endpoint, access_token, ad_preview_params)
1541
-
1542
- if "preview_shareable_link" in ad_preview_data:
1543
- preview_link = ad_preview_data["preview_shareable_link"]
1544
- attempt["preview_link"] = preview_link
1545
-
1546
- # We can't directly download the preview image, but let's note it for manual inspection
1547
- attempt["note"] = "Preview link available for manual inspection in browser"
1548
-
1549
- # This approach doesn't actually download the image, but the link might be useful
1550
- # for debugging purposes or manual verification
1551
- except Exception as e:
1552
- attempt["error"] = str(e)
1590
+ image_data = await make_api_request(image_endpoint, access_token, image_params)
1553
1591
 
1554
- # Overall result
1555
- if "success" in result and result["success"]:
1556
- result["message"] = "Successfully retrieved ad image through one of the API methods"
1557
- else:
1558
- result["message"] = "Failed to retrieve ad image through any API method"
1559
-
1560
- return json.dumps(result, indent=2)
1561
-
1562
- @mcp_server.tool()
1563
- @meta_api_tool
1564
- async def get_login_link(access_token: str = None) -> str:
1565
- """
1566
- Get a clickable login link for Meta Ads authentication.
1592
+ if "error" in image_data:
1593
+ return json.dumps({
1594
+ "error": f"Failed to get image data - {image_data['error']}",
1595
+ "hash": image_hash
1596
+ }, indent=2)
1567
1597
 
1568
- Args:
1569
- access_token: Meta API access token (optional - will use cached token if not provided)
1598
+ if "data" not in image_data or not image_data["data"]:
1599
+ return json.dumps({
1600
+ "error": "No image data returned from API",
1601
+ "hash": image_hash
1602
+ }, indent=2)
1570
1603
 
1571
- Returns:
1572
- A clickable resource link for Meta authentication
1573
- """
1574
- # Check if we have a cached token
1575
- cached_token = auth_manager.get_access_token()
1576
- token_status = "No token" if not cached_token else "Valid token"
1604
+ # Get the URL from the first image
1605
+ first_image = image_data["data"][0]
1606
+ image_url = first_image.get("url")
1577
1607
 
1578
- # If we already have a valid token and none was provided, just return success
1579
- if cached_token and not access_token:
1608
+ if not image_url:
1580
1609
  return json.dumps({
1581
- "message": "Already authenticated",
1582
- "token_status": token_status,
1583
- "token_preview": cached_token[:10] + "...",
1584
- "created_at": auth_manager.token_info.created_at,
1585
- "expires_in": auth_manager.token_info.expires_in
1610
+ "error": "No image URL found in API response",
1611
+ "api_response": image_data
1586
1612
  }, indent=2)
1587
1613
 
1588
- # IMPORTANT: Start the callback server first by calling our helper function
1589
- # This ensures the server is ready before we provide the URL to the user
1590
- port = start_callback_server()
1591
-
1592
- # Generate direct login URL
1593
- auth_manager.redirect_uri = f"http://localhost:{port}/callback" # Ensure port is set correctly
1594
- login_url = auth_manager.get_auth_url()
1595
-
1596
- # Return a special format that helps the LLM format the response properly
1597
- response = {
1598
- "login_url": login_url,
1599
- "token_status": token_status,
1600
- "server_status": f"Callback server running on port {port}",
1601
- "markdown_link": f"[Click here to authenticate with Meta Ads]({login_url})",
1602
- "message": "IMPORTANT: Please use the Markdown link format in your response to allow the user to click it.",
1603
- "instructions_for_llm": "You must present this link as clickable Markdown to the user using the markdown_link format provided.",
1604
- "note": "After authenticating, the token will be automatically saved."
1605
- }
1606
-
1607
- # Wait a moment to ensure the server is fully started
1608
- await asyncio.sleep(1)
1614
+ # Try to save the image by directly downloading it
1615
+ results["image_url"] = image_url
1609
1616
 
1610
- return json.dumps(response, indent=2)
1617
+ try:
1618
+ # Try multiple download methods
1619
+ image_bytes = await try_multiple_download_methods(image_url)
1620
+
1621
+ if not image_bytes:
1622
+ return json.dumps({
1623
+ "error": "Failed to download image from URL provided by API",
1624
+ "image_url": image_url,
1625
+ "suggestion": "Try using the debug_image_download tool for more details"
1626
+ }, indent=2)
1627
+
1628
+ # Create a resource ID for this image
1629
+ resource_id = f"ad_{ad_id}_{int(time.time())}"
1630
+
1631
+ # Store the image
1632
+ img = PILImage.open(io.BytesIO(image_bytes))
1633
+ mime_type = f"image/{img.format.lower()}" if img.format else "image/jpeg"
1634
+
1635
+ # Save to our global dictionary
1636
+ ad_creative_images[resource_id] = {
1637
+ "data": image_bytes,
1638
+ "mime_type": mime_type,
1639
+ "name": f"Ad {ad_id} Image",
1640
+ "width": img.width,
1641
+ "height": img.height,
1642
+ "format": img.format
1643
+ }
1644
+
1645
+ # Return the success result with resource info
1646
+ return json.dumps({
1647
+ "success": True,
1648
+ "message": "Successfully saved image",
1649
+ "resource_id": resource_id,
1650
+ "resource_uri": f"meta-ads://images/{resource_id}",
1651
+ "image_details": {
1652
+ "width": img.width,
1653
+ "height": img.height,
1654
+ "format": img.format,
1655
+ "size_bytes": len(image_bytes)
1656
+ }
1657
+ }, indent=2)
1658
+
1659
+ except Exception as e:
1660
+ return json.dumps({
1661
+ "error": f"Error saving image: {str(e)}",
1662
+ "image_url": image_url
1663
+ }, indent=2)
1611
1664
 
1612
1665
  # Helper function to start the login flow
1613
1666
  def login():
@@ -1616,6 +1669,16 @@ def login():
1616
1669
  """
1617
1670
  print("Starting Meta Ads authentication flow...")
1618
1671
 
1672
+ # Ensure auth_manager has the current app ID from config
1673
+ current_app_id = meta_config.get_app_id()
1674
+ if not current_app_id:
1675
+ print("Error: No Meta App ID available. Authentication will fail.")
1676
+ print("Please provide an app ID using --app-id or via META_APP_ID environment variable.")
1677
+ return
1678
+
1679
+ auth_manager.app_id = current_app_id
1680
+ print(f"Using Meta App ID from config: {current_app_id}")
1681
+
1619
1682
  try:
1620
1683
  # Start the callback server first
1621
1684
  port = start_callback_server()
@@ -1767,6 +1830,12 @@ def start_callback_server():
1767
1830
 
1768
1831
  # Update auth manager's redirect URI with new port
1769
1832
  auth_manager.redirect_uri = f"http://localhost:{port}/callback"
1833
+
1834
+ # Always make sure auth_manager has the current app ID from config
1835
+ current_app_id = meta_config.get_app_id()
1836
+ if current_app_id:
1837
+ auth_manager.app_id = current_app_id
1838
+
1770
1839
  callback_server_port = port
1771
1840
 
1772
1841
  try:
@@ -1834,9 +1903,17 @@ def login_cli():
1834
1903
 
1835
1904
  args = parser.parse_args()
1836
1905
 
1837
- # Update app ID if provided
1906
+ # Update app ID if provided via CLI
1838
1907
  if args.app_id:
1839
- auth_manager.app_id = args.app_id
1908
+ meta_config.set_app_id(args.app_id)
1909
+ else:
1910
+ # Use existing config or environment variable
1911
+ if not meta_config.is_configured():
1912
+ print("Error: No Meta App ID provided. Please provide using --app-id or META_APP_ID environment variable.")
1913
+ return 1
1914
+
1915
+ # Update auth_manager with app ID from config
1916
+ auth_manager.app_id = meta_config.get_app_id()
1840
1917
 
1841
1918
  if args.force_login:
1842
1919
  # Clear existing token
@@ -1849,7 +1926,7 @@ def login_cli():
1849
1926
 
1850
1927
  def main():
1851
1928
  """
1852
- Main entry point for the Meta Ads MCP server.
1929
+ Main entry point for the Meta Ads MCP Server.
1853
1930
  This function handles command line arguments and runs the server.
1854
1931
  """
1855
1932
  # Set up command line arguments
@@ -1859,12 +1936,22 @@ def main():
1859
1936
 
1860
1937
  args = parser.parse_args()
1861
1938
 
1862
- # Update app ID if provided
1939
+ # Update app ID if provided via CLI (highest priority)
1863
1940
  if args.app_id:
1864
- auth_manager.app_id = args.app_id
1941
+ meta_config.set_app_id(args.app_id)
1942
+
1943
+ # Ensure auth_manager has the current app ID
1944
+ app_id = get_current_app_id()
1945
+ if app_id:
1946
+ auth_manager.app_id = app_id
1947
+ else:
1948
+ print("Warning: No Meta App ID provided. Authentication will fail.")
1865
1949
 
1866
1950
  # Handle login command
1867
1951
  if args.login:
1952
+ if not meta_config.is_configured():
1953
+ print("Error: Cannot login without a Meta App ID. Please provide using --app-id or META_APP_ID environment variable.")
1954
+ return 1
1868
1955
  login()
1869
1956
  else:
1870
1957
  # Initialize and run the server