Metadata-Version: 2.4
Name: google-ads-mcp-write
Version: 0.1.0
Summary: Self-hosted MCP server for Google Ads Search campaigns. Forks googleads/google-ads-mcp and adds a guardrailed write layer (validate -> diff -> confirm -> commit), shaped reporting tools, and a local undo journal.
Author-email: Dean Lukies <lukiesd@users.noreply.github.com>, Mattia Tommasone <Raibaz@users.noreply.github.com>, Vladimir Iachimovici <iachimovici@gmail.com>
License-Expression: Apache-2.0
Project-URL: homepage, https://github.com/iachi/google-ads-mcp-write
Project-URL: repository, https://github.com/iachi/google-ads-mcp-write.git
Project-URL: issues, https://github.com/iachi/google-ads-mcp-write/issues
Keywords: Google Ads,Google Ads API,MCP
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: End Users/Desktop
Classifier: Programming Language :: Python
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
License-File: NOTICE
Requires-Dist: google-ads>=32.0.0
Requires-Dist: mcp[cli]==2.0.0
Requires-Dist: fastmcp>=4.0.3
Requires-Dist: google-auth-oauthlib
Provides-Extra: dev
Requires-Dist: black; extra == "dev"
Requires-Dist: nox<2026,>=2025.5.1; extra == "dev"
Provides-Extra: firestore
Requires-Dist: py-key-value-aio[firestore]<0.5.0,>=0.4.5; extra == "firestore"
Dynamic: license-file

# Google Ads MCP Server (write fork)

Manage your Google Ads account by talking to an AI assistant (like Claude)
instead of clicking through the Ads UI or a rules engine -- read
performance, find wasted spend, adjust budgets and bids, pause things,
build campaigns -- with every change shown to you as a plain before/after
diff before it touches your account, and a one-command undo if you change
your mind.

