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:
- v2 tools (
list_campaigns,get_campaign_statistics,run_custom_report,get_selective_bidding_statistics,list_conversion_targets): omitclientIdwhen operating on the logged-in account (ads-mcp uses the token's storedclient_id). PassclientIdonly for sub-account access. - Never pass
authIdasclientId— v2 answers 401.get_accountreturns both, correctly labelled. - Plan files: store both
auth_idandclient_idinscopewhen known.
"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
-
Portfolio reads go through
get_portfolio_statistics.list_campaignshas no spend or conversion metrics. Do not loopget_campaign_statisticsover every campaign to find spend — that is one call per campaign. Useget_campaign_statisticsonly once a campaign is selected and you need its day-by-day rows (cap detection, period-over-period). -
Selective bidding entity choice (from campaign config):
sourcesOptimization: true→entity: sourcessourcesOptimization: false→entity: widgets- Push / provider bidding →
entity: providers
-
Target CPA / bid comes from
get_campaign→bidding.strategy+bidding.price, or from the user. There is noget_account_goals. -
Date ranges on v2 statistics tools: max 90 days; end date = yesterday for complete days.
get_portfolio_statisticsuses v1 named intervals (lastSeven,last30Days,thisMonth, …) orstartDate+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 |
"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 nocampaign— that iscampaignIdorcampaignName. - 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. ordersmay only sort by a field that is already inmetricsordimensions.- 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_teasersreturns[]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) andget_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