@adcp/sdk

Official TypeScript/JavaScript client for the Ad Context Protocol (AdCP). Build distributed advertising operations that work synchronously OR asynchronously with the same code.
For AI Agents
Start with docs/llms.txt — the full protocol spec in one file (tools, types, error codes, examples). Building a server? See docs/guides/BUILD-AN-AGENT.md. Calling an AdCP agent as a buyer? Load skills/call-adcp-agent/SKILL.md — wire contract, async flow, and error-recovery priors that aren't in the type signatures. Setting up request signing? See docs/guides/SIGNING-GUIDE.md. For type signatures, use docs/TYPE-SUMMARY.md. Skip src/lib/types/*.generated.ts — they're machine-generated and will burn context.
These docs are also available in node_modules/@adcp/sdk/docs/ after install.
The Core Concept
AdCP operations are distributed and asynchronous by default. An agent might:
- Complete your request immediately (synchronous)
- Need time to process and send results via webhook (asynchronous)
- Ask for clarifications before proceeding
- Send periodic status updates as work progresses
Your code stays the same. You write handlers once, and they work for both sync completions and webhook deliveries.
Installation
npm install @adcp/sdk@adcp-3.0
npm install @adcp/sdk@adcp-3.1
Upgrading from v7? See MIGRATION-v8.md — TL;DR is three changes for most adopters; full guide covers wire shape, type shape, and SDK behavior deltas. Moving from 8.0 beta to 8.1? See docs/migration-8.0-to-8.1.md, plus the inbound webhook recipe at docs/recipes/verifying-inbound-webhooks.md.
Narrow type imports (@adcp/sdk/types/<tool>)
Adopters who only need a single AdCP tool's types can import a per-tool slice instead of the full surface. Each slice is a self-contained .d.ts covering the tool's Request / Response / Success / Error / Submitted types and every type they reference:
import type { SyncAccountsRequest } from '@adcp/sdk/types/sync-accounts';
Why bother: the full @adcp/sdk type surface is ~45,000 lines and crashes tsc at Node's default 4 GB heap under strict + skipLibCheck:false. A single per-tool slice peaks at ~50 MB. That's the difference between adopters chasing the cryptic FATAL: mark-compact Node flag and just having their build pass.
Slices use kebab-case filenames matching the schema cache (sync_accounts → @adcp/sdk/types/sync-accounts). Requires moduleResolution: "node16" / "nodenext" / "bundler" on the adopter side. A machine-readable index of available slices ships at @adcp/sdk/types/per-tool-index.json.
Large workspaces should keep the generated schema surface out of the default type-check path unless they actually need runtime Zod validators. The root @adcp/sdk export and @adcp/sdk/types do not re-export generated Zod schemas; import those schemas from @adcp/sdk/schemas so ordinary SDK imports avoid pulling the full generated schema declaration set into tsc.
| Client, server, signing, and response helpers | @adcp/sdk, @adcp/sdk/client, @adcp/sdk/server, or another focused runtime subpath |
| One tool's request/response types | @adcp/sdk/types/<tool> such as @adcp/sdk/types/sync-accounts |
| Runtime Zod schemas and tool schema maps | @adcp/sdk/schemas |
| Broad generated protocol type barrel | @adcp/sdk/types |
For application monorepos, keep skipLibCheck: true unless you are intentionally auditing SDK declarations. If a package only needs request/response types for a few tools, prefer the per-tool slices over importing generated types through the root package or the broad @adcp/sdk/types barrel.
Quick Start: Distributed Operations
import { ADCPMultiAgentClient } from '@adcp/sdk';
const client = new ADCPMultiAgentClient(
[
{
id: 'agent_x',
agent_uri: 'https://agent-x.com',
protocol: 'a2a',
},
{
id: 'agent_y',
agent_uri: 'https://agent-y.com/mcp/',
protocol: 'mcp',
},
],
{
webhookUrlTemplate: 'https://myapp.com/webhook/{task_type}/{agent_id}/{operation_id}',
onActivity: activity => {
console.log(`[${activity.type}] ${activity.task_type} - ${activity.operation_id}`);
},
handlers: {
onGetProductsStatusChange: (response, metadata) => {
console.log(`[${metadata.status}] Got products for ${metadata.operation_id}`);
if (metadata.status === 'completed') {
db.saveProducts(metadata.operation_id, response.products);
} else if (metadata.status === 'failed') {
db.markFailed(metadata.operation_id, metadata.message);
} else if (metadata.status === 'input-required') {
console.log('Needs input:', metadata.message);
}
},
},
}
);
const agent = client.agent('agent_x');
const result = await agent.getProducts({ brief: 'Coffee brands' });
if (result.status === 'completed') {
console.log('✅ Sync completion:', result.data.products.length, 'products');
}
if (result.status === 'submitted') {
console.log('⏳ Async - webhook registered at:', result.submitted?.webhookUrl);
}
Handling Clarifications (input-required)
When an agent needs more information, you can continue the conversation:
const result = await agent.getProducts({ brief: 'Coffee brands' });
if (result.status === 'input-required') {
console.log('❓ Agent needs clarification:', result.metadata.inputRequest?.question);
const refined = await agent.continueConversation('Only premium brands above $50');
if (refined.status === 'completed') {
console.log('✅ Got refined results:', refined.data.products.length);
}
}
Webhook Pattern
All webhooks (task completions AND notifications) use one endpoint with flexible URL templates.
Configure Your Webhook URL Structure
const client = new ADCPMultiAgentClient(agents, {
webhookUrlTemplate: 'https://myapp.com/webhook/{task_type}/{agent_id}/{operation_id}',
webhookUrlTemplate: 'https://myapp.com/webhook?agent={agent_id}&op={operation_id}&type={task_type}',
webhookUrlTemplate: 'https://myapp.com/api/v1/adcp/{agent_id}?operation={operation_id}',
webhookUrlTemplate: 'https://myapp.com/adcp-webhooks/{agent_id}/{task_type}/{operation_id}',
});
Single Webhook Endpoint
app.post('/webhook/:task_type/:agent_id/:operation_id', async (req, res) => {
const { task_type, agent_id, operation_id } = req.params;
const agent = client.agent(agent_id);
await agent.handleWebhook(
req.body,
task_type,
operation_id,
req.headers['x-adcp-signature'],
req.headers['x-adcp-timestamp']
);
res.json({ received: true });
});
URL Generation is Automatic
const operationId = createOperationId();
const webhookUrl = agent.getWebhookUrl('sync_creatives', operationId);
Activity Events
Get observability into everything happening:
const client = new ADCPMultiAgentClient(agents, {
onActivity: activity => {
console.log({
type: activity.type,
operation_id: activity.operation_id,
agent_id: activity.agent_id,
status: activity.status,
});
eventStream.send(activity);
},
});
Activity types:
protocol_request - Request sent to agent
protocol_response - Response received from agent
status_change - Task status changed
webhook_received - Webhook received from agent
Notifications (Agent-Initiated)
Mental Model: Notifications are operations that get set up when you create a media buy. The agent sends periodic updates (like delivery reports) to the webhook URL you configured during media buy creation.
const result = await agent.createMediaBuy({
campaign_id: 'camp_123',
budget: { amount: 10000, currency: 'USD' },
});
const client = new ADCPMultiAgentClient(agents, {
handlers: {
onMediaBuyDeliveryNotification: (notification, metadata) => {
console.log(`Report #${metadata.sequence_number}: ${metadata.notification_type}`);
db.saveDeliveryUpdate(metadata.operation_id, notification);
if (metadata.notification_type === 'final') {
db.markOperationComplete(metadata.operation_id);
}
},
},
});
Notifications use the same webhook URL pattern as regular operations:
POST https://myapp.com/webhook/media_buy_delivery/agent_x/delivery_report_agent_x_2025-10
The operation_id is lazily generated from agent + month: delivery_report_{agent_id}_{YYYY-MM}
All intermediate reports for the same agent + month → same operation_id
Type Safety
Full TypeScript support with IntelliSense:
const result = await agent.getProducts(params);
if (result.success) {
result.data.products.forEach(p => {
console.log(p.name, p.price);
});
}
handlers: {
onCreateMediaBuyStatusChange: (response, metadata) => {
if (metadata.status === 'completed') {
const buyId = (response as CreateMediaBuyResponse).media_buy_id;
}
};
}
Platform Implementors
Building a server that receives AdCP tool calls? v6 (recommended for new agents): declare a typed DecisioningPlatform per-specialism and let the framework wire idempotency, signing, capability projection, async tasks, status normalization, and lifecycle state.
import { serve } from '@adcp/sdk';
import { createAdcpServerFromPlatform, definePlatform, defineSalesCorePlatform, refAccountId } from '@adcp/sdk/server';
const platform = definePlatform({
capabilities: {
specialisms: ['sales-non-guaranteed'] as const,
channels: ['display'] as const,
pricingModels: ['cpm'] as const,
},
accounts: {
resolve: async (ref, ctx) => {
const id = refAccountId(ref);
if (!id) return null;
return db.findAccount(id, ctx);
},
},
sales: defineSalesCorePlatform({
getProducts: async (req, ctx) => ({ products: catalog.search(req) }),
createMediaBuy: async (req, ctx) => ({
media_buy_id: 'mb_1',
status: 'pending_creatives',
confirmed_at: new Date().toISOString(),
packages: [],
}),
updateMediaBuy: async (id, patch, ctx) => ({ media_buy_id: id, status: 'active' }),
getMediaBuyDelivery: async (req, ctx) => ({
currency: 'USD',
reporting_period: { start: '2026-05-01T00:00:00Z', end: '2026-05-31T23:59:59Z' },
media_buy_deliveries: [],
}),
getMediaBuys: async (req, ctx) => ({ media_buys: [] }),
}),
});
serve(() => createAdcpServerFromPlatform(platform, { name: 'My Publisher', version: '1.0.0' }));
RequiredPlatformsFor<S> enforces specialism claims at compile time — claim 'sales-non-guaranteed' and the typechecker requires SalesCorePlatform & SalesIngestionPlatform on sales. creative-template and creative-generative claims both map to CreativeBuilderPlatform; creative-ad-server is its own archetype with listCreatives + getCreativeDelivery.
6.7 helpers worth knowing about:
definePlatform / defineSalesCorePlatform / defineSalesIngestionPlatform / sibling define<X>Platform factories — drop req: unknown casts on inline platform objects.
composeMethod(inner, { before, after }) — typed before/after wrappers around any platform method (caching, enrichment under ext.*, typed-error guards). Pre-built accounts.resolve guards: requireAccountMatch, requireAdvertiserMatch, requireOrgScope.
- Typed errors:
AuthMissingError, AuthInvalidError, PermissionDeniedError, RateLimitedError, ServiceUnavailableError, GovernanceDeniedError, IdempotencyConflictError, plus the not-found family. AuthRequiredError remains as a deprecated AUTH_REQUIRED compatibility wrapper. Throw these instead of new AdcpError(code, ...).
BuyerAgentRegistry — durable buyer-agent identity surface threaded through ctx.agent to every AccountStore method. See docs/migration-buyer-agent-registry.md.
- Three reference
AccountStore shapes: InMemoryImplicitAccountStore (Shape A — buyer-driven sync_accounts), createOAuthPassthroughResolver (Shape B — vendor OAuth + /me/adaccounts), createRosterAccountStore (Shape C — publisher-curated roster).
- Multi-tenant:
createTenantRegistry({...}) for host-routed (one server per tenant) or createTenantStore({...}) for account-routed (one server, per-entry tenant gate built in, fail-closed when auth principal can't be resolved).
createMediaBuyStore — opt-in targeting_overlay echo on get_media_buys for sellers claiming property-lists / collection-lists.
MEDIA_BUY_TRANSITIONS / assertMediaBuyTransition (and the creative pair) — canonical lifecycle graphs.
Worked reference adapters live in examples/hello_* — pick the one whose specialism matches yours and fork.
v5 lower-level API (still fully supported as the substrate the v6 path calls into):
import type { CreateMediaBuyRequest, CreateMediaBuyResponse } from '@adcp/sdk';
import { CreateMediaBuyRequestSchema } from '@adcp/sdk/schemas';
function handleCreateMediaBuy(rawParams: unknown): CreateMediaBuyResponse {
const request: CreateMediaBuyRequest = CreateMediaBuyRequestSchema.parse(rawParams);
}
Migration path from 6.6 → 6.7: see docs/migration-6.6-to-6.7.md (fifteen recipes, two breaking — 'implicit'-resolution platforms now actually enforce the inline-account_id refusal the docstring has long claimed (pre-6.7 it was silent-pass, so audit your callers); SalesPlatform split into SalesCorePlatform & SalesIngestionPlatform). 5.x → 6.x: docs/migration-5.x-to-6.x.md. Note: PackageRequest (creation-shaped, required fields) differs from Package (response-shaped). See the type catalog for all request types and their required fields.
Multi-Agent Operations
Execute across multiple agents simultaneously:
const client = new ADCPMultiAgentClient([agentX, agentY, agentZ]);
const results = await client.allAgents().getProducts({ brief: 'Coffee brands' });
const agentIds = client.getAgentIds();
results.forEach((result, i) => {
console.log(`${agentIds[i]}: ${result.status}`);
if (result.status === 'completed') {
console.log(` Sync: ${result.data?.products?.length} products`);
} else if (result.status === 'submitted') {
console.log(` Async: webhook to ${result.submitted?.webhookUrl}`);
}
});
Idempotency
Every mutating tool call (createMediaBuy, syncCreatives, activateSignal, etc.) auto-generates an idempotency_key (UUID v4) when the caller omits one. Internal retries reuse the key so a re-sent request returns the cached response rather than double-booking. See docs/llms.txt for the full protocol story.
const result = await client.createMediaBuy({ account, brand, start_time, end_time, packages });
result.metadata.idempotency_key;
result.metadata.replayed;
Typed errors on replay conflicts — check result.errorInstance with instanceof instead of switching on error codes:
import { IdempotencyConflictError, IdempotencyExpiredError } from '@adcp/sdk';
if (result.errorInstance instanceof IdempotencyConflictError) {
}
if (result.errorInstance instanceof IdempotencyExpiredError) {
}
BYOK (persist keys across process restarts so crash-recovery can resend the exact key):
import { useIdempotencyKey } from '@adcp/sdk';
const key = await db.getOrCreateIdempotencyKey(campaign.id);
await client.createMediaBuy({ ...params, ...useIdempotencyKey(key) });
const ttlSeconds = await client.getIdempotencyReplayTtlSeconds();
Idempotency keys are retry-pattern oracles within their TTL, so the SDK truncates them to the first 8 characters in debug logs by default. Set ADCP_LOG_IDEMPOTENCY_KEYS=1 to opt into full logging for local debugging.
Crash recovery: if your process dies mid-retry and you need to decide whether to re-send — look up the persisted key by natural key, check result.metadata.replayed, and handle IdempotencyConflictError / IdempotencyExpiredError. Worked recipe in docs/guides/idempotency-crash-recovery.md.
Security
Webhook Signature Verification
const client = new ADCPMultiAgentClient(agents, {
webhookSecret: process.env.WEBHOOK_SECRET,
});
Request Signing (RFC 9421)
AdCP 3.0 supports HTTP Message Signatures (RFC 9421) for cryptographic request authentication. A buyer signs outbound requests so the seller can verify who sent them and that nothing was tampered with. A seller signs outbound webhooks so the buyer can verify authenticity. Optional in 3.0, mandatory in 3.1+ for mutating operations.
Generate a signing key:
adcp signing generate-key --alg ed25519 --kid my-agent-2026 \
--private-out ./private.jwk --public-out ./public-jwks.json
Sign outbound requests (buyer):
import { createSigningFetch } from '@adcp/sdk/signing';
const signingFetch = createSigningFetch(fetch, {
keyid: 'my-agent-2026',
alg: 'ed25519',
privateKey: privateJwk,
});
await signingFetch('https://seller.example.com/mcp', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
Verify inbound signatures (seller):
import { createExpressVerifier, StaticJwksResolver, InMemoryReplayStore } from '@adcp/sdk/signing';
app.post(
'/mcp',
rawBodyMiddleware(),
createExpressVerifier({
capability: {
supported: true,
covers_content_digest: 'required',
required_for: ['create_media_buy'],
},
jwks: new StaticJwksResolver(buyerPublicKeys),
replayStore: new InMemoryReplayStore(),
resolveOperation: req => req.body?.method ?? 'unknown',
}),
handler
);
Full guide covering key generation, JWKS publication, brand.json setup, webhook signing, capability declaration, key rotation, and conformance testing: docs/guides/SIGNING-GUIDE.md.
Authentication
const agents = [
{
id: 'agent_x',
name: 'Agent X',
agent_uri: 'https://agent-x.com',
protocol: 'a2a',
auth_token: process.env.AGENT_X_TOKEN,
},
];
Environment Configuration
WEBHOOK_URL_TEMPLATE="https://myapp.com/webhook/{task_type}/{agent_id}/{operation_id}"
WEBHOOK_SECRET="your-webhook-secret"
ADCP_AGENTS_CONFIG='[
{
"id": "agent_x",
"name": "Agent X",
"agent_uri": "https://agent-x.com",
"protocol": "a2a",
"auth_token": "actual-token-here"
}
]'
const client = ADCPMultiAgentClient.fromEnv();
Available Tools
All AdCP tools with full type safety:
Media Buy Lifecycle:
getProducts() - Discover advertising products
listCreativeFormats() - Get supported creative formats
createMediaBuy() - Create new media buy
updateMediaBuy() - Update existing media buy
syncCreatives() - Upload/sync creative assets
listCreatives() - List creative assets
getMediaBuyDelivery() - Get delivery performance
Audience & Targeting:
getSignals() - Get audience signals
activateSignal() - Activate audience signals
providePerformanceFeedback() - Send performance feedback
Protocol:
getAdcpCapabilities() - Get agent capabilities (v3)
Property Discovery (AdCP v2.2.0)
Build agent registries by discovering properties agents can sell. Works with AdCP v2.2.0's publisher-domain model.
How It Works
- Agents return publisher domains: Call
listAuthorizedProperties() → get publisher_domains[]
- Fetch property definitions: Get
https://{domain}/.well-known/adagents.json from each domain
- Index properties: Build fast lookups for "who can sell X?" and "what can agent Y sell?"
Three Key Queries
import { PropertyCrawler, getPropertyIndex } from '@adcp/sdk';
const crawler = new PropertyCrawler();
await crawler.crawlAgents([
{ agent_url: 'https://agent-x.com', protocol: 'a2a' },
{ agent_url: 'https://agent-y.com/mcp/', protocol: 'mcp' },
]);
const index = getPropertyIndex();
const matches = index.findAgentsForProperty('domain', 'cnn.com');
const auth = index.getAgentAuthorizations('https://agent-x.com');
const premiumProperties = index.findAgentsByPropertyTags(['premium', 'ctv']);
Full Example
import { PropertyCrawler, getPropertyIndex } from '@adcp/sdk';
const crawler = new PropertyCrawler();
const result = await crawler.crawlAgents([
{ agent_url: 'https://sales.cnn.com' },
{ agent_url: 'https://sales.espn.com' },
]);
console.log(`✅ ${result.successfulAgents} agents`);
console.log(`📡 ${result.totalPublisherDomains} publisher domains`);
console.log(`📦 ${result.totalProperties} properties indexed`);
const index = getPropertyIndex();
const whoCanSell = index.findAgentsForProperty('ios_bundle', 'com.cnn.app');
for (const match of whoCanSell) {
console.log(`${match.agent_url} can sell ${match.property.name}`);
}
Property Types
Supports 18 identifier types: domain, subdomain, ios_bundle, android_package, apple_app_store_id, google_play_id, roku_channel_id, podcast_rss_feed, and more.
Use Case
Build a registry service that:
- Periodically crawls agents with
PropertyCrawler
- Persists discovered properties to a database
- Exposes fast query APIs using the in-memory index patterns
- Provides web UI for browsing properties and agents
Library provides discovery logic - you add persistence layer.
Brand Hierarchy Resolution
Use RegistryClient.resolveBrandHierarchy() when rules need the ordered corporate chain for a brand domain. The chain is ordered from the resolved brand itself to the house brand, so nearest-ancestor matching can scan from left to right.
import { RegistryClient, RegistrySync } from '@adcp/sdk';
const registry = new RegistryClient({ apiKey: process.env.ADCP_REGISTRY_API_KEY });
const brand = await registry.resolveBrandHierarchy('wpp-spain.com', { ttlMs: 60_000 });
const domains = brand?.chain.map(node => node.canonical_domain) ?? [];
const both = await registry.resolveBrandHierarchies(['wpp-spain.com', 'operator.example'], { ttlMs: 60_000 });
const sync = new RegistrySync({ client: registry });
await sync.start();
const ancestors = sync.getAncestors('wpp-spain.com');
ResolvedBrand.parent_brand is a hierarchy reference, not a portable traversal API. New registry responses use the parent brand's canonical domain when known, but older rows may still carry a portfolio-internal brand.json id. Use the hierarchy APIs instead of N+1 walking parent_brand.
RegistrySync maintains its hierarchy index from registry feed events. On a cold start before a relevant hierarchy event has been applied, call resolveBrandHierarchy() for a one-off read and treat getAncestors() as the zero-latency mirror once the feed has populated that domain.
Use buildCommunityMirrorAdagents() when publishing catalog-only AAO/community mirrors for platforms that have not adopted AdCP or published seller-authorized files yet. The helper emits authorized_agents: [] and refuses caller-supplied authorization entries, so format and placement metadata cannot be mistaken for a seller authorization claim.
import { RegistryClient, buildCommunityMirrorAdagents } from '@adcp/sdk';
const catalog = buildCommunityMirrorAdagents({
catalog_etag: 'meta-creative-formats-2026-05',
formats: [
{
format_option_id: 'meta-feed-image',
format_kind: 'image',
params: {
width: 1080,
height: 1080,
},
v1_format_ref: [
{
agent_url: 'https://creative.adcontextprotocol.org/translated/meta',
id: 'feed_image',
},
],
},
],
placements: [
{
placement_id: 'feed',
name: 'Feed',
property_tags: ['feed'],
format_options: [{ format_option_id: 'meta-feed-image' }],
},
],
placement_tags: {
feed: { name: 'Feed', description: 'Main feed placement' },
},
});
await new RegistryClient().createAdagents(catalog);
RegistryClient.createAdagents() and createCommunityMirrorAdagents() are intended for build-time generation and cache fills. Public /.well-known/adagents.json routes should serve generated JSON from static storage or an application cache rather than calling the registry on every request.
To persist an AAO/community mirror in the registry, use the keyed upsert path:
await new RegistryClient({ apiKey: process.env.ADCP_REGISTRY_API_KEY }).upsertCommunityMirrorAdagents('meta', {
catalog_etag: 'meta-creative-formats-2026-05',
formats: catalog.formats,
});
await new RegistryClient({ apiKey: process.env.ADCP_REGISTRY_API_KEY }).upsertCommunityMirrorAdagents({
platform: 'meta',
catalog_etag: 'meta-creative-formats-2026-05',
formats: catalog.formats,
});
upsertCommunityMirrorAdagents() writes to the hosted mirror lifecycle endpoint, while createCommunityMirrorAdagents() remains a side-effect-free generator helper.
Database Schema
Simple unified event log for all operations:
CREATE TABLE webhook_events (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
operation_id TEXT NOT NULL,
agent_id TEXT NOT NULL,
task_type TEXT NOT NULL,
status TEXT,
notification_type TEXT,
sequence_number INTEGER,
payload JSONB NOT NULL,
timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_events_operation ON webhook_events(operation_id);
CREATE INDEX idx_events_agent ON webhook_events(agent_id);
CREATE INDEX idx_events_timestamp ON webhook_events(timestamp DESC);
SELECT * FROM webhook_events
WHERE operation_id = 'op_123'
ORDER BY timestamp;
SELECT * FROM webhook_events
WHERE operation_id = 'delivery_report_agent_x_2025-10'
ORDER BY sequence_number;
CLI Tool
For development and testing, use the included CLI tool to interact with AdCP agents.
Quick Start with Aliases
Save agents for quick access:
npx @adcp/sdk@adcp-3.0 --save-auth test https://test-agent.adcontextprotocol.org
npx @adcp/sdk@adcp-3.0 test get_products '{"brief":"Coffee brands"}'
npx @adcp/sdk@adcp-3.0 --list-agents
Direct URL Usage
Auto-detect protocol and call directly:
npx @adcp/sdk@adcp-3.0 https://test-agent.adcontextprotocol.org get_products '{"brief":"Coffee"}'
npx @adcp/sdk@adcp-3.0 https://agent.example.com get_products '{"brief":"Coffee"}' --protocol mcp
npx @adcp/sdk@adcp-3.0 https://agent.example.com list_authorized_properties --protocol a2a
npx @adcp/sdk@adcp-3.0 https://agent.example.com
npx @adcp/sdk@adcp-3.0 https://agent.example.com create_media_buy @payload.json
npx @adcp/sdk@adcp-3.0 https://agent.example.com get_products '{"brief":"..."}' --json | jq '.products'
Authentication
Three ways to provide auth tokens (priority order):
npx @adcp/sdk@adcp-3.0 test get_products '{"brief":"..."}' --auth your-token
npx @adcp/sdk@adcp-3.0 --save-auth prod https://prod-agent.com
export ADCP_AUTH_TOKEN=your-token
npx @adcp/sdk@adcp-3.0 test get_products '{"brief":"..."}'
Agent Management
npx @adcp/sdk@adcp-3.0 --save-auth prod https://prod-agent.com mcp
npx @adcp/sdk@adcp-3.0 --list-agents
npx @adcp/sdk@adcp-3.0 --remove-agent test
npx @adcp/sdk@adcp-3.0 --show-config
Testing & Compliance
npx @adcp/sdk@adcp-3.0 test test-mcp full_sales_flow
npx @adcp/sdk@adcp-3.0 test test-mcp --list-scenarios
npx @adcp/sdk@adcp-3.0 comply test-mcp
npx @adcp/sdk@adcp-3.0 comply test-mcp --platform-type social_platform
npx @adcp/sdk@adcp-3.0 comply --list-platform-types
Protocol Auto-Detection: The CLI automatically detects whether an endpoint uses MCP or A2A by checking URL patterns and discovery endpoints. Override with --protocol mcp or --protocol a2a if needed.
Config File: Agent configurations are saved to ~/.adcp/config.json with secure file permissions (0600).
See docs/CLI.md for complete CLI documentation including webhook support for async operations.
Claude Code Plugin
Install the AdCP CLI as a Claude Code plugin to use /adcp-client:adcp directly in your AI coding assistant:
/plugin marketplace add adcontextprotocol/adcp-client
/plugin install adcp-client@adcp
Or test locally during development:
claude --plugin-dir ./path/to/adcp-client
Testing
Try the live testing UI at http://localhost:8080 when running the server:
npm start
Features:
- Configure multiple agents (test agents + your own)
- Execute ONE operation across all agents
- See live activity stream (protocol requests, webhooks, handlers)
- View sync vs async completions side-by-side
- Test different scenarios (clarifications, errors, timeouts)
Examples
Basic Operation
const result = await agent.getProducts({ brief: 'Coffee brands' });
With Clarification Handler
const result = await agent.createMediaBuy(
{ buyer_ref: 'campaign-123', account_id: 'acct-456', packages: [...] },
(context) => {
if (context.inputRequest.field === 'budget') {
return 50000;
}
return context.deferToHuman();
}
);
With Webhook for Long-Running Operations
const operationId = createOperationId();
const result = await agent.syncCreatives(
{ creatives: largeCreativeList },
null,
{
contextId: operationId,
webhookUrl: agent.getWebhookUrl('sync_creatives', operationId),
}
);
Building an Agent (Server)
The fastest way to build an AdCP agent is to point your coding tool (Claude Code, Codex, Cursor, etc.) at the right skill file:
# Seller agent (publisher, SSP, retail media)
"Read skills/build-seller-agent/SKILL.md and build me a [your platform description]"
# Signals agent (CDP, data provider)
"Read skills/build-signals-agent/SKILL.md and build me a [your data platform description]"
The skill guides domain decisions, scaffolds code, and tells you how to validate:
npx tsx agent.ts
npx @adcp/sdk@adcp-3.0 storyboard run http://localhost:3001/mcp media_buy_seller --json
Available skills:
For manual implementation, see the Build an Agent guide and examples/signals-agent.ts.
Contributing
Contributions welcome! See CONTRIBUTING.md for guidelines.
License
Apache 2.0 License - see LICENSE file for details.
Support