Skip to main content
Glama
api.py16.9 kB
"""Core API functionality for Meta Ads API.""" from typing import Any, Dict, Optional, Callable import json import httpx import asyncio import functools import os from . import auth from .auth import needs_authentication, auth_manager, start_callback_server, shutdown_callback_server from .utils import logger # Constants META_GRAPH_API_VERSION = "v22.0" META_GRAPH_API_BASE = f"https://graph.facebook.com/{META_GRAPH_API_VERSION}" USER_AGENT = "meta-ads-mcp/1.0" # Log key environment and configuration at startup logger.info("Core API module initialized") logger.info(f"Graph API Version: {META_GRAPH_API_VERSION}") logger.info(f"META_APP_ID env var present: {'Yes' if os.environ.get('META_APP_ID') else 'No'}") class GraphAPIError(Exception): """Exception raised for errors from the Graph API.""" def __init__(self, error_data: Dict[str, Any]): self.error_data = error_data self.message = error_data.get('message', 'Unknown Graph API error') super().__init__(self.message) # Log error details logger.error(f"Graph API Error: {self.message}") logger.debug(f"Error details: {error_data}") # Check if this is an auth error if "code" in error_data and error_data["code"] in [190, 102, 4]: # Common auth error codes logger.warning(f"Auth error detected (code: {error_data['code']}). Invalidating token.") auth_manager.invalidate_token() async def make_api_request( endpoint: str, access_token: str, params: Optional[Dict[str, Any]] = None, method: str = "GET" ) -> Dict[str, Any]: """ Make a request to the Meta Graph API. Args: endpoint: API endpoint path (without base URL) access_token: Meta API access token params: Additional query parameters method: HTTP method (GET, POST, DELETE) Returns: API response as a dictionary """ # Validate access token before proceeding if not access_token: logger.error("API request attempted with blank access token") return { "error": { "message": "Authentication Required", "details": "A valid access token is required to access the Meta API", "action_required": "Please authenticate first" } } url = f"{META_GRAPH_API_BASE}/{endpoint}" headers = { "User-Agent": USER_AGENT, } request_params = params or {} request_params["access_token"] = access_token # Logging the request (masking token for security) masked_params = {k: "***TOKEN***" if k == "access_token" else v for k, v in request_params.items()} logger.debug(f"API Request: {method} {url}") logger.debug(f"Request params: {masked_params}") # Check for app_id in params app_id = auth_manager.app_id logger.debug(f"Current app_id from auth_manager: {app_id}") async with httpx.AsyncClient() as client: try: if method == "GET": # For GET, JSON-encode dict/list params (e.g., targeting_spec) to proper strings encoded_params = {} for key, value in request_params.items(): if isinstance(value, (dict, list)): encoded_params[key] = json.dumps(value) else: encoded_params[key] = value response = await client.get(url, params=encoded_params, headers=headers, timeout=30.0) elif method == "POST": # For Meta API, POST requests need data, not JSON if 'targeting' in request_params and isinstance(request_params['targeting'], dict): # Convert targeting dict to string for the API request_params['targeting'] = json.dumps(request_params['targeting']) # Convert lists and dicts to JSON strings for key, value in request_params.items(): if isinstance(value, (list, dict)): request_params[key] = json.dumps(value) logger.debug(f"POST params (prepared): {masked_params}") response = await client.post(url, data=request_params, headers=headers, timeout=30.0) elif method == "DELETE": response = await client.delete(url, params=request_params, headers=headers, timeout=30.0) else: raise ValueError(f"Unsupported HTTP method: {method}") response.raise_for_status() logger.debug(f"API Response status: {response.status_code}") # Ensure the response is JSON and return it as a dictionary try: return response.json() except json.JSONDecodeError: # If not JSON, return text content in a structured format return { "text_response": response.text, "status_code": response.status_code } except httpx.HTTPStatusError as e: error_info = {} try: error_info = e.response.json() except: error_info = {"status_code": e.response.status_code, "text": e.response.text} logger.error(f"HTTP Error: {e.response.status_code} - {error_info}") # Check for authentication errors if e.response.status_code == 401 or e.response.status_code == 403: logger.warning("Detected authentication error (401/403)") auth_manager.invalidate_token() elif "error" in error_info: error_obj = error_info.get("error", {}) # Check for specific FB API errors related to auth if isinstance(error_obj, dict) and error_obj.get("code") in [190, 102, 4, 200, 10]: logger.warning(f"Detected Facebook API auth error: {error_obj.get('code')}") # Log more details about app ID related errors if error_obj.get("code") == 200 and "Provide valid app ID" in error_obj.get("message", ""): logger.error("Meta API authentication configuration issue") logger.error(f"Current app_id: {app_id}") # Provide a clearer error message without the confusing "Provide valid app ID" message return { "error": { "message": "Meta API authentication configuration issue. Please check your app credentials.", "original_error": error_obj.get("message"), "code": error_obj.get("code") } } auth_manager.invalidate_token() # Include full details for technical users full_response = { "headers": dict(e.response.headers), "status_code": e.response.status_code, "url": str(e.response.url), "reason": getattr(e.response, "reason_phrase", "Unknown reason"), "request_method": e.request.method, "request_url": str(e.request.url) } # Return a properly structured error object return { "error": { "message": f"HTTP Error: {e.response.status_code}", "details": error_info, "full_response": full_response } } except Exception as e: logger.error(f"Request Error: {str(e)}") return {"error": {"message": str(e)}} # Generic wrapper for all Meta API tools def meta_api_tool(func): """Decorator for Meta API tools that handles authentication and error handling.""" @functools.wraps(func) async def wrapper(*args, **kwargs): try: # Log function call logger.debug(f"Function call: {func.__name__}") logger.debug(f"Args: {args}") # Log kwargs without sensitive info safe_kwargs = {k: ('***TOKEN***' if k == 'access_token' else v) for k, v in kwargs.items()} logger.debug(f"Kwargs: {safe_kwargs}") # Log app ID information app_id = auth_manager.app_id logger.debug(f"Current app_id: {app_id}") logger.debug(f"META_APP_ID env var: {os.environ.get('META_APP_ID')}") # If access_token is not in kwargs or not kwargs['access_token'], try to get it from auth_manager if 'access_token' not in kwargs or not kwargs['access_token']: try: access_token = await auth.get_current_access_token() if access_token: kwargs['access_token'] = access_token logger.debug("Using access token from auth_manager") else: logger.warning("No access token available from auth_manager") # Add more details about why token might be missing if (auth_manager.app_id == "YOUR_META_APP_ID" or not auth_manager.app_id) and not auth_manager.use_pipeboard: logger.error("TOKEN VALIDATION FAILED: No valid app_id configured") logger.error("Please set META_APP_ID environment variable or configure in your code") elif auth_manager.use_pipeboard: logger.error("TOKEN VALIDATION FAILED: Pipeboard authentication enabled but no valid token available") logger.error("Complete authentication via Pipeboard service or check PIPEBOARD_API_TOKEN") else: logger.error("Check logs above for detailed token validation failures") except Exception as e: logger.error(f"Error getting access token: {str(e)}") # Add stack trace for better debugging import traceback logger.error(f"Stack trace: {traceback.format_exc()}") # Final validation - if we still don't have a valid token, return authentication required if 'access_token' not in kwargs or not kwargs['access_token']: logger.warning("No access token available, authentication needed") # Add more specific troubleshooting information auth_url = auth_manager.get_auth_url() app_id = auth_manager.app_id using_pipeboard = auth_manager.use_pipeboard logger.error("TOKEN VALIDATION SUMMARY:") logger.error(f"- Current app_id: '{app_id}'") logger.error(f"- Environment META_APP_ID: '{os.environ.get('META_APP_ID', 'Not set')}'") logger.error(f"- Pipeboard API token configured: {'Yes' if os.environ.get('PIPEBOARD_API_TOKEN') else 'No'}") logger.error(f"- Using Pipeboard authentication: {'Yes' if using_pipeboard else 'No'}") # Check for common configuration issues - but only if not using Pipeboard if not using_pipeboard and (app_id == "YOUR_META_APP_ID" or not app_id): logger.error("ISSUE DETECTED: No valid Meta App ID configured") logger.error("ACTION REQUIRED: Set META_APP_ID environment variable with a valid App ID") elif using_pipeboard: logger.error("ISSUE DETECTED: Pipeboard authentication configured but no valid token available") logger.error("ACTION REQUIRED: Complete authentication via Pipeboard service") # Provide different guidance based on authentication method if using_pipeboard: return json.dumps({ "error": { "message": "Pipeboard Authentication Required", "details": { "description": "Your Pipeboard API token is invalid or has expired", "action_required": "Update your Pipeboard token", "setup_url": "https://pipeboard.co/setup", "token_url": "https://pipeboard.co/api-tokens", "configuration_status": { "app_id_configured": bool(app_id) and app_id != "YOUR_META_APP_ID", "pipeboard_enabled": True, }, "troubleshooting": "Go to https://pipeboard.co/setup to verify your account setup, then visit https://pipeboard.co/api-tokens to obtain a new API token", "setup_link": "[Verify your Pipeboard account setup](https://pipeboard.co/setup)", "token_link": "[Get a new Pipeboard API token](https://pipeboard.co/api-tokens)" } } }, indent=2) else: return json.dumps({ "error": { "message": "Authentication Required", "details": { "description": "You need to authenticate with the Meta API before using this tool", "action_required": "Please authenticate first", "auth_url": auth_url, "configuration_status": { "app_id_configured": bool(app_id) and app_id != "YOUR_META_APP_ID", "pipeboard_enabled": False, }, "troubleshooting": "Check logs for TOKEN VALIDATION FAILED messages", "markdown_link": f"[Click here to authenticate with Meta Ads API]({auth_url})" } } }, indent=2) # Call the original function result = await func(*args, **kwargs) # If the result is a string (JSON), try to parse it to check for errors if isinstance(result, str): try: result_dict = json.loads(result) if "error" in result_dict: logger.error(f"Error in API response: {result_dict['error']}") # If this is an app ID error, log more details if isinstance(result_dict.get("details", {}).get("error", {}), dict): error_obj = result_dict["details"]["error"] if error_obj.get("code") == 200 and "Provide valid app ID" in error_obj.get("message", ""): logger.error("Meta API authentication configuration issue") logger.error(f"Current app_id: {app_id}") # Replace the confusing error with a more user-friendly one return json.dumps({ "error": { "message": "Meta API Configuration Issue", "details": { "description": "Your Meta API app is not properly configured", "action_required": "Check your META_APP_ID environment variable", "current_app_id": app_id, "original_error": error_obj.get("message") } } }, indent=2) except Exception: # Not JSON or other parsing error, wrap it in a dictionary return json.dumps({"data": result}, indent=2) # If result is already a dictionary, ensure it's properly serialized if isinstance(result, dict): return json.dumps(result, indent=2) return result except Exception as e: logger.error(f"Error in {func.__name__}: {str(e)}") return json.dumps({"error": str(e)}, indent=2) return wrapper

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/pipeboard-co/meta-ads-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server