Google Ads MCP
Google Ads MCP
An MCP server that lets an LLM agent operate Google Ads — both Search and App/UAC campaigns. It exposes read tools (GAQL) always, and guarded write tools behind a mutation flag.
Status: Phase 1 — reads only (
list_accessible_customers,search,describe_resource). Writes (budgets, campaigns, ad groups, keywords, ads, UAC) land in later phases behindADS_MCP_ENABLE_MUTATIONS.
Design principles: every change will be reversible, confirmable, account- and budget-scoped. Reads are always safe.
Requirements
Python 3.12+
uv(curl -LsSf https://astral.sh/uv/install.sh | sh)A Google Ads developer token, an OAuth2 client, and a refresh token.
Google Ads API version is pinned to v23 in
src/google_ads_mcp/config.py.
Install
uv sync --extra devGetting credentials
You need five values in a google-ads.yaml file (copy from
google-ads.yaml.example).
developer_token — Google Ads UI → Tools → API Center. Basic Access is enough (15k operations/day).
OAuth client (
client_id,client_secret) — Google Cloud Console → APIs & Services → Credentials → Create OAuth client ID → Desktop app. Enable the Google Ads API for the project first.refresh_token — generate once with the OAuth consent flow for scope
https://www.googleapis.com/auth/adwords. The official google-ads-python repo shipsgenerate_user_credentials.pyfor exactly this; run it with your client id/secret and paste the resulting refresh token.login_customer_id — your manager (MCC) account id without dashes. Omit if you authenticate directly against a single account.
Then:
cp google-ads.yaml.example google-ads.yaml # fill in real values (gitignored)
cp .env.example .env # set GOOGLE_ADS_CREDENTIALS to its path
export GOOGLE_ADS_CREDENTIALS="$PWD/google-ads.yaml"Secrets are never committed.
google-ads.yaml,.env, and*.audit.jsonlare gitignored.
Run
# stdio (default — what Claude Desktop uses)
uv run google-ads-mcp
# or HTTP (future Cloud Run hosting)
ADS_MCP_HTTP=true uv run google-ads-mcpConnect to Claude Desktop
Edit Claude Desktop's config file:
macOS:
~/Library/Application Support/Claude/claude_desktop_config.jsonWindows:
%APPDATA%\Claude\claude_desktop_config.json
Add an entry (use absolute paths):
{
"mcpServers": {
"google-ads": {
"command": "uv",
"args": [
"--directory",
"/absolute/path/to/Google Ads MCP",
"run",
"google-ads-mcp"
],
"env": {
"GOOGLE_ADS_CREDENTIALS": "/absolute/path/to/Google Ads MCP/google-ads.yaml"
}
}
}
}If uv isn't on Claude Desktop's PATH, use its absolute path (which uv).
Restart Claude Desktop. The three read tools appear under the 🔌 connector menu.
First check
Ask Claude: "List my accessible Google Ads customers", then "Run this GAQL on account : SELECT campaign.id, campaign.name, campaign.status FROM campaign LIMIT 10".
Tools (Phase 1)
Tool | Description |
| Customer ids the auth user can access. |
| Execute a GAQL query. |
| Discover selectable/filterable fields for GAQL. |
All return a unified envelope: {status, dry_run, resource_name, diff, message, data}.
Safety flags (parsed now, enforced from Phase 2)
Env var | Default | Purpose |
|
| Master switch for all write tools. |
| (empty) | CSV allowlist of customer ids. |
|
| Budget/bid ceiling without |
|
| Mutations dry-run by default. |
|
| Applied-change log. |
Security model
Secrets never enter git.
google-ads.yaml,.env, and*.audit.jsonlare gitignored. The repo ships only*.examplefiles with placeholders. Account ids and app ids in tests are fake.Writes are off by default (
ADS_MCP_ENABLE_MUTATIONS=false).Allowlist — mutations are refused for any account outside
GOOGLE_ADS_ALLOWED_CUSTOMER_IDS. Reads are unaffected.Budget cap — a budget/bid above
GOOGLE_ADS_MAX_DAILY_BUDGET_USDis refused unless the call passesoverride=true.Two-key apply — a real change needs BOTH
validate_only=falseANDconfirm=true. Otherwise the tool returns the diff (validate_only) or a preview, applying nothing.New campaigns/ad groups/ads are created PAUSED — nothing serves until you deliberately enable it.
Audit log — every applied change (and every failure during apply) is appended to the JSONL audit log; dry-runs are not logged as applied.
Atomic batches — multi-operation tools (
manage_keywords,manage_negative_keywords) are all-or-nothing by design (nopartial_failure), so a batch never half-applies.
If a credential is ever exposed, rotate it: developer token in the Ads API Center, OAuth client secret in Google Cloud Console → Credentials.
Tests
uv run pytest # offline tests (no network); live calls need real credsRoadmap
✅ Skeleton + reads.
✅ Safety layer (validate_only wrapper, allowlist, budget guard, confirm, audit, error mapping) — see
safety.py,audit.py,tests/test_guardrails.py.✅ Search writes (budget → campaign → ad group → keywords/negatives → RSA → statuses) —
writes_common.py,writes_search.py. New campaigns/ad groups/ads are created PAUSED; live validate_only verified.✅ UAC writes (app campaign, app assets, target updates) —
writes_app.py. App campaigns created PAUSED; create_app_campaign structurally validated against the live API. Real apply + manage_app_assets verify on the final account (where the app is provisioned).✅ Hardening + packaging (green pytest suite, security model documented, secret scan, optional HTTP transport for Cloud Run).