openai-ads-mcp
Provides tools for managing and optimizing OpenAI Ads (ChatGPT Ads) campaigns, ad groups, and ads, including account health checks, ad performance analysis with dead/underperforming detection, near-duplicate copy auditing, context hints auditing, and write operations such as pausing/archiving ads, updating ad copy, creating ad variants, and managing campaigns.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@openai-ads-mcp find dead ads in my account"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
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, httpx, and Pydantic. Managed with uv,
linted/formatted with ruff, type-checked with ty.
Quickstart
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.
Related MCP server: OpenAI Ads 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. ${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.
{
"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:
{
"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:
{
"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
envblock above — that's the standard MCP mechanism andpydantic-settingsreads it straight from the process environment. There is intentionally no--api-keyCLI flag: secrets inargvare visible to other users viapsand tend to leak into shell history and logs. Useenv, or a.envfile 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:
{
"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 |
| — (required) | Advertiser API key; identifies the single ad account. |
|
| API base URL. |
|
| When |
|
|
|
|
| Bind address for |
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 |
| Validate the key; report account currency, timezone, and |
| Walk campaign → ad group → ad once; compact tree with statuses and counts. |
| Per-ad insights + conversions over a window, with CTR/CPC/CPM and a |
| Find near-duplicate titles/bodies and length/guidance issues in a scope. |
| Flag ad groups with missing or thin |
| Escape hatch for raw insight rows at any aggregation level. |
| By-id reads, including |
Write (only when READ_ONLY=false)
Tool | What it does |
| Bulk pause (reversible) / archive (irreversible, needs |
| Single activate/pause/archive transition on a campaign, ad group, or ad. |
| Update an ad's creative copy (re-sends the full creative for you). |
| Upload a creative image and create a new ad in one step. |
| Create entities with flattened, agent-friendly params. |
| Update entities (budget/targeting/bidding/context hints). |
A sibling agent skill at 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 scripts/ci.sh # ruff check + format check + ty + pytestRequires Python 3.14 and uv.
Available Tools
18 toolsaccount_healthARead-only
Validate the API key and report the account's currency, timezone, and write mode.
Call this first. read_only tells you whether write tools are available this
session; currency and timezone apply to all spend figures and date windows.
Inputs: no named parameters. Returns: Result of validating the API key and reading account metadata.
account_overviewARead-only
Compact campaign -> ad group -> ad tree with statuses and roll-up counts.
Use this instead of trying to "list all ads": the API has no global ad list, so this
walks the hierarchy for you. Archived objects are never returned by the list endpoints
(they're only retrievable by id), so the tree shows active/paused objects only. Each
node has a derived serving flag: a child of a paused campaign/ad group stays
active but does not serve, with serving_issues naming the paused ancestor.
Inputs: include_paused. Returns: account_id, campaigns, counts
ad_performanceARead-only
Per-ad performance over an inclusive YYYY-MM-DD window, flagging weak ads.
Returns impressions/clicks/spend with derived CTR/CPC/CPM and joined conversions &
cost-per-conversion. Each row's flag is dead (enough impressions, ~no
clicks), underperforming (very low CTR), or ok. Rows are sorted worst-first.
Same-day data is partial; today is handled but may be incomplete.
Inputs: since (required), until (required), campaign_id, min_impressions, +2 more. Returns: since, until, currency, rows, +3 more
archive_adsADestructive
Archive many ads. IRREVERSIBLE and removes them from list views — pass confirm=true.
Prefer pause_ads unless you are certain. Failures are reported per id.
Inputs: ad_ids (required), confirm. Returns: action, succeeded, failed
context_hints_auditCRead-only
Flag ad groups whose context_hints are missing or thin (fewer than 3).
Inputs: campaign_id. Returns: ad_groups_audited, findings
copy_auditARead-only
Find near-identical ad copy + length issues, and return all copy in scope.
Near-identical pairs use normalized token overlap (Jaccard 0..1) — a cheap, deterministic
screen for mechanical near-duplicates (reworded/reordered text), NOT same-angle
paraphrases. For semantic "these say the same thing" dedup, reason over the returned
copies list yourself. Scope defaults to the whole account; narrow with campaign_id
or ad_group_id.
Inputs: campaign_id, ad_group_id, similarity_threshold. Returns: scope, ads_audited, near_identical, length_issues, +2 more
create_ad_groupA
Create an ad group under a campaign.
max_bid_micros is 1..100_000_000. Add 3-5 conversational context_hints
describing the questions/needs users bring (not just keywords).
Inputs: campaign_id (required), name (required), billing_event_type (required), max_bid_micros (required), +3 more. Returns: id, campaign_id, name, description, +6 more
create_ad_variantA
Create a new ad: upload the creative image, then create the ad in one step.
Provide the image as image_url (preferred) or base64 in image_file_b64.
Defaults to paused so you can review before it serves. Use a specific landing
page (target_url), not a generic homepage.
Inputs: ad_group_id (required), name (required), title (required), body (required), +4 more. Returns: id, ad_group_id, name, status, +7 more
create_campaignA
Create a campaign. bidding_type is immutable post-creation (default impressions/CPM).
Set exactly one budget — daily_spend_limit_micros OR lifetime_spend_limit_micros
(a campaign carries a single budget mode; one is required). Limits are in micros
(1_000_000 = one currency unit). country_codes are ISO codes for location targeting.
Inputs: name (required), status, daily_spend_limit_micros, lifetime_spend_limit_micros, +3 more. Returns: id, name, description, status, +8 more
get_adARead-only
Fetch one ad by id — use to diagnose non-serving ads.
Backfills the parent ad_group_id and the effective serving_issues (the API's
own undocumented codes plus parent-not-active derived from the tree), since a paused
parent campaign/ad group leaves an ad active but not serving. Both require one walk
of the account tree; an archived ad is absent from that walk, so its parents stay blank.
Inputs: ad_id (required). Returns: id, ad_group_id, name, status, +7 more
get_ad_groupARead-only
Fetch one ad group by id; backfills its parent campaign_id and serving issues.
The API omits campaign_id and (when the parent is paused) the parent-not-active
serving issue on by-id reads, so this walks the campaign→ad-group tree once to fill both.
Inputs: ad_group_id (required). Returns: id, campaign_id, name, description, +6 more
get_campaignBRead-only
Fetch one campaign by id, with serving_issues normalized to a list.
serving_issues is undocumented and returned by by-id reads only (list endpoints omit
it); it is empty when nothing blocks delivery. A campaign has no parent, so there is
nothing to derive — its issues are whatever the API reports.
Inputs: campaign_id (required). Returns: id, name, description, status, +8 more
get_insightsARead-only
Escape hatch for raw insight rows at a given aggregation level.
aggregation_level is one of ad_account/campaign/ad_group/ad.
If fields is omitted, sensible defaults for that level are used (fields must match
the level, e.g. campaign.impressions not ad.impressions). The until == today quirk is handled for you. Prefer ad_performance for the common 'find weak
ads' task.
Inputs: aggregation_level (required), since (required), until (required), fields, +1 more. Returns: aggregation_level, since, until, rows
pause_adsA
Pause many ads at once (reversible). Failures are reported per id, not fatal.
Inputs: ad_ids (required). Returns: action, succeeded, failed
set_statusADestructive
Apply a single lifecycle transition to a campaign, ad group, or ad.
Note: pausing a campaign or ad group does NOT change its children's status (they
stay 'active' but stop serving). archive is IRREVERSIBLE and removes the object
from all list views — it requires confirm=true (matching archive_ads); prefer
action="pause" unless you are certain.
Inputs: kind (required), resource_id (required), action (required), confirm. Returns: kind, id, status, review_status
update_ad_copyA
Update an ad's creative copy. Pass only the fields you want to change.
Re-sends the full creative (which the API requires on ad updates) for you. Title must be 3-50 chars, body <= 100. Aim for a headline near 16 chars and body near 32, and make variants take a distinct angle rather than rephrasing.
Inputs: ad_id (required), title, body, target_url. Returns: id, ad_group_id, name, status, +7 more
update_ad_groupC
Update an ad group — including adding/replacing conversational context_hints.
Inputs: ad_group_id (required), name, description, status, +3 more. Returns: id, campaign_id, name, description, +6 more
update_campaignB
Update a campaign. Budget and targeting are full-object replaces when provided.
Inputs: campaign_id (required), name, description, status, +3 more. Returns: id, name, description, status, +8 more
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
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.
Maintenance
Related MCP Connectors
Hosted MCP server for marketing agencies: connect Google Ads, GA4, Search Console, Meta, LinkedIn and more to Claude or ChatGPT. Pull reports and make changes via OAuth login, no setup.
Google Ads MCP server — manage campaigns, keywords, and metrics.
Meta Ads and Google Ads MCP server: 250+ tools for Claude, ChatGPT, Cursor and Codex.
Hosted Google Ads MCP server for Claude, ChatGPT and Cursor, with each action switched per account.
Related MCP Servers
- AlicenseNot gradedqualityDmaintenanceAn MCP server that exposes the ChatGPT Ads API as tools an LLM host can call.4MIT
- AlicenseBqualityDmaintenanceTyped MCP server for OpenAI Ads and ChatGPT Ads via the Advertiser API. Supports account, campaign, ad group, ad, creative, audience, insight, and conversion tools with readonly mode and guarded writes.124MIT
- FlicenseNot gradedqualityDmaintenanceMCP server for the Vibe-Marketer Agent API that enables generating ads, auditing landing pages, and running local preflight checks on ad compliance, with an autonomous agent loop to improve results.-

LoomaScale Google Adsofficial
AlicenseNot gradedqualityBmaintenanceMCP server for Google Ads. Audit spend, find wasted budget, create campaigns, monitor performance, and optimize ads directly from ChatGPT, Claude, or any MCP-compatible client.1MIT