Skip to main content
Glama
openai-ads-mcp

openai-ads-mcp

by shinypebble

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 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:

{
  "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 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 + pytest

Requires Python 3.14 and uv.

Available Tools

18 tools
account_healthA
Read-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_overviewA
Read-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_performanceA
Read-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_adsA
Destructive

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_auditC
Read-only

Flag ad groups whose context_hints are missing or thin (fewer than 3).

Inputs: campaign_id. Returns: ad_groups_audited, findings

copy_auditA
Read-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_adA
Read-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_groupA
Read-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_campaignB
Read-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_insightsA
Read-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_statusA
Destructive

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

    A3.8/5.0

    Scored across 18 tools

    Disambiguation5/5

    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.

    Naming Consistency4/5

    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.

    Tool Count5/5

    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.

    Completeness5/5

    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

    ActivityInactive
    ResponsivenessNo recent community issues

    Related MCP Connectors

    Related MCP Servers

    • A
      license
      B
      quality
      D
      maintenance
      Typed 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.
      12
      4
      MIT
    • F
      license
      Not graded
      quality
      D
      maintenance
      MCP 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.
      -
    • A
      license
      Not graded
      quality
      B
      maintenance
      MCP 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.
      1
      MIT