Skip to main content
Glama
lucagalvani

google-ads-agent

by lucagalvani

google-ads-agent

An MCP server and autonomous agent for Google Ads: build search campaigns, analyse what they're doing, and — optionally — let it make spend-reducing changes on a schedule without a human in the loop.

28 tools. 20 are read-only. One is irreversible and can only be triggered by a person. One reaches every mutable resource in the API and is governed by a permission table rather than by hand-written logic.

The distinguishing idea is that the safety boundary is a policy engine inside the server, not instructions in a prompt. Whatever drives the tools — a cron job, an LLM, you typing — hits the same enforcement point, so the driver is a swappable detail rather than a security control.


⚠️ Disclaimer — read before using

This software modifies live advertising accounts. It can cause money to be spent, cause campaigns to stop serving, and permanently delete campaigns. You use it entirely at your own risk.

  • Provided "as is", without warranty of any kind. See LICENSE. The authors and contributors accept no liability for any advertising spend, lost revenue, lost data, account suspension, or other damages arising from use or misuse of this software, however caused.

  • You are solely responsible for everything this software does in your accounts, including in autonomous mode, where it acts without human review of individual changes.

  • You are responsible for your own compliance with the Google Ads API Terms of Service, the obligations attached to your developer token, and any applicable advertising, privacy, and data-protection law.

  • Test on a Google Ads test account first. Verify the behaviour you expect before pointing it at an account that spends money.

  • Campaign removal cannot be undone. Google Ads has no un-remove.

  • Nothing here is legal, financial, or professional advertising advice.

Not affiliated with Google. This is an independent, unofficial project. It is not created, endorsed, sponsored by, or affiliated with Google LLC. "Google", "Google Ads", and related marks are trademarks of Google LLC, used here only to identify the API this software targets.


Related MCP server: Google Ads MCP Server

Table of contents


What it does

Build. Resolve locations and languages to targeting IDs, get keyword ideas, assemble a campaign spec, validate it against the live API without writing anything, then commit it as a single atomic mutation. Campaigns are always created paused.

Analyse. Performance by campaign, ad group, or ad, segmented by network, device, or date. Per-keyword metrics with quality score. The real search queries that triggered your ads. A structural audit that needs no performance data at all. Raw GAQL when none of that fits.

Act. Turn Display expansion and search partners off, add negative keywords, pause keywords and ad groups — each gated on evidence the server measures itself, with a change budget, per-entity cooldowns, a circuit breaker, and a full audit journal that supports one-command reversal.

Delete. Remove a campaign, behind three simultaneous gates, reachable only by a human.


The safety model

Actions are tiered by spend direction, which is a mechanical property rather than a judgement call:

Tier

Actions

Rule

autonomous

add_negative_keywords, set_networks (off only), pause_keywords, pause_ad_groups

Can only narrow delivery. Worst case: serves too little

propose

budgets, bids, new campaigns, RSA edits

Can increase spend, so it gets written up for a human instead of executed

forbidden

enabling/unpausing, removing, raising budgets

No autonomous path and no proposal path

Unknown actions fail closed to forbidden.

Six properties make that boundary real rather than decorative:

  1. The server measures the evidence; it never accepts it from the caller. An agent that supplies its own justification can fabricate it. add_negative_keywords queries the search-term data itself; set_networks queries the network split itself.

  2. Evidence thresholds. The failure mode of automated ad management is acting on noise. A keyword with 4 clicks and no conversions is not evidence of a bad keyword. Volume floors are enforced per action type.

  3. Conversions protect an entity, but not at any price. A max_conversions: 0 rule alone lets a single token conversion shield unlimited waste, so max_cost_per_conversion overrides that protection.

  4. Pause, never remove. Every autonomous action stores its inverse in the journal, so revert_last_run can undo an entire run.

  5. Change budget and cooldowns. Caps per run and per week, plus a per-entity cooldown, because frequent changes actively degrade Smart Bidding — it needs stability to relearn.

  6. Circuit breaker. Halts on a spend spike relative to daily budget, or inside a bidding learning period. The learning-period input comes from change_event, counting only changes made outside the API — so the agent backs off when a person has been editing, without tripping over its own changes. If that history is unavailable the check fails open, and check_setup reports the gap.

The trust switch

conversion_tracking_ok defaults to false, and while it is false no autonomous action is permitted at all.

Every autonomous decision keys off conversions. An agent running against under-firing conversion tracking does not fail loudly — it quietly pauses your best-performing keywords for lacking conversions that were never recorded.

