I wired Claude to a client's Google Ads and GA4 via MCP servers, then ran a live 90 day audit in one session. Here is the setup and what fell out.

← Back to blog

Connecting Claude to Google Ads and GA4 via MCP (A Live Agency Audit)

By Agnel Nieves15 min read
View as Markdown

TL;DR

I connected Claude (via Claude Code and Cursor) directly to a client's Google Ads account and GA4 property using two MCP servers: the official googleads/google-ads-mcp and googleanalytics/google-analytics-mcp. Both are read only, both are Python, both are maintained by Google.

Once they were wired up, I pointed Claude at the account and asked it to run a 90 day audit. It pulled live data via GAQL and the GA4 Data API, cross referenced it with the WordPress IDX plugin source in the same repo, and produced a 119 KB HTML report ranking 5 critical issues, 10 high priority opportunities, and a 30/60/90 day roadmap. Total active build time, including the setup itself: about three hours. The output is hosted at /lyfe-realty-audit-report.html if you want to see what falls out of this pattern on a small, real account.

This post is the runbook for anyone who wants to do the same thing on an agency account. It covers what to install, the two gotchas that will eat your first afternoon (the Google Ads developer token tier and the GA4 service account email bug), and how to structure the repo so a fresh Claude session can run the audit again next month without re asking you for IDs.

Why MCP, not CSV exports

The first instinct for an ad hoc audit is to download Search Terms, Negative Keywords, and a few GA4 reports as CSVs and hand them to Claude. That works once. It does not work as a workflow.

The MCP approach is different in three ways that compound:

  1. It is live. The data Claude is reasoning over is the same data the dashboard shows you, refreshed at query time. There is no "I exported this yesterday, has anything changed?" lag.
  2. It is queryable. Claude can decide what fields to pull, what date ranges to use, and what segments to slice by. If a finding raises a follow up question, the follow up is one tool call away, not "go back to the UI and re export."
  3. It is reusable. Same MCP, same auth, different client. Once you have set it up for one account, every future account is a claude mcp add-json away.

The cost is a one time setup that has some sharp edges. The rest of this post is about those edges.

The two servers

Both are official Google projects. Both were pushed within the last week as of writing. Both are exposed as Python entry points you can install with pipx.

MCPRepoAuthSurface
Google Adsgoogleads/google-ads-mcpOAuth refresh token + developer token + login_customer_id for MCCsearch, get_resource_metadata, list_accessible_customers
GA4googleanalytics/google-analytics-mcpService account JSON, Viewer on the GA4 propertyrun_report, run_conversions_report, run_funnel_report, run_realtime_report, get_account_summaries, get_property_details, list_google_ads_links

I also kept cohnen/mcp-google-ads on standby as a backup. The official server has an open bug where its search tool rejects array parameters, so for any query that needs multiple values you either pass them one at a time or fall back to the community fork.

Set GA4 up first

GA4 is the fast half. No developer tokens, no Google approval queues, no OAuth dance. You can be running queries in under an hour.

The steps:

  1. In your Google Cloud project, enable the Google Analytics Admin API and the Google Analytics Data API.

  2. Create a service account, give it no roles inside GCP itself (it does not need any), and download a JSON key.

  3. Grant the service account Viewer access on the GA4 property. This is the step where you will hit the bug below.

  4. Install the MCP: pipx install analytics-mcp.

  5. Wire it into Claude Code at user scope so it follows you across projects:

    claude mcp add-json --scope user google-analytics '{
      "command": "/Users/you/.local/bin/google-analytics-mcp",
      "env": {
        "GOOGLE_APPLICATION_CREDENTIALS": "/Users/you/.gcp-keys/ga4-service-account.json"
      }
    }'
  6. Restart claude. Smoke test with claude mcp list. You should see google-analytics: ✓ Connected.

The GA4 UI bug (Property Access Management)

When you try to add the service account email under GA4 Admin → Property Access Management, the form will reject it with "This email doesn't match a Google Account." This is a long standing bug. The form refuses to accept emails ending in .iam.gserviceaccount.com even though the underlying API accepts them just fine.

