Docs · Quickstart

Install AdRails with Claude CodeFirst validated plan in about 5 minutes · adrails.dev

AdRails is the open-source MCP server for Google Ads that Claude Code can read freely and write to only through your policy. This page installs it, registers it, schedules the guard and gets a first plan that changes nothing.

Direct answer. AdRails (adrails.dev) is a local, open-source MCP server that lets Claude Code read your Google Ads account freely and write to it only through a policy file no AdRails tool can write. You install the adsops package, point it at your own Google Cloud credentials, register it over stdio, and ask Claude for a plan. The first plan comes back as a diff with a single-use plan ID and commits nothing.

Last tested with AdRails 0.2.2 · Python 3 · macOS and Linux · Claude Code as the MCP client
What you need first

Your own Google Ads API access

AdRails has no hosted proxy and no token custody. It runs against your own Google Cloud project, which also follows Google's policy that each business uses its own Cloud project and access.

  • A Google Cloud project with the Google Ads API enabled, and a developer token for it.
  • A service account added to your Google Ads account with the access you want the agent to have, and its JSON key file on disk.
  • The customer ID of the account and the campaign ID of the one campaign you are starting with.
  • Python 3 on the machine, plus pipx or uvx, and Claude Code installed and working.
  • A git repository for the desired-state and policy files. git diff is the change review, and require_clean_git is on by default.
Steps

Six steps to a first validated plan

  1. Install the package

    The Python package and module are named adsops and the console script is adsops-mcp. There are no other dependencies: service-account tokens are signed with your system openssl when google-auth isn't installed.

    shell · from GitHubPyPI release coming soon
    # from GitHub (PyPI release coming soon)
    pipx install git+https://github.com/antonioac1/adrails     # console script: adsops-mcp
    # or run without installing:
    uvx --from git+https://github.com/antonioac1/adrails adsops-mcp --config /absolute/path/config.json

    Check it offline first. Everything is mocked and no network call is made.

    shell · offline check
    python3 -m adsops --selftest      # or: deploy/smoke_docker.sh
  2. Write a config

    Every account, credential source and path lives here. Paths are ~-expanded at load time. Save it somewhere absolute, for example ~/.adsops/config.json.

    config.json
    {
      "tenant": "your-tenant",
      "meter_log": "~/.adsops/meter.jsonl",
      "audit_log": "~/.adsops/audit.jsonl",
      "plan_ledger": "~/.adsops/plans.jsonl",
      "lock_dir": "~/.adsops/locks",
      "guard_heartbeat": "~/.adsops/guard.heartbeat",
      "credentials": {"google_ads": {"type": "service_account",
          "sources": [{"kind": "file", "path": "~/.adsops/sa.json"}]}},
      "google_ads": {"api_version": "v24", "end_field": "end_date_time", "login_customer_id": null},
      "commit_policy": {"require_clean_git": true, "allow_commit": true, "allow_set": true},
      "accounts": [{"key": "main", "platform": "google_ads", "customer_id": "000-000-0000",
          "campaigns": [{"key": "search_core", "state_file": "~/ads/search_core.json",
                         "policy_file": "~/ads/search_core.policy.json"}]}]
    }

    commit_policy defaults to require_clean_git: true and allow_commit: false, so commits and adsops_set stay off until you turn them on. The example above shows them enabled for a first run on a sandbox campaign; leave them off on a live account until you have read a few plans.

  3. Write the policy file

    One policy file per campaign, referenced by policy_file in the config. AdRails only ever reads it: no tool writes caps, and adsops_set refuses guards and allow_enable outright. If the state file also carries caps they are ignored, and adsops_status says so.

    search_core.policy.json
    {
      "customer_id": "000-000-0000",
      "campaign_id": 1000000000,
      "guards": {
        "max_daily_budget_usd": 40,
        "min_daily_budget_usd": 5,
        "max_end_date": "2026-12-31",
        "lifetime_spend_pause_usd": 900,
        "daily_spend_pause_usd": 60,
        "max_ops_per_run": 12
      },
      "allow_enable": false,
      "max_guard_age_min": 30,
      "require_plan_id": true,
      "plan_ttl_s": 900
    }

    Commit both the config-referenced files and the policy file to git so git diff is the change review. A missing or malformed field is an error that names the field, not a silent default.

  4. Register the MCP server in Claude Code

    Diagnostics go to stderr; the server speaks JSON-RPC 2.0 over stdio, one object per line. Use an absolute path for the config, and add the entry to the .mcp.json your Claude Code session reads.

    .mcp.json
    {
      "mcpServers": {
        "adrails": {
          "command": "python3",
          "args": ["-m", "adsops", "--config", "/absolute/path/config.json"]
        }
      }
    }

    Google's official google-ads-mcp server is read-only and can run beside AdRails in the same client: the agent explores ad-hoc GAQL with Google's server and writes only through AdRails.

  5. Schedule the guard

    A chat session cannot watch spend, so the pause-only spend check runs from your OS scheduler every 15 minutes. Templates for launchd, systemd and cron live in deploy/; render the placeholders (__PYTHON__, __CONFIG__, __PKG__, __LOGDIR__) and read deploy/GUARD.md before installing. The command all three units run is PYTHONPATH=$PKG $PY -m adsops --config $CFG guard-all.

    shell · macOS launchd
    PY=$(command -v python3); CFG=/absolute/path/config.json
    PKG=/absolute/dir/containing/adsops; LOGDIR="$HOME/.adsops/logs"
    mkdir -p "$LOGDIR"
    sed -e "s|__PYTHON__|$PY|g" -e "s|__CONFIG__|$CFG|g" \
        -e "s|__PKG__|$PKG|g" -e "s|__LOGDIR__|$LOGDIR|g" \
        deploy/launchd/com.adsops.guard.plist.template \
        > ~/Library/LaunchAgents/com.adsops.guard.plist
    plutil -lint ~/Library/LaunchAgents/com.adsops.guard.plist
    launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.adsops.guard.plist

    With max_guard_age_min set, any change that adds spend exposure is refused while the guard heartbeat is stale, so this step is what makes budget raises possible at all. The guard is pause-only: it never enables, raises a budget or edits keywords.

  6. Ask Claude for the first validated plan

    In Claude Code, ask it to list what it can operate, read live state, then plan. Three tool calls, no writes:

    CallWhat comes back
    adsops_campaignsCampaign keys, accounts, state files and the active commit policy for this deployment.
    adsops_statusLive 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_planA pure diff against live state: the operations, the notes, ops_count, the exposure increases it found, and a single-use plan_id with its expiry.

    A good opening prompt: “Call adsops_campaigns, then adsops_status for search_core, then adsops_plan. Don't commit anything, just show me the diff and the plan ID.” You now have a validated plan: the diff is against the live account, checked against your caps, and nothing has been submitted.

    shell · what the plan is, and is not
    ▸ adsops_plan campaign=search_core
    ✓ planned   diff vs live account · caps checked · exposure: none
                plan_id … (single use · 900 s)
                nothing written, nothing submitted

    Committing later is a separate call. adsops_apply validate-only unless commit=true, and 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. Google validates the identical payload in validate-only mode first; ambiguous failures are reconciled by re-reading the account, never blind-retried.

    Last tested with AdRails 0.2.2. Commands on this page are copied from the repository README; if a flag has moved, the README wins.
