Skip to main content

adsmith

Let Claude actually run your Google Ads — with a diff, a dry-run, and an undo before anything touches your budget.

CI PyPI License Google Ads API

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.


Try it in 30 seconds — no Google account needed

uvx adsmith --demo

That'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 --demo

Then 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.


Safety, up front

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 loss

Ask 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_changeset without confirm=True returns the diff and changes nothing.
  • Dry run first. Every apply is validated through the API's validate_only mode 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_history answers "what changed last Tuesday?" with every proposal, verdict, dry-run, application, and rollback.

Full detail in SECURITY.md.


Playbooks

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

Tools

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

Running it unattended

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 fixes
9 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.

Read-only mode

adsmith --read-only

Unregisters every mutation tool, so the server physically cannot write — removal, not refusal. A good way to spend a first week on a real account.

Connecting 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 -- adsmith

Set account_daily_spend_ceiling in your guardrails before you do anything else.


How this differs from Google's official server

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 Google 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.


Contributing

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.

Status

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.

Licence

MIT.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

adsmith-0.1.0.tar.gz (97.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

adsmith-0.1.0-py3-none-any.whl (83.6 kB view details)

Uploaded Python 3

File details

Details for the file adsmith-0.1.0.tar.gz.

File metadata

  • Download URL: adsmith-0.1.0.tar.gz
  • Upload date:
  • Size: 97.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for adsmith-0.1.0.tar.gz
Algorithm Hash digest
SHA256 3bce0e0a27a7043bc5869cd91dabb2678d8a1b47d6eb8e76321c8269885ade99
MD5 ab760140a880e76ff3119ca9ac7faca4
BLAKE2b-256 a19f7a09b7443fede2339ad528a72c6837d2370d52023c0ce8aecd062e3699c1

See more details on using hashes here.

Provenance

The following attestation bundles were made for adsmith-0.1.0.tar.gz:

Publisher: release.yml on sujayrittikar/adsmith

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file adsmith-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: adsmith-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 83.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for adsmith-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 effeee926064f2c230f5188e5c14ac31ee3a6c830ce1cdb10fd0a4c2ad418a43
MD5 26f4bc61ad1a8a6128daab0d659cdadb
BLAKE2b-256 4f1f33a370a7386aaf785df5f8621bd220abbe82d0ec774a02d8f4f3cf6c350a

See more details on using hashes here.

Provenance

The following attestation bundles were made for adsmith-0.1.0-py3-none-any.whl:

Publisher: release.yml on sujayrittikar/adsmith

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page