Call adcp agent
Wire-level invariants for any AdCP buyer call — idempotency_key replay semantics, account `oneOf` variants, async `status:'submitted'`+`task_id` polling, error recovery from `adcp_error.issues[]`. Load before any per-protocol task skill (adcp-media-buy, adcp-creative, adcp-signals, adcp-governance, adcp-si, adcp-brand) when calling an AdCP agent as a buyer.From its SKILL.md
npx -y skills add adcontextprotocol/adcp-client --skill call-adcp-agentAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 18 stars18 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
18.5 KB, as published. Nobody here has run it
Call an AdCP agent
Overview
AdCP (Ad Context Protocol) agents expose a fixed tool surface (get_products, create_media_buy, get_signals, …) over MCP or A2A. Tool names come from get_adcp_capabilities; exact request/response shapes come from get_schema(tool_name) when the agent exposes it, otherwise from the bundled JSON Schemas your SDK ships (the layout differs by SDK — see "Discovery chain" below). This skill teaches the invariants that don't live cleanly in any schema: cross-tool patterns, async flow, error recovery.
When to Use
- User wants to call a publisher / SSP / retail media network over AdCP
- Tool names like
get_products,create_media_buy,sync_creatives,get_signalsappear in the available-tools list - Agent card advertises
protocolVersion: '0.3.0'withskillslisting AdCP tool names - Not this skill: building an AdCP seller agent (see
@adcp/client/skills/build-seller-agent/and analogous SDK skills)
Discovery chain
Walk these in order on first contact:
- Agent card (A2A) or
tools/list(MCP): returns tool NAMES. Most AdCP MCP servers publish passthrough schemas intools/listby default — everything shows{type: 'object', properties: {}}. Some servers opt intoexposeToolSchemas: trueand publish shallow top-level key hints there; treat those as call hints, not as the canonical schema. get_adcp_capabilities: returns supported protocols (media_buy,signals,creative, …), AdCP major versions, feature flags. Tells you WHICH tools this agent supports, not how to call them.get_schema(tool_name)(when the agent exposes it — pending standardization in #3057, not yet universal): returns the JSON Schema for a tool's request/response. Preferred over reading bundled schemas when available.- Bundled schemas (offline, authoritative): every SDK ships the AdCP JSON Schemas locally. Path differs by SDK — spec repo source uses
dist/schemas/<version>/bundled/,@adcp/clientputs them atschemas/cache/<version>/bundled/afternpm run sync-schemas, Python and Go SDKs use their own conventions. Don't hardcode a path — let the SDK's loader find them, or ask the developer. Each schema is<protocol>/<tool>-{request,response}.jsononce you locate the bundle. The canonical source for every SDK ishttps://adcontextprotocol.org/protocol/<version>.tgz.
Non-obvious rules every buyer must follow
idempotency_key is required on every mutating tool
UUID format. The key is your retry-safety guarantee — and the most common way naive callers create duplicate media buys is by misunderstanding it:
- Same key on retry → replay. The server returns the SAME response — same
task_id, samemedia_buy_id, same shape, byte-for-byte. Use this for transport-level retries (timeout, 5xx, dropped connection). - Fresh key on retry → NEW operation. Generating a new UUID because the previous attempt failed is how you double-book. Reuse the key until you've seen a terminal response (success, error, or non-retryable error).
- Same key, different body → server-defined. Most agents return the original cached response and ignore the body change. Don't rely on it — pick a fresh key only when you genuinely want a new operation.
- For async flows, the replayed response carries the same
task_id, so polling continues against the same task instead of forking a duplicate.
Required on: create_media_buy, update_media_buy, sync_creatives, sync_audiences, sync_accounts, sync_catalogs, sync_event_sources, sync_plans, sync_governance, activate_signal, acquire_rights, log_event, report_usage, provide_performance_feedback, report_plan_outcome, create_property_list, update_property_list, delete_property_list, create_collection_list, update_collection_list, delete_collection_list, create_content_standards, update_content_standards, calibrate_content, si_initiate_session, si_send_message.
Missing the key → adcp_error.code: 'VALIDATION_ERROR' with /idempotency_key in issues.
account is a oneOf — pick ONE variant, send ONLY its fields
Probably the single most common stumble for naive LLMs. account is a discriminated union. Per AdCP 3.0, two variants on create_media_buy / update_media_buy:
// variant 0: by seller-assigned id (from sync_accounts or list_accounts)
"account": { "account_id": "seller_assigned_id" }
// variant 1: by natural key (brand + operator, optional sandbox)
"account": { "brand": { "domain": "acme.com" }, "operator": "sales.example" }
Do NOT merge required fields across variants — additionalProperties: false on each variant means {account_id, brand} fails BOTH. Pick one variant and send only its fields. Always check the specific tool's schema because other tools (e.g. sync_creatives) may accept a superset.
brand takes {domain} — not {brand_id}
"brand": { "domain": "acme.example" }
Async responses: status: 'submitted' means "queued, poll later"
A mutating tool can return one of three shapes:
// Success (sync): the work is done
{ "media_buy_id": "mb_123", "packages": [...], "confirmed_at": "..." }
// Submitted (async): the work is queued
{ "status": "submitted", "task_id": "tk_abc", "message": "Awaiting IO signature" }
// Error: don't retry without fixing
{ "errors": [{ "code": "PRODUCT_NOT_FOUND", "message": "..." }] }
When you see status: 'submitted', the work is NOT complete. Poll via tasks/get (A2A) or the MCP async task extension, using the task_id. Over A2A the AdCP task_id also rides on artifact.metadata.adcp_task_id — both work.
packages[*] on media buys
"packages": [
{ "buyer_ref": "pkg_1", "product_id": "p_from_catalog", "budget": 10000, "pricing_option_id": "po_xyz" }
]
budget is a number (not {amount, currency} — currency is implied by the pricing option). Required per package: product_id, budget, pricing_option_id. buyer_ref is optional but strongly recommended as a buyer-side correlation id across retries and reporting.
Error envelope — read issues[] to recover
Every validation failure produces:
{
"adcp_error": {
"code": "VALIDATION_ERROR",
"recovery": "correctable",
"field": "/first/offending/pointer",
"issues": [
{
"pointer": "/account",
"keyword": "oneOf",
"message": "must match exactly one schema in oneOf",
"variants": [
{ "index": 0, "required": ["account_id"], "properties": ["account_id"] },
{ "index": 1, "required": ["brand", "operator"], "properties": ["brand", "operator", "sandbox"] }
]
},
{ "pointer": "/brand/domain", "keyword": "required", "message": "must have required property 'domain'" }
]
}
}
issues[].pointer— RFC 6901 JSON Pointer to the field.issues[].keyword— AJV keyword (required,type,oneOf,anyOf,additionalProperties,format,enum).issues[].variants— when the keyword isoneOforanyOf, each entry lists one variant'srequired+ declaredproperties. Pick ONE variant, send only itsrequiredfields. This is the fastest recovery path when you didn't know the field was a union.issues[].discriminator— when an SDK ≥6.7 picks a "best surviving variant" of a const-discriminated union, this is the[{field, value}, …]pairs that variant requires. Reads as the validator's verdict on which branch you were inferred to be targeting. Example:discriminator: [{field: 'type', value: 'key_value'}]pluspointer: '/deployments/0/activation_key/key'andkeyword: 'required'means "you picked thekey_valueactivation_key variant and it requires top-levelkeyandvalue." Compound discriminators likeaudience-selector's(type, value_type)produce two-entry arrays.issues[].schemaId—$idof the rejecting schema. For tools served from the bundled tree this is usually the response root; for flat-tree tools it can land on the deeper sub-schema. Diagnostic only; the actionable lever isdiscriminator+variants+pointer.issues[].allowedValues— closed enum lists forkeyword: 'enum'issues. Picking from this list closes the case in one round.issues[].hint— SDK ≥6.8 only. One-sentence curated recipe for known shape gotchas: discriminator nesting (activation_key, VASTdelivery_type), shape mismatches (format_idobject,budgetnumber,signal_idsprovenance objects), and discriminator merging (account). When present, the hint is the most-direct fix path; read it before walking variants. Absent on the long tail — no hint just means there's no curated rule for the pattern.
Recovery order: read hint first (when present, it's the validated fix path); then discriminator (names which branch to fix); then variants (lists every option if you're not in a branch); then pointer + keyword + message for the leaf fix. Patch and resend. Three attempts should cover every field.
Minimal working examples
get_products
{
"buying_mode": "brief",
"brief": "premium CTV sports inventory for live NBA finals in major US markets"
}
Returns { cache_scope: "public" | "account", products: [{ product_id, name, description, delivery_type, pricing_options, ... }] }.
create_media_buy
{
"idempotency_key": "<uuid>",
"account": { "account_id": "seller_assigned_id" },
"brand": { "domain": "acme.example" },
"start_time": "2026-05-01T00:00:00Z",
"end_time": "2026-05-31T23:59:59Z",
"packages": [
{
"buyer_ref": "pkg_1",
"product_id": "<product_id from get_products>",
"budget": 10000,
"pricing_option_id": "<pricing_option_id from product.pricing_options>"
}
]
}
If you don't have a seller_assigned_id, use the natural-key variant instead:
"account": { "brand": { "domain": "acme.example" }, "operator": "sales.example" }.
Returns either { media_buy_id, packages: [...], confirmed_at } (sync) or { status: 'submitted', task_id, message } (async — guaranteed / IO-signed flows).
sync_creatives
{
"idempotency_key": "<uuid>",
"account": { "account_id": "seller_assigned_id" },
"creatives": [
{
"creative_id": "cr_1",
"name": "My Creative",
"format_id": { "agent_url": "https://creatives.adcontextprotocol.org", "id": "video_1920x1080" },
"assets": {}
}
]
}
Per-creative required: creative_id, name, format_id: { agent_url, id }, assets (shape depends on format_id; start with {} then fill required asset keys per format spec). Returns { creatives: [{ creative_id, action, status }] } — items may fail individually without failing the batch.
get_signals
{
"signal_spec": "female professionals 25-54 in major US metros"
}
Returns { signals: [{ signal_agent_segment_id, match_rate, pricing, ... }] }. Note: the identifier field is signal_agent_segment_id (not signal_id) — used as input to activate_signal below.
activate_signal
{
"idempotency_key": "<uuid>",
"signal_agent_segment_id": "sig_premium_ctv_sports",
"destinations": [
{ "type": "platform", "platform": "the-trade-desk" }
]
}
destinations[] is a oneOf: either {type: 'platform', platform, account?} OR {type, agent_url, account?}. Pick one shape per destination.
Transport notes
- MCP:
tools/callwith{ name: 'tool_name', arguments: {...} }. Returns{ content, structuredContent, isError? }. ReadstructuredContentfor the typed response. - A2A:
message/sendwith aDataPartof shape{ skill: 'tool_name', input: {...} }(the legacy keyparametersis also accepted). Returns an A2ATask; the typed response is attask.artifacts[0].parts[0].data.
Both transports share: idempotency, error shape, schema enforcement, and handler semantics. If a call works on one, the equivalent call works on the other.
Gotchas I keep seeing
- Merging
oneOfvariants: see the account section above. If you see threeadditionalPropertieserrors under one pointer, you merged. Drop to one variant. budgetas an object: it's a number. Currency comes from thepricing_option.brand.brand_idinstead ofbrand.domain: spec usesdomain.- Forgetting
idempotency_key: required on every mutating tool; see the list above. - Treating A2A
Task.state: 'completed'as AdCP completion: A2A task state = transport call lifecycle. AdCP-level completion is in the artifact's payload (structuredContent.statusordata.status). AcompletedA2A task can still carry asubmittedAdCP response. format_idas a string:format_idis always an object{ agent_url, id }(and sometimes{ width, height, duration_ms }for dimensions). Sending"format_id": "video_1920x1080"fails with anadditionalProperties/typeerror — pass the object.
Symptom → fix
Quick lookup before reading the full envelope. Match what you see in adcp_error.issues[*], apply the fix:
| Symptom | What it means | Fix |
|---|---|---|
keyword: 'oneOf' with variants[] | Discriminated union — you sent fields from multiple variants, or none | Pick ONE variant from variants[]. Send only its required fields. |
discriminator: [{field, value}] on a required issue | Validator inferred which branch you targeted; you missed required fields IN that branch | Read the discriminator pair, fill the missing required fields at the same level (don't nest under the discriminator field name). |
hint: field present on the issue | SDK matched a curated shape-gotcha rule | Apply the hint directly — it's the validated fix path. |
2-3 additionalProperties errors at the same pointer | You merged oneOf variants ({account_id, brand, operator, …}) | Drop to one variant. Don't keep "extra" fields "for completeness". |
keyword: 'required', pointer: '/idempotency_key' | Mutating tool, no UUID | Generate fresh UUID per logical operation. Reuse it on retries. |
keyword: 'type' or additionalProperties at /budget | Sent {amount, currency} | budget is a number. Currency is implied by pricing_option_id. |
additionalProperties at /format_id (string passed) | Sent "format_id": "video_..." | format_id is {agent_url, id} — always an object. |
keyword: 'enum' at /destinations/*/type | Made-up destination type | Use 'platform' (with platform) or 'agent' (with agent_url). |
Response carries status: 'submitted' and task_id | Async — work is queued, NOT done | Poll via tasks/get (A2A) or the MCP async task extension using task_id. |
recovery: 'transient' (rate limit, 5xx, timeout) | Server-side, retry-safe | Retry with the same idempotency_key. |
recovery: 'correctable' | Buyer-side fix | Read issues[], patch the pointers, resend. Most cases close in one attempt. |
recovery: 'terminal' (account suspended, payment required, …) | Requires human action | Don't retry. Surface to the user. |
[adcp] Warning: agent … does not expose get_adcp_capabilities (one-shot per client, @adcp/client only) | Seller didn't ship v3 discovery — routed through the v2 adapter with idempotency-TTL guarantee unknown | Branch on client.isSyntheticV2() before dispatch. Tighten retry policy (lower attempt cap, longer backoff) or fall back to natural-key recovery for these sellers. Same predicate also flags synthetic v3 caveat — see isSyntheticV2() JSDoc. |
adcp_error.code: 'AUTH_MISSING' | No credential was presented | Escalate by default. Retry only if you own a credential supply or refresh path; otherwise surface re-auth to a human. |
adcp_error.code: 'AUTH_INVALID' | Credential was presented but rejected or revoked | Do not retry blindly. Refresh once only if you own a valid refresh grant and the rejection is token-expiry-shaped; otherwise surface to a human to rotate credentials. |
adcp_error.code: 'AUTH_REQUIRED' | Legacy compatibility code; older sellers and some SDK helper paths still use it for missing or rejected credentials | Treat as auth escalation by default. Retry only if your agent owns a credential-refresh path; otherwise surface to a human. |
HTTP 401 with WWW-Authenticate header | Transport auth challenge; may be missing credentials, wrong scheme, rejected credentials, or required request signing | Follow the challenge scheme. Bearer means send/refresh an OAuth-style Authorization: Bearer ... token. Basic means configure basic auth (auth: { type: 'basic', username, password } or --auth user:pass --auth-scheme basic) — Bearer will never succeed against that gateway. Signature means enable RFC 9421 request signing with an AdCP signing key, not an Authorization retry. Saved aliases: if registered with --auth-scheme basic, the scheme persists in ~/.adcp/config.json and auto-applies on every later invocation — don't redundantly repeat the flag on each call. |
If your symptom isn't here, fall through to the next section.
If you get stuck
Priority order:
- Re-read the failure's
issues[]. The pointer list plus this skill covers 80% of cases. - Call
get_schema(tool_name)if the agent exposes it (see #3057 for the pending standard). - Read the bundled JSON Schema for
<protocol>/<tool>-request.json— see Discovery chain step 4 for path resolution. If you can't locate the SDK's schema cache, ask the developer or fall back toget_schema(). - Consult the per-protocol skill (
adcp-media-buy,adcp-creative, …) for specialism-specific patterns.
Related
- Calling an agent (docs) — human-readable narrative form of this skill
skills/adcp-media-buy/,skills/adcp-creative/,skills/adcp-signals/,skills/adcp-governance/,skills/adcp-si/,skills/adcp-brand/— per-protocol task skills (layered on top of this one)@adcp/client/skills/build-seller-agent/SKILL.md— building agents on the other side of the call- Bundled JSON Schemas — canonical for every tool, version-pinned. Path differs by SDK (see Discovery chain step 4). Pulled from the protocol tarball at
/protocol/<version>.tgz.