#
tokens: 26780/50000 13/13 files
lines: off (toggle) GitHub
raw markdown copy
# 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:
--------------------------------------------------------------------------------

```
3.11

```

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
# Python-generated files
__pycache__/
*.py[oc]
build/
dist/
wheels/
*.egg-info/
*.egg

# Virtual environments
.venv/
venv/
ENV/
env/
.env

# Environment and credentials
.env
*.env
service_account_credentials.json
credentials.json
token.json

# Editor-specific files
.vscode/
.idea/
*.sublime-*
*.swp
*.swo
*~

# OS-specific files
.DS_Store
Thumbs.db
desktop.ini

# Testing and coverage
.coverage
.coverage.*
htmlcov/
.pytest_cache/
.tox/
nosetests.xml
coverage.xml
*.cover

# Documentation
docs/_build/
site/

# Logs
*.log

google_ads_token.json

uv.lock
```

--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------

```
# Google Ads MCP Environment Configuration
# Copy this file to .env and fill in your actual values

# Authentication Type (choose one: "oauth" or "service_account")
GOOGLE_ADS_AUTH_TYPE=oauth

# Credentials Path
# For OAuth: Path to client_secret.json or saved token file
# For Service Account: Path to service account key file
GOOGLE_ADS_CREDENTIALS_PATH=/path/to/credentials.json

# Google Ads Developer Token (required)
GOOGLE_ADS_DEVELOPER_TOKEN=your_developer_token_here

# Manager Account ID (optional, for MCC accounts)
# Format: XXX-XXX-XXXX or XXXXXXXXXX
GOOGLE_ADS_LOGIN_CUSTOMER_ID=

# For OAuth-specific config (required if using OAuth and there's no client_secret.json)
GOOGLE_ADS_CLIENT_ID=your_client_id_here
GOOGLE_ADS_CLIENT_SECRET=your_client_secret_here

# For Service Account-specific config (optional)
# Email to impersonate with the service account (typically your admin email)
GOOGLE_ADS_IMPERSONATION_EMAIL=

```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
# Google Ads MCP

![Google Ads MCP](bg.jpeg)

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.

---

## What Can This Tool Do For Advertising Professionals?

1. **Account Management**  
   - See all your Google Ads accounts in one place
   - Get account details and basic campaign information

2. **Campaign Analytics & Reporting**  
   - Discover which campaigns are performing best
   - Track impressions, clicks, conversions, and cost metrics
   - Analyze performance trends over time
   - Compare different time periods to spot changes
   - **Visualize your data** with charts and graphs created by Claude

3. **Keyword & Ad Performance**  
   - Identify top and underperforming keywords
   - Analyze ad copy effectiveness 
   - Check quality scores and competitive metrics
   - Get actionable insights on how to improve your campaigns

4. **Budget & Bid Management**  
   - Monitor campaign budgets and spending
   - Analyze bid strategies and performance
   - Identify opportunities for optimization
   - Get recommendations for budget allocation

---

## Google Ads MCP Architecture Flow

```mermaid
flowchart TB
    User(User) -->|Interacts with| Claude
    Claude(Claude AI Assistant) -->|Makes requests to| MCP[Google Ads MCP Server]
    User -->|Can also use| Cursor[Cursor AI Code Editor]
    Cursor -->|Makes requests to| MCP
    
    subgraph "MCP Server"
        FastMCP[FastMCP Server] 
        Tools[Available Tools]
        Auth[Authentication]
        
        FastMCP -->|Exposes| Tools
        FastMCP -->|Uses| Auth
    end
    
    subgraph "Google Ads Tools"
        ListAccounts[list_accounts]
        ExecuteGAQL[execute_gaql_query]
        CampaignPerf[get_campaign_performance]
        AdPerf[get_ad_performance]
        RunGAQL[run_gaql]
    end
    
    Tools -->|Includes| ListAccounts
    Tools -->|Includes| ExecuteGAQL
    Tools -->|Includes| CampaignPerf
    Tools -->|Includes| AdPerf
    Tools -->|Includes| RunGAQL
    
    subgraph "Authentication"
        OAuth[OAuth 2.0 Client ID]
        ServiceAccount[Service Account]
        Credentials[Google Ads API Credentials]
        
        OAuth -->|Provides| Credentials
        ServiceAccount -->|Provides| Credentials
    end
    
    MCP -->|Communicates with| GoogleAdsAPI[Google Ads API]
    GoogleAdsAPI -->|Returns| AdData[Advertising Data]
    AdData -->|Analyzed by| Claude
    AdData -->|Visualized by| Claude
    AdData -->|Can be used by| Cursor
    
    Credentials -->|Authorizes| GoogleAdsAPI
    
    subgraph "Configuration"
        EnvVars[Environment Variables]
        ConfigFiles[Configuration Files]
        
        EnvVars -->|Configures| MCP
        ConfigFiles -->|Configures| Claude
        ConfigFiles -->|Configures| Cursor
    end
```

## Available Tools

Here's what you can ask Claude to do once you've set up this integration:

| **What You Can Ask For**        | **What It Does**                                            | **What You'll Need to Provide**                                 |
|---------------------------------|-------------------------------------------------------------|----------------------------------------------------------------|
| `list_accounts`                 | Shows all your Google Ads accounts                          | Nothing - just ask!                                             |
| `execute_gaql_query`            | Runs a Google Ads Query Language query                      | Your account ID and a GAQL query                               |
| `get_campaign_performance`      | Shows campaign metrics with performance data                | Your account ID and time period                                 |
| `get_ad_performance`            | Detailed analysis of your ad creative performance           | Your account ID and time period                                 |
| `run_gaql`                      | Runs any arbitrary GAQL query with formatting options       | Your account ID, query, and format (table, JSON, or CSV)        |

### Using the Advanced Query Tools

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:

### Example 1: Basic campaign metrics

```sql
SELECT 
    campaign.name, 
    metrics.clicks, 
    metrics.impressions 
FROM campaign 
WHERE segments.date DURING LAST_7DAYS
```

### Example 2: Ad group performance

```sql
SELECT 
    ad_group.name, 
    metrics.conversions, 
    metrics.cost_micros 
FROM ad_group 
WHERE metrics.clicks > 100
```

### Example 3: Keyword analysis

```sql
SELECT 
    keyword.text, 
    metrics.average_position, 
    metrics.ctr 
FROM keyword_view 
ORDER BY metrics.impressions DESC
```

*For a complete list of all available tools and their detailed descriptions, ask Claude to "list tools" after setup.*

---

## Getting Started (No Coding Experience Required!)

### 1. Set Up Google Ads API Access

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:

#### Option A: OAuth 2.0 Client ID (User Authentication)

Best for individual users or desktop applications:

1. Go to the [Google Cloud Console](https://console.cloud.google.com/)
2. Create a new project or select an existing one
3. Enable the Google Ads API
4. Go to "Credentials" → "Create Credentials" → "OAuth Client ID"
5. Choose "Desktop Application" as the application type
6. Download the OAuth client configuration file (client_secret.json)
7. Create a Google Ads API Developer token (see below)

#### Option B: Service Account (Server-to-Server Authentication)

Better for automated systems or managing multiple accounts:

1. Go to the [Google Cloud Console](https://console.cloud.google.com/)
2. Create a new project or select an existing one
3. Enable the Google Ads API
4. Go to "Credentials" → "Create Credentials" → "Service Account"
5. Download the service account key file (JSON)
6. Grant the service account access to your Google Ads accounts
7. Create a Google Ads API Developer token (see below)

#### Authentication Token Refreshing

The application now includes robust token refresh handling:

- **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.
- **Service Account Tokens**: Service account tokens are automatically generated and refreshed as needed without user intervention.

#### Authentication Method Comparison

Choose OAuth 2.0 Client ID if:

- You're building a desktop application
- Users need to explicitly grant access
- You're managing a single account or a few personal accounts
- You want users to have control over access permissions

Choose Service Account if:

- You're building an automated system
- You need server-to-server authentication
- You're managing multiple accounts programmatically
- You don't want/need user interaction for authentication
- You need automatic token refreshing without user intervention

#### Getting a Developer Token

1. Sign in to your Google Ads account at [https://ads.google.com](https://ads.google.com)
2. Click on Tools & Settings (wrench icon) in the top navigation
3. Under "Setup", click "API Center"
4. If you haven't already, accept the Terms of Service
5. Click "Apply for token" 
6. Fill out the application form with details about how you plan to use the API
7. Submit the application and wait for approval (usually 1-3 business days)

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.

### Understanding the Login Customer ID

The `GOOGLE_ADS_LOGIN_CUSTOMER_ID` is optional and is primarily used when:

- You're working with a Google Ads Manager Account (MCC)
- You need to access multiple client accounts under that manager account

The Login Customer ID should be your Manager Account ID (format: XXX-XXX-XXXX) if:

- You're accessing multiple accounts under a manager account
- You want to use manager account credentials to access client accounts

You can skip this setting if:

- You're only accessing a single Google Ads account
- You're using credentials directly from the account you want to access

To find your Manager Account ID:

1. Sign in to your Google Ads Manager Account
2. Click on the settings icon (gear)
3. Your Manager Account ID will be displayed in the format XXX-XXX-XXXX
4. Download the credentials file (a JSON file)

**🎬 Watch this beginner-friendly tutorial on Youtube:**
COMING SOON

### 2. Install Required Software

You'll need to install these tools on your computer:

- [Python](https://www.python.org/downloads/) (version 3.11 or newer) - This runs the connection between Google Ads and Claude
- [Node.js](https://nodejs.org/en) - Required for running the MCP inspector and certain MCP components
- [Claude Desktop](https://claude.ai/download) - The AI assistant you'll chat with

Make sure both Python and Node.js are properly installed and available in your system path before proceeding.

### 3. Download the Google Ads MCP 

You need to download this tool to your computer. The easiest way is:

1. Click the green "Code" button at the top of this page
2. Select "Download ZIP"
3. Unzip the downloaded file to a location you can easily find (like your Documents folder)

Alternatively, if you're familiar with Git:

```bash
git clone https://github.com/ixigo/mcp-google-ads.git
```

### 4. Install Required Components

Open your computer's Terminal (Mac) or Command Prompt (Windows):

1. Navigate to the folder where you unzipped the files:

   ```bash
   # Example (replace with your actual path):
   cd ~/Documents/mcp-google-ads-main
   ```

2. Create a virtual environment (this keeps the project dependencies isolated):

   ```bash
   # Using uv (recommended):
   uv venv .venv
   
   # If uv is not installed, install it first:
   pip install uv
   # Then create the virtual environment:
   uv venv .venv

   # OR using standard Python:
   python -m venv .venv
   ```

   **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.

3. Activate the virtual environment:

   ```bash
   # On Mac/Linux:
   source .venv/bin/activate
   
   # On Windows:
   .venv\Scripts\activate
   ```

4. Install the required dependencies:

   ```bash
   # Using uv:
   uv pip install -r requirements.txt

   # OR using standard pip:
   pip install -r requirements.txt
   
   # If you encounter any issues with the MCP package, install it separately:
   pip install mcp
   ```

   **If you get "pip not found" error:**

   ```bash
   # First ensure pip is installed and updated:
   python3 -m ensurepip --upgrade
   python3 -m pip install --upgrade pip
   
   # Then try installing the requirements again:
   python3 -m pip install -r requirements.txt
   
   # Or to install uv:
   python3 -m pip install uv
   ```

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.

### 5. Setting Up Environment Configuration

The Google Ads MCP now supports environment file configuration for easier setup.

#### Using .env File (Recommended)

1. Copy the `.env.example` file to `.env` in your project directory:

   ```bash
   cp .env.example .env
   ```

2. Edit the `.env` file with your actual configuration values:

   ```bash
   # Edit the .env file with your favorite text editor
   # For Mac:
   nano .env
   
   # For Windows:
   notepad .env
   ```

3. Set the following values in your `.env` file:

   ```
   # Authentication Type: "oauth" or "service_account"
   GOOGLE_ADS_AUTH_TYPE=oauth
   
   # Path to your credentials file (OAuth client secret or service account key)
   GOOGLE_ADS_CREDENTIALS_PATH=/path/to/your/credentials.json
   
   # Your Google Ads Developer Token
   GOOGLE_ADS_DEVELOPER_TOKEN=your_developer_token_here
   
   # Optional: Manager Account ID (if applicable)
   GOOGLE_ADS_LOGIN_CUSTOMER_ID=your_manager_account_id
   ```

4. Save the file.

The application will automatically load these values from the `.env` file when it starts.

#### Using Direct Environment Variables

You can also set environment variables directly in your system or in the configuration files for Claude or Cursor:

##### For Claude Desktop

```json
{
  "mcpServers": {
    "googleAdsServer": {
      "command": "/FULL/PATH/TO/mcp-google-ads-main/.venv/bin/python",
      "args": ["/FULL/PATH/TO/mcp-google-ads-main/google_ads_server.py"],
      "env": {
        "GOOGLE_ADS_AUTH_TYPE": "oauth",
        "GOOGLE_ADS_CREDENTIALS_PATH": "/FULL/PATH/TO/mcp-google-ads-main/credentials.json",
        "GOOGLE_ADS_DEVELOPER_TOKEN": "YOUR_DEVELOPER_TOKEN_HERE",
        "GOOGLE_ADS_LOGIN_CUSTOMER_ID": "YOUR_MANAGER_ACCOUNT_ID_HERE"
      }
    }
  }
}
```

##### For Cursor

```json
{
  "mcpServers": {
    "googleAdsServer": {
      "command": "/FULL/PATH/TO/mcp-google-ads-main/.venv/bin/python",
      "args": ["/FULL/PATH/TO/mcp-google-ads-main/google_ads_server.py"],
      "env": {
        "GOOGLE_ADS_AUTH_TYPE": "oauth",
        "GOOGLE_ADS_CREDENTIALS_PATH": "/FULL/PATH/TO/mcp-google-ads-main/credentials.json",
        "GOOGLE_ADS_DEVELOPER_TOKEN": "YOUR_DEVELOPER_TOKEN_HERE",
        "GOOGLE_ADS_LOGIN_CUSTOMER_ID": "YOUR_MANAGER_ACCOUNT_ID_HERE"
      }
    }
  }
}
```

### 6. Connect Claude to Google Ads

1. Download and install [Claude Desktop](https://claude.ai/download) if you haven't already
2. Make sure you have your Google service account credentials file saved somewhere on your computer
3. Open your computer's Terminal (Mac) or Command Prompt (Windows) and type:

```bash
# For Mac users:
nano ~/Library/Application\ Support/Claude/claude_desktop_config.json

