openai-ads-mcp
# openai-ads-mcp
An MCP server for the **OpenAI Ads (ChatGPT Ads) Advertiser API**, built for agent-led
performance marketing. It exposes a small set of high-leverage, *useful-work* tools — find
dead ads, audit near-duplicate copy, audit ad-group context hints, and (optionally) act on the
findings — rather than a 1:1 mirror of the REST API.
Built with [FastMCP](https://gofastmcp.com), `httpx`, and Pydantic. Managed with `uv`,
linted/formatted with `ruff`, type-checked with `ty`.
## Quickstart
```bash
uv sync # create .venv and install
cp .env.example .env # then set OPENAI_ADS_API_KEY
uv run python -m openai_ads_mcp # run over stdio (default)
```
Or point an MCP client at it with `fastmcp run src/openai_ads_mcp/server.py:mcp`.
## MCP client configuration
Real agents launch the server over **stdio** and pass configuration in the `env` block of
the client's config — no `.env` file or shared working directory required. The examples below
go in `.mcp.json` (Claude Code), `claude_desktop_config.json` (Claude Desktop), or the
equivalent `mcpServers` block for any other client.
### Project-scoped (agent runs inside the checkout)
This is the form in this repo's [`.mcp.json`](.mcp.json). `${CLAUDE_PROJECT_DIR:-.}` resolves
to the project root, so `uv` finds the right environment. The key comes from the ambient shell
or a `.env` here — add it to `env` to be explicit.
```json
{
"mcpServers": {
"openai-ads": {
"type": "stdio",
"command": "uv",
"args": ["run", "--directory", "${CLAUDE_PROJECT_DIR:-.}", "python", "-m", "openai_ads_mcp"],
"env": {
"OPENAI_ADS_API_KEY": "sk-svcacct-...",
"READ_ONLY": "false"
}
}
}
}
```
### From any directory (absolute path)
Most users run the agent somewhere other than this repo. Point `--directory` at an **absolute**
path to the checkout and pass the key in `env` — this works regardless of the agent's cwd:
```json
{
"mcpServers": {
"openai-ads": {
"type": "stdio",
"command": "uv",
"args": ["run", "--directory", "/abs/path/to/openai-ads-mcp", "python", "-m", "openai_ads_mcp"],
"env": {
"OPENAI_ADS_API_KEY": "sk-svcacct-..."
}
}
}
}
```
### Installed console script
Install once with `uv tool install /abs/path/to/openai-ads-mcp` (or
`uv tool install git+<repo-url>`), then reference the `openai-ads-mcp` script directly — no
`--directory` needed:
```json
{
"mcpServers": {
"openai-ads": {
"type": "stdio",
"command": "openai-ads-mcp",
"env": {
"OPENAI_ADS_API_KEY": "sk-svcacct-...",
"READ_ONLY": "true"
}
}
}
}
```
> **Passing the API key.** Put it in the `env` block above — that's the standard MCP mechanism
> and `pydantic-settings` reads it straight from the process environment. There is intentionally
> no `--api-key` CLI flag: secrets in `argv` are visible to other users via `ps` and tend to leak
> into shell history and logs. Use `env`, or a `.env` file when the agent shares the checkout.
### HTTP transport
To run the server once and connect over HTTP instead of spawning it per-client, start it with
`MCP_TRANSPORT=http` (see the table below) and point the client at the URL:
```json
{
"mcpServers": {
"openai-ads": { "type": "http", "url": "http://127.0.0.1:8000/mcp" }
}
}
```
## Configuration
All configuration is via environment variables — set them in the client's `env` block (above),
the ambient shell, or a local `.env`:
| Variable | Default | Purpose |
| --- | --- | --- |
| `OPENAI_ADS_API_KEY` | — (required) | Advertiser API key; identifies the single ad account. |
| `OPENAI_ADS_BASE_URL` | `https://api.ads.openai.com/v1` | API base URL. |
| `READ_ONLY` | `false` | When `true`, write tools are **not registered** and cannot be called. |
| `MCP_TRANSPORT` | `stdio` | `stdio` or `http`. |
| `MCP_HOST` / `MCP_PORT` | `127.0.0.1` / `8000` | Bind address for `http` transport. |
### Read-only mode
Set `READ_ONLY=true` to run a safe, analysis-only server. The write tools are never
registered, so they don't appear in the tool list and can't be invoked — there is no way for a
client to bypass it. The default is `false` (writes enabled).
## Tools
### Read (always available)
| Tool | What it does |
| --- | --- |
| `account_health` | Validate the key; report account currency, timezone, and `read_only`. Call first. |
| `account_overview` | Walk campaign → ad group → ad once; compact tree with statuses and counts. |
| `ad_performance` | Per-ad insights + conversions over a window, with CTR/CPC/CPM and a `dead`/`underperforming`/`ok` flag. The core "find dead ads" tool. |
| `copy_audit` | Find near-duplicate titles/bodies and length/guidance issues in a scope. |
| `context_hints_audit` | Flag ad groups with missing or thin `context_hints`. |
| `get_insights` | Escape hatch for raw insight rows at any aggregation level. |
| `get_campaign` / `get_ad_group` / `get_ad` | By-id reads, including `serving_issues` (which list calls omit). |
### Write (only when `READ_ONLY=false`)
| Tool | What it does |
| --- | --- |
| `pause_ads` / `archive_ads` | Bulk pause (reversible) / archive (irreversible, needs `confirm=true`), error-isolated per id. |
| `set_status` | Single activate/pause/archive transition on a campaign, ad group, or ad. |
| `update_ad_copy` | Update an ad's creative copy (re-sends the full creative for you). |
| `create_ad_variant` | Upload a creative image and create a new ad in one step. |
| `create_campaign` / `create_ad_group` | Create entities with flattened, agent-friendly params. |
| `update_campaign` / `update_ad_group` | Update entities (budget/targeting/bidding/context hints). |
A sibling agent skill at [`skill/openai-ads-optimizer/SKILL.md`](skill/openai-ads-optimizer/SKILL.md)
teaches an agent how and when to use these tools, plus the OpenAI Ads creative guidance. The server
also serves this skill as an MCP resource (`skill://openai-ads-optimizer/SKILL.md`), so any connected
client can discover the playbook without a local copy.
## Architecture
```
tools/ → MCP layer (@mcp.tool wrappers, READ_ONLY gate)
services/ → orchestration (hierarchy walk, performance, audits, mutations)
domain/ → pure data shapes + business rules (entities, insights, thresholds, copy audit)
api/ → async httpx client (auth, retry, pagination, errors)
```
The client mirrors the API's real semantics: reads (and the conversions POST, which has read
semantics) are retried on transient failures; other writes are never retried. It also handles
the API's quirks — no global ad list, `until = today` rejection, conversions on a separate
endpoint, decimal spend vs. micros bids.
## Development
```bash
bash scripts/ci.sh # ruff check + format check + ty + pytest
```
Requires Python 3.14 and `uv`.
TDQS
Scored across 18 tools
Each tool has a clearly distinct purpose. Overlapping functionalities (e.g., pause_ads vs set_status, archive_ads vs set_status) are differentiated by scope (batch vs single) and supported object types. Detailed descriptions further clarify boundaries.
Most tool names follow a verb_noun pattern (create_, get_, update_, pause_, archive_, set_). However, three tools (account_health, account_overview, ad_performance) use noun phrases without a leading verb, creating a minor inconsistency.
18 tools cover the advertising domain comprehensively without being excessive. Each tool addresses a specific need, from account health and hierarchy overview to ad performance and copy auditing. The count is well within the typical 3-15 range but still justified.
The tool set provides full CRUD/lifecycle coverage for campaigns, ad groups, and ads (archive via set_status or dedicated tools). Additional diagnostic tools (account_health, account_overview, ad_performance, copy_audit, context_hints_audit) fill common analysis needs. No obvious gaps for the intended scope.