The workaround is to call the Admin API directly. Pick up a fresh access token via the OAuth Playground (scoped to https://www.googleapis.com/auth/analytics.manage.users) and POST the access binding:

curl -X POST \
  "https://analyticsadmin.googleapis.com/v1alpha/properties/YOUR_PROPERTY_ID/accessBindings" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "user": "ga4-mcp-service-account@your-project.iam.gserviceaccount.com",
    "roles": ["predefinedRoles/viewer"]
  }'

If it returns a name field, the binding is live. Reload the UI and you will now see the service account in the access list. The MCP can authenticate against the property starting on its next call.

Then set up Google Ads

Google Ads is the slow half. The most common failure mode is people thinking they are blocked by setup complexity when they are actually blocked by their developer token tier. Fix that first.

Step 1: Confirm your developer token tier

In your Google Ads manager account (MCC), go to Tools → API Center. There are four tiers:

TierWhat it lets you do
TestOnly query Google Ads test accounts. Production accounts return developer token only approved for use with test accounts.
ExplorerQuery production accounts, up to 2,880 operations per day. Auto issued on apply, usually within 24 hours.
BasicHigher quota, full production. Needs a written use case review by Google. Usually 1 to 3 business days.
StandardHighest quota, for tool builders. Heavier review.

For an audit workflow on a few client accounts, Explorer is enough. If you only have Test, the first real query you run on the client account will fail with the exact error string above. Apply for Explorer before you do anything else.

Step 2: Create the OAuth client and mint a refresh token

Inside the same GCP project, enable the Google Ads API and create an OAuth 2.0 Client ID of type Desktop. Download the installed block JSON. This is the client identity that the MCP will impersonate as you.

Run a one shot Python script to walk the consent flow and capture a long lived refresh token. The script is small enough to keep in the repo (mine lives at scripts/mint_google_ads_refresh_token.py):

from google_auth_oauthlib.flow import InstalledAppFlow
import json, argparse, pathlib
 
SCOPES = ["https://www.googleapis.com/auth/adwords"]
 
def main(client_secret, output):
    flow = InstalledAppFlow.from_client_secrets_file(client_secret, SCOPES)
    creds = flow.run_local_server(port=0, prompt="consent", access_type="offline")
    pathlib.Path(output).write_text(json.dumps({
        "type": "authorized_user",
        "client_id": creds.client_id,
        "client_secret": creds.client_secret,
        "refresh_token": creds.refresh_token,
    }, indent=2))
    print("Wrote", output)
 
if __name__ == "__main__":
    p = argparse.ArgumentParser()
    p.add_argument("--client-secret-file", required=True)
    p.add_argument("--output", required=True)
    main(p.parse_args().client_secret_file, p.parse_args().output)

A browser pops, you sign in with the agency account, click through the unverified app warning (Advanced → Go to ... (unsafe)), approve, and the script writes an authorized_user ADC JSON to disk. The MCP picks it up automatically.

Step 3: Wire the MCP

pipx run --spec git+https://github.com/googleads/google-ads-mcp.git google-ads-mcp --help
 
claude mcp add-json --scope user google-ads '{
  "command": "/Users/you/.local/bin/google-ads-mcp",
  "env": {
    "GOOGLE_APPLICATION_CREDENTIALS": "/Users/you/.gcp-keys/google-ads-adc.json",
    "GOOGLE_PROJECT_ID": "your-gcp-project",
    "GOOGLE_ADS_DEVELOPER_TOKEN": "redacted",
    "GOOGLE_ADS_LOGIN_CUSTOMER_ID": "YOUR_MCC_ID_NO_DASHES"
  }
}'

The two details that catch people:

  • GOOGLE_ADS_LOGIN_CUSTOMER_ID is the MCC ID, not the client account ID. Without it, the API routes the request to whatever account it can find directly and returns nothing for the client. With it, the MCC routes the call through and you get the client's data.
  • IDs are stored without dashes in the API. The UI shows 767-971-9496, the env var wants 7679719496. When in doubt, strip dashes.

