# Directory Structure ``` ├── .cursor │ └── rules │ ├── fastmcp.mdc │ └── gaql-google-ads-query-language.mdc ├── .env.example ├── .gitignore ├── .python-version ├── bg.jpeg ├── docs │ ├── fastmcp.md │ ├── gaql-google-ads-query-language.md │ └── great-gaql-samples.md ├── format_customer_id_test.py ├── gaql-google-ads-query-language.mdc ├── google_ads_server.py ├── google-ads.svg ├── ixigo-logo.png ├── LICENSE ├── pulls │ └── 9 │ └── comments ├── pyproject.toml ├── README.md ├── requirements.txt ├── test_google_ads_mcp.py └── test_token_refresh.py ``` # Files -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- ``` 1 | 3.11 2 | ``` -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` 1 | # Python-generated files 2 | __pycache__/ 3 | *.py[oc] 4 | build/ 5 | dist/ 6 | wheels/ 7 | *.egg-info/ 8 | *.egg 9 | 10 | # Virtual environments 11 | .venv/ 12 | venv/ 13 | ENV/ 14 | env/ 15 | .env 16 | 17 | # Environment and credentials 18 | .env 19 | *.env 20 | service_account_credentials.json 21 | credentials.json 22 | token.json 23 | 24 | # Editor-specific files 25 | .vscode/ 26 | .idea/ 27 | *.sublime-* 28 | *.swp 29 | *.swo 30 | *~ 31 | 32 | # OS-specific files 33 | .DS_Store 34 | Thumbs.db 35 | desktop.ini 36 | 37 | # Testing and coverage 38 | .coverage 39 | .coverage.* 40 | htmlcov/ 41 | .pytest_cache/ 42 | .tox/ 43 | nosetests.xml 44 | coverage.xml 45 | *.cover 46 | 47 | # Documentation 48 | docs/_build/ 49 | site/ 50 | 51 | # Logs 52 | *.log 53 | 54 | google_ads_token.json 55 | 56 | uv.lock ``` -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- ``` 1 | # Google Ads MCP Environment Configuration 2 | # Copy this file to .env and fill in your actual values 3 | 4 | # Authentication Type (choose one: "oauth" or "service_account") 5 | GOOGLE_ADS_AUTH_TYPE=oauth 6 | 7 | # Credentials Path 8 | # For OAuth: Path to client_secret.json or saved token file 9 | # For Service Account: Path to service account key file 10 | GOOGLE_ADS_CREDENTIALS_PATH=/path/to/credentials.json 11 | 12 | # Google Ads Developer Token (required) 13 | GOOGLE_ADS_DEVELOPER_TOKEN=your_developer_token_here 14 | 15 | # Manager Account ID (optional, for MCC accounts) 16 | # Format: XXX-XXX-XXXX or XXXXXXXXXX 17 | GOOGLE_ADS_LOGIN_CUSTOMER_ID= 18 | 19 | # For OAuth-specific config (required if using OAuth and there's no client_secret.json) 20 | GOOGLE_ADS_CLIENT_ID=your_client_id_here 21 | GOOGLE_ADS_CLIENT_SECRET=your_client_secret_here 22 | 23 | # For Service Account-specific config (optional) 24 | # Email to impersonate with the service account (typically your admin email) 25 | GOOGLE_ADS_IMPERSONATION_EMAIL= 26 | ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown 1 | # Google Ads MCP 2 | 3 |  4 | 5 | A tool that connects [Google Ads](https://ads.google.com/) with Claude AI, allowing you to analyze your advertising data through natural language conversations. This integration gives you access to campaign information, performance metrics, keyword analytics, and ad management—all through simple chat with Claude. 6 | 7 | --- 8 | 9 | ## What Can This Tool Do For Advertising Professionals? 10 | 11 | 1. **Account Management** 12 | - See all your Google Ads accounts in one place 13 | - Get account details and basic campaign information 14 | 15 | 2. **Campaign Analytics & Reporting** 16 | - Discover which campaigns are performing best 17 | - Track impressions, clicks, conversions, and cost metrics 18 | - Analyze performance trends over time 19 | - Compare different time periods to spot changes 20 | - **Visualize your data** with charts and graphs created by Claude 21 | 22 | 3. **Keyword & Ad Performance** 23 | - Identify top and underperforming keywords 24 | - Analyze ad copy effectiveness 25 | - Check quality scores and competitive metrics 26 | - Get actionable insights on how to improve your campaigns 27 | 28 | 4. **Budget & Bid Management** 29 | - Monitor campaign budgets and spending 30 | - Analyze bid strategies and performance 31 | - Identify opportunities for optimization 32 | - Get recommendations for budget allocation 33 | 34 | --- 35 | 36 | ## Google Ads MCP Architecture Flow 37 | 38 | ```mermaid 39 | flowchart TB 40 | User(User) -->|Interacts with| Claude 41 | Claude(Claude AI Assistant) -->|Makes requests to| MCP[Google Ads MCP Server] 42 | User -->|Can also use| Cursor[Cursor AI Code Editor] 43 | Cursor -->|Makes requests to| MCP 44 | 45 | subgraph "MCP Server" 46 | FastMCP[FastMCP Server] 47 | Tools[Available Tools] 48 | Auth[Authentication] 49 | 50 | FastMCP -->|Exposes| Tools 51 | FastMCP -->|Uses| Auth 52 | end 53 | 54 | subgraph "Google Ads Tools" 55 | ListAccounts[list_accounts] 56 | ExecuteGAQL[execute_gaql_query] 57 | CampaignPerf[get_campaign_performance] 58 | AdPerf[get_ad_performance] 59 | RunGAQL[run_gaql] 60 | end 61 | 62 | Tools -->|Includes| ListAccounts 63 | Tools -->|Includes| ExecuteGAQL 64 | Tools -->|Includes| CampaignPerf 65 | Tools -->|Includes| AdPerf 66 | Tools -->|Includes| RunGAQL 67 | 68 | subgraph "Authentication" 69 | OAuth[OAuth 2.0 Client ID] 70 | ServiceAccount[Service Account] 71 | Credentials[Google Ads API Credentials] 72 | 73 | OAuth -->|Provides| Credentials 74 | ServiceAccount -->|Provides| Credentials 75 | end 76 | 77 | MCP -->|Communicates with| GoogleAdsAPI[Google Ads API] 78 | GoogleAdsAPI -->|Returns| AdData[Advertising Data] 79 | AdData -->|Analyzed by| Claude 80 | AdData -->|Visualized by| Claude 81 | AdData -->|Can be used by| Cursor 82 | 83 | Credentials -->|Authorizes| GoogleAdsAPI 84 | 85 | subgraph "Configuration" 86 | EnvVars[Environment Variables] 87 | ConfigFiles[Configuration Files] 88 | 89 | EnvVars -->|Configures| MCP 90 | ConfigFiles -->|Configures| Claude 91 | ConfigFiles -->|Configures| Cursor 92 | end 93 | ``` 94 | 95 | ## Available Tools 96 | 97 | Here's what you can ask Claude to do once you've set up this integration: 98 | 99 | | **What You Can Ask For** | **What It Does** | **What You'll Need to Provide** | 100 | |---------------------------------|-------------------------------------------------------------|----------------------------------------------------------------| 101 | | `list_accounts` | Shows all your Google Ads accounts | Nothing - just ask! | 102 | | `execute_gaql_query` | Runs a Google Ads Query Language query | Your account ID and a GAQL query | 103 | | `get_campaign_performance` | Shows campaign metrics with performance data | Your account ID and time period | 104 | | `get_ad_performance` | Detailed analysis of your ad creative performance | Your account ID and time period | 105 | | `run_gaql` | Runs any arbitrary GAQL query with formatting options | Your account ID, query, and format (table, JSON, or CSV) | 106 | 107 | ### Using the Advanced Query Tools 108 | 109 | The `run_gaql` tool is especially powerful as it allows you to run any custom Google Ads Query Language (GAQL) query. Here are some example queries you can use: 110 | 111 | ### Example 1: Basic campaign metrics 112 | 113 | ```sql 114 | SELECT 115 | campaign.name, 116 | metrics.clicks, 117 | metrics.impressions 118 | FROM campaign 119 | WHERE segments.date DURING LAST_7DAYS 120 | ``` 121 | 122 | ### Example 2: Ad group performance 123 | 124 | ```sql 125 | SELECT 126 | ad_group.name, 127 | metrics.conversions, 128 | metrics.cost_micros 129 | FROM ad_group 130 | WHERE metrics.clicks > 100 131 | ``` 132 | 133 | ### Example 3: Keyword analysis 134 | 135 | ```sql 136 | SELECT 137 | keyword.text, 138 | metrics.average_position, 139 | metrics.ctr 140 | FROM keyword_view 141 | ORDER BY metrics.impressions DESC 142 | ``` 143 | 144 | *For a complete list of all available tools and their detailed descriptions, ask Claude to "list tools" after setup.* 145 | 146 | --- 147 | 148 | ## Getting Started (No Coding Experience Required!) 149 | 150 | ### 1. Set Up Google Ads API Access 151 | 152 | Before using this tool, you'll need to create API credentials that allow Claude to access your Google Ads data. You can choose between two authentication methods: 153 | 154 | #### Option A: OAuth 2.0 Client ID (User Authentication) 155 | 156 | Best for individual users or desktop applications: 157 | 158 | 1. Go to the [Google Cloud Console](https://console.cloud.google.com/) 159 | 2. Create a new project or select an existing one 160 | 3. Enable the Google Ads API 161 | 4. Go to "Credentials" → "Create Credentials" → "OAuth Client ID" 162 | 5. Choose "Desktop Application" as the application type 163 | 6. Download the OAuth client configuration file (client_secret.json) 164 | 7. Create a Google Ads API Developer token (see below) 165 | 166 | #### Option B: Service Account (Server-to-Server Authentication) 167 | 168 | Better for automated systems or managing multiple accounts: 169 | 170 | 1. Go to the [Google Cloud Console](https://console.cloud.google.com/) 171 | 2. Create a new project or select an existing one 172 | 3. Enable the Google Ads API 173 | 4. Go to "Credentials" → "Create Credentials" → "Service Account" 174 | 5. Download the service account key file (JSON) 175 | 6. Grant the service account access to your Google Ads accounts 176 | 7. Create a Google Ads API Developer token (see below) 177 | 178 | #### Authentication Token Refreshing 179 | 180 | The application now includes robust token refresh handling: 181 | 182 | - **OAuth 2.0 Tokens**: The tool will automatically refresh expired OAuth tokens when possible, or prompt for re-authentication if the refresh token is invalid. 183 | - **Service Account Tokens**: Service account tokens are automatically generated and refreshed as needed without user intervention. 184 | 185 | #### Authentication Method Comparison 186 | 187 | Choose OAuth 2.0 Client ID if: 188 | 189 | - You're building a desktop application 190 | - Users need to explicitly grant access 191 | - You're managing a single account or a few personal accounts 192 | - You want users to have control over access permissions 193 | 194 | Choose Service Account if: 195 | 196 | - You're building an automated system 197 | - You need server-to-server authentication 198 | - You're managing multiple accounts programmatically 199 | - You don't want/need user interaction for authentication 200 | - You need automatic token refreshing without user intervention 201 | 202 | #### Getting a Developer Token 203 | 204 | 1. Sign in to your Google Ads account at [https://ads.google.com](https://ads.google.com) 205 | 2. Click on Tools & Settings (wrench icon) in the top navigation 206 | 3. Under "Setup", click "API Center" 207 | 4. If you haven't already, accept the Terms of Service 208 | 5. Click "Apply for token" 209 | 6. Fill out the application form with details about how you plan to use the API 210 | 7. Submit the application and wait for approval (usually 1-3 business days) 211 | 212 | Note: Initially, you'll get a test Developer Token that has some limitations. Once you've tested your implementation, you can apply for a production token that removes these restrictions. 213 | 214 | ### Understanding the Login Customer ID 215 | 216 | The `GOOGLE_ADS_LOGIN_CUSTOMER_ID` is optional and is primarily used when: 217 | 218 | - You're working with a Google Ads Manager Account (MCC) 219 | - You need to access multiple client accounts under that manager account 220 | 221 | The Login Customer ID should be your Manager Account ID (format: XXX-XXX-XXXX) if: 222 | 223 | - You're accessing multiple accounts under a manager account 224 | - You want to use manager account credentials to access client accounts 225 | 226 | You can skip this setting if: 227 | 228 | - You're only accessing a single Google Ads account 229 | - You're using credentials directly from the account you want to access 230 | 231 | To find your Manager Account ID: 232 | 233 | 1. Sign in to your Google Ads Manager Account 234 | 2. Click on the settings icon (gear) 235 | 3. Your Manager Account ID will be displayed in the format XXX-XXX-XXXX 236 | 4. Download the credentials file (a JSON file) 237 | 238 | **🎬 Watch this beginner-friendly tutorial on Youtube:** 239 | COMING SOON 240 | 241 | ### 2. Install Required Software 242 | 243 | You'll need to install these tools on your computer: 244 | 245 | - [Python](https://www.python.org/downloads/) (version 3.11 or newer) - This runs the connection between Google Ads and Claude 246 | - [Node.js](https://nodejs.org/en) - Required for running the MCP inspector and certain MCP components 247 | - [Claude Desktop](https://claude.ai/download) - The AI assistant you'll chat with 248 | 249 | Make sure both Python and Node.js are properly installed and available in your system path before proceeding. 250 | 251 | ### 3. Download the Google Ads MCP 252 | 253 | You need to download this tool to your computer. The easiest way is: 254 | 255 | 1. Click the green "Code" button at the top of this page 256 | 2. Select "Download ZIP" 257 | 3. Unzip the downloaded file to a location you can easily find (like your Documents folder) 258 | 259 | Alternatively, if you're familiar with Git: 260 | 261 | ```bash 262 | git clone https://github.com/ixigo/mcp-google-ads.git 263 | ``` 264 | 265 | ### 4. Install Required Components 266 | 267 | Open your computer's Terminal (Mac) or Command Prompt (Windows): 268 | 269 | 1. Navigate to the folder where you unzipped the files: 270 | 271 | ```bash 272 | # Example (replace with your actual path): 273 | cd ~/Documents/mcp-google-ads-main 274 | ``` 275 | 276 | 2. Create a virtual environment (this keeps the project dependencies isolated): 277 | 278 | ```bash 279 | # Using uv (recommended): 280 | uv venv .venv 281 | 282 | # If uv is not installed, install it first: 283 | pip install uv 284 | # Then create the virtual environment: 285 | uv venv .venv 286 | 287 | # OR using standard Python: 288 | python -m venv .venv 289 | ``` 290 | 291 | **Note:** If you get a "pip not found" error when trying to install uv, see the "If you get 'pip not found' error" section below. 292 | 293 | 3. Activate the virtual environment: 294 | 295 | ```bash 296 | # On Mac/Linux: 297 | source .venv/bin/activate 298 | 299 | # On Windows: 300 | .venv\Scripts\activate 301 | ``` 302 | 303 | 4. Install the required dependencies: 304 | 305 | ```bash 306 | # Using uv: 307 | uv pip install -r requirements.txt 308 | 309 | # OR using standard pip: 310 | pip install -r requirements.txt 311 | 312 | # If you encounter any issues with the MCP package, install it separately: 313 | pip install mcp 314 | ``` 315 | 316 | **If you get "pip not found" error:** 317 | 318 | ```bash 319 | # First ensure pip is installed and updated: 320 | python3 -m ensurepip --upgrade 321 | python3 -m pip install --upgrade pip 322 | 323 | # Then try installing the requirements again: 324 | python3 -m pip install -r requirements.txt 325 | 326 | # Or to install uv: 327 | python3 -m pip install uv 328 | ``` 329 | 330 | When you see `(.venv)` at the beginning of your command prompt, it means the virtual environment is active and the dependencies will be installed there without affecting your system Python installation. 331 | 332 | ### 5. Setting Up Environment Configuration 333 | 334 | The Google Ads MCP now supports environment file configuration for easier setup. 335 | 336 | #### Using .env File (Recommended) 337 | 338 | 1. Copy the `.env.example` file to `.env` in your project directory: 339 | 340 | ```bash 341 | cp .env.example .env 342 | ``` 343 | 344 | 2. Edit the `.env` file with your actual configuration values: 345 | 346 | ```bash 347 | # Edit the .env file with your favorite text editor 348 | # For Mac: 349 | nano .env 350 | 351 | # For Windows: 352 | notepad .env 353 | ``` 354 | 355 | 3. Set the following values in your `.env` file: 356 | 357 | ``` 358 | # Authentication Type: "oauth" or "service_account" 359 | GOOGLE_ADS_AUTH_TYPE=oauth 360 | 361 | # Path to your credentials file (OAuth client secret or service account key) 362 | GOOGLE_ADS_CREDENTIALS_PATH=/path/to/your/credentials.json 363 | 364 | # Your Google Ads Developer Token 365 | GOOGLE_ADS_DEVELOPER_TOKEN=your_developer_token_here 366 | 367 | # Optional: Manager Account ID (if applicable) 368 | GOOGLE_ADS_LOGIN_CUSTOMER_ID=your_manager_account_id 369 | ``` 370 | 371 | 4. Save the file. 372 | 373 | The application will automatically load these values from the `.env` file when it starts. 374 | 375 | #### Using Direct Environment Variables 376 | 377 | You can also set environment variables directly in your system or in the configuration files for Claude or Cursor: 378 | 379 | ##### For Claude Desktop 380 | 381 | ```json 382 | { 383 | "mcpServers": { 384 | "googleAdsServer": { 385 | "command": "/FULL/PATH/TO/mcp-google-ads-main/.venv/bin/python", 386 | "args": ["/FULL/PATH/TO/mcp-google-ads-main/google_ads_server.py"], 387 | "env": { 388 | "GOOGLE_ADS_AUTH_TYPE": "oauth", 389 | "GOOGLE_ADS_CREDENTIALS_PATH": "/FULL/PATH/TO/mcp-google-ads-main/credentials.json", 390 | "GOOGLE_ADS_DEVELOPER_TOKEN": "YOUR_DEVELOPER_TOKEN_HERE", 391 | "GOOGLE_ADS_LOGIN_CUSTOMER_ID": "YOUR_MANAGER_ACCOUNT_ID_HERE" 392 | } 393 | } 394 | } 395 | } 396 | ``` 397 | 398 | ##### For Cursor 399 | 400 | ```json 401 | { 402 | "mcpServers": { 403 | "googleAdsServer": { 404 | "command": "/FULL/PATH/TO/mcp-google-ads-main/.venv/bin/python", 405 | "args": ["/FULL/PATH/TO/mcp-google-ads-main/google_ads_server.py"], 406 | "env": { 407 | "GOOGLE_ADS_AUTH_TYPE": "oauth", 408 | "GOOGLE_ADS_CREDENTIALS_PATH": "/FULL/PATH/TO/mcp-google-ads-main/credentials.json", 409 | "GOOGLE_ADS_DEVELOPER_TOKEN": "YOUR_DEVELOPER_TOKEN_HERE", 410 | "GOOGLE_ADS_LOGIN_CUSTOMER_ID": "YOUR_MANAGER_ACCOUNT_ID_HERE" 411 | } 412 | } 413 | } 414 | } 415 | ``` 416 | 417 | ### 6. Connect Claude to Google Ads 418 | 419 | 1. Download and install [Claude Desktop](https://claude.ai/download) if you haven't already 420 | 2. Make sure you have your Google service account credentials file saved somewhere on your computer 421 | 3. Open your computer's Terminal (Mac) or Command Prompt (Windows) and type: 422 | 423 | ```bash 424 | # For Mac users: 425 | nano ~/Library/Application\ Support/Claude/claude_desktop_config.json 426 | 427 | # For Windows users: 428 | notepad %APPDATA%\Claude\claude_desktop_config.json 429 | ``` 430 | 431 | Add the following text (this tells Claude how to connect to Google Ads): 432 | 433 | ```json 434 | { 435 | "mcpServers": { 436 | "googleAdsServer": { 437 | "command": "/FULL/PATH/TO/mcp-google-ads-main/.venv/bin/python", 438 | "args": ["/FULL/PATH/TO/mcp-google-ads-main/google_ads_server.py"], 439 | "env": { 440 | "GOOGLE_ADS_CREDENTIALS_PATH": "/FULL/PATH/TO/mcp-google-ads-main/service_account_credentials.json", 441 | "GOOGLE_ADS_DEVELOPER_TOKEN": "YOUR_DEVELOPER_TOKEN_HERE", 442 | "GOOGLE_ADS_LOGIN_CUSTOMER_ID": "YOUR_MANAGER_ACCOUNT_ID_HERE" 443 | } 444 | } 445 | } 446 | } 447 | ``` 448 | 449 | **Important:** Replace all paths and values with the actual information for your account: 450 | 451 | - The first path should point to the Python executable inside your virtual environment 452 | - The second path should point to the `google_ads_server.py` file inside the folder you unzipped 453 | - The third path should point to your Google service account credentials JSON file 454 | - Add your Google Ads Developer Token 455 | - Add your Google Ads Manager Account ID (if applicable) 456 | 457 | Examples: 458 | 459 | - Mac: 460 | - Python path: `/Users/ernesto/Documents/mcp-google-ads/.venv/bin/python` 461 | - Script path: `/Users/ernesto/Documents/mcp-google-ads/google_ads_server.py` 462 | - Windows: 463 | - Python path: `C:\\Users\\ernesto\\Documents\\mcp-google-ads\\.venv\\Scripts\\python.exe` 464 | - Script path: `C:\\Users\\ernesto\\Documents\\mcp-google-ads\\google_ads_server.py` 465 | 466 | 4. Save the file: 467 | 468 | - Mac: Press Ctrl+O, then Enter, then Ctrl+X to exit 469 | - Windows: Click File > Save, then close Notepad 470 | 471 | 5. Restart Claude Desktop 472 | 473 | 6. When Claude opens, you should now see Google Ads tools available in the tools section 474 | 475 | ### 5a. Connect to Cursor (AI Code Editor) 476 | 477 | Cursor is an AI-powered code editor that can be enhanced with MCP tools. You can integrate this Google Ads MCP tool with Cursor to analyze advertising data directly within your coding environment. 478 | 479 | #### Setting Up Cursor Integration 480 | 481 | 1. If you haven't already, download and install [Cursor](https://cursor.sh/) 482 | 2. Create a Cursor MCP configuration file: 483 | 484 | **For project-specific configuration:** 485 | Create a `.cursor/mcp.json` file in your project directory. 486 | 487 | **For global configuration (available in all projects):** 488 | Create a `~/.cursor/mcp.json` file in your home directory. 489 | 490 | 3. Add the following configuration to your MCP config file: 491 | 492 | ```json 493 | { 494 | "mcpServers": { 495 | "googleAdsServer": { 496 | "command": "/FULL/PATH/TO/mcp-google-ads-main/.venv/bin/python", 497 | "args": ["/FULL/PATH/TO/mcp-google-ads-main/google_ads_server.py"], 498 | "env": { 499 | "GOOGLE_ADS_CREDENTIALS_PATH": "/FULL/PATH/TO/mcp-google-ads-main/service_account_credentials.json", 500 | "GOOGLE_ADS_DEVELOPER_TOKEN": "YOUR_DEVELOPER_TOKEN_HERE", 501 | "GOOGLE_ADS_LOGIN_CUSTOMER_ID": "YOUR_MANAGER_ACCOUNT_ID_HERE" 502 | } 503 | } 504 | } 505 | } 506 | ``` 507 | 508 | **Important:** Replace all paths and values with the actual information for your account, just like in the Claude Desktop configuration. 509 | 510 | 4. Restart Cursor or reload the workspace to apply the new configuration. 511 | 512 | 5. The Google Ads MCP will now appear in Cursor's "Available Tools" section and can be used by Cursor's AI agent when needed. 513 | 514 | #### Using Google Ads MCP in Cursor 515 | 516 | When working in Cursor, you can ask the AI agent to use the Google Ads tools directly. For example: 517 | 518 | - "Use the Google Ads MCP to list all my accounts and show me which ones have the highest spend." 519 | - "Can you analyze my campaign performance for the last 30 days using the Google Ads MCP?" 520 | - "Run a GAQL query to find my top converting keywords using the Google Ads tools." 521 | 522 | Cursor will prompt you to approve the tool usage (unless you've enabled Yolo mode) and then display the results directly in the chat interface. 523 | 524 | #### Cursor-Specific Features 525 | 526 | When using the Google Ads MCP with Cursor, you can: 527 | 528 | 1. **Combine Code and Ads Analysis**: Ask Cursor to analyze your marketing-related code alongside actual campaign performance data. 529 | 2. **Generate Data Visualizations**: Request charts and visualizations of your ad performance directly in your development environment. 530 | 3. **Implement Recommendations**: Let Cursor suggest code improvements based on your actual advertising data. 531 | 532 | This integration is particularly valuable for developers working on marketing automation, analytics dashboards, or e-commerce applications where ad performance directly impacts code decisions. 533 | 534 | ### 6. Start Analyzing Your Advertising Data! 535 | 536 | Now you can ask Claude questions about your Google Ads data! Claude can not only retrieve the data but also analyze it, explain trends, and create visualizations to help you understand your advertising performance better. 537 | 538 | Here are some powerful prompts you can use with each tool: 539 | 540 | | **Tool Name** | **Sample Prompt** | 541 | |---------------------------------|--------------------------------------------------------------------------------------------------| 542 | | `list_accounts` | "List all my Google Ads accounts and tell me which ones have the highest spend this month." | 543 | | `execute_gaql_query` | "Execute this query for account 123-456-7890: SELECT campaign.name, metrics.clicks FROM campaign WHERE metrics.impressions > 1000" | 544 | | `get_campaign_performance` | "Show me the top 10 campaigns for account 123-456-7890 in the last 30 days, highlight any with ROAS below 2, and suggest optimization strategies." | 545 | | `get_ad_performance` | "Do a comprehensive analysis of which ad copy elements are driving the best CTR in my search campaigns and give me actionable recommendations." | 546 | | `run_gaql` | "Run this query and format it as a CSV: SELECT ad_group.name, metrics.clicks, metrics.conversions FROM ad_group WHERE campaign.name LIKE '%Brand%'" | 547 | 548 | You can also ask Claude to combine multiple tools and analyze the results. For example: 549 | 550 | - "Find my top 20 converting keywords, check their quality scores and impression share, and create a report highlighting opportunities for scaling." 551 | 552 | - "Analyze my account's performance trend over the last 90 days, identify my fastest-growing campaigns, and check if there are any budget limitations holding them back." 553 | 554 | - "Compare my desktop vs. mobile ad performance, visualize the differences with charts, and recommend specific campaigns that need mobile bid adjustments based on performance gaps." 555 | 556 | - "Identify campaigns where I'm spending the most on search terms that aren't in my keyword list, then suggest which ones should be added as exact match keywords." 557 | 558 | Claude will use the Google Ads tools to fetch the data, present it in an easy-to-understand format, create visualizations when helpful, and provide actionable insights based on the results. 559 | 560 | --- 561 | 562 | ## Data Visualization Capabilities 563 | 564 | Claude can help you visualize your Google Ads data in various ways: 565 | 566 | - **Trend Charts**: See how metrics change over time 567 | - **Comparison Graphs**: Compare different campaigns or ad groups 568 | - **Performance Distributions**: Understand how your ads perform across devices or audiences 569 | - **Correlation Analysis**: Identify relationships between spend and conversion metrics 570 | - **Heatmaps**: Visualize complex datasets with color-coded representations 571 | 572 | Simply ask Claude to "visualize" or "create a chart" when analyzing your data, and it will generate appropriate visualizations to help you understand the information better. 573 | 574 | --- 575 | 576 | ## Troubleshooting 577 | 578 | ### Python Command Not Found 579 | 580 | On macOS, the default Python command is often `python3` rather than `python`, which can cause issues with some applications including Node.js integrations. 581 | 582 | If you encounter errors related to Python not being found, you can create an alias: 583 | 584 | 1. Create a Python alias (one-time setup): 585 | ```bash 586 | # For macOS users: 587 | sudo ln -s $(which python3) /usr/local/bin/python 588 | 589 | # If that doesn't work, try finding your Python installation: 590 | sudo ln -s /Library/Frameworks/Python.framework/Versions/3.11/bin/python3 /usr/local/bin/python 591 | ``` 592 | 593 | 2. Verify the alias works: 594 | 595 | ```bash 596 | python --version 597 | ``` 598 | 599 | This creates a symbolic link so that when applications call `python`, they'll actually use your `python3` installation. 600 | 601 | ### Claude Configuration Issues 602 | 603 | If you're having trouble connecting: 604 | 605 | 1. Make sure all file paths in your configuration are correct and use the full path 606 | 2. Check that your service account has access to your Google Ads accounts 607 | 3. Verify that your Developer Token is valid and correctly entered 608 | 4. Restart Claude Desktop after making any changes 609 | 5. Look for error messages in Claude's response when you try to use a tool 610 | 6. Ensure your virtual environment is activated when running the server manually 611 | 612 | ### Google Ads API Limitations 613 | 614 | If you encounter issues related to API quotas or permissions: 615 | 616 | 1. Check your Google Ads API quota limits in the Google Cloud Console 617 | 2. Ensure your Developer Token has the appropriate access level 618 | 3. Verify that you've granted the proper permissions to your service account 619 | 620 | ### Other Unexpected Issues 621 | 622 | If you encounter any other unexpected issues during installation or usage: 623 | 624 | 1. Copy the exact error message you're receiving 625 | 2. Contact Ernesto Cohnen at [email protected] for support, including: 626 | - What you were trying to do 627 | - The exact error message 628 | - Your operating system 629 | - Any steps you've already tried 630 | 631 | You can also consult AI assistants which can often help diagnose and resolve technical issues by suggesting specific solutions for your situation. 632 | 633 | Remember that most issues have been encountered by others before, and there's usually a straightforward solution available. 634 | 635 | ### Testing Your Setup 636 | 637 | The repository includes test files that let you verify your Google Ads API connection is working correctly before using it with Claude or Cursor. 638 | 639 | #### Testing Basic Functionality 640 | 641 | 1. Make sure your virtual environment is activated: 642 | 643 | ```bash 644 | # On Mac/Linux: 645 | source .venv/bin/activate 646 | 647 | # On Windows: 648 | .venv\Scripts\activate 649 | ``` 650 | 651 | 2. Configure the environment variables in the test file or set them in your environment: 652 | - Open `test_google_ads_mcp.py` in a text editor 653 | - Find the section starting with `if not os.environ.get("GOOGLE_ADS_CREDENTIALS_PATH"):` 654 | - Update the placeholder values with your actual credentials or comment out this section if you've set them as environment variables 655 | 656 | 3. Run the test: 657 | ```bash 658 | python test_google_ads_mcp.py 659 | ``` 660 | 661 | 4. The test will: 662 | - List all your Google Ads accounts 663 | - Use the first account ID to test campaign performance retrieval 664 | - Test ad performance data 665 | - Retrieve ad creatives 666 | - Run a sample GAQL query 667 | 668 | #### Testing Authentication and Token Refresh 669 | 670 | To specifically test the authentication and token refresh mechanisms: 671 | 672 | 1. Make sure your virtual environment is activated and your `.env` file is configured. 673 | 674 | 2. Run the token refresh test: 675 | ```bash 676 | python test_token_refresh.py 677 | ``` 678 | 679 | 3. This test will: 680 | - Verify that credentials can be loaded from your configured auth type (OAuth or service account) 681 | - Display information about the current token status and expiry 682 | - Test the customer ID formatting function 683 | - For OAuth tokens, attempt to refresh the token and verify it worked 684 | 685 | The token refresh test can help confirm that both OAuth and service account credentials are properly configured before using the server with Claude or Cursor. 686 | 687 | If all tests complete successfully, your setup is working correctly and ready to use with Claude or Cursor. 688 | 689 | --- 690 | 691 | ## Contributing 692 | 693 | Found a bug or have an idea for improvement? We welcome your input! Open an issue or submit a pull request on GitHub, or contact Ernesto Cohnen directly at [[email protected]](mailto:[email protected]). 694 | 695 | --- 696 | 697 | ## License 698 | 699 | This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details. 700 | 701 | --- 702 | 703 | ## About ixigo 704 | 705 | ixigo is India's leading travel app, helping millions of travelers find the best deals on flights, trains, buses, and hotels. For more information, visit [ixigo.com](https://www.ixigo.com). 706 | 707 | <img src="ixigo-logo.png" alt="ixigo logo" width="200px" /> 708 | 709 | ixigo is a technology company that builds products to help people find the best deals on flights, trains, buses, and hotels. We're a team of travel enthusiasts who are passionate about making travel more affordable and accessible to everyone. ``` -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- ``` 1 | # MCP requirements 2 | mcp>=0.0.11 3 | 4 | # Google API requirements 5 | google-auth>=2.25.2 6 | google-auth-oauthlib>=1.1.0 7 | google-auth-httplib2>=0.1.1 8 | requests>=2.31.0 9 | 10 | # Environment configuration 11 | python-dotenv>=1.0.0 12 | 13 | # Optional visualization dependencies 14 | matplotlib>=3.7.3 15 | pandas>=2.1.4 ``` -------------------------------------------------------------------------------- /google-ads.svg: -------------------------------------------------------------------------------- ``` 1 | <?xml version="1.0" encoding="UTF-8"?> 2 | <!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools --> 3 | <svg width="800px" height="800px" viewBox="0 -13 256 256" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid"> 4 | <g> 5 | <path d="M5.888,166.405103 L90.88,20.9 C101.676138,27.2558621 156.115862,57.3844138 164.908138,63.1135172 L79.9161379,208.627448 C70.6206897,220.906621 -5.888,185.040138 5.888,166.396276 L5.888,166.405103 Z" fill="#FBBC04"> 6 | </path> 7 | <path d="M250.084224,166.401789 L165.092224,20.9055131 C153.210293,1.13172 127.619121,-6.05393517 106.600638,5.62496138 C85.582155,17.3038579 79.182155,42.4624786 91.0640861,63.1190303 L176.056086,208.632961 C187.938017,228.397927 213.52919,235.583582 234.547672,223.904686 C254.648086,212.225789 261.966155,186.175582 250.084224,166.419444 L250.084224,166.401789 Z" fill="#4285F4"> 8 | </path> 9 | <ellipse fill="#34A853" cx="42.6637241" cy="187.924414" rx="42.6637241" ry="41.6044138"> 10 | </ellipse> 11 | </g> 12 | </svg> ``` -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- ```toml 1 | [project] 2 | name = "mcp-google-ads" 3 | version = "0.1.0" 4 | description = "Google Ads API integration for Model Context Protocol (MCP)" 5 | readme = "README.md" 6 | requires-python = ">=3.11" 7 | license = {text = "MIT"} 8 | authors = [ 9 | {name = "Ernesto Cohnen", email = "[email protected]"} 10 | ] 11 | keywords = ["mcp", "google ads", "seo", "sem", "claude","search analytics"] 12 | classifiers = [ 13 | "Development Status :: 4 - Beta", 14 | "Intended Audience :: Developers", 15 | "License :: OSI Approved :: MIT License", 16 | "Programming Language :: Python :: 3", 17 | "Programming Language :: Python :: 3.11", 18 | "Topic :: Internet :: WWW/HTTP :: Indexing/Search", 19 | "Topic :: Software Development :: Libraries :: Python Modules" 20 | ] 21 | dependencies = [ 22 | "google-api-python-client>=2.163.0", 23 | "google-auth-httplib2>=0.2.0", 24 | "google-auth-oauthlib>=1.2.1", 25 | "mcp[cli]>=1.3.0", 26 | ] 27 | 28 | [project.urls] 29 | "Homepage" = "https://github.com/cohnen/mcp-google-ads" 30 | "Bug Tracker" = "https://github.com/cohnen/mcp-google-ads/issues" 31 | 32 | [build-system] 33 | requires = ["setuptools>=61.0", "wheel"] 34 | build-backend = "setuptools.build_meta" 35 | 36 | [tool.setuptools] 37 | packages = ["mcp_google_ads"] 38 | ``` -------------------------------------------------------------------------------- /format_customer_id_test.py: -------------------------------------------------------------------------------- ```python 1 | def format_customer_id(customer_id: str) -> str: 2 | """Format customer ID to ensure it's 10 digits without dashes.""" 3 | # Convert to string if passed as integer or another type 4 | customer_id = str(customer_id) 5 | 6 | # Remove any quotes surrounding the customer_id (both escaped and unescaped) 7 | customer_id = customer_id.replace('\"', '').replace('"', '') 8 | 9 | # Remove any non-digit characters (including dashes, braces, etc.) 10 | customer_id = ''.join(char for char in customer_id if char.isdigit()) 11 | 12 | # Ensure it's 10 digits with leading zeros if needed 13 | return customer_id.zfill(10) 14 | 15 | def test_format_customer_id(): 16 | """Test the format_customer_id function with various input formats.""" 17 | test_cases = [ 18 | # Regular ID 19 | ("9873186703", "9873186703"), 20 | # ID with dashes 21 | ("987-318-6703", "9873186703"), 22 | # ID with quotes 23 | ('"9873186703"', "9873186703"), 24 | # ID with escaped quotes 25 | ('\"9873186703\"', "9873186703"), 26 | # ID with leading zeros that exceed 10 digits - should preserve only last 10 27 | ("0009873186703", "0009873186703"), 28 | # Short ID that needs padding 29 | ("12345", "0000012345"), 30 | # ID with other non-digit characters 31 | ("{9873186703}", "9873186703"), 32 | ] 33 | 34 | print("\n=== Testing format_customer_id with various formats ===") 35 | for input_id, expected in test_cases: 36 | result = format_customer_id(input_id) 37 | print(f"Input: {input_id}") 38 | print(f"Result: {result}") 39 | print(f"Expected: {expected}") 40 | print(f"Test {'PASSED' if result == expected else 'FAILED'}") 41 | print("-" * 50) 42 | 43 | if __name__ == "__main__": 44 | # Run format_customer_id tests 45 | test_format_customer_id() ``` -------------------------------------------------------------------------------- /test_token_refresh.py: -------------------------------------------------------------------------------- ```python 1 | #!/usr/bin/env python3 2 | """ 3 | Test script for Google Ads token refresh mechanism and authentication methods. 4 | 5 | This script tests both OAuth 2.0 and Service Account authentication methods, 6 | and verifies that token refresh works correctly. 7 | """ 8 | 9 | import os 10 | import json 11 | import time 12 | from datetime import datetime, timedelta 13 | from dotenv import load_dotenv 14 | 15 | # Load environment variables from .env file 16 | load_dotenv() 17 | 18 | # Import get_credentials function from the server 19 | from google_ads_server import get_credentials, get_headers, format_customer_id 20 | 21 | def test_token_refresh(): 22 | """Test the token refresh mechanism.""" 23 | print("\n" + "="*50) 24 | print("GOOGLE ADS TOKEN REFRESH TEST") 25 | print("="*50) 26 | 27 | # Get the authentication type from environment 28 | auth_type = os.environ.get("GOOGLE_ADS_AUTH_TYPE", "oauth") 29 | print(f"\nAuthentication type: {auth_type}") 30 | 31 | # Get credentials 32 | print("\nGetting credentials...") 33 | creds = get_credentials() 34 | 35 | # Print credentials info 36 | if hasattr(creds, 'expired') and hasattr(creds, 'expiry'): 37 | print(f"Token expired: {creds.expired}") 38 | print(f"Token expiry: {creds.expiry}") 39 | 40 | # Calculate time until expiry 41 | if creds.expiry: 42 | now = datetime.now() 43 | expiry = creds.expiry 44 | if isinstance(expiry, str): 45 | expiry = datetime.fromisoformat(expiry.replace('Z', '+00:00')) 46 | 47 | time_until_expiry = expiry - now 48 | print(f"Time until expiry: {time_until_expiry}") 49 | else: 50 | print("Service account credentials (no expiry info available)") 51 | 52 | # Get headers using the credentials 53 | print("\nGetting API headers...") 54 | headers = get_headers(creds) 55 | 56 | # Remove sensitive info for display 57 | safe_headers = headers.copy() 58 | if 'Authorization' in safe_headers: 59 | token = safe_headers['Authorization'] 60 | if token: 61 | # Show only the first 10 chars of the token 62 | token_start = token[:15] 63 | safe_headers['Authorization'] = f"{token_start}...TRUNCATED" 64 | 65 | print("API Headers:") 66 | for key, value in safe_headers.items(): 67 | print(f" {key}: {value}") 68 | 69 | # Test if we can force a token refresh (for OAuth tokens) 70 | if auth_type.lower() == "oauth" and hasattr(creds, 'refresh'): 71 | print("\nAttempting to force token refresh...") 72 | try: 73 | old_token = creds.token[:15] if hasattr(creds, 'token') else None 74 | creds.refresh(Request()) 75 | new_token = creds.token[:15] if hasattr(creds, 'token') else None 76 | 77 | print(f"Old token started with: {old_token}...") 78 | print(f"New token starts with: {new_token}...") 79 | print("Token refresh successful!" if old_token != new_token else "Token stayed the same") 80 | except Exception as e: 81 | print(f"Error refreshing token: {str(e)}") 82 | 83 | print("\nToken test completed successfully!") 84 | 85 | def test_customer_id_formatting(): 86 | """Test the customer ID formatting function.""" 87 | print("\n" + "="*50) 88 | print("CUSTOMER ID FORMATTING TEST") 89 | print("="*50) 90 | 91 | test_cases = [ 92 | "1234567890", 93 | "123-456-7890", 94 | "123.456.7890", 95 | "123 456 7890", 96 | "\"1234567890\"", 97 | "1234", 98 | 1234567890, 99 | None 100 | ] 101 | 102 | print("\nTesting customer ID formatting:") 103 | for test_case in test_cases: 104 | try: 105 | formatted = format_customer_id(test_case) 106 | print(f" Input: {test_case}, Output: {formatted}") 107 | except Exception as e: 108 | print(f" Input: {test_case}, Error: {str(e)}") 109 | 110 | if __name__ == "__main__": 111 | # Import Request here to avoid circular imports 112 | from google.auth.transport.requests import Request 113 | 114 | try: 115 | test_token_refresh() 116 | test_customer_id_formatting() 117 | print("\nAll tests completed successfully!") 118 | except Exception as e: 119 | print(f"\nTest failed with error: {str(e)}") ``` -------------------------------------------------------------------------------- /test_google_ads_mcp.py: -------------------------------------------------------------------------------- ```python 1 | import asyncio 2 | import json 3 | import os 4 | import sys 5 | from pathlib import Path 6 | 7 | # Add the parent directory to Python path for imports 8 | sys.path.insert(0, str(Path(__file__).parent)) 9 | 10 | # Import your MCP server module 11 | import google_ads_server 12 | 13 | def test_format_customer_id(): 14 | """Test the format_customer_id function with various input formats.""" 15 | test_cases = [ 16 | # Regular ID 17 | ("9873186703", "9873186703"), 18 | # ID with dashes 19 | ("987-318-6703", "9873186703"), 20 | # ID with quotes 21 | ('"9873186703"', "9873186703"), 22 | # ID with escaped quotes 23 | ('\"9873186703\"', "9873186703"), 24 | # ID with leading zeros 25 | ("0009873186703", "9873186703"), 26 | # Short ID that needs padding 27 | ("12345", "0000012345"), 28 | # ID with other non-digit characters 29 | ("{9873186703}", "9873186703"), 30 | ] 31 | 32 | print("\n=== Testing format_customer_id with various formats ===") 33 | for input_id, expected in test_cases: 34 | result = google_ads_server.format_customer_id(input_id) 35 | print(f"Input: {input_id}") 36 | print(f"Result: {result}") 37 | print(f"Expected: {expected}") 38 | print(f"Test {'PASSED' if result == expected else 'FAILED'}") 39 | print("-" * 50) 40 | 41 | async def test_mcp_tools(): 42 | """Test Google Ads MCP tools directly.""" 43 | # Get a list of available customer IDs first 44 | print("=== Testing list_accounts ===") 45 | accounts_result = await google_ads_server.list_accounts() 46 | print(accounts_result) 47 | 48 | # Parse the accounts to extract a customer ID for further tests 49 | customer_id = None 50 | for line in accounts_result.split('\n'): 51 | if line.startswith("Account ID:"): 52 | customer_id = line.replace("Account ID:", "").strip() 53 | break 54 | 55 | if not customer_id: 56 | print("No customer IDs found. Cannot continue testing.") 57 | return 58 | 59 | print(f"\nUsing customer ID: {customer_id} for testing\n") 60 | 61 | # Test campaign performance 62 | print("\n=== Testing get_campaign_performance ===") 63 | campaign_result = await google_ads_server.get_campaign_performance(customer_id, days=90) 64 | print(campaign_result) 65 | 66 | # Test ad performance 67 | print("\n=== Testing get_ad_performance ===") 68 | ad_result = await google_ads_server.get_ad_performance(customer_id, days=90) 69 | print(ad_result) 70 | 71 | # Test ad creatives 72 | print("\n=== Testing get_ad_creatives ===") 73 | creatives_result = await google_ads_server.get_ad_creatives(customer_id) 74 | print(creatives_result) 75 | 76 | # Test custom GAQL query 77 | print("\n=== Testing run_gaql ===") 78 | query = """ 79 | SELECT 80 | campaign.id, 81 | campaign.name, 82 | campaign.status 83 | FROM campaign 84 | LIMIT 5 85 | """ 86 | gaql_result = await google_ads_server.run_gaql(customer_id, query, format="json") 87 | print(gaql_result) 88 | 89 | async def test_asset_methods(): 90 | """Test Asset-related MCP tools directly.""" 91 | # Get a list of available customer IDs first 92 | print("=== Testing Asset Methods ===") 93 | accounts_result = await google_ads_server.list_accounts() 94 | 95 | # Parse the accounts to extract a customer ID for further tests 96 | customer_id = None 97 | for line in accounts_result.split('\n'): 98 | if line.startswith("Account ID:"): 99 | customer_id = line.replace("Account ID:", "").strip() 100 | break 101 | 102 | if not customer_id: 103 | print("No customer IDs found. Cannot continue testing.") 104 | return 105 | 106 | print(f"\nUsing customer ID: {customer_id} for testing asset methods\n") 107 | 108 | # Test get_image_assets 109 | print("\n=== Testing get_image_assets ===") 110 | image_assets_result = await google_ads_server.get_image_assets(customer_id, limit=10) 111 | print(image_assets_result) 112 | 113 | # Extract an asset ID for further testing if available 114 | asset_id = None 115 | for line in image_assets_result.split('\n'): 116 | if line.startswith("1. Asset ID:"): 117 | asset_id = line.replace("1. Asset ID:", "").strip() 118 | break 119 | 120 | # Use a smaller number of days for testing to avoid the INVALID_VALUE_WITH_DURING_OPERATOR error 121 | days_to_test = 30 # Use 30 instead of 90 122 | 123 | # Test get_asset_usage if we found an asset ID 124 | if asset_id: 125 | print(f"\n=== Testing get_asset_usage with asset ID: {asset_id} ===") 126 | try: 127 | asset_usage_result = await google_ads_server.get_asset_usage(customer_id, asset_id=asset_id, asset_type="IMAGE") 128 | print(asset_usage_result) 129 | except Exception as e: 130 | print(f"Error in get_asset_usage: {str(e)}") 131 | else: 132 | print("\nNo asset ID found to test get_asset_usage") 133 | 134 | # Test analyze_image_assets with a valid date range 135 | print(f"\n=== Testing analyze_image_assets with {days_to_test} days ===") 136 | try: 137 | analyze_result = await google_ads_server.analyze_image_assets(customer_id, days=days_to_test) 138 | print(analyze_result) 139 | except Exception as e: 140 | print(f"Error in analyze_image_assets: {str(e)}") 141 | 142 | if __name__ == "__main__": 143 | # Run format_customer_id tests first 144 | # test_format_customer_id() 145 | 146 | # Setup environment variables if they're not already set 147 | if not os.environ.get("GOOGLE_ADS_CREDENTIALS_PATH"): 148 | # Set environment variables for testing (comment out if already set in your environment) 149 | os.environ["GOOGLE_ADS_CREDENTIALS_PATH"] = "google_ads_token.json" 150 | os.environ["GOOGLE_ADS_DEVELOPER_TOKEN"] = "YOUR_DEVELOPER_TOKEN" # Replace with placeholder 151 | os.environ["GOOGLE_ADS_CLIENT_ID"] = "YOUR_CLIENT_ID" # Replace with placeholder 152 | os.environ["GOOGLE_ADS_CLIENT_SECRET"] = "YOUR_CLIENT_SECRET" # Replace with placeholder 153 | 154 | # Run the MCP tools test (uncomment to run full tests) 155 | # asyncio.run(test_mcp_tools()) 156 | 157 | # Run the asset methods test (uncomment to run full tests) 158 | asyncio.run(test_asset_methods()) ``` -------------------------------------------------------------------------------- /docs/great-gaql-samples.md: -------------------------------------------------------------------------------- ```markdown 1 | ## Advanced GAQL Query Examples 2 | 3 | ### 1. Multi-level Performance Analysis with Geographic and Device Segmentation 4 | 5 | ```sql 6 | SELECT 7 | campaign.id, 8 | campaign.name, 9 | ad_group.id, 10 | ad_group.name, 11 | segments.geo_target_region, 12 | segments.device, 13 | segments.day_of_week, 14 | metrics.impressions, 15 | metrics.clicks, 16 | metrics.conversions, 17 | metrics.conversion_value, 18 | metrics.cost_micros, 19 | metrics.cost_per_conversion, 20 | metrics.conversion_rate, 21 | metrics.return_on_ad_spend 22 | FROM ad_group 23 | WHERE 24 | campaign.status = 'ENABLED' 25 | AND ad_group.status = 'ENABLED' 26 | AND segments.date DURING LAST_90_DAYS 27 | AND metrics.impressions > 100 28 | ORDER BY 29 | segments.geo_target_region, 30 | segments.device, 31 | metrics.return_on_ad_spend DESC 32 | LIMIT 1000 33 | ``` 34 | 35 | This query provides a comprehensive performance breakdown by geography, device type, and day of week, helping identify specific combinations that drive the best return on ad spend. 36 | 37 | ### 2. Bidding Strategy Effectiveness Analysis 38 | 39 | ```sql 40 | SELECT 41 | campaign.id, 42 | campaign.name, 43 | campaign.bidding_strategy_type, 44 | bidding_strategy.id, 45 | bidding_strategy.name, 46 | bidding_strategy.type, 47 | campaign.target_cpa.target_cpa_micros, 48 | campaign.target_roas.target_roas, 49 | segments.date, 50 | metrics.impressions, 51 | metrics.clicks, 52 | metrics.conversions, 53 | metrics.conversion_value, 54 | metrics.cost_micros, 55 | metrics.average_cpc, 56 | metrics.cost_per_conversion 57 | FROM campaign 58 | WHERE 59 | campaign.status = 'ENABLED' 60 | AND segments.date DURING LAST_30_DAYS 61 | AND metrics.impressions > 0 62 | ORDER BY 63 | campaign.bidding_strategy_type, 64 | segments.date 65 | ``` 66 | 67 | This query helps analyze the effectiveness of different bidding strategies by comparing key performance metrics across campaigns using various automated bidding approaches. 68 | 69 | ### 3. Ad Performance by Landing Page with Quality Score Analysis 70 | 71 | ```sql 72 | SELECT 73 | campaign.id, 74 | campaign.name, 75 | ad_group.id, 76 | ad_group.name, 77 | ad_group_ad.ad.id, 78 | ad_group_ad.ad.final_urls, 79 | ad_group_ad.ad.type, 80 | ad_group_ad.ad.expanded_text_ad.headline_part1, 81 | ad_group_ad.ad.expanded_text_ad.headline_part2, 82 | ad_group_criterion.keyword.text, 83 | ad_group_criterion.quality_info.quality_score, 84 | ad_group_criterion.quality_info.creative_quality_score, 85 | ad_group_criterion.quality_info.post_click_quality_score, 86 | ad_group_criterion.quality_info.search_predicted_ctr, 87 | metrics.impressions, 88 | metrics.clicks, 89 | metrics.conversions, 90 | metrics.conversion_value, 91 | metrics.cost_micros, 92 | metrics.average_cpc, 93 | metrics.ctr 94 | FROM ad_group_ad 95 | WHERE 96 | campaign.status = 'ENABLED' 97 | AND ad_group.status = 'ENABLED' 98 | AND ad_group_ad.status = 'ENABLED' 99 | AND segments.date DURING LAST_30_DAYS 100 | AND metrics.impressions > 100 101 | ORDER BY 102 | metrics.conversion_value DESC, 103 | ad_group_criterion.quality_info.quality_score DESC 104 | ``` 105 | 106 | This query examines ad performance in relation to landing pages and quality scores, helping identify high-performing ad creatives and their associated landing pages. 107 | 108 | ### 4. Keyword Performance Analysis with Impression Share and Position Metrics 109 | 110 | ```sql 111 | SELECT 112 | campaign.id, 113 | campaign.name, 114 | ad_group.id, 115 | ad_group.name, 116 | ad_group_criterion.criterion_id, 117 | ad_group_criterion.keyword.text, 118 | ad_group_criterion.keyword.match_type, 119 | metrics.impressions, 120 | metrics.clicks, 121 | metrics.conversions, 122 | metrics.conversion_value, 123 | metrics.cost_micros, 124 | metrics.absolute_top_impression_percentage, 125 | metrics.top_impression_percentage, 126 | metrics.search_impression_share, 127 | metrics.search_rank_lost_impression_share, 128 | metrics.search_budget_lost_impression_share 129 | FROM keyword_view 130 | WHERE 131 | campaign.status = 'ENABLED' 132 | AND ad_group.status = 'ENABLED' 133 | AND ad_group_criterion.status = 'ENABLED' 134 | AND segments.date DURING LAST_90_DAYS 135 | AND metrics.impressions > 10 136 | ORDER BY 137 | metrics.conversion_value DESC, 138 | metrics.search_impression_share ASC 139 | ``` 140 | 141 | This query helps identify keywords that are performing well but may be limited by impression share, indicating opportunities for bid or budget adjustments. 142 | 143 | ### 5. Complex Audience Segmentation Performance Analysis 144 | 145 | ```sql 146 | SELECT 147 | campaign.id, 148 | campaign.name, 149 | ad_group.id, 150 | ad_group.name, 151 | segments.audience.id, 152 | segments.audience.name, 153 | segments.audience.type, 154 | segments.date, 155 | metrics.impressions, 156 | metrics.clicks, 157 | metrics.conversions, 158 | metrics.conversion_value, 159 | metrics.cost_micros, 160 | metrics.average_cpc, 161 | metrics.ctr, 162 | metrics.conversion_rate, 163 | metrics.value_per_conversion 164 | FROM ad_group 165 | WHERE 166 | campaign.advertising_channel_type = 'DISPLAY' 167 | AND campaign.status = 'ENABLED' 168 | AND ad_group.status = 'ENABLED' 169 | AND segments.date DURING LAST_90_DAYS 170 | AND segments.audience.id IS NOT NULL 171 | ORDER BY 172 | segments.audience.type, 173 | metrics.conversion_value DESC 174 | ``` 175 | 176 | This query analyzes the performance of different audience segments across display campaigns, helping identify the most valuable audience types. 177 | 178 | ### 6. Shopping Campaign Product Performance Analysis 179 | 180 | ```sql 181 | SELECT 182 | campaign.id, 183 | campaign.name, 184 | ad_group.id, 185 | ad_group.name, 186 | segments.product_item_id, 187 | segments.product_title, 188 | segments.product_type_l1, 189 | segments.product_type_l2, 190 | segments.product_type_l3, 191 | segments.product_type_l4, 192 | segments.product_type_l5, 193 | segments.product_brand, 194 | metrics.impressions, 195 | metrics.clicks, 196 | metrics.conversions, 197 | metrics.conversion_value, 198 | metrics.cost_micros, 199 | metrics.ctr, 200 | metrics.conversion_rate, 201 | metrics.return_on_ad_spend 202 | FROM shopping_performance_view 203 | WHERE 204 | campaign.advertising_channel_type = 'SHOPPING' 205 | AND campaign.status = 'ENABLED' 206 | AND ad_group.status = 'ENABLED' 207 | AND segments.date DURING LAST_30_DAYS 208 | AND metrics.impressions > 0 209 | ORDER BY 210 | metrics.return_on_ad_spend DESC 211 | ``` 212 | 213 | This query provides a detailed breakdown of shopping campaign performance by product attributes, helping identify high-performing products and product categories. 214 | 215 | ### 7. Ad Schedule Performance with Bid Modifier Analysis 216 | 217 | ```sql 218 | SELECT 219 | campaign.id, 220 | campaign.name, 221 | ad_group.id, 222 | ad_group.name, 223 | ad_schedule_view.day_of_week, 224 | ad_schedule_view.start_hour, 225 | ad_schedule_view.end_hour, 226 | campaign_criterion.bid_modifier, 227 | segments.date, 228 | metrics.impressions, 229 | metrics.clicks, 230 | metrics.conversions, 231 | metrics.conversion_value, 232 | metrics.cost_micros, 233 | metrics.ctr, 234 | metrics.conversion_rate, 235 | metrics.value_per_conversion 236 | FROM ad_schedule_view 237 | WHERE 238 | campaign.status = 'ENABLED' 239 | AND segments.date DURING LAST_14_DAYS 240 | ORDER BY 241 | ad_schedule_view.day_of_week, 242 | ad_schedule_view.start_hour 243 | ``` 244 | 245 | This query analyzes performance across different ad schedules and compares it with the applied bid modifiers, helping identify opportunities for schedule-based bid adjustments. 246 | 247 | ### 8. Cross-Campaign Asset Performance Analysis 248 | 249 | ```sql 250 | SELECT 251 | campaign.id, 252 | campaign.name, 253 | ad_group.id, 254 | ad_group.name, 255 | asset.id, 256 | asset.type, 257 | asset.name, 258 | asset.text_asset.text, 259 | asset.image_asset.full_size.url, 260 | asset_performance_label, 261 | metrics.impressions, 262 | metrics.clicks, 263 | metrics.conversions, 264 | metrics.cost_micros, 265 | metrics.ctr 266 | FROM asset_performance_label_view 267 | WHERE 268 | campaign.status = 'ENABLED' 269 | AND ad_group.status = 'ENABLED' 270 | AND segments.date DURING LAST_30_DAYS 271 | ORDER BY 272 | asset.type, 273 | metrics.conversions DESC 274 | ``` 275 | 276 | This query helps analyze performance of assets (images, text, etc.) across campaigns, helping identify high-performing creative elements. 277 | 278 | ### 9. Geographic Performance with Location Bid Modifier Analysis 279 | 280 | ```sql 281 | SELECT 282 | campaign.id, 283 | campaign.name, 284 | geographic_view.country_criterion_id, 285 | geographic_view.location_type, 286 | geographic_view.geo_target_constant, 287 | campaign_criterion.bid_modifier, 288 | segments.date, 289 | metrics.impressions, 290 | metrics.clicks, 291 | metrics.conversions, 292 | metrics.conversion_value, 293 | metrics.cost_micros, 294 | metrics.ctr, 295 | metrics.conversion_rate 296 | FROM geographic_view 297 | WHERE 298 | campaign.status = 'ENABLED' 299 | AND segments.date DURING LAST_30_DAYS 300 | ORDER BY 301 | geographic_view.country_criterion_id, 302 | metrics.conversion_value DESC 303 | ``` 304 | 305 | This query analyzes performance across different geographic locations and compares it with location bid modifiers, helping identify opportunities for geographic bid adjustments. 306 | 307 | ### 10. Advanced Budget Utilization and Performance Analysis 308 | 309 | ```sql 310 | SELECT 311 | campaign.id, 312 | campaign.name, 313 | campaign.status, 314 | campaign_budget.amount_micros, 315 | campaign_budget.total_amount_micros, 316 | campaign_budget.delivery_method, 317 | campaign_budget.reference_count, 318 | campaign_budget.has_recommended_budget, 319 | campaign_budget.recommended_budget_amount_micros, 320 | segments.date, 321 | metrics.cost_micros, 322 | metrics.impressions, 323 | metrics.clicks, 324 | metrics.conversions, 325 | metrics.conversion_value, 326 | (metrics.cost_micros * 1.0) / (campaign_budget.amount_micros * 1.0) AS budget_utilization_rate 327 | FROM campaign 328 | WHERE 329 | campaign.status IN ('ENABLED', 'PAUSED') 330 | AND segments.date DURING LAST_30_DAYS 331 | ORDER BY 332 | segments.date DESC, 333 | budget_utilization_rate DESC 334 | ``` 335 | 336 | This query helps analyze budget utilization across campaigns, with a calculated field for budget utilization rate, helping identify campaigns that consistently use their full budget or need budget adjustments. 337 | 338 | ## Practical Applications of These Queries 339 | 340 | These advanced GAQL queries can help you: 341 | 342 | 1. **Identify performance trends** across different dimensions (geographic, temporal, device-based) 343 | 2. **Optimize bidding strategies** by comparing performance across different automated bidding approaches 344 | 3. **Improve quality scores** by analyzing the relationship between landing pages, ad creatives, and performance metrics 345 | 4. **Maximize impression share** for high-performing keywords and ad groups 346 | 5. **Refine audience targeting** by identifying the most valuable audience segments 347 | 6. **Optimize product feeds** for shopping campaigns by analyzing performance at the product level 348 | 7. **Fine-tune ad scheduling** based on day and hour performance analysis 349 | 8. **Improve creative assets** by identifying high-performing images, text, and other creative elements 350 | 9. **Adjust geographic targeting** based on performance differences across locations 351 | 10. **Optimize budget allocation** to maximize return on ad spend 352 | 353 | ``` -------------------------------------------------------------------------------- /docs/gaql-google-ads-query-language.md: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | description: Use this to write better GAQL queries 3 | globs: 4 | alwaysApply: false 5 | --- 6 | # Google Ads Query Language (GAQL) Guidelines 7 | 8 | ## Overview 9 | 10 | The Google Ads Query Language (GAQL) is a powerful tool for querying the Google Ads API that allows you to retrieve: 11 | 12 | 1. **Resources** and their related attributes, segments, and metrics using `GoogleAdsService.Search` or `GoogleAdsService.SearchStream` 13 | 2. **Metadata** about available fields and resources using `GoogleAdsFieldService` 14 | 15 | ## Field Categories 16 | 17 | Understanding field categories is essential for building effective GAQL queries: 18 | 19 | 1. **RESOURCE**: Represents a primary entity (e.g., `campaign`, `ad_group`) that can be used in the FROM clause 20 | 2. **ATTRIBUTE**: Properties of a resource (e.g., `campaign.id`, `campaign.name`). Including these may segment results depending on the resource relationship 21 | 3. **SEGMENT**: Fields that always segment search queries (e.g., `segments.date`, `segments.device`) 22 | 4. **METRIC**: Performance data fields (e.g., `metrics.impressions`, `metrics.clicks`) that never segment search queries 23 | 24 | ## Query Structure 25 | 26 | A GAQL query consists of the following components: 27 | 28 | ``` 29 | SELECT 30 | <field_1>, 31 | <field_2>, 32 | ... 33 | FROM <resource> 34 | WHERE <condition_1> AND <condition_2> AND ... 35 | ORDER BY <field_1> [ASC|DESC], <field_2> [ASC|DESC], ... 36 | LIMIT <number_of_results> 37 | ``` 38 | 39 | ### SELECT Clause 40 | 41 | The `SELECT` clause specifies the fields to return in the query results: 42 | 43 | ``` 44 | SELECT 45 | campaign.id, 46 | campaign.name, 47 | metrics.impressions, 48 | segments.device 49 | ``` 50 | 51 | Only fields that are marked as `selectable: true` in the `GoogleAdsField` metadata can be used in the SELECT clause. 52 | 53 | ### FROM Clause 54 | 55 | The `FROM` clause specifies the primary resource type to query from. Only one resource can be specified, and it must have the category `RESOURCE`. 56 | 57 | ``` 58 | FROM campaign 59 | ``` 60 | 61 | ### WHERE Clause (optional) 62 | 63 | The `WHERE` clause specifies conditions to filter the results. Only fields marked as `filterable: true` in the `GoogleAdsField` metadata can be used for filtering. 64 | 65 | ``` 66 | WHERE 67 | campaign.status = 'ENABLED' 68 | AND metrics.impressions > 1000 69 | AND segments.date DURING LAST_30_DAYS 70 | ``` 71 | 72 | ### ORDER BY Clause (optional) 73 | 74 | The `ORDER BY` clause specifies how to sort the results. Only fields marked as `sortable: true` in the `GoogleAdsField` metadata can be used for sorting. 75 | 76 | ``` 77 | ORDER BY metrics.impressions DESC, campaign.id 78 | ``` 79 | 80 | ### LIMIT Clause (optional) 81 | 82 | The `LIMIT` clause restricts the number of results returned. 83 | 84 | ``` 85 | LIMIT 100 86 | ``` 87 | 88 | ## Field Metadata Exploration 89 | 90 | To explore available fields and their properties, use the `GoogleAdsFieldService`: 91 | 92 | ``` 93 | SELECT 94 | name, 95 | category, 96 | selectable, 97 | filterable, 98 | sortable, 99 | selectable_with, 100 | attribute_resources, 101 | metrics, 102 | segments, 103 | data_type, 104 | enum_values, 105 | is_repeated 106 | WHERE name = "campaign.id" 107 | ``` 108 | 109 | Key metadata properties to understand: 110 | 111 | - **`selectable`**: Whether the field can be used in a SELECT clause 112 | - **`filterable`**: Whether the field can be used in a WHERE clause 113 | - **`sortable`**: Whether the field can be used in an ORDER BY clause 114 | - **`selectable_with`**: Lists resources, segments, and metrics that are selectable with this field 115 | - **`attribute_resources`**: For RESOURCE fields, lists the resources that are selectable with this resource and don't segment metrics 116 | - **`metrics`**: For RESOURCE fields, lists metrics that are selectable when this resource is in the FROM clause 117 | - **`segments`**: For RESOURCE fields, lists fields that segment metrics when this resource is used in the FROM clause 118 | - **`data_type`**: Determines which operators can be used with the field in WHERE clauses 119 | - **`enum_values`**: Lists possible values for ENUM type fields 120 | - **`is_repeated`**: Whether the field can contain multiple values 121 | 122 | ## Data Types and Operators 123 | 124 | Different field data types support different operators in WHERE clauses: 125 | 126 | ### String Fields 127 | - `=`, `!=`, `IN`, `NOT IN` 128 | - `LIKE`, `NOT LIKE` (case-sensitive string matching) 129 | - `CONTAINS ANY`, `CONTAINS ALL`, `CONTAINS NONE` (for repeated fields) 130 | 131 | ### Numeric Fields 132 | - `=`, `!=`, `<`, `<=`, `>`, `>=` 133 | - `IN`, `NOT IN` 134 | 135 | ### Date Fields 136 | - `=`, `!=`, `<`, `<=`, `>`, `>=` 137 | - `DURING` (with named date ranges) 138 | - `BETWEEN` (with date literals) 139 | 140 | ### Enum Fields 141 | - `=`, `!=`, `IN`, `NOT IN` 142 | - Values must match exactly as listed in `enum_values` 143 | 144 | ### Boolean Fields 145 | - `=`, `!=` 146 | - Values must be `TRUE` or `FALSE` 147 | 148 | ## Date Ranges 149 | 150 | ### Literal Date Ranges 151 | ``` 152 | WHERE segments.date BETWEEN '2020-01-01' AND '2020-01-31' 153 | ``` 154 | 155 | ### Named Date Ranges 156 | ``` 157 | WHERE segments.date DURING LAST_7_DAYS 158 | WHERE segments.date DURING LAST_14_DAYS 159 | WHERE segments.date DURING LAST_30_DAYS 160 | WHERE segments.date DURING LAST_90_DAYS 161 | WHERE segments.date DURING THIS_MONTH 162 | WHERE segments.date DURING LAST_MONTH 163 | WHERE segments.date DURING THIS_QUARTER 164 | ``` 165 | 166 | ### Date Functions 167 | ``` 168 | WHERE segments.date = YESTERDAY 169 | WHERE segments.date = TODAY 170 | ``` 171 | 172 | ## Case Sensitivity Rules 173 | 174 | 1. **Field and resource names**: Case-sensitive (`campaign.id` not `Campaign.Id`) 175 | 2. **Enumeration values**: Case-sensitive (`'ENABLED'` not `'enabled'`) 176 | 3. **String literals in conditions**: 177 | - Case-insensitive by default (`WHERE campaign.name = 'brand campaign'`) 178 | - Use `LIKE` for case-sensitive matching (`WHERE campaign.name LIKE 'Brand Campaign'`) 179 | 180 | ## Ordering and Limiting Results 181 | 182 | ### Ordering 183 | - Results can be ordered by one or more fields 184 | - Use `ASC` (default) or `DESC` to specify direction 185 | - Only fields marked as `sortable: true` can be used 186 | 187 | ``` 188 | ORDER BY metrics.impressions DESC, campaign.id ASC 189 | ``` 190 | 191 | ### Limiting 192 | - Use LIMIT to restrict the number of rows returned 193 | - Always use ORDER BY with LIMIT for consistent pagination 194 | - The maximum value is system-dependent 195 | 196 | ``` 197 | LIMIT 100 198 | ``` 199 | 200 | ## Query Examples 201 | 202 | ### Basic Campaign Query 203 | ``` 204 | SELECT 205 | campaign.id, 206 | campaign.name, 207 | campaign.status 208 | FROM campaign 209 | ORDER BY campaign.id 210 | ``` 211 | 212 | ### Query with Metrics and Filtering 213 | ``` 214 | SELECT 215 | campaign.id, 216 | campaign.name, 217 | metrics.impressions, 218 | metrics.clicks, 219 | metrics.cost_micros 220 | FROM campaign 221 | WHERE 222 | campaign.status = 'ENABLED' 223 | AND metrics.impressions > 1000 224 | ORDER BY metrics.impressions DESC 225 | LIMIT 10 226 | ``` 227 | 228 | ### Query with Segments 229 | ``` 230 | SELECT 231 | campaign.id, 232 | campaign.name, 233 | segments.date, 234 | metrics.impressions, 235 | metrics.clicks, 236 | metrics.conversions 237 | FROM campaign 238 | WHERE 239 | segments.date DURING LAST_30_DAYS 240 | AND campaign.status = 'ENABLED' 241 | ORDER BY segments.date DESC, metrics.impressions DESC 242 | ``` 243 | 244 | ### Query with Attributed Resources 245 | ``` 246 | SELECT 247 | campaign.id, 248 | campaign.name, 249 | campaign.status, 250 | bidding_strategy.id, 251 | bidding_strategy.name, 252 | bidding_strategy.type 253 | FROM campaign 254 | WHERE campaign.status = 'ENABLED' 255 | ``` 256 | 257 | ### Field Metadata Query 258 | ``` 259 | SELECT 260 | name, 261 | category, 262 | selectable, 263 | filterable, 264 | sortable, 265 | data_type, 266 | enum_values 267 | WHERE name LIKE 'campaign.%' 268 | ``` 269 | 270 | I'll help add information about assets to the GAQL guidelines document. Here's how I would incorporate the new information: 271 | 272 | ```markdown:gaql-google-ads-query-language.mdc 273 | // ... existing code ... 274 | 275 | ## Asset Queries 276 | 277 | ### Asset Entity Queries 278 | 279 | You can get a list of assets and their attributes by querying the `asset` entity: 280 | 281 | ``` 282 | SELECT 283 | asset.id, 284 | asset.name, 285 | asset.resource_name, 286 | asset.type 287 | FROM asset 288 | ``` 289 | 290 | ### Type-Specific Asset Attributes 291 | 292 | Assets have type-specific attributes that can be queried based on their type: 293 | 294 | ``` 295 | SELECT 296 | asset.id, 297 | asset.name, 298 | asset.resource_name, 299 | asset.youtube_video_asset.youtube_video_id 300 | FROM asset 301 | WHERE asset.type = 'YOUTUBE_VIDEO' 302 | ``` 303 | 304 | ### Asset Metrics at Different Levels 305 | 306 | Asset metrics are available through three main resources: 307 | 308 | 1. **ad_group_asset**: Asset metrics at the ad group level 309 | 2. **campaign_asset**: Asset metrics at the campaign level 310 | 3. **customer_asset**: Asset metrics at the customer level 311 | 312 | Example of querying ad-group level asset metrics: 313 | 314 | ``` 315 | SELECT 316 | ad_group.id, 317 | asset.id, 318 | metrics.clicks, 319 | metrics.impressions 320 | FROM ad_group_asset 321 | WHERE segments.date DURING LAST_MONTH 322 | ORDER BY metrics.impressions DESC 323 | ``` 324 | 325 | ### Ad-Level Asset Performance 326 | 327 | Ad-level performance metrics for assets are aggregated in the `ad_group_ad_asset_view`. 328 | 329 | **Note**: The `ad_group_ad_asset_view` only returns information for assets related to App ads. 330 | 331 | This view includes the `performance_label` attribute with the following possible values: 332 | - `BEST`: Best performing assets 333 | - `GOOD`: Good performing assets 334 | - `LOW`: Worst performing assets 335 | - `LEARNING`: Asset has impressions but stats aren't statistically significant yet 336 | - `PENDING`: Asset doesn't have performance information yet (may be under review) 337 | - `UNKNOWN`: Value unknown in this version 338 | - `UNSPECIFIED`: Not specified 339 | 340 | Example query for ad-level asset performance: 341 | 342 | ``` 343 | SELECT 344 | ad_group_ad_asset_view.ad_group_ad, 345 | ad_group_ad_asset_view.asset, 346 | ad_group_ad_asset_view.field_type, 347 | ad_group_ad_asset_view.performance_label, 348 | metrics.impressions, 349 | metrics.clicks, 350 | metrics.cost_micros, 351 | metrics.conversions 352 | FROM ad_group_ad_asset_view 353 | WHERE segments.date DURING LAST_MONTH 354 | ORDER BY ad_group_ad_asset_view.performance_label 355 | ``` 356 | 357 | ### Asset Source Information 358 | 359 | - `Asset.source` is only accurate for mutable Assets 360 | - For the source of RSA (Responsive Search Ad) Assets, use `AdGroupAdAsset.source` 361 | 362 | // ... existing code ... 363 | ``` 364 | 365 | This addition provides comprehensive information about querying assets in GAQL, including different asset types, how to access metrics at various levels, performance labeling, and important notes about asset source information. 366 | 367 | 368 | ## Best Practices 369 | 370 | 1. **Field Selection**: Only select the fields you need to reduce response size and improve performance. 371 | 372 | 2. **Filtering**: Apply filters in the `WHERE` clause to limit results to relevant data. 373 | 374 | 3. **Verify Field Properties**: Before using a field in a query, check its metadata to ensure it's selectable, filterable, or sortable as needed. 375 | 376 | 4. **Result Ordering**: Always use `ORDER BY` to ensure consistent results, especially when using pagination. 377 | 378 | 5. **Result Limiting**: Use `LIMIT` to restrict number of returned rows and improve performance. 379 | 380 | 6. **Handle Repeated Fields**: For fields where `is_repeated = true`, use appropriate operators like `CONTAINS ANY`, `CONTAINS ALL`, or `CONTAINS NONE`. 381 | 382 | 7. **Understand Segmentation**: Be aware that including segment fields or certain attribute fields will cause metrics to be segmented in the results. 383 | 384 | 8. **Date Handling**: Use appropriate date functions and ranges for filtering by date segments. 385 | 386 | 9. **Pagination**: For large result sets, use the page token provided in the response to retrieve subsequent pages. 387 | 388 | 10. **Check Enum Values**: For enum fields, verify the allowed values in the `enum_values` property before using them in queries. 389 | 390 | By following these guidelines and understanding the metadata of GAQL fields, you'll be able to create effective and efficient GAQL queries for retrieving and analyzing your Google Ads data. 391 | ``` -------------------------------------------------------------------------------- /google_ads_server.py: -------------------------------------------------------------------------------- ```python 1 | from typing import Any, Dict, List, Optional, Union 2 | from pydantic import Field 3 | import os 4 | import json 5 | import requests 6 | from datetime import datetime, timedelta 7 | from pathlib import Path 8 | 9 | from google_auth_oauthlib.flow import InstalledAppFlow 10 | from google.oauth2.credentials import Credentials 11 | from google.oauth2 import service_account 12 | from google.auth.transport.requests import Request 13 | from google.auth.exceptions import RefreshError 14 | import logging 15 | 16 | # MCP 17 | from mcp.server.fastmcp import FastMCP 18 | 19 | # Configure logging 20 | logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') 21 | logger = logging.getLogger('google_ads_server') 22 | 23 | mcp = FastMCP( 24 | "google-ads-server", 25 | dependencies=[ 26 | "google-auth-oauthlib", 27 | "google-auth", 28 | "requests", 29 | "python-dotenv" 30 | ] 31 | ) 32 | 33 | # Constants and configuration 34 | SCOPES = ['https://www.googleapis.com/auth/adwords'] 35 | API_VERSION = "v19" # Google Ads API version 36 | 37 | # Load environment variables 38 | try: 39 | from dotenv import load_dotenv 40 | # Load from .env file if it exists 41 | load_dotenv() 42 | logger.info("Environment variables loaded from .env file") 43 | except ImportError: 44 | logger.warning("python-dotenv not installed, skipping .env file loading") 45 | 46 | # Get credentials from environment variables 47 | GOOGLE_ADS_CREDENTIALS_PATH = os.environ.get("GOOGLE_ADS_CREDENTIALS_PATH") 48 | GOOGLE_ADS_DEVELOPER_TOKEN = os.environ.get("GOOGLE_ADS_DEVELOPER_TOKEN") 49 | GOOGLE_ADS_LOGIN_CUSTOMER_ID = os.environ.get("GOOGLE_ADS_LOGIN_CUSTOMER_ID", "") 50 | GOOGLE_ADS_AUTH_TYPE = os.environ.get("GOOGLE_ADS_AUTH_TYPE", "oauth") # oauth or service_account 51 | 52 | def format_customer_id(customer_id: str) -> str: 53 | """Format customer ID to ensure it's 10 digits without dashes.""" 54 | # Convert to string if passed as integer or another type 55 | customer_id = str(customer_id) 56 | 57 | # Remove any quotes surrounding the customer_id (both escaped and unescaped) 58 | customer_id = customer_id.replace('\"', '').replace('"', '') 59 | 60 | # Remove any non-digit characters (including dashes, braces, etc.) 61 | customer_id = ''.join(char for char in customer_id if char.isdigit()) 62 | 63 | # Ensure it's 10 digits with leading zeros if needed 64 | return customer_id.zfill(10) 65 | 66 | def get_credentials(): 67 | """ 68 | Get and refresh OAuth credentials or service account credentials based on the auth type. 69 | 70 | This function supports two authentication methods: 71 | 1. OAuth 2.0 (User Authentication) - For individual users or desktop applications 72 | 2. Service Account (Server-to-Server Authentication) - For automated systems 73 | 74 | Returns: 75 | Valid credentials object to use with Google Ads API 76 | """ 77 | if not GOOGLE_ADS_CREDENTIALS_PATH: 78 | raise ValueError("GOOGLE_ADS_CREDENTIALS_PATH environment variable not set") 79 | 80 | auth_type = GOOGLE_ADS_AUTH_TYPE.lower() 81 | logger.info(f"Using authentication type: {auth_type}") 82 | 83 | # Service Account authentication 84 | if auth_type == "service_account": 85 | try: 86 | return get_service_account_credentials() 87 | except Exception as e: 88 | logger.error(f"Error with service account authentication: {str(e)}") 89 | raise 90 | 91 | # OAuth 2.0 authentication (default) 92 | return get_oauth_credentials() 93 | 94 | def get_service_account_credentials(): 95 | """Get credentials using a service account key file.""" 96 | logger.info(f"Loading service account credentials from {GOOGLE_ADS_CREDENTIALS_PATH}") 97 | 98 | if not os.path.exists(GOOGLE_ADS_CREDENTIALS_PATH): 99 | raise FileNotFoundError(f"Service account key file not found at {GOOGLE_ADS_CREDENTIALS_PATH}") 100 | 101 | try: 102 | credentials = service_account.Credentials.from_service_account_file( 103 | GOOGLE_ADS_CREDENTIALS_PATH, 104 | scopes=SCOPES 105 | ) 106 | 107 | # Check if impersonation is required 108 | impersonation_email = os.environ.get("GOOGLE_ADS_IMPERSONATION_EMAIL") 109 | if impersonation_email: 110 | logger.info(f"Impersonating user: {impersonation_email}") 111 | credentials = credentials.with_subject(impersonation_email) 112 | 113 | return credentials 114 | 115 | except Exception as e: 116 | logger.error(f"Error loading service account credentials: {str(e)}") 117 | raise 118 | 119 | def get_oauth_credentials(): 120 | """Get and refresh OAuth user credentials.""" 121 | creds = None 122 | client_config = None 123 | 124 | # Path to store the refreshed token 125 | token_path = GOOGLE_ADS_CREDENTIALS_PATH 126 | if os.path.exists(token_path) and not os.path.basename(token_path).endswith('.json'): 127 | # If it's not explicitly a .json file, append a default name 128 | token_dir = os.path.dirname(token_path) 129 | token_path = os.path.join(token_dir, 'google_ads_token.json') 130 | 131 | # Check if token file exists and load credentials 132 | if os.path.exists(token_path): 133 | try: 134 | logger.info(f"Loading OAuth credentials from {token_path}") 135 | with open(token_path, 'r') as f: 136 | creds_data = json.load(f) 137 | # Check if this is a client config or saved credentials 138 | if "installed" in creds_data or "web" in creds_data: 139 | client_config = creds_data 140 | logger.info("Found OAuth client configuration") 141 | else: 142 | logger.info("Found existing OAuth token") 143 | creds = Credentials.from_authorized_user_info(creds_data, SCOPES) 144 | except json.JSONDecodeError: 145 | logger.warning(f"Invalid JSON in token file: {token_path}") 146 | creds = None 147 | except Exception as e: 148 | logger.warning(f"Error loading credentials: {str(e)}") 149 | creds = None 150 | 151 | # If credentials don't exist or are invalid, get new ones 152 | if not creds or not creds.valid: 153 | if creds and creds.expired and creds.refresh_token: 154 | try: 155 | logger.info("Refreshing expired token") 156 | creds.refresh(Request()) 157 | logger.info("Token successfully refreshed") 158 | except RefreshError as e: 159 | logger.warning(f"Error refreshing token: {str(e)}, will try to get new token") 160 | creds = None 161 | except Exception as e: 162 | logger.error(f"Unexpected error refreshing token: {str(e)}") 163 | raise 164 | 165 | # If we need new credentials 166 | if not creds: 167 | # If no client_config is defined yet, create one from environment variables 168 | if not client_config: 169 | logger.info("Creating OAuth client config from environment variables") 170 | client_id = os.environ.get("GOOGLE_ADS_CLIENT_ID") 171 | client_secret = os.environ.get("GOOGLE_ADS_CLIENT_SECRET") 172 | 173 | if not client_id or not client_secret: 174 | raise ValueError("GOOGLE_ADS_CLIENT_ID and GOOGLE_ADS_CLIENT_SECRET must be set if no client config file exists") 175 | 176 | client_config = { 177 | "installed": { 178 | "client_id": client_id, 179 | "client_secret": client_secret, 180 | "auth_uri": "https://accounts.google.com/o/oauth2/auth", 181 | "token_uri": "https://oauth2.googleapis.com/token", 182 | "redirect_uris": ["urn:ietf:wg:oauth:2.0:oob", "http://localhost"] 183 | } 184 | } 185 | 186 | # Run the OAuth flow 187 | logger.info("Starting OAuth authentication flow") 188 | flow = InstalledAppFlow.from_client_config(client_config, SCOPES) 189 | creds = flow.run_local_server(port=0) 190 | logger.info("OAuth flow completed successfully") 191 | 192 | # Save the refreshed/new credentials 193 | try: 194 | logger.info(f"Saving credentials to {token_path}") 195 | # Ensure directory exists 196 | os.makedirs(os.path.dirname(token_path), exist_ok=True) 197 | with open(token_path, 'w') as f: 198 | f.write(creds.to_json()) 199 | except Exception as e: 200 | logger.warning(f"Could not save credentials: {str(e)}") 201 | 202 | return creds 203 | 204 | def get_headers(creds): 205 | """Get headers for Google Ads API requests.""" 206 | if not GOOGLE_ADS_DEVELOPER_TOKEN: 207 | raise ValueError("GOOGLE_ADS_DEVELOPER_TOKEN environment variable not set") 208 | 209 | # Handle different credential types 210 | if isinstance(creds, service_account.Credentials): 211 | # For service account, we need to get a new bearer token 212 | auth_req = Request() 213 | creds.refresh(auth_req) 214 | token = creds.token 215 | else: 216 | # For OAuth credentials, check if token needs refresh 217 | if not creds.valid: 218 | if creds.expired and creds.refresh_token: 219 | try: 220 | logger.info("Refreshing expired OAuth token in get_headers") 221 | creds.refresh(Request()) 222 | logger.info("Token successfully refreshed in get_headers") 223 | except RefreshError as e: 224 | logger.error(f"Error refreshing token in get_headers: {str(e)}") 225 | raise ValueError(f"Failed to refresh OAuth token: {str(e)}") 226 | except Exception as e: 227 | logger.error(f"Unexpected error refreshing token in get_headers: {str(e)}") 228 | raise 229 | else: 230 | raise ValueError("OAuth credentials are invalid and cannot be refreshed") 231 | 232 | token = creds.token 233 | 234 | headers = { 235 | 'Authorization': f'Bearer {token}', 236 | 'developer-token': GOOGLE_ADS_DEVELOPER_TOKEN, 237 | 'content-type': 'application/json' 238 | } 239 | 240 | if GOOGLE_ADS_LOGIN_CUSTOMER_ID: 241 | headers['login-customer-id'] = format_customer_id(GOOGLE_ADS_LOGIN_CUSTOMER_ID) 242 | 243 | return headers 244 | 245 | @mcp.tool() 246 | async def list_accounts() -> str: 247 | """ 248 | Lists all accessible Google Ads accounts. 249 | 250 | This is typically the first command you should run to identify which accounts 251 | you have access to. The returned account IDs can be used in subsequent commands. 252 | 253 | Returns: 254 | A formatted list of all Google Ads accounts accessible with your credentials 255 | """ 256 | try: 257 | creds = get_credentials() 258 | headers = get_headers(creds) 259 | 260 | url = f"https://googleads.googleapis.com/{API_VERSION}/customers:listAccessibleCustomers" 261 | response = requests.get(url, headers=headers) 262 | 263 | if response.status_code != 200: 264 | return f"Error accessing accounts: {response.text}" 265 | 266 | customers = response.json() 267 | if not customers.get('resourceNames'): 268 | return "No accessible accounts found." 269 | 270 | # Format the results 271 | result_lines = ["Accessible Google Ads Accounts:"] 272 | result_lines.append("-" * 50) 273 | 274 | for resource_name in customers['resourceNames']: 275 | customer_id = resource_name.split('/')[-1] 276 | formatted_id = format_customer_id(customer_id) 277 | result_lines.append(f"Account ID: {formatted_id}") 278 | 279 | return "\n".join(result_lines) 280 | 281 | except Exception as e: 282 | return f"Error listing accounts: {str(e)}" 283 | 284 | @mcp.tool() 285 | async def execute_gaql_query( 286 | customer_id: str = Field(description="Google Ads customer ID (10 digits, no dashes). Example: '9873186703'"), 287 | query: str = Field(description="Valid GAQL query string following Google Ads Query Language syntax") 288 | ) -> str: 289 | """ 290 | Execute a custom GAQL (Google Ads Query Language) query. 291 | 292 | This tool allows you to run any valid GAQL query against the Google Ads API. 293 | 294 | Args: 295 | customer_id: The Google Ads customer ID as a string (10 digits, no dashes) 296 | query: The GAQL query to execute (must follow GAQL syntax) 297 | 298 | Returns: 299 | Formatted query results or error message 300 | 301 | Example: 302 | customer_id: "1234567890" 303 | query: "SELECT campaign.id, campaign.name FROM campaign LIMIT 10" 304 | """ 305 | try: 306 | creds = get_credentials() 307 | headers = get_headers(creds) 308 | 309 | formatted_customer_id = format_customer_id(customer_id) 310 | url = f"https://googleads.googleapis.com/{API_VERSION}/customers/{formatted_customer_id}/googleAds:search" 311 | 312 | payload = {"query": query} 313 | response = requests.post(url, headers=headers, json=payload) 314 | 315 | if response.status_code != 200: 316 | return f"Error executing query: {response.text}" 317 | 318 | results = response.json() 319 | if not results.get('results'): 320 | return "No results found for the query." 321 | 322 | # Format the results as a table 323 | result_lines = [f"Query Results for Account {formatted_customer_id}:"] 324 | result_lines.append("-" * 80) 325 | 326 | # Get field names from the first result 327 | fields = [] 328 | first_result = results['results'][0] 329 | for key in first_result: 330 | if isinstance(first_result[key], dict): 331 | for subkey in first_result[key]: 332 | fields.append(f"{key}.{subkey}") 333 | else: 334 | fields.append(key) 335 | 336 | # Add header 337 | result_lines.append(" | ".join(fields)) 338 | result_lines.append("-" * 80) 339 | 340 | # Add data rows 341 | for result in results['results']: 342 | row_data = [] 343 | for field in fields: 344 | if "." in field: 345 | parent, child = field.split(".") 346 | value = str(result.get(parent, {}).get(child, "")) 347 | else: 348 | value = str(result.get(field, "")) 349 | row_data.append(value) 350 | result_lines.append(" | ".join(row_data)) 351 | 352 | return "\n".join(result_lines) 353 | 354 | except Exception as e: 355 | return f"Error executing GAQL query: {str(e)}" 356 | 357 | @mcp.tool() 358 | async def get_campaign_performance( 359 | customer_id: str = Field(description="Google Ads customer ID (10 digits, no dashes). Example: '9873186703'"), 360 | days: int = Field(default=30, description="Number of days to look back (7, 30, 90, etc.)") 361 | ) -> str: 362 | """ 363 | Get campaign performance metrics for the specified time period. 364 | 365 | RECOMMENDED WORKFLOW: 366 | 1. First run list_accounts() to get available account IDs 367 | 2. Then run get_account_currency() to see what currency the account uses 368 | 3. Finally run this command to get campaign performance 369 | 370 | Args: 371 | customer_id: The Google Ads customer ID as a string (10 digits, no dashes) 372 | days: Number of days to look back (default: 30) 373 | 374 | Returns: 375 | Formatted table of campaign performance data 376 | 377 | Note: 378 | Cost values are in micros (millionths) of the account currency 379 | (e.g., 1000000 = 1 USD in a USD account) 380 | 381 | Example: 382 | customer_id: "1234567890" 383 | days: 14 384 | """ 385 | query = f""" 386 | SELECT 387 | campaign.id, 388 | campaign.name, 389 | campaign.status, 390 | metrics.impressions, 391 | metrics.clicks, 392 | metrics.cost_micros, 393 | metrics.conversions, 394 | metrics.average_cpc 395 | FROM campaign 396 | WHERE segments.date DURING LAST_{days}_DAYS 397 | ORDER BY metrics.cost_micros DESC 398 | LIMIT 50 399 | """ 400 | 401 | return await execute_gaql_query(customer_id, query) 402 | 403 | @mcp.tool() 404 | async def get_ad_performance( 405 | customer_id: str = Field(description="Google Ads customer ID (10 digits, no dashes). Example: '9873186703'"), 406 | days: int = Field(default=30, description="Number of days to look back (7, 30, 90, etc.)") 407 | ) -> str: 408 | """ 409 | Get ad performance metrics for the specified time period. 410 | 411 | RECOMMENDED WORKFLOW: 412 | 1. First run list_accounts() to get available account IDs 413 | 2. Then run get_account_currency() to see what currency the account uses 414 | 3. Finally run this command to get ad performance 415 | 416 | Args: 417 | customer_id: The Google Ads customer ID as a string (10 digits, no dashes) 418 | days: Number of days to look back (default: 30) 419 | 420 | Returns: 421 | Formatted table of ad performance data 422 | 423 | Note: 424 | Cost values are in micros (millionths) of the account currency 425 | (e.g., 1000000 = 1 USD in a USD account) 426 | 427 | Example: 428 | customer_id: "1234567890" 429 | days: 14 430 | """ 431 | query = f""" 432 | SELECT 433 | ad_group_ad.ad.id, 434 | ad_group_ad.ad.name, 435 | ad_group_ad.status, 436 | campaign.name, 437 | ad_group.name, 438 | metrics.impressions, 439 | metrics.clicks, 440 | metrics.cost_micros, 441 | metrics.conversions 442 | FROM ad_group_ad 443 | WHERE segments.date DURING LAST_{days}_DAYS 444 | ORDER BY metrics.impressions DESC 445 | LIMIT 50 446 | """ 447 | 448 | return await execute_gaql_query(customer_id, query) 449 | 450 | @mcp.tool() 451 | async def run_gaql( 452 | customer_id: str = Field(description="Google Ads customer ID (10 digits, no dashes). Example: '9873186703'"), 453 | query: str = Field(description="Valid GAQL query string following Google Ads Query Language syntax"), 454 | format: str = Field(default="table", description="Output format: 'table', 'json', or 'csv'") 455 | ) -> str: 456 | """ 457 | Execute any arbitrary GAQL (Google Ads Query Language) query with custom formatting options. 458 | 459 | This is the most powerful tool for custom Google Ads data queries. 460 | 461 | Args: 462 | customer_id: The Google Ads customer ID as a string (10 digits, no dashes) 463 | query: The GAQL query to execute (any valid GAQL query) 464 | format: Output format ("table", "json", or "csv") 465 | 466 | Returns: 467 | Query results in the requested format 468 | 469 | EXAMPLE QUERIES: 470 | 471 | 1. Basic campaign metrics: 472 | SELECT 473 | campaign.name, 474 | metrics.clicks, 475 | metrics.impressions, 476 | metrics.cost_micros 477 | FROM campaign 478 | WHERE segments.date DURING LAST_7_DAYS 479 | 480 | 2. Ad group performance: 481 | SELECT 482 | ad_group.name, 483 | metrics.conversions, 484 | metrics.cost_micros, 485 | campaign.name 486 | FROM ad_group 487 | WHERE metrics.clicks > 100 488 | 489 | 3. Keyword analysis: 490 | SELECT 491 | keyword.text, 492 | metrics.average_position, 493 | metrics.ctr 494 | FROM keyword_view 495 | ORDER BY metrics.impressions DESC 496 | 497 | 4. Get conversion data: 498 | SELECT 499 | campaign.name, 500 | metrics.conversions, 501 | metrics.conversions_value, 502 | metrics.cost_micros 503 | FROM campaign 504 | WHERE segments.date DURING LAST_30_DAYS 505 | 506 | Note: 507 | Cost values are in micros (millionths) of the account currency 508 | (e.g., 1000000 = 1 USD in a USD account) 509 | """ 510 | try: 511 | creds = get_credentials() 512 | headers = get_headers(creds) 513 | 514 | formatted_customer_id = format_customer_id(customer_id) 515 | url = f"https://googleads.googleapis.com/{API_VERSION}/customers/{formatted_customer_id}/googleAds:search" 516 | 517 | payload = {"query": query} 518 | response = requests.post(url, headers=headers, json=payload) 519 | 520 | if response.status_code != 200: 521 | return f"Error executing query: {response.text}" 522 | 523 | results = response.json() 524 | if not results.get('results'): 525 | return "No results found for the query." 526 | 527 | if format.lower() == "json": 528 | return json.dumps(results, indent=2) 529 | 530 | elif format.lower() == "csv": 531 | # Get field names from the first result 532 | fields = [] 533 | first_result = results['results'][0] 534 | for key, value in first_result.items(): 535 | if isinstance(value, dict): 536 | for subkey in value: 537 | fields.append(f"{key}.{subkey}") 538 | else: 539 | fields.append(key) 540 | 541 | # Create CSV string 542 | csv_lines = [",".join(fields)] 543 | for result in results['results']: 544 | row_data = [] 545 | for field in fields: 546 | if "." in field: 547 | parent, child = field.split(".") 548 | value = str(result.get(parent, {}).get(child, "")).replace(",", ";") 549 | else: 550 | value = str(result.get(field, "")).replace(",", ";") 551 | row_data.append(value) 552 | csv_lines.append(",".join(row_data)) 553 | 554 | return "\n".join(csv_lines) 555 | 556 | else: # default table format 557 | result_lines = [f"Query Results for Account {formatted_customer_id}:"] 558 | result_lines.append("-" * 100) 559 | 560 | # Get field names and maximum widths 561 | fields = [] 562 | field_widths = {} 563 | first_result = results['results'][0] 564 | 565 | for key, value in first_result.items(): 566 | if isinstance(value, dict): 567 | for subkey in value: 568 | field = f"{key}.{subkey}" 569 | fields.append(field) 570 | field_widths[field] = len(field) 571 | else: 572 | fields.append(key) 573 | field_widths[key] = len(key) 574 | 575 | # Calculate maximum field widths 576 | for result in results['results']: 577 | for field in fields: 578 | if "." in field: 579 | parent, child = field.split(".") 580 | value = str(result.get(parent, {}).get(child, "")) 581 | else: 582 | value = str(result.get(field, "")) 583 | field_widths[field] = max(field_widths[field], len(value)) 584 | 585 | # Create formatted header 586 | header = " | ".join(f"{field:{field_widths[field]}}" for field in fields) 587 | result_lines.append(header) 588 | result_lines.append("-" * len(header)) 589 | 590 | # Add data rows 591 | for result in results['results']: 592 | row_data = [] 593 | for field in fields: 594 | if "." in field: 595 | parent, child = field.split(".") 596 | value = str(result.get(parent, {}).get(child, "")) 597 | else: 598 | value = str(result.get(field, "")) 599 | row_data.append(f"{value:{field_widths[field]}}") 600 | result_lines.append(" | ".join(row_data)) 601 | 602 | return "\n".join(result_lines) 603 | 604 | except Exception as e: 605 | return f"Error executing GAQL query: {str(e)}" 606 | 607 | @mcp.tool() 608 | async def get_ad_creatives( 609 | customer_id: str = Field(description="Google Ads customer ID (10 digits, no dashes). Example: '9873186703'") 610 | ) -> str: 611 | """ 612 | Get ad creative details including headlines, descriptions, and URLs. 613 | 614 | This tool retrieves the actual ad content (headlines, descriptions) 615 | for review and analysis. Great for creative audits. 616 | 617 | RECOMMENDED WORKFLOW: 618 | 1. First run list_accounts() to get available account IDs 619 | 2. Then run this command with the desired account ID 620 | 621 | Args: 622 | customer_id: The Google Ads customer ID as a string (10 digits, no dashes) 623 | 624 | Returns: 625 | Formatted list of ad creative details 626 | 627 | Example: 628 | customer_id: "1234567890" 629 | """ 630 | query = """ 631 | SELECT 632 | ad_group_ad.ad.id, 633 | ad_group_ad.ad.name, 634 | ad_group_ad.ad.type, 635 | ad_group_ad.ad.final_urls, 636 | ad_group_ad.status, 637 | ad_group_ad.ad.responsive_search_ad.headlines, 638 | ad_group_ad.ad.responsive_search_ad.descriptions, 639 | ad_group.name, 640 | campaign.name 641 | FROM ad_group_ad 642 | WHERE ad_group_ad.status != 'REMOVED' 643 | ORDER BY campaign.name, ad_group.name 644 | LIMIT 50 645 | """ 646 | 647 | try: 648 | creds = get_credentials() 649 | headers = get_headers(creds) 650 | 651 | formatted_customer_id = format_customer_id(customer_id) 652 | url = f"https://googleads.googleapis.com/{API_VERSION}/customers/{formatted_customer_id}/googleAds:search" 653 | 654 | payload = {"query": query} 655 | response = requests.post(url, headers=headers, json=payload) 656 | 657 | if response.status_code != 200: 658 | return f"Error retrieving ad creatives: {response.text}" 659 | 660 | results = response.json() 661 | if not results.get('results'): 662 | return "No ad creatives found for this customer ID." 663 | 664 | # Format the results in a readable way 665 | output_lines = [f"Ad Creatives for Customer ID {formatted_customer_id}:"] 666 | output_lines.append("=" * 80) 667 | 668 | for i, result in enumerate(results['results'], 1): 669 | ad = result.get('adGroupAd', {}).get('ad', {}) 670 | ad_group = result.get('adGroup', {}) 671 | campaign = result.get('campaign', {}) 672 | 673 | output_lines.append(f"\n{i}. Campaign: {campaign.get('name', 'N/A')}") 674 | output_lines.append(f" Ad Group: {ad_group.get('name', 'N/A')}") 675 | output_lines.append(f" Ad ID: {ad.get('id', 'N/A')}") 676 | output_lines.append(f" Ad Name: {ad.get('name', 'N/A')}") 677 | output_lines.append(f" Status: {result.get('adGroupAd', {}).get('status', 'N/A')}") 678 | output_lines.append(f" Type: {ad.get('type', 'N/A')}") 679 | 680 | # Handle Responsive Search Ads 681 | rsa = ad.get('responsiveSearchAd', {}) 682 | if rsa: 683 | if 'headlines' in rsa: 684 | output_lines.append(" Headlines:") 685 | for headline in rsa['headlines']: 686 | output_lines.append(f" - {headline.get('text', 'N/A')}") 687 | 688 | if 'descriptions' in rsa: 689 | output_lines.append(" Descriptions:") 690 | for desc in rsa['descriptions']: 691 | output_lines.append(f" - {desc.get('text', 'N/A')}") 692 | 693 | # Handle Final URLs 694 | final_urls = ad.get('finalUrls', []) 695 | if final_urls: 696 | output_lines.append(f" Final URLs: {', '.join(final_urls)}") 697 | 698 | output_lines.append("-" * 80) 699 | 700 | return "\n".join(output_lines) 701 | 702 | except Exception as e: 703 | return f"Error retrieving ad creatives: {str(e)}" 704 | 705 | @mcp.tool() 706 | async def get_account_currency( 707 | customer_id: str = Field(description="Google Ads customer ID (10 digits, no dashes). Example: '9873186703'") 708 | ) -> str: 709 | """ 710 | Retrieve the default currency code used by the Google Ads account. 711 | 712 | IMPORTANT: Run this first before analyzing cost data to understand which currency 713 | the account uses. Cost values are always displayed in the account's currency. 714 | 715 | Args: 716 | customer_id: The Google Ads customer ID as a string (10 digits, no dashes) 717 | 718 | Returns: 719 | The account's default currency code (e.g., 'USD', 'EUR', 'GBP') 720 | 721 | Example: 722 | customer_id: "1234567890" 723 | """ 724 | query = """ 725 | SELECT 726 | customer.id, 727 | customer.currency_code 728 | FROM customer 729 | LIMIT 1 730 | """ 731 | 732 | try: 733 | creds = get_credentials() 734 | 735 | # Force refresh if needed 736 | if not creds.valid: 737 | logger.info("Credentials not valid, attempting refresh...") 738 | if hasattr(creds, 'refresh_token') and creds.refresh_token: 739 | creds.refresh(Request()) 740 | logger.info("Credentials refreshed successfully") 741 | else: 742 | raise ValueError("Invalid credentials and no refresh token available") 743 | 744 | headers = get_headers(creds) 745 | 746 | formatted_customer_id = format_customer_id(customer_id) 747 | url = f"https://googleads.googleapis.com/{API_VERSION}/customers/{formatted_customer_id}/googleAds:search" 748 | 749 | payload = {"query": query} 750 | response = requests.post(url, headers=headers, json=payload) 751 | 752 | if response.status_code != 200: 753 | return f"Error retrieving account currency: {response.text}" 754 | 755 | results = response.json() 756 | if not results.get('results'): 757 | return "No account information found for this customer ID." 758 | 759 | # Extract the currency code from the results 760 | customer = results['results'][0].get('customer', {}) 761 | currency_code = customer.get('currencyCode', 'Not specified') 762 | 763 | return f"Account {formatted_customer_id} uses currency: {currency_code}" 764 | 765 | except Exception as e: 766 | logger.error(f"Error retrieving account currency: {str(e)}") 767 | return f"Error retrieving account currency: {str(e)}" 768 | 769 | @mcp.resource("gaql://reference") 770 | def gaql_reference() -> str: 771 | """Google Ads Query Language (GAQL) reference documentation.""" 772 | return """ 773 | # Google Ads Query Language (GAQL) Reference 774 | 775 | GAQL is similar to SQL but with specific syntax for Google Ads. Here's a quick reference: 776 | 777 | ## Basic Query Structure 778 | ``` 779 | SELECT field1, field2, ... 780 | FROM resource_type 781 | WHERE condition 782 | ORDER BY field [ASC|DESC] 783 | LIMIT n 784 | ``` 785 | 786 | ## Common Field Types 787 | 788 | ### Resource Fields 789 | - campaign.id, campaign.name, campaign.status 790 | - ad_group.id, ad_group.name, ad_group.status 791 | - ad_group_ad.ad.id, ad_group_ad.ad.final_urls 792 | - keyword.text, keyword.match_type 793 | 794 | ### Metric Fields 795 | - metrics.impressions 796 | - metrics.clicks 797 | - metrics.cost_micros 798 | - metrics.conversions 799 | - metrics.ctr 800 | - metrics.average_cpc 801 | 802 | ### Segment Fields 803 | - segments.date 804 | - segments.device 805 | - segments.day_of_week 806 | 807 | ## Common WHERE Clauses 808 | 809 | ### Date Ranges 810 | - WHERE segments.date DURING LAST_7_DAYS 811 | - WHERE segments.date DURING LAST_30_DAYS 812 | - WHERE segments.date BETWEEN '2023-01-01' AND '2023-01-31' 813 | 814 | ### Filtering 815 | - WHERE campaign.status = 'ENABLED' 816 | - WHERE metrics.clicks > 100 817 | - WHERE campaign.name LIKE '%Brand%' 818 | 819 | ## Tips 820 | - Always check account currency before analyzing cost data 821 | - Cost values are in micros (millionths): 1000000 = 1 unit of currency 822 | - Use LIMIT to avoid large result sets 823 | """ 824 | 825 | @mcp.prompt("google_ads_workflow") 826 | def google_ads_workflow() -> str: 827 | """Provides guidance on the recommended workflow for using Google Ads tools.""" 828 | return """ 829 | I'll help you analyze your Google Ads account data. Here's the recommended workflow: 830 | 831 | 1. First, let's list all the accounts you have access to: 832 | - Run the `list_accounts()` tool to get available account IDs 833 | 834 | 2. Before analyzing cost data, let's check which currency the account uses: 835 | - Run `get_account_currency(customer_id="ACCOUNT_ID")` with your selected account 836 | 837 | 3. Now we can explore the account data: 838 | - For campaign performance: `get_campaign_performance(customer_id="ACCOUNT_ID", days=30)` 839 | - For ad performance: `get_ad_performance(customer_id="ACCOUNT_ID", days=30)` 840 | - For ad creative review: `get_ad_creatives(customer_id="ACCOUNT_ID")` 841 | 842 | 4. For custom queries, use the GAQL query tool: 843 | - `run_gaql(customer_id="ACCOUNT_ID", query="YOUR_QUERY", format="table")` 844 | 845 | 5. Let me know if you have specific questions about: 846 | - Campaign performance 847 | - Ad performance 848 | - Keywords 849 | - Budgets 850 | - Conversions 851 | 852 | Important: Always provide the customer_id as a string. 853 | For example: customer_id="1234567890" 854 | """ 855 | 856 | @mcp.prompt("gaql_help") 857 | def gaql_help() -> str: 858 | """Provides assistance for writing GAQL queries.""" 859 | return """ 860 | I'll help you write a Google Ads Query Language (GAQL) query. Here are some examples to get you started: 861 | 862 | ## Get campaign performance last 30 days 863 | ``` 864 | SELECT 865 | campaign.id, 866 | campaign.name, 867 | campaign.status, 868 | metrics.impressions, 869 | metrics.clicks, 870 | metrics.cost_micros, 871 | metrics.conversions 872 | FROM campaign 873 | WHERE segments.date DURING LAST_30_DAYS 874 | ORDER BY metrics.cost_micros DESC 875 | ``` 876 | 877 | ## Get keyword performance 878 | ``` 879 | SELECT 880 | keyword.text, 881 | keyword.match_type, 882 | metrics.impressions, 883 | metrics.clicks, 884 | metrics.cost_micros, 885 | metrics.conversions 886 | FROM keyword_view 887 | WHERE segments.date DURING LAST_30_DAYS 888 | ORDER BY metrics.clicks DESC 889 | ``` 890 | 891 | ## Get ads with poor performance 892 | ``` 893 | SELECT 894 | ad_group_ad.ad.id, 895 | ad_group_ad.ad.name, 896 | campaign.name, 897 | ad_group.name, 898 | metrics.impressions, 899 | metrics.clicks, 900 | metrics.conversions 901 | FROM ad_group_ad 902 | WHERE 903 | segments.date DURING LAST_30_DAYS 904 | AND metrics.impressions > 1000 905 | AND metrics.ctr < 0.01 906 | ORDER BY metrics.impressions DESC 907 | ``` 908 | 909 | Once you've chosen a query, use it with: 910 | ``` 911 | run_gaql(customer_id="YOUR_ACCOUNT_ID", query="YOUR_QUERY_HERE") 912 | ``` 913 | 914 | Remember: 915 | - Always provide the customer_id as a string 916 | - Cost values are in micros (1,000,000 = 1 unit of currency) 917 | - Use LIMIT to avoid large result sets 918 | - Check the account currency before analyzing cost data 919 | """ 920 | 921 | @mcp.tool() 922 | async def get_image_assets( 923 | customer_id: str = Field(description="Google Ads customer ID (10 digits, no dashes). Example: '9873186703'"), 924 | limit: int = Field(default=50, description="Maximum number of image assets to return") 925 | ) -> str: 926 | """ 927 | Retrieve all image assets in the account including their full-size URLs. 928 | 929 | This tool allows you to get details about image assets used in your Google Ads account, 930 | including the URLs to download the full-size images for further processing or analysis. 931 | 932 | RECOMMENDED WORKFLOW: 933 | 1. First run list_accounts() to get available account IDs 934 | 2. Then run this command with the desired account ID 935 | 936 | Args: 937 | customer_id: The Google Ads customer ID as a string (10 digits, no dashes) 938 | limit: Maximum number of image assets to return (default: 50) 939 | 940 | Returns: 941 | Formatted list of image assets with their download URLs 942 | 943 | Example: 944 | customer_id: "1234567890" 945 | limit: 100 946 | """ 947 | query = f""" 948 | SELECT 949 | asset.id, 950 | asset.name, 951 | asset.type, 952 | asset.image_asset.full_size.url, 953 | asset.image_asset.full_size.height_pixels, 954 | asset.image_asset.full_size.width_pixels, 955 | asset.image_asset.file_size 956 | FROM 957 | asset 958 | WHERE 959 | asset.type = 'IMAGE' 960 | LIMIT {limit} 961 | """ 962 | 963 | try: 964 | creds = get_credentials() 965 | headers = get_headers(creds) 966 | 967 | formatted_customer_id = format_customer_id(customer_id) 968 | url = f"https://googleads.googleapis.com/{API_VERSION}/customers/{formatted_customer_id}/googleAds:search" 969 | 970 | payload = {"query": query} 971 | response = requests.post(url, headers=headers, json=payload) 972 | 973 | if response.status_code != 200: 974 | return f"Error retrieving image assets: {response.text}" 975 | 976 | results = response.json() 977 | if not results.get('results'): 978 | return "No image assets found for this customer ID." 979 | 980 | # Format the results in a readable way 981 | output_lines = [f"Image Assets for Customer ID {formatted_customer_id}:"] 982 | output_lines.append("=" * 80) 983 | 984 | for i, result in enumerate(results['results'], 1): 985 | asset = result.get('asset', {}) 986 | image_asset = asset.get('imageAsset', {}) 987 | full_size = image_asset.get('fullSize', {}) 988 | 989 | output_lines.append(f"\n{i}. Asset ID: {asset.get('id', 'N/A')}") 990 | output_lines.append(f" Name: {asset.get('name', 'N/A')}") 991 | 992 | if full_size: 993 | output_lines.append(f" Image URL: {full_size.get('url', 'N/A')}") 994 | output_lines.append(f" Dimensions: {full_size.get('widthPixels', 'N/A')} x {full_size.get('heightPixels', 'N/A')} px") 995 | 996 | file_size = image_asset.get('fileSize', 'N/A') 997 | if file_size != 'N/A': 998 | # Convert to KB for readability 999 | file_size_kb = int(file_size) / 1024 1000 | output_lines.append(f" File Size: {file_size_kb:.2f} KB") 1001 | 1002 | output_lines.append("-" * 80) 1003 | 1004 | return "\n".join(output_lines) 1005 | 1006 | except Exception as e: 1007 | return f"Error retrieving image assets: {str(e)}" 1008 | 1009 | @mcp.tool() 1010 | async def download_image_asset( 1011 | customer_id: str = Field(description="Google Ads customer ID (10 digits, no dashes). Example: '9873186703'"), 1012 | asset_id: str = Field(description="The ID of the image asset to download"), 1013 | output_dir: str = Field(default="./ad_images", description="Directory to save the downloaded image") 1014 | ) -> str: 1015 | """ 1016 | Download a specific image asset from a Google Ads account. 1017 | 1018 | This tool allows you to download the full-size version of an image asset 1019 | for further processing, analysis, or backup. 1020 | 1021 | RECOMMENDED WORKFLOW: 1022 | 1. First run list_accounts() to get available account IDs 1023 | 2. Then run get_image_assets() to get available image asset IDs 1024 | 3. Finally use this command to download specific images 1025 | 1026 | Args: 1027 | customer_id: The Google Ads customer ID as a string (10 digits, no dashes) 1028 | asset_id: The ID of the image asset to download 1029 | output_dir: Directory where the image should be saved (default: ./ad_images) 1030 | 1031 | Returns: 1032 | Status message indicating success or failure of the download 1033 | 1034 | Example: 1035 | customer_id: "1234567890" 1036 | asset_id: "12345" 1037 | output_dir: "./my_ad_images" 1038 | """ 1039 | query = f""" 1040 | SELECT 1041 | asset.id, 1042 | asset.name, 1043 | asset.image_asset.full_size.url 1044 | FROM 1045 | asset 1046 | WHERE 1047 | asset.type = 'IMAGE' 1048 | AND asset.id = {asset_id} 1049 | LIMIT 1 1050 | """ 1051 | 1052 | try: 1053 | creds = get_credentials() 1054 | headers = get_headers(creds) 1055 | 1056 | formatted_customer_id = format_customer_id(customer_id) 1057 | url = f"https://googleads.googleapis.com/{API_VERSION}/customers/{formatted_customer_id}/googleAds:search" 1058 | 1059 | payload = {"query": query} 1060 | response = requests.post(url, headers=headers, json=payload) 1061 | 1062 | if response.status_code != 200: 1063 | return f"Error retrieving image asset: {response.text}" 1064 | 1065 | results = response.json() 1066 | if not results.get('results'): 1067 | return f"No image asset found with ID {asset_id}" 1068 | 1069 | # Extract the image URL 1070 | asset = results['results'][0].get('asset', {}) 1071 | image_url = asset.get('imageAsset', {}).get('fullSize', {}).get('url') 1072 | asset_name = asset.get('name', f"image_{asset_id}") 1073 | 1074 | if not image_url: 1075 | return f"No download URL found for image asset ID {asset_id}" 1076 | 1077 | # Validate and sanitize the output directory to prevent path traversal 1078 | try: 1079 | # Get the base directory (current working directory) 1080 | base_dir = Path.cwd() 1081 | # Resolve the output directory to an absolute path 1082 | resolved_output_dir = Path(output_dir).resolve() 1083 | 1084 | # Ensure the resolved path is within or under the current working directory 1085 | # This prevents path traversal attacks like "../../../etc" 1086 | try: 1087 | resolved_output_dir.relative_to(base_dir) 1088 | except ValueError: 1089 | # If the path is not relative to base_dir, use the default safe directory 1090 | resolved_output_dir = base_dir / "ad_images" 1091 | logger.warning(f"Invalid output directory '{output_dir}' - using default './ad_images'") 1092 | 1093 | # Create output directory if it doesn't exist 1094 | resolved_output_dir.mkdir(parents=True, exist_ok=True) 1095 | 1096 | except Exception as e: 1097 | return f"Error creating output directory: {str(e)}" 1098 | 1099 | # Download the image 1100 | image_response = requests.get(image_url) 1101 | if image_response.status_code != 200: 1102 | return f"Failed to download image: HTTP {image_response.status_code}" 1103 | 1104 | # Clean the filename to be safe for filesystem 1105 | safe_name = ''.join(c for c in asset_name if c.isalnum() or c in ' ._-') 1106 | filename = f"{asset_id}_{safe_name}.jpg" 1107 | file_path = resolved_output_dir / filename 1108 | 1109 | # Save the image 1110 | with open(file_path, 'wb') as f: 1111 | f.write(image_response.content) 1112 | 1113 | return f"Successfully downloaded image asset {asset_id} to {file_path}" 1114 | 1115 | except Exception as e: 1116 | return f"Error downloading image asset: {str(e)}" 1117 | 1118 | @mcp.tool() 1119 | async def get_asset_usage( 1120 | customer_id: str = Field(description="Google Ads customer ID (10 digits, no dashes). Example: '9873186703'"), 1121 | asset_id: str = Field(default=None, description="Optional: specific asset ID to look up (leave empty to get all image assets)"), 1122 | asset_type: str = Field(default="IMAGE", description="Asset type to search for ('IMAGE', 'TEXT', 'VIDEO', etc.)") 1123 | ) -> str: 1124 | """ 1125 | Find where specific assets are being used in campaigns, ad groups, and ads. 1126 | 1127 | This tool helps you analyze how assets are linked to campaigns and ads across your account, 1128 | which is useful for creative analysis and optimization. 1129 | 1130 | RECOMMENDED WORKFLOW: 1131 | 1. First run list_accounts() to get available account IDs 1132 | 2. Run get_image_assets() to see available assets 1133 | 3. Use this command to see where specific assets are used 1134 | 1135 | Args: 1136 | customer_id: The Google Ads customer ID as a string (10 digits, no dashes) 1137 | asset_id: Optional specific asset ID to look up (leave empty to get all assets of the specified type) 1138 | asset_type: Type of asset to search for (default: 'IMAGE') 1139 | 1140 | Returns: 1141 | Formatted report showing where assets are used in the account 1142 | 1143 | Example: 1144 | customer_id: "1234567890" 1145 | asset_id: "12345" 1146 | asset_type: "IMAGE" 1147 | """ 1148 | # Build the query based on whether a specific asset ID was provided 1149 | where_clause = f"asset.type = '{asset_type}'" 1150 | if asset_id: 1151 | where_clause += f" AND asset.id = {asset_id}" 1152 | 1153 | # First get the assets themselves 1154 | assets_query = f""" 1155 | SELECT 1156 | asset.id, 1157 | asset.name, 1158 | asset.type 1159 | FROM 1160 | asset 1161 | WHERE 1162 | {where_clause} 1163 | LIMIT 100 1164 | """ 1165 | 1166 | # Then get the associations between assets and campaigns/ad groups 1167 | # Try using campaign_asset instead of asset_link 1168 | associations_query = f""" 1169 | SELECT 1170 | campaign.id, 1171 | campaign.name, 1172 | asset.id, 1173 | asset.name, 1174 | asset.type 1175 | FROM 1176 | campaign_asset 1177 | WHERE 1178 | {where_clause} 1179 | LIMIT 500 1180 | """ 1181 | 1182 | # Also try ad_group_asset for ad group level information 1183 | ad_group_query = f""" 1184 | SELECT 1185 | ad_group.id, 1186 | ad_group.name, 1187 | asset.id, 1188 | asset.name, 1189 | asset.type 1190 | FROM 1191 | ad_group_asset 1192 | WHERE 1193 | {where_clause} 1194 | LIMIT 500 1195 | """ 1196 | 1197 | try: 1198 | creds = get_credentials() 1199 | headers = get_headers(creds) 1200 | 1201 | formatted_customer_id = format_customer_id(customer_id) 1202 | 1203 | # First get the assets 1204 | url = f"https://googleads.googleapis.com/{API_VERSION}/customers/{formatted_customer_id}/googleAds:search" 1205 | payload = {"query": assets_query} 1206 | assets_response = requests.post(url, headers=headers, json=payload) 1207 | 1208 | if assets_response.status_code != 200: 1209 | return f"Error retrieving assets: {assets_response.text}" 1210 | 1211 | assets_results = assets_response.json() 1212 | if not assets_results.get('results'): 1213 | return f"No {asset_type} assets found for this customer ID." 1214 | 1215 | # Now get the associations 1216 | payload = {"query": associations_query} 1217 | assoc_response = requests.post(url, headers=headers, json=payload) 1218 | 1219 | if assoc_response.status_code != 200: 1220 | return f"Error retrieving asset associations: {assoc_response.text}" 1221 | 1222 | assoc_results = assoc_response.json() 1223 | 1224 | # Format the results in a readable way 1225 | output_lines = [f"Asset Usage for Customer ID {formatted_customer_id}:"] 1226 | output_lines.append("=" * 80) 1227 | 1228 | # Create a dictionary to organize asset usage by asset ID 1229 | asset_usage = {} 1230 | 1231 | # Initialize the asset usage dictionary with basic asset info 1232 | for result in assets_results.get('results', []): 1233 | asset = result.get('asset', {}) 1234 | asset_id = asset.get('id') 1235 | if asset_id: 1236 | asset_usage[asset_id] = { 1237 | 'name': asset.get('name', 'Unnamed asset'), 1238 | 'type': asset.get('type', 'Unknown'), 1239 | 'usage': [] 1240 | } 1241 | 1242 | # Add usage information from the associations 1243 | for result in assoc_results.get('results', []): 1244 | asset = result.get('asset', {}) 1245 | asset_id = asset.get('id') 1246 | 1247 | if asset_id and asset_id in asset_usage: 1248 | campaign = result.get('campaign', {}) 1249 | ad_group = result.get('adGroup', {}) 1250 | ad = result.get('adGroupAd', {}).get('ad', {}) if 'adGroupAd' in result else {} 1251 | asset_link = result.get('assetLink', {}) 1252 | 1253 | usage_info = { 1254 | 'campaign_id': campaign.get('id', 'N/A'), 1255 | 'campaign_name': campaign.get('name', 'N/A'), 1256 | 'ad_group_id': ad_group.get('id', 'N/A'), 1257 | 'ad_group_name': ad_group.get('name', 'N/A'), 1258 | 'ad_id': ad.get('id', 'N/A') if ad else 'N/A', 1259 | 'ad_name': ad.get('name', 'N/A') if ad else 'N/A' 1260 | } 1261 | 1262 | asset_usage[asset_id]['usage'].append(usage_info) 1263 | 1264 | # Format the output 1265 | for asset_id, info in asset_usage.items(): 1266 | output_lines.append(f"\nAsset ID: {asset_id}") 1267 | output_lines.append(f"Name: {info['name']}") 1268 | output_lines.append(f"Type: {info['type']}") 1269 | 1270 | if info['usage']: 1271 | output_lines.append("\nUsed in:") 1272 | output_lines.append("-" * 60) 1273 | output_lines.append(f"{'Campaign':<30} | {'Ad Group':<30}") 1274 | output_lines.append("-" * 60) 1275 | 1276 | for usage in info['usage']: 1277 | campaign_str = f"{usage['campaign_name']} ({usage['campaign_id']})" 1278 | ad_group_str = f"{usage['ad_group_name']} ({usage['ad_group_id']})" 1279 | 1280 | output_lines.append(f"{campaign_str[:30]:<30} | {ad_group_str[:30]:<30}") 1281 | 1282 | output_lines.append("=" * 80) 1283 | 1284 | return "\n".join(output_lines) 1285 | 1286 | except Exception as e: 1287 | return f"Error retrieving asset usage: {str(e)}" 1288 | 1289 | @mcp.tool() 1290 | async def analyze_image_assets( 1291 | customer_id: str = Field(description="Google Ads customer ID (10 digits, no dashes). Example: '9873186703'"), 1292 | days: int = Field(default=30, description="Number of days to look back (7, 30, 90, etc.)") 1293 | ) -> str: 1294 | """ 1295 | Analyze image assets with their performance metrics across campaigns. 1296 | 1297 | This comprehensive tool helps you understand which image assets are performing well 1298 | by showing metrics like impressions, clicks, and conversions for each image. 1299 | 1300 | RECOMMENDED WORKFLOW: 1301 | 1. First run list_accounts() to get available account IDs 1302 | 2. Then run get_account_currency() to see what currency the account uses 1303 | 3. Finally run this command to analyze image asset performance 1304 | 1305 | Args: 1306 | customer_id: The Google Ads customer ID as a string (10 digits, no dashes) 1307 | days: Number of days to look back (default: 30) 1308 | 1309 | Returns: 1310 | Detailed report of image assets and their performance metrics 1311 | 1312 | Example: 1313 | customer_id: "1234567890" 1314 | days: 14 1315 | """ 1316 | # Make sure to use a valid date range format 1317 | # Valid formats are: LAST_7_DAYS, LAST_14_DAYS, LAST_30_DAYS, etc. (with underscores) 1318 | if days == 7: 1319 | date_range = "LAST_7_DAYS" 1320 | elif days == 14: 1321 | date_range = "LAST_14_DAYS" 1322 | elif days == 30: 1323 | date_range = "LAST_30_DAYS" 1324 | else: 1325 | # Default to 30 days if not a standard range 1326 | date_range = "LAST_30_DAYS" 1327 | 1328 | query = f""" 1329 | SELECT 1330 | asset.id, 1331 | asset.name, 1332 | asset.image_asset.full_size.url, 1333 | asset.image_asset.full_size.width_pixels, 1334 | asset.image_asset.full_size.height_pixels, 1335 | campaign.name, 1336 | metrics.impressions, 1337 | metrics.clicks, 1338 | metrics.conversions, 1339 | metrics.cost_micros 1340 | FROM 1341 | campaign_asset 1342 | WHERE 1343 | asset.type = 'IMAGE' 1344 | AND segments.date DURING LAST_30_DAYS 1345 | ORDER BY 1346 | metrics.impressions DESC 1347 | LIMIT 200 1348 | """ 1349 | 1350 | try: 1351 | creds = get_credentials() 1352 | headers = get_headers(creds) 1353 | 1354 | formatted_customer_id = format_customer_id(customer_id) 1355 | url = f"https://googleads.googleapis.com/{API_VERSION}/customers/{formatted_customer_id}/googleAds:search" 1356 | 1357 | payload = {"query": query} 1358 | response = requests.post(url, headers=headers, json=payload) 1359 | 1360 | if response.status_code != 200: 1361 | return f"Error analyzing image assets: {response.text}" 1362 | 1363 | results = response.json() 1364 | if not results.get('results'): 1365 | return "No image asset performance data found for this customer ID and time period." 1366 | 1367 | # Group results by asset ID 1368 | assets_data = {} 1369 | for result in results.get('results', []): 1370 | asset = result.get('asset', {}) 1371 | asset_id = asset.get('id') 1372 | 1373 | if asset_id not in assets_data: 1374 | assets_data[asset_id] = { 1375 | 'name': asset.get('name', f"Asset {asset_id}"), 1376 | 'url': asset.get('imageAsset', {}).get('fullSize', {}).get('url', 'N/A'), 1377 | 'dimensions': f"{asset.get('imageAsset', {}).get('fullSize', {}).get('widthPixels', 'N/A')} x {asset.get('imageAsset', {}).get('fullSize', {}).get('heightPixels', 'N/A')}", 1378 | 'impressions': 0, 1379 | 'clicks': 0, 1380 | 'conversions': 0, 1381 | 'cost_micros': 0, 1382 | 'campaigns': set(), 1383 | 'ad_groups': set() 1384 | } 1385 | 1386 | # Aggregate metrics 1387 | metrics = result.get('metrics', {}) 1388 | assets_data[asset_id]['impressions'] += int(metrics.get('impressions', 0)) 1389 | assets_data[asset_id]['clicks'] += int(metrics.get('clicks', 0)) 1390 | assets_data[asset_id]['conversions'] += float(metrics.get('conversions', 0)) 1391 | assets_data[asset_id]['cost_micros'] += int(metrics.get('costMicros', 0)) 1392 | 1393 | # Add campaign and ad group info 1394 | campaign = result.get('campaign', {}) 1395 | ad_group = result.get('adGroup', {}) 1396 | 1397 | if campaign.get('name'): 1398 | assets_data[asset_id]['campaigns'].add(campaign.get('name')) 1399 | if ad_group.get('name'): 1400 | assets_data[asset_id]['ad_groups'].add(ad_group.get('name')) 1401 | 1402 | # Format the results 1403 | output_lines = [f"Image Asset Performance Analysis for Customer ID {formatted_customer_id} (Last {days} days):"] 1404 | output_lines.append("=" * 100) 1405 | 1406 | # Sort assets by impressions (highest first) 1407 | sorted_assets = sorted(assets_data.items(), key=lambda x: x[1]['impressions'], reverse=True) 1408 | 1409 | for asset_id, data in sorted_assets: 1410 | output_lines.append(f"\nAsset ID: {asset_id}") 1411 | output_lines.append(f"Name: {data['name']}") 1412 | output_lines.append(f"Dimensions: {data['dimensions']}") 1413 | 1414 | # Calculate CTR if there are impressions 1415 | ctr = (data['clicks'] / data['impressions'] * 100) if data['impressions'] > 0 else 0 1416 | 1417 | # Format metrics 1418 | output_lines.append(f"\nPerformance Metrics:") 1419 | output_lines.append(f" Impressions: {data['impressions']:,}") 1420 | output_lines.append(f" Clicks: {data['clicks']:,}") 1421 | output_lines.append(f" CTR: {ctr:.2f}%") 1422 | output_lines.append(f" Conversions: {data['conversions']:.2f}") 1423 | output_lines.append(f" Cost (micros): {data['cost_micros']:,}") 1424 | 1425 | # Show where it's used 1426 | output_lines.append(f"\nUsed in {len(data['campaigns'])} campaigns:") 1427 | for campaign in list(data['campaigns'])[:5]: # Show first 5 campaigns 1428 | output_lines.append(f" - {campaign}") 1429 | if len(data['campaigns']) > 5: 1430 | output_lines.append(f" - ... and {len(data['campaigns']) - 5} more") 1431 | 1432 | # Add URL 1433 | if data['url'] != 'N/A': 1434 | output_lines.append(f"\nImage URL: {data['url']}") 1435 | 1436 | output_lines.append("-" * 100) 1437 | 1438 | return "\n".join(output_lines) 1439 | 1440 | except Exception as e: 1441 | return f"Error analyzing image assets: {str(e)}" 1442 | 1443 | @mcp.tool() 1444 | async def list_resources( 1445 | customer_id: str = Field(description="Google Ads customer ID (10 digits, no dashes). Example: '9873186703'") 1446 | ) -> str: 1447 | """ 1448 | List valid resources that can be used in GAQL FROM clauses. 1449 | 1450 | Args: 1451 | customer_id: The Google Ads customer ID as a string 1452 | 1453 | Returns: 1454 | Formatted list of valid resources 1455 | """ 1456 | # Example query that lists some common resources 1457 | # This might need to be adjusted based on what's available in your API version 1458 | query = """ 1459 | SELECT 1460 | google_ads_field.name, 1461 | google_ads_field.category, 1462 | google_ads_field.data_type 1463 | FROM 1464 | google_ads_field 1465 | WHERE 1466 | google_ads_field.category = 'RESOURCE' 1467 | ORDER BY 1468 | google_ads_field.name 1469 | """ 1470 | 1471 | # Use your existing run_gaql function to execute this query 1472 | return await run_gaql(customer_id, query) 1473 | 1474 | if __name__ == "__main__": 1475 | # Start the MCP server on stdio transport 1476 | mcp.run(transport="stdio") 1477 | ```