# ads-mcp tool reference

Canonical mapping for MGID MCP skills. Source of truth: the tool list the server
advertises over MCP (`tools/list`). Skills must use ads-mcp tool names only —
never a tool name borrowed from another MGID interface.

Skills are served by the server itself as MCP prompts (`prompts/list`), so a
connected client always gets the version matching the running tool surface.

---

## Auth ID vs client ID (do not confuse)

MGID exposes two account identifiers:

| ID | Example | Where |
|---|---|---|
| **auth_id** | `200316` | `get_account` → `authId`; v1 API (`get_campaign`, `list_teasers`, `get_portfolio_statistics`) |
| **client_id** | `330838` | `get_account` → `clientId`; v2 API path `/ads/account/{clientId}/…`; optional `clientId` tool arg |

**Rules for skills:**

1. **v2 tools** (`list_campaigns`, `get_campaign_statistics`, `run_custom_report`,
   `get_selective_bidding_statistics`, `list_conversion_targets`): omit `clientId`
   when operating on the logged-in account (ads-mcp uses the token's stored
   `client_id`). Pass `clientId` only for **sub-account** access.
2. **Never pass `authId` as `clientId`** — v2 answers 401. `get_account` returns
   both, correctly labelled.
3. Plan files: store both `auth_id` and `client_id` in `scope` when known.

```json
"scope": {
  "auth_id": 200316,
  "client_id": 330838,
  "account_label": "Internovus / Plexop"
}
```

---

## Read tools (available on the read-only OAuth scope)

| Tool | Purpose |
|---|---|
| `get_account` | v1 client profile plus **both ids**: `authId` (v1) and `clientId` (v2), timezone, wallet |
| `get_portfolio_statistics` | **One row per campaign for the whole account in one call** — imps, clicks, spent, avcpc, conversions per funnel stage, revenue, EPC, profit |
| `list_campaigns` | Campaign **config** only: name, status, type, bidding, limits, conversionTracking, sourcesOptimization, targeting fields |
| `get_campaign` | Single campaign config (same fields as list) |
| `get_campaign_statistics` | **Daily rows + totals** for one campaign (spend, clicks, conversionsBuy, conversionsCostBuy, roas, ctr, …). Max 90-day range |
| `get_selective_bidding_statistics` | Per-entity stats. **`entity`**: `sources` \| `widgets` \| `providers` |
| `get_quality_factors` | Current QF multipliers. **`entity`**: `sources` \| `widgets` \| `providers` |
| `get_source_filters` | Block/allow filters for a campaign |
| `list_teasers` | Teaser list (config + status + rejection reason; **no** performance metrics) |
| `get_teaser` | Single teaser config |
| `get_teaser_statistics` | Teaser performance (requires `teaserId`; date range or dateInterval) |
| `list_conversion_targets` | Account conversion targets (postback/event/url) |
| `lookup_dictionary` | Geo, OS, browser, language, search-feed-provider pickers |
| `run_custom_report` | Flexible report via v2 `GET …/statistics/reports` — requires **customReports** grant; params flattened to PHP-style query (see `ads-mcp` `objectToQuery`) |
| `raw_api_request` | Escape hatch — avoid in skills unless a tool is missing |

### Critical API facts

1. **Portfolio reads go through `get_portfolio_statistics`.** `list_campaigns`
   has no spend or conversion metrics. Do **not** loop `get_campaign_statistics`
   over every campaign to find spend — that is one call per campaign. Use
   `get_campaign_statistics` only once a campaign is selected and you need its
   day-by-day rows (cap detection, period-over-period).

2. **Selective bidding entity choice** (from campaign config):
   - `sourcesOptimization: true` → `entity: sources`
   - `sourcesOptimization: false` → `entity: widgets`
   - Push / provider bidding → `entity: providers`

3. **Target CPA / bid** comes from `get_campaign` → `bidding.strategy` +
   `bidding.price`, or from the **user**. There is no `get_account_goals`.

4. **Date ranges** on v2 statistics tools: max **90 days**; end date = yesterday
   for complete days. `get_portfolio_statistics` uses v1 named intervals
   (`lastSeven`, `last30Days`, `thisMonth`, …) or `startDate`+`endDate`.

---

## Upstream contract traps (verified against the api2 source)

Upstream refuses a wrong value with "not a valid value" and never lists the
alternatives, so these are the shapes that cost round-trips. Each one is now also
stated in the tool description; this is the same information in one place.

### `limits` is a discriminated object

