chatgpt-ads-mcp
An MCP server plus Claude skills for the OpenAI Ads Advertiser API (ChatGPT Ads). Python, uv, Docker.
Built because the existing servers miss three things that cost money in practice: they do not retry failed requests, they do not bound their responses, and they do not know the quirks of the location dataset.
What this does differently
Retries on 409. The API sporadically answers
409 "Ad Account is missing details." with no relation to the request. In a
probe on 2026-09-04 that hit 8 of 32 calls; with backoff all 32 succeeded.
Other servers surface this as a failure when nothing is wrong.
Response budgeting. Insights return up to 2000 rows. Serialised verbatim that fills the context window before the model can evaluate anything. This server trims the longest lists and records what it trimmed and how to fetch the rest.
Location lookup that works. Three pitfalls, all verified live:
| Input | Results | Reason |
|---|---|---|
Muenchen |
0 | umlauts are mandatory |
München |
78 | |
Munich |
0 with country_code: DE |
exonyms do not resolve |
a German Landkreis |
0 | districts are not a location type |
The server rewrites ASCII transliterations, maps English exonyms, and says so when a query names something that is not a location type.
Postcode targeting. The documentation lists country, region and dma.
There is a fourth type:
{"id": "10015849", "type": "postal_code",
"canonical_name": "10115, Berlin, Germany", "country_code": "DE"}
Conversely, dma never appeared for Germany in any query.
Micros with an explanation. Two expensive traps:
- CPM:
max_bid_microsis the price per single impression, not per 1000. A 60.00 CPM is60000, not60000000. - oCPC:
max_bid_microsis a CPA bid, yet billing happens per click.
explain_micros converts a value and states what it means.
Safety
- Every create tool forces
status="paused"at the type level. A newly created campaign cannot spend until it is explicitly activated. - Budgets above the ceiling require
confirm_budget=True. archiveis irreversible and requiresconfirm_archive=True.OPENAI_ADS_MCP_READONLY=1registers no write tools at all.- The API key is never logged and never appears in error messages.
Install
uv sync --group dev
uv run pytest
PyPI: chatgpt-ads-mcp
Configuration
Copy .env.example to .env:
OPENAI_ADS_API_KEY=sk-... # from https://ads.openai.com → Settings
OPENAI_ADS_BUDGET_CEILING=500 # confirmation threshold
OPENAI_ADS_CURRENCY=EUR
OPENAI_ADS_MCP_READONLY=0
One key covers exactly one ad account; there is no account_id parameter.
Add it to Claude Code
One command, no clone and no install — uvx fetches the package on first run:
claude mcp add chatgpt-ads -e OPENAI_ADS_API_KEY=sk-... -- uvx chatgpt-ads-mcp
Read-only, if you want to look before you touch anything:
claude mcp add chatgpt-ads \
-e OPENAI_ADS_API_KEY=sk-... \
-e OPENAI_ADS_MCP_READONLY=1 \
-- uvx chatgpt-ads-mcp
Verify with claude mcp list, then ask the assistant to call get_account.
Other MCP clients
Any client that speaks stdio takes the same command:
{
"mcpServers": {
"chatgpt-ads": {
"command": "uvx",
"args": ["chatgpt-ads-mcp"],
"env": { "OPENAI_ADS_API_KEY": "sk-..." }
}
}
}
To run from a checkout instead, use "command": "uv" with
"args": ["run", "--directory", "/path/to/openai-ads-tools", "chatgpt-ads-mcp"].
Docker
docker build -t openai-ads-mcp .
docker run --rm -i --env-file .env openai-ads-mcp
Tools
Read: get_account, list_accounts, check_feature_access, search_geo,
list_campaigns, get_campaign, list_ad_groups, get_ad_group, list_ads,
get_ad, get_insights, list_audiences, list_conversion_setup,
explain_micros
Write: create_campaign, create_ad_group, create_ad, upload_creative,
set_account_brand, preview_ad, set_campaign_state, set_ad_group_state,
set_ad_state, set_negative_keywords
check_feature_access is the quickest way in: it reports which features the
account has. This API answers 403, and sometimes 404, when a feature is gated —
not only when something is missing.
Skills
skills/ holds Claude skills covering the judgement the API does not:
| Skill | Purpose |
|---|---|
ads |
Orchestrator, shared reference files |
ads-campaign |
Clarify, decide, create |
ads-creative |
Copy, character economy, validation |
ads-hints |
Topic clusters, negative keywords |
ads-geo |
Postcode sets instead of a radius |
ads-audit |
Reporting with a data sufficiency gate |
The three reference files under skills/ads/references/ are the substance:
platform-facts.md (only what is sourced), misinformation.md (what is
falsely claimed, with corrections), benchmarks.md (sufficiency thresholds and
how to derive a tolerable CPA).
Copy validator
uv run python scripts/validate_creative.py \
--title "Prototype in 4 weeks" --body "..." --brand "YourBrand"
Counts grapheme clusters rather than code points, and checks the rules language models actually break: dashes, rules of three, filler verbs, invisible characters. The style rules target German copy, which is the harder case; the length and structural checks are language-independent.
What the platform cannot do
So nobody goes looking:
- No keyword targeting, no match types, no keyword planner. Delivery is
steered through
context_hints— free text, evaluated semantically. - No reporting per context hint. The ad group is the smallest unit of measurement. Put 2000 hints in one group and you will never learn which worked. Keep hints few per group and use more groups.
- No radius targeting. Model a catchment area as a list of postcodes.
- No personalised ads in the EEA or Switzerland.
- No published benchmarks. There is no official average CTR or reference CPC. Figures circulating as such are unsourced.
- No sandbox. Create everything paused and use
preview_ad.
Character limits per OpenAPI spec v2.3.0: title 3–50, body at most 100.
The 35 and 67 figures in circulation contradict the spec.
Status
Early. The read path and the safety model are exercised against a live account;
the write path is implemented and unit-tested but has seen limited production
use. Treat create_* as beta and check the preview before activating anything.
License
MIT. See NOTICE for patterns adopted from other projects.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file chatgpt_ads_mcp-0.1.0.tar.gz.
File metadata
- Download URL: chatgpt_ads_mcp-0.1.0.tar.gz
- Upload date:
- Size: 92.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
uv/0.11.15 {"installer":{"name":"uv","version":"0.11.15","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
876eb7a90c638ef513712c28862b05fed157e6162a3976cde6c679bb007515ee
|
|
| MD5 |
c4815cd4c7fda9b02b11dc5b6299fe06
|
|
| BLAKE2b-256 |
307b7baf60fea3f978501062cb61461f059f24be879521189a7733af3ca9165a
|
File details
Details for the file chatgpt_ads_mcp-0.1.0-py3-none-any.whl.
File metadata
- Download URL: chatgpt_ads_mcp-0.1.0-py3-none-any.whl
- Upload date:
- Size: 25.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
uv/0.11.15 {"installer":{"name":"uv","version":"0.11.15","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ee77f6b1e64ebdb087f899dbc729871b417b63d8caae8e144a086ded429324b5
|
|
| MD5 |
421f1f9666e06de043d634d1d5de11f0
|
|
| BLAKE2b-256 |
d45f8e79682e6dd1063329f904b89367ea339921a87c0f540b27ce576ec55257
|