Run conversion_actions to check. It flags the failure signatures that matter: a primary action recording nothing, lookback windows too short to catch your sales cycle, lead goals counting many-per-click, and conversion-based bidding running on too little volume to model anything. If it reports nothing HIGH, set the switch deliberately.


Full API coverage and the permission table

The tools above cover campaign construction and the four spend-reducing changes. Everything else the Google Ads API can write is reached through one tool, mutate, which takes a resource, an operation, and a JSON object of fields. There are 64 mutable resources in v25, and hand-writing a tool per resource would be unmaintainable and would swamp the tool list, so the generic path exists instead.

That path is governed by a permission table rather than by code that understands each resource. A resource, operation or field with no rule in the table is refused. Absence means denial, which is why new API surface never becomes writable by accident: Google adding a resource does not add a rule.

Four tiers, in descending order of trust:

Tier

Meaning

autonomous

Runs unattended, once the server has measured the evidence. Refused if it names an evidence rule the server cannot measure for that resource.

confirm

Runs when a human approves the exact diff in-session. Unreachable in an unattended run.

propose

Never executed. Written up for a person to do.

forbidden

Refused always, however it is confirmed.

confirm is what makes full coverage possible without making scheduled runs dangerous. Evidence thresholds, change budgets and circuit breakers substitute for human judgement, so a present human approving one specific diff supplies it directly and those checks do not apply. The tier still does, and agent/run.sh exports ADS_AGENT_UNATTENDED=1 so a scheduled run cannot self-approve.

Rules are specific about direction and value, because the same field is not one permission. Lowering a budget and raising it are separate grants. Setting a campaign's status to PAUSED and to ENABLED are separate grants. The most specific matching rule wins, and in a call touching several fields the strictest tier present governs the whole call.

Every update reads the current values before writing, which is not optional: it supplies the direction of a numeric change and the undo recorded in the journal. An update whose before-state cannot be read is refused rather than performed blind.

Managing permissions

ads-agent-policy edits the table through validation, and writes only the rules that differ from the shipped seed, so git diff shows exactly what you chose to allow.

ads-agent-policy list                      # what is granted now
ads-agent-policy resources                 # all 64 mutable resources, marked
ads-agent-policy fields campaign_budget    # every field, with its rules
ads-agent-policy show campaign update --field status --value ENABLED
ads-agent-policy grant campaign_budget update --field amount_micros \
    --direction increase --tier confirm --note "approved 7 Sep"
ads-agent-policy simulate campaign_budget update --field amount_micros \
    --value 40000000 --current 28000000
ads-agent-policy revoke campaign_budget update --field amount_micros \
    --direction increase
ads-agent-policy diff                      # your rules versus the seed

grant refuses to set autonomous without an --evidence rule, because an unattended write with no volume threshold acts on noise.

The table lives at ~/.ads-agent/capabilities.yaml. The shipped seed grants nothing autonomously, puts pausing and budget decreases at confirm, and names the dangerous operations as forbidden so the table documents them rather than staying silent about them.

Requirements

  • Python 3.11+

  • A Google Ads manager account. Developer tokens are issued only to manager accounts — the Admin → API Center menu does not exist on a regular account.

  • A developer token. Test access is immediate; Basic access is an application and is required for the Keyword Planner tools. See access tiers.

  • An OAuth client of type Desktop app, in a Google Cloud project with the Google Ads API enabled.

  • For autonomous mode only: the Claude Code CLI on PATH. The MCP server itself works with any MCP client.

  • For the bundled scheduler only: macOS (launchd). On Linux use cron with agent/run.sh.


Install

git clone https://github.com/<your-account>/google-ads-agent.git
cd google-ads-agent

python3 -m venv .venv
./.venv/bin/python -m pip install -e ".[dev]"

./.venv/bin/python -m pytest -q     # 90 tests, no credentials needed

The test suite is fully offline: it builds an API client with anonymous credentials and sends no requests, so it passes before you have any credentials.


Configure credentials

1. Developer token

Sign in to your manager accountAdmin → API Center. Copy the token. Apply for Basic access from the same page if you need the Keyword Planner tools.

2. OAuth client

Google Cloud Console → enable the Google Ads APICredentials → Create credentials → OAuth client ID → application type Desktop app.

Desktop app matters: it permits the loopback redirect the token script uses, so you don't have to register a redirect URI. A Web application client will reject the flow.

3. Refresh token