Step 4: Smoke test

Restart claude and ask:

Using the google-ads MCP, run list_accessible_customers and tell me what comes back.

You should see your MCC and any other manager accounts you own. The client account does not appear in this list when accessed via MCC, which trips people up. To confirm the client is reachable, ask Claude to query the customer resource directly:

Using the google-ads MCP for customer 7679719496 (login-customer-id 5754939043), select customer.id, customer.descriptive_name, customer.currency_code, customer.time_zone from customer.

If you get back the client's name, you are wired.

Make the setup outlast the session

The whole point of MCP is that next month, when you want to run the same audit, you do not start from zero. The pattern I landed on: a single markdown runbook in the repo that any fresh Claude session can read to operate the account without re asking for IDs.

The structure is intentionally boring:

docs/marketing-mcps-runbook.md
  1. Client and business context
  2. MCP servers that are wired up
  3. Account inventory (every ID you need)
  4. Credential storage (paths, never values)
  5. MCP client configuration
  6. How to call the MCPs from a chat
  7. How the auth pipeline actually works
  8. Audit playbook
  9. Known issues and gotchas
 10. Files in this repo that support this setup
 11. If you (a future agent) are starting fresh

Section 11 is the load bearing one. It tells the next agent: read sections 1, 8, and 9 first; confirm both MCPs are visible; run a smoke query against each before doing real work; if either smoke check fails, surface the error to the user instead of improvising. The whole file is about 400 lines. It is the difference between "Claude needed me to dig up the MCC ID again" and "Claude opened the file, ran the audit, and produced a report."

Pair the runbook with a project level CLAUDE.md that points to it and any agent that opens the project will pick it up automatically. This is the same pattern I described in Optimizing Your Website for AI Agents and LLMs: write the context once, in markdown, near the code, so both humans and agents read from the same source of truth.

What a live audit actually looks like

Once both MCPs are live and the runbook exists, the audit prompt is short:

Read docs/marketing-mcps-runbook.md, run the audit playbook in section 8 against the last 90 days, and put the detailed output in an HTML report in this directory.

What Claude does next:

  1. Field discovery. The Google Ads search tool will only accept fields that exist for a given resource in the API version your MCP is pinned to. Claude calls get_resource_metadata on each resource before constructing the query. This avoids the common failure where you copy a GAQL snippet from documentation and find that one field has been renamed or moved.

  2. Parallel pulls. Claude fires the cheap reads in parallel: campaign settings, conversion actions, account level negatives, ad groups, ads, geo breakdown, device split, day of week. Most of the audit data is in your context within a minute.

  3. The expensive reads. Search terms and keyword views are the heavy queries. They blow past most agent context windows if you let them, so the MCP returns them as files on disk and the agent uses jq and Python to extract the slices it actually needs. This is built into the tool result protocol. You do not have to wire it.

  4. Cross referencing source. Because the audit is running inside the client's repo, Claude can also read the WordPress plugin source to confirm, for example, that the MLS feed is correctly scoped to one office, or that the IDX integration is not creating publicly indexable rental pages. The campaign data and the site code get reconciled in the same session.

  5. Report generation. I asked for the output as a single, standalone HTML file with inline CSS so it travels well over email and prints cleanly. Claude wrote about 1,500 lines of HTML and styled it as a proper audit deliverable.

A representative GAQL query, the kind Claude runs without intervention once it knows the resource shape:

SELECT
  campaign.id,
  campaign.name,
  campaign.geo_target_type_setting.positive_geo_target_type,
  campaign.bidding_strategy_type,
  campaign_budget.amount_micros,
  metrics.cost_micros,
  metrics.clicks,
  metrics.conversions,
  metrics.search_impression_share,
  metrics.search_budget_lost_impression_share,
  metrics.search_rank_lost_impression_share
FROM campaign
WHERE segments.date BETWEEN '2026-02-22' AND '2026-05-22'
  AND campaign.status IN ('ENABLED', 'PAUSED')

