Let Claude actually run your Google Ads — with a diff, a dry-run, and an undo before anything touches your budget.
A Google Ads MCP server that does more than read. It finds your wasted spend, your budget-capped winners, and your weak ad copy — then proposes the fix as a reviewable diff that you approve before a single cent moves.
Google's official Google Ads MCP server is read-only: three tools, no mutations. adsmith is the other half — the part that changes things, wrapped in the safety machinery that makes letting an LLM near an ad budget a reasonable idea.
uvx adsmith --demoThat's it. No OAuth, no developer token, no waiting on Google's approval queue.
--demo boots a synthetic ad account with realistically messy data, and every
tool works against it, including proposing, applying, and rolling back changes.
Wire it into Claude Code:
claude mcp add adsmith -- uvx adsmith --demoThen ask: "where am I wasting money?"
> where am I wasting money?
Pulling the search-terms report for Northwind Outdoor Gear...
$6,274.92 of your $14,525 spend over the last 30 days went to search terms
that produced zero conversions — about 43%. The pattern is clear:
| search term | 30-day cost | campaign |
|--------------------------------|-------------|------------------------|
| free hiking boots | $261.98 | Search - Hiking Boots |
| hiking boots repair near me | $238.70 | Search - Hiking Boots |
| northwind outdoor gear careers | $231.27 | Brand - Northwind |
| how to clean hiking boots | $215.41 | Search - Hiking Boots |
| used camping gear cheap | $191.82 | Search - Generic Outdoor |
Three intent groups worth blocking: job seekers, repair/rental, and
free-seekers. Want me to propose negatives?
> yes
Change-set cs_1a5956d1 (pending) on customer 1234567890
Rationale: Non-commercial intent
1. Negative keyword on Search - Generic Outdoor: + keyword=jobs
2. Negative keyword on Search - Generic Outdoor: + keyword=rental
3. Negative keyword on Search - Generic Outdoor: + keyword=diy
4. Negative keyword on Search - Generic Outdoor: + keyword=wikipedia
5. Negative keyword on Search - Generic Outdoor: + keyword=donation
Estimated daily spend decrease: 19.92
Estimated 30-day impact: 597.60
Expires: 2026-08-07T18:06:21+00:00
To apply: apply_changeset(changeset_id='cs_1a5956d1', confirm=True)
Nothing has been applied. Note I left "careers" alone — that spend is on your
brand campaign, which your guardrails protect.
Nothing was applied. That's the point.
The honest first reaction to "AI edits my ad account" is no. So here is exactly what stops it going wrong.
Every mutation takes the same path. There is no other route to the API:
propose_* → guardrails → validate_only dry-run → apply_changeset(confirm=True)
↓ ↓
BLOCK is final Google validates without committing
Guardrails live in a file on disk, outside the model's reach. No tool can
relax one. A BLOCK is terminal — the assistant can't argue past it, because the
check is Python reading YAML, not a suggestion in a prompt.
budgets:
max_increase_pct_per_day: 20 # no 10x raises from a stray decimal
max_decrease_pct_per_day: 30
account_daily_spend_ceiling: null # set this before automating anything
bids:
max_target_cpa_change_pct: 15 # big swings reset Smart Bidding's learning
pausing:
never_pause_if_conversions_last_30d: 5 # conversions lag; "dead" often isn't
max_keywords_paused_per_changeset: 50
allow_pausing_campaigns: false
protected:
campaign_ids: []
campaign_name_patterns: ["*Brand*"] # brand campaigns are cheap and high-ROAS;
# "optimizing" them is nearly always a lossAsk for something out of bounds and you get this, not a workaround:
Guardrails:
- [BLOCK] budgets.max_increase_pct_per_day (change 1): Raising the budget for
Search - Hiking Boots from 80.00 to 800.00 is +900.0%, above the 20% daily cap.
BLOCKED by guardrails — this change-set cannot be applied. The [BLOCK] entries
above are final.
These limits live in ~/.config/adsmith/guardrails.yaml. I cannot override them;
editing that file is the only way to change them.
The rest of the safety model:
- Approval is explicit.
apply_changesetwithoutconfirm=Truereturns the diff and changes nothing. - Dry run first. Every apply is validated through the API's
validate_onlymode before it commits. - Undo. Change-sets carry pre-built inverse operations. Where something genuinely can't be reversed (creating an ad), it says so plainly instead of implying a clean undo.
- Proposals go stale. A change-set built on last week's numbers expires rather than applying against an account that has moved.
- Guardrails re-run at apply time, not just at propose time — so tightening the policy invalidates a proposal that was already sitting in the queue.
- Everything is logged.
audit_historyanswers "what changed last Tuesday?" with every proposal, verdict, dry-run, application, and rollback.
Full detail in SECURITY.md.
Five slash commands, each a written analysis procedure rather than a black box. They're plain prose you can read and edit — including the statistical guardrails that stop an eager assistant acting on noise.
| Command | What it does |
|---|---|
/wasted-spend |
Mines search terms, groups them by intent (job seekers, DIY, free-seekers, repair), proposes negatives with a projected saving |
/budget-check |
Finds campaigns losing impression share to budget while hitting their CPA target, proposes a zero-sum reallocation |
/bid-tuning |
Compares actual CPA/ROAS to targets, proposes bounded steps — and refuses to act on fewer than 30 conversions |
/ad-audit |
Finds poor-strength RSAs, too-few headlines, over-pinned assets; proposes replacements that vary the angle, not the wording |
/weekly-report |
Digest with anomaly detection — and it checks whether a conversion cliff is a tracking break before calling it a performance drop |
Reporting (9) — curated reports, not a raw GAQL passthrough
| Tool | Returns |
|---|---|
list_accounts |
Every reachable account with currency and timezone |
account_overview |
Spend, conversions, CPA, ROAS vs. the preceding period |
campaign_performance |
Per-campaign metrics + impression share + budget |
ad_group_performance |
Per-ad-group metrics |
keyword_performance |
Per-keyword metrics with quality score |
search_terms |
What people typed, with wasted spend totalled |
ad_performance |
Ads plus a precomputed weak_ads diagnosis |
budget_pacing |
MTD spend, month-end projection, budget-capped campaigns |
run_gaql |
Read-only escape hatch — SELECT only |
Reports pre-aggregate totals and cap rows by default. When a report truncates it says so, and the totals still cover every row — a capped report never understates account spend.
Mutations (11) — all routed through change-sets
| Tool | Notes |
|---|---|
propose_negative_keywords |
Defaults to PHRASE; BROAD negatives block converting traffic too often |
propose_budget_change |
Takes a whole reallocation map as one reviewable change-set |
propose_bid_adjustment |
tCPA/tROAS only; refuses to switch bidding strategy |
propose_pause |
Keywords, ads, campaigns |
propose_rsa |
Validates Google's 30/90-character limits before proposing |
preview_changeset |
Diff + validate_only dry run |
apply_changeset |
Requires confirm=True |
rollback_changeset |
Replays stored inverses |
list_changesets |
Recent proposals and their status |
audit_history |
The full record |
show_guardrails |
Active policy and where it's loaded from |
adsmith scan does the analysis headlessly — point cron at it and the work
happens while you sleep.
adsmith scan --demo # digest of everything worth your attention
adsmith scan --propose # ...and queue change-sets for the clear-cut fixes9 finding(s), roughly 9,157.94 per 30 days at stake.
1. [WARNING] Search - Generic Outdoor: 2,384.42 on non-converting search terms
85 search term(s) spent money over 30 days with no conversions.
Queued: cs_ace79e8b (pending your approval)
3. [WARNING] Search - Camping Gear: CPA is +105% against target
Actual CPA 61.41 vs target 30.00 over 61 conversions.
5. [WARNING] Search - Hiking Boots is capped and beating its target
Losing 38% of impressions to budget on 80.00/day, at a CPA of 38.00
against a 45.00 target.
It never applies anything — --propose queues change-sets in pending, and
guardrails apply in full with nobody watching. The scan is deterministic Python,
not an LLM: it costs nothing per run, gives identical output for identical input,
and can't hallucinate a campaign ID. The judgement calls stay in the playbooks
where you're present to read them.
Exit codes are cron-shaped (0 clean, 1 error, 2 findings), so
adsmith scan || notify-me just works. Full setup, including systemd timers, in
docs/SCHEDULING.md.
adsmith --read-onlyUnregisters every mutation tool, so the server physically cannot write — removal, not refusal. A good way to spend a first week on a real account.
--demo needs nothing. A real account needs three things from Google, and the
developer token is the slow one — Google's review queue runs days to weeks, and
a fresh token only works against test accounts until Basic access is approved.
Start the token application on day one, then keep using --demo while you
wait. Full walkthrough in docs/SETUP.md.
pip install 'adsmith[live]'
adsmith auth # one-time OAuth; writes ~/.config/adsmith/credentials.json (0600)
adsmith init-config # writes ~/.config/adsmith/guardrails.yaml — edit it
claude mcp add adsmith -- adsmithSet account_daily_spend_ceiling in your guardrails before you do anything else.
Both are open source and both are Python. They solve different halves.
| googleads/google-ads-mcp | adsmith | |
|---|---|---|
| Reads | Raw GAQL passthrough | 9 curated reports + a GAQL escape hatch |
| Writes | — | 5 proposal tools behind change-set review |
| Guardrails | — | Policy file the model can't override |
| Undo | — | Inverse operations + audit log |
| Playbooks | — | 5 slash commands |
| Unattended scanning | — | adsmith scan (cron-shaped exit codes) |
| Read-only mode | Always read-only | --read-only opt-in |
| Try without a dev token | — | uvx adsmith --demo |
| Telemetry | Usage headers on API calls | None |
| Maintained by | This repo |
Use theirs if you want a thin, official, read-only wrapper. Use adsmith if you want the thing to actually improve the account.
You don't need a Google Ads account to contribute — the whole test suite runs against the demo backend. See CONTRIBUTING.md.
The one rule: nothing reaches the API except through a change-set. A tool
that calls mutate directly won't be merged, however convenient.
Alpha, and honest about it. 102 tests, CI on 3.12/3.13.
The safety layer is thoroughly covered. The live Google API path is tested against real protobuf types — message construction, enums, field masks, error translation — with the network stubbed, but it has never run against Google's actual servers. That's the part most likely to surprise you.
If you connect a real account: start with --read-only for a week, then tight
guardrails and one small change. docs/SETUP.md §7 has the round trip worth
doing first.
MIT.