Troubleshooting

When the first run doesn't take

The MCP server doesn't appear in Claude Code

Check the config path is absolute and that python3 -m adsops --config … runs from the same shell Claude Code launches. Diagnostics go to stderr, not to the JSON-RPC stream; a server that prints startup errors to stdout is the usual cause of a client that hangs. python3 -m adsops --selftest isolates package problems from config problems.

Authentication keeps failing

AdRails reads credentials from the sources you configure: a service-account JSON file, a static token, or a secret-manager lookup. Confirm the JSON key file exists at the configured path, that the service account is added to the Google Ads account, and that the Cloud project has the Google Ads API enabled with a valid developer token. The login_customer_id field matters when you operate a manager account.

A commit or adsops_set is refused

Expected until you opt in. commit_policy.allow_commit defaults to false and allow_set stays off; and require_clean_git: true refuses to act on uncommitted edits to the state file. Commit the file, then try again.

“Guard heartbeat is stale”

The guard has not run inside max_guard_age_min. Check the scheduler unit is loaded and that the machine was awake during the last 15-minute window. Anything that adds spend exposure stays refused until a fresh heartbeat lands — that is the rail doing its job, not a bug.

The plan ID was rejected

A plan_id is single-use and expires after plan_ttl_s (default 900, range 60–86400). A changed, reused or expired plan is refused, so re-run adsops_plan and quote the new ID.

The campaign won't go to ENABLED

Setting a campaign to ENABLED needs allow_enable: true in the policy file. Re-enables are also treated as spend exposure and need a fresh heartbeat. Caps and allow_enable are the two things adsops_set refuses outright.

A customer or campaign ID mismatch

The policy file's customer_id is compared with the state file after stripping dashes, and campaign_id must match exactly. A mismatch is refused before anything is planned.

Limits

What this install does not protect you from

AdRails reduces risk; it does not remove it. Read these before pointing it at a live account.

  • A threshold, not a spend ceiling. Caps bound what AdRails submits. A daily budget is an average, not a maximum, and a pause is an API call that can be rejected or land late. Keep platform-side billing limits as the real ceiling.
  • Google can deliver up to 2× the average daily budget on a given day. Reporting is minutes to hours behind delivery, so the spend figures AdRails reads can understate what is already gone.
  • 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.
  • 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.
  • Wrong negative keywords can suppress good traffic. Negatives are append-only, so a bad one is also awkward to withdraw. List your protected terms.
  • You are responsible for your account and your spend. AdRails is Apache-2.0 and runs on your machine with your keys; there is no hosted partner holding the token for you.
Last tested with AdRails 0.2.2