`limitType` has exactly three values and decides which cap fields exist:

| `limitType` | Cap fields | Units |
|---|---|---|
| `unlimited` | none (any caps sent are ignored) | — |
| `budget` | `dailyLimit`, `overallLimit` | account currency, float |
| `clicks` | `dailyLimit`, `overallLimit` | whole clicks, int |

```json
"limits": {"limitType": "budget", "dailyLimit": 100, "overallLimit": 1000}
```

`day`, `daily`, `perDay`, `total`, `overall` and `limited` are **not** values —
a budget cap is `budget`. Optional `splitDailyLimitEvenly` spreads the daily cap
across the hours of the day and is unavailable on push campaigns. The read side
reports `unlimited` when no cap is set, which is the upstream default, not an
error.

### `run_custom_report` takes closed enumerations

`metrics` and `dimensions` are both **required**. `dimensions` accepts at most
**3** values, and the range at most **90 days**.

- Dimensions: `month`, `week`, `day`, `hour`, `campaignId`, `campaignName`,
  `campaignType`, `teaserId`, `country`, `region`, `os`, `browser`, `deviceType`,
  `widgetId`, `source`. There is **no** `campaign` — that is `campaignId` or
  `campaignName`.
- Metrics: `adRequests`, `impressions`, `viewability`, `clicks`, `ctr`, `spent`,
  `cpm`, `cpc`, `cpcWithoutDataFee`, `conversions`, `conversionsInterest`,
  `conversionsBuy`, `conversionsDecision`, `conversionsRateInterest`,
  `conversionsRateBuy`, `conversionsRateDecision`, `conversionsCostInterest`,
  `conversionsCostBuy`, `conversionsCostDecision`, `revenue`, `profit`, `roas`,
  `epc`, `vCpm`, `vCtr`, `etr`, `actions`, `engagement`, `engagementTimeOnSite`,
  `engagementVisits`, `engagementCostPerVisit`, `engagementLandingRate`.
- `orders` may only sort by a field that is already in `metrics` or `dimensions`.
- Dates: pass `YYYY-MM-DD`; the tool expands them to the RFC3339 datetimes the
  endpoint demands. A bare date sent straight through answers 422.

### Conversion targets do not round-trip

`target.type` is a discriminator — `url`, `event` or `postback`. Every type needs
`name` and `categoryId`; `description` is optional.

A `url` target takes `conditions`: an array of **exactly one** `{type, value}`,
where `type` is `starts`, `contain` or `ends`. `list_conversion_targets` returns
that same condition as a single `urlCondition` object. **The write shape and the
read shape differ upstream**, so a target that was read cannot be resent as a
write — rebuild it. There is no delete tool: creation is one-way.

### Units differ between adjacent tools

| Field | Tool | Unit |
|---|---|---|
| `bidding.price` | `create_campaign`, `update_campaign` | dollars (`0.06`) |
| `priceOfClick` | `create_teaser`, `set_teaser_status` | US cents (`6`) |

### Response shapes to defend against

- `list_teasers` returns `[]` when empty but an object keyed by teaser id when
  populated — never assume an array.
- v1 can answer HTTP 200 with an `errors[]` body; the tool surfaces that as an
  error result reading "MGID API error (HTTP 200)", which is a real failure
  despite the status.
- `get_campaign` (v1) and `get_source_filters` (v2) can disagree about a
  campaign's filters — they read different sides. Trust whichever backs the tool
  you are about to call, and re-read after a write rather than assuming.

---

## Campaign types — check before applying any heuristic

`get_campaign` / `list_campaigns` → `type` is one of:

| `type` | Serving model | Skill applicability |
|---|---|---|
| `content` | Native content teasers | All skills apply |
| `product` | Product/e-commerce teasers | All skills apply |
| `push` | Push notifications via providers | Use `entity: providers` for selective bidding; teaser fatigue math is weaker (no widget-level placement) |
| `search_feed` | Search feed via a `searchFeedProviderId` | **Not supported.** No teasers, no source/widget entities — CTR, fatigue and source-audit rules are meaningless. Skip with an explicit note |
| `newsletter` | Newsletter placements | **Not supported.** Treat like `search_feed` until rules exist |

Every skill that computes CTR, CPA-per-source, or creative rules **must branch on
`type` first** and state plainly which campaigns it skipped and why. Silently
applying content math to a `search_feed` campaign produces confident nonsense.

---

## Write tools (require the full OAuth scope)

