Skip to main content
Glama

estimate_audience_size

Estimate audience size for Meta Ads targeting specifications using demographics, geography, interests, and behaviors to validate reach before campaign launch.

Instructions

Estimate audience size for targeting specifications using Meta's delivery_estimate API.

This function provides comprehensive audience estimation for complex targeting combinations
including demographics, geography, interests, and behaviors. It also maintains backwards
compatibility for simple interest validation.

Args:
    access_token: Meta API access token (optional - will use cached token if not provided)
    account_id: Meta Ads account ID (format: act_XXXXXXXXX) - required for comprehensive estimation
    targeting: Complete targeting specification including demographics, geography, interests, etc.
              Example: {
                  "age_min": 25,
                  "age_max": 65,
                  "geo_locations": {"countries": ["PL"]},
                  "flexible_spec": [
                      {"interests": [{"id": "6003371567474"}]},
                      {"interests": [{"id": "6003462346642"}]}
                  ]
              }
    optimization_goal: Optimization goal for estimation (default: "REACH"). 
                      Options: "REACH", "LINK_CLICKS", "IMPRESSIONS", "CONVERSIONS", etc.
    interest_list: [DEPRECATED - for backwards compatibility] List of interest names to validate
    interest_fbid_list: [DEPRECATED - for backwards compatibility] List of interest IDs to validate

Returns:
    JSON string with audience estimation results including estimated_audience_size,
    reach_estimate, and targeting validation

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
access_tokenNo
account_idNo
targetingNo
optimization_goalNoREACH
interest_listNo
interest_fbid_listNo

Implementation Reference

  • The primary handler function for the 'estimate_audience_size' tool. It uses Meta Ads API endpoints (reachestimate with delivery_estimate fallback) for comprehensive audience estimation, handles backwards compatibility for interest validation, includes preflight validation for locations/custom audiences, and returns formatted JSON results.
    @mcp_server.tool()
    @meta_api_tool
    async def estimate_audience_size(
        access_token: Optional[str] = None,
        account_id: Optional[str] = None,
        targeting: Optional[Dict[str, Any]] = None,
        optimization_goal: str = "REACH",
        # Backwards compatibility for simple interest validation
        interest_list: Optional[List[str]] = None,
        interest_fbid_list: Optional[List[str]] = None
    ) -> str:
        """
        Estimate audience size for targeting specifications using Meta's delivery_estimate API.
        
        This function provides comprehensive audience estimation for complex targeting combinations
        including demographics, geography, interests, and behaviors. It also maintains backwards
        compatibility for simple interest validation.
        
        Args:
            access_token: Meta API access token (optional - will use cached token if not provided)
            account_id: Meta Ads account ID (format: act_XXXXXXXXX) - required for comprehensive estimation
            targeting: Complete targeting specification including demographics, geography, interests, etc.
                      Example: {
                          "age_min": 25,
                          "age_max": 65,
                          "geo_locations": {"countries": ["PL"]},
                          "flexible_spec": [
                              {"interests": [{"id": "6003371567474"}]},
                              {"interests": [{"id": "6003462346642"}]}
                          ]
                      }
            optimization_goal: Optimization goal for estimation (default: "REACH"). 
                              Options: "REACH", "LINK_CLICKS", "IMPRESSIONS", "CONVERSIONS", etc.
            interest_list: [DEPRECATED - for backwards compatibility] List of interest names to validate
            interest_fbid_list: [DEPRECATED - for backwards compatibility] List of interest IDs to validate
        
        Returns:
            JSON string with audience estimation results including estimated_audience_size,
            reach_estimate, and targeting validation
        """
        # Handle backwards compatibility - simple interest validation
        # Check if we're in backwards compatibility mode (interest params provided OR no comprehensive params)
        is_backwards_compatible_call = (interest_list or interest_fbid_list) or (not account_id and not targeting)
        
        if is_backwards_compatible_call and not targeting:
            if not interest_list and not interest_fbid_list:
                return json.dumps({"error": "No interest list or FBID list provided"}, indent=2)
            
            endpoint = "search"
            params = {
                "type": "adinterestvalid"
            }
            
            if interest_list:
                params["interest_list"] = json.dumps(interest_list)
            
            if interest_fbid_list:
                params["interest_fbid_list"] = json.dumps(interest_fbid_list)
            
            data = await make_api_request(endpoint, access_token, params)
            
            return json.dumps(data, indent=2)
        
        # Comprehensive audience estimation using delivery_estimate API
        if not account_id:
            return json.dumps({
                "error": "account_id is required for comprehensive audience estimation",
                "details": "For simple interest validation, use interest_list or interest_fbid_list parameters"
            }, indent=2)
        
        if not targeting:
            return json.dumps({
                "error": "targeting specification is required for comprehensive audience estimation",
                "example": {
                    "age_min": 25,
                    "age_max": 65,
                    "geo_locations": {"countries": ["US"]},
                    "flexible_spec": [
                        {"interests": [{"id": "6003371567474"}]}
                    ]
                }
            }, indent=2)
        
        # Preflight validation: require at least one location OR a custom audience
        def _has_location_or_custom_audience(t: Dict[str, Any]) -> bool:
            if not isinstance(t, dict):
                return False
            geo = t.get("geo_locations") or {}
            if isinstance(geo, dict):
                for key in [
                    "countries",
                    "regions",
                    "cities",
                    "zips",
                    "geo_markets",
                    "country_groups"
                ]:
                    val = geo.get(key)
                    if isinstance(val, list) and len(val) > 0:
                        return True
            # Top-level custom audiences
            ca = t.get("custom_audiences")
            if isinstance(ca, list) and len(ca) > 0:
                return True
            # Custom audiences within flexible_spec
            flex = t.get("flexible_spec")
            if isinstance(flex, list):
                for spec in flex:
                    if isinstance(spec, dict):
                        ca_spec = spec.get("custom_audiences")
                        if isinstance(ca_spec, list) and len(ca_spec) > 0:
                            return True
            return False
    
        if not _has_location_or_custom_audience(targeting):
            return json.dumps({
                "error": "Missing target audience location",
                "details": "Select at least one location in targeting.geo_locations or include a custom audience.",
                "action_required": "Add geo_locations with countries/regions/cities/zips or include custom_audiences.",
                "example": {
                    "geo_locations": {"countries": ["US"]},
                    "age_min": 25,
                    "age_max": 65
                }
            }, indent=2)
        
        # Build reach estimate request (using correct Meta API endpoint)
        endpoint = f"{account_id}/reachestimate"
        params = {
            "targeting_spec": targeting
        }
        
        # Note: reachestimate endpoint doesn't support optimization_goal or objective parameters
        
        try:
            data = await make_api_request(endpoint, access_token, params, method="GET")
            
            # Surface Graph API errors directly for better diagnostics.
            # If reachestimate fails, optionally attempt a fallback using delivery_estimate.
            if isinstance(data, dict) and "error" in data:
                # Special handling for Missing Target Audience Location error (subcode 1885364)
                try:
                    err_wrapper = data.get("error", {})
                    details_obj = err_wrapper.get("details", {})
                    raw_err = details_obj.get("error", {}) if isinstance(details_obj, dict) else {}
                    if (
                        isinstance(raw_err, dict) and (
                            raw_err.get("error_subcode") == 1885364 or
                            raw_err.get("error_user_title") == "Missing Target Audience Location"
                        )
                    ):
                        return json.dumps({
                            "error": "Missing target audience location",
                            "details": raw_err.get("error_user_msg") or "Select at least one location, or choose a custom audience.",
                            "endpoint_used": f"{account_id}/reachestimate",
                            "action_required": "Add geo_locations with at least one of countries/regions/cities/zips or include custom_audiences.",
                            "blame_field_specs": raw_err.get("error_data", {}).get("blame_field_specs") if isinstance(raw_err.get("error_data"), dict) else None
                        }, indent=2)
                except Exception:
                    pass
                # Allow disabling fallback via environment variable
                # Default: fallback disabled unless explicitly enabled by setting DISABLE flag to "0"
                disable_fallback = os.environ.get("META_MCP_DISABLE_DELIVERY_FALLBACK", "1") == "1"
                if disable_fallback:
                    return json.dumps({
                        "error": "Graph API returned an error for reachestimate",
                        "details": data.get("error"),
                        "endpoint_used": f"{account_id}/reachestimate",
                        "request_params": {
                            "has_targeting_spec": bool(targeting),
                        },
                        "note": "delivery_estimate fallback disabled via META_MCP_DISABLE_DELIVERY_FALLBACK"
                    }, indent=2)
    
                # Try fallback to delivery_estimate endpoint
                try:
                    fallback_endpoint = f"{account_id}/delivery_estimate"
                    fallback_params = {
                        "targeting_spec": json.dumps(targeting),
                        # Some API versions accept optimization_goal here
                        "optimization_goal": optimization_goal
                    }
                    fallback_data = await make_api_request(fallback_endpoint, access_token, fallback_params, method="GET")
                    
                    # If fallback returns usable data, format similarly
                    if isinstance(fallback_data, dict) and "data" in fallback_data and len(fallback_data["data"]) > 0:
                        estimate_data = fallback_data["data"][0]
                        formatted_response = {
                            "success": True,
                            "account_id": account_id,
                            "targeting": targeting,
                            "optimization_goal": optimization_goal,
                            "estimated_audience_size": estimate_data.get("estimate_mau", 0),
                            "estimate_details": {
                                "monthly_active_users": estimate_data.get("estimate_mau", 0),
                                "daily_outcomes_curve": estimate_data.get("estimate_dau", []),
                                "bid_estimate": estimate_data.get("bid_estimates", {}),
                                "unsupported_targeting": estimate_data.get("unsupported_targeting", [])
                            },
                            "raw_response": fallback_data,
                            "fallback_endpoint_used": "delivery_estimate"
                        }
                        return json.dumps(formatted_response, indent=2)
                    
                    # Fallback returned but not in expected format
                    return json.dumps({
                        "error": "Graph API returned an error for reachestimate; delivery_estimate fallback did not return usable data",
                        "reachestimate_error": data.get("error"),
                        "fallback_endpoint_used": "delivery_estimate",
                        "fallback_raw_response": fallback_data,
                        "endpoint_used": f"{account_id}/reachestimate",
                        "request_params": {
                            "has_targeting_spec": bool(targeting)
                        }
                    }, indent=2)
                except Exception as _fallback_exc:
                    return json.dumps({
                        "error": "Graph API returned an error for reachestimate; delivery_estimate fallback also failed",
                        "reachestimate_error": data.get("error"),
                        "fallback_endpoint_used": "delivery_estimate",
                        "fallback_exception": str(_fallback_exc),
                        "endpoint_used": f"{account_id}/reachestimate",
                        "request_params": {
                            "has_targeting_spec": bool(targeting)
                        }
                    }, indent=2)
    
            # Format the response for easier consumption
            if "data" in data:
                response_data = data["data"]
                # Case 1: delivery_estimate-like list structure
                if isinstance(response_data, list) and len(response_data) > 0:
                    estimate_data = response_data[0]
                    formatted_response = {
                        "success": True,
                        "account_id": account_id,
                        "targeting": targeting,
                        "optimization_goal": optimization_goal,
                        "estimated_audience_size": estimate_data.get("estimate_mau", 0),
                        "estimate_details": {
                            "monthly_active_users": estimate_data.get("estimate_mau", 0),
                            "daily_outcomes_curve": estimate_data.get("estimate_dau", []),
                            "bid_estimate": estimate_data.get("bid_estimates", {}),
                            "unsupported_targeting": estimate_data.get("unsupported_targeting", [])
                        },
                        "raw_response": data
                    }
                    return json.dumps(formatted_response, indent=2)
                # Case 1b: explicit handling for empty list responses
                if isinstance(response_data, list) and len(response_data) == 0:
                    return json.dumps({
                        "error": "No estimation data returned from Meta API",
                        "raw_response": data,
                        "debug_info": {
                            "response_keys": list(data.keys()) if isinstance(data, dict) else "not_a_dict",
                            "response_type": str(type(data)),
                            "endpoint_used": f"{account_id}/reachestimate"
                        }
                    }, indent=2)
                # Case 2: reachestimate dict structure with bounds
                if isinstance(response_data, dict):
                    lower = response_data.get("users_lower_bound", response_data.get("estimate_mau_lower_bound"))
                    upper = response_data.get("users_upper_bound", response_data.get("estimate_mau_upper_bound"))
                    estimate_ready = response_data.get("estimate_ready")
                    midpoint = None
                    try:
                        if isinstance(lower, (int, float)) and isinstance(upper, (int, float)):
                            midpoint = int((lower + upper) / 2)
                    except Exception:
                        midpoint = None
                    formatted_response = {
                        "success": True,
                        "account_id": account_id,
                        "targeting": targeting,
                        "optimization_goal": optimization_goal,
                        "estimated_audience_size": midpoint if midpoint is not None else 0,
                        "estimate_details": {
                            "users_lower_bound": lower,
                            "users_upper_bound": upper,
                            "estimate_ready": estimate_ready
                        },
                        "raw_response": data
                    }
                    return json.dumps(formatted_response, indent=2)
            else:
                return json.dumps({
                    "error": "No estimation data returned from Meta API",
                    "raw_response": data,
                    "debug_info": {
                        "response_keys": list(data.keys()) if isinstance(data, dict) else "not_a_dict",
                        "response_type": str(type(data)),
                        "endpoint_used": f"{account_id}/reachestimate"
                    }
                }, indent=2)
        
        except Exception as e:
            # Try fallback to delivery_estimate first when an exception occurs (unless disabled)
            # Default: fallback disabled unless explicitly enabled by setting DISABLE flag to "0"
            disable_fallback = os.environ.get("META_MCP_DISABLE_DELIVERY_FALLBACK", "1") == "1"
            if not disable_fallback:
                try:
                    fallback_endpoint = f"{account_id}/delivery_estimate"
                    fallback_params = {
                    "targeting_spec": json.dumps(targeting) if isinstance(targeting, dict) else targeting,
                    "optimization_goal": optimization_goal
                }
                    fallback_data = await make_api_request(fallback_endpoint, access_token, fallback_params, method="GET")
                
                    if isinstance(fallback_data, dict) and "data" in fallback_data and len(fallback_data["data"]) > 0:
                        estimate_data = fallback_data["data"][0]
                        formatted_response = {
                            "success": True,
                            "account_id": account_id,
                            "targeting": targeting,
                            "optimization_goal": optimization_goal,
                            "estimated_audience_size": estimate_data.get("estimate_mau", 0),
                            "estimate_details": {
                                "monthly_active_users": estimate_data.get("estimate_mau", 0),
                                "daily_outcomes_curve": estimate_data.get("estimate_dau", []),
                                "bid_estimate": estimate_data.get("bid_estimates", {}),
                                "unsupported_targeting": estimate_data.get("unsupported_targeting", [])
                            },
                            "raw_response": fallback_data,
                            "fallback_endpoint_used": "delivery_estimate"
                        }
                        return json.dumps(formatted_response, indent=2)
                except Exception as _fallback_exc:
                    # If fallback also fails, proceed to detailed error handling below
                    pass
    
            # Check if this is the specific Business Manager system user permission error
            error_str = str(e)
            if "100" in error_str and "33" in error_str:
                # Try to provide fallback estimation using individual interests if available
                interests_found = []
                if targeting and "interests" in targeting:
                    interests_found.extend([interest.get("id") for interest in targeting["interests"] if interest.get("id")])
                elif targeting and "flexible_spec" in targeting:
                    for spec in targeting["flexible_spec"]:
                        if "interests" in spec:
                            interests_found.extend([interest.get("id") for interest in spec["interests"] if interest.get("id")])
                
                if interests_found:
                    # Attempt to get individual interest data as fallback
                    try:
                        fallback_result = await estimate_audience_size(
                            access_token=access_token,
                            interest_fbid_list=interests_found
                        )
                        fallback_data = json.loads(fallback_result)
                        
                        return json.dumps({
                            "comprehensive_targeting_failed": True,
                            "error_code": "100-33",
                            "fallback_used": True,
                            "details": {
                                "issue": "reachestimate endpoint returned error - possibly due to targeting parameters or account limitations",
                                "solution": "Individual interest validation used as fallback - comprehensive targeting may have specific requirements",
                                "endpoint_used": f"{account_id}/reachestimate"
                            },
                            "individual_interest_data": fallback_data,
                            "note": "Individual interest audience sizes provided as fallback. Comprehensive targeting via reachestimate endpoint failed."
                        }, indent=2)
                    except:
                        pass
                
                return json.dumps({
                    "error": "reachestimate endpoint returned error (previously was incorrectly using delivery_estimate)",
                    "error_code": "100-33", 
                    "details": {
                        "issue": "The endpoint returned an error, possibly due to targeting parameters or account limitations",
                        "endpoint_used": f"{account_id}/reachestimate",
                        "previous_issue": "Code was previously using non-existent delivery_estimate endpoint - now fixed",
                        "available_alternative": "Use interest_list or interest_fbid_list parameters for individual interest validation"
                    },
                    "raw_error": error_str
                }, indent=2)
            else:
                return json.dumps({
                    "error": f"Failed to get audience estimation from reachestimate endpoint: {str(e)}",
                    "details": "Check targeting parameters and account permissions",
                    "error_type": "general_api_error",
                    "endpoint_used": f"{account_id}/reachestimate"
                }, indent=2)
  • Imports the estimate_audience_size function from targeting.py into core.__init__.py, which triggers the execution of the @mcp_server.tool() decorator for automatic MCP tool registration when the core module is imported.
    from .targeting import search_interests, get_interest_suggestions, estimate_audience_size, search_behaviors, search_demographics, search_geo_locations
  • Creates the FastMCP server instance named 'mcp_server' that is used by the @mcp_server.tool() decorators in tool modules (like targeting.py) to register the 'estimate_audience_size' tool with the MCP server.
    mcp_server = FastMCP("meta-ads")
  • Re-exports estimate_audience_size from the core module at the top-level package __init__.py, making it available when importing from meta_ads_mcp.
    from .core import (
        get_ad_accounts,
        get_account_info,
        get_campaigns,
        get_campaign_details,
        create_campaign,
        get_adsets,
        get_adset_details,
        update_adset,
        get_ads,
        get_ad_details,
        get_ad_creatives,
        get_ad_image,
        update_ad,
        get_insights,
        login_cli,
        main,
        search_interests,
        get_interest_suggestions,
        estimate_audience_size,
        search_behaviors,
        search_demographics,
        search_geo_locations
    )
  • The @meta_api_tool decorator applied to estimate_audience_size, providing automatic access token management, API request execution via make_api_request, comprehensive error handling, and JSON response formatting.
    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