./.venv/bin/python scripts/get_refresh_token.py \
  --client-id YOUR_ID.apps.googleusercontent.com \
  --client-secret YOUR_SECRET \
  --developer-token YOUR_DEV_TOKEN \
  --login-customer-id 1234567890 \
  --write

--write produces ~/google-ads.yaml at mode 600. Without it the token is only printed. Sign in with an account that can reach the manager account.

--login-customer-id is the manager account's ID, digits only. It selects the hierarchy you act through, not the account you write to — that is a separate per-call argument.

If no refresh token comes back, the Google account has already granted this client. Revoke it at myaccount.google.com/permissions and retry; Google returns a refresh token only on first consent.

Set GOOGLE_ADS_DEVELOPER_TOKEN, GOOGLE_ADS_CLIENT_ID, GOOGLE_ADS_CLIENT_SECRET, GOOGLE_ADS_REFRESH_TOKEN, and optionally GOOGLE_ADS_LOGIN_CUSTOMER_ID. A config file takes precedence if it exists. Point GOOGLE_ADS_CONFIGURATION_FILE_PATH elsewhere to move it.


Use it interactively

Register the MCP server with your client. For Claude Code:

claude mcp add -s user google-ads -- \
  "$PWD/.venv/bin/python" "$PWD/run_server.py"

-s user matters: without it the server registers against the directory you ran the command in and is invisible everywhere else.

Verify with claude mcp list. Note that "connected" only means the process starts and speaks MCP — the API client is built lazily, so credential problems surface on the first tool call, not at startup.

Then ask for what you want:

List my Google Ads accounts.

Search campaign for project management software, 30/day, targeting Spain and the United States, pointing at https://example.com/signup. Preview it first.

Audit campaign 1234567890 and show me last 30 days broken down by network.

Always look at the network split before drawing conclusions from a search campaign's totals. Display expansion and search-partner spend are folded into the campaign number, so a campaign that appears to be buying search traffic can be spending most of its budget elsewhere. performance with segment="network" is the tool for that.


Run it autonomously

1. Write your policy

mkdir -p ~/.ads-agent
cp policy.yaml.example ~/.ads-agent/policy.yaml

Edit it. This file is the agent's mandate and the whole reason it can be trusted to act unattended, so keep it in version control and review changes to it the way you would review a permission grant. Anything omitted falls back to the defaults in ads_agent/policy.py.

The shipped example is deliberately inert: conversion_tracking_ok is false, so nothing autonomous will run until you verify your conversion tracking and change it.

2. Run once by hand

./agent/run.sh

Read the report it writes to ~/.ads-agent/reports/. Do this before scheduling anything.

3. Schedule it

./agent/install-schedule.sh          # daily at 07:15
./agent/install-schedule.sh 6 30     # daily at 06:30
./agent/install-schedule.sh --uninstall

Paths are derived from wherever the repo actually lives; nothing is hardcoded. Re-run it after moving the repo.

On Linux, add agent/run.sh to cron instead:

15 7 * * * /path/to/google-ads-agent/agent/run.sh

What a run produces

Path

Contents

~/.ads-agent/reports/

One dated Markdown report per run: actions taken, proposals, blocked attempts, data-quality notes

~/.ads-agent/logs/

Full transcript per run

~/.ads-agent/journal.jsonl

Append-only audit trail. Also the input to change-budget and cooldown enforcement

agent/prompt.md is the standing instruction set — edit it to change what the agent looks at and how it reports.

create_campaign and remove_campaign are deliberately absent from run.sh's allowed tools. Both are human-gated, so an unattended run must not be able to reach them.

Undoing a run

revert_last_run for account 1234567890

It replays the inverse operations stored in the journal: re-enabling what was paused and removing negatives the agent itself added. Campaign removals are recorded with an explicitly empty inverse, so they report as unrevertible rather than appearing to be undone.


Tool reference

Build

Tool

Writes

Purpose

list_accounts

Accessible accounts with currency, time zone, manager/test flags

suggest_geo_targets

Location name → numeric geo target ID

preview_campaign

Lint locally, then validate_only against the API. Returns the plan and a confirm token

create_campaign

Commit a previewed spec atomically, always paused

list_campaigns

Account inventory

get_campaign

Full read-back: settings, targeting, ad groups, keywords, RSA assets, ad strength

create_campaign requires a confirm_token that only a successful preview_campaign in the same process can mint. Campaign status is hardcoded to PAUSED with no parameter to override it, and the whole campaign goes in one mutation with partial_failure off — there is no path to a half-built campaign that sits invisible until it spends.

Analyse

Tool

Purpose

