Google Ads MCP Worker
by ambo-sk
README.md
# Google Ads MCP Worker
Google Ads MCP server on a Cloudflare Worker. Same shape as `mcp-gsc`: TypeScript, REST via `fetch`, no Google client library.
- Reimplementation of [FGRibreau/mcp-google-ads](https://github.com/FGRibreau/mcp-google-ads) (Rust, API v23) on **API v25**
- Auth: OAuth2 refresh token. The Google Ads API does **not** accept service accounts, so the GSC JWT approach does not apply here
- Transports: Streamable HTTP (`/mcp`) and legacy SSE (`/sse`)
> **Writes are unguarded.** No dry-run, no budget ceiling, no bid-increase cap, no two-step confirmation, no audit log.
> Every mutation tool applies immediately to whichever account `customer_id` points at. `create_campaign`,
> `create_ad_group` and `create_responsive_search_ad` default to `PAUSED`, but that is a default you can override,
> not a lock. Point this at a test account until you trust the setup.
## Setup
### 1. Google Ads developer token
Manager (MCC) account → **Tools & Settings → API Center** → apply for a developer token.
Until Basic Access is approved the token only works against **test accounts**.
### 2. OAuth client
1. Google Cloud Console → pick/create a project → enable the **Google Ads API**.
2. **APIs & Services → Credentials → Create credentials → OAuth client ID → Desktop app**.
3. Note the client ID and client secret.
### 3. Refresh token
```bash
npm install --legacy-peer-deps
npm run token -- <CLIENT_ID> <CLIENT_SECRET>
```
Opens a browser, asks for the `adwords` scope, prints `GOOGLE_ADS_REFRESH_TOKEN`.
Sign in as a Google account that has access to the Ads account you want to manage.
### 4. Deploy
```bash
npx wrangler secret put GOOGLE_ADS_DEVELOPER_TOKEN
npx wrangler secret put GOOGLE_ADS_CLIENT_ID
npx wrangler secret put GOOGLE_ADS_CLIENT_SECRET
npx wrangler secret put GOOGLE_ADS_REFRESH_TOKEN
npx wrangler secret put MCP_AUTH_TOKEN # any long random string: openssl rand -hex 32
npm run deploy
```
Set the non-secret defaults in `wrangler.jsonc`:
| Var | Meaning |
|---|---|
| `GOOGLE_ADS_CUSTOMER_ID` | Default account, digits only. Tools take `customer_id` to override per call. |
| `GOOGLE_ADS_LOGIN_CUSTOMER_ID` | Your MCC id, sent as `login-customer-id`. Leave empty if not under a manager. |
| `GOOGLE_ADS_API_VERSION` | Defaults to `v25`. Bump here when Google ships a new version. |
Local dev: copy `.dev.vars.example` to `.dev.vars`, then `npm run dev`.
### 5. Connect a client
```bash
claude mcp add --transport http gads https://gads-mcp.<your-subdomain>.workers.dev/mcp \
--header "Authorization: Bearer <MCP_AUTH_TOKEN>"
```
Claude Desktop / stdio-only clients:
```json
{
"mcpServers": {
"gads": {
"command": "npx",
"args": [
"mcp-remote",
"https://gads-mcp.<your-subdomain>.workers.dev/mcp",
"--header",
"Authorization: Bearer <MCP_AUTH_TOKEN>"
]
}
}
}
```
Then call `health_check` — it reports OAuth status, developer token presence, and the accounts reachable.
## Tools
46 tools. Every customer-scoped tool takes an optional `customer_id`.
### Read (21)
| Tool | What |
|---|---|
| `health_check` | Auth status, API version, configured accounts |
| `list_accounts` | Accessible accounts (with names when an MCC is configured) |
| `get_account_info` | Currency, time zone, auto-tagging, test-account flag |
| `run_gaql` | Any GAQL query; `searchStream` by default, paged when `page_size`/`page_token` given |
| `get_campaign_performance` | Campaign metrics by cost |
| `get_ad_group_performance` | Ad group metrics |
| `get_ad_performance` | Ad metrics with RSA headlines/descriptions and approval status |
| `get_keyword_performance` | Keyword metrics with match type, quality score, bid |
| `get_search_terms` | Actual queries — the negative keyword source |
| `get_negative_keywords` | Campaign / ad group / shared-set negatives |
| `get_geo_performance` | Metrics by location |
| `get_device_performance` | Metrics by device |
| `get_change_history` | Change events (API retains 30 days) |
| `list_assets` | Assets and their attachment level |
| `get_policy_issues` | Disapproved and limited ads with policy topics |
| `get_conversion_actions` | Conversion actions with category, counting, lookback |
| `list_recommendations` | Google's recommendations plus impact estimates |
| `search_geo_targets` | Place name → geo target constant id |
| `discover_keywords` | Keyword ideas from seeds and/or a URL |
| `get_keyword_historical_metrics` | Volume/competition for a fixed keyword list |
| `get_keyword_forecasts` | Forecast for a hypothetical campaign |
### Write (25)
| Tool | What |
|---|---|
| `create_campaign_budget` | Daily budget, shared or not |
| `create_campaign` | Campaign + optional budget in one call |
| `update_campaign` | Name, status, budget, dates, bidding strategy |
| `create_ad_group` / `update_ad_group` | Ad groups and default CPC |
| `add_keywords` / `update_keyword` | Positive keywords, bids, final URLs |
| `add_negative_keywords` | Campaign or ad group negatives |
| `create_responsive_search_ad` | RSA with pinning; length limits checked before sending |
| `set_entity_status` | Pause / enable / remove anything by resource name |
| `add_geo_target` | Target or exclude locations, with bid modifiers |
| `set_campaign_geo_target_type` | Presence vs presence-or-interest |
| `set_ad_schedule` | Dayparting windows and bid modifiers |
| `add_audience_targeting` / `create_custom_audience` | Audiences |
| `create_asset` / `attach_assets` / `remove_asset_link` | Sitelinks, callouts, snippets, text, images |
| `create_conversion_action` / `update_conversion_action` | Conversion setup, primary/secondary |
| `create_bidding_strategy` | Portfolio strategies |
| `apply_recommendation` / `dismiss_recommendation` | Act on Google's suggestions |
| `mutate` | Raw mutate on any resource collection — covers everything above |
| `mutate_batch` | Atomic `googleAds:mutate` with temp ids across resources |
`mutate` and `mutate_batch` are the escape hatches: anything Google adds, or any resource with no named tool
(shared sets, user lists, asset groups, experiments, labels), goes through them without a code change.
Both accept `validate_only` if you want a manual dry run.
## Checks
```bash
npm run typecheck # tsc --noEmit
npm test # pure-logic selfcheck: micros, dates, resource names, bidding payloads
```
## Notes
- Money is passed in currency units (`25.5`) and converted to micros internally.
- Mutate payloads use `YYYYMMDD` dates; reporting uses `YYYY-MM-DD`. Tools take `YYYY-MM-DD` everywhere and convert.
- `updateMask` uses snake_case field names — relevant if you hand-write operations for `mutate`.
- Access tokens are cached per Worker isolate; refresh tokens do not expire unless revoked or unused for 6 months.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessSyncing