@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
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? Import request types for handler signatures and Zod schemas for validation:
import { CreateMediaBuyRequest, CreateMediaBuyResponse, CreateMediaBuyRequestSchema } from '@adcp/sdk';
function handleCreateMediaBuy(rawParams: unknown): CreateMediaBuyResponse {
const request: CreateMediaBuyRequest = CreateMediaBuyRequestSchema.parse(rawParams);
}
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.
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@latest --save-auth test https://test-agent.adcontextprotocol.org
npx @adcp/sdk@latest test get_products '{"brief":"Coffee brands"}'
npx @adcp/sdk@latest --list-agents
Direct URL Usage
Auto-detect protocol and call directly:
npx @adcp/sdk@latest https://test-agent.adcontextprotocol.org get_products '{"brief":"Coffee"}'
npx @adcp/sdk@latest https://agent.example.com get_products '{"brief":"Coffee"}' --protocol mcp
npx @adcp/sdk@latest https://agent.example.com list_authorized_properties --protocol a2a
npx @adcp/sdk@latest https://agent.example.com
npx @adcp/sdk@latest https://agent.example.com create_media_buy @payload.json
npx @adcp/sdk@latest https://agent.example.com get_products '{"brief":"..."}' --json | jq '.products'
Authentication
Three ways to provide auth tokens (priority order):
npx @adcp/sdk@latest test get_products '{"brief":"..."}' --auth your-token
npx @adcp/sdk@latest --save-auth prod https://prod-agent.com
export ADCP_AUTH_TOKEN=your-token
npx @adcp/sdk@latest test get_products '{"brief":"..."}'
Agent Management
npx @adcp/sdk@latest --save-auth prod https://prod-agent.com mcp
npx @adcp/sdk@latest --list-agents
npx @adcp/sdk@latest --remove-agent test
npx @adcp/sdk@latest --show-config
Testing & Compliance
npx @adcp/sdk@latest test test-mcp full_sales_flow
npx @adcp/sdk@latest test test-mcp --list-scenarios
npx @adcp/sdk@latest comply test-mcp
npx @adcp/sdk@latest comply test-mcp --platform-type social_platform
npx @adcp/sdk@latest 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@latest 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