Skip to main content
Glama
johnoconnor0

Google Ads MCP Server

by johnoconnor0
google_ads_mcp.py41 kB
""" Google Ads MCP Server A comprehensive Model Context Protocol server for Google Ads API integration. Provides tools for account analysis, campaign management, and optimization. Requirements: - google-ads>=25.0.0 - mcp>=1.1.0 - httpx - pydantic>=2.0.0 """ from mcp.server.fastmcp import FastMCP from pydantic import BaseModel, Field, field_validator, ConfigDict from typing import Optional, List, Dict, Any, Literal from enum import Enum from google.ads.googleads.client import GoogleAdsClient from google.ads.googleads.errors import GoogleAdsException import json import asyncio from datetime import datetime, timedelta # Constants CHARACTER_LIMIT = 25000 DEFAULT_PAGE_SIZE = 50 MAX_PAGE_SIZE = 100 # Initialize MCP server mcp = FastMCP("google_ads_mcp") # Global client - will be initialized with credentials _google_ads_client = None # ============================================================================ # Configuration & Initialization # ============================================================================ def initialize_client( developer_token: str, client_id: str, client_secret: str, refresh_token: str, login_customer_id: Optional[str] = None ) -> None: """ Initialize the Google Ads API client with OAuth credentials. Args: developer_token: Your Google Ads API developer token client_id: OAuth2 client ID client_secret: OAuth2 client secret refresh_token: OAuth2 refresh token login_customer_id: Optional MCC account ID (without hyphens) """ global _google_ads_client credentials = { "developer_token": developer_token, "client_id": client_id, "client_secret": client_secret, "refresh_token": refresh_token, "use_proto_plus": True } if login_customer_id: credentials["login_customer_id"] = login_customer_id _google_ads_client = GoogleAdsClient.load_from_dict(credentials) def get_client() -> GoogleAdsClient: """Get the initialized Google Ads client.""" if _google_ads_client is None: raise RuntimeError( "Google Ads client not initialized. Call initialize_client() first with your credentials." ) return _google_ads_client # ============================================================================ # Enums and Response Formats # ============================================================================ class ResponseFormat(str, Enum): """Output format for tool responses.""" MARKDOWN = "markdown" JSON = "json" class DateRange(str, Enum): """Predefined date ranges for queries.""" TODAY = "TODAY" YESTERDAY = "YESTERDAY" LAST_7_DAYS = "LAST_7_DAYS" LAST_14_DAYS = "LAST_14_DAYS" LAST_30_DAYS = "LAST_30_DAYS" THIS_MONTH = "THIS_MONTH" LAST_MONTH = "LAST_MONTH" LAST_90_DAYS = "LAST_90_DAYS" class CampaignStatus(str, Enum): """Campaign status options.""" ENABLED = "ENABLED" PAUSED = "PAUSED" REMOVED = "REMOVED" # ============================================================================ # Helper Functions # ============================================================================ def format_micros(micros: int) -> float: """Convert micros to standard currency units.""" return micros / 1_000_000 def format_percentage(value: float) -> str: """Format a decimal as a percentage.""" return f"{value * 100:.2f}%" def truncate_response(response: str, message: str = "Response truncated. Use filters to reduce data.") -> str: """Truncate response if it exceeds CHARACTER_LIMIT.""" if len(response) > CHARACTER_LIMIT: return response[:CHARACTER_LIMIT] + f"\n\n... {message}" return response async def execute_query( client: GoogleAdsClient, customer_id: str, query: str ) -> List[Any]: """ Execute a GAQL query and return results. Args: client: Google Ads client customer_id: Customer ID (without hyphens) query: GAQL query string Returns: List of row results """ ga_service = client.get_service("GoogleAdsService") try: response = ga_service.search(customer_id=customer_id, query=query) return list(response) except GoogleAdsException as ex: error_messages = [] for error in ex.failure.errors: error_messages.append( f"Error: {error.message} " f"(Field: {error.location.field_path_elements[0].field_name if error.location.field_path_elements else 'unknown'})" ) raise RuntimeError(f"Google Ads API error: {'; '.join(error_messages)}") def format_account_list_markdown(accounts: List[Dict]) -> str: """Format account list as markdown.""" if not accounts: return "No accounts found." output = "# Google Ads Accounts\n\n" for account in accounts: output += f"## {account['name']}\n" output += f"- **Customer ID**: {account['id']}\n" output += f"- **Currency**: {account['currency_code']}\n" output += f"- **Timezone**: {account['time_zone']}\n" output += f"- **Status**: {account['status']}\n" if account.get('manager'): output += f"- **Manager Account**: Yes\n" output += "\n" return output def format_campaign_performance_markdown(campaigns: List[Dict]) -> str: """Format campaign performance data as markdown.""" if not campaigns: return "No campaigns found matching the criteria." output = "# Campaign Performance Report\n\n" # Summary statistics total_cost = sum(c.get('cost', 0) for c in campaigns) total_clicks = sum(c.get('clicks', 0) for c in campaigns) total_impressions = sum(c.get('impressions', 0) for c in campaigns) total_conversions = sum(c.get('conversions', 0) for c in campaigns) output += "## Summary\n" output += f"- **Total Campaigns**: {len(campaigns)}\n" output += f"- **Total Cost**: ${total_cost:,.2f}\n" output += f"- **Total Clicks**: {total_clicks:,}\n" output += f"- **Total Impressions**: {total_impressions:,}\n" output += f"- **Total Conversions**: {total_conversions:.2f}\n" if total_cost > 0: output += f"- **Average CPC**: ${total_cost / total_clicks:.2f}\n" if total_clicks > 0 else "" output += f"- **Cost per Conversion**: ${total_cost / total_conversions:.2f}\n" if total_conversions > 0 else "" output += "\n## Campaign Details\n\n" for campaign in campaigns: output += f"### {campaign['name']} (ID: {campaign['id']})\n" output += f"- **Status**: {campaign['status']}\n" output += f"- **Cost**: ${campaign.get('cost', 0):,.2f}\n" output += f"- **Clicks**: {campaign.get('clicks', 0):,}\n" output += f"- **Impressions**: {campaign.get('impressions', 0):,}\n" output += f"- **CTR**: {format_percentage(campaign.get('ctr', 0))}\n" output += f"- **Avg CPC**: ${campaign.get('average_cpc', 0):.2f}\n" output += f"- **Conversions**: {campaign.get('conversions', 0):.2f}\n" output += f"- **Conversion Rate**: {format_percentage(campaign.get('conversion_rate', 0))}\n" if campaign.get('cost', 0) > 0 and campaign.get('conversions', 0) > 0: output += f"- **Cost per Conversion**: ${campaign['cost'] / campaign['conversions']:.2f}\n" output += "\n" return output # ============================================================================ # Input Models # ============================================================================ class InitializeInput(BaseModel): """Input for initializing Google Ads client.""" model_config = ConfigDict(str_strip_whitespace=True) developer_token: str = Field(..., description="Your Google Ads API developer token", min_length=1) client_id: str = Field(..., description="OAuth2 client ID", min_length=1) client_secret: str = Field(..., description="OAuth2 client secret", min_length=1) refresh_token: str = Field(..., description="OAuth2 refresh token", min_length=1) login_customer_id: Optional[str] = Field( default=None, description="MCC account ID (without hyphens) if accessing client accounts. Example: '1234567890'" ) class ListAccountsInput(BaseModel): """Input for listing accessible accounts.""" model_config = ConfigDict(str_strip_whitespace=True) response_format: ResponseFormat = Field( default=ResponseFormat.MARKDOWN, description="Output format: 'markdown' for readable or 'json' for structured data" ) class CampaignPerformanceInput(BaseModel): """Input for getting campaign performance data.""" model_config = ConfigDict(str_strip_whitespace=True) customer_id: str = Field( ..., description="Customer ID without hyphens (e.g., '1234567890')", min_length=1, pattern=r'^\d+$' ) date_range: Optional[DateRange] = Field( default=DateRange.LAST_30_DAYS, description="Predefined date range for the report" ) campaign_status: Optional[List[CampaignStatus]] = Field( default=None, description="Filter by campaign status (e.g., ['ENABLED', 'PAUSED']). If not specified, returns all statuses." ) min_cost: Optional[float] = Field( default=None, description="Minimum cost threshold (filters campaigns with cost >= this value)", ge=0 ) limit: Optional[int] = Field( default=DEFAULT_PAGE_SIZE, description="Maximum number of campaigns to return", ge=1, le=MAX_PAGE_SIZE ) response_format: ResponseFormat = Field( default=ResponseFormat.MARKDOWN, description="Output format: 'markdown' for human-readable or 'json' for structured data" ) class KeywordPerformanceInput(BaseModel): """Input for getting keyword performance data.""" model_config = ConfigDict(str_strip_whitespace=True) customer_id: str = Field( ..., description="Customer ID without hyphens", pattern=r'^\d+$' ) campaign_id: Optional[str] = Field( default=None, description="Optional campaign ID to filter keywords" ) date_range: Optional[DateRange] = Field( default=DateRange.LAST_30_DAYS, description="Date range for keyword data" ) min_impressions: Optional[int] = Field( default=None, description="Filter keywords with impressions >= this value", ge=0 ) limit: Optional[int] = Field( default=DEFAULT_PAGE_SIZE, ge=1, le=MAX_PAGE_SIZE ) response_format: ResponseFormat = Field( default=ResponseFormat.MARKDOWN, description="Output format" ) class SearchTermsInput(BaseModel): """Input for search terms report.""" model_config = ConfigDict(str_strip_whitespace=True) customer_id: str = Field(..., description="Customer ID without hyphens", pattern=r'^\d+$') campaign_id: Optional[str] = Field(default=None, description="Optional campaign ID filter") date_range: DateRange = Field(default=DateRange.LAST_30_DAYS) min_impressions: Optional[int] = Field(default=10, ge=0) limit: int = Field(default=DEFAULT_PAGE_SIZE, ge=1, le=MAX_PAGE_SIZE) response_format: ResponseFormat = Field(default=ResponseFormat.MARKDOWN) class AdGroupPerformanceInput(BaseModel): """Input for ad group performance.""" model_config = ConfigDict(str_strip_whitespace=True) customer_id: str = Field(..., pattern=r'^\d+$') campaign_id: Optional[str] = Field(default=None) date_range: DateRange = Field(default=DateRange.LAST_30_DAYS) limit: int = Field(default=DEFAULT_PAGE_SIZE, ge=1, le=MAX_PAGE_SIZE) response_format: ResponseFormat = Field(default=ResponseFormat.MARKDOWN) class RecommendationsInput(BaseModel): """Input for optimization recommendations.""" model_config = ConfigDict(str_strip_whitespace=True) customer_id: str = Field(..., pattern=r'^\d+$') recommendation_types: Optional[List[str]] = Field( default=None, description="Filter by recommendation types (e.g., ['KEYWORD', 'TARGET_CPA_OPT'])" ) limit: int = Field(default=20, ge=1, le=100) response_format: ResponseFormat = Field(default=ResponseFormat.MARKDOWN) class UpdateCampaignBudgetInput(BaseModel): """Input for updating campaign budget.""" model_config = ConfigDict(str_strip_whitespace=True) customer_id: str = Field(..., pattern=r'^\d+$') campaign_id: str = Field(..., description="Campaign ID to update") budget_amount_micros: int = Field( ..., description="New budget amount in micros (e.g., 50000000 for $50)", ge=10000 # Minimum 0.01 ) class UpdateCampaignStatusInput(BaseModel): """Input for pausing/enabling campaigns.""" model_config = ConfigDict(str_strip_whitespace=True) customer_id: str = Field(..., pattern=r'^\d+$') campaign_id: str = Field(...) status: Literal["ENABLED", "PAUSED"] = Field(...) class CustomQueryInput(BaseModel): """Input for custom GAQL queries.""" model_config = ConfigDict(str_strip_whitespace=True) customer_id: str = Field(..., pattern=r'^\d+$') query: str = Field( ..., description="GAQL query (use Google Ads Query Builder: https://developers.google.com/google-ads/api/fields/latest/overview_query_builder)", min_length=1 ) response_format: ResponseFormat = Field(default=ResponseFormat.JSON) # ============================================================================ # MCP Tools # ============================================================================ @mcp.tool( name="google_ads_initialize", annotations={ "title": "Initialize Google Ads Connection", "readOnlyHint": False, "destructiveHint": False, "idempotentHint": True, "openWorldHint": True } ) async def google_ads_initialize(params: InitializeInput) -> str: """ Initialize the Google Ads API connection with OAuth credentials. This must be called before using any other Google Ads tools. Provide your developer token, OAuth2 credentials, and optionally an MCC login customer ID if you're accessing client accounts. Args: params: Configuration containing: - developer_token: API developer token - client_id: OAuth2 client ID - client_secret: OAuth2 client secret - refresh_token: OAuth2 refresh token - login_customer_id: Optional MCC account ID Returns: Confirmation message with initialization status """ try: initialize_client( developer_token=params.developer_token, client_id=params.client_id, client_secret=params.client_secret, refresh_token=params.refresh_token, login_customer_id=params.login_customer_id ) message = "✓ Google Ads API client initialized successfully." if params.login_customer_id: message += f"\n✓ Using MCC account: {params.login_customer_id}" message += "\n\nYou can now use other Google Ads tools to access your account data." return message except Exception as e: return f"✗ Failed to initialize: {str(e)}\n\nPlease verify your credentials and try again." @mcp.tool( name="google_ads_list_accounts", annotations={ "title": "List Accessible Google Ads Accounts", "readOnlyHint": True, "destructiveHint": False, "idempotentHint": True, "openWorldHint": True } ) async def google_ads_list_accounts(params: ListAccountsInput) -> str: """ List all Google Ads accounts accessible with current credentials. Returns details about all accounts you have access to, including customer IDs, names, currency codes, and whether they are manager accounts. Args: params: Query parameters containing response format preference Returns: List of accessible accounts with their details """ try: client = get_client() customer_service = client.get_service("CustomerService") # Get accessible customers accessible_customers = customer_service.list_accessible_customers() customer_ids = accessible_customers.resource_names accounts = [] for resource_name in customer_ids: customer_id = resource_name.split('/')[-1] query = """ SELECT customer.id, customer.descriptive_name, customer.currency_code, customer.time_zone, customer.manager, customer.status FROM customer WHERE customer.id = {customer_id} """.replace("{customer_id}", customer_id) try: rows = await execute_query(client, customer_id, query) if rows: customer = rows[0].customer accounts.append({ 'id': str(customer.id), 'name': customer.descriptive_name, 'currency_code': customer.currency_code, 'time_zone': customer.time_zone, 'manager': customer.manager, 'status': customer.status.name }) except Exception as e: # Skip accounts we don't have access to continue if params.response_format == ResponseFormat.JSON: return json.dumps({"accounts": accounts, "count": len(accounts)}, indent=2) else: return format_account_list_markdown(accounts) except Exception as e: return f"Error listing accounts: {str(e)}" @mcp.tool( name="google_ads_campaign_performance", annotations={ "title": "Get Campaign Performance Metrics", "readOnlyHint": True, "destructiveHint": False, "idempotentHint": True, "openWorldHint": True } ) async def google_ads_campaign_performance(params: CampaignPerformanceInput) -> str: """ Get comprehensive performance metrics for campaigns. Retrieves key performance indicators including cost, clicks, impressions, CTR, conversions, and more for campaigns in the specified date range. Supports filtering by status and cost thresholds. Args: params: Query parameters including: - customer_id: Account to query - date_range: Time period (default: LAST_30_DAYS) - campaign_status: Filter by status (optional) - min_cost: Minimum cost filter (optional) - limit: Max results to return - response_format: Output format Returns: Campaign performance data with metrics and analysis """ try: client = get_client() # Build GAQL query query = f""" SELECT campaign.id, campaign.name, campaign.status, metrics.cost_micros, metrics.clicks, metrics.impressions, metrics.ctr, metrics.average_cpc, metrics.conversions, metrics.conversions_value, metrics.cost_per_conversion, metrics.conversion_rate FROM campaign WHERE segments.date DURING {params.date_range.value} """ # Add status filter if specified if params.campaign_status: status_filter = " OR ".join([f"campaign.status = '{s.value}'" for s in params.campaign_status]) query += f" AND ({status_filter})" query += f" ORDER BY metrics.cost_micros DESC LIMIT {params.limit}" rows = await execute_query(client, params.customer_id, query) campaigns = [] for row in rows: cost = format_micros(row.metrics.cost_micros) # Apply min_cost filter if params.min_cost is not None and cost < params.min_cost: continue campaigns.append({ 'id': str(row.campaign.id), 'name': row.campaign.name, 'status': row.campaign.status.name, 'cost': cost, 'clicks': row.metrics.clicks, 'impressions': row.metrics.impressions, 'ctr': row.metrics.ctr, 'average_cpc': format_micros(row.metrics.average_cpc), 'conversions': row.metrics.conversions, 'conversions_value': row.metrics.conversions_value, 'conversion_rate': row.metrics.conversion_rate, 'cost_per_conversion': format_micros(row.metrics.cost_per_conversion) if row.metrics.conversions > 0 else 0 }) if params.response_format == ResponseFormat.JSON: result = { 'campaigns': campaigns, 'count': len(campaigns), 'date_range': params.date_range.value, 'customer_id': params.customer_id } return json.dumps(result, indent=2) else: return truncate_response(format_campaign_performance_markdown(campaigns)) except Exception as e: return f"Error retrieving campaign performance: {str(e)}" @mcp.tool( name="google_ads_keyword_performance", annotations={ "title": "Get Keyword Performance Analysis", "readOnlyHint": True, "destructiveHint": False, "idempotentHint": True, "openWorldHint": True } ) async def google_ads_keyword_performance(params: KeywordPerformanceInput) -> str: """ Analyze keyword-level performance metrics. Get detailed performance data for keywords including quality scores, average positions, costs, and conversion metrics. Helps identify high and low performers. Args: params: Query parameters for keyword analysis Returns: Keyword performance metrics with quality and position data """ try: client = get_client() query = f""" SELECT campaign.id, campaign.name, ad_group.id, ad_group.name, ad_group_criterion.keyword.text, ad_group_criterion.keyword.match_type, ad_group_criterion.quality_info.quality_score, metrics.cost_micros, metrics.clicks, metrics.impressions, metrics.ctr, metrics.average_cpc, metrics.conversions, metrics.cost_per_conversion, metrics.average_position FROM keyword_view WHERE segments.date DURING {params.date_range.value} """ if params.campaign_id: query += f" AND campaign.id = {params.campaign_id}" if params.min_impressions: query += f" AND metrics.impressions >= {params.min_impressions}" query += f" ORDER BY metrics.cost_micros DESC LIMIT {params.limit}" rows = await execute_query(client, params.customer_id, query) keywords = [] for row in rows: keywords.append({ 'campaign_name': row.campaign.name, 'ad_group_name': row.ad_group.name, 'keyword': row.ad_group_criterion.keyword.text, 'match_type': row.ad_group_criterion.keyword.match_type.name, 'quality_score': row.ad_group_criterion.quality_info.quality_score, 'cost': format_micros(row.metrics.cost_micros), 'clicks': row.metrics.clicks, 'impressions': row.metrics.impressions, 'ctr': row.metrics.ctr, 'average_cpc': format_micros(row.metrics.average_cpc), 'conversions': row.metrics.conversions, 'average_position': row.metrics.average_position }) if params.response_format == ResponseFormat.JSON: return json.dumps({'keywords': keywords, 'count': len(keywords)}, indent=2) else: output = "# Keyword Performance Report\n\n" output += f"**Total Keywords**: {len(keywords)}\n\n" for kw in keywords: output += f"## {kw['keyword']} ({kw['match_type']})\n" output += f"- Campaign: {kw['campaign_name']}\n" output += f"- Ad Group: {kw['ad_group_name']}\n" output += f"- Quality Score: {kw['quality_score']}/10\n" output += f"- Cost: ${kw['cost']:,.2f}\n" output += f"- Clicks: {kw['clicks']:,} | Impressions: {kw['impressions']:,}\n" output += f"- CTR: {format_percentage(kw['ctr'])}\n" output += f"- Avg CPC: ${kw['average_cpc']:.2f}\n" output += f"- Conversions: {kw['conversions']:.2f}\n" output += f"- Avg Position: {kw['average_position']:.2f}\n\n" return truncate_response(output) except Exception as e: return f"Error retrieving keyword performance: {str(e)}" @mcp.tool( name="google_ads_search_terms", annotations={ "title": "Get Search Terms Report", "readOnlyHint": True, "destructiveHint": False, "idempotentHint": True, "openWorldHint": True } ) async def google_ads_search_terms(params: SearchTermsInput) -> str: """ Get search terms that triggered your ads. Discover actual search queries that matched your keywords. Essential for finding new keyword opportunities and negative keywords to exclude. Args: params: Search terms query parameters Returns: Search terms with performance metrics and matched keywords """ try: client = get_client() query = f""" SELECT campaign.name, ad_group.name, segments.search_term_match_type, search_term_view.search_term, ad_group_criterion.keyword.text, metrics.impressions, metrics.clicks, metrics.ctr, metrics.cost_micros, metrics.conversions FROM search_term_view WHERE segments.date DURING {params.date_range.value} AND metrics.impressions >= {params.min_impressions or 0} """ if params.campaign_id: query += f" AND campaign.id = {params.campaign_id}" query += f" ORDER BY metrics.impressions DESC LIMIT {params.limit}" rows = await execute_query(client, params.customer_id, query) search_terms = [] for row in rows: search_terms.append({ 'search_term': row.search_term_view.search_term, 'matched_keyword': row.ad_group_criterion.keyword.text, 'match_type': row.segments.search_term_match_type.name, 'campaign': row.campaign.name, 'ad_group': row.ad_group.name, 'impressions': row.metrics.impressions, 'clicks': row.metrics.clicks, 'ctr': row.metrics.ctr, 'cost': format_micros(row.metrics.cost_micros), 'conversions': row.metrics.conversions }) if params.response_format == ResponseFormat.JSON: return json.dumps({'search_terms': search_terms, 'count': len(search_terms)}, indent=2) else: output = "# Search Terms Report\n\n" output += f"**Total Search Terms**: {len(search_terms)}\n\n" output += "These are actual queries that triggered your ads. Look for:\n" output += "- High-performing terms to add as exact match keywords\n" output += "- Irrelevant terms to add as negative keywords\n\n" for term in search_terms[:30]: # Limit display output += f"## \"{term['search_term']}\"\n" output += f"- **Matched Keyword**: {term['matched_keyword']} ({term['match_type']})\n" output += f"- **Campaign**: {term['campaign']}\n" output += f"- **Impressions**: {term['impressions']:,} | **Clicks**: {term['clicks']}\n" output += f"- **CTR**: {format_percentage(term['ctr'])} | **Cost**: ${term['cost']:.2f}\n" output += f"- **Conversions**: {term['conversions']:.2f}\n\n" if len(search_terms) > 30: output += f"\n... and {len(search_terms) - 30} more search terms. Use JSON format or filters for full data.\n" return truncate_response(output) except Exception as e: return f"Error retrieving search terms: {str(e)}" @mcp.tool( name="google_ads_ad_group_performance", annotations={ "title": "Get Ad Group Performance", "readOnlyHint": True, "destructiveHint": False, "idempotentHint": True, "openWorldHint": True } ) async def google_ads_ad_group_performance(params: AdGroupPerformanceInput) -> str: """ Analyze performance at the ad group level. Get metrics for ad groups to identify which groups are performing well and which need optimization. Args: params: Ad group query parameters Returns: Ad group performance metrics """ try: client = get_client() query = f""" SELECT campaign.name, ad_group.id, ad_group.name, ad_group.status, metrics.cost_micros, metrics.clicks, metrics.impressions, metrics.ctr, metrics.average_cpc, metrics.conversions, metrics.conversion_rate FROM ad_group WHERE segments.date DURING {params.date_range.value} """ if params.campaign_id: query += f" AND campaign.id = {params.campaign_id}" query += f" ORDER BY metrics.cost_micros DESC LIMIT {params.limit}" rows = await execute_query(client, params.customer_id, query) ad_groups = [] for row in rows: ad_groups.append({ 'campaign': row.campaign.name, 'id': str(row.ad_group.id), 'name': row.ad_group.name, 'status': row.ad_group.status.name, 'cost': format_micros(row.metrics.cost_micros), 'clicks': row.metrics.clicks, 'impressions': row.metrics.impressions, 'ctr': row.metrics.ctr, 'average_cpc': format_micros(row.metrics.average_cpc), 'conversions': row.metrics.conversions, 'conversion_rate': row.metrics.conversion_rate }) if params.response_format == ResponseFormat.JSON: return json.dumps({'ad_groups': ad_groups, 'count': len(ad_groups)}, indent=2) else: output = "# Ad Group Performance\n\n" for ag in ad_groups: output += f"## {ag['name']} ({ag['status']})\n" output += f"- Campaign: {ag['campaign']}\n" output += f"- Cost: ${ag['cost']:,.2f}\n" output += f"- Clicks: {ag['clicks']:,} | Impressions: {ag['impressions']:,}\n" output += f"- CTR: {format_percentage(ag['ctr'])}\n" output += f"- Avg CPC: ${ag['average_cpc']:.2f}\n" output += f"- Conversions: {ag['conversions']:.2f} ({format_percentage(ag['conversion_rate'])})\n\n" return truncate_response(output) except Exception as e: return f"Error retrieving ad group performance: {str(e)}" @mcp.tool( name="google_ads_recommendations", annotations={ "title": "Get Google Ads Optimization Recommendations", "readOnlyHint": True, "destructiveHint": False, "idempotentHint": True, "openWorldHint": True } ) async def google_ads_recommendations(params: RecommendationsInput) -> str: """ Get AI-powered optimization recommendations from Google. Retrieve Google's automated recommendations for improving campaign performance, including keyword suggestions, bid adjustments, and budget recommendations. Args: params: Recommendations query parameters Returns: List of actionable optimization recommendations """ try: client = get_client() query = """ SELECT recommendation.type, recommendation.campaign, recommendation.ad_group, recommendation.resource_name, recommendation.impact FROM recommendation """ if params.recommendation_types: type_filter = " OR ".join([f"recommendation.type = '{t}'" for t in params.recommendation_types]) query += f" WHERE ({type_filter})" query += f" LIMIT {params.limit}" rows = await execute_query(client, params.customer_id, query) recommendations = [] for row in rows: rec = row.recommendation recommendations.append({ 'type': rec.type_.name if hasattr(rec.type_, 'name') else str(rec.type_), 'resource_name': rec.resource_name, 'impact': str(rec.impact) if hasattr(rec, 'impact') else 'N/A' }) if params.response_format == ResponseFormat.JSON: return json.dumps({'recommendations': recommendations, 'count': len(recommendations)}, indent=2) else: output = "# Google Ads Recommendations\n\n" output += f"**Total Recommendations**: {len(recommendations)}\n\n" if not recommendations: output += "No recommendations available at this time.\n" else: for i, rec in enumerate(recommendations, 1): output += f"{i}. **{rec['type']}**\n" output += f" - Impact: {rec['impact']}\n" output += f" - Resource: {rec['resource_name']}\n\n" return output except Exception as e: return f"Error retrieving recommendations: {str(e)}" @mcp.tool( name="google_ads_update_campaign_budget", annotations={ "title": "Update Campaign Budget", "readOnlyHint": False, "destructiveHint": False, "idempotentHint": False, "openWorldHint": True } ) async def google_ads_update_campaign_budget(params: UpdateCampaignBudgetInput) -> str: """ Update the daily budget for a campaign. Modify campaign budget allocation. Budget is specified in micros (multiply your dollar amount by 1,000,000). Args: params: Budget update parameters including campaign ID and new amount Returns: Confirmation of budget update """ try: client = get_client() campaign_service = client.get_service("CampaignService") campaign_budget_service = client.get_service("CampaignBudgetService") # First get the campaign to find its budget query = f""" SELECT campaign.id, campaign.name, campaign.campaign_budget FROM campaign WHERE campaign.id = {params.campaign_id} """ rows = await execute_query(client, params.customer_id, query) if not rows: return f"Campaign {params.campaign_id} not found." budget_resource = rows[0].campaign.campaign_budget # Update the budget budget_operation = client.get_type("CampaignBudgetOperation") budget = budget_operation.update budget.resource_name = budget_resource budget.amount_micros = params.budget_amount_micros budget_operation.update_mask = client.get_type("FieldMask") budget_operation.update_mask.paths.append("amount_micros") response = campaign_budget_service.mutate_campaign_budgets( customer_id=params.customer_id, operations=[budget_operation] ) budget_amount = format_micros(params.budget_amount_micros) return f"✓ Successfully updated budget for campaign {params.campaign_id} to ${budget_amount:.2f}/day" except Exception as e: return f"Error updating campaign budget: {str(e)}" @mcp.tool( name="google_ads_update_campaign_status", annotations={ "title": "Pause or Enable Campaign", "readOnlyHint": False, "destructiveHint": False, "idempotentHint": True, "openWorldHint": True } ) async def google_ads_update_campaign_status(params: UpdateCampaignStatusInput) -> str: """ Pause or enable a campaign. Change campaign status to control when ads are shown. Args: params: Status update parameters Returns: Confirmation of status change """ try: client = get_client() campaign_service = client.get_service("CampaignService") campaign_operation = client.get_type("CampaignOperation") campaign = campaign_operation.update campaign.resource_name = campaign_service.campaign_path(params.customer_id, params.campaign_id) # Set status if params.status == "ENABLED": campaign.status = client.enums.CampaignStatusEnum.ENABLED else: campaign.status = client.enums.CampaignStatusEnum.PAUSED campaign_operation.update_mask = client.get_type("FieldMask") campaign_operation.update_mask.paths.append("status") response = campaign_service.mutate_campaigns( customer_id=params.customer_id, operations=[campaign_operation] ) return f"✓ Successfully {params.status.lower()} campaign {params.campaign_id}" except Exception as e: return f"Error updating campaign status: {str(e)}" @mcp.tool( name="google_ads_custom_query", annotations={ "title": "Execute Custom GAQL Query", "readOnlyHint": True, "destructiveHint": False, "idempotentHint": True, "openWorldHint": True } ) async def google_ads_custom_query(params: CustomQueryInput) -> str: """ Execute a custom Google Ads Query Language (GAQL) query. For advanced users who want to write their own GAQL queries. Use the Google Ads Query Builder to construct queries: https://developers.google.com/google-ads/api/fields/latest/overview_query_builder Args: params: Custom query parameters including GAQL query string Returns: Query results in specified format """ try: client = get_client() rows = await execute_query(client, params.customer_id, params.query) # Convert rows to list of dicts results = [] for row in rows: row_dict = {} for field in row._pb: if field: row_dict[field] = str(getattr(row, field)) results.append(row_dict) if params.response_format == ResponseFormat.JSON: return json.dumps({'results': results, 'count': len(results)}, indent=2) else: output = "# Custom Query Results\n\n" output += f"**Query**: {params.query}\n\n" output += f"**Result Count**: {len(results)}\n\n" output += json.dumps(results, indent=2) return truncate_response(output) except Exception as e: return f"Error executing custom query: {str(e)}\n\nTip: Use the Query Builder to validate your GAQL syntax." # ============================================================================ # Server Entry Point # ============================================================================ if __name__ == "__main__": # Run the MCP server mcp.run()

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