Not enabled in all deployments — a read-only connection hides them, and a server
with no audit-log storage refuses them. Skills run **simulation** without them;
the apply phase marks `blocked_by: ["mcp_tool_unavailable"]` when missing.

| Tool | Purpose |
|---|---|
| `create_campaign` | Create a campaign, including targeting, schedule, limits, conversion tracking |
| `update_campaign` | Partial update — name, bidding, limits, schedule, targeting, conversion tracking. `type`, `showName` and `sourcesOptimization` are **not** editable |
| `set_campaign_status` | `active` / `paused` |
| `create_teaser` | Create a teaser under a campaign (v1, own account, hosted `imageLink` only) |
| `update_teaser` | **Full replace** (v1 PUT) — fetch the teaser first and resend every field you want to keep |
| `set_teaser_status` | Block/unblock a teaser, optionally adjust `priceOfClick` |
| `create_conversion_target` | Add a url/event/postback target |
| `update_source_filters` | Block/unblock sources, widgets or providers |
| `delete_source_filters` | Remove a campaign's filters for one entity |
| `update_quality_factors` | QF changes (`entity`: sources \| widgets \| providers) |
| `delete_campaign` | Two-phase: preview → `confirmationToken` → execute |
| `delete_teaser` | Two-phase, same shape |

**Upstream rejects unknown fields** on `create_campaign` / `update_campaign` —
do not invent parameter names; the call fails rather than silently ignoring them.

There is no `send_to_moderation`, `batch-update-status`, or
`batch-update-quality-factor` tool. Moderation is submitted by creating or
editing the teaser; its verdict is read back from the teaser payload.

### Creative generation is not the agent's job

MGID has its own teaser and image generation with tuned prompts (creative
auto-recommendation strategies, text-to-image and image-to-image in the
advertiser panel). Skills must **not** write ad copy or invent creative
concepts. Until those generators are exposed on the advertiser API and wired
into ads-mcp, a creative step ends with "produce variants in MGID Campaign
Studio / Media Gallery" — not with model-written headlines.

---

## Not provided by skills

Skills must **not** assume the agent or the skill text provides:

- Plan persistence / plan storage
- Idempotent apply (`plan_id:recommendation_id`)
- Scheduled digests

A skill produces a plan file and reads it back at apply time. Calls are recorded
server-side, but no tool reads that record back.

---

## Typical call patterns

### Portfolio triage (start here for anything account-wide)

```
get_portfolio_statistics(dateInterval: lastSeven)   // 1 call, all campaigns
  → rank by spend / CPA / profit, pick the campaigns worth a closer look
list_campaigns → config for the picked ids (type, bidding, limits, sourcesOptimization)
```

### Tracking gate (per campaign)

```
get_portfolio_statistics → campaigns with spend
list_campaigns → status=active, conversionTracking, bidding
list_conversion_targets → account has targets configured
get_campaign_statistics(campaignId, dateFrom, dateTo) → daily rows when a
  campaign needs the when-did-it-stop answer
```

### Source audit (one campaign)

```
get_campaign(campaignId) → type, sourcesOptimization, bidding
get_selective_bidding_statistics(campaignId, entity, dateFrom, dateTo)
get_quality_factors(campaignId, entity)  // optional, for proposed QF from/to
get_source_filters(campaignId, entity)   // avoid duplicate recommendations
```

### Budget reallocation (portfolio)

```
get_portfolio_statistics → spend + conversions per campaign
list_campaigns → limits.dailyLimit
get_campaign_statistics(candidate ids) → daily rows for cap-binding detection
```

### Creative fatigue (one campaign)

**Primary** (customReports grant):

```
run_custom_report(current window) + run_custom_report(baseline window)
  dimensions: [teaserId]
  metrics: [clicks, spent, ctr, conversionsBuy, conversionsCostBuy]
  filters: { campaigns: [campaignId], dateRange }
  → join on teaserId for period-over-period rules (2 calls/campaign)
list_teasers(campaignId) → active count (never pause last active teaser)
get_campaign_statistics(campaignId) → campaign avg CPA/CTR for relative rules
```

v2 custom report has **no comparison mode** — do not pass `dateRangePrevious`
(it is ignored). Run two reports and join them instead.

**Fallback** (no customReports grant):

```
list_teasers(campaignId) → active teaser ids (cap 20)
get_teaser_statistics(teaserId, startDate, endDate) → current vs baseline
get_campaign_statistics(campaignId) → campaign avg CPA/CTR
```