# For Windows users:
notepad %APPDATA%\Claude\claude_desktop_config.json
```

Add the following text (this tells Claude how to connect to Google Ads):

```json
{
  "mcpServers": {
    "googleAdsServer": {
      "command": "/FULL/PATH/TO/mcp-google-ads-main/.venv/bin/python",
      "args": ["/FULL/PATH/TO/mcp-google-ads-main/google_ads_server.py"],
      "env": {
        "GOOGLE_ADS_CREDENTIALS_PATH": "/FULL/PATH/TO/mcp-google-ads-main/service_account_credentials.json",
        "GOOGLE_ADS_DEVELOPER_TOKEN": "YOUR_DEVELOPER_TOKEN_HERE",
        "GOOGLE_ADS_LOGIN_CUSTOMER_ID": "YOUR_MANAGER_ACCOUNT_ID_HERE"
      }
    }
  }
}
```

**Important:** Replace all paths and values with the actual information for your account:

- The first path should point to the Python executable inside your virtual environment
- The second path should point to the `google_ads_server.py` file inside the folder you unzipped
- The third path should point to your Google service account credentials JSON file
- Add your Google Ads Developer Token 
- Add your Google Ads Manager Account ID (if applicable)

Examples:

- Mac: 
  - Python path: `/Users/ernesto/Documents/mcp-google-ads/.venv/bin/python`
  - Script path: `/Users/ernesto/Documents/mcp-google-ads/google_ads_server.py`
- Windows: 
  - Python path: `C:\\Users\\ernesto\\Documents\\mcp-google-ads\\.venv\\Scripts\\python.exe`
  - Script path: `C:\\Users\\ernesto\\Documents\\mcp-google-ads\\google_ads_server.py`

4. Save the file:

   - Mac: Press Ctrl+O, then Enter, then Ctrl+X to exit
   - Windows: Click File > Save, then close Notepad

5. Restart Claude Desktop

6. When Claude opens, you should now see Google Ads tools available in the tools section

### 5a. Connect to Cursor (AI Code Editor)

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.

#### Setting Up Cursor Integration

1. If you haven't already, download and install [Cursor](https://cursor.sh/) 
2. Create a Cursor MCP configuration file:

   **For project-specific configuration:**
   Create a `.cursor/mcp.json` file in your project directory.

   **For global configuration (available in all projects):**
   Create a `~/.cursor/mcp.json` file in your home directory.

3. Add the following configuration to your MCP config file:

   ```json
   {
     "mcpServers": {
       "googleAdsServer": {
         "command": "/FULL/PATH/TO/mcp-google-ads-main/.venv/bin/python",
         "args": ["/FULL/PATH/TO/mcp-google-ads-main/google_ads_server.py"],
         "env": {
           "GOOGLE_ADS_CREDENTIALS_PATH": "/FULL/PATH/TO/mcp-google-ads-main/service_account_credentials.json",
           "GOOGLE_ADS_DEVELOPER_TOKEN": "YOUR_DEVELOPER_TOKEN_HERE",
           "GOOGLE_ADS_LOGIN_CUSTOMER_ID": "YOUR_MANAGER_ACCOUNT_ID_HERE"
         }
       }
     }
   }
   ```

   **Important:** Replace all paths and values with the actual information for your account, just like in the Claude Desktop configuration.

4. Restart Cursor or reload the workspace to apply the new configuration.

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.

#### Using Google Ads MCP in Cursor

When working in Cursor, you can ask the AI agent to use the Google Ads tools directly. For example:

- "Use the Google Ads MCP to list all my accounts and show me which ones have the highest spend."
- "Can you analyze my campaign performance for the last 30 days using the Google Ads MCP?"
- "Run a GAQL query to find my top converting keywords using the Google Ads tools."

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.

#### Cursor-Specific Features

When using the Google Ads MCP with Cursor, you can:

1. **Combine Code and Ads Analysis**: Ask Cursor to analyze your marketing-related code alongside actual campaign performance data.
2. **Generate Data Visualizations**: Request charts and visualizations of your ad performance directly in your development environment.
3. **Implement Recommendations**: Let Cursor suggest code improvements based on your actual advertising data.

This integration is particularly valuable for developers working on marketing automation, analytics dashboards, or e-commerce applications where ad performance directly impacts code decisions.

### 6. Start Analyzing Your Advertising Data!

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.

Here are some powerful prompts you can use with each tool:

| **Tool Name**                   | **Sample Prompt**                                                                                |
|---------------------------------|--------------------------------------------------------------------------------------------------|
| `list_accounts`                 | "List all my Google Ads accounts and tell me which ones have the highest spend this month."      |
| `execute_gaql_query`            | "Execute this query for account 123-456-7890: SELECT campaign.name, metrics.clicks FROM campaign WHERE metrics.impressions > 1000" |
| `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." |
| `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." |
| `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%'" |

You can also ask Claude to combine multiple tools and analyze the results. For example:

- "Find my top 20 converting keywords, check their quality scores and impression share, and create a report highlighting opportunities for scaling."

- "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."

- "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."

- "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."

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.

---

## Data Visualization Capabilities

Claude can help you visualize your Google Ads data in various ways:

- **Trend Charts**: See how metrics change over time
- **Comparison Graphs**: Compare different campaigns or ad groups
- **Performance Distributions**: Understand how your ads perform across devices or audiences
- **Correlation Analysis**: Identify relationships between spend and conversion metrics
- **Heatmaps**: Visualize complex datasets with color-coded representations

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.

---

## Troubleshooting

### Python Command Not Found

On macOS, the default Python command is often `python3` rather than `python`, which can cause issues with some applications including Node.js integrations.

If you encounter errors related to Python not being found, you can create an alias:

1. Create a Python alias (one-time setup):
   ```bash
   # For macOS users:
   sudo ln -s $(which python3) /usr/local/bin/python
   
   # If that doesn't work, try finding your Python installation:
   sudo ln -s /Library/Frameworks/Python.framework/Versions/3.11/bin/python3 /usr/local/bin/python
   ```

2. Verify the alias works:

   ```bash
   python --version
   ```

This creates a symbolic link so that when applications call `python`, they'll actually use your `python3` installation.

### Claude Configuration Issues

If you're having trouble connecting:

1. Make sure all file paths in your configuration are correct and use the full path
2. Check that your service account has access to your Google Ads accounts
3. Verify that your Developer Token is valid and correctly entered
4. Restart Claude Desktop after making any changes
5. Look for error messages in Claude's response when you try to use a tool
6. Ensure your virtual environment is activated when running the server manually

### Google Ads API Limitations

If you encounter issues related to API quotas or permissions:

1. Check your Google Ads API quota limits in the Google Cloud Console
2. Ensure your Developer Token has the appropriate access level
3. Verify that you've granted the proper permissions to your service account

### Other Unexpected Issues

If you encounter any other unexpected issues during installation or usage:

1. Copy the exact error message you're receiving
2. Contact Ernesto Cohnen at [email protected] for support, including:
   - What you were trying to do
   - The exact error message
   - Your operating system
   - Any steps you've already tried

You can also consult AI assistants which can often help diagnose and resolve technical issues by suggesting specific solutions for your situation.

Remember that most issues have been encountered by others before, and there's usually a straightforward solution available.

### Testing Your Setup

The repository includes test files that let you verify your Google Ads API connection is working correctly before using it with Claude or Cursor.

#### Testing Basic Functionality

1. Make sure your virtual environment is activated:

   ```bash
   # On Mac/Linux:
   source .venv/bin/activate
   
   # On Windows:
   .venv\Scripts\activate
   ```

2. Configure the environment variables in the test file or set them in your environment:
   - Open `test_google_ads_mcp.py` in a text editor
   - Find the section starting with `if not os.environ.get("GOOGLE_ADS_CREDENTIALS_PATH"):`
   - Update the placeholder values with your actual credentials or comment out this section if you've set them as environment variables

3. Run the test:
   ```bash
   python test_google_ads_mcp.py
   ```

4. The test will:
   - List all your Google Ads accounts
   - Use the first account ID to test campaign performance retrieval
   - Test ad performance data
   - Retrieve ad creatives
   - Run a sample GAQL query

#### Testing Authentication and Token Refresh

To specifically test the authentication and token refresh mechanisms:

1. Make sure your virtual environment is activated and your `.env` file is configured.

2. Run the token refresh test:
   ```bash
   python test_token_refresh.py
   ```

3. This test will:
   - Verify that credentials can be loaded from your configured auth type (OAuth or service account)
   - Display information about the current token status and expiry
   - Test the customer ID formatting function
   - For OAuth tokens, attempt to refresh the token and verify it worked

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.
   
If all tests complete successfully, your setup is working correctly and ready to use with Claude or Cursor.

---

## Contributing

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]).

---

## License

This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.

---

## About ixigo

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).

<img src="ixigo-logo.png" alt="ixigo logo" width="200px" />

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:
--------------------------------------------------------------------------------

```
# MCP requirements
mcp>=0.0.11

# Google API requirements
google-auth>=2.25.2
google-auth-oauthlib>=1.1.0
google-auth-httplib2>=0.1.1
requests>=2.31.0

# Environment configuration
python-dotenv>=1.0.0

# Optional visualization dependencies
matplotlib>=3.7.3
pandas>=2.1.4
```

--------------------------------------------------------------------------------
/google-ads.svg:
--------------------------------------------------------------------------------

```
<?xml version="1.0" encoding="UTF-8"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<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">
    <g>
				<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">

</path>
				<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">

</path>
				<ellipse fill="#34A853" cx="42.6637241" cy="187.924414" rx="42.6637241" ry="41.6044138">

</ellipse>
    </g>
</svg>
```

--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------

```toml
[project]
name = "mcp-google-ads"
version = "0.1.0"
description = "Google Ads API integration for Model Context Protocol (MCP)"
readme = "README.md"
requires-python = ">=3.11"
license = {text = "MIT"}
authors = [
    {name = "Ernesto Cohnen", email = "[email protected]"}
]
keywords = ["mcp", "google ads", "seo", "sem", "claude","search analytics"]
classifiers = [
    "Development Status :: 4 - Beta",
    "Intended Audience :: Developers",
    "License :: OSI Approved :: MIT License",
    "Programming Language :: Python :: 3",
    "Programming Language :: Python :: 3.11",
    "Topic :: Internet :: WWW/HTTP :: Indexing/Search",
    "Topic :: Software Development :: Libraries :: Python Modules"
]
dependencies = [
    "google-api-python-client>=2.163.0",
    "google-auth-httplib2>=0.2.0",
    "google-auth-oauthlib>=1.2.1",
    "mcp[cli]>=1.3.0",
]

[project.urls]
"Homepage" = "https://github.com/cohnen/mcp-google-ads"
"Bug Tracker" = "https://github.com/cohnen/mcp-google-ads/issues"

[build-system]
requires = ["setuptools>=61.0", "wheel"]
build-backend = "setuptools.build_meta"

[tool.setuptools]
packages = ["mcp_google_ads"]

```

--------------------------------------------------------------------------------
/format_customer_id_test.py:
--------------------------------------------------------------------------------

```python
def format_customer_id(customer_id: str) -> str:
    """Format customer ID to ensure it's 10 digits without dashes."""
    # Convert to string if passed as integer or another type
    customer_id = str(customer_id)
    
    # Remove any quotes surrounding the customer_id (both escaped and unescaped)
    customer_id = customer_id.replace('\"', '').replace('"', '')
    
    # Remove any non-digit characters (including dashes, braces, etc.)
    customer_id = ''.join(char for char in customer_id if char.isdigit())
    
    # Ensure it's 10 digits with leading zeros if needed
    return customer_id.zfill(10)

def test_format_customer_id():
    """Test the format_customer_id function with various input formats."""
    test_cases = [
        # Regular ID
        ("9873186703", "9873186703"),
        # ID with dashes
        ("987-318-6703", "9873186703"),
        # ID with quotes
        ('"9873186703"', "9873186703"),
        # ID with escaped quotes
        ('\"9873186703\"', "9873186703"),
        # ID with leading zeros that exceed 10 digits - should preserve only last 10
        ("0009873186703", "0009873186703"),
        # Short ID that needs padding
        ("12345", "0000012345"),
        # ID with other non-digit characters
        ("{9873186703}", "9873186703"),
    ]
    
    print("\n=== Testing format_customer_id with various formats ===")
    for input_id, expected in test_cases:
        result = format_customer_id(input_id)
        print(f"Input: {input_id}")
        print(f"Result: {result}")
        print(f"Expected: {expected}")
        print(f"Test {'PASSED' if result == expected else 'FAILED'}")
        print("-" * 50)

if __name__ == "__main__":
    # Run format_customer_id tests
    test_format_customer_id() 
```

