AdRails
Provides tools for interacting with the Google Ads API to read campaigns, status, and search terms; plan and apply desired-state changes to budgets, status, end dates, and negative keywords; and run spend guard checks that can pause campaigns based on policy limits.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@AdRailsdiff my search_core desired-state file against live Google Ads and show the plan"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
AdRails
Guardrails for AI agents running your ads.
AdRails is a local MCP server for Google Ads. An agent may read campaigns, status and search
terms as often as it likes; every write has to land in a reviewed desired-state file on disk,
get diffed against live state, and clear caps that live in a policy file no AdRails tool writes.
The Python package and module are still named adsops — that is the name you install and run.
Three things it does differently:
Your keys. It runs on your machine against your own Google Cloud project and Google Ads credentials. Nothing is hosted: the only endpoint the package talks to is the Google Ads API, and credentials are read from the sources you configure (a service-account JSON file, a static token, or a secret-manager lookup).
Your caps. Caps live in a policy file that no AdRails tool writes; with
require_plan_idon, every commit must quote a fresh, single-useplan_idissued for that exact plan; and a change that adds spend exposure is refused while the scheduled guard's heartbeat is stale.Pay per call in sats. Optional hosted add-ons, Lightning-first, metered per call in sats — coming soon. Nothing in this release contacts any hosted service.
Testing
python3 -m adsops --selftest # 430 unit checks, everything mocked
python3 tests/qa_invariants.py # 200 random sequences against a simulated Google Ads account
python3 tests/qa_inputs.py # hostile policy/state/config inputs (NaN, Infinity, bad dates, bad UTF-8, ...)
python3 tests/qa_secrets.py # canary tokens injected everywhere; none may reach output or logsThe QA suites were written adversarially by a separate agent with no write access to adsops/, and every finding was fixed
or documented as intended behaviour.
Related MCP server: Google Ads MCP Server
What AdRails cannot guarantee
Read this before pointing it at a live account.
It is a threshold with automatic pause attempts, not a hard spend ceiling. The guard compares spend with numbers you wrote and issues pause operations. That is containment, not billing control.
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.
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. Sandbox the agent if that matters to you.
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. Keep platform-side billing limits as the real ceiling.
Quickstart
1. Install.
# 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.jsonNo other dependencies. Service-account tokens are signed with your system openssl
when google-auth isn't installed (install google-auth if you prefer it).
The core is stdlib-only. Check it offline (everything mocked, no network):
python3 -m adsops --selftest # or: deploy/smoke_docker.sh2. Write a config. Every account, credential source and path lives here; paths are
~-expanded at load time.
{
"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.
3. Write the policy file for the campaign (next section), and commit both files to git so
git diff is the change review.
4. Register the server in your MCP client. Diagnostics go to stderr; the server speaks JSON-RPC 2.0 over stdio, one object per line.
{
"mcpServers": {
"adrails": {
"command": "python3",
"args": ["-m", "adsops", "--config", "/absolute/path/config.json"]
}
}
}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:
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.plistThe command all three units run is PYTHONPATH=$PKG $PY -m adsops --config $CFG guard-all.
Tools
Tool | What it does |
| Lists the campaigns this deployment may operate: keys, accounts, state files, and the active commit policy. |
| Live 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. |
| Clicked search terms for the last N days (1–90, default 14): term, matched keyword, clicks, cost. |
| Pure diff of the desired-state file against live state; writes nothing. Returns the operations, notes, |
| Validate-only unless |
| Proposes a budget / status / end-date / negatives change with a short |
| Reads spend and pauses the campaign if the daily or lifetime threshold is crossed. Pause only: it never enables, raises a budget or edits keywords. Validate-only unless |
| Metering summary for the tenant: calls, errors, units, ops and median latency per tool. |
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 — the policy file always wins, and adsops_status
says so.
{
"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
}Field | Meaning |
| Account the campaign lives in; compared with the state file after stripping dashes. A mismatch is refused. |
| The one campaign this policy governs; must match the state file exactly. |
| Required. Upper bound of the daily budget, and the budget ceiling checked at plan time. |
| Optional. Lower bound of the daily budget; must not exceed the maximum. |
| Required, |
| Required. Lifetime spend at which the guard issues a pause. |
| Required. Today's spend at which the guard issues a pause. |
| Required integer. Most operations one run may emit; more is refused. |
| Default |
| Default |
| Default |
| Default |
A missing or malformed field is an error that names the field, not a silent default.
Desired-state file
One JSON file per campaign, referenced by state_file. It says what the campaign should look
like; the server computes the diff.
{
"customer_id": "000-000-0000",
"campaign_id": 1000000000,
"campaign_name": "Search - Core",
"status": "PAUSED",
"end_date": "2026-11-30",
"daily_budget_usd": 20,
"negatives_add": [
{"text": "free", "match_type": "BROAD"},
{"text": "cheap", "match_type": "PHRASE"}
],
"guards": {
"max_end_date": "2026-12-31",
"min_daily_budget_usd": 5,
"max_daily_budget_usd": 40,
"lifetime_spend_pause_usd": 900,
"daily_spend_pause_usd": 60,
"max_ops_per_run": 12
}
}Field | Meaning |
| Account the campaign lives in; dashes are stripped before use. |
| The one campaign this file owns. Both must match live state or the run aborts. |
| Desired status, |
| Desired end date, |
| Desired daily budget, bounded by the min/max guard band. |
| Append-only list of |
| Read only when the campaign has no |
Keep these files in git. That is what makes "reviewed desired state" real: git diff is the
change review, and require_clean_git makes the server refuse to act on uncommitted edits.
Works with Google's official read-only google-ads-mcp
Run both servers side by side in the same MCP client. Google's server answers ad-hoc GAQL
exploration and never writes; AdRails is the only write path. The agent reads with the official
server, proposes a change to a desired-state or policy file, and applies it through
adsops_plan / adsops_apply — with caps, a validate pass, an account lock, and an audit line
in between. Both speak JSON-RPC over stdio, so one .mcp.json entry each is the whole setup.
License
Apache-2.0.
This server cannot be deployed
Maintenance
Related MCP Connectors
Google Ads MCP: reports, search terms, negatives, budgets, campaigns. Approval on every write.
Open-source ad agent for LinkedIn Ads and Google Ads. Drafts campaigns, audiences and reports.
Run Google Ads and Meta Ads from ChatGPT or Claude: audit wasted spend, create and manage campaigns.
Google Ads, GA4, Search Console and Tag Manager for AI assistants. Every change is proposed first.
Related MCP Servers
- FlicenseAqualityDmaintenanceEnables AI agents to manage Google Ads campaigns (Search and UAC) through natural language, with read tools always available and guarded write operations for budgets, campaigns, ad groups, and keywords.3-
- AlicenseNot gradedqualityDmaintenanceEnables reading and mutating Google Ads campaigns via MCP tools, and includes structured marketer workflows for search term mining, budget optimization, and weekly reviews.27 npm6MIT
- AlicenseNot gradedqualityDmaintenanceEnables reading and writing the Google Ads API for full campaign management, with a dry-run/confirm safety flow on every write.MIT
- FlicenseNot gradedqualityCmaintenanceConnects AI assistants to Google Ads, Meta Ads, ChatGPT Ads, TikTok Ads and LinkedIn Ads so users can query live campaign performance, budgets and audiences in the account's own currency and prepare changes as proposals that only take effect after explicit approval. Reads run immediately, while every write is staged as an approvable proposal with before/after values, and newly created campaigns, ad sets and ads start paused.1-