This is a fork of Google's own official
[googleads/google-ads-mcp](https://github.com/googleads/google-ads-mcp)
(which is read-only) that adds a guardrailed **write** layer, a few
marketer-specific reporting tools, and a local undo journal. See
[NOTICE](NOTICE) for attribution.

## Is this for you?

If you manage Google Ads accounts -- in an agency, in-house, or as a
freelancer -- and you're comfortable typing into Claude, this is built for
you specifically. You don't need to write code or touch the Google Ads API
directly; the one-time setup below is a guided script, not a dev
environment. If you're technical and want to self-host this for a team,
skip to [Advanced: hosting and other MCP
clients](#advanced-hosting-and-other-mcp-clients) once you've read the
safety model.

**What it's actually good for.** Being honest about this rather than
overselling it: this tool's strongest, most differentiated value is in two
places. First, the search-term-review-to-negative-keyword workflow, since
that's consistently the single biggest recurring time-sink in PPC account
management. Second, and more importantly, the local **undo journal** --
no other tool in this space, including Google's own account change history,
gives you a real one-command rollback for a mutation you regret. Building
brand-new campaigns from scratch is *not* this tool's strongest use case --
Google Ads Editor already does that well, for free, and campaign creation
isn't something you do often anyway. Think of this as "safely execute and
undo the fixes you already decided on," not "replace your whole PPC
toolkit."

## What you need

- **A Google Ads account you have access to** (yours, your client's, or
  your employer's).
- **A free Google Cloud project with the Google Ads API enabled.** This is
  not optional and it is not this project's infrastructure -- it's a
  policy requirement (see [Credentials and hosting
  policy](#credentials-and-hosting-policy) below) and it takes about 10
  minutes with the setup script.
- **About 15-20 minutes** for the one-time setup. You won't need to touch
  this again afterward.
- **Python** installed (the setup script checks for everything else,
  including whether you have `gcloud` and offers to install it).

## Quick start (recommended path)

This runs entirely on your own computer. Nothing you set up here is shared
with anyone, including us -- there is no hosted version of this tool; see
[why](#credentials-and-hosting-policy).

1.  **Install it.** No git clone, no virtualenv to manage -- if you don't
    have [pipx](https://pipx.pypa.io/stable/#install-pipx) yet, install
    that first (`brew install pipx` on macOS), then:

    ```shell
    pipx install google-ads-mcp-write
    ```

2.  **Run the guided setup command:**

    ```shell
    google-ads-mcp-write-setup
    ```

    This walks you through everything that can be automated -- signing
    into `gcloud`, creating a Cloud project, enabling the Google Ads API --
    using real commands instead of you clicking through Google Cloud
    Console screens. It then opens your browser at exactly the two steps
    Google requires a human to click through by design (creating the OAuth
    client, and granting consent) and writes the result to a local `.env`
    file in whatever directory you ran it from. At the end it tells you
    plainly what's still manual: applying for API access on your Cloud
    project, and creating a Google Ads test account if you want one for
    practice before touching a live account.

    If something goes wrong partway through, the command is safe to
    re-run -- it picks up from wherever you left off.

3.  **Add the server to Claude Desktop.** Open Claude's settings, find the
    MCP server configuration, and add:

    ```json
    {
      "mcpServers": {
        "google-ads-mcp-write": {
          "command": "/absolute/path/to/google-ads-mcp-write",
          "env": {
            "GOOGLE_ADS_CLIENT_ID": "YOUR_OAUTH_CLIENT_ID",
            "GOOGLE_ADS_CLIENT_SECRET": "YOUR_OAUTH_CLIENT_SECRET",
            "GOOGLE_ADS_REFRESH_TOKEN": "YOUR_REFRESH_TOKEN"
          }
        }
      }
    }
    ```

    The three credential values come straight out of the `.env` file the
    setup command wrote. For `command`, run `which google-ads-mcp-write`
    in the same terminal you installed it in and paste that path -- Claude
    Desktop is a separate app and may not see your shell's PATH, so the
    bare command name alone can fail to resolve even though it works in
    your terminal. If your Google Ads account sits under a manager (MCC)
    account, also add `"GOOGLE_ADS_LOGIN_CUSTOMER_ID": "YOUR_MANAGER_CUSTOMER_ID"`
    -- see [Login Customer Id](#login-customer-id) below.

    Other MCP clients (Claude Code, Cursor, VS Code with Copilot) use the
    same `mcpServers` JSON block in their own settings file -- see [Other
    MCP clients](#other-mcp-clients).

4.  **Restart Claude and try it.** Ask something like *"what customers do
    I have access to?"* to confirm it's connected, then jump to [What you
    can actually do with it](#what-you-can-actually-do-with-it) below for
    real workflows to try.

If you'd rather build from source, contribute to the project, or the
command gets stuck on a step, the full manual walkthrough is in [Manual
setup and troubleshooting](#manual-setup-and-troubleshooting).

## What you can actually do with it

These are organized by the actual job, not by tool name -- the tool names
in parentheses are what's happening under the hood if you want to look
them up in the [full tool reference](#full-tool-reference).

### Find and cut wasted spend

The classic, highest-value PPC task: find search terms that are costing
you money without converting, and block them.

```
Show me search terms that got clicks but zero conversions in the last 30 days for customer 1234567890
```

*(`get_negative_keyword_candidates`)* This comes back sorted by wasted
spend, and flags anything that's already an active keyword in your account
(you'd want to pause that keyword rather than also negative it). Review
the list, then:

```
Add "leather sofa cheap" as an exact-match negative keyword to that ad group
```

*(`add_negative_keywords`)* You'll see the exact change before it commits
-- nothing happens to your account until you say so.

### Understand where you stand

```
What's the account structure for customer 1234567890?
How is my campaign performance this week?
What are my top search terms by cost?
```

*(`get_account_structure`, `get_campaign_performance`,
`get_ad_group_performance`, `get_keyword_performance`, `get_search_terms`)*

### Check what Google itself is suggesting

```
What recommendations does Google have for this account?
```

*(`get_recommendations`)* Worth a caveat: these are Google's own
suggestions, and practitioners are often right to be skeptical of them --
treat this as one more input to judge, not an instruction to follow.

### Adjust budgets, bids, and status

```
Raise the daily budget on campaign X to $50
Pause the "Winter Sale" campaign
Lower the CPC bid on this ad group to $1.20
```

*(`update_budget`, `pause`/`enable`, `update_keyword`, `update_ad_group`)*

### Build something new

```
Create a new paused Search campaign called "Spring Launch" with a $30 daily budget
Add these keywords to the new ad group: ...
Write a responsive search ad for this ad group with these headlines and descriptions: ...
```

*(`create_campaign`, `create_ad_group`, `add_keywords`,
`create_responsive_search_ad`)* New campaigns, ad groups, and ads are
always created **paused** by default, so nothing spends until you
explicitly enable it.

### See what changed, and undo it

This is the tool's real safety net, and it's worth using on purpose, not
just when something goes wrong.

```
What have you changed in this account recently?
Undo that last change
```

*(`list_recent_changes`, `undo_change`)* Every commit is logged locally
before it happens. Undo re-reads the current state and reverts through the
same review-before-commit process -- it's a reviewed write too, not a
silent rollback. The one thing that can't be undone is removing a keyword
outright (`remove_keywords`) -- prefer pausing a keyword over removing it
if you might want it back.

## How the safety model works

Every write tool follows the same four steps, always:

```
1. READ     current state of what you're about to change
2. VALIDATE a real dry-run against the Google Ads API
3. DIFF     shows you exactly what would change, before/after
4. COMMIT   only after you explicitly approve -- nothing happens silently
```

**Nothing commits on the first call.** You'll always see the diff first.
Only when you (or the assistant, after you've said yes) calls the tool
again with `confirm=true` does anything actually change in your account.
Every commit is journaled locally -- to `~/.google-ads-mcp-write/journal.jsonl`
(override with `GOOGLE_ADS_MCP_JOURNAL_PATH`) -- *before* the change is
sent, so there's always a record even if something goes wrong mid-write.

## Credentials and hosting policy

This section exists because Google's Ads Developer Policies name MCP
servers explicitly and prohibit operating a hosted, shared "programmatic
proxy" to the Google Ads API. This project stays inside the policy's
open-source carve-out, which is why setup looks the way it does:

- **You bring your own Google Cloud project and your own Google Ads
  access.** No credentials of any kind ship with this software, in
  examples or otherwise -- that's not a corner we cut, it's not allowed to
  exist any other way.
- **This runs locally on your machine**, under your own account, with no
  shared or hosted endpoint. (The FastMCP OAuth-proxy / Streamable-HTTP
  path documented under [Advanced](#advanced-hosting-and-other-mcp-clients)
  is for running your *own* private instance across your own multiple
  local clients -- it is not a green light to operate a shared or
  multi-tenant deployment of this fork's write tools. Do that and Google's
  Developer Secondary Interface Review requirements land on you.)
- **Every write requires your explicit confirmation** before it touches
  your account. This isn't just good UX -- it's a compliance requirement.
  Google's policy requires that you, the account owner, are the one
  authorizing changes, not a fully autonomous background process.

This is not legal advice. Get your own compliance read from the Google Ads
API team before hosting any part of this for other people.

## Full tool reference

### Read tools

- `search`: Retrieves information about the Google Ads account.
- `get_resource_metadata`: Retrieves metadata about a Google Ads API resource type, for example "campaign". This is useful to understand the structure of the data and what fields are available for querying.
- `list_accessible_customers`: Returns ids of customers directly accessible
  by the user authenticating the call.
- `get_account_structure`: Campaigns -> ad groups -> ads/keywords, names/IDs/status only. Cached for the life of the server session.
- `get_campaign_performance`, `get_ad_group_performance`, `get_keyword_performance`, `get_search_terms`: Shaped performance reports (impressions, clicks, cost, conversions). Default to the last 30 days, cap the number of rows returned, and convert cost fields out of the API's micros into plain currency units.
- `get_recommendations`: Google's own auto-generated account/campaign recommendations (the same ones shown in the Ads UI's "Recommendations" page), with Google's estimated impact metrics. Read-only -- applying a recommendation means using the specific write tool for the change it suggests.
- `get_negative_keyword_candidates`: The read half of the search-term-review workflow. Filters `get_search_terms`-style data down to terms with clicks/spend and zero conversions (the standard PPC triage heuristic), sorted by wasted spend. A heuristic for a human to review, not an automatic verdict -- pass the ones you approve to `add_negative_keywords`.

### Write tools

- `pause`, `enable`: Pauses/enables a campaign, ad group, ad, or keyword.
- `update_budget`: Updates a campaign's daily budget.
- `update_campaign`: Updates a campaign's name, status, linked budget, and/or bidding strategy.
- `create_campaign`: Creates a new Search campaign with its own budget, atomically (via temporary resource names). Defaults to Manual CPC bidding; also supports Target CPA, Target Spend, Maximize Conversions, Maximize Conversion Value, and Target ROAS -- the strategies that make sense for a Search campaign, out of the API's full set of 16 (most of the rest are Display/Video/Shopping-only). The Smart Bidding strategies (Target CPA/ROAS, Maximize Conversions/Value) require conversion tracking to be set up on the account -- Google rejects them with "Conversion tracking is not enabled" otherwise, which is a real account-eligibility restriction, not a bug in this tool. The Target-style strategies specifically also need the campaign itself to already have conversion history -- a brand-new campaign can't set a Target CPA/ROAS/Spend on day one, regardless of account access level.
- `create_ad_group`, `update_ad_group`: Creates an ad group, or updates its name/status/default CPC bid.
- `create_responsive_search_ad`: Creates a Responsive Search Ad. Ad content (headlines, descriptions, final URLs, name) is entirely immutable after creation -- confirmed against the live API -- so there is no `update_ad` tool; to change ad copy, create a new ad and `pause` the old one (`entity_type="ad"`, the only field on an ad that stays mutable).
- `add_keywords`, `update_keyword`, `remove_keywords`: Adds positive keywords, updates a keyword's status/bid, or removes keywords (irreversible).
- `add_negative_keywords`: Adds negative keywords to an ad group or campaign.
- `create_conversion_action`, `update_conversion_action`: Creates or updates a website (Google tag) conversion action. Required before the Smart Bidding strategies (Target CPA/ROAS, Maximize Conversions/Value) above will work -- Google rejects those with "Conversion tracking is not enabled" otherwise.

### Safety tools

- `list_recent_changes`: Lists this server's own recent writes, from its local mutation journal.
- `undo_change`: Reverts a previously committed write, looked up by the id from `list_recent_changes`. Reconstructs and runs the inverse of that write through the same preview/confirm pipeline -- it does not erase or edit the original journal entry, it adds a new one.

Every write tool runs a dry-run `validate_only` call against the real API
and returns a before/after diff on its first call. It only commits when
called again with `confirm=true` -- **never set `confirm=true` without first
showing the diff to the user and getting their explicit approval.** New
campaigns, ad groups, and ads default to status `PAUSED` so nothing starts
spending without an explicit `enable` call. `create_campaign` only
supports Search campaigns; RSA headlines and descriptions are immutable
once created (Google Ads requires creating a new ad instead of editing
them); `remove_keywords` cannot be undone even with `undo_change` --
re-create with `add_keywords` instead -- so prefer `pause` wherever the
semantics allow.

### Error handling

Failures from the Google Ads API are mapped to actionable guidance where
possible (`ads_mcp/errors.py`): a missing/wrong `login-customer-id`, a
Cloud project that isn't approved for production access, the known
post-2026-09-09 developer-token-sunset authorization issue, hitting the
daily operation cap vs. a transient QPS limit, sending more than 10,000
operations in one mutate, and querying date segments outside the 37-month
retention window each get a specific explanation rather than just the raw
API message. Every error always leads with the request ID, since that's
the only thing Google Ads API support can act on.

### Resources available

- `discovery-document`: Retrieve the Google Ads API discovery document. Provides the discovery document for the latest version of the Google Ads API, which describes the API surface, including resources, methods, and schemas. Host LLMs should access this resource to understand the structure of the Google Ads API and discover available features.
- `metrics`: Retrieve information about the metrics available for reporting in the Google Ads API.
- `segments`: Retrieve information about the segments available for reporting in the Google Ads API.
- `release-notes`: Retrieve the release notes for the latest version of the Google Ads API.

### Notes

1.  The MCP Server will expose your data to the Agent or LLM that you connect to it.
1.  If you have technical issues, please use the [GitHub issue tracker](https://github.com/googleads/google-ads-mcp/issues).
1.  To help us collect usage data, you will notice an extra header has been added to your API calls: this data is used to improve the product.

## Manual setup and troubleshooting

### Configure Python

The [Quick start](#quick-start-recommended-path) above already covers the
`pipx install google-ads-mcp-write` path most people want, including a
pinned version if you need one:

```shell
pipx run --spec "google-ads-mcp-write==X.Y.Z" google-ads-mcp-write
```

If you're contributing to the project or want to run from a local
checkout instead:

```shell
git clone https://github.com/iachi/google-ads-mcp-write.git
cd google-ads-mcp-write
python3 -m venv .venv
source .venv/bin/activate
pip install -e .
google-ads-mcp-write
```

An editable install (`pip install -e .`) also gives you
`google-ads-mcp-write-setup` and `google-ads-mcp-write-get-refresh-token`
pointed at your local checkout, so you can test changes to the setup flow
itself.

### Configure Developer Token (usually not needed)

**Developer tokens were sunset on 2026-09-09.** Access level is now derived
from the Cloud project that owns your OAuth client, not from a separate
developer token. Don't request or set one unless you know your specific
setup still requires it.

We recommend applying for **Basic access** (15,000 operations/day, now
auto-reviewed within minutes) on your Cloud project rather than relying on
Explorer access (2,880 ops/day) -- an agent session burns through
Explorer's quota quickly, though Explorer is enough to get started and
already allows production reads and writes. See [Access levels and
permissible
use](https://developers.google.com/google-ads/api/docs/api-policy/access-levels).

**Known upstream issue:** a Cloud project still on the Free Trial, or with
suspended/disabled billing, gets rejected for Basic or Explorer access even
after brand verification succeeds. If your access application is denied for
no clear reason, check your project's billing status first -- enable
billing (a paid account, not just a valid payment method on a trial) and
re-apply.

If your setup still requires a legacy developer token, follow the instructions for [Obtaining a Developer Token](https://developers.google.com/google-ads/api/docs/get-started/dev-token).

Your developer token must have at least [Explorer access](https://developers.google.com/google-ads/api/docs/get-started/dev-token#access-levels) to query production accounts. New tokens may be automatically upgraded to Explorer access; if not, you can apply through the API Center. See the [access levels documentation](https://developers.google.com/google-ads/api/docs/get-started/dev-token#access-levels) for details.

If you see the error *"The developer token is only approved for use with test
accounts"*, your token does not yet have access to production accounts. See the
[access levels documentation](https://developers.google.com/google-ads/api/docs/access-levels)
for how to request the access level you need.

### Enable APIs in your project

[Follow the instructions](https://support.google.com/googleapi/answer/6158841)
to enable the following APIs in your Google Cloud project:

* [Google Ads API](https://console.cloud.google.com/apis/library/googleads.googleapis.com)

### Configure Credentials
#### Option 1: Standalone OAuth client + refresh token (what the setup command does)

This is the most common path for running this server locally over stdio
against your own account, and needs no `gcloud` setup. Create an OAuth 2.0
client ID (Desktop app type) in the Google Cloud project that has the Google
Ads API enabled, then run `google-ads-mcp-write-get-refresh-token` yourself
to obtain a refresh token for the `https://www.googleapis.com/auth/adwords`
scope -- it opens your own browser for you to sign in and grant access with
your own Google account, and prints the values below:

```shell
google-ads-mcp-write-get-refresh-token \
  --client-id YOUR_CLIENT_ID \
  --client-secret YOUR_CLIENT_SECRET
```

Then set:

- `GOOGLE_ADS_CLIENT_ID`: The OAuth 2.0 Client ID from that Cloud project.
- `GOOGLE_ADS_CLIENT_SECRET`: The OAuth 2.0 Client Secret from that Cloud project.
- `GOOGLE_ADS_REFRESH_TOKEN`: The refresh token obtained from that client's consent flow.

These are checked before falling back to Application Default Credentials, so
you don't need to touch `utils.py` or run `gcloud auth application-default
login`. Note that **developer tokens were sunset on 2026-09-09** -- do not
set `GOOGLE_ADS_DEVELOPER_TOKEN` unless your setup specifically still
requires one; access level now derives from the Cloud project that owns the
OAuth client.

#### Option 2: Application Default Credentials

Configure your [Application Default Credentials
(ADC)](https://cloud.google.com/docs/authentication/provide-credentials-adc).
Make sure the credentials are for a user with access to your Google Ads
accounts or properties.

Credentials must include the Google Ads API scope:

```
https://www.googleapis.com/auth/adwords
```

Check out
[Manage OAuth Clients](https://support.google.com/cloud/answer/15549257)
for how to create an OAuth client.

Here are some sample `gcloud` commands you might find useful:

- Set up ADC using user credentials and an OAuth desktop or web client after
  downloading the client JSON to `YOUR_CLIENT_JSON_FILE`.

  ```shell
  gcloud auth application-default login \
    --scopes https://www.googleapis.com/auth/adwords,https://www.googleapis.com/auth/cloud-platform \
    --client-id-file=YOUR_CLIENT_JSON_FILE
  ```

- Set up ADC using service account impersonation.

  ```shell
  gcloud auth application-default login \
    --impersonate-service-account=SERVICE_ACCOUNT_EMAIL \
    --scopes=https://www.googleapis.com/auth/adwords,https://www.googleapis.com/auth/cloud-platform
  ```

When the `gcloud auth application-default` command completes, copy the
`PATH_TO_CREDENTIALS_JSON` file location printed to the console in the
following message. You will need this for a later step!

```
Credentials saved to file: [PATH_TO_CREDENTIALS_JSON]
```

#### Option 3: Google Ads API Python client library config

[Follow the instructions](https://developers.google.com/google-ads/api/docs/client-libs/python/)
to setup and configure the Google Ads API Python client library.

If you have already done this and have a working `google-ads.yaml`, you can reuse this file!

In the utils.py file, change get_googleads_client() to use the load_from_storage() method.

### Configure your MCP client

#### Antigravity / Antigravity IDE

1.  Install [Antigravity](https://antigravity.google/product/antigravity-cli)
    or Antigravity IDE.

1.  Configure your server. Refer to the docs at [https://antigravity.google/docs/mcp](https://antigravity.google/docs/mcp) for details on setting up MCP servers.

- Option 1: the standalone OAuth client + refresh token method (recommended)

    ```json
    {
      "mcpServers": {
        "google-ads-mcp-write": {
          "command": "/absolute/path/to/google-ads-mcp-write/.venv/bin/google-ads-mcp-write",
          "env": {
            "GOOGLE_ADS_CLIENT_ID": "YOUR_OAUTH_CLIENT_ID",
            "GOOGLE_ADS_CLIENT_SECRET": "YOUR_OAUTH_CLIENT_SECRET",
            "GOOGLE_ADS_REFRESH_TOKEN": "YOUR_REFRESH_TOKEN"
          }
        }
      }
    }
    ```

- Option 2: the Application Default Credentials method

    This remains a supported alternative, but it provides less credential
    isolation than the standalone OAuth method above. The MCP client
    starts the server and its configuration contains the ADC file path.

    Replace `PATH_TO_CREDENTIALS_JSON` with the path you copied in the
    previous step. We also recommend adding a `GOOGLE_CLOUD_PROJECT`
    attribute to the `env` object -- replace `YOUR_PROJECT_ID` with the
    [project ID](https://support.google.com/googleapi/answer/7014113) of
    your Google Cloud project.

    ```json
    {
      "mcpServers": {
        "google-ads-mcp-write": {
          "command": "/absolute/path/to/google-ads-mcp-write/.venv/bin/google-ads-mcp-write",
          "env": {
            "GOOGLE_APPLICATION_CREDENTIALS": "PATH_TO_CREDENTIALS_JSON",
            "GOOGLE_PROJECT_ID": "YOUR_PROJECT_ID"
          }
        }
      }
    }
    ```

- Option 3: the Python client library method

    ```json
    {
      "mcpServers": {
        "google-ads-mcp-write": {
          "command": "/absolute/path/to/google-ads-mcp-write/.venv/bin/google-ads-mcp-write",
          "env": {
            "GOOGLE_PROJECT_ID": "YOUR_PROJECT_ID"
          }
        }
      }
    }
    ```

#### Login Customer Id

If your access to the customer account is through a manager account, you will
need to add the customer ID of the manager account to the settings file.

See [here](https://developers.google.com/google-ads/api/docs/concepts/call-structure#cid) for details.

The final file will look like this:

  ```json
  {
    "mcpServers": {
      "google-ads-mcp-write": {
        "command": "/absolute/path/to/google-ads-mcp-write/.venv/bin/google-ads-mcp-write",
        "env": {
          "GOOGLE_ADS_CLIENT_ID": "YOUR_OAUTH_CLIENT_ID",
          "GOOGLE_ADS_CLIENT_SECRET": "YOUR_OAUTH_CLIENT_SECRET",
          "GOOGLE_ADS_REFRESH_TOKEN": "YOUR_REFRESH_TOKEN",
          "GOOGLE_ADS_LOGIN_CUSTOMER_ID": "YOUR_MANAGER_CUSTOMER_ID"
        }
      }
    }
  }
  ```

#### Other MCP clients

The `mcpServers` block format is the same across all MCP clients. Add the configuration shown above to the appropriate settings file for your client (e.g., `~/.claude/settings.json` for Claude Code, `.cursor/mcp.json` for Cursor, `.vscode/mcp.json` for VS Code with Copilot).

### Configuring and Namespacing Tools

The Google Ads MCP server uses the `tools_config.yaml` to let you selectively enable or disable individual tools or tool categories (namespaces) and customize their namespace prefixes.

A default `tools_config.yaml` with all tools enabled is bundled with the package, so the server works out of the box with no extra setup. To customize your installation, the server resolves the configuration in the following order:

1. An explicit path set via the `GOOGLE_ADS_MCP_TOOLS_CONFIG` environment variable.
2. A `tools_config.yaml` file in the current working directory.
3. The default `tools_config.yaml` bundled with the package.

If an explicitly requested configuration file (via the environment variable) is missing, or any resolved file is invalid, the server raises an error and fails to start.

#### Configuration Example:
```yaml
namespaces:
  # Option 1: Enable category 'customers' with default prefix -> "customers_list_accessible_customers"
  customers: true

  # Option 2: Enable category 'search' with a custom prefix -> "query_search"
  search: "query"

  # Option 3: Fine-grained control over tools in a category
  metadata:
    enabled: true
    prefix: "metadata"
    enabled_tools:
      - get_resource_metadata: true
```

## Advanced: hosting and other MCP clients

Most users should stop at [Quick start](#quick-start-recommended-path)
above -- this section is for running the server as a shared local
endpoint across several of your own clients, or self-hosting for a team.
Re-read [Credentials and hosting policy](#credentials-and-hosting-policy)
first: this still has to run under your own access and can't become a
shared multi-tenant service for other people.

### Local WSL and Podman deployment

This deployment has been tested with rootless Podman in WSL and exposes a single
Streamable HTTP endpoint at `http://localhost:8080/mcp`. Build the image from
the repository inside WSL:

```shell
podman build --tag localhost/google-ads-mcp:latest --file Dockerfile .
```

Keep server credentials out of MCP client configuration. The tested Quadlet
loads Google Ads and OAuth settings from a private host-side file through
`EnvironmentFile=` and uses a separate named volume for persistent encrypted
OAuth state:

```ini
[Container]
Image=localhost/google-ads-mcp:latest
PublishPort=127.0.0.1:8080:8080
EnvironmentFile=/absolute/host/path/google-ads-mcp.env
Volume=google-ads-mcp-oauth.volume:/var/lib/google-ads-mcp:rw
ReadOnly=true
NoNewPrivileges=true
DropCapability=all
```

The environment file and the OAuth-state volume serve different purposes: the
volume does not contain the `.env` file. Keep the environment file outside the
repository, restrict it to the service owner, and never commit it. Antigravity
and Codex then need only the MCP endpoint and their own OAuth authorization;
they do not need the server's Google Ads developer token, OAuth client secret,
or signing and storage keys. Publish port 8080 only on the loopback interface
when the server is intended for local agents.

The endpoint deliberately keeps stateful Streamable HTTP enabled. It supports
legacy MCP 2025 clients that use `Mcp-Session-Id` and GET SSE as well as MCP
2026 clients that use sessionless POST requests and `subscriptions/listen`.
Do not enable FastMCP's `stateless_http` option on this shared endpoint; doing
so removes the legacy GET channel.

The server runs on FastMCP 4 (`fastmcp>=4.0.3`) paired with `mcp[cli]==2.0.0`. The Docker build also applies a version-guarded
OAuth metadata workaround for Codex CLI 0.146. It stops advertising the RFC
9207 authorization-response `iss` parameter as mandatory while FastMCP still
includes it in redirects. The build fails if the expected FastMCP version or
patch location changes, so upgrades require explicit interoperability tests.

For Codex, configure and authenticate the server as described in the
[official Codex MCP documentation](https://developers.openai.com/codex/mcp/):

```shell
codex mcp add google_ads --url http://localhost:8080/mcp
codex mcp login google_ads
```

For Antigravity, configure the same URL as `serverUrl` in its MCP configuration.
This key is required for Streamable HTTP in Antigravity 2.8.1 and Antigravity
IDE 2.5.5; `httpUrl` is not accepted by those versions. After authentication,
both clients should list these namespaced tools:

- `customers_list_accessible_customers`
- `metadata_get_resource_metadata`
- `search_search`

### Using FastMCP OAuth Proxy

The server supports FastMCP's [OAuth proxy](https://gofastmcp.com/servers/auth/oauth-proxy) feature for dynamic user authentication. This is useful when running the server as a web service for your own multiple clients.

To enable it, set the following environment variables:

- `GOOGLE_ADS_MCP_OAUTH_CLIENT_ID`: Your Google Cloud OAuth 2.0 Client ID.
- `GOOGLE_ADS_MCP_OAUTH_CLIENT_SECRET`: Your Google Cloud OAuth 2.0 Client Secret.
- `GOOGLE_ADS_MCP_BASE_URL`: (Optional) The base URL where the server is accessible (defaults to `http://localhost:8080`).
- `GOOGLE_ADS_MCP_JWT_SIGNING_KEY`: (Optional) Secret key used to sign FastMCP JWT tokens across multiple server instances or deployments.
- `GOOGLE_ADS_MCP_STORAGE_TYPE`: (Optional) Storage backend for OAuth state (`filetree`, `redis`, `firestore`, or `memory`).
- `GOOGLE_ADS_MCP_STORAGE_PATH`: (Optional) Directory path for `filetree` persistent storage.
- `GOOGLE_ADS_MCP_STORAGE_REDIS_URL`: (Optional) Redis URL for `redis` persistent storage.
- `GOOGLE_ADS_MCP_STORAGE_FIRESTORE_PROJECT`: (Optional) Google Cloud project for `firestore` persistent storage. Defaults to the project inferred from Application Default Credentials. Setting it selects the `firestore` backend even if `GOOGLE_ADS_MCP_STORAGE_TYPE` is unset.
- `GOOGLE_ADS_MCP_STORAGE_FIRESTORE_DATABASE`: (Optional) Firestore database name for `firestore` persistent storage. Defaults to `(default)`.
- `GOOGLE_ADS_MCP_STORAGE_ENCRYPTION_KEY`: (Optional) Encryption key for stored OAuth tokens.
- `GOOGLE_ADS_MCP_STORAGE_DISABLE_ENCRYPTION`: (Optional) Set to `true` to disable token encryption.

The `redis` and `firestore` backends need their storage library installed
alongside the server: `pip install py-key-value-aio[redis]` and
`pip install google-ads-mcp[firestore]` respectively.

Once this is enabled, you can authenticate to the API through your MCP client.

When these variables are set, the server automatically switches to the
`streamable-http` transport instead of `stdio`.

You will need to run the server as a separate process and configure your MCP
client to connect to the Streamable HTTP endpoint (for example,
`http://localhost:8080/mcp`), e.g.:

```json
{
  "mcpServers": {
    "google-ads-mcp": {
      "serverUrl": "http://localhost:8080/mcp"
    }
  }
}
```

### Deployment to Google Cloud Platform

Instead of hosting this MCP server locally, you can host it on Google Cloud Run or on any other cloud-based infrastructure, for your own use across your own clients. This only supports authentication with an OAuth Client ID and Client Secret pair through the OAuth proxy above.

#### Prerequisites

1.  A Google Cloud project.
2.  The `gcloud` CLI installed, authenticated, and active project set.
    ```shell
    gcloud config set project YOUR_PROJECT_ID
    ```

#### Step 1: Build and Push Docker Image

You can use Cloud Build to build and push the image to Artifact Registry without needing Docker installed locally.

1.  Create a repository in Artifact Registry:
    ```shell
    gcloud artifacts repositories create mcp-servers --repository-format=docker --location=us-central1
    ```
2.  Build and submit the image:
    ```shell
    gcloud builds submit --tag us-central1-docker.pkg.dev/YOUR_PROJECT_ID/mcp-servers/google-ads-mcp:latest .
    ```
    Replace `YOUR_PROJECT_ID` with your Google Cloud project ID.

#### Step 2: Deploy to Google Cloud Run

Make sure to set the required environment variables:

- `GOOGLE_PROJECT_ID`: Your Google Cloud project ID.
- `GOOGLE_ADS_MCP_OAUTH_CLIENT_ID`: The OAuth Client ID you want the MCP server to use.
- `GOOGLE_ADS_MCP_OAUTH_CLIENT_SECRET`: The OAuth Client secret you want the MCP server to use.
- `GOOGLE_ADS_MCP_BASE_URL`: The base URL where your MCP server is accessible: this will be automatically assigned by Google Cloud Run after your first deployment. You can update the environment variables after deployment.
- `GOOGLE_ADS_MCP_JWT_SIGNING_KEY`: (Recommended for production) Persistent JWT signing key across Cloud Run instances.
- `GOOGLE_ADS_MCP_STORAGE_TYPE`: (Recommended for production) Storage backend to persist OAuth tokens across instances. Set it to `firestore` to use Firestore through Application Default Credentials, which needs no VPC connector, or to `redis` along with `GOOGLE_ADS_MCP_STORAGE_REDIS_URL`.

  Using `firestore` requires three things: build the image with the extra
  installed (change the Dockerfile to `uv pip install --system .[firestore]`),
  create a Firestore database in the project, since one is not provisioned
  automatically, and grant the Cloud Run service account `roles/datastore.user`.
  Note that entries are not expired automatically: the store filters expired
  entries on read but never deletes them, and `expires_at` is written as a
  string, so a Firestore TTL policy cannot collect them either. Plan on a
  periodic cleanup job for long-running deployments. Redis expires entries on
  its own.
- `FASTMCP_HOST`: Set this to `0.0.0.0` to allow FastMCP to accept connections from all IP addresses.
- `GOOGLE_ADS_LOGIN_CUSTOMER_ID`: Required if your access to the customer account is through a manager account. Set it to the customer ID of the manager account. See [Login Customer Id](#login-customer-id) above for details.

```shell
gcloud run deploy google-ads-mcp \
  --image us-central1-docker.pkg.dev/YOUR_PROJECT_ID/mcp-servers/google-ads-mcp:latest \
  --platform managed \
  --region us-central1 \
  --allow-unauthenticated \
  --set-env-vars="GOOGLE_PROJECT_ID=YOUR_PROJECT_ID,GOOGLE_ADS_MCP_OAUTH_CLIENT_ID=YOUR_CLIENT_ID,GOOGLE_ADS_MCP_OAUTH_CLIENT_SECRET=YOUR_CLIENT_SECRET,GOOGLE_ADS_MCP_BASE_URL=YOUR_BASE_URL,GOOGLE_ADS_MCP_JWT_SIGNING_KEY=YOUR_JWT_SIGNING_KEY,GOOGLE_ADS_MCP_STORAGE_TYPE=firestore,FASTMCP_HOST=0.0.0.0"
```

#### Step 3: Configure MCP Client

Once deployed, update your MCP client configuration (refer to the docs at [https://antigravity.google/docs/mcp](https://antigravity.google/docs/mcp)) to use the Cloud Run URL.

```json
{
  "mcpServers": {
    "google-ads-mcp": {
      "httpUrl": "https://your-cloud-run-url.a.run.app/mcp"
    }
  }
}
```

## Contributing

Contributions welcome! See the [Contributing Guide](CONTRIBUTING.md).
Project maintainers can find the Trusted Publishing and release procedure in
the [release guide](docs/releasing.md).