--------------------------------------------------------------------------------
/test_token_refresh.py:
--------------------------------------------------------------------------------

```python
#!/usr/bin/env python3
"""
Test script for Google Ads token refresh mechanism and authentication methods.

This script tests both OAuth 2.0 and Service Account authentication methods,
and verifies that token refresh works correctly.
"""

import os
import json
import time
from datetime import datetime, timedelta
from dotenv import load_dotenv

# Load environment variables from .env file
load_dotenv()

# Import get_credentials function from the server
from google_ads_server import get_credentials, get_headers, format_customer_id

def test_token_refresh():
    """Test the token refresh mechanism."""
    print("\n" + "="*50)
    print("GOOGLE ADS TOKEN REFRESH TEST")
    print("="*50)
    
    # Get the authentication type from environment
    auth_type = os.environ.get("GOOGLE_ADS_AUTH_TYPE", "oauth")
    print(f"\nAuthentication type: {auth_type}")
    
    # Get credentials
    print("\nGetting credentials...")
    creds = get_credentials()
    
    # Print credentials info
    if hasattr(creds, 'expired') and hasattr(creds, 'expiry'):
        print(f"Token expired: {creds.expired}")
        print(f"Token expiry: {creds.expiry}")
        
        # Calculate time until expiry
        if creds.expiry:
            now = datetime.now()
            expiry = creds.expiry
            if isinstance(expiry, str):
                expiry = datetime.fromisoformat(expiry.replace('Z', '+00:00'))
            
            time_until_expiry = expiry - now
            print(f"Time until expiry: {time_until_expiry}")
    else:
        print("Service account credentials (no expiry info available)")
    
    # Get headers using the credentials
    print("\nGetting API headers...")
    headers = get_headers(creds)
    
    # Remove sensitive info for display
    safe_headers = headers.copy()
    if 'Authorization' in safe_headers:
        token = safe_headers['Authorization']
        if token:
            # Show only the first 10 chars of the token
            token_start = token[:15]
            safe_headers['Authorization'] = f"{token_start}...TRUNCATED"
    
    print("API Headers:")
    for key, value in safe_headers.items():
        print(f"  {key}: {value}")
    
    # Test if we can force a token refresh (for OAuth tokens)
    if auth_type.lower() == "oauth" and hasattr(creds, 'refresh'):
        print("\nAttempting to force token refresh...")
        try:
            old_token = creds.token[:15] if hasattr(creds, 'token') else None
            creds.refresh(Request())
            new_token = creds.token[:15] if hasattr(creds, 'token') else None
            
            print(f"Old token started with: {old_token}...")
            print(f"New token starts with: {new_token}...")
            print("Token refresh successful!" if old_token != new_token else "Token stayed the same")
        except Exception as e:
            print(f"Error refreshing token: {str(e)}")
    
    print("\nToken test completed successfully!")

def test_customer_id_formatting():
    """Test the customer ID formatting function."""
    print("\n" + "="*50)
    print("CUSTOMER ID FORMATTING TEST")
    print("="*50)
    
    test_cases = [
        "1234567890",
        "123-456-7890",
        "123.456.7890",
        "123 456 7890",
        "\"1234567890\"",
        "1234",
        1234567890,
        None
    ]
    
    print("\nTesting customer ID formatting:")
    for test_case in test_cases:
        try:
            formatted = format_customer_id(test_case)
            print(f"  Input: {test_case}, Output: {formatted}")
        except Exception as e:
            print(f"  Input: {test_case}, Error: {str(e)}")

if __name__ == "__main__":
    # Import Request here to avoid circular imports
    from google.auth.transport.requests import Request
    
    try:
        test_token_refresh()
        test_customer_id_formatting()
        print("\nAll tests completed successfully!")
    except Exception as e:
        print(f"\nTest failed with error: {str(e)}") 
```

--------------------------------------------------------------------------------
/test_google_ads_mcp.py:
--------------------------------------------------------------------------------

```python
import asyncio
import json
import os
import sys
from pathlib import Path

# Add the parent directory to Python path for imports
sys.path.insert(0, str(Path(__file__).parent))

# Import your MCP server module
import google_ads_server

def test_format_customer_id():
    """Test the format_customer_id function with various input formats."""
    test_cases = [
        # Regular ID
        ("9873186703", "9873186703"),
        # ID with dashes
        ("987-318-6703", "9873186703"),
        # ID with quotes
        ('"9873186703"', "9873186703"),
        # ID with escaped quotes
        ('\"9873186703\"', "9873186703"),
        # ID with leading zeros
        ("0009873186703", "9873186703"),
        # Short ID that needs padding
        ("12345", "0000012345"),
        # ID with other non-digit characters
        ("{9873186703}", "9873186703"),
    ]
    
    print("\n=== Testing format_customer_id with various formats ===")
    for input_id, expected in test_cases:
        result = google_ads_server.format_customer_id(input_id)
        print(f"Input: {input_id}")
        print(f"Result: {result}")
        print(f"Expected: {expected}")
        print(f"Test {'PASSED' if result == expected else 'FAILED'}")
        print("-" * 50)

async def test_mcp_tools():
    """Test Google Ads MCP tools directly."""
    # Get a list of available customer IDs first
    print("=== Testing list_accounts ===")
    accounts_result = await google_ads_server.list_accounts()
    print(accounts_result)
    
    # Parse the accounts to extract a customer ID for further tests
    customer_id = None
    for line in accounts_result.split('\n'):
        if line.startswith("Account ID:"):
            customer_id = line.replace("Account ID:", "").strip()
            break
    
    if not customer_id:
        print("No customer IDs found. Cannot continue testing.")
        return
    
    print(f"\nUsing customer ID: {customer_id} for testing\n")
    
    # Test campaign performance
    print("\n=== Testing get_campaign_performance ===")
    campaign_result = await google_ads_server.get_campaign_performance(customer_id, days=90)
    print(campaign_result)
    
    # Test ad performance
    print("\n=== Testing get_ad_performance ===")
    ad_result = await google_ads_server.get_ad_performance(customer_id, days=90)
    print(ad_result)
    
    # Test ad creatives
    print("\n=== Testing get_ad_creatives ===")
    creatives_result = await google_ads_server.get_ad_creatives(customer_id)
    print(creatives_result)
    
    # Test custom GAQL query
    print("\n=== Testing run_gaql ===")
    query = """
        SELECT 
            campaign.id, 
            campaign.name, 
            campaign.status 
        FROM campaign 
        LIMIT 5
    """
    gaql_result = await google_ads_server.run_gaql(customer_id, query, format="json")
    print(gaql_result)

async def test_asset_methods():
    """Test Asset-related MCP tools directly."""
    # Get a list of available customer IDs first
    print("=== Testing Asset Methods ===")
    accounts_result = await google_ads_server.list_accounts()
    
    # Parse the accounts to extract a customer ID for further tests
    customer_id = None
    for line in accounts_result.split('\n'):
        if line.startswith("Account ID:"):
            customer_id = line.replace("Account ID:", "").strip()
            break
    
    if not customer_id:
        print("No customer IDs found. Cannot continue testing.")
        return
    
    print(f"\nUsing customer ID: {customer_id} for testing asset methods\n")
    
    # Test get_image_assets
    print("\n=== Testing get_image_assets ===")
    image_assets_result = await google_ads_server.get_image_assets(customer_id, limit=10)
    print(image_assets_result)
    
    # Extract an asset ID for further testing if available
    asset_id = None
    for line in image_assets_result.split('\n'):
        if line.startswith("1. Asset ID:"):
            asset_id = line.replace("1. Asset ID:", "").strip()
            break
    
    # Use a smaller number of days for testing to avoid the INVALID_VALUE_WITH_DURING_OPERATOR error
    days_to_test = 30  # Use 30 instead of 90
    
    # Test get_asset_usage if we found an asset ID
    if asset_id:
        print(f"\n=== Testing get_asset_usage with asset ID: {asset_id} ===")
        try:
            asset_usage_result = await google_ads_server.get_asset_usage(customer_id, asset_id=asset_id, asset_type="IMAGE")
            print(asset_usage_result)
        except Exception as e:
            print(f"Error in get_asset_usage: {str(e)}")
    else:
        print("\nNo asset ID found to test get_asset_usage")
    
    # Test analyze_image_assets with a valid date range
    print(f"\n=== Testing analyze_image_assets with {days_to_test} days ===")
    try:
        analyze_result = await google_ads_server.analyze_image_assets(customer_id, days=days_to_test)
        print(analyze_result)
    except Exception as e:
        print(f"Error in analyze_image_assets: {str(e)}")

if __name__ == "__main__":
    # Run format_customer_id tests first
    # test_format_customer_id()
    
    # Setup environment variables if they're not already set
    if not os.environ.get("GOOGLE_ADS_CREDENTIALS_PATH"):
        # Set environment variables for testing (comment out if already set in your environment)
        os.environ["GOOGLE_ADS_CREDENTIALS_PATH"] = "google_ads_token.json"
        os.environ["GOOGLE_ADS_DEVELOPER_TOKEN"] = "YOUR_DEVELOPER_TOKEN"  # Replace with placeholder
        os.environ["GOOGLE_ADS_CLIENT_ID"] = "YOUR_CLIENT_ID"  # Replace with placeholder
        os.environ["GOOGLE_ADS_CLIENT_SECRET"] = "YOUR_CLIENT_SECRET"  # Replace with placeholder
    
    # Run the MCP tools test (uncomment to run full tests)
    # asyncio.run(test_mcp_tools())
    
    # Run the asset methods test (uncomment to run full tests)
    asyncio.run(test_asset_methods())
```

--------------------------------------------------------------------------------
/docs/great-gaql-samples.md:
--------------------------------------------------------------------------------

```markdown
## Advanced GAQL Query Examples

### 1. Multi-level Performance Analysis with Geographic and Device Segmentation

```sql
SELECT
  campaign.id,
  campaign.name,
  ad_group.id,
  ad_group.name,
  segments.geo_target_region,
  segments.device,
  segments.day_of_week,
  metrics.impressions,
  metrics.clicks,
  metrics.conversions,
  metrics.conversion_value,
  metrics.cost_micros,
  metrics.cost_per_conversion,
  metrics.conversion_rate,
  metrics.return_on_ad_spend
FROM ad_group
WHERE
  campaign.status = 'ENABLED'
  AND ad_group.status = 'ENABLED'
  AND segments.date DURING LAST_90_DAYS
  AND metrics.impressions > 100
ORDER BY
  segments.geo_target_region,
  segments.device,
  metrics.return_on_ad_spend DESC
LIMIT 1000
```

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.

### 2. Bidding Strategy Effectiveness Analysis

```sql
SELECT
  campaign.id,
  campaign.name,
  campaign.bidding_strategy_type,
  bidding_strategy.id,
  bidding_strategy.name,
  bidding_strategy.type,
  campaign.target_cpa.target_cpa_micros,
  campaign.target_roas.target_roas,
  segments.date,
  metrics.impressions,
  metrics.clicks,
  metrics.conversions,
  metrics.conversion_value,
  metrics.cost_micros,
  metrics.average_cpc,
  metrics.cost_per_conversion
FROM campaign
WHERE
  campaign.status = 'ENABLED'
  AND segments.date DURING LAST_30_DAYS
  AND metrics.impressions > 0
ORDER BY
  campaign.bidding_strategy_type,
  segments.date
```

This query helps analyze the effectiveness of different bidding strategies by comparing key performance metrics across campaigns using various automated bidding approaches.

### 3. Ad Performance by Landing Page with Quality Score Analysis

```sql
SELECT
  campaign.id,
  campaign.name,
  ad_group.id,
  ad_group.name,
  ad_group_ad.ad.id,
  ad_group_ad.ad.final_urls,
  ad_group_ad.ad.type,
  ad_group_ad.ad.expanded_text_ad.headline_part1,
  ad_group_ad.ad.expanded_text_ad.headline_part2,
  ad_group_criterion.keyword.text,
  ad_group_criterion.quality_info.quality_score,
  ad_group_criterion.quality_info.creative_quality_score,
  ad_group_criterion.quality_info.post_click_quality_score,
  ad_group_criterion.quality_info.search_predicted_ctr,
  metrics.impressions,
  metrics.clicks,
  metrics.conversions,
  metrics.conversion_value,
  metrics.cost_micros,
  metrics.average_cpc,
  metrics.ctr
FROM ad_group_ad
WHERE
  campaign.status = 'ENABLED'
  AND ad_group.status = 'ENABLED'
  AND ad_group_ad.status = 'ENABLED'
  AND segments.date DURING LAST_30_DAYS
  AND metrics.impressions > 100
ORDER BY
  metrics.conversion_value DESC,
  ad_group_criterion.quality_info.quality_score DESC
