Content
# google-ads-mcp
An MCP server for managing Google Ads accounts. Reads via GAQL, writes via two-phase preview/apply, full v24 API coverage in ~19 tools.
> Status: alpha (v0.0.1). End-to-end use against a real account requires Basic Access on your dev token (1–3 business days from Google).
## Features
- **GAQL reads** with row + byte caps that keep responses LLM-friendly.
- **Two-phase writes.** Every mutation runs `validate_only=true` first, returns a per-operation diff and a 15-minute `mutate_id`. `apply(mutate_id)` commits; re-applying is a no-op.
- **Customer-ID allowlist.** Server refuses calls for accounts the credentials can't access. Allowlist is built from `ListAccessibleCustomers` plus sub-accounts under your manager.
- **Append-only audit log.** JSONL at `~/.local/share/google-ads-mcp/audit.log` (mode `0600`). One line per state-changing attempt, including failures and idempotent replays.
- **Layer-1 outcome tools** for the common workflows: pause/enable campaigns, ad groups, keywords; set keyword bids; set campaign budgets; add negatives; apply Google's recommendations; generate keyword ideas.
- **Layer-2 generic dispatchers** cover the entire v24 surface. `mutate` covers all 64 services that fit the unified `MutateOperation` proto. `call_read_rpc` and `call_mutate_rpc` cover the long-tail RPCs (recommendation dismiss, experiment lifecycle, conversion uploads, audience insights, MCC management, link services, etc.).
- **Async-job lifecycles.** `batch_job` and `offline_user_data_job` cover BatchJob and Customer Match / Store Sales lifecycles via single dispatcher tools.
- **Discovery via MCP resources** — `gads-schema://`, `gads-rpc-catalog://`, `gads-rpc-schema://`. Loaded on demand, no ambient cost.
- **MCP tool annotations** — every tool declares `readOnlyHint`, `destructiveHint`, `idempotentHint` so clients render confirmations correctly.
- **Local-only OAuth.** Credentials at `~/.config/google-ads-mcp/credentials.yaml` (mode `0600`). No SaaS, no per-seat anything.
## Prerequisites
Set these up before installing the server. They're Google's processes, not ours, so we can't automate them.
1. **Python 3.13** and [**uv**](https://docs.astral.sh/uv/getting-started/installation/).
2. **Google Cloud project** with the [Google Ads API enabled](https://console.cloud.google.com/apis/library/googleads.googleapis.com) and an [OAuth 2.0 "Desktop app" client](https://developers.google.com/google-ads/api/docs/oauth/cloud-project) configured. Download the client JSON — you'll point at it during setup. ~5 minutes in the Cloud Console.
3. **Developer token** from the [Google Ads API Center](https://ads.google.com/aw/apicenter). Basic Access approval takes 1–3 business days. Test-only tokens work immediately against [Google Ads test accounts](https://developers.google.com/google-ads/api/docs/best-practices/test-accounts) — recommended for development.
4. *(Optional)* **Manager (MCC) account ID** if your dev token belongs to a manager and you want to operate on its sub-accounts.
## Installation
PyPI publishing is on the roadmap but not done yet — install from source:
```sh
git clone https://github.com/ball2jh/google-ads-mcp.git
cd google-ads-mcp
uv sync
```
## Setup
### 1. Run the interactive wizard
```sh
uv run google-ads-mcp init
```
`init` will prompt for:
- The path to your OAuth client JSON
- Your developer token
- Your Manager (MCC) ID (skip if not applicable; dashes are stripped automatically)
- OAuth consent in your browser — it opens a localhost loopback flow on a free port and captures the resulting refresh token
Credentials persist to `~/.config/google-ads-mcp/credentials.yaml` (mode `0600`).
### 2. Verify
```sh
uv run google-ads-mcp validate
```
This re-runs the credential checks without redoing OAuth. If validation fails (e.g., your Cloud project hasn't enabled the Google Ads API yet), fix the upstream issue and run `validate` again — **don't re-run `init`**, that burns another OAuth refresh token.
## Connect it to your MCP client
Replace `/path/to/google-ads-mcp` below with the absolute path where you cloned this repo.
### Claude Code
```sh
claude mcp add google-ads -- uv run --directory /path/to/google-ads-mcp google-ads-mcp
```
### Codex
Add to `~/.codex/config.toml`:
```toml
[mcp_servers.google-ads]
command = "uv"
args = ["run", "--directory", "/path/to/google-ads-mcp", "google-ads-mcp"]
```
### Other MCP clients
The server runs over stdio. Point your client at the same `uv run --directory /path/to/google-ads-mcp google-ads-mcp` invocation in whatever shape its config expects.
## Tool surface
| Tool | Layer | Description |
|---|---|---|
| `gaql(customer_id, query)` | 2 | Run any GAQL `SELECT`. Capped to keep responses LLM-context-friendly. |
| `mutate(customer_id, operations)` | 2 | Generic write via `GoogleAdsService.Mutate` (64 services). Returns a previewable `mutate_id`. |
| `call_read_rpc(customer_id, service, method, params)` | 2 | Generic read RPC for the long tail — keyword ideas, reach forecasts, audience insights, benchmarks, suggestions, list_invoices, etc. |
| `call_mutate_rpc(customer_id, service, method, params)` | 2 | Generic mutating RPC — recommendation apply/dismiss, experiment lifecycle, MCC management, conversion uploads, etc. |
| `apply(mutate_id)` | 2 | Commit a previewed mutate (operations or RPC). Idempotent. |
| `pause_campaign` / `enable_campaign(customer_id, campaign_id)` | 1 | Preview pausing/enabling a campaign. |
| `pause_ad_group` / `enable_ad_group(customer_id, ad_group_id)` | 1 | Granular pause/enable below the campaign level. |
| `pause_keyword` / `enable_keyword(customer_id, criterion_resource_name)` | 1 | Pause/enable a single ad-group criterion. |
| `set_keyword_bid(customer_id, criterion_resource_name, cpc_usd)` | 1 | Update a keyword's max CPC. USD → micros internally. |
| `set_campaign_budget(customer_id, budget_id, daily_amount_usd)` | 1 | Preview a daily budget change. USD → micros internally. |
| `add_negative_keyword(customer_id, scope, ref_id, text, match_type)` | 1 | Preview adding a campaign- or ad-group-level negative. |
| `apply_recommendation(customer_id, recommendation_resource_name)` | 1 | Apply one Google Ads recommendation. One-shot — Google has already validated it. |
| `generate_keyword_ideas(customer_id, seed_type, ...)` | 1 | SEM keyword research. Returns Google's keyword-idea expansions with avg searches, competition, suggested bids. |
| `batch_job(customer_id, action, ...)` | 1 | Async batch lifecycle: `create` → `add_operations` → `run` → `status` → `results`. |
| `offline_user_data_job(customer_id, action, ...)` | 1 | Customer Match / Store Sales upload lifecycle: `create` → `add_operations` → `run` → `status`. |
| `ping()` | — | Connectivity check. Returns `"pong"`. |
Resources:
- `gads-account://accessible` — customer IDs the credentials can operate on.
- `gads-schema://{resource_type}` — selectable / filterable / sortable fields per GAQL resource.
- `gads-rpc-catalog://` — every public RPC across the v24 SDK with `read_only` / `supports_validate_only` hints.
- `gads-rpc-schema://{service}/{method}` — per-method request proto fields.
## Architecture
Three layers behind a single SDK boundary. **Layer 1** has ~13 outcome-shaped tools that wrap **Layer 2**'s five generic escape hatches (`gaql`, `mutate`, `call_read_rpc`, `call_mutate_rpc`, `apply`). The `ads/` package is the only place that imports `google.ads.googleads.*`. Schema, account, and RPC discovery are MCP resources, not tools — they don't count against ambient context. Total tools stay constant as Google adds services.
Full structural plan: see `plan.md`.
## Safety model
The server enforces *invariants*, not your account's *policies*:
1. **Two-phase writes.** `mutate(...)` (or any Layer-1 write tool) calls the API with `validate_only=true`, renders a per-operation diff, stores under a `mutate_id` with a 15-minute TTL. `apply(mutate_id)` commits. Re-applying the same id returns the cached result and does **not** re-call the API.
2. **Customer-ID allowlist** built from real OAuth grants. Hallucinated IDs get rejected.
3. **Append-only audit log** of every attempt — success, validation failure, API error, expired re-apply, idempotent replay.
4. **MCP `ToolAnnotations`** on every tool so clients render confirmation prompts correctly.
The server deliberately doesn't enforce CPC caps, budget caps, or batch-size caps — those depend on your vertical (insurance routinely bids $200; ecommerce hits diminishing returns at $5). The two-phase preview is the actual safety mechanism: the new value is in the diff before commit, where the LLM and the human can both see it.
## Observability
Three logs, each with a different audience:
| Log | Path | Format | What it records |
|---|---|---|---|
| **Audit** | `~/.local/share/google-ads-mcp/audit.log` | JSONL, mode `0600` | Every state-changing attempt. The forensic answer to "did the LLM do X?" |
| **Activity** | `~/.local/share/google-ads-mcp/activity.log` | JSONL | Every tool/resource call: name, args summary, duration_ms, outcome. Reads included. |
| **Diagnostics** | stderr | text | Server lifecycle and operator-facing warnings. |
Audit and activity are write-once-per-line (POSIX append is atomic up to 4 KB). The diagnostic log is configurable via `log_level`; audit and activity are always on.
**Audit schema** (one JSON object per line):
```json
{
"timestamp": "2026-04-28T18:30:15.123456+00:00",
"phase": "preview" | "apply",
"outcome": "ok" | "guardrail_rejection" | "validation_failed"
| "api_error" | "expired" | "not_found" | "cached_replay",
"mutate_id": "...",
"customer_id": "1234567890",
"payload_kind": "operations" | "rpc_call" | null,
"operations": [{...}] | null, // payload_kind=operations
"rpc_call": {"service":"...", "method":"...", "params":{...}} | null, // payload_kind=rpc_call
"result": {"resource_names": [...]} | null,
"error": {"type": "...", "message": "...", "request_id": "..."} | null
}
```
**Activity schema**:
```json
{
"timestamp": "...",
"kind": "tool" | "resource",
"name": "pause_campaign",
"args_summary": {...},
"duration_ms": 123,
"outcome": "ok" | "error",
"error": {"type": "...", "message": "..."} | null
}
```
## Configuration
Env vars prefixed `GOOGLE_ADS_MCP_` override compiled-in defaults.
| Setting | Default | Notes |
|---|---|---|
| `credentials_path` | `~/.config/google-ads-mcp/credentials.yaml` | XDG-aware. |
| `audit_log_path` | `~/.local/share/google-ads-mcp/audit.log` | XDG-aware. |
| `activity_log_path` | `~/.local/share/google-ads-mcp/activity.log` | XDG-aware. |
| `gaql_max_rows` | `1000` | GAQL row cap. |
| `gaql_max_response_bytes` | `256000` | Approximate response-size cap returned to the LLM. |
| `mutate_id_ttl_seconds` | `900` (15 min) | TTL for previewed mutates. |
| `log_level` | `INFO` | Diagnostic stderr level. |
## Development
```sh
git clone <this-repo>
cd google-ads-mcp
uv sync --group dev
uv run pytest # 100 unit tests
uv run ruff check .
uv run pyright src tests # strict mode
```
The test suite runs without credentials: anything above the `ads/` SDK boundary is testable with mocked stubs.
## License
MIT. See `LICENSE`.
MCP Config
Below is the configuration for this MCP Server. You can copy it directly to Cursor or other MCP clients.
mcp.json
Connection Info
You Might Also Like
Vibe-Trading
Vibe-Trading: Your Personal Trading Agent
ai-berkshire
Berkshire in the AI Era: A Value Investment Research Framework Based on...
hexstrike-ai
HexStrike AI is an AI-powered MCP cybersecurity automation platform with 150+ tools.
valuecell
Valuecell is a Python project for efficient data management.
tradingview-mcp
AI-assisted TradingView chart analysis — connect Claude Code to your...
tradingview-mcp
TradingView MCP Server offers real-time market analysis for crypto and stocks.