Google Ads MCP
# Google Ads MCP
> Manage Google Ads campaigns through an AI assistant — safely, with every write gated behind a preview and an audit log.
[](https://github.com/LucasSantana-Dev/google-ads-mcp/actions/workflows/test.yml)
[](https://www.python.org/downloads/)
[](LICENSE)
A [Model Context Protocol](https://modelcontextprotocol.io) server that exposes Google Ads API v24 operations as tools for Claude (or any MCP client). Read campaign metrics, search terms, and recommendations; make controlled changes to bids, budgets, statuses, and creative — all from a single conversation.
---
## Features
31 tools across five capability layers:
### Read-only reporting
| Tool | What it does |
|------|-------------|
| `list_accessible_customers` | List all accounts under the MCC |
| `get_campaigns` | List campaigns with status and channel type |
| `get_campaign_performance` | Clicks, impressions, cost, conversions, CTR |
| `get_ad_groups` | List ad groups with status |
| `get_keywords` | Keywords with match type and status |
| `get_search_terms` | Actual search queries with performance metrics |
| `get_change_events` | Account change history |
| `run_gaql_query` | Execute any read-only GAQL query (up to 10k rows) |
| `get_segments_metadata` | Available GAQL segments |
| `get_metrics_metadata` | Available GAQL metrics |
| `get_resource_metadata` | Fields for any GAQL resource |
### Recommendations & planning
| Tool | What it does |
|------|-------------|
| `get_recommendations` | Active Google Ads recommendations for an account or campaign |
| `generate_keyword_ideas` | Keyword ideas from seed keywords or a URL |
| `get_forecast_metrics` | Bid simulations and forecast data |
| `apply_recommendation` | Apply a recommendation (gated) |
| `dismiss_recommendation` | Dismiss a recommendation (gated) |
### Status writes (gated)
| Tool | What it does |
|------|-------------|
| `pause_campaign` / `enable_campaign` | Toggle campaign status |
| `pause_ad_group` / `enable_ad_group` | Toggle ad group status |
| `pause_keyword` / `enable_keyword` | Toggle keyword status |
| `pause_ad` / `enable_ad` | Toggle ad status |
### Bid & budget writes (gated + capped)
| Tool | What it does |
|------|-------------|
| `update_keyword_bid` | Set keyword max CPC — capped at ±25% by default |
| `update_ad_group_bid` | Set ad group default CPC — capped |
| `update_campaign_budget` | Set daily budget — capped at ±20% by default |
### Entity creation (gated)
| Tool | What it does |
|------|-------------|
| `create_campaign` | Create campaign + shared budget atomically (starts paused) |
| `create_ad_group` | Create ad group under a campaign |
| `create_keyword` | Add keyword to an ad group |
| `create_ad` | Create a responsive search ad (starts paused) |
---
## Safety model
Every write passes through four sequential gates:
```
1. Allowlist — customer_id must be in GOOGLE_ADS_MUTATE_ALLOWLIST (default: deny all)
2. Cap — bid/budget changes exceeding the configured % are blocked outright
3. Preview — confirm=False (default) runs validate_only; nothing changes in the account
4. Audit — confirmed mutations are appended to an append-only JSONL log (chmod 0600)
```
New entities (campaigns, ad groups, keywords, ads) always start **paused**.
Transient quota errors (`RESOURCE_TEMPORARILY_EXHAUSTED`) are retried automatically with exponential back-off and jitter (configurable via `GOOGLE_ADS_MAX_RETRIES`, default 3).
---
## Setup
### Prerequisites
- Python 3.10+
- [uv](https://docs.astral.sh/uv/getting-started/installation/)
- A Google Ads developer token and manager account (MCC)
### Install
```bash
git clone https://github.com/LucasSantana-Dev/google-ads-mcp
cd google-ads-mcp
uv sync --extra dev
```
### Credentials
```bash
cp .env.example .env
```
| Variable | Description |
|----------|-------------|
| `GOOGLE_ADS_CLIENT_ID` | OAuth 2.0 Desktop App client ID |
| `GOOGLE_ADS_CLIENT_SECRET` | OAuth 2.0 Desktop App client secret |
| `GOOGLE_ADS_DEVELOPER_TOKEN` | Your Google Ads developer token |
| `GOOGLE_ADS_LOGIN_CUSTOMER_ID` | MCC account ID (digits only, no hyphens) |
| `GOOGLE_ADS_REFRESH_TOKEN` | Long-lived OAuth refresh token (see below) |
| `GOOGLE_ADS_MUTATE_ALLOWLIST` | Comma-separated customer IDs that may receive writes |
| `GOOGLE_ADS_AUDIT_LOG` | Audit log path (default: `audit-log.jsonl`) |
| `GOOGLE_ADS_MAX_BID_CHANGE_PCT` | Max bid change fraction (default: `0.25`) |
| `GOOGLE_ADS_MAX_BUDGET_CHANGE_PCT` | Max budget change fraction (default: `0.20`) |
| `GOOGLE_ADS_MAX_RETRIES` | Quota retry attempts (default: `3`) |
#### Mint a refresh token
```bash
GOOGLE_ADS_CLIENT_ID=... GOOGLE_ADS_CLIENT_SECRET=... \
uv run python scripts/get_refresh_token.py
```
A browser opens for OAuth consent. Copy the printed refresh token into `.env`.
---
## Claude Desktop integration
Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
```json
{
"mcpServers": {
"google-ads": {
"command": "/Users/<you>/.local/bin/uv",
"args": [
"run",
"--directory", "/path/to/google-ads-mcp",
"--env-file", "/path/to/google-ads-mcp/.env",
"python", "-m", "google_ads_mcp.server"
]
}
}
}
```
Restart Claude Desktop. The 31 tools appear automatically.
---
## Example conversations
```
# Reporting
"Show me all campaigns for account 123-456-7890 with spend > $100 this week"
"What search terms triggered our branded keywords in the last 7 days?"
"Any keyword ideas for 'project management software'?"
"What recommendations does Google have for my account?"
# Writes — always previews first; apply with confirm=true
"Pause campaign 9876543, it's burning budget"
"Set the CPC bid for keyword 111 in ad group 222 to $1.50"
"Create a new Search campaign 'Brand Q3' with a $20/day budget"
"Apply the 'add keyword' recommendation for campaign 555"
```
---
## Development
```bash
uv run pytest # 102 tests, 89% coverage
uv run ruff check src/ tests/ # lint
uv run mypy src/ # type check
```
CI runs on every push and pull request.
---
## Architecture
```
src/google_ads_mcp/
├── server.py # FastMCP entrypoint, tool registration
├── config.py # GoogleAdsClient factory (reads .env)
├── gaql.py # GAQL execution, validators, row serialization, retry
├── mutate.py # Write-safety harness (allowlist → cap → preview → audit)
├── retry.py # Exponential back-off for quota errors
├── errors.py # tool_handler wrapper → {success, error} for any exception
├── testing.py # FakeGoogleAdsClient — unit tests without network or credentials
└── tools/
├── accounts.py # list_accessible_customers
├── campaigns.py # get_campaigns, get_campaign_performance
├── adgroups.py # get_ad_groups
├── keywords.py # get_keywords, get_search_terms
├── changes.py # get_change_events
├── query.py # run_gaql_query
├── metadata.py # get_*_metadata (3 tools)
├── writes_status.py # pause/enable campaign/ad_group/keyword/ad (8 tools)
├── writes_value.py # update keyword bid, ad group bid, campaign budget (3 tools)
├── creates.py # create campaign, ad group, keyword, ad (4 tools)
└── planning.py # recommendations, keyword ideas, forecast (5 tools)
```
**Transport:** stdio only — no HTTP port, no public exposure.
**Auth:** OAuth 2.0 Desktop App + refresh token. Credentials stay on the local machine.
---
## License
MIT
TDQS
Scored across 31 tools
Every tool targets a distinct resource or action (e.g., create_ad vs. create_campaign, get_keywords vs. get_recommendations). No two tools have overlapping purposes, and even similar verbs like 'enable' and 'pause' are clearly separated by entity.
All tool names follow a consistent verb_noun pattern (e.g., create_campaign, get_keywords, pause_ad). Verbs like 'apply', 'dismiss', 'generate', 'list', 'run' are distinct and appropriately used. No mixing of casing or other conventions.
With 31 tools, the server covers a broad range of Google Ads operations without becoming unwieldy. Each tool serves a specific purpose, and the count is reasonable given the complexity of the domain.
The toolset lacks update operations for most entities (e.g., no update_ad, update_campaign, update_ad_group) and no removal tools. While raw GAQL queries can fill some gaps, the surface is incomplete for standard lifecycle management.