```

This query examines ad performance in relation to landing pages and quality scores, helping identify high-performing ad creatives and their associated landing pages.

### 4. Keyword Performance Analysis with Impression Share and Position Metrics

```sql
SELECT
  campaign.id,
  campaign.name,
  ad_group.id,
  ad_group.name,
  ad_group_criterion.criterion_id,
  ad_group_criterion.keyword.text,
  ad_group_criterion.keyword.match_type,
  metrics.impressions,
  metrics.clicks,
  metrics.conversions,
  metrics.conversion_value,
  metrics.cost_micros,
  metrics.absolute_top_impression_percentage,
  metrics.top_impression_percentage,
  metrics.search_impression_share,
  metrics.search_rank_lost_impression_share,
  metrics.search_budget_lost_impression_share
FROM keyword_view
WHERE
  campaign.status = 'ENABLED'
  AND ad_group.status = 'ENABLED'
  AND ad_group_criterion.status = 'ENABLED'
  AND segments.date DURING LAST_90_DAYS
  AND metrics.impressions > 10
ORDER BY
  metrics.conversion_value DESC,
  metrics.search_impression_share ASC
```

This query helps identify keywords that are performing well but may be limited by impression share, indicating opportunities for bid or budget adjustments.

### 5. Complex Audience Segmentation Performance Analysis

```sql
SELECT
  campaign.id,
  campaign.name,
  ad_group.id,
  ad_group.name,
  segments.audience.id,
  segments.audience.name,
  segments.audience.type,
  segments.date,
  metrics.impressions,
  metrics.clicks,
  metrics.conversions,
  metrics.conversion_value,
  metrics.cost_micros,
  metrics.average_cpc,
  metrics.ctr,
  metrics.conversion_rate,
  metrics.value_per_conversion
FROM ad_group
WHERE
  campaign.advertising_channel_type = 'DISPLAY'
  AND campaign.status = 'ENABLED'
  AND ad_group.status = 'ENABLED'
  AND segments.date DURING LAST_90_DAYS
  AND segments.audience.id IS NOT NULL
ORDER BY
  segments.audience.type,
  metrics.conversion_value DESC
```

This query analyzes the performance of different audience segments across display campaigns, helping identify the most valuable audience types.

### 6. Shopping Campaign Product Performance Analysis

```sql
SELECT
  campaign.id,
  campaign.name,
  ad_group.id,
  ad_group.name,
  segments.product_item_id,
  segments.product_title,
  segments.product_type_l1,
  segments.product_type_l2,
  segments.product_type_l3,
  segments.product_type_l4,
  segments.product_type_l5,
  segments.product_brand,
  metrics.impressions,
  metrics.clicks,
  metrics.conversions,
  metrics.conversion_value,
  metrics.cost_micros,
  metrics.ctr,
  metrics.conversion_rate,
  metrics.return_on_ad_spend
FROM shopping_performance_view
WHERE
  campaign.advertising_channel_type = 'SHOPPING'
  AND campaign.status = 'ENABLED'
  AND ad_group.status = 'ENABLED'
  AND segments.date DURING LAST_30_DAYS
  AND metrics.impressions > 0
ORDER BY
  metrics.return_on_ad_spend DESC
```

This query provides a detailed breakdown of shopping campaign performance by product attributes, helping identify high-performing products and product categories.

### 7. Ad Schedule Performance with Bid Modifier Analysis

```sql
SELECT
  campaign.id,
  campaign.name,
  ad_group.id,
  ad_group.name,
  ad_schedule_view.day_of_week,
  ad_schedule_view.start_hour,
  ad_schedule_view.end_hour,
  campaign_criterion.bid_modifier,
  segments.date,
  metrics.impressions,
  metrics.clicks,
  metrics.conversions,
  metrics.conversion_value,
  metrics.cost_micros,
  metrics.ctr,
  metrics.conversion_rate,
  metrics.value_per_conversion
FROM ad_schedule_view
WHERE
  campaign.status = 'ENABLED' 
  AND segments.date DURING LAST_14_DAYS
ORDER BY
  ad_schedule_view.day_of_week,
  ad_schedule_view.start_hour
```

This query analyzes performance across different ad schedules and compares it with the applied bid modifiers, helping identify opportunities for schedule-based bid adjustments.

### 8. Cross-Campaign Asset Performance Analysis

```sql
SELECT
  campaign.id,
  campaign.name,
  ad_group.id,
  ad_group.name,
  asset.id,
  asset.type,
  asset.name,
  asset.text_asset.text,
  asset.image_asset.full_size.url,
  asset_performance_label,
  metrics.impressions,
  metrics.clicks,
  metrics.conversions,
  metrics.cost_micros,
  metrics.ctr
FROM asset_performance_label_view
WHERE
  campaign.status = 'ENABLED'
  AND ad_group.status = 'ENABLED'
  AND segments.date DURING LAST_30_DAYS
ORDER BY
  asset.type,
  metrics.conversions DESC
```

This query helps analyze performance of assets (images, text, etc.) across campaigns, helping identify high-performing creative elements.

### 9. Geographic Performance with Location Bid Modifier Analysis

```sql
SELECT
  campaign.id,
  campaign.name,
  geographic_view.country_criterion_id,
  geographic_view.location_type,
  geographic_view.geo_target_constant,
  campaign_criterion.bid_modifier,
  segments.date,
  metrics.impressions,
  metrics.clicks,
  metrics.conversions,
  metrics.conversion_value,
  metrics.cost_micros,
  metrics.ctr,
  metrics.conversion_rate
FROM geographic_view
WHERE
  campaign.status = 'ENABLED'
  AND segments.date DURING LAST_30_DAYS
ORDER BY
  geographic_view.country_criterion_id,
  metrics.conversion_value DESC
```

This query analyzes performance across different geographic locations and compares it with location bid modifiers, helping identify opportunities for geographic bid adjustments.

### 10. Advanced Budget Utilization and Performance Analysis

```sql
SELECT
  campaign.id,
  campaign.name,
  campaign.status,
  campaign_budget.amount_micros,
  campaign_budget.total_amount_micros,
  campaign_budget.delivery_method,
  campaign_budget.reference_count,
  campaign_budget.has_recommended_budget,
  campaign_budget.recommended_budget_amount_micros,
  segments.date,
  metrics.cost_micros,
  metrics.impressions,
  metrics.clicks,
  metrics.conversions,
  metrics.conversion_value,
  (metrics.cost_micros * 1.0) / (campaign_budget.amount_micros * 1.0) AS budget_utilization_rate
FROM campaign
WHERE
  campaign.status IN ('ENABLED', 'PAUSED')
  AND segments.date DURING LAST_30_DAYS
ORDER BY
  segments.date DESC,
  budget_utilization_rate DESC
```

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.

## Practical Applications of These Queries

These advanced GAQL queries can help you:

1. **Identify performance trends** across different dimensions (geographic, temporal, device-based)
2. **Optimize bidding strategies** by comparing performance across different automated bidding approaches
3. **Improve quality scores** by analyzing the relationship between landing pages, ad creatives, and performance metrics
4. **Maximize impression share** for high-performing keywords and ad groups
5. **Refine audience targeting** by identifying the most valuable audience segments
6. **Optimize product feeds** for shopping campaigns by analyzing performance at the product level
7. **Fine-tune ad scheduling** based on day and hour performance analysis
8. **Improve creative assets** by identifying high-performing images, text, and other creative elements
9. **Adjust geographic targeting** based on performance differences across locations
10. **Optimize budget allocation** to maximize return on ad spend


```

--------------------------------------------------------------------------------
/docs/gaql-google-ads-query-language.md:
--------------------------------------------------------------------------------

```markdown
---
description: Use this to write better GAQL queries
globs: 
alwaysApply: false
---
# Google Ads Query Language (GAQL) Guidelines

## Overview

The Google Ads Query Language (GAQL) is a powerful tool for querying the Google Ads API that allows you to retrieve:

1. **Resources** and their related attributes, segments, and metrics using `GoogleAdsService.Search` or `GoogleAdsService.SearchStream`
2. **Metadata** about available fields and resources using `GoogleAdsFieldService`

## Field Categories

Understanding field categories is essential for building effective GAQL queries:

1. **RESOURCE**: Represents a primary entity (e.g., `campaign`, `ad_group`) that can be used in the FROM clause
2. **ATTRIBUTE**: Properties of a resource (e.g., `campaign.id`, `campaign.name`). Including these may segment results depending on the resource relationship
3. **SEGMENT**: Fields that always segment search queries (e.g., `segments.date`, `segments.device`)
4. **METRIC**: Performance data fields (e.g., `metrics.impressions`, `metrics.clicks`) that never segment search queries

## Query Structure

A GAQL query consists of the following components:

```
SELECT
  <field_1>,
  <field_2>,
  ...
FROM <resource>
WHERE <condition_1> AND <condition_2> AND ...
ORDER BY <field_1> [ASC|DESC], <field_2> [ASC|DESC], ...
LIMIT <number_of_results>
```

### SELECT Clause

The `SELECT` clause specifies the fields to return in the query results:

```
SELECT
  campaign.id,
  campaign.name,
  metrics.impressions,
  segments.device
```

Only fields that are marked as `selectable: true` in the `GoogleAdsField` metadata can be used in the SELECT clause.

### FROM Clause

The `FROM` clause specifies the primary resource type to query from. Only one resource can be specified, and it must have the category `RESOURCE`.

```
FROM campaign
```

### WHERE Clause (optional)

The `WHERE` clause specifies conditions to filter the results. Only fields marked as `filterable: true` in the `GoogleAdsField` metadata can be used for filtering.

```
WHERE 
  campaign.status = 'ENABLED'
  AND metrics.impressions > 1000
  AND segments.date DURING LAST_30_DAYS
```

### ORDER BY Clause (optional)

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.

```
ORDER BY metrics.impressions DESC, campaign.id
```

### LIMIT Clause (optional)

The `LIMIT` clause restricts the number of results returned.

```
LIMIT 100
```

## Field Metadata Exploration

To explore available fields and their properties, use the `GoogleAdsFieldService`:

```
SELECT
  name,
  category,
  selectable,
  filterable,
  sortable,
  selectable_with,
  attribute_resources,
  metrics,
  segments,
  data_type,
  enum_values,
  is_repeated
WHERE name = "campaign.id"
```

Key metadata properties to understand:

- **`selectable`**: Whether the field can be used in a SELECT clause
- **`filterable`**: Whether the field can be used in a WHERE clause
- **`sortable`**: Whether the field can be used in an ORDER BY clause
- **`selectable_with`**: Lists resources, segments, and metrics that are selectable with this field
- **`attribute_resources`**: For RESOURCE fields, lists the resources that are selectable with this resource and don't segment metrics
- **`metrics`**: For RESOURCE fields, lists metrics that are selectable when this resource is in the FROM clause
- **`segments`**: For RESOURCE fields, lists fields that segment metrics when this resource is used in the FROM clause
- **`data_type`**: Determines which operators can be used with the field in WHERE clauses
- **`enum_values`**: Lists possible values for ENUM type fields
- **`is_repeated`**: Whether the field can contain multiple values

## Data Types and Operators

Different field data types support different operators in WHERE clauses:

### String Fields
- `=`, `!=`, `IN`, `NOT IN`
- `LIKE`, `NOT LIKE` (case-sensitive string matching)
- `CONTAINS ANY`, `CONTAINS ALL`, `CONTAINS NONE` (for repeated fields)

### Numeric Fields
- `=`, `!=`, `<`, `<=`, `>`, `>=`
- `IN`, `NOT IN`

### Date Fields
- `=`, `!=`, `<`, `<=`, `>`, `>=`
- `DURING` (with named date ranges)
- `BETWEEN` (with date literals)

### Enum Fields
- `=`, `!=`, `IN`, `NOT IN`
- Values must match exactly as listed in `enum_values`

### Boolean Fields
- `=`, `!=`
- Values must be `TRUE` or `FALSE`

## Date Ranges

### Literal Date Ranges
```
WHERE segments.date BETWEEN '2020-01-01' AND '2020-01-31'
```

### Named Date Ranges
```
WHERE segments.date DURING LAST_7_DAYS
WHERE segments.date DURING LAST_14_DAYS
WHERE segments.date DURING LAST_30_DAYS
WHERE segments.date DURING LAST_90_DAYS
WHERE segments.date DURING THIS_MONTH
WHERE segments.date DURING LAST_MONTH
WHERE segments.date DURING THIS_QUARTER
```

### Date Functions
```
WHERE segments.date = YESTERDAY
WHERE segments.date = TODAY
```

## Case Sensitivity Rules

1. **Field and resource names**: Case-sensitive (`campaign.id` not `Campaign.Id`)
2. **Enumeration values**: Case-sensitive (`'ENABLED'` not `'enabled'`)
3. **String literals in conditions**:
   - Case-insensitive by default (`WHERE campaign.name = 'brand campaign'`)
   - Use `LIKE` for case-sensitive matching (`WHERE campaign.name LIKE 'Brand Campaign'`)

## Ordering and Limiting Results

### Ordering
- Results can be ordered by one or more fields
- Use `ASC` (default) or `DESC` to specify direction
- Only fields marked as `sortable: true` can be used

```
ORDER BY metrics.impressions DESC, campaign.id ASC
```

### Limiting
- Use LIMIT to restrict the number of rows returned
- Always use ORDER BY with LIMIT for consistent pagination
- The maximum value is system-dependent

```
LIMIT 100
```

## Query Examples

### Basic Campaign Query
```
SELECT
  campaign.id,
  campaign.name,
  campaign.status