performance

Metrics by campaign, ad group, or ad. segment breaks it down by network, device, month, or date. Campaign level includes impression share and whether it is lost to budget or to rank

keyword_performance

Per-keyword metrics with quality score, sorted by cost. zero_conversions_only isolates spend with nothing to show for it

search_terms

The real queries that triggered ads, with added/excluded status. only_unadded surfaces negative-keyword candidates

audit_campaign

Structural review — targeting gaps, Display expansion left on, thin RSAs, poor ad strength, all-broad-match ad groups, keywords duplicated across ad groups, budget-to-bid mismatch. Uses no performance data, so it works on test accounts and brand-new campaigns

conversion_actions

Every conversion action with status, goal role, counting type, lookback window, and how many conversions it actually recorded. Diagnoses whether a low conversion count is a weak funnel or a broken tag

change_history

Who changed what, when, and from which client (web UI, Editor, API, scripts), with the fields that changed. Google retains 30 days

check_setup

Credentials, API version, and which API methods your token's access level permits. Run this first when something is refused

run_gaql

Arbitrary read-only GAQL. Only SELECT is accepted

check_setup probes each method with a real call — the one write probe uses validate_only and writes nothing. It is the fastest way to find out whether you are on Test, Explorer, or Basic access.

conversion_actions judges bidding inclusion from primary_for_goal, not the legacy include_in_conversions_metric field. Goal-based accounts ignore the legacy flag: verified on a live account where an action with include_in_conversions_metric=false still counted toward metrics.conversions.

Keyword Planner

All four KeywordPlanIdeaService methods. All require Basic access.

Tool

Purpose

suggest_keywords

Expand seed terms and/or a URL into ideas with volume, competition, and bid range

keyword_volumes

Volumes for a keyword list you already have. Reports on exactly what you pass; does not expand

forecast_campaign

Projected clicks, cost, CPC, conversions, and CPA at a given budget and bid. Run before preview_campaign

assign_ad_groups

Distribute keywords across ad groups that already exist

assign_ad_groups assigns and refines; it does not invent themes. Google's generate_ad_group_themes takes resource names of existing ad groups, so to split one oversized ad group you must create the themed ad groups first, then run this to distribute keywords into them.

Act

Tool

Purpose

policy_status

What is autonomous, propose-only, or forbidden; the evidence thresholds; remaining change budget; whether the trust switch is on

set_networks

Turn Display expansion and/or search partners off. A request to enable one is refused

add_negative_keywords

Campaign-level negatives, per-term evidence measured server-side

pause_keywords

Pause keywords spending without converting

pause_ad_groups

Same for whole ad groups, at higher thresholds

revert_last_run

Undo a run from the journal's stored inverses

All of these accept dry_run: true, which validates against the live API and journals the intent without writing. Dry-run entries do not consume change budget.

Remove

Tool

Purpose

remove_campaign

Permanently remove a campaign. Irreversible

Three gates, all required together:

  1. A confirm_token minted only by the tool's own preview, salted per process and bound to the specific account, campaign ID, and name.

  2. acknowledge_irreversible: true.

  3. The campaign must already be PAUSED (require_pause_before_removal), which makes removal two deliberate steps separated in time rather than one destructive one.

Called with no token it previews: name, status, budget, how many ad groups, keywords, and ads go with it, and recent spend and conversions. Historical stats remain queryable after removal, so reporting data is not lost — but the campaign can never be re-enabled.


Configuration reference

Environment variables

Variable

Default

Purpose

GOOGLE_ADS_CONFIGURATION_FILE_PATH

~/google-ads.yaml

Credentials file

ADS_AGENT_POLICY

~/.ads-agent/policy.yaml

Policy file

ADS_AGENT_JOURNAL

~/.ads-agent/journal.jsonl

Audit journal

ADS_AGENT_STATE

~/.ads-agent

State directory used by agent/run.sh

ADS_AGENT_MAX_DAILY_BUDGET

100

Refuse to create a campaign above this daily budget

ADS_AGENT_ALLOWED_CUSTOMER_IDS

unset

Comma-separated allowlist. Unset means any accessible account

Policy file

See policy.yaml.example, which documents every key. The main sections:

  • actions — the tier for each action

  • evidence — volume floors, conversion caps, and CPA ceilings per action

  • change_budget — per-run and per-week caps, per-entity cooldown

  • circuit_breaker — spend-spike multiple, learning-period length

  • conversion_tracking_ok / require_conversion_tracking — the trust switch

  • require_pause_before_removal — gate on campaign removal


