Skip to main content
Glama
YerayRodri

google-ads-write-mcp

by YerayRodri
README.md
# google-ads-write-mcp

MCP server that exposes **write** operations on Google Ads: campaigns, ad
groups, keywords, RSA ads, sitelinks, images, Customer Match audiences and
recommendations.

**Complements the official `google-ads` MCP** (read-only). Use the official
one (or [google-ads-kw-mcp](../google-ads-kw-mcp)) to read data, this one to
write.

## Tools (26)

### Campaigns
| Tool | What it does |
|---|---|
| `update_campaign_status` | Pause, enable or remove a campaign |
| `update_campaign_budget` | Change the daily budget (locates the budget entity automatically) |

### Ad groups
| Tool | What it does |
|---|---|
| `update_ad_group_status` | Pause, enable or remove an ad group |
| `update_ad_group_cpc_bid` | Change the ad group's max CPC bid |

### Keywords
| Tool | What it does |
|---|---|
| `add_keywords` | Add keywords to an ad group (BROAD/PHRASE/EXACT, optional bid) |
| `update_keyword_status` | Pause, enable or remove a keyword |
| `update_keyword_bid` | Change a keyword's CPC bid |
| `add_negative_keywords` | Add negative keywords at campaign level |

### RSA ads
| Tool | What it does |
|---|---|
| `update_rsa_headlines` | Replace headlines (and optionally descriptions) of an RSA |

### Sitelinks
| Tool | What it does |
|---|---|
| `create_sitelink_asset` | Create a sitelink asset |
| `link_sitelink_to_campaign` | Link the sitelink to a campaign |

### Ad assets
| Tool | What it does |
|---|---|
| `create_callout_asset` | Create a callout text (max 25 chars) |
| `create_call_asset` | Create a call extension with a phone number |
| `create_structured_snippet_asset` | Create a structured snippet (header + list of values) |
| `link_asset_to_campaign` | Link any asset to a campaign (SITELINK, CALLOUT, CALL, STRUCTURED_SNIPPET, MARKETING_IMAGE…) |

### Images
| Tool | What it does |
|---|---|
| `upload_image_asset` | Download an image from a URL and upload it as an asset (Display/PMax) |

### Audiences / Customer Match
| Tool | What it does |
|---|---|
| `list_user_lists` | List all audiences (remarketing + Customer Match) |
| `create_customer_match_list` | Create a new Customer Match list |
| `upload_customer_match_emails` | Upload emails to a list (SHA-256 hashing done automatically) |

### Create full campaigns
| Tool | What it does |
|---|---|
| `create_campaign` | Create a Search or Performance Max campaign (budget, bid, country, language, EU political-ads flag) |
| `create_ad_group` | Create an ad group inside a Search campaign |
| `create_rsa` | Create a full RSA ad (3–15 headlines, 2–4 descriptions, URL, display paths) |
| `create_asset_group` | Create a PMax asset group (headlines, long headlines, descriptions, images, logo) |

### Recommendations
| Tool | What it does |
|---|---|
| `list_recommendations` | List pending recommendations (optional type filter) |
| `apply_recommendation` | Apply a recommendation |
| `dismiss_recommendation` | Dismiss a recommendation |

## Security