FROM campaign
ORDER BY campaign.id
```

### Query with Metrics and Filtering
```
SELECT
  campaign.id,
  campaign.name,
  metrics.impressions,
  metrics.clicks,
  metrics.cost_micros
FROM campaign
WHERE 
  campaign.status = 'ENABLED'
  AND metrics.impressions > 1000
ORDER BY metrics.impressions DESC
LIMIT 10
```

### Query with Segments
```
SELECT
  campaign.id,
  campaign.name,
  segments.date,
  metrics.impressions,
  metrics.clicks,
  metrics.conversions
FROM campaign
WHERE 
  segments.date DURING LAST_30_DAYS
  AND campaign.status = 'ENABLED'
ORDER BY segments.date DESC, metrics.impressions DESC
```

### Query with Attributed Resources
```
SELECT
  campaign.id,
  campaign.name,
  campaign.status,
  bidding_strategy.id,
  bidding_strategy.name,
  bidding_strategy.type
FROM campaign
WHERE campaign.status = 'ENABLED'
```

### Field Metadata Query
```
SELECT
  name,
  category,
  selectable,
  filterable,
  sortable,
  data_type,
  enum_values
WHERE name LIKE 'campaign.%'
```

I'll help add information about assets to the GAQL guidelines document. Here's how I would incorporate the new information:

```markdown:gaql-google-ads-query-language.mdc
// ... existing code ...

## Asset Queries

### Asset Entity Queries

You can get a list of assets and their attributes by querying the `asset` entity:

```
SELECT
  asset.id,
  asset.name,
  asset.resource_name,
  asset.type
FROM asset
```

### Type-Specific Asset Attributes

Assets have type-specific attributes that can be queried based on their type:

```
SELECT
  asset.id,
  asset.name,
  asset.resource_name,
  asset.youtube_video_asset.youtube_video_id
FROM asset
WHERE asset.type = 'YOUTUBE_VIDEO'
```

### Asset Metrics at Different Levels

Asset metrics are available through three main resources:

1. **ad_group_asset**: Asset metrics at the ad group level
2. **campaign_asset**: Asset metrics at the campaign level
3. **customer_asset**: Asset metrics at the customer level

Example of querying ad-group level asset metrics:

```
SELECT
  ad_group.id,
  asset.id,
  metrics.clicks,
  metrics.impressions
FROM ad_group_asset
WHERE segments.date DURING LAST_MONTH
ORDER BY metrics.impressions DESC
```

### Ad-Level Asset Performance

Ad-level performance metrics for assets are aggregated in the `ad_group_ad_asset_view`. 

**Note**: The `ad_group_ad_asset_view` only returns information for assets related to App ads.

This view includes the `performance_label` attribute with the following possible values:
- `BEST`: Best performing assets
- `GOOD`: Good performing assets
- `LOW`: Worst performing assets
- `LEARNING`: Asset has impressions but stats aren't statistically significant yet
- `PENDING`: Asset doesn't have performance information yet (may be under review)
- `UNKNOWN`: Value unknown in this version
- `UNSPECIFIED`: Not specified

Example query for ad-level asset performance:

```
SELECT
  ad_group_ad_asset_view.ad_group_ad,
  ad_group_ad_asset_view.asset,
  ad_group_ad_asset_view.field_type,
  ad_group_ad_asset_view.performance_label,
  metrics.impressions,
  metrics.clicks,
  metrics.cost_micros,
  metrics.conversions
FROM ad_group_ad_asset_view
WHERE segments.date DURING LAST_MONTH
ORDER BY ad_group_ad_asset_view.performance_label
```

### Asset Source Information

- `Asset.source` is only accurate for mutable Assets
- For the source of RSA (Responsive Search Ad) Assets, use `AdGroupAdAsset.source`

// ... existing code ...
```

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.


## Best Practices

1. **Field Selection**: Only select the fields you need to reduce response size and improve performance.

2. **Filtering**: Apply filters in the `WHERE` clause to limit results to relevant data.

3. **Verify Field Properties**: Before using a field in a query, check its metadata to ensure it's selectable, filterable, or sortable as needed.

4. **Result Ordering**: Always use `ORDER BY` to ensure consistent results, especially when using pagination.

5. **Result Limiting**: Use `LIMIT` to restrict number of returned rows and improve performance.

6. **Handle Repeated Fields**: For fields where `is_repeated = true`, use appropriate operators like `CONTAINS ANY`, `CONTAINS ALL`, or `CONTAINS NONE`.

7. **Understand Segmentation**: Be aware that including segment fields or certain attribute fields will cause metrics to be segmented in the results.

8. **Date Handling**: Use appropriate date functions and ranges for filtering by date segments.

9. **Pagination**: For large result sets, use the page token provided in the response to retrieve subsequent pages.

10. **Check Enum Values**: For enum fields, verify the allowed values in the `enum_values` property before using them in queries.

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.

```

--------------------------------------------------------------------------------
/google_ads_server.py:
--------------------------------------------------------------------------------

