Skip to main content
Glama
johnoconnor0

Google Ads MCP Server

by johnoconnor0
bidding_strategy_manager.py23.7 kB
""" Bidding Strategy Manager Handles portfolio bidding strategies and bid adjustments across campaigns. Supports all Google Ads Smart Bidding strategies: - Target CPA - Target ROAS - Maximize Conversions - Maximize Conversion Value - Target Impression Share - Manual CPC (Enhanced) Includes bid adjustments for: - Devices (mobile, desktop, tablet) - Locations (geographic modifiers) - Demographics (age, gender) - Audiences (affinity, in-market, custom) - Ad Schedule (dayparting) """ from typing import Dict, Any, List, Optional from dataclasses import dataclass from enum import Enum from google.ads.googleads.client import GoogleAdsClient class BiddingStrategyType(str, Enum): """Supported bidding strategy types.""" TARGET_CPA = "TARGET_CPA" TARGET_ROAS = "TARGET_ROAS" MAXIMIZE_CONVERSIONS = "MAXIMIZE_CONVERSIONS" MAXIMIZE_CONVERSION_VALUE = "MAXIMIZE_CONVERSION_VALUE" TARGET_IMPRESSION_SHARE = "TARGET_IMPRESSION_SHARE" MANUAL_CPC = "MANUAL_CPC" class ImpressionShareLocation(str, Enum): """Impression share location options.""" ANYWHERE_ON_PAGE = "ANYWHERE_ON_PAGE" TOP_OF_PAGE = "TOP_OF_PAGE" ABSOLUTE_TOP_OF_PAGE = "ABSOLUTE_TOP_OF_PAGE" class Device(str, Enum): """Device types for bid adjustments.""" MOBILE = "MOBILE" DESKTOP = "DESKTOP" TABLET = "TABLET" class DayOfWeek(str, Enum): """Days of week for ad scheduling.""" MONDAY = "MONDAY" TUESDAY = "TUESDAY" WEDNESDAY = "WEDNESDAY" THURSDAY = "THURSDAY" FRIDAY = "FRIDAY" SATURDAY = "SATURDAY" SUNDAY = "SUNDAY" @dataclass class BiddingStrategyConfig: """Configuration for creating a portfolio bidding strategy.""" name: str strategy_type: BiddingStrategyType target_cpa_micros: Optional[int] = None # For TARGET_CPA target_roas: Optional[float] = None # For TARGET_ROAS (e.g., 4.0 = 400%) target_impression_share: Optional[float] = None # For TARGET_IMPRESSION_SHARE (0.0-1.0) location: Optional[ImpressionShareLocation] = None # For TARGET_IMPRESSION_SHARE cpc_bid_ceiling_micros: Optional[int] = None # Max CPC for impression share enhanced_cpc_enabled: Optional[bool] = None # For MANUAL_CPC @dataclass class AdScheduleConfig: """Configuration for ad scheduling bid adjustment.""" day_of_week: DayOfWeek start_hour: int # 0-23 start_minute: int # 0, 15, 30, 45 end_hour: int # 0-24 end_minute: int # 0, 15, 30, 45 bid_modifier: float # 0.1 to 10.0 (1.0 = no change) class BiddingStrategyManager: """Manager for portfolio bidding strategies and bid adjustments.""" def __init__(self, client: GoogleAdsClient): """Initialize the bidding strategy manager. Args: client: Authenticated GoogleAdsClient instance """ self.client = client def create_bidding_strategy( self, customer_id: str, config: BiddingStrategyConfig ) -> Dict[str, Any]: """Create a portfolio bidding strategy. Args: customer_id: Customer ID (without hyphens) config: Bidding strategy configuration Returns: Dictionary with strategy resource name and ID """ bidding_strategy_service = self.client.get_service("BiddingStrategyService") bidding_strategy_operation = self.client.get_type("BiddingStrategyOperation") bidding_strategy = bidding_strategy_operation.create bidding_strategy.name = config.name # Set strategy type and parameters if config.strategy_type == BiddingStrategyType.TARGET_CPA: bidding_strategy.target_cpa.target_cpa_micros = config.target_cpa_micros elif config.strategy_type == BiddingStrategyType.TARGET_ROAS: bidding_strategy.target_roas.target_roas = config.target_roas elif config.strategy_type == BiddingStrategyType.MAXIMIZE_CONVERSIONS: # Maximize Conversions has no additional parameters bidding_strategy.maximize_conversions.SetInParent() elif config.strategy_type == BiddingStrategyType.MAXIMIZE_CONVERSION_VALUE: # Maximize Conversion Value has optional target ROAS if config.target_roas: bidding_strategy.maximize_conversion_value.target_roas = config.target_roas else: bidding_strategy.maximize_conversion_value.SetInParent() elif config.strategy_type == BiddingStrategyType.TARGET_IMPRESSION_SHARE: target_is = bidding_strategy.target_impression_share target_is.target_impression_share = config.target_impression_share if config.location: target_is.location = self.client.enums.TargetImpressionShareLocationEnum[ config.location.value ] if config.cpc_bid_ceiling_micros: target_is.cpc_bid_ceiling_micros = config.cpc_bid_ceiling_micros elif config.strategy_type == BiddingStrategyType.MANUAL_CPC: if config.enhanced_cpc_enabled: bidding_strategy.enhanced_cpc.SetInParent() # Create the bidding strategy response = bidding_strategy_service.mutate_bidding_strategies( customer_id=customer_id, operations=[bidding_strategy_operation] ) resource_name = response.results[0].resource_name strategy_id = resource_name.split("/")[-1] return { 'resource_name': resource_name, 'bidding_strategy_id': strategy_id, 'name': config.name, 'type': config.strategy_type.value } def update_bidding_strategy( self, customer_id: str, bidding_strategy_id: str, config: BiddingStrategyConfig ) -> Dict[str, Any]: """Update an existing portfolio bidding strategy. Args: customer_id: Customer ID (without hyphens) bidding_strategy_id: Bidding strategy ID to update config: Updated bidding strategy configuration Returns: Dictionary with updated strategy details """ bidding_strategy_service = self.client.get_service("BiddingStrategyService") bidding_strategy_operation = self.client.get_type("BiddingStrategyOperation") bidding_strategy = bidding_strategy_operation.update bidding_strategy.resource_name = bidding_strategy_service.bidding_strategy_path( customer_id, bidding_strategy_id ) # Update strategy parameters based on type field_mask_paths = [] if config.name: bidding_strategy.name = config.name field_mask_paths.append("name") if config.strategy_type == BiddingStrategyType.TARGET_CPA and config.target_cpa_micros: bidding_strategy.target_cpa.target_cpa_micros = config.target_cpa_micros field_mask_paths.append("target_cpa.target_cpa_micros") elif config.strategy_type == BiddingStrategyType.TARGET_ROAS and config.target_roas: bidding_strategy.target_roas.target_roas = config.target_roas field_mask_paths.append("target_roas.target_roas") elif config.strategy_type == BiddingStrategyType.TARGET_IMPRESSION_SHARE: if config.target_impression_share: bidding_strategy.target_impression_share.target_impression_share = config.target_impression_share field_mask_paths.append("target_impression_share.target_impression_share") if config.cpc_bid_ceiling_micros: bidding_strategy.target_impression_share.cpc_bid_ceiling_micros = config.cpc_bid_ceiling_micros field_mask_paths.append("target_impression_share.cpc_bid_ceiling_micros") # Set field mask self.client.copy_from( bidding_strategy_operation.update_mask, self.client.get_type("FieldMask", version="v17")(paths=field_mask_paths) ) response = bidding_strategy_service.mutate_bidding_strategies( customer_id=customer_id, operations=[bidding_strategy_operation] ) return { 'resource_name': response.results[0].resource_name, 'bidding_strategy_id': bidding_strategy_id, 'updated_fields': field_mask_paths } def assign_bidding_strategy_to_campaign( self, customer_id: str, campaign_id: str, bidding_strategy_id: str ) -> Dict[str, Any]: """Assign a portfolio bidding strategy to a campaign. Args: customer_id: Customer ID (without hyphens) campaign_id: Campaign ID to update bidding_strategy_id: Bidding strategy ID to assign Returns: Dictionary with assignment details """ campaign_service = self.client.get_service("CampaignService") bidding_strategy_service = self.client.get_service("BiddingStrategyService") campaign_operation = self.client.get_type("CampaignOperation") campaign = campaign_operation.update campaign.resource_name = campaign_service.campaign_path(customer_id, campaign_id) campaign.bidding_strategy = bidding_strategy_service.bidding_strategy_path( customer_id, bidding_strategy_id ) self.client.copy_from( campaign_operation.update_mask, self.client.get_type("FieldMask", version="v17")(paths=["bidding_strategy"]) ) response = campaign_service.mutate_campaigns( customer_id=customer_id, operations=[campaign_operation] ) return { 'campaign_id': campaign_id, 'bidding_strategy_id': bidding_strategy_id, 'resource_name': response.results[0].resource_name } def get_bidding_strategy_performance( self, customer_id: str, bidding_strategy_id: str, date_range: str = "LAST_30_DAYS" ) -> Dict[str, Any]: """Get performance metrics for a portfolio bidding strategy. Args: customer_id: Customer ID (without hyphens) bidding_strategy_id: Bidding strategy ID date_range: Date range for metrics Returns: Dictionary with performance metrics """ ga_service = self.client.get_service("GoogleAdsService") query = f""" SELECT bidding_strategy.id, bidding_strategy.name, bidding_strategy.type, bidding_strategy.campaign_count, metrics.impressions, metrics.clicks, metrics.ctr, metrics.average_cpc, metrics.cost_micros, metrics.conversions, metrics.conversions_value, metrics.cost_per_conversion FROM bidding_strategy WHERE bidding_strategy.id = {bidding_strategy_id} AND segments.date DURING {date_range} """ response = ga_service.search(customer_id=customer_id, query=query) results = list(response) if not results: return { 'bidding_strategy_id': bidding_strategy_id, 'error': 'No data found for this bidding strategy' } row = results[0] return { 'bidding_strategy_id': str(row.bidding_strategy.id), 'name': row.bidding_strategy.name, 'type': row.bidding_strategy.type.name, 'campaign_count': row.bidding_strategy.campaign_count, 'impressions': row.metrics.impressions, 'clicks': row.metrics.clicks, 'ctr': row.metrics.ctr, 'average_cpc': row.metrics.average_cpc / 1_000_000, 'cost': row.metrics.cost_micros / 1_000_000, 'conversions': row.metrics.conversions, 'conversions_value': row.metrics.conversions_value, 'cost_per_conversion': row.metrics.cost_per_conversion if row.metrics.conversions > 0 else 0 } def set_device_bid_adjustments( self, customer_id: str, campaign_id: str, device_adjustments: Dict[Device, float] ) -> Dict[str, Any]: """Set bid adjustments for different device types. Args: customer_id: Customer ID (without hyphens) campaign_id: Campaign ID device_adjustments: Dictionary mapping Device to bid modifier (0.1 to 10.0) Returns: Dictionary with adjustment results """ campaign_criterion_service = self.client.get_service("CampaignCriterionService") operations = [] for device, bid_modifier in device_adjustments.items(): operation = self.client.get_type("CampaignCriterionOperation") criterion = operation.update # Build resource name device_id = self._get_device_criterion_id(device) criterion.resource_name = campaign_criterion_service.campaign_criterion_path( customer_id, campaign_id, device_id ) criterion.bid_modifier = bid_modifier self.client.copy_from( operation.update_mask, self.client.get_type("FieldMask", version="v17")(paths=["bid_modifier"]) ) operations.append(operation) response = campaign_criterion_service.mutate_campaign_criteria( customer_id=customer_id, operations=operations ) return { 'campaign_id': campaign_id, 'updated_devices': len(response.results), 'adjustments': device_adjustments } def set_location_bid_adjustments( self, customer_id: str, campaign_id: str, location_adjustments: Dict[str, float] ) -> Dict[str, Any]: """Set bid adjustments for specific locations. Args: customer_id: Customer ID (without hyphens) campaign_id: Campaign ID location_adjustments: Dictionary mapping location criterion ID to bid modifier Returns: Dictionary with adjustment results """ campaign_criterion_service = self.client.get_service("CampaignCriterionService") operations = [] for location_id, bid_modifier in location_adjustments.items(): operation = self.client.get_type("CampaignCriterionOperation") criterion = operation.update criterion.resource_name = campaign_criterion_service.campaign_criterion_path( customer_id, campaign_id, location_id ) criterion.bid_modifier = bid_modifier self.client.copy_from( operation.update_mask, self.client.get_type("FieldMask", version="v17")(paths=["bid_modifier"]) ) operations.append(operation) response = campaign_criterion_service.mutate_campaign_criteria( customer_id=customer_id, operations=operations ) return { 'campaign_id': campaign_id, 'updated_locations': len(response.results), 'adjustments': location_adjustments } def set_ad_schedule_bid_adjustments( self, customer_id: str, campaign_id: str, schedules: List[AdScheduleConfig] ) -> Dict[str, Any]: """Set bid adjustments for ad scheduling (dayparting). Args: customer_id: Customer ID (without hyphens) campaign_id: Campaign ID schedules: List of ad schedule configurations with bid modifiers Returns: Dictionary with adjustment results """ campaign_criterion_service = self.client.get_service("CampaignCriterionService") operations = [] for schedule in schedules: operation = self.client.get_type("CampaignCriterionOperation") criterion = operation.create campaign_service = self.client.get_service("CampaignService") criterion.campaign = campaign_service.campaign_path(customer_id, campaign_id) # Set ad schedule ad_schedule = criterion.ad_schedule ad_schedule.day_of_week = self.client.enums.DayOfWeekEnum[schedule.day_of_week.value] ad_schedule.start_hour = schedule.start_hour ad_schedule.start_minute = self.client.enums.MinuteOfHourEnum(schedule.start_minute) ad_schedule.end_hour = schedule.end_hour ad_schedule.end_minute = self.client.enums.MinuteOfHourEnum(schedule.end_minute) criterion.bid_modifier = schedule.bid_modifier operations.append(operation) response = campaign_criterion_service.mutate_campaign_criteria( customer_id=customer_id, operations=operations ) return { 'campaign_id': campaign_id, 'created_schedules': len(response.results), 'schedules': [ { 'day': s.day_of_week.value, 'time': f"{s.start_hour:02d}:{s.start_minute:02d}-{s.end_hour:02d}:{s.end_minute:02d}", 'bid_modifier': s.bid_modifier } for s in schedules ] } def get_bid_simulator_data( self, customer_id: str, campaign_id: str, criterion_id: Optional[str] = None ) -> Dict[str, Any]: """Get bid simulation data showing potential performance at different bid levels. Args: customer_id: Customer ID (without hyphens) campaign_id: Campaign ID criterion_id: Optional keyword criterion ID for keyword-level simulation Returns: Dictionary with bid simulation data """ ga_service = self.client.get_service("GoogleAdsService") if criterion_id: # Keyword-level bid simulation query = f""" SELECT ad_group_criterion_simulation.criterion_id, ad_group_criterion_simulation.type, ad_group_criterion_simulation.simulation_type, ad_group_criterion_simulation.start_date, ad_group_criterion_simulation.end_date, ad_group_criterion_simulation.cpc_bid_point_list.points FROM ad_group_criterion_simulation WHERE ad_group_criterion_simulation.criterion_id = {criterion_id} """ else: # Campaign-level bid simulation query = f""" SELECT campaign_simulation.campaign_id, campaign_simulation.type, campaign_simulation.start_date, campaign_simulation.end_date, campaign_simulation.cpc_bid_point_list.points FROM campaign_simulation WHERE campaign_simulation.campaign_id = {campaign_id} AND campaign_simulation.type = 'CPC_BID' """ response = ga_service.search(customer_id=customer_id, query=query) results = list(response) if not results: return { 'error': 'No bid simulation data available', 'note': 'Simulations require at least 7 days of historical data' } row = results[0] # Parse simulation points if criterion_id: points = row.ad_group_criterion_simulation.cpc_bid_point_list.points else: points = row.campaign_simulation.cpc_bid_point_list.points simulation_data = [] for point in points: simulation_data.append({ 'cpc_bid': point.cpc_bid_micros / 1_000_000, 'impressions': point.impressions, 'clicks': point.clicks, 'cost': point.cost_micros / 1_000_000, 'conversions': point.conversions, 'conversions_value': point.conversions_value }) return { 'campaign_id': campaign_id, 'criterion_id': criterion_id, 'simulation_points': simulation_data, 'total_scenarios': len(simulation_data) } def get_bid_recommendations( self, customer_id: str, campaign_id: Optional[str] = None ) -> List[Dict[str, Any]]: """Get AI-powered bid recommendations from Google Ads. Args: customer_id: Customer ID (without hyphens) campaign_id: Optional campaign ID to filter recommendations Returns: List of bid recommendations """ ga_service = self.client.get_service("GoogleAdsService") query = """ SELECT recommendation.type, recommendation.campaign, recommendation.keyword_recommendation.keyword, recommendation.keyword_recommendation.recommended_cpc_bid_micros, recommendation.campaign_budget_recommendation.current_budget_amount_micros, recommendation.campaign_budget_recommendation.recommended_budget_amount_micros, recommendation.impact.base_metrics.impressions, recommendation.impact.base_metrics.clicks, recommendation.impact.base_metrics.conversions FROM recommendation WHERE recommendation.type IN ('KEYWORD', 'CAMPAIGN_BUDGET') """ if campaign_id: campaign_service = self.client.get_service("CampaignService") campaign_resource = campaign_service.campaign_path(customer_id, campaign_id) query += f" AND recommendation.campaign = '{campaign_resource}'" response = ga_service.search(customer_id=customer_id, query=query) recommendations = [] for row in response: rec = row.recommendation rec_data = { 'type': rec.type.name, 'campaign': rec.campaign.split('/')[-1] if rec.campaign else None } # Parse based on recommendation type if rec.type.name == 'KEYWORD': rec_data['keyword'] = rec.keyword_recommendation.keyword.text rec_data['recommended_cpc_bid'] = rec.keyword_recommendation.recommended_cpc_bid_micros / 1_000_000 elif rec.type.name == 'CAMPAIGN_BUDGET': rec_data['current_budget'] = rec.campaign_budget_recommendation.current_budget_amount_micros / 1_000_000 rec_data['recommended_budget'] = rec.campaign_budget_recommendation.recommended_budget_amount_micros / 1_000_000 # Impact metrics if rec.impact: rec_data['impact'] = { 'impressions': rec.impact.base_metrics.impressions, 'clicks': rec.impact.base_metrics.clicks, 'conversions': rec.impact.base_metrics.conversions } recommendations.append(rec_data) return recommendations def _get_device_criterion_id(self, device: Device) -> str: """Get criterion ID for device type. Args: device: Device enum value Returns: Criterion ID as string """ device_map = { Device.MOBILE: "30001", Device.DESKTOP: "30000", Device.TABLET: "30002" } return device_map[device]

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/johnoconnor0/google-ads-mcp'

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