The MCP returns this as JSON, Claude correlates it with the rest of the pulls, and the findings start writing themselves.

What fell out of one real account

The account in this case was a residential real estate practice in Southwest Broward, FL. The trigger was the client complaining about renter calls coming through ads targeted at buyers and sellers. The full report lives at /lyfe-realty-audit-report.html. The shape of the findings, without the full numbers, looked like this:

  • The original renter call problem was already closed at the campaign level. A paused Performance Max campaign, 92 negative keywords on the active Search campaign, presence only geo targeting, and a correctly scoped MLS feed had already shut the gap. The audit pivoted from defense to offense.
  • The Buyers Search campaign was configured with PRESENCE_OR_INTEREST geo targeting instead of PRESENCE. Roughly 92% of its spend was going to people who searched about Southwest Broward, not people in it. 1,484 clicks in 90 days produced about 55 actual GA4 sessions. The 84% click to session gap was the smoking gun. Five minute fix.
  • 82% of paid keywords had Quality Score 0 or null. Sellers Search was losing 62% of available impression share to ad rank, only 4% to budget. The bottleneck was creative and landing page relevance, not spend.
  • The lead form had a 94% abandonment rate on mobile. 66 form starts produced 4 form completions. The single highest leverage point in the funnel and not visible from inside Google Ads.
  • Phone clicks were happening (27 in 90 days, recorded in GA4) but the matching Google Ads conversion action was HIDDEN. The bidder was blind to roughly a third of the real lead volume.
  • Both campaigns ran Maximize Conversions with no Target CPA. The bidder had no efficiency anchor on a five month old account that had earned the right to one.

The cumulative projection, again on the same budget: visible conversions per month from 7 to 25 or 40, CPA from $138 to $25 to $40. The audit ranks every recommendation by dollar impact, effort, and owner so the marketing team can sequence the work without re reading the source data.

The interesting thing is that none of those findings required cleverness from the model. They required the model having access to the right data, knowing where to look, and being willing to cross reference fields most analysts skim past (the location_type on geographic_view, the status field on conversion_action, the form_start vs form_submission gap in GA4 events). MCP plus a decent runbook gets you that for the same effort as a single one off Looker Studio dashboard.

Lessons from the build

A few things I would tell my one day earlier self:

  1. Set up GA4 first. It is the fast half and it gives you something to play with while you wait on the Google Ads developer token review.
  2. Apply for Explorer tier on day one. Even if your token comes back as Test, you can still rehearse the rest of the setup against a test account while the Explorer review runs.
  3. Keep credentials in ~/.gcp-keys/ with chmod 600, never in the repo. The runbook can describe the paths and roles, but the JSON files belong outside the repo. The Google Ads ADC JSON in particular is effectively a production password for every linked client account.
  4. Use pipx run --spec git+... to install the official Google Ads MCP. It is not on PyPI yet. The community fork at cohnen/mcp-google-ads is on PyPI and useful as a fallback.
  5. Wire MCPs at user scope, not project scope. Agency work spans many directories. claude mcp add-json --scope user makes the MCPs visible from anywhere.
  6. Write the runbook the day you do the setup. Not later. Not "when I have time." The 30 minutes you spend documenting the IDs and the gotchas will save you (and the next agent) an afternoon every time you come back.
  7. Read tool results as data, not as conclusions. The MCP will happily return field values that look right but are stale, or null fields the API simply does not surface in this version. When something looks off, check the resource metadata before changing anything in the account.

What I would build next

Two things are sitting on the list:

  • A Meta Ads MCP. Facebook ads are 30% of the same client's paid mix and currently convert better than Google. There is no first party Meta MCP yet, but the Marketing API is well covered and a thin wrapper would be a couple of evenings.
  • A "monthly delta" prompt. Same runbook, but instead of a 90 day audit it diffs the current month against the prior. Lower cognitive load for the agent, faster to run, and natural to schedule.

If you have done a similar build, or you are running into one of the gotchas above and stuck, I would like to compare notes. Let's connect.

View as Markdown