adrails.dev / docs / with Google's official Ads MCP
Docs · two servers, one client

Use AdRails alongside Google's official read-only Google Ads MCP

Google's server answers questions about your account. AdRails is the guarded write path. Both are local stdio MCP servers, so one MCP client can run both.

The short answer

Google's official Google Ads MCP server is read-only: it gives agents tools to list accessible customers, look up resource metadata and run GAQL searches. AdRails adds policy-checked changes. Run both in the same client — read with Google's server, change with AdRails, where every write goes through a policy file no AdRails tool writes, a single-use plan_id, a validate-only pass and a pause-only scheduled guard.


What each server does

They overlap on nothing important: one is a read lens, the other is a reviewed write path. Reading is unlimited and free of consequence; changing is where the rails are.

Google's official google-ads-mcp

OFFICIAL · READ-ONLY
  • Official Google Ads MCP server.
  • Read-only: it does not change your account.
  • Tools to list the customers it can access.
  • Tools to look up resource metadata.
  • Tools to run GAQL searches.

AdRails (adsops)

LOCAL · GUARDED WRITES
  • Local MCP server for Google Ads; the Python package and module are named adsops.
  • An agent may read campaigns, status and search terms as often as it likes.
  • Every write lands in a reviewed desired-state file on disk, is diffed against live state, and must clear caps that live in a policy file no AdRails tool writes.
  • With require_plan_id on, every commit must quote a fresh, single-use plan_id issued for that exact plan.
  • A change that adds spend exposure is refused while the scheduled guard's heartbeat is stale.
  • Nothing is hosted: the only endpoint it talks to is the Google Ads API, with credentials from the sources you configure.

AdRails tools

ToolWhat it does
adsops_campaignsLists the campaigns this deployment may operate: keys, accounts, state files and the active commit policy.
adsops_statusLive read for one campaign: status, end date, daily budget, spend today and lifetime, negative count, whether caps come from the policy file, and the guard heartbeat's age.
adsops_termsClicked search terms for the last N days (1–90, default 14): term, matched keyword, clicks, cost.
adsops_planPure diff of the desired-state file against live state; writes nothing. Returns the operations, notes, ops_count, the exposure increases it found, and a single-use plan_id with its expiry.
adsops_applyValidate-only unless commit=true. A commit needs commit_policy.allow_commit, a clean git-tracked state file, a fresh guard heartbeat when the change adds exposure, and the plan_id when the policy requires it.
adsops_setProposes a budget / status / end-date / negatives change with a short reason; checks it against policy caps and live state, writes the state file and commits it to git. dry_run=true returns a plan_id instead of writing.
adsops_guardReads spend and pauses the campaign if the daily or lifetime threshold is crossed. Pause only: it never enables, raises a budget or edits keywords.
adsops_usageMetering summary for the tenant: calls, errors, units, ops and median latency per tool.

Run both servers side by side in the same MCP client. Google's server answers ad-hoc GAQL exploration; AdRails is the only write path — with caps, a validate pass, an account lock and an audit line in between.

Two servers, one client config

Both speak JSON-RPC over stdio, so one .mcp.json entry each is the whole setup. Diagnostics go to stderr; one object per line goes over stdio.

.mcp.json2 servers · stdio
{
  "mcpServers": {
    "adrails": {
      "command": "python3",
      "args": ["-m", "adsops", "--config", "/absolute/path/config.json"]
    },
    "google-ads": {
      // PLACEHOLDER — not an AdRails command.
      // See Google's installation docs for google-ads-mcp and copy
      // their exact command, args and env here, unchanged.
      "command": "<see Google's installation docs>",
      "args": ["<see Google's installation docs>"]
    }
  }
}

The AdRails block is the entry from the project README: point --config at the absolute path of your own config file. Everything AdRails needs — accounts, credential source, state files, policy files, log paths — lives in that file, not in this one. Google's server entry is deliberately a placeholder; AdRails does not ship, mirror or verify Google's launch command.

Keep the two configs separate in your head: Google's server holds its own credentials and reads only. AdRails reads credentials from the sources you configure in config.json (a service-account JSON file, a static token, or a secret-manager lookup) and writes only through adsops_plan / adsops_apply / adsops_set against your policy file.