Developer token access tiers

Access level gates methods, not just which accounts you can reach. Verified behaviour on Explorer access:

Works on Explorer

Requires Basic

All GAQL reporting (performance, keyword_performance, search_terms, audit_campaign, run_gaql, list_*, get_campaign)

Every Keyword Planner tool

suggest_geo_targets

mutate with validate_only

Explorer access reaches production data, so reporting working against your real account is not evidence that Basic access was approved. The Keyword Planner tools return authorization_error.DEVELOPER_TOKEN_NOT_APPROVED — "not allowed for use with explorer access" — until Basic is granted. Check the level in your manager account's API Center rather than inferring it from a successful query.

Test accounts, which a Test-access token is limited to, must live under a separate test manager account, not your production manager account.


Limitations

  • Search campaigns only. No Performance Max, Display, Video, Shopping, or Demand Gen. Performance Max in particular uses asset groups rather than ad groups, so it is a separate build rather than a flag.

  • No enabling or unpausing, ever. Forbidden outright, not gated. Bringing a campaign live is a human action in the Google Ads UI.

  • No budget or bid changes. Propose-only: the agent writes up the case and the numbers, you decide.

  • Auction insights is not available. The Google Ads API does not expose it in any version.

  • Committed writes are lightly exercised. The mutation path validates cleanly against the live API, but has seen limited real-world use. Start on a test account and use dry_run: true.

  • The bundled scheduler is macOS-only. Use cron elsewhere.

  • Autonomous mode requires the Claude Code CLI. The MCP server itself is client-agnostic.


Development

./.venv/bin/python -m pytest -q

90 offline tests: campaign spec linting, temp-ID wiring, the always-paused invariant, bidding-strategy oneofs, RSA assembly, GAQL query building, the read-only guard, every audit rule, and the whole policy engine — tiering, fail-closed behaviour, evidence floors, the CPA override, change budget, cooldowns, circuit breaker, and journal accounting.

Layout

ads_agent/
  client.py    lazy API client, readable error formatting
  spec.py      campaign spec, local lint, confirm tokens
  build.py     spec → atomic mutate operations
  report.py    GAQL query building, table formatting, audit rules
  policy.py    the policy engine
  journal.py   append-only audit log
  server.py    MCP tool definitions
agent/
  prompt.md              standing instructions for autonomous runs
  run.sh                 one scheduled run
  install-schedule.sh    generates and installs the launchd job
scripts/
  get_refresh_token.py   OAuth desktop flow

Notes for contributors

  • Never build an update mask with google.api_core.protobuf_helpers.field_mask(). It diffs by value, so setting a boolean to False produces a mask that omits the field and the API silently ignores the change. Every spend-reducing network change sets a boolean to False, so that helper turns set_networks into a no-op that reports success. Use explicit mask paths.

  • Verify field names against the installed library, not from memory. The API drifts; for example v25 uses campaign.start_date_time in "yyyy-MM-dd HH:mm:ss" format, not the older start_date.

  • Raise ToolError (from mcp.server.mcpserver.exceptions) for anything whose message the model needs to read. Any other exception has its text replaced with a generic "Error executing tool", which silently discards the API's validation messages.

  • Evidence must be measured server-side. If a future change lets a tool caller pass in the statistics that justify an action, the policy engine stops being a safety mechanism.

  • The API version is pinned in ads_agent/client.py (API_VERSION).


License

MIT. Set the copyright holder in LICENSE before publishing.

See the disclaimer above. This software is provided without warranty, and you are solely responsible for what it does in your advertising accounts.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections. Dates show when Glama detected each change.

No tool schema history has been recorded yet.

Maintenance

ActivityMaintained
ResponsivenessNo issues

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Connectors

Related MCP Servers

  • A
    license
    B
    quality
    B
    maintenance
    Open-source MCP server for Google Ads (Meta Ads coming soon) that lets AI assistants manage ad campaigns, reporting, keywords, and targeting in plain English from any MCP client, with safety-first creation of paused campaigns.
    54
    602
    2
    MIT
  • A
    license
    Not graded
    quality
    C
    maintenance
    MCP server for managing Google Ads campaigns through the official Google Ads API, covering accounts, campaigns, budgets, keywords, search terms, and keyword ideas. It provides tools for both reading and mutating live ads data, such as pausing campaigns, updating budgets, and adding keywords.
    MIT

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/lucagalvani/google-ads-agent'

If you have feedback or need assistance with the MCP directory API, please join our Discord server