```python
from typing import Any, Dict, List, Optional, Union
from pydantic import Field
import os
import json
import requests
from datetime import datetime, timedelta
from pathlib import Path

from google_auth_oauthlib.flow import InstalledAppFlow
from google.oauth2.credentials import Credentials
from google.oauth2 import service_account
from google.auth.transport.requests import Request
from google.auth.exceptions import RefreshError
import logging

# MCP
from mcp.server.fastmcp import FastMCP

# Configure logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger('google_ads_server')

mcp = FastMCP(
    "google-ads-server",
    dependencies=[
        "google-auth-oauthlib",
        "google-auth",
        "requests",
        "python-dotenv"
    ]
)

# Constants and configuration
SCOPES = ['https://www.googleapis.com/auth/adwords']
API_VERSION = "v19"  # Google Ads API version

# Load environment variables
try:
    from dotenv import load_dotenv
    # Load from .env file if it exists
    load_dotenv()
    logger.info("Environment variables loaded from .env file")
except ImportError:
    logger.warning("python-dotenv not installed, skipping .env file loading")

# Get credentials from environment variables
GOOGLE_ADS_CREDENTIALS_PATH = os.environ.get("GOOGLE_ADS_CREDENTIALS_PATH")
GOOGLE_ADS_DEVELOPER_TOKEN = os.environ.get("GOOGLE_ADS_DEVELOPER_TOKEN")
GOOGLE_ADS_LOGIN_CUSTOMER_ID = os.environ.get("GOOGLE_ADS_LOGIN_CUSTOMER_ID", "")
GOOGLE_ADS_AUTH_TYPE = os.environ.get("GOOGLE_ADS_AUTH_TYPE", "oauth")  # oauth or service_account

def format_customer_id(customer_id: str) -> str:
    """Format customer ID to ensure it's 10 digits without dashes."""
    # Convert to string if passed as integer or another type
    customer_id = str(customer_id)
    
    # Remove any quotes surrounding the customer_id (both escaped and unescaped)
    customer_id = customer_id.replace('\"', '').replace('"', '')
    
    # Remove any non-digit characters (including dashes, braces, etc.)
    customer_id = ''.join(char for char in customer_id if char.isdigit())
    
    # Ensure it's 10 digits with leading zeros if needed
    return customer_id.zfill(10)

def get_credentials():
    """
    Get and refresh OAuth credentials or service account credentials based on the auth type.
    
    This function supports two authentication methods:
    1. OAuth 2.0 (User Authentication) - For individual users or desktop applications
    2. Service Account (Server-to-Server Authentication) - For automated systems

    Returns:
        Valid credentials object to use with Google Ads API
    """
    if not GOOGLE_ADS_CREDENTIALS_PATH:
        raise ValueError("GOOGLE_ADS_CREDENTIALS_PATH environment variable not set")
    
    auth_type = GOOGLE_ADS_AUTH_TYPE.lower()
    logger.info(f"Using authentication type: {auth_type}")
    
    # Service Account authentication
    if auth_type == "service_account":
        try:
            return get_service_account_credentials()
        except Exception as e:
            logger.error(f"Error with service account authentication: {str(e)}")
            raise
    
    # OAuth 2.0 authentication (default)
    return get_oauth_credentials()

def get_service_account_credentials():
    """Get credentials using a service account key file."""
    logger.info(f"Loading service account credentials from {GOOGLE_ADS_CREDENTIALS_PATH}")
    
    if not os.path.exists(GOOGLE_ADS_CREDENTIALS_PATH):
        raise FileNotFoundError(f"Service account key file not found at {GOOGLE_ADS_CREDENTIALS_PATH}")
    
    try:
        credentials = service_account.Credentials.from_service_account_file(
            GOOGLE_ADS_CREDENTIALS_PATH, 
            scopes=SCOPES
        )
        
        # Check if impersonation is required
        impersonation_email = os.environ.get("GOOGLE_ADS_IMPERSONATION_EMAIL")
        if impersonation_email:
            logger.info(f"Impersonating user: {impersonation_email}")
            credentials = credentials.with_subject(impersonation_email)
            
        return credentials
        
    except Exception as e:
        logger.error(f"Error loading service account credentials: {str(e)}")
        raise

def get_oauth_credentials():
    """Get and refresh OAuth user credentials."""
    creds = None
    client_config = None
    
    # Path to store the refreshed token
    token_path = GOOGLE_ADS_CREDENTIALS_PATH
    if os.path.exists(token_path) and not os.path.basename(token_path).endswith('.json'):
        # If it's not explicitly a .json file, append a default name
        token_dir = os.path.dirname(token_path)
        token_path = os.path.join(token_dir, 'google_ads_token.json')
    
    # Check if token file exists and load credentials
    if os.path.exists(token_path):
        try:
            logger.info(f"Loading OAuth credentials from {token_path}")
            with open(token_path, 'r') as f:
                creds_data = json.load(f)
                # Check if this is a client config or saved credentials
                if "installed" in creds_data or "web" in creds_data:
                    client_config = creds_data
                    logger.info("Found OAuth client configuration")
                else:
                    logger.info("Found existing OAuth token")
                    creds = Credentials.from_authorized_user_info(creds_data, SCOPES)
        except json.JSONDecodeError:
            logger.warning(f"Invalid JSON in token file: {token_path}")
            creds = None
        except Exception as e:
            logger.warning(f"Error loading credentials: {str(e)}")
            creds = None
    
    # If credentials don't exist or are invalid, get new ones
    if not creds or not creds.valid:
        if creds and creds.expired and creds.refresh_token:
            try:
                logger.info("Refreshing expired token")
                creds.refresh(Request())
                logger.info("Token successfully refreshed")
            except RefreshError as e:
                logger.warning(f"Error refreshing token: {str(e)}, will try to get new token")
                creds = None
            except Exception as e:
                logger.error(f"Unexpected error refreshing token: {str(e)}")
                raise
        
        # If we need new credentials
        if not creds:
            # If no client_config is defined yet, create one from environment variables
            if not client_config:
                logger.info("Creating OAuth client config from environment variables")
                client_id = os.environ.get("GOOGLE_ADS_CLIENT_ID")
                client_secret = os.environ.get("GOOGLE_ADS_CLIENT_SECRET")
                
                if not client_id or not client_secret:
                    raise ValueError("GOOGLE_ADS_CLIENT_ID and GOOGLE_ADS_CLIENT_SECRET must be set if no client config file exists")
                
                client_config = {
                    "installed": {
                        "client_id": client_id,
                        "client_secret": client_secret,
                        "auth_uri": "https://accounts.google.com/o/oauth2/auth",
                        "token_uri": "https://oauth2.googleapis.com/token",
                        "redirect_uris": ["urn:ietf:wg:oauth:2.0:oob", "http://localhost"]
                    }
                }
            
            # Run the OAuth flow
            logger.info("Starting OAuth authentication flow")
            flow = InstalledAppFlow.from_client_config(client_config, SCOPES)
            creds = flow.run_local_server(port=0)
            logger.info("OAuth flow completed successfully")
        
        # Save the refreshed/new credentials
        try:
            logger.info(f"Saving credentials to {token_path}")
            # Ensure directory exists
            os.makedirs(os.path.dirname(token_path), exist_ok=True)
            with open(token_path, 'w') as f:
                f.write(creds.to_json())
        except Exception as e:
            logger.warning(f"Could not save credentials: {str(e)}")
    
    return creds

def get_headers(creds):
    """Get headers for Google Ads API requests."""
    if not GOOGLE_ADS_DEVELOPER_TOKEN:
        raise ValueError("GOOGLE_ADS_DEVELOPER_TOKEN environment variable not set")
    
    # Handle different credential types
    if isinstance(creds, service_account.Credentials):
        # For service account, we need to get a new bearer token
        auth_req = Request()
        creds.refresh(auth_req)
        token = creds.token
    else:
        # For OAuth credentials, check if token needs refresh
        if not creds.valid:
            if creds.expired and creds.refresh_token:
                try:
                    logger.info("Refreshing expired OAuth token in get_headers")
                    creds.refresh(Request())
                    logger.info("Token successfully refreshed in get_headers")
                except RefreshError as e:
                    logger.error(f"Error refreshing token in get_headers: {str(e)}")
                    raise ValueError(f"Failed to refresh OAuth token: {str(e)}")
                except Exception as e:
                    logger.error(f"Unexpected error refreshing token in get_headers: {str(e)}")
                    raise
            else:
                raise ValueError("OAuth credentials are invalid and cannot be refreshed")
        
        token = creds.token
        
    headers = {
        'Authorization': f'Bearer {token}',
        'developer-token': GOOGLE_ADS_DEVELOPER_TOKEN,
        'content-type': 'application/json'
    }
    
    if GOOGLE_ADS_LOGIN_CUSTOMER_ID:
        headers['login-customer-id'] = format_customer_id(GOOGLE_ADS_LOGIN_CUSTOMER_ID)
    
    return headers

@mcp.tool()
async def list_accounts() -> str:
    """
    Lists all accessible Google Ads accounts.
    
    This is typically the first command you should run to identify which accounts 
    you have access to. The returned account IDs can be used in subsequent commands.
    
    Returns:
        A formatted list of all Google Ads accounts accessible with your credentials
    """
    try:
        creds = get_credentials()
        headers = get_headers(creds)
        
        url = f"https://googleads.googleapis.com/{API_VERSION}/customers:listAccessibleCustomers"
        response = requests.get(url, headers=headers)
        
        if response.status_code != 200:
            return f"Error accessing accounts: {response.text}"
        
        customers = response.json()
        if not customers.get('resourceNames'):
            return "No accessible accounts found."
        
        # Format the results
        result_lines = ["Accessible Google Ads Accounts:"]
        result_lines.append("-" * 50)
        
        for resource_name in customers['resourceNames']:
            customer_id = resource_name.split('/')[-1]
            formatted_id = format_customer_id(customer_id)
            result_lines.append(f"Account ID: {formatted_id}")
        
        return "\n".join(result_lines)
    
    except Exception as e:
        return f"Error listing accounts: {str(e)}"

@mcp.tool()
async def execute_gaql_query(
    customer_id: str = Field(description="Google Ads customer ID (10 digits, no dashes). Example: '9873186703'"),
    query: str = Field(description="Valid GAQL query string following Google Ads Query Language syntax")
) -> str:
    """
    Execute a custom GAQL (Google Ads Query Language) query.
    
    This tool allows you to run any valid GAQL query against the Google Ads API.
    
    Args:
        customer_id: The Google Ads customer ID as a string (10 digits, no dashes)
        query: The GAQL query to execute (must follow GAQL syntax)
        
    Returns:
        Formatted query results or error message
        
    Example:
        customer_id: "1234567890"
        query: "SELECT campaign.id, campaign.name FROM campaign LIMIT 10"
    """
    try:
        creds = get_credentials()
        headers = get_headers(creds)
        
        formatted_customer_id = format_customer_id(customer_id)
        url = f"https://googleads.googleapis.com/{API_VERSION}/customers/{formatted_customer_id}/googleAds:search"
        
        payload = {"query": query}
        response = requests.post(url, headers=headers, json=payload)
        
        if response.status_code != 200:
            return f"Error executing query: {response.text}"
        
        results = response.json()
        if not results.get('results'):
            return "No results found for the query."
        
        # Format the results as a table
        result_lines = [f"Query Results for Account {formatted_customer_id}:"]
        result_lines.append("-" * 80)
        
        # Get field names from the first result
        fields = []
        first_result = results['results'][0]
        for key in first_result:
            if isinstance(first_result[key], dict):
                for subkey in first_result[key]:
                    fields.append(f"{key}.{subkey}")
            else:
                fields.append(key)
        
        # Add header
        result_lines.append(" | ".join(fields))
        result_lines.append("-" * 80)
        
        # Add data rows
        for result in results['results']:
            row_data = []
            for field in fields:
                if "." in field:
                    parent, child = field.split(".")
                    value = str(result.get(parent, {}).get(child, ""))
                else:
                    value = str(result.get(field, ""))
                row_data.append(value)
            result_lines.append(" | ".join(row_data))
        
        return "\n".join(result_lines)
    
    except Exception as e:
        return f"Error executing GAQL query: {str(e)}"

@mcp.tool()
async def get_campaign_performance(
    customer_id: str = Field(description="Google Ads customer ID (10 digits, no dashes). Example: '9873186703'"),
    days: int = Field(default=30, description="Number of days to look back (7, 30, 90, etc.)")
) -> str:
    """
    Get campaign performance metrics for the specified time period.
    
    RECOMMENDED WORKFLOW:
    1. First run list_accounts() to get available account IDs
    2. Then run get_account_currency() to see what currency the account uses
    3. Finally run this command to get campaign performance
    
    Args:
        customer_id: The Google Ads customer ID as a string (10 digits, no dashes)
        days: Number of days to look back (default: 30)
        
    Returns:
        Formatted table of campaign performance data
        
    Note:
        Cost values are in micros (millionths) of the account currency
        (e.g., 1000000 = 1 USD in a USD account)
        
    Example:
        customer_id: "1234567890"
        days: 14
    """
    query = f"""
        SELECT
            campaign.id,
            campaign.name,
            campaign.status,
            metrics.impressions,
            metrics.clicks,
            metrics.cost_micros,
            metrics.conversions,
            metrics.average_cpc
        FROM campaign
        WHERE segments.date DURING LAST_{days}_DAYS
        ORDER BY metrics.cost_micros DESC
        LIMIT 50
    """
    
    return await execute_gaql_query(customer_id, query)

@mcp.tool()
async def get_ad_performance(
    customer_id: str = Field(description="Google Ads customer ID (10 digits, no dashes). Example: '9873186703'"),
    days: int = Field(default=30, description="Number of days to look back (7, 30, 90, etc.)")
) -> str:
    """
    Get ad performance metrics for the specified time period.
    
    RECOMMENDED WORKFLOW:
    1. First run list_accounts() to get available account IDs
    2. Then run get_account_currency() to see what currency the account uses
    3. Finally run this command to get ad performance
    
    Args:
        customer_id: The Google Ads customer ID as a string (10 digits, no dashes)
        days: Number of days to look back (default: 30)
        
    Returns:
        Formatted table of ad performance data
        
    Note:
        Cost values are in micros (millionths) of the account currency
        (e.g., 1000000 = 1 USD in a USD account)
        
    Example:
        customer_id: "1234567890"
        days: 14
    """
    query = f"""
        SELECT
            ad_group_ad.ad.id,
            ad_group_ad.ad.name,
            ad_group_ad.status,
            campaign.name,
            ad_group.name,
            metrics.impressions,
            metrics.clicks,
            metrics.cost_micros,
            metrics.conversions
        FROM ad_group_ad
        WHERE segments.date DURING LAST_{days}_DAYS
        ORDER BY metrics.impressions DESC
        LIMIT 50
    """
    
    return await execute_gaql_query(customer_id, query)

@mcp.tool()
async def run_gaql(
    customer_id: str = Field(description="Google Ads customer ID (10 digits, no dashes). Example: '9873186703'"),
    query: str = Field(description="Valid GAQL query string following Google Ads Query Language syntax"),
    format: str = Field(default="table", description="Output format: 'table', 'json', or 'csv'")
) -> str:
    """
    Execute any arbitrary GAQL (Google Ads Query Language) query with custom formatting options.
    
    This is the most powerful tool for custom Google Ads data queries.
    
    Args:
        customer_id: The Google Ads customer ID as a string (10 digits, no dashes)
        query: The GAQL query to execute (any valid GAQL query)
        format: Output format ("table", "json", or "csv")
    
    Returns:
        Query results in the requested format
    
    EXAMPLE QUERIES:
    
    1. Basic campaign metrics:
        SELECT 
          campaign.name, 
          metrics.clicks, 
          metrics.impressions,
          metrics.cost_micros
        FROM campaign 
        WHERE segments.date DURING LAST_7_DAYS
    
    2. Ad group performance:
        SELECT 
          ad_group.name, 
          metrics.conversions, 
          metrics.cost_micros,
          campaign.name
        FROM ad_group 
        WHERE metrics.clicks > 100
    
    3. Keyword analysis:
        SELECT 
          keyword.text, 
          metrics.average_position, 
          metrics.ctr
        FROM keyword_view 
        ORDER BY metrics.impressions DESC
        
    4. Get conversion data:
        SELECT
          campaign.name,
          metrics.conversions,
          metrics.conversions_value,
          metrics.cost_micros
        FROM campaign
        WHERE segments.date DURING LAST_30_DAYS
        
            Note:
        Cost values are in micros (millionths) of the account currency
        (e.g., 1000000 = 1 USD in a USD account)
    """
    try:
        creds = get_credentials()
        headers = get_headers(creds)
        
        formatted_customer_id = format_customer_id(customer_id)
        url = f"https://googleads.googleapis.com/{API_VERSION}/customers/{formatted_customer_id}/googleAds:search"
        
        payload = {"query": query}
        response = requests.post(url, headers=headers, json=payload)
        
        if response.status_code != 200:
            return f"Error executing query: {response.text}"
        
        results = response.json()
        if not results.get('results'):
            return "No results found for the query."
        
        if format.lower() == "json":
            return json.dumps(results, indent=2)
        
        elif format.lower() == "csv":
            # Get field names from the first result
            fields = []
            first_result = results['results'][0]
            for key, value in first_result.items():
                if isinstance(value, dict):
                    for subkey in value:
                        fields.append(f"{key}.{subkey}")
                else:
                    fields.append(key)
            
            # Create CSV string
            csv_lines = [",".join(fields)]
            for result in results['results']:
                row_data = []
                for field in fields:
                    if "." in field:
                        parent, child = field.split(".")
                        value = str(result.get(parent, {}).get(child, "")).replace(",", ";")
                    else:
                        value = str(result.get(field, "")).replace(",", ";")
                    row_data.append(value)
                csv_lines.append(",".join(row_data))
            
            return "\n".join(csv_lines)
        
        else:  # default table format
            result_lines = [f"Query Results for Account {formatted_customer_id}:"]
            result_lines.append("-" * 100)
            
            # Get field names and maximum widths
            fields = []
            field_widths = {}
            first_result = results['results'][0]
            
            for key, value in first_result.items():
                if isinstance(value, dict):
                    for subkey in value:
                        field = f"{key}.{subkey}"
                        fields.append(field)
                        field_widths[field] = len(field)
                else:
                    fields.append(key)
                    field_widths[key] = len(key)
            
            # Calculate maximum field widths
            for result in results['results']:
                for field in fields:
                    if "." in field:
                        parent, child = field.split(".")
                        value = str(result.get(parent, {}).get(child, ""))
                    else:
                        value = str(result.get(field, ""))
                    field_widths[field] = max(field_widths[field], len(value))
            
            # Create formatted header
            header = " | ".join(f"{field:{field_widths[field]}}" for field in fields)
            result_lines.append(header)
            result_lines.append("-" * len(header))
            
            # Add data rows
            for result in results['results']:
                row_data = []
                for field in fields:
                    if "." in field:
                        parent, child = field.split(".")
                        value = str(result.get(parent, {}).get(child, ""))
                    else:
                        value = str(result.get(field, ""))
                    row_data.append(f"{value:{field_widths[field]}}")
                result_lines.append(" | ".join(row_data))
            
            return "\n".join(result_lines)
    
    except Exception as e:
        return f"Error executing GAQL query: {str(e)}"

@mcp.tool()
async def get_ad_creatives(
    customer_id: str = Field(description="Google Ads customer ID (10 digits, no dashes). Example: '9873186703'")
) -> str:
    """
    Get ad creative details including headlines, descriptions, and URLs.
    
    This tool retrieves the actual ad content (headlines, descriptions) 
    for review and analysis. Great for creative audits.
    
    RECOMMENDED WORKFLOW:
    1. First run list_accounts() to get available account IDs
    2. Then run this command with the desired account ID
    
    Args:
        customer_id: The Google Ads customer ID as a string (10 digits, no dashes)
        
    Returns:
        Formatted list of ad creative details
        
    Example:
        customer_id: "1234567890"
    """
    query = """
        SELECT
            ad_group_ad.ad.id,
            ad_group_ad.ad.name,
            ad_group_ad.ad.type,
            ad_group_ad.ad.final_urls,
            ad_group_ad.status,
            ad_group_ad.ad.responsive_search_ad.headlines,
            ad_group_ad.ad.responsive_search_ad.descriptions,
            ad_group.name,
            campaign.name
        FROM ad_group_ad
        WHERE ad_group_ad.status != 'REMOVED'
        ORDER BY campaign.name, ad_group.name
        LIMIT 50
    """
    
    try:
        creds = get_credentials()
        headers = get_headers(creds)
        
        formatted_customer_id = format_customer_id(customer_id)
        url = f"https://googleads.googleapis.com/{API_VERSION}/customers/{formatted_customer_id}/googleAds:search"
        
        payload = {"query": query}
        response = requests.post(url, headers=headers, json=payload)
        
        if response.status_code != 200:
            return f"Error retrieving ad creatives: {response.text}"
        
        results = response.json()
        if not results.get('results'):
            return "No ad creatives found for this customer ID."
        
        # Format the results in a readable way
        output_lines = [f"Ad Creatives for Customer ID {formatted_customer_id}:"]
        output_lines.append("=" * 80)
        
        for i, result in enumerate(results['results'], 1):
            ad = result.get('adGroupAd', {}).get('ad', {})
            ad_group = result.get('adGroup', {})
            campaign = result.get('campaign', {})
            
            output_lines.append(f"\n{i}. Campaign: {campaign.get('name', 'N/A')}")
            output_lines.append(f"   Ad Group: {ad_group.get('name', 'N/A')}")
            output_lines.append(f"   Ad ID: {ad.get('id', 'N/A')}")
            output_lines.append(f"   Ad Name: {ad.get('name', 'N/A')}")
            output_lines.append(f"   Status: {result.get('adGroupAd', {}).get('status', 'N/A')}")
            output_lines.append(f"   Type: {ad.get('type', 'N/A')}")
            
            # Handle Responsive Search Ads
            rsa = ad.get('responsiveSearchAd', {})
            if rsa:
                if 'headlines' in rsa:
                    output_lines.append("   Headlines:")
                    for headline in rsa['headlines']:
                        output_lines.append(f"     - {headline.get('text', 'N/A')}")
                
                if 'descriptions' in rsa:
                    output_lines.append("   Descriptions:")
                    for desc in rsa['descriptions']:
                        output_lines.append(f"     - {desc.get('text', 'N/A')}")
            
            # Handle Final URLs
            final_urls = ad.get('finalUrls', [])
            if final_urls:
                output_lines.append(f"   Final URLs: {', '.join(final_urls)}")
            
            output_lines.append("-" * 80)
        
        return "\n".join(output_lines)
    
    except Exception as e:
        return f"Error retrieving ad creatives: {str(e)}"

@mcp.tool()
async def get_account_currency(
    customer_id: str = Field(description="Google Ads customer ID (10 digits, no dashes). Example: '9873186703'")
) -> str:
    """
    Retrieve the default currency code used by the Google Ads account.
    
    IMPORTANT: Run this first before analyzing cost data to understand which currency
    the account uses. Cost values are always displayed in the account's currency.
    
    Args:
        customer_id: The Google Ads customer ID as a string (10 digits, no dashes)
    
    Returns:
        The account's default currency code (e.g., 'USD', 'EUR', 'GBP')
        
    Example:
        customer_id: "1234567890"
    """
    query = """
        SELECT
            customer.id,
            customer.currency_code
        FROM customer
        LIMIT 1
    """
    
    try:
        creds = get_credentials()
        
        # Force refresh if needed
        if not creds.valid:
            logger.info("Credentials not valid, attempting refresh...")
            if hasattr(creds, 'refresh_token') and creds.refresh_token:
                creds.refresh(Request())
                logger.info("Credentials refreshed successfully")
            else:
                raise ValueError("Invalid credentials and no refresh token available")
        
        headers = get_headers(creds)
        
        formatted_customer_id = format_customer_id(customer_id)
        url = f"https://googleads.googleapis.com/{API_VERSION}/customers/{formatted_customer_id}/googleAds:search"
        
        payload = {"query": query}
        response = requests.post(url, headers=headers, json=payload)
        
        if response.status_code != 200:
            return f"Error retrieving account currency: {response.text}"
        
        results = response.json()
        if not results.get('results'):
            return "No account information found for this customer ID."
        
        # Extract the currency code from the results
        customer = results['results'][0].get('customer', {})
        currency_code = customer.get('currencyCode', 'Not specified')
        
        return f"Account {formatted_customer_id} uses currency: {currency_code}"
    
    except Exception as e:
        logger.error(f"Error retrieving account currency: {str(e)}")
        return f"Error retrieving account currency: {str(e)}"

@mcp.resource("gaql://reference")
def gaql_reference() -> str:
    """Google Ads Query Language (GAQL) reference documentation."""
    return """
    # Google Ads Query Language (GAQL) Reference
    
    GAQL is similar to SQL but with specific syntax for Google Ads. Here's a quick reference:
    
    ## Basic Query Structure
    ```
    SELECT field1, field2, ... 
    FROM resource_type
    WHERE condition
    ORDER BY field [ASC|DESC]
    LIMIT n
    ```
    
    ## Common Field Types
    
    ### Resource Fields
    - campaign.id, campaign.name, campaign.status
    - ad_group.id, ad_group.name, ad_group.status
    - ad_group_ad.ad.id, ad_group_ad.ad.final_urls
    - keyword.text, keyword.match_type
    
    ### Metric Fields
    - metrics.impressions
    - metrics.clicks
    - metrics.cost_micros
    - metrics.conversions
    - metrics.ctr
    - metrics.average_cpc
    
    ### Segment Fields
    - segments.date
    - segments.device
    - segments.day_of_week
    
    ## Common WHERE Clauses
    
    ### Date Ranges
    - WHERE segments.date DURING LAST_7_DAYS
    - WHERE segments.date DURING LAST_30_DAYS
    - WHERE segments.date BETWEEN '2023-01-01' AND '2023-01-31'
    
    ### Filtering
    - WHERE campaign.status = 'ENABLED'
    - WHERE metrics.clicks > 100
    - WHERE campaign.name LIKE '%Brand%'
    
    ## Tips
    - Always check account currency before analyzing cost data
    - Cost values are in micros (millionths): 1000000 = 1 unit of currency
    - Use LIMIT to avoid large result sets
    """

@mcp.prompt("google_ads_workflow")
def google_ads_workflow() -> str:
    """Provides guidance on the recommended workflow for using Google Ads tools."""
    return """
    I'll help you analyze your Google Ads account data. Here's the recommended workflow:
    
    1. First, let's list all the accounts you have access to:
       - Run the `list_accounts()` tool to get available account IDs
    
    2. Before analyzing cost data, let's check which currency the account uses:
       - Run `get_account_currency(customer_id="ACCOUNT_ID")` with your selected account
    
    3. Now we can explore the account data:
       - For campaign performance: `get_campaign_performance(customer_id="ACCOUNT_ID", days=30)`
       - For ad performance: `get_ad_performance(customer_id="ACCOUNT_ID", days=30)`
       - For ad creative review: `get_ad_creatives(customer_id="ACCOUNT_ID")`
    
    4. For custom queries, use the GAQL query tool:
       - `run_gaql(customer_id="ACCOUNT_ID", query="YOUR_QUERY", format="table")`
    
    5. Let me know if you have specific questions about:
       - Campaign performance
       - Ad performance
       - Keywords
       - Budgets
       - Conversions
    
    Important: Always provide the customer_id as a string.
    For example: customer_id="1234567890"
    """

@mcp.prompt("gaql_help")
def gaql_help() -> str:
    """Provides assistance for writing GAQL queries."""
    return """
    I'll help you write a Google Ads Query Language (GAQL) query. Here are some examples to get you started:
    
    ## Get campaign performance last 30 days
    ```
    SELECT
      campaign.id,
      campaign.name,
      campaign.status,
      metrics.impressions,
      metrics.clicks,
      metrics.cost_micros,
      metrics.conversions
    FROM campaign
    WHERE segments.date DURING LAST_30_DAYS
    ORDER BY metrics.cost_micros DESC
    ```
    
    ## Get keyword performance
    ```
    SELECT
      keyword.text,
      keyword.match_type,
      metrics.impressions,
      metrics.clicks,
      metrics.cost_micros,
      metrics.conversions
    FROM keyword_view
    WHERE segments.date DURING LAST_30_DAYS
    ORDER BY metrics.clicks DESC
    ```
    
    ## Get ads with poor performance
    ```
    SELECT
      ad_group_ad.ad.id,
      ad_group_ad.ad.name,
      campaign.name,
      ad_group.name,
      metrics.impressions,
      metrics.clicks,
      metrics.conversions
    FROM ad_group_ad
    WHERE 
      segments.date DURING LAST_30_DAYS
      AND metrics.impressions > 1000
      AND metrics.ctr < 0.01
    ORDER BY metrics.impressions DESC
    ```
    
    Once you've chosen a query, use it with:
    ```
    run_gaql(customer_id="YOUR_ACCOUNT_ID", query="YOUR_QUERY_HERE")
    ```
    
    Remember:
    - Always provide the customer_id as a string
    - Cost values are in micros (1,000,000 = 1 unit of currency)
    - Use LIMIT to avoid large result sets
    - Check the account currency before analyzing cost data
    """

@mcp.tool()
async def get_image_assets(
    customer_id: str = Field(description="Google Ads customer ID (10 digits, no dashes). Example: '9873186703'"),
    limit: int = Field(default=50, description="Maximum number of image assets to return")
) -> str:
    """
    Retrieve all image assets in the account including their full-size URLs.
    
    This tool allows you to get details about image assets used in your Google Ads account,
    including the URLs to download the full-size images for further processing or analysis.
    
    RECOMMENDED WORKFLOW:
    1. First run list_accounts() to get available account IDs
    2. Then run this command with the desired account ID
    
    Args:
        customer_id: The Google Ads customer ID as a string (10 digits, no dashes)
        limit: Maximum number of image assets to return (default: 50)
        
    Returns:
        Formatted list of image assets with their download URLs
        
    Example:
        customer_id: "1234567890"
        limit: 100
    """
    query = f"""
        SELECT
            asset.id,
            asset.name,
            asset.type,
            asset.image_asset.full_size.url,
            asset.image_asset.full_size.height_pixels,
            asset.image_asset.full_size.width_pixels,
            asset.image_asset.file_size
        FROM
            asset
        WHERE
            asset.type = 'IMAGE'
        LIMIT {limit}
    """
    
    try:
        creds = get_credentials()
        headers = get_headers(creds)
        
        formatted_customer_id = format_customer_id(customer_id)
        url = f"https://googleads.googleapis.com/{API_VERSION}/customers/{formatted_customer_id}/googleAds:search"
        
        payload = {"query": query}
        response = requests.post(url, headers=headers, json=payload)
        
        if response.status_code != 200:
            return f"Error retrieving image assets: {response.text}"
        
        results = response.json()
        if not results.get('results'):
            return "No image assets found for this customer ID."
        
        # Format the results in a readable way
        output_lines = [f"Image Assets for Customer ID {formatted_customer_id}:"]
        output_lines.append("=" * 80)
        
        for i, result in enumerate(results['results'], 1):
            asset = result.get('asset', {})
            image_asset = asset.get('imageAsset', {})
            full_size = image_asset.get('fullSize', {})
            
            output_lines.append(f"\n{i}. Asset ID: {asset.get('id', 'N/A')}")
            output_lines.append(f"   Name: {asset.get('name', 'N/A')}")
            
            if full_size:
                output_lines.append(f"   Image URL: {full_size.get('url', 'N/A')}")
                output_lines.append(f"   Dimensions: {full_size.get('widthPixels', 'N/A')} x {full_size.get('heightPixels', 'N/A')} px")
            
            file_size = image_asset.get('fileSize', 'N/A')
            if file_size != 'N/A':
                # Convert to KB for readability
                file_size_kb = int(file_size) / 1024
                output_lines.append(f"   File Size: {file_size_kb:.2f} KB")
            
            output_lines.append("-" * 80)
        
        return "\n".join(output_lines)
    
    except Exception as e:
        return f"Error retrieving image assets: {str(e)}"

@mcp.tool()
async def download_image_asset(
    customer_id: str = Field(description="Google Ads customer ID (10 digits, no dashes). Example: '9873186703'"),
    asset_id: str = Field(description="The ID of the image asset to download"),
    output_dir: str = Field(default="./ad_images", description="Directory to save the downloaded image")
) -> str:
    """
    Download a specific image asset from a Google Ads account.
    
    This tool allows you to download the full-size version of an image asset
    for further processing, analysis, or backup.
    
    RECOMMENDED WORKFLOW:
    1. First run list_accounts() to get available account IDs
    2. Then run get_image_assets() to get available image asset IDs
    3. Finally use this command to download specific images
    
    Args:
        customer_id: The Google Ads customer ID as a string (10 digits, no dashes)
        asset_id: The ID of the image asset to download
        output_dir: Directory where the image should be saved (default: ./ad_images)
        
    Returns:
        Status message indicating success or failure of the download
        
    Example:
        customer_id: "1234567890"
        asset_id: "12345"
        output_dir: "./my_ad_images"
    """
    query = f"""
        SELECT
            asset.id,
            asset.name,
            asset.image_asset.full_size.url
        FROM
            asset
        WHERE
            asset.type = 'IMAGE'
            AND asset.id = {asset_id}
        LIMIT 1
    """
    
    try:
        creds = get_credentials()
        headers = get_headers(creds)
        
        formatted_customer_id = format_customer_id(customer_id)
        url = f"https://googleads.googleapis.com/{API_VERSION}/customers/{formatted_customer_id}/googleAds:search"
        
        payload = {"query": query}
        response = requests.post(url, headers=headers, json=payload)
        
        if response.status_code != 200:
            return f"Error retrieving image asset: {response.text}"
        
        results = response.json()
        if not results.get('results'):
            return f"No image asset found with ID {asset_id}"
        
        # Extract the image URL
        asset = results['results'][0].get('asset', {})
        image_url = asset.get('imageAsset', {}).get('fullSize', {}).get('url')
        asset_name = asset.get('name', f"image_{asset_id}")
        
        if not image_url:
            return f"No download URL found for image asset ID {asset_id}"
        
        # Validate and sanitize the output directory to prevent path traversal
        try:
            # Get the base directory (current working directory)
            base_dir = Path.cwd()
            # Resolve the output directory to an absolute path
            resolved_output_dir = Path(output_dir).resolve()
            
            # Ensure the resolved path is within or under the current working directory
            # This prevents path traversal attacks like "../../../etc"
            try:
                resolved_output_dir.relative_to(base_dir)
            except ValueError:
                # If the path is not relative to base_dir, use the default safe directory
                resolved_output_dir = base_dir / "ad_images"
                logger.warning(f"Invalid output directory '{output_dir}' - using default './ad_images'")
            
            # Create output directory if it doesn't exist
            resolved_output_dir.mkdir(parents=True, exist_ok=True)
            
        except Exception as e:
            return f"Error creating output directory: {str(e)}"
        
        # Download the image
        image_response = requests.get(image_url)
        if image_response.status_code != 200:
            return f"Failed to download image: HTTP {image_response.status_code}"
        
        # Clean the filename to be safe for filesystem
        safe_name = ''.join(c for c in asset_name if c.isalnum() or c in ' ._-')
        filename = f"{asset_id}_{safe_name}.jpg"
        file_path = resolved_output_dir / filename
        
        # Save the image
        with open(file_path, 'wb') as f:
            f.write(image_response.content)
        
        return f"Successfully downloaded image asset {asset_id} to {file_path}"
    
    except Exception as e:
        return f"Error downloading image asset: {str(e)}"

@mcp.tool()
async def get_asset_usage(
    customer_id: str = Field(description="Google Ads customer ID (10 digits, no dashes). Example: '9873186703'"),
    asset_id: str = Field(default=None, description="Optional: specific asset ID to look up (leave empty to get all image assets)"),
    asset_type: str = Field(default="IMAGE", description="Asset type to search for ('IMAGE', 'TEXT', 'VIDEO', etc.)")
) -> str:
    """
    Find where specific assets are being used in campaigns, ad groups, and ads.
    
    This tool helps you analyze how assets are linked to campaigns and ads across your account,
    which is useful for creative analysis and optimization.
    
    RECOMMENDED WORKFLOW:
    1. First run list_accounts() to get available account IDs
    2. Run get_image_assets() to see available assets
    3. Use this command to see where specific assets are used
    
    Args:
        customer_id: The Google Ads customer ID as a string (10 digits, no dashes)
        asset_id: Optional specific asset ID to look up (leave empty to get all assets of the specified type)
        asset_type: Type of asset to search for (default: 'IMAGE')
        
    Returns:
        Formatted report showing where assets are used in the account
        
    Example:
        customer_id: "1234567890"
        asset_id: "12345"
        asset_type: "IMAGE"
    """
    # Build the query based on whether a specific asset ID was provided
    where_clause = f"asset.type = '{asset_type}'"
    if asset_id:
        where_clause += f" AND asset.id = {asset_id}"
    
    # First get the assets themselves
    assets_query = f"""
        SELECT
            asset.id,
            asset.name,
            asset.type
        FROM
            asset
        WHERE
            {where_clause}
        LIMIT 100
    """
    
    # Then get the associations between assets and campaigns/ad groups
    # Try using campaign_asset instead of asset_link
    associations_query = f"""
        SELECT
            campaign.id,
            campaign.name,
            asset.id,
            asset.name,
            asset.type
        FROM
            campaign_asset
        WHERE
            {where_clause}
        LIMIT 500
    """

    # Also try ad_group_asset for ad group level information
    ad_group_query = f"""
        SELECT
            ad_group.id,
            ad_group.name,
            asset.id,
            asset.name,
            asset.type
        FROM
            ad_group_asset
        WHERE
            {where_clause}
        LIMIT 500
    """
    
    try:
        creds = get_credentials()
        headers = get_headers(creds)
        
        formatted_customer_id = format_customer_id(customer_id)
        
        # First get the assets
        url = f"https://googleads.googleapis.com/{API_VERSION}/customers/{formatted_customer_id}/googleAds:search"
        payload = {"query": assets_query}
        assets_response = requests.post(url, headers=headers, json=payload)
        
        if assets_response.status_code != 200:
            return f"Error retrieving assets: {assets_response.text}"
        
        assets_results = assets_response.json()
        if not assets_results.get('results'):
            return f"No {asset_type} assets found for this customer ID."
        
        # Now get the associations
        payload = {"query": associations_query}
        assoc_response = requests.post(url, headers=headers, json=payload)
        
        if assoc_response.status_code != 200:
            return f"Error retrieving asset associations: {assoc_response.text}"
        
        assoc_results = assoc_response.json()
        
        # Format the results in a readable way
        output_lines = [f"Asset Usage for Customer ID {formatted_customer_id}:"]
        output_lines.append("=" * 80)
        
        # Create a dictionary to organize asset usage by asset ID
        asset_usage = {}
        
        # Initialize the asset usage dictionary with basic asset info
        for result in assets_results.get('results', []):
            asset = result.get('asset', {})
            asset_id = asset.get('id')
            if asset_id:
                asset_usage[asset_id] = {
                    'name': asset.get('name', 'Unnamed asset'),
                    'type': asset.get('type', 'Unknown'),
                    'usage': []
                }
        
        # Add usage information from the associations
        for result in assoc_results.get('results', []):
            asset = result.get('asset', {})
            asset_id = asset.get('id')
            
            if asset_id and asset_id in asset_usage:
                campaign = result.get('campaign', {})
                ad_group = result.get('adGroup', {})
                ad = result.get('adGroupAd', {}).get('ad', {}) if 'adGroupAd' in result else {}
                asset_link = result.get('assetLink', {})
                
                usage_info = {
                    'campaign_id': campaign.get('id', 'N/A'),
                    'campaign_name': campaign.get('name', 'N/A'),
                    'ad_group_id': ad_group.get('id', 'N/A'),
                    'ad_group_name': ad_group.get('name', 'N/A'),
                    'ad_id': ad.get('id', 'N/A') if ad else 'N/A',
                    'ad_name': ad.get('name', 'N/A') if ad else 'N/A'
                }
                
                asset_usage[asset_id]['usage'].append(usage_info)
        
        # Format the output
        for asset_id, info in asset_usage.items():
            output_lines.append(f"\nAsset ID: {asset_id}")
            output_lines.append(f"Name: {info['name']}")
            output_lines.append(f"Type: {info['type']}")
            
            if info['usage']:
                output_lines.append("\nUsed in:")
                output_lines.append("-" * 60)
                output_lines.append(f"{'Campaign':<30} | {'Ad Group':<30}")
                output_lines.append("-" * 60)
                
                for usage in info['usage']:
                    campaign_str = f"{usage['campaign_name']} ({usage['campaign_id']})"
                    ad_group_str = f"{usage['ad_group_name']} ({usage['ad_group_id']})"
                    
                    output_lines.append(f"{campaign_str[:30]:<30} | {ad_group_str[:30]:<30}")
            
            output_lines.append("=" * 80)
        
        return "\n".join(output_lines)
    
    except Exception as e:
        return f"Error retrieving asset usage: {str(e)}"

@mcp.tool()
async def analyze_image_assets(
    customer_id: str = Field(description="Google Ads customer ID (10 digits, no dashes). Example: '9873186703'"),
    days: int = Field(default=30, description="Number of days to look back (7, 30, 90, etc.)")
) -> str:
    """
    Analyze image assets with their performance metrics across campaigns.
    
    This comprehensive tool helps you understand which image assets are performing well
    by showing metrics like impressions, clicks, and conversions for each image.
    
    RECOMMENDED WORKFLOW:
    1. First run list_accounts() to get available account IDs
    2. Then run get_account_currency() to see what currency the account uses
    3. Finally run this command to analyze image asset performance
    
    Args:
        customer_id: The Google Ads customer ID as a string (10 digits, no dashes)
        days: Number of days to look back (default: 30)
        
    Returns:
        Detailed report of image assets and their performance metrics
        
    Example:
        customer_id: "1234567890"
        days: 14
    """
    # Make sure to use a valid date range format
    # Valid formats are: LAST_7_DAYS, LAST_14_DAYS, LAST_30_DAYS, etc. (with underscores)
    if days == 7:
        date_range = "LAST_7_DAYS"
    elif days == 14:
        date_range = "LAST_14_DAYS"
    elif days == 30:
        date_range = "LAST_30_DAYS"
    else:
        # Default to 30 days if not a standard range
        date_range = "LAST_30_DAYS"
        
    query = f"""
        SELECT
            asset.id,
            asset.name,
            asset.image_asset.full_size.url,
            asset.image_asset.full_size.width_pixels,
            asset.image_asset.full_size.height_pixels,
            campaign.name,
            metrics.impressions,
            metrics.clicks,
            metrics.conversions,
            metrics.cost_micros
        FROM
            campaign_asset
        WHERE
            asset.type = 'IMAGE'
            AND segments.date DURING LAST_30_DAYS
        ORDER BY
            metrics.impressions DESC
        LIMIT 200
    """
    
    try:
        creds = get_credentials()
        headers = get_headers(creds)
        
        formatted_customer_id = format_customer_id(customer_id)
        url = f"https://googleads.googleapis.com/{API_VERSION}/customers/{formatted_customer_id}/googleAds:search"
        
        payload = {"query": query}
        response = requests.post(url, headers=headers, json=payload)
        
        if response.status_code != 200:
            return f"Error analyzing image assets: {response.text}"
        
        results = response.json()
        if not results.get('results'):
            return "No image asset performance data found for this customer ID and time period."
        
        # Group results by asset ID
        assets_data = {}
        for result in results.get('results', []):
            asset = result.get('asset', {})
            asset_id = asset.get('id')
            
            if asset_id not in assets_data:
                assets_data[asset_id] = {
                    'name': asset.get('name', f"Asset {asset_id}"),
                    'url': asset.get('imageAsset', {}).get('fullSize', {}).get('url', 'N/A'),
                    'dimensions': f"{asset.get('imageAsset', {}).get('fullSize', {}).get('widthPixels', 'N/A')} x {asset.get('imageAsset', {}).get('fullSize', {}).get('heightPixels', 'N/A')}",
                    'impressions': 0,
                    'clicks': 0,
                    'conversions': 0,
                    'cost_micros': 0,
                    'campaigns': set(),
                    'ad_groups': set()
                }
            
            # Aggregate metrics
            metrics = result.get('metrics', {})
            assets_data[asset_id]['impressions'] += int(metrics.get('impressions', 0))
            assets_data[asset_id]['clicks'] += int(metrics.get('clicks', 0))
            assets_data[asset_id]['conversions'] += float(metrics.get('conversions', 0))
            assets_data[asset_id]['cost_micros'] += int(metrics.get('costMicros', 0))
            
            # Add campaign and ad group info
            campaign = result.get('campaign', {})
            ad_group = result.get('adGroup', {})
            
            if campaign.get('name'):
                assets_data[asset_id]['campaigns'].add(campaign.get('name'))
            if ad_group.get('name'):
                assets_data[asset_id]['ad_groups'].add(ad_group.get('name'))
        
        # Format the results
        output_lines = [f"Image Asset Performance Analysis for Customer ID {formatted_customer_id} (Last {days} days):"]
        output_lines.append("=" * 100)
        
        # Sort assets by impressions (highest first)
        sorted_assets = sorted(assets_data.items(), key=lambda x: x[1]['impressions'], reverse=True)
        
        for asset_id, data in sorted_assets:
            output_lines.append(f"\nAsset ID: {asset_id}")
            output_lines.append(f"Name: {data['name']}")
            output_lines.append(f"Dimensions: {data['dimensions']}")
            
            # Calculate CTR if there are impressions
            ctr = (data['clicks'] / data['impressions'] * 100) if data['impressions'] > 0 else 0
            
            # Format metrics
            output_lines.append(f"\nPerformance Metrics:")
            output_lines.append(f"  Impressions: {data['impressions']:,}")
            output_lines.append(f"  Clicks: {data['clicks']:,}")
            output_lines.append(f"  CTR: {ctr:.2f}%")
            output_lines.append(f"  Conversions: {data['conversions']:.2f}")
            output_lines.append(f"  Cost (micros): {data['cost_micros']:,}")
            
            # Show where it's used
            output_lines.append(f"\nUsed in {len(data['campaigns'])} campaigns:")
            for campaign in list(data['campaigns'])[:5]:  # Show first 5 campaigns
                output_lines.append(f"  - {campaign}")
            if len(data['campaigns']) > 5:
                output_lines.append(f"  - ... and {len(data['campaigns']) - 5} more")
            
            # Add URL
            if data['url'] != 'N/A':
                output_lines.append(f"\nImage URL: {data['url']}")
            
            output_lines.append("-" * 100)
        
        return "\n".join(output_lines)
    
    except Exception as e:
        return f"Error analyzing image assets: {str(e)}"

@mcp.tool()
async def list_resources(
    customer_id: str = Field(description="Google Ads customer ID (10 digits, no dashes). Example: '9873186703'")
) -> str:
    """
    List valid resources that can be used in GAQL FROM clauses.
    
    Args:
        customer_id: The Google Ads customer ID as a string
        
    Returns:
        Formatted list of valid resources
    """
    # Example query that lists some common resources
    # This might need to be adjusted based on what's available in your API version
    query = """
        SELECT
            google_ads_field.name,
            google_ads_field.category,
            google_ads_field.data_type
        FROM
            google_ads_field
        WHERE
            google_ads_field.category = 'RESOURCE'
        ORDER BY
            google_ads_field.name
    """
    
    # Use your existing run_gaql function to execute this query
    return await run_gaql(customer_id, query)

if __name__ == "__main__":
    # Start the MCP server on stdio transport
    mcp.run(transport="stdio")

```