From adcp-client
Enforces wire-level invariants for AdCP buyer calls: idempotency_key semantics, account oneOf variants, async status:submitted+task_id polling, adcp_error recovery. Use before per-protocol AdCP skills like adcp-media-buy.
npx claudepluginhub adcontextprotocol/adcp-client --plugin adcp-clientThis skill uses the workspace's default tool permissions.
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 t...
Interact with AdCP advertising agents via MCP/A2A protocols: call tools (get_products, create_media_buy), discover capabilities, run compliance tests, query registry, manage aliases, debug responses. Requires @adcp/sdk (npm).
Automates Adrapid ad operations via Composio toolkit and Rube MCP tools like RUBE_SEARCH_TOOLS, RUBE_MANAGE_CONNECTIONS, and RUBE_MULTI_EXECUTE_TOOL. Use after connecting Adrapid.
Manages Google Ads, Meta Ads, LinkedIn Ads, and TikTok Ads campaigns via natural language. Automates keyword research, budget optimization, ROAS tracking, wasted spend analysis, and reporting.
Share bugs, ideas, or general feedback.
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.
get_products, create_media_buy, sync_creatives, get_signals appear in the available-tools listprotocolVersion: '0.3.0' with skills listing AdCP tool names@adcp/client/skills/build-seller-agent/ and analogous SDK skills)Walk these in order on first contact:
tools/list (MCP): returns tool NAMES. AdCP MCP servers no longer publish per-tool parameter schemas in tools/list — everything shows {type: 'object', properties: {}}. Don't try to infer shape from here.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.dist/schemas/<version>/bundled/, @adcp/client puts them at schemas/cache/<version>/bundled/ after npm 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}.json once you locate the bundle. The canonical source for every SDK is https://adcontextprotocol.org/protocol/<version>.tgz.idempotency_key is required on every mutating toolUUID format. The key is your retry-safety guarantee — and the most common way naive callers create duplicate media buys is by misunderstanding it:
task_id, same media_buy_id, same shape, byte-for-byte. Use this for transport-level retries (timeout, 5xx, dropped connection).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 fieldsProbably 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" }
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.
issues[] to recoverEvery 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 is oneOf or anyOf, each entry lists one variant's required + declared properties. Pick ONE variant, send only its required fields. 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'}] plus pointer: '/deployments/0/activation_key/key' and keyword: 'required' means "you picked the key_value activation_key variant and it requires top-level key and value." Compound discriminators like audience-selector's (type, value_type) produce two-entry arrays.issues[].schemaId — $id of 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 is discriminator + variants + pointer.issues[].allowedValues — closed enum lists for keyword: '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, VAST delivery_type), shape mismatches (format_id object, budget number, signal_ids provenance 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.
{
"buying_mode": "brief",
"brief": "premium CTV sports inventory for live NBA finals in major US markets"
}
Returns { products: [{ product_id, name, description, delivery_type, pricing_options, ... }] }.
{
"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).
{
"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.
{
"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.
{
"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.
tools/call with { name: 'tool_name', arguments: {...} }. Returns { content, structuredContent, isError? }. Read structuredContent for the typed response.message/send with a DataPart of shape { skill: 'tool_name', input: {...} } (the legacy key parameters is also accepted). Returns an A2A Task; the typed response is at task.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.
oneOf variants: see the account section above. If you see three additionalProperties errors under one pointer, you merged. Drop to one variant.budget as an object: it's a number. Currency comes from the pricing_option.brand.brand_id instead of brand.domain: spec uses domain.idempotency_key: required on every mutating tool; see the list above.Task.state: 'completed' as AdCP completion: A2A task state = transport call lifecycle. AdCP-level completion is in the artifact's payload (structuredContent.status or data.status). A completed A2A task can still carry a submitted AdCP response.format_id as a string: format_id is always an object { agent_url, id } (and sometimes { width, height, duration_ms } for dimensions). Sending "format_id": "video_1920x1080" fails with an additionalProperties / type error — pass the object.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. |
HTTP 401 with WWW-Authenticate header | Missing or expired credential | Add Authorization per the agent's auth spec; re-auth if applicable. |
If your symptom isn't here, fall through to the next section.
Priority order:
issues[]. The pointer list plus this skill covers 80% of cases.get_schema(tool_name) if the agent exposes it (see #3057 for the pending standard).<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 to get_schema().adcp-media-buy, adcp-creative, …) for specialism-specific patterns.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/protocol/<version>.tgz.