- Every tool ships with [MCP Tool Annotations](https://modelcontextprotocol.io/specification)
  (`readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint`), so MCP clients can use
  them to decide whether to prompt for confirmation before running a tool.
- Execution errors propagate as real MCP protocol errors (`isError: true`), not as a JSON payload
  that looks like a success with an `"error"` key buried inside — so the calling model actually
  sees the failure and can self-correct instead of silently treating it as a success.

## Setup

Same credentials as [google-ads-kw-mcp](../google-ads-kw-mcp) — you can point
both at the same `credentials.json`.

```bash
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
```

## MCP client configuration

```json
{
  "mcpServers": {
    "google-ads-write": {
      "command": "/path/to/.venv/bin/python3",
      "args": ["/path/to/google-ads-write-mcp/server.py"],
      "env": {
        "GOOGLE_ADS_CREDENTIALS_PATH": "~/.config/google-ads-mcp/credentials.json",
        "GOOGLE_ADS_DEVELOPER_TOKEN": "<your developer token>",
        "GOOGLE_ADS_LOGIN_CUSTOMER_ID": "<your MCC customer id, no dashes>"
      }
    }
  }
}
```

| Env var | Purpose |
|---|---|
| `GOOGLE_ADS_CREDENTIALS_PATH` | Path to the credentials JSON (default: `~/.config/google-ads-mcp/credentials.json`) |
| `GOOGLE_ADS_DEVELOPER_TOKEN` | Your Google Ads developer token |
| `GOOGLE_ADS_LOGIN_CUSTOMER_ID` | Your MCC/login customer ID, no dashes |
| `GOOGLE_ADS_API_VERSION` | Google Ads API version (default: `v25`, pinned in `server.py`). All ~80 mutate calls in this server inherit it from the client. It's deliberately fixed rather than left to the `google-ads` library's default, so a `pip install --upgrade google-ads` can't silently move every write operation to a newer API version. Only override after reading the [release notes](https://developers.google.com/google-ads/api/docs/release-notes) — must match [google-ads-kw-mcp](../google-ads-kw-mcp)'s version if you run both against the same account. |

## Confirmation required for critical operations

These tools require `confirmed=True`. Called without it, they return a JSON
with `requires_confirmation: true` that the calling agent MUST show to the
user and wait for a response before repeating the call with `confirmed=True`.

| Tool | When it asks for confirmation |
|---|---|
| `update_campaign_status` | status = PAUSED or REMOVED |
| `update_campaign_budget` | always |
| `update_ad_group_status` | status = REMOVED |

```
# 1. First call — no confirmed
update_campaign_status(customer_id="1234567890", campaign_id="111222333", status="PAUSED")
# → returns requires_confirmation: true → show it to the user

# 2. Second call — after the user confirms
update_campaign_status(customer_id="1234567890", campaign_id="111222333", status="PAUSED", confirmed=True)
```

## Gotchas found in production use

### Auth
- **Never use `gcloud auth print-access-token`** — that token doesn't carry
  the `adwords` scope → 403. The only valid source is the `refresh_token`
  against `oauth2.googleapis.com/token`, which this server already does
  correctly.

### Negatives vs. positives
- Adding a negative EXACT keyword to an ad group where the same keyword
  already exists as a positive EXACT match returns `IMMUTABLE_FIELD`. The fix
  is to **pause the positive** instead of adding the negative.

### Resource names
- `adGroupCriteria` resource names always need a `~` between the ad group ID
  and the criterion ID: `customers/{cid}/adGroupCriteria/{ag_id}~{crit_id}`.

### GAQL field limits
- `ad_group_criterion` doesn't accept metrics (clicks, cost, conversions) in
  the same query — query `search_term_view` separately for performance data.
- Filter `ad_group_criterion` by campaign with
  `WHERE ad_group.campaign = 'customers/{cid}/campaigns/{campaign_id}'`, not
  `campaign_id` directly.

### Budgets
- The budget is a separate entity (`CampaignBudget`). If a campaign shares a
  budget with others, changing it affects all of them.

### `contains_eu_political_advertising`
- This is an enum (`EuPoliticalAdvertisingStatus`), not a bool. `False`
  serializes as `UNSPECIFIED`, which the API rejects as "not present". Use
  `DOES_NOT_CONTAIN_EU_POLITICAL_ADVERTISING` explicitly.

### PMax with Brand Guidelines
- If the account has Brand Guidelines enabled, PMax campaigns need a
  `business_name` and a `logo` (min. 128×128px, square) linked as
  `CampaignAsset` at creation time — not after.

### RSA headline updates
- Use `AdService.mutate_ads()` with field mask `"responsive_search_ad.headlines"`.
  Using `AdGroupAdService` with `"ad.responsive_search_ad.headlines"` gives
  `IMMUTABLE_FIELD`.

### Removing a keyword
- Use the `remove` operation on `AdGroupCriterionOperation`. Trying an
  `update` with `status = REMOVED` gives `INVALID_ENUM_VALUE`.

### REMOVED resources are immutable
- No write operation works on ad groups, keywords or ads whose parent
  campaign has `status = REMOVED`
  (`OPERATION_NOT_PERMITTED_FOR_REMOVED_RESOURCE`).

### Customer Match allowlist
- `upload_customer_match_emails` fails with
  `CUSTOMER_NOT_ALLOWLISTED_FOR_THIS_FEATURE` unless your developer token has
  explicit Customer Match access. `create_customer_match_list` works without
  it — only the email upload needs the allowlist.

## Notes

- Monetary amounts are passed in the account's currency units and converted
  to micros internally (1 unit = 1,000,000 micros).
- `customer_id` is always passed without dashes.
- `update_rsa_headlines` replaces ALL current headlines — pass the full
  desired list, not just the new ones.
- Customer Match lists can take up to 48h to process on Google's side.

## License

MIT — see [LICENSE](LICENSE).

Maintenance

ActivityMaintained
ResponsivenessNo issues