Skip to main content
Glama
minholi

Google Ads MCP Server

Google Ads MCP Server

MCP (Model Context Protocol) server that integrates with the Google Ads API. Lets AI agents (Claude, Cursor, etc.) query campaigns, metrics, audiences, and geography, and analyze budgets — all through natural language.


Two operating modes

Mode

Who uses it

Auth

Google refresh token

stdio

Just you (Claude Desktop / Cursor local)

None — local subprocess

Single, in .env (generated by scripts/get_refresh_token.py)

HTTP multi-tenant

Team / remote access

Native MCP OAuth 2.1 — each user authenticates with their own Google account

One per user, encrypted in local SQLite

Pick stdio for personal use (simpler). Pick HTTP when multiple users (with different Google accounts) need to share the same server.


Related MCP server: Altaviz

Available tools

Tool

Description

google_ads_list_accounts

Lists accessible accounts (useful under an MCC)

google_ads_list_campaigns

Campaigns with KPIs (cost, clicks, CTR, ROAS, CPA)

google_ads_list_ad_groups

Ad groups with performance grouped by campaign

google_ads_list_ads

Individual ads with final URL

google_ads_get_keywords

Keywords with performance

google_ads_get_search_terms

Search terms that triggered ads (filters: min_spend, conversion_filter, aggregate_cross_campaign)

google_ads_get_metrics

Aggregated or daily metrics (campaign or account)

google_ads_get_breakdown

Generic breakdown by network, hour_of_day, day_of_week, or conversion_action (levels: campaign / ad_group / account)

google_ads_get_geographic_performance

Performance by location

google_ads_list_audience_performance

Performance by audience (includes CUSTOM_INTENT)

google_ads_get_budget_analysis

"Budget drains" — high cost + low ROAS

google_ads_get_performance_summary

Executive summary, period comparison, alerts

google_ads_list_change_events

Detailed change history (CREATE/UPDATE/REMOVE) with per-field diff — last 30 days

google_ads_list_change_status

Lightweight indicator of recent changes per resource (no diff)

All accept: flexible date ranges (last_7_days, last_30_days, this_month, last_month, custom YYYY-MM-DD), output format (markdown or json), and optional customer_id.

The synthesis tools google_ads_find_wasted_spend and google_ads_diagnose_campaign_health also accept a language parameter ("pt" or "en", default "pt") that controls the stopword set used to group search terms by their first meaningful word.


Setup A — stdio mode (single-tenant)

1. Prerequisites

2. Installation

git clone <repo>
cd google-ads-mcp
uv sync

3. "Desktop" OAuth Client in GCP

  1. APIs & Services → Credentials → Create Credentials → OAuth client ID

  2. Application type: Desktop app

  3. Save the Client ID and Client secret

4. Refresh token

Interactive wizard:

uv run python scripts/get_refresh_token.py

Paste the Client ID/Client secret when prompted; it opens the browser, you log in, and copy the resulting refresh_token.

5. .env

GOOGLE_ADS_DEVELOPER_TOKEN=...
GOOGLE_ADS_CLIENT_ID=...apps.googleusercontent.com
GOOGLE_ADS_CLIENT_SECRET=...
GOOGLE_ADS_REFRESH_TOKEN=...
GOOGLE_ADS_CUSTOMER_ID=123-456-7890
# Optional, if the account above is under an MCC:
# GOOGLE_ADS_LOGIN_CUSTOMER_ID=999-888-7777

6. Client configuration

Claude Desktop~/Library/Application Support/Claude/claude_desktop_config.json:

{
  "mcpServers": {
    "google-ads": {
      "command": "uv",
      "args": ["run", "python", "main.py", "--transport", "stdio"],
      "cwd": "/path/to/google-ads-mcp"
    }
  }
}

Cursor.cursor/mcp.json:

{
  "mcpServers": {
    "google-ads": {
      "command": "uv",
      "args": ["run", "python", "main.py", "--transport", "stdio"],
      "cwd": "/path/to/google-ads-mcp"
    }
  }
}

uv run loads the .env automatically via python-dotenv.


Setup B — HTTP multi-tenant mode

How it works

Claude Desktop ──(MCP OAuth: tokens issued by us)──▶ our MCP (AS+RS)
                                                          │
                                                          └──(Google OAuth: user's refresh_token)──▶ Google Ads API

There are two chained OAuth flows: the server is simultaneously an Authorization Server (issues its own JWTs to Claude Desktop) and a Google OAuth Client (holds the encrypted refresh_token for the user's Google account). Desktop never sees the Google refresh_token.

UX in Claude Desktop:

  1. User adds the MCP URL under Settings → Connectors (once).

  2. Clicks "Connect" → browser opens → Google login → adwords consent → returns connected.

  3. From then on, all calls are authenticated. Refresh is silent.

1. Prerequisites

  • Public domain with TLS (e.g., mcp.your-domain.com)

  • Caddy (or Nginx) running on the host — will terminate TLS and reverse-proxy to 127.0.0.1:8000

  • Docker + Docker Compose

2. Developer Token

Same as Setup A (step 1). It belongs to the MCP operator and is shared across all users.

3. "Web application" OAuth Client in GCP

⚠️ Separate client from the Desktop one used in Setup A.

  1. Enable the Google Ads API if you haven't already.

  2. Configure the OAuth consent screen:

    • User type: External

    • Add the scope https://www.googleapis.com/auth/adwords

    • Under "Test users" add the emails that will test (or click Publish — the adwords scope is restricted and Google may require verification).

  3. Create Credentials → OAuth client ID:

    • Application type: Web application

    • Authorized redirect URIs: https://mcp.your-domain.com/auth/callback (must match the MCP_PUBLIC_URL you configure exactly)

  4. Save the Client ID and Client secret.

4. (Optional) Generate a JWT signing key

# Only if you want to pin the key (e.g., multiple replicas).
# Without this, GoogleProvider derives the key from the client_secret.
uv run python -c "import secrets; print(secrets.token_urlsafe(48))"

5. .env

# MCP operator (shared)
GOOGLE_ADS_DEVELOPER_TOKEN=AbCdEf123...

# Web OAuth Client (step 3)
GOOGLE_OAUTH_WEB_CLIENT_ID=123456789-abc.apps.googleusercontent.com
GOOGLE_OAUTH_WEB_CLIENT_SECRET=GOCSPX-xxxxxxxxxxxxxxxxx

# Public URL — no trailing slash, with https
MCP_PUBLIC_URL=https://mcp.your-domain.com

# Optional: pinned key for signing MCP JWTs (step 4)
# OAUTH_JWT_SIGNING_KEY=Z9K1u7c-paste-the-output-of-secrets-token-urlsafe-48

# Server
MCP_TRANSPORT=http
MCP_HOST=0.0.0.0
MCP_PORT=8000

Setup A variables (GOOGLE_ADS_CLIENT_ID, GOOGLE_ADS_CLIENT_SECRET, GOOGLE_ADS_REFRESH_TOKEN, GOOGLE_ADS_CUSTOMER_ID) are not used in HTTP mode — you can remove or comment them out.

6. Host Caddy

mcp.your-domain.com {
    reverse_proxy 127.0.0.1:8000
}

7. Start

mkdir -p data
docker compose up -d --build
docker compose logs -f google-ads-mcp

8. Verification

curl https://mcp.your-domain.com/.well-known/oauth-authorization-server

Should return JSON with issuer, authorization_endpoint, etc.

9. Connect from Claude Desktop

Settings → Connectors → Add custom connector → paste https://mcp.your-domain.com/mcp → click Connect → Google flow opens → log in → account authorized.

Done. Other users can do the same on their machines.


Environment variables

Common (both modes)

Variable

Description

GOOGLE_ADS_DEVELOPER_TOKEN

Google Ads API developer token

MCP_TRANSPORT

stdio (default) or http

MCP_PORT

HTTP port (default 8000)

MCP_HOST

HTTP host (default 0.0.0.0)

stdio mode

Variable

Required

Description

GOOGLE_ADS_CLIENT_ID

Desktop OAuth Client ID

GOOGLE_ADS_CLIENT_SECRET

Desktop OAuth Client Secret

GOOGLE_ADS_REFRESH_TOKEN

Refresh token generated by scripts/get_refresh_token.py

GOOGLE_ADS_CUSTOMER_ID

⚠️

Default account ID (can be passed per call)

GOOGLE_ADS_LOGIN_CUSTOMER_ID

MCC ID, if applicable

HTTP multi-tenant mode

Variable

Required

Description

MCP_PUBLIC_URL

Public URL, no trailing /. Must match the redirect URI in GCP

GOOGLE_OAUTH_WEB_CLIENT_ID

Web OAuth Client ID

GOOGLE_OAUTH_WEB_CLIENT_SECRET

Web OAuth Client Secret

OAUTH_JWT_SIGNING_KEY

Optional. If absent, GoogleProvider derives the key from the client_secret. Set only for multiple replicas


Architecture

google-ads-mcp/
├── main.py                       # Entry point — stdio or HTTP (mcp.http_app)
├── src/
│   ├── server.py                 # 14 MCP tools (fastmcp) + conditional GoogleProvider
│   ├── client.py                 # REST/GAQL client (Google Ads API v24)
│   ├── auth.py                   # GoogleAdsAuth — env vars (stdio) or injected access_token (HTTP)
│   └── formatters.py             # Markdown / JSON
├── scripts/
│   ├── get_refresh_token.py      # OAuth wizard (stdio mode)
│   └── update_geo_targets.py     # Updates src/data/geo_targets.json
├── Dockerfile
├── docker-compose.yml            # Exposes 127.0.0.1:8000 (host Caddy handles TLS)
├── pyproject.toml
└── .env.example

Data flow (HTTP mode): tool call → JWT verified by GoogleProvider (token-swap JTI → decrypted upstream Google access_token) → get_access_token().token → GoogleAdsAuth.for_access_token → Google Ads REST v24 → formatter. The Google refresh_token stays encrypted in GoogleProvider's internal key-value store; refresh is transparent when the access_token expires.


Usage examples (natural language)

"List active campaigns from the last 7 days"
"Which campaign has the worst ROAS in the last month?"
"Show daily metrics for campaign 123456 over the last 14 days"
"Which keywords are spending a lot without converting?"
"Which search terms appeared most in the last 30 days?"
"Show performance by state in the last month"
"Which audiences are performing best?"
"Analyze the account's budget and identify drains"
"Give me an executive summary comparing this week to the previous one"

Operations

Logs

docker compose logs -f google-ads-mcp

Backup

  • GoogleProvider persists DCR clients and encrypted Google refresh_tokens in its own key-value store (default: file inside the container's platformdirs — to survive restarts, mount the directory on the host).

  • OAUTH_JWT_SIGNING_KEY — if you change it (or if you're using the default derivation and change GOOGLE_OAUTH_WEB_CLIENT_SECRET), all users must reconnect.

Rebuild after code changes

docker compose up -d --build

Update the geo targets table

uv run python scripts/update_geo_targets.py            # default: BR
uv run python scripts/update_geo_targets.py --country BR AR MX
git add src/data/geo_targets.json && git commit

Troubleshooting

Symptom

Likely cause

Fix

redirect_uri_mismatch in Google consent

URI in GCP ≠ ${MCP_PUBLIC_URL}/auth/callback

Check character by character (https, no trailing slash in MCP_PUBLIC_URL)

"Google did not return a refresh_token" in the callback

User previously authorized the app (Google only returns refresh_token on the first consent)

Ask them to revoke at myaccount.google.com/permissions and reconnect

401 invalid_token on /mcp

MCP JWT expired or OAUTH_JWT_SIGNING_KEY changed

Desktop refreshes on its own; if it persists, reconnect from Connectors

Tool returns PERMISSION_DENIED from the Google Ads API

User doesn't have access to the requested customer_id, or wrong MCC

MCC auto-resolve tries to sort it out; if it fails, check access in Google Ads

Docker healthcheck failing

Missing MCP_PUBLIC_URL or some OAUTH_* in .env

docker compose logs google-ads-mcp shows which variable is missing

OPENID_DISCOVERY_FAILED in Claude Desktop

Broken DNS/TLS or Caddy not routing

curl -v https://mcp.../.well-known/oauth-authorization-server


Security

  • Credentials never in code — always in .env (already in .gitignore).

  • Host Caddy — terminates TLS. The container only listens on 127.0.0.1:8000, not reachable directly from the internet.

  • Rate limiting — configure on the host Caddy, not in the app.

  • Container runs as a non-root user.

  • Google refresh_tokens encrypted at rest by GoogleProvider's internal key-value store.

  • Short-lived MCP JWTs with transparent upstream Google refresh and MCP refresh token rotation (OAuth 2.1) — all managed by GoogleProvider.

  • Treat OAUTH_JWT_SIGNING_KEY (if set) and GOOGLE_OAUTH_WEB_CLIENT_SECRET as critical secrets (vault/secret manager in production).

Related MCP Connectors

Related MCP Servers

  • A
    license
    A
    quality
    D
    maintenance
    Enables natural language querying of Amazon Advertising data including Sponsored Products, Brands, Display, DSP reports, and AMC SQL workflows, with live data from the Amazon Ads API.
    23
    MIT
  • F
    license
    Not graded
    quality
    B
    maintenance
    Enables monitoring and managing multi-platform media buying accounts via natural language, detecting anomalies like creative fatigue and spend spikes, with AI-powered recommendations and a human-in-the-loop approval queue.
    -
  • A
    license
    Not graded
    quality
    C
    maintenance
    Enables AI agents to browse and query Facebook Ads data including ad accounts, campaigns, ad sets, and performance insights via natural language.
    13 npm
    MIT
  • F
    license
    Not graded
    quality
    D
    maintenance
    Enables querying ad campaign performance and setup across Meta, TikTok, and Google Ads using natural language through AI agents.
    1
    -