A workflow that uses both

Explore freely with Google's read-only server, then make the change you decided on through AdRails, where it is diffed, capped, validated and logged.

  1. Read with Google's server

    Ask the agent for a GAQL search over the last 14 days: which search terms converted, which burned clicks with no conversions. Google's server lists the customers it can access, looks up resource metadata, and runs the search. Nothing is changed.

  2. Cross-check the campaign through AdRails

    adsops_status returns the live status, end date, daily budget, spend today and lifetime, negative count, whether caps come from the policy file, and how old the guard heartbeat is. adsops_terms gives the clicked search terms with clicks and cost.

  3. Propose a guarded change

    adsops_set with a budget, status, end-date or negative-keyword change and a short reason, run as dry_run=true. It checks policy caps and live state, then returns a single-use plan_id with an expiry — and nothing is written.

  4. Commit exactly that plan

    adsops_set … apply=true plan_id=… (or adsops_apply, which is validate-only unless commit=true). The commit needs commit_policy.allow_commit, a clean git-tracked state file, a fresh guard heartbeat when the change adds spend exposure, and the plan_id. Google validates the identical payload in validate-only mode first; if Google times out mid-commit, AdRails re-reads the account and reports what actually landed instead of retrying blind.

  5. Let the guard watch, and read the log

    A chat session cannot watch spend, so the pause-only spend check runs from your OS scheduler every 15 minutes (adsops guard-all, templates in deploy/). Agent changes are committed to git with their stated reason, and every call is metered and audited.

agent session · read then change2 servers
# google-ads (read-only): browsed, it did not write
▸ google-ads GAQL search · search_term_view · last 14 days ✓ 68 rows

# adrails: the write path, same campaign, same decision
▸ adsops_status campaign=search_core
✓ live   PAUSED? no · budget 18.00 · spend today 31.40 · guard heartbeat 6 min

▸ adsops_set campaign=search_core daily_budget_usd=40 dry_run=true
✗ refused  daily_budget_usd 40.0 outside guard range (policy max 25)

▸ adsops_set campaign=search_core daily_budget_usd=22 dry_run=true reason="winning terms, CPA under target"
✓ planned   budget 18.00 → 22.00 · exposure: budget up · plan_id 9f2c…e41a (single use · 15 min)

▸ adsops_set … apply=true plan_id=9f2c…e41a
✓ validated by Google · ✓ committed once · ✓ audited · git a71d9c0 "winning terms, CPA under target"

Limits of running both

Pairing the servers changes what an agent can do. It does not change what a Google Ads account can do to you.

  • It is a threshold with automatic pause attempts, not a spend ceiling. The guard compares spend with numbers you wrote and issues pause operations. That is containment, not billing control — keep platform-side billing limits as the real ceiling.
  • Google can deliver up to 2x the average daily budget on a given day. A daily budget is an average, not a maximum, and a pause is an API call that can be rejected or land late.
  • Reporting is delayed. The spend figures AdRails reads are minutes to hours behind delivery, so a threshold crosses on the account before the guard can see it.
  • The guard only acts when it is scheduled and the machine is awake. A chat-session MCP process is not a monitor: while the machine sleeps no checks run, spend keeps moving, and the heartbeat goes stale — which is why AdRails refuses changes that add spend exposure while it is stale.
  • An agent with shell access as the same user can bypass any local tool. AdRails constrains the tool calls it serves, not the machine underneath. Run the executor as its own user and sandbox the agent if that matters to you.
  • Two servers means two credential configurations to keep straight. Google's server and AdRails are configured independently; a fix in one does not fix the other, and neither proxies the other's calls.
  • Wrong negative keywords can suppress good traffic. Negatives are append-only here, so a bad one is also awkward to withdraw.
  • You are responsible for your account and your spend. AdRails is Apache-2.0 and free; it is not affiliated with Google, and running it is at your own risk.
Last tested with AdRails 0.2.2. Tool names, flags and policy fields on this page match that release; check the repository README and